This URL has Read-Only access.

Statistics
| Branch: | Tag: | Revision:

root / py / scenic / streamer.py @ d1bb10bd

History | View | Annotate | Download (34.1 kB)

1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
# 
4
# Scenic
5
# Copyright (C) 2008 Société des arts technologiques (SAT)
6
# http://www.sat.qc.ca
7
# All rights reserved.
8
#
9
# This file is free software: you can redistribute it and/or modify
10
# it under the terms of the GNU General Public License as published by
11
# the Free Software Foundation, either version 2 of the License, or
12
# (at your option) any later version.
13
#
14
# Scenic is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
# GNU General Public License for more details.
18
#
19
# You should have received a copy of the GNU General Public License
20
# along with Scenic. If not, see <http://www.gnu.org/licenses/>.
21

    
22
"""
23
Manages local streamer processes.
24
"""
25

    
26
from scenic import process
27
import subprocess
28
from scenic import sig
29
from scenic import dialogs
30
from scenic.internationalization import _
31
from scenic import logger
32
from twisted.python import procutils
33
from twisted.internet import defer
34
from twisted.internet import utils
35
import os
36

    
37
log = logger.start(name="streamer")
38

    
39
class StreamerManager(object):
40
    """
41
    Manages local streamer processes.
42
    """
43
    def __init__(self, app):
44
        self.app = app
45
        self.sender = None 
46
        self.receiver = None 
47
        self.extra_sender = None # extra sender/receiver used only if not synchronized
48
        self.extra_receiver = None
49
        
50
        self.midi_receiver = None
51
        self.midi_sender = None
52
        self.state = process.STATE_STOPPED
53
        self.state_changed_signal = sig.Signal()
54
        # for stats
55
        self.session_details = None # either None or a big dict
56
        self.rtcp_stats = None # either None or a big dict
57
        self.error_messages = None # either None or a big dict
58
        self.warnings = None # either None or a big dict
59

    
60
    def get_max_channels_in_raw(self):
61
        """
62
        Calls deferred with an int as argument
63
        @rtype: Deferred
64
        """
65
        def _cb(text, deferred):
66
            for i in text.splitlines():
67
                if "raw supports up to " in i:
68
                    ret = int(i.split()[-2])
69
            deferred.callback(ret)
70
            
71
        def _eb(reason, deferred):
72
            deferred.errback(reason)
73
            print("Error getting max channels: %s" % (reason))
74
        
75
        command_name = "milhouse"
76
        args = ['--max-channels']
77
        try:
78
            executable = procutils.which(command_name)[0] # gets the executable
79
        except IndexError:
80
            return defer.fail(RuntimeError("Could not find command %s" % (command_name)))
81
        deferred = defer.Deferred()
82
        d = utils.getProcessOutput(executable, args=args, env=os.environ, errortoo=True) # errortoo puts stderr in output
83
        d.addCallback(_cb, deferred)
84
        d.addErrback(_eb, deferred)
85
        return deferred
86

    
87
    def _gather_config_to_stream(self, addr):
88
        """
89
        Gathers all settings in a big dict.
90
        
91
        Useful for feedback to the user.
92
        """
93
        contact_name = addr
94
        contact = self.app._get_contact_by_addr(addr)
95
        if contact is not None:
96
            contact_name = contact["name"]
97
        
98
        remote_config = self.app.remote_config # FIXME: should the remote config be passed as a param to this method?
99
        send_width, send_height = self.app.config.video_capture_size.split("x")
100
        receive_width, receive_height = remote_config["video"]["capture_size"].split("x")
101
        
102
        # MIDI
103
        midi_send_enabled = self.app.config.midi_send_enabled and remote_config["midi"]["recv_enabled"]
104
        midi_recv_enabled = self.app.config.midi_recv_enabled and remote_config["midi"]["send_enabled"]
105
        midi_input_device = self.app.config.midi_input_device
106
        midi_output_device = self.app.config.midi_output_device
107
        
108
        audio_jitterbuffer = self.app.config.audio_jitterbuffer
109
        if self.app.config.audio_video_synchronized and self.app.config.video_recv_enabled:
110
            audio_jitterbuffer = self.app.config.video_jitterbuffer
111
        
112
        log.debug("remote_config: %s" % (remote_config))
113
        
114
        self.session_details = {
115
            "peer": {
116
                "address": addr,
117
                "name": contact_name,
118
                },
119
            # ----------------- send ---------------
120
            "send": {
121
                "video": {
122
                    # Decided by both:
123
                    "enabled": self.app.config.video_send_enabled and remote_config["video"]["recv_enabled"],
124
                    
125
                    # Decided locally:
126
                    "source": self.app.config.video_source,
127
                    "device": self.app.config.video_device,
128
                    "width": int(send_width), # int
129
                    "height": int(send_height), # int
130
                    "aspect-ratio": self.app.config.video_aspect_ratio,
131
                    
132
                    # Decided by remote peer:
133
                    "port": remote_config["video"]["port"], 
134
                    "bitrate": remote_config["video"]["bitrate"], 
135
                    "codec": remote_config["video"]["codec"], 
136
                },
137
                
138
                "audio": {
139
                    # Decided by both:
140
                    "enabled": self.app.config.audio_send_enabled and remote_config["audio"]["recv_enabled"],
141

    
142
                    # Decided locally:
143
                    "source": self.app.config.audio_source,
144
                    "vumeter-id": self.app.gui.audio_levels_input_socket_id,
145
                    "buffer": self.app.config.audio_input_buffer,
146
                    "jack_autoconnect": self.app.config.audio_jack_enable_autoconnect,
147

    
148
                    # Decided by remote peer:
149
                    "numchannels": remote_config["audio"]["numchannels"],
150
                    "codec": remote_config["audio"]["codec"],
151
                    "port": remote_config["audio"]["port"], 
152
                    "synchronized": remote_config["audio"]["synchronized"],
153
                }, 
154
                "midi": {
155
                    "enabled": midi_send_enabled,
156
                    "input_device": midi_input_device,
157
                    "port": remote_config["midi"]["port"]
158
                }
159
            },
160
            # -------------------- recv ------------
161
            "receive": {
162
                "video": {
163
                    # Decided by both:
164
                    "enabled": self.app.config.video_recv_enabled and remote_config["video"]["send_enabled"],
165

    
166
                    # decided locally:
167
                    "sink": self.app.config.video_sink,
168
                    "port": str(self.app.recv_video_port), #decided by the app
169
                    "codec": self.app.config.video_codec,
170
                    "deinterlace": self.app.config.video_deinterlace, # bool
171
                    "window-title": "\"From %s\"" % (contact_name), #TODO: i18n
172
                    "jitterbuffer": self.app.config.video_jitterbuffer, 
173
                    "fullscreen": self.app.config.video_fullscreen, # bool
174
                    "display": self.app.config.video_display,
175
                    "bitrate": self.app.config.video_bitrate, # float
176
                    
177
                    # Decided by remote peer:
178
                    "aspect-ratio": remote_config["video"]["aspect_ratio"],
179
                    "width": int(receive_width), # int
180
                    "height": int(receive_height), # int
181
                },
182
                "audio": {
183
                    # Decided by both:
184
                    "enabled": self.app.config.audio_recv_enabled and remote_config["audio"]["send_enabled"],
185
                    
186
                    # decided locally:
187
                    "numchannels": self.app.config.audio_channels, # int
188
                    "vumeter-id": self.app.gui.audio_levels_output_socket_id,
189
                    "codec": self.app.config.audio_codec, 
190
                    "port": self.app.recv_audio_port,
191
                    "sink": self.app.config.audio_sink,
192
                    "buffer": self.app.config.audio_output_buffer,
193
                    "synchronized": self.app.config.audio_video_synchronized,
194
                    "jitterbuffer": audio_jitterbuffer,
195
                    "jack_autoconnect": self.app.config.audio_jack_enable_autoconnect,
196
                },
197
                "midi": {
198
                    "enabled": midi_recv_enabled,
199
                    "jitterbuffer": self.app.config.midi_jitterbuffer,
200
                    "output_device": midi_output_device,
201
                    "port": str(self.app.recv_midi_port),
202
                }
203
            }
204
        }
205
        if self.session_details["send"]["video"]["source"] != "v4l2src":
206
            self.session_details["send"]["video"]["device"] = None
207
        if self.session_details["send"]["video"]["codec"] == "theora":
208
            self.session_details["send"]["video"]["bitrate"] = None
209
        
210
        # limit to max numchannels if in raw
211
        # ...according to remote's max
212
        if self.session_details["receive"]["audio"]["codec"] == "raw":
213
            if self.session_details["receive"]["audio"]["numchannels"] > remote_config["audio"]["max_channels_in_raw"]:
214
                num =  remote_config["audio"]["max_channels_in_raw"]
215
                self.session_details["receive"]["audio"]["numchannels"] = num
216
                msg = _("Limiting the number of audio channels to receive to %(number)d since remote peer only support up to that much.\nDecrease it to get rid of this message.") % {"number": num}
217
                log.error(msg)
218
                self.app.gui.show_error_dialog(msg)
219
        # ...according to local's max
220
        if self.session_details["send"]["audio"]["codec"] == "raw":
221
            if self.session_details["send"]["audio"]["numchannels"] > self.app.max_channels_in_raw:
222
                num = self.app.max_channels_in_raw
223
                self.session_details["send"]["audio"]["numchannels"] = num
224
                msg = _("Limiting the number of audio channels to send to %(number)d since we only support up to that much.\nDecrease it to get rid of this message.") % {"number": num}
225
                details = _("Update your jackaudiosrc Gstreamer element.")
226
                log.error(msg)
227
                log.error(details)
228
                self.app.gui.show_error_dialog(msg, details=details)
229
        
230
        log.debug(str(self.session_details))
231

    
232
    def _prepare_stats_and_errors_dicts(self):
233
        # setting up
234
        self.rtcp_stats = {
235
            "send": {
236
                "video": {
237
                    "packets-lost": 0,
238
                    "packets-sent": 0,
239
                    "packet-loss-percent": 0.0,
240
                    "jitter": 0,
241
                    "bitrate": None,
242
                    "connected": False
243
                },
244
                "audio": {
245
                    "packets-lost": 0,
246
                    "packets-sent": 0,
247
                    "packet-loss-percent": 0.0,
248
                    "jitter": 0,
249
                    "bitrate": None,
250
                    "connected": False
251
                }
252
            },
253
            "receive": {
254
                "video": {
255
                    "connected": False,
256
                    "bitrate": None 
257
                },
258
                "audio": {
259
                    "connected": False,
260
                    "bitrate": None 
261
                }
262
            }
263
        }
264
        self.error_messages = {
265
            "send": [], # list of strings
266
            "receive": [], # list of strings
267
            }
268
        self.warnings = {
269
            "send": [], # list of strings
270
            "receive": [], # list of strings
271
            }
272
        
273
    def start(self, host):
274
        """
275
        Starts the sender and receiver processes.
276
        
277
        @param host: str ip addr
278
        Raises a RuntimeError if a sesison is already in progress.
279
        """
280
        if self.state != process.STATE_STOPPED:
281
            raise RuntimeError("Cannot start streamers since they are %s." % (self.state)) # the programmer has done something wrong if we're here.
282
        
283
        self._gather_config_to_stream(host)
284
        details = self.session_details
285
        send_video_enabled = self.session_details["send"]["video"]["enabled"]
286
        send_audio_enabled = self.session_details["send"]["audio"]["enabled"]
287
        recv_video_enabled = self.session_details["receive"]["video"]["enabled"]
288
        recv_audio_enabled = self.session_details["receive"]["audio"]["enabled"]
289
        midi_recv_enabled = self.session_details["receive"]["midi"]["enabled"]
290
        midi_send_enabled = self.session_details["send"]["midi"]["enabled"]
291
        jack_autoconnect = self.session_details["send"]["audio"]["jack_autoconnect"] # either send or recv
292
        extra_send_enabled = not self.session_details["send"]["audio"]["synchronized"] and send_audio_enabled
293
        extra_recv_enabled = not self.session_details["receive"]["audio"]["synchronized"] and recv_audio_enabled
294
        normal_recv_enabled = recv_video_enabled or recv_audio_enabled and not extra_recv_enabled # if the self.sender and self.receiver are enabled
295
        normal_send_enabled = send_video_enabled or send_audio_enabled and not extra_send_enabled
296
        if not send_audio_enabled and not send_video_enabled and not midi_send_enabled and not midi_recv_enabled and not recv_audio_enabled and not recv_video_enabled:
297
            raise RuntimeError("Everything is disabled, but the application is trying to start to stream. This should not happen.") # the programmer has done a mistake if we're here.
298

    
299
        log.debug("send_video_enabled %s" % (send_video_enabled))
300
        log.debug("send_audio_enabled %s" % (send_audio_enabled))
301
        log.debug("recv_video_enabled %s" % (recv_video_enabled))
302
        log.debug("recv_audio_enabled %s" % (recv_audio_enabled))
303
        log.debug("midi_recv_enabled %s" % (midi_recv_enabled))
304
        log.debug("midi_send_enabled %s" % (midi_send_enabled))
305

    
306
        # we store the arguments in lists
307
        milhouse_send_cmd_common = []
308
        milhouse_send_cmd_audio = []
309
        milhouse_send_cmd_video = []
310
        milhouse_recv_cmd_common = []
311
        milhouse_recv_cmd_audio = []
312
        milhouse_recv_cmd_video = []
313

    
314
        # ------------------ send ---------------
315
        # every element in the lists must be strings since we join them .
316
        # int elements are converted to str.
317
        milhouse_send_cmd_common.extend([
318
            "milhouse", 
319
            '--sender', 
320
            '--address', details["peer"]["address"]
321
            ])
322

    
323
        # send video:
324
        if send_video_enabled:
325
            milhouse_send_cmd_video.extend([
326
                '--videosource', details["send"]["video"]["source"],
327
                '--videocodec', details["send"]["video"]["codec"],
328
                '--videoport', str(details["send"]["video"]["port"]),
329
                '--width', str(details["send"]["video"]["width"]), 
330
                '--height', str(details["send"]["video"]["height"]),
331
                '--aspect-ratio', str(details["send"]["video"]["aspect-ratio"]),
332
                ])
333
            if details["send"]["video"]["source"] == "v4l2src":
334
                dev = self.app.parse_v4l2_device_name(details["send"]["video"]["device"])
335
                if dev is None:
336
                    log.error("v4l2 device is not found !!!!")
337
                else:
338
                    v4l2_dev_name = dev["name"]
339
                milhouse_send_cmd_video.extend(["--videodevice", v4l2_dev_name])
340
            if details["send"]["video"]["codec"] != "theora":
341
                milhouse_send_cmd_video.extend(['--videobitrate', str(int(details["send"]["video"]["bitrate"] * 1000000))])
342

    
343
        # send audio:
344
        if send_audio_enabled:
345
            milhouse_send_cmd_audio.extend([
346
                '--audiosource', details["send"]["audio"]["source"],
347
                '--numchannels', str(details["send"]["audio"]["numchannels"]),
348
                '--audiocodec', details["send"]["audio"]["codec"],
349
                '--audioport', str(details["send"]["audio"]["port"]),
350
                '--vumeter-id', str(details["send"]["audio"]["vumeter-id"]),
351
                '--audio-buffer', str(details["send"]["audio"]["buffer"])
352
                ])
353
            if not jack_autoconnect:
354
                milhouse_send_cmd_audio.extend([
355
                    "--disable-jack-autoconnect"
356
                    ])
357

    
358
        # ------------------- recv ----------------
359
        milhouse_recv_cmd_common.extend([
360
            "milhouse",
361
            '--receiver', 
362
            '--address', details["peer"]["address"]
363
            ])
364

    
365
        # recv video:
366
        if recv_video_enabled:
367
            milhouse_recv_cmd_video.extend([
368
                '--videosink', details["receive"]["video"]["sink"],
369
                '--videocodec', details["receive"]["video"]["codec"],
370
                '--videoport', str(details["receive"]["video"]["port"]),
371
                '--jitterbuffer', str(details["receive"]["video"]["jitterbuffer"]),
372
                '--width', str(details["receive"]["video"]["width"]),
373
                '--height', str(details["receive"]["video"]["height"]),
374
                '--aspect-ratio', details["receive"]["video"]["aspect-ratio"],
375
                '--window-title', details["receive"]["video"]["window-title"],
376
                '--display', details["receive"]["video"]["display"],
377
                ])
378
            if details["receive"]["video"]["fullscreen"]:
379
                milhouse_recv_cmd_video.append('--fullscreen')
380
            if details["receive"]["video"]["deinterlace"]:
381
                milhouse_recv_cmd_video.append('--deinterlace')
382

    
383
        # recv audio:
384
        if recv_audio_enabled:
385
            milhouse_recv_cmd_audio.extend([
386
                # audio:
387
                '--audiosink', details["receive"]["audio"]["sink"],
388
                '--numchannels', str(details["receive"]["audio"]["numchannels"]),
389
                '--audiocodec', details["receive"]["audio"]["codec"],
390
                '--audioport', str(details["receive"]["audio"]["port"]),
391
                '--vumeter-id', str(details["receive"]["audio"]["vumeter-id"]),
392
                '--audio-buffer', str(details["receive"]["audio"]["buffer"])
393
                ])
394
            if not jack_autoconnect:
395
                milhouse_recv_cmd_audio.extend([
396
                    "--disable-jack-autoconnect"
397
                    ])
398

    
399
        self._prepare_stats_and_errors_dicts()
400
        # every element in the lists must be strings since we join them 
401
        # TODO: not sync
402
        # TODO: if both disabled, do not start sender/receiver
403
        # ---- audio/video receiver ----
404
        if recv_audio_enabled or recv_video_enabled: # receiver(s)
405
            milhouse_recv_cmd_final = []
406
            milhouse_recv_cmd_extra = []
407
            
408
            milhouse_recv_cmd_final.extend(milhouse_recv_cmd_common)
409
            milhouse_recv_cmd_extra.extend(milhouse_recv_cmd_common)
410
            milhouse_recv_cmd_final.extend(milhouse_recv_cmd_video)
411
            if extra_recv_enabled:
412
                milhouse_recv_cmd_extra.extend(milhouse_recv_cmd_audio)
413
                milhouse_recv_cmd_extra.extend(["--jitterbuffer", str(details["receive"]["audio"]["jitterbuffer"])])
414
            else:
415
                milhouse_recv_cmd_final.extend(milhouse_recv_cmd_audio)
416
            try:
417
                recv_cmd = " ".join(milhouse_recv_cmd_final)
418
            except TypeError, e:
419
                log.error(e)
420
                log.error(milhouse_recv_cmd_final)
421
                raise
422
            try:
423
                extra_recv_cmd = " ".join(milhouse_recv_cmd_extra)
424
            except TypeError, e:
425
                log.error(e)
426
                log.error(milhouse_recv_cmd_extra)
427
                raise
428
            if normal_recv_enabled:
429
                self.receiver = process.ProcessManager(command=recv_cmd, identifier="receiver")
430
                self.receiver.state_changed_signal.connect(self.on_process_state_changed)
431
                self.receiver.stdout_line_signal.connect(self.on_receiver_stdout_line)
432
                self.receiver.stderr_line_signal.connect(self.on_receiver_stderr_line)
433
            if extra_recv_enabled:
434
                self.extra_receiver = process.ProcessManager(command=extra_recv_cmd, identifier="extra_receiver")
435
                self.extra_receiver.state_changed_signal.connect(self.on_process_state_changed)
436
                self.extra_receiver.stdout_line_signal.connect(self.on_receiver_stdout_line)
437
                self.extra_receiver.stderr_line_signal.connect(self.on_receiver_stderr_line)
438
        # ---- audio/video sender ----
439
        if send_audio_enabled or send_video_enabled: # sender(s)
440
            milhouse_send_cmd_final = []
441
            milhouse_send_cmd_extra = []
442
            
443
            milhouse_send_cmd_final.extend(milhouse_send_cmd_common)
444
            milhouse_send_cmd_extra.extend(milhouse_send_cmd_common) # only used if we have to
445
            milhouse_send_cmd_final.extend(milhouse_send_cmd_video)
446
            if extra_send_enabled:
447
                milhouse_send_cmd_extra.extend(milhouse_send_cmd_audio)
448
            else:
449
                milhouse_send_cmd_final.extend(milhouse_send_cmd_audio)
450
            send_cmd = " ".join(milhouse_send_cmd_final)
451
            extra_send_cmd = " ".join(milhouse_send_cmd_extra)
452
            if normal_send_enabled:
453
                self.sender = process.ProcessManager(command=send_cmd, identifier="sender")
454
                self.sender.state_changed_signal.connect(self.on_process_state_changed)
455
                self.sender.stdout_line_signal.connect(self.on_sender_stdout_line)
456
                self.sender.stderr_line_signal.connect(self.on_sender_stderr_line)
457
            if extra_send_enabled:
458
                self.extra_sender = process.ProcessManager(command=extra_send_cmd, identifier="extra_sender")
459
                self.extra_sender.state_changed_signal.connect(self.on_process_state_changed)
460
                self.extra_sender.stdout_line_signal.connect(self.on_sender_stdout_line)
461
                self.extra_sender.stderr_line_signal.connect(self.on_sender_stderr_line)
462
                # FIXME: too much code duplication
463
        
464
        if midi_recv_enabled:
465
            midi_out_device = self.app.parse_midi_device_name(details["receive"]["midi"]["output_device"], is_input=False)
466
            #TODO: check if is None
467
            midi_recv_args = [
468
                "midistream",
469
                "--address", details["peer"]["address"],
470
                "--receiving-port", str(details["receive"]["midi"]["port"]),
471
                "--jitter-buffer", str(details["receive"]["midi"]["jitterbuffer"]),
472
                "--output-device", str(midi_out_device["number"])
473
                ]
474
                #"--verbose",
475
            midi_recv_command = " ".join(midi_recv_args) 
476
            self.midi_receiver = process.ProcessManager(command=midi_recv_command, identifier="midi_receiver")
477
            self.midi_receiver.state_changed_signal.connect(self.on_process_state_changed)
478
            self.midi_receiver.stdout_line_signal.connect(self.on_midi_stdout_line)
479
            self.midi_receiver.stderr_line_signal.connect(self.on_midi_stderr_line)
480
        
481
        if midi_send_enabled:
482
            midi_in_device = self.app.parse_midi_device_name(details["send"]["midi"]["input_device"], is_input=True)
483
            #TODO: check if is None
484
            midi_send_args = [
485
                "midistream",
486
                "--address", details["peer"]["address"],
487
                "--sending-port", str(details["send"]["midi"]["port"]),
488
                "--input-device", str(midi_in_device["number"])
489
                ]
490
                #"--verbose",
491
            midi_send_command = " ".join(midi_send_args) 
492
            self.midi_sender = process.ProcessManager(command=midi_send_command, identifier="midi_sender")
493
            self.midi_sender.state_changed_signal.connect(self.on_process_state_changed)
494
            self.midi_sender.stdout_line_signal.connect(self.on_midi_stdout_line)
495
            self.midi_sender.stderr_line_signal.connect(self.on_midi_stderr_line)
496
        
497
        # starting
498
        self._set_state(process.STATE_STARTING)
499
        if send_audio_enabled or send_video_enabled:
500
            log.info("$ %s" % (send_cmd))
501
            if normal_send_enabled:
502
                self.sender.start()
503
            if extra_send_enabled:
504
                self.extra_sender.start()
505
        if recv_audio_enabled or recv_video_enabled:
506
            log.info("$ %s" % (recv_cmd))
507
            if normal_recv_enabled:
508
                self.receiver.start()
509
            if extra_recv_enabled:
510
                self.extra_receiver.start()
511
        if midi_recv_enabled:
512
            self.midi_receiver.start()
513
        if midi_send_enabled:
514
            self.midi_sender.start()
515

    
516
    def get_command_lines(self):
517
        """
518
        Returns a list of all the current processes command lines.
519
        @rettype: list
520
        """
521
        ret = []
522
        for proc in self.get_all_streamer_process_managers():
523
            ret.append(proc.command)
524
        return ret
525

    
526
    def on_midi_stdout_line(self, process_manager, line):
527
        log.debug("%s %s" % (process_manager.identifier, line))
528

    
529
    def on_midi_stderr_line(self, process_manager, line):
530
        log.error("%s %s" % (process_manager.identifier, line))
531

    
532
    def on_receiver_stdout_line(self, process_manager, line):
533
        """
534
        Handles a new line from our receiver process' stdout
535
        """
536
        
537
        if "WARNING" in line:
538
            log.warning(line)
539
            self.warnings["receive"].append(line)
540
        try:
541
            if "stream connected" in line:
542
                if "audio" in line:
543
                    self.rtcp_stats["receive"]["audio"]["connected"] = True
544
                elif "video" in line:
545
                    self.rtcp_stats["receive"]["video"]["connected"] = True
546
            elif "BITRATE" in line:
547
                if "video" in line:
548
                    self.rtcp_stats["receive"]["video"]["bitrate"] = int(line.split(":")[-1])
549
                elif "audio" in line:
550
                    self.rtcp_stats["receive"]["audio"]["bitrate"] = int(line.split(":")[-1])
551
            else:
552
                log.debug("%s stdout: %s" % (process_manager.identifier, line))
553
        except ValueError, e:
554
            log.error("%s when parsing line '%s' from receiver" % (e, line))
555

    
556
    def on_receiver_stderr_line(self, process_manager, line):
557
        """
558
        Handles a new line from our receiver process' stderr
559
        """
560
        log.error("%9s stderr: %s" % (process_manager.identifier, line))
561
        if "CRITICAL" in line or "ERROR" in line:
562
            self.error_messages["receive"].append(line)
563
        if "WARNING" in line:
564
            log.warning(line)
565
            self.warnings["receive"].append(line)
566
    
567
    def on_sender_stdout_line(self, process_manager, line):
568
        """
569
        Handles a new line from our receiver process' stdout
570
        """
571
        log.debug("%s stdout: %s" % (process_manager.identifier, line))
572
        if "WARNING" in line:
573
            log.warning(line)
574
            self.warnings["receive"].append(line)
575
        try:
576
            if "PACKETS-LOST" in line:
577
                if "video" in line:
578
                    self.rtcp_stats["send"]["video"]["packets-lost"] = int(line.split(":")[-1])
579
                elif "audio" in line:
580
                    self.rtcp_stats["send"]["audio"]["packets-lost"] = int(line.split(":")[-1])
581
                #self._calculate_packet_loss()
582
            elif "AVERAGE PACKET-LOSS" in line:
583
                if "video" in line:
584
                    self.rtcp_stats["send"]["video"]["packet-loss-percent"] = float(line.split(":")[-1])
585
                elif "audio" in line:
586
                    self.rtcp_stats["send"]["audio"]["packet-loss-percent"] = float(line.split(":")[-1])
587
            elif "PACKETS-SENT" in line:
588
                if "video" in line:
589
                    self.rtcp_stats["send"]["video"]["packets-sent"] = int(line.split(":")[-1])
590
                elif "audio" in line:
591
                    self.rtcp_stats["send"]["audio"]["packets-sent"] = int(line.split(":")[-1])
592
                #self._calculate_packet_loss()
593
            elif "JITTER" in line:
594
                if "video" in line:
595
                    self.rtcp_stats["send"]["video"]["jitter"] = int(line.split(":")[-1])
596
                elif "audio" in line:
597
                    self.rtcp_stats["send"]["audio"]["jitter"] = int(line.split(":")[-1])
598
            elif "BITRATE" in line:
599
                if "video" in line:
600
                    self.rtcp_stats["send"]["video"]["bitrate"] = int(line.split(":")[-1])
601
                elif "audio" in line:
602
                    self.rtcp_stats["send"]["audio"]["bitrate"] = int(line.split(":")[-1])
603
            elif "connected" in line:
604
                if "video" in line:
605
                    self.rtcp_stats["send"]["video"]["connected"] = True
606
                elif "audio" in line:
607
                    self.rtcp_stats["send"]["audio"]["connected"] = True
608
        except ValueError, e:
609
            log.error("%s when parsing line '%s' from sender" % (e, line))
610

    
611
    def on_sender_stderr_line(self, process_manager, line):
612
        """
613
        Handles a new line from our receiver process' stderr
614
        """
615
        log.error("%9s stderr: %s" % (process_manager.identifier, line))
616
        if "CRITICAL" in line or "ERROR" in line:
617
            self.error_messages["send"].append(line)
618
        if "WARNING" in line:
619
            log.warning(line)
620
            self.warnings["receive"].append(line)
621

    
622
    def is_busy(self):
623
        """
624
        Retuns True if a streaming session is in progress.
625
        """
626
        return self.state != process.STATE_STOPPED
627

    
628
    def get_all_streamer_process_managers(self):
629
        """
630
        Returns all the current streaming process managers for the current session.
631
        @rtype: list
632
        """
633
        ret = []
634
        for proc in [self.receiver, self.extra_receiver, self.sender, self.extra_sender, self.midi_receiver, self.midi_sender]:
635
            if proc is not None:
636
                ret.append(proc)
637
        return ret
638

    
639
    def on_process_state_changed(self, process_manager, process_state):
640
        """
641
        Slot for the ProcessManager.state_changed_signal
642
        Calls stop() if one of the processes crashed.
643
        """
644
        log.debug("%s %s" % (process_manager, process_state))
645
        if process_state == process.STATE_RUNNING:
646
            # As soon as one is running, set our state to running
647
            if self.state == process.STATE_STARTING:
648
                self._set_state(process.STATE_RUNNING)
649
        elif process_state == process.STATE_STOPPING:
650
            pass
651
        elif process_state == process.STATE_STARTING:
652
            pass
653
        elif process_state == process.STATE_STOPPED:
654
            # As soon as one crashes or is not able to start, stop all streamer processes.
655
            if self.state in [process.STATE_RUNNING, process.STATE_STARTING]:
656
                log.info("A streamer process died. Stopping the local streamer manager.")
657
                self.stop() # sets self.state to STOPPING
658
            # Next, if all streamers are dead, we can say this manager is stopped
659
            if self.state == process.STATE_STOPPING:
660
                one_is_left = False
661
                for proc in self.get_all_streamer_process_managers():
662
                    if process_manager is not proc and proc.state != process.STATE_STOPPED:
663
                        log.info("Streamer process %s is not dead, so we are not done stopping. Its state is %s." % (proc, proc.state))
664
                        one_is_left = True
665
                if not one_is_left:
666
                    log.info("Setting streamers manager to STOPPED")
667
                    self._set_state(process.STATE_STOPPED)
668

    
669
    def on_stopped(self):
670
        """
671
        When the state changes to stopped, 
672
         * check for errors and display them to the user.
673
        """
674
        msg = ""
675
        details = ""
676
        show_error_dialog = False
677
        #TODO: internationalize
678
        log.info("All streamers are stopped.")
679
        if len(self.error_messages["send"]) != 0:
680
            show_error_dialog = True
681
            log.error("Error messages from the sender for this session: %s" % (self.error_messages["send"]))
682
            details += _("Errors from local sender:") + "\n"
683
            for line in self.error_messages["send"]:
684
                details += " * " + line + "\n"
685
            if len(self.warnings["send"]) != 0:
686
                details += _("Warnings from local sender:") + "\n"
687
                for line in self.warnings["send"]:
688
                    details += " * " + line + "\n"
689
        if len(self.error_messages["receive"]) != 0:
690
            show_error_dialog = True
691
            log.error("Error messages from the receiver for this session: %s" % (self.error_messages["receive"]))
692
            details += _("Errors from local receiver:") + "\n"
693
            for line in self.error_messages["receive"]:
694
                details += " * " + line + "\n"
695
            if len(self.warnings["receive"]) != 0:
696
                details += _("Warnings from local receiver:") + "\n"
697
                for line in self.warnings["send"]:
698
                    details += " * " + line + "\n"
699
        if show_error_dialog:
700
            msg = _("Some errors occured during the audio/video streaming session.")
701
            self.app.gui.show_error_dialog(msg, details=details)
702
        # TODO: should we set all our process managers to None
703

    
704
        # set all to None:
705
        self.sender = None
706
        self.receiver = None
707
        self.extra_sender = None
708
        self.extra_receiver = None
709
        self.midi_receiver = None
710
        self.midi_sender = None
711
        log.debug("Done stopping steaming")
712
    
713
    def _set_state(self, new_state):
714
        """
715
        Handles state changes.
716
        """
717
        if self.state != new_state:
718
            self.state_changed_signal(self, new_state)
719
            self.state = new_state
720
            if new_state == process.STATE_STOPPED:
721
                self.on_stopped()
722
        else:
723
            raise RuntimeError("Setting state to %s, which is already the current state." % (self.state))
724
            
725
    def stop(self):
726
        """
727
        Stops the sender and receiver processes.
728
        Does not send any message to the remote peer ! This must be done somewhere else.
729
        """
730
        #TODO: return a deferred
731
        # stopping
732
        if self.state in [process.STATE_RUNNING, process.STATE_STARTING]:
733
            self._set_state(process.STATE_STOPPING)
734
            for proc in self.get_all_streamer_process_managers():
735
                if proc is not None:
736
                    if proc.state != process.STATE_STOPPED and proc.state != process.STATE_STOPPING:
737
                        proc.stop()
738