This URL has Read-Only access.

Statistics
| Branch: | Tag: | Revision:

root / py / scenic / application.py @ 8c2ed4f7

History | View | Annotate | Download (37.9 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
Main application classes.
24

25
Summary of events
26
=================
27
 - At startup, the config file is read.
28
 - Next, we need to disable the interactivity of widgets
29
 - We then set the widget's values, and make them interactive again.
30
 - Some widgets do things when they are changed. Some toggle the sensitivity (gray out) of some other widgets, whereas some other will call external processes to change video and audio devices properties. 
31
 - When the user decides to start a streaming session, the value of all widgets is read, and we save those values in the config file. 
32
 - Next, the offerer connects to the answerer and sends it a dict of its configuration options, serialized in JSON. 
33
 - If the answerer accepts, he sends back its options. Each peer decides which port he listens to for each service. (audio, video, MIDI streams) 
34
 - Next, the streamer manager store a summary of both peer's options in a large dict. That's where we check which processes we will need to start. 
35
 - The streamer manager starts the processes. 
36
 - Some processes' output might be checked for error messages, which can be shown to the user in error dialogs.
37
 - As soon as one process dies or the user wants to stop the streaming session, we kill all streamer processes and send "BYE" to the other peer. The other peer also stops all its streamer processes.
38
 - When a session is in progress, many widgets are grayed out. It is not the case when there is no session in progress.
39
 - When we quit, the state of each widget is saved to the config file.
40

41
The preview
42
===========
43
The preview works a little like the streamer manager, but is simpler since it does not involve a remote peer. It is a process that is started. When it dies, we toggle the start of the start/stop button. 
44

45
Negotiation sequence
46
====================
47
 - {"msg":"INVITE", "videoport":10000, "audioport":11000, "sid":0, "please_send_to_port":999}
48
  - Each peer ask for ports to send to, and of media settings as well. "video": [{"port":10000, "codec":"mpeg4", "bitrate":3000000}]
49
 - {"msg":"ACCEPT", "videoport":10000, "audioport":11000, "sid":0}
50
 - {"msg":"REFUSE", "sid":0}
51
 - {"msg":"CANCEL", "sid":0}
52
 - {"msg":"ACK", "sid":0}
53
 - {"msg":"BYE", "sid":0}
54
 - {"msg":"OK", "sid":0}
55

56
Devices names
57
=============
58
Identifying the devices is a difficult task. The users prefers to see the name of the device, not its number. That's what we show to the user and keep in the state saving. That makes it easier to identify them when there number changes. 
59

60
For example, a given V4L2 video device can be mounted as /dev/video0 once and as /dev/video1 at an other time. Same for MIDI devices. 
61

62
But what if we have two devices with same name? Here are two examples:
63

64
MIDI example
65
------------
66
 - M Audio Delta 1010LT MIDI (2)
67
 - USB Oxygen 8 v2 MIDI 1 (3)
68
 - USB Oxygen 8 v2 MIDI 1 (5)
69

70
V4L2 example
71
------------
72
 - BT878 video (Osprey 210/220/230 (/dev/video0)
73
 - BT878 video (Osprey 210/220/230 (/dev/video1)
74
 - UVC Camera (046d:0990) (/dev/video2)
75

76
It's nice to show the device number/identifier to the user. In the worst case, the user can test the device to see if it's the right one or not. 
77

78
So, our choice is to store both the name of the device and its number in the combo box widget and in the state saving. When we load the device name and number from the config file, we first check for the device with that name and number. If it does not exist, we try to find the first device with that name that we can find. If it does not exist, it defaults to the first choice in the list of devices of that kind. 
79
"""
80
import os
81
from twisted.internet import defer
82
from twisted.internet import error
83
from twisted.internet import task
84
from twisted.internet import reactor
85
from scenic import communication
86
from scenic import saving
87
from scenic import process # just for constants
88
from scenic.streamer import StreamerManager
89
from scenic import dialogs
90
from scenic import ports
91
from scenic.devices import jackd
92
from scenic.devices import x11
93
from scenic.devices import cameras
94
from scenic.devices import midi
95
from scenic import gui
96
from scenic import internationalization
97
_ = internationalization._
98

    
99
class Config(saving.ConfigStateSaving):
100
    """
101
    Configuration for the application.
102
    """
103
    def __init__(self):
104
        # Default values
105
        self.negotiation_port = 17446 # receiving TCP (SIC) messages on it.
106
        self.smtpserver = "smtp.sat.qc.ca" 
107
        # ----------- AUDIO --------------
108
        self.email_info = "scenic@sat.qc.ca"
109
        self.audio_source = "jackaudiosrc"
110
        self.audio_sink = "jackaudiosink"
111
        self.audio_codec = "raw"
112
        self.audio_channels = 2
113
        # ------------- VIDEO -------------
114
        self.video_source = "v4l2src"
115
        self.video_device = "/dev/video0"
116
        self.video_deinterlace = False
117
        self.video_input = 0
118
        self.video_standard = "ntsc"
119
        self.video_sink = "xvimagesink"
120
        self.video_codec = "mpeg4"
121
        self.video_display = ":0.0"
122
        self.video_fullscreen = False
123
        self.video_capture_size = "640x480"
124
        self.preview_in_window = False
125
        #video_window_size = "640x480"
126
        self.video_aspect_ratio = "4:3" 
127
        self.confirm_quit = True
128
        #self.theme = "Darklooks"
129
        self.video_bitrate = 3.0
130
        self.video_jitterbuffer = 75
131
        # ----------- MIDI ----------------
132
        self.midi_recv_enabled = False
133
        self.midi_send_enabled = False
134
        self.midi_input_device = "" # ID and name
135
        self.midi_output_device = "" # ID and name
136
        self.midi_jitterbuffer = 10 # ms
137
        
138
        # Done with the configuration entries.
139
        config_file = 'configuration.json'
140
        config_dir = os.path.expanduser("~/.scenic")
141
        config_file_path = os.path.join(config_dir, config_file)
142
        saving.ConfigStateSaving.__init__(self, config_file_path)
143

    
144
def _format_device_name_and_identifier(name, identifier):
145
    """
146
    Formats a device name to show it to the user and save it to the state saving.
147
    
148
    If you change the format here, change the parsing in the device name parsing method.
149
    See _parse_device_name_and_identifier.
150
    @param name: Name of the device.
151
    @param identifier: Identifier of the device.
152
    @rtype: str
153
    """
154
    #@param midi_device_dict: Dict of MIDI device info, as given by the MIDI device driver.
155
    #@type midi_device_dict: dict
156
    #@rtype: str
157
    return "%s (%s)" % (name, identifier)
158

    
159
def _parse_device_name_and_identifier(formatted_name):
160
    """
161
    Splits a device name and identifier.
162

163
    See _format_device_name_and_identifier.
164
    @param formatted_name: Name and identifier as shown to the user.
165
    @rtype: tuple
166
    @return: Name and identifier of the device. Both strings.
167
    """
168
    tokens = formatted_name.split("(") # split tokens
169
    number = tokens[-1].split(")")[0] # last token without closing parenthesis
170
    name = "(".join(tokens[0 : -1]).strip() # all tokens except last
171
    return name, number
172

    
173
class Application(object):
174
    """
175
    Main class of the application.
176

177
    The devices attributes is a very interesting dict. See the source code.
178
    """
179
    def __init__(self, kiosk_mode=False, fullscreen=False, log_file_name=None):
180
        self.config = Config()
181
        self.log_file_name = log_file_name
182
        self.recv_video_port = None
183
        self.recv_audio_port = None
184
        self.recv_midi_port = None
185
        self.remote_config = {} # dict
186
        self.ports_allocator = ports.PortsAllocator()
187
        self.address_book = saving.AddressBook()
188
        self.streamer_manager = StreamerManager(self)
189
        self.streamer_manager.state_changed_signal.connect(self.on_streamer_state_changed) # XXX
190
        print("Starting SIC server on port %s" % (self.config.negotiation_port)) 
191
        self.server = communication.Server(self, self.config.negotiation_port) # XXX
192
        self.client = communication.Client()
193
        #self.client.connection_error_signal.connect(self.on_connection_error)
194
        self.protocol_version = "SIC 0.1"
195
        self.got_bye = False 
196
        # starting the GUI:
197
        internationalization.setup_i18n()
198
        self.gui = gui.Gui(self, kiosk_mode=kiosk_mode, fullscreen=fullscreen)
199
        self.devices = {
200
            "x11_displays": [], # list of dicts
201
            "cameras": {}, # dict of dicts (only V4L2 cameras for now)
202
            #"dc_cameras": [], # list of dicts
203
            "xvideo_is_present": False, # bool
204
            "jackd_is_running": False,
205
            "jackd_is_zombie": False,
206
            "jack_servers": [], # list of dicts
207
            "midi_input_devices": [],
208
            "midi_output_devices": [],
209
            }
210
        self._jackd_watch_task = task.LoopingCall(self._poll_jackd)
211
        reactor.callLater(0, self._start_the_application)
212
    
213
    def format_midi_device_name(self, midi_device_dict):
214
        """
215
        Formats a MIDI device name to show it to the user and save it to the state saving.
216
        @param midi_device_dict: Dict of MIDI device info, as given by the MIDI device driver.
217
        @type midi_device_dict: dict
218
        @rtype: str
219
        """
220
        return _format_device_name_and_identifier(midi_device_dict["name"], str(midi_device_dict["number"]))
221

    
222
    def format_v4l2_device_name(self, device_dict):
223
        print "formatting v4l2 device name", device_dict
224
        return _format_device_name_and_identifier(device_dict["card"], device_dict["name"])
225

    
226
    def parse_v4l2_device_name(self, formatted_name):
227
        ret = None
228
        name, identifier = _parse_device_name_and_identifier(formatted_name)
229
        key = "cameras"
230
        # try to find a device that matches both name and identifier
231
        for dev in self.devices[key].values():
232
            if dev["card"] == name and dev["name"] == identifier:
233
                ret = dev
234
        if ret is None:
235
            # try to find a device that matches only the name
236
            for dev in self.devices[key].values():
237
                if dev["card"] == name:
238
                    ret = dev
239
        return ret
240
    
241
    def parse_midi_device_name(self, formatted_name, is_input=False):
242
        """
243
        Parses a MIDI device name shown to the user, and return the device's number, or None if it is not found.
244
        
245
        It will not be found in the system if it doesn't exist anymore.        
246
        See format_midi_device_name.
247

248
        @param formatted_name: Name of the device, as given by the format_midi_device_name method.
249
        @type formatted_name: str
250
        @param is_input: True if it's an input device, False for an output device.
251
        @type is_input: bool
252
        @rtype: dict
253
        """
254
        ret = None
255
        name, number = _parse_device_name_and_identifier(formatted_name)
256
        if is_input:
257
            key = "midi_input_devices"
258
        else:
259
            key = "midi_output_devices"
260
        # try to find a device that matches both name and number
261
        for dev in self.devices[key]:
262
            if dev["name"] == name and dev["number"] == int(number):
263
                ret = dev
264
        # try to find a device that matches only the name
265
        if ret is None:
266
            for dev in self.devices[key]:
267
                if dev["name"] == name:
268
                    ret = dev
269
        return ret
270

    
271
    def _start_the_application(self):
272
        """
273
        Should be called only once.
274
        (once Twisted's reactor is running)
275
        """
276
        reactor.addSystemEventTrigger("before", "shutdown", self.before_shutdown)
277
        try:
278
            self.server.start_listening()
279
        except error.CannotListenError, e:
280
            def _cb(result):
281
                reactor.stop()
282
            print("Cannot start SIC server. %s" % (e))
283
            deferred = dialogs.ErrorDialog.create(_("Is another Scenic running? Cannot bind to port %(port)d") % {"port": self.config.negotiation_port}, parent=self.gui.main_window)
284
            deferred.addCallback(_cb)
285
            return
286
        # Devices: JACKD (every 5 seconds)
287
        self._jackd_watch_task.start(5, now=True)
288
        # Devices: X11 and XV
289
        def _callback(result):
290
            self.gui.update_widgets_with_saved_config()
291
        deferred_list = defer.DeferredList([
292
            self.poll_x11_devices(), 
293
            self.poll_xvideo_extension(),
294
            self.poll_camera_devices(), 
295
            self.poll_midi_devices()
296
            ])
297
        deferred_list.addCallback(_callback)
298

    
299
    def poll_midi_devices(self):
300
        """
301
        Called once at startup, and then the GUI can call it.
302
        @rtype: L{Deferred}
303
        """
304
        deferred = midi.list_midi_devices()
305
        def _callback(midi_devices):
306
            input_devices = []
307
            output_devices = []
308
            for device in midi_devices:
309
                if device["is_input"]:
310
                    input_devices.append(device)
311
                else:
312
                    output_devices.append(device)
313
            self.devices["midi_input_devices"] = input_devices
314
            self.devices["midi_output_devices"] = output_devices
315
            print("MIDI inputs: %s" % (input_devices))
316
            print("MIDI outputs: %s" % (output_devices))
317
            self.gui.update_midi_devices()
318
        deferred.addCallback(_callback)
319
        return deferred
320

    
321
    def poll_x11_devices(self):
322
        """
323
        Called once at startup, and then the GUI can call it.
324
        Calls gui.update_x11_devices.
325
        @rtype: Deferred
326
        """
327
        deferred = x11.list_x11_displays(verbose=False)
328
        def _callback(x11_displays):
329
            self.devices["x11_displays"] = x11_displays
330
            print("displays: %s" % (x11_displays))
331
            self.gui.update_x11_devices()
332
        deferred.addCallback(_callback)
333
        return deferred
334

    
335
    def poll_camera_devices(self):
336
        """
337
        Called once at startup, and then the GUI can call it.
338
        Calls gui.update_camera_devices.
339
        For now, we only take into account V4L2 cameras.
340
        @rtype: Deferred
341
        """
342
        deferred = cameras.list_cameras()
343
        toggle_size_sensitivity = self.gui.video_capture_size_widget.get_property("sensitive")
344
        def _callback(cameras):
345
            self.devices["cameras"] = cameras
346
            print("cameras: %s" % (cameras))
347
            self.gui.update_camera_devices()
348
            if toggle_size_sensitivity:
349
                self.gui.video_capture_size_widget.set_sensitive(True)
350
            print("setting video_capture_size widget sensitive to true")
351
        def _errback(reason):
352
            if toggle_size_sensitivity:
353
                self.gui.video_capture_size_widget.set_sensitive(True)
354
            print("setting video_capture_size widget sensitive to true")
355
            return reason
356
        if toggle_size_sensitivity:
357
            self.gui.video_capture_size_widget.set_sensitive(False)
358
        deferred.addCallback(_callback)
359
        print("setting video_capture_size widget sensitive to false")
360
        deferred.addErrback(_errback)
361
        return deferred
362

    
363
    def poll_xvideo_extension(self):
364
        """
365
        Called once at startup, and then the GUI can call it.
366
        @rtype: Deferred
367
        """
368
        deferred = x11.xvideo_extension_is_present()
369
        def _callback(xvideo_is_present):
370
            self.devices["xvideo_is_present"] = xvideo_is_present
371
            if not xvideo_is_present:
372
                msg = _("It seems like the xvideo extension is not present. Video display is not possible.")
373
                print(msg)
374
                dialogs.ErrorDialog.create(msg, parent=self.gui.main_window)
375
        deferred.addCallback(_callback)
376
        return deferred
377
        
378
    def _poll_jackd(self):
379
        """
380
        Checks if the jackd default audio server is running.
381
        Called every n seconds.
382
        """
383
        is_running = False
384
        is_zombie = False
385
        try:
386
            jack_servers = jackd.jackd_get_infos() # returns a list of dicts
387
        except jackd.JackFrozenError, e:
388
            print e 
389
            msg = _("The JACK audio server seems frozen ! \n%s") % (e)
390
            print(msg)
391
            #dialogs.ErrorDialog.create(msg, parent=self.gui.main_window)
392
            is_zombie = True
393
        else:
394
            #print "jackd servers:", jack_servers
395
            if len(jack_servers) == 0:
396
                is_running = False
397
            else:
398
                is_running = True
399
        if self.devices["jackd_is_running"] != is_running:
400
            print("Jackd server changed state: %s" % (jack_servers))
401
        self.devices["jackd_is_running"] = is_running
402
        self.devices["jackd_is_zombie"] = is_zombie
403
        self.devices["jack_servers"] = jack_servers
404
        self.gui.update_jackd_status()
405
    
406
    def before_shutdown(self):
407
        """
408
        Last things done before quitting.
409
        @rtype: L{DeferredList}
410
        """
411
        deferred = defer.Deferred()
412
        print("The application is shutting down.")
413
        # TODO: stop streamers
414
        self.save_configuration()
415
        if self.client.is_connected():
416
            if not self.got_bye:
417
                self.send_bye() # returns None
418
                self.stop_streamers() # returns None
419
        def _cb(result):
420
            print "done quitting."
421
            deferred.callback(True)
422
        def _later():
423
            d2 = self.disconnect_client()
424
            d2.addCallback(_cb)
425
            print('stopping server')
426
        reactor.callLater(0.1, _later)
427
        d1 = self.server.close()
428
        d2 = self.gui.close_preview_if_running()
429
        return defer.DeferredList([deferred, d1, d2])
430
        
431
    # ------------------------- session occuring -------------
432
    def has_session(self):
433
        """
434
        @rtype: bool
435
        """
436
        return self.streamer_manager.is_busy()
437
    # -------------------- streamer ports -----------------
438
    def prepare_before_rtp_stream(self):
439
        #TODO: return a Deferred
440
        self.gui.close_preview_if_running() # TODO: use its deferred
441
        self.save_configuration()
442
        self._allocate_ports()
443
        
444
    def cleanup_after_rtp_stream(self):
445
        self._free_ports()
446
    
447
    def _allocate_ports(self):
448
        # TODO: start_session
449
        self.recv_video_port = self.ports_allocator.allocate()
450
        self.recv_audio_port = self.ports_allocator.allocate()
451
        self.recv_midi_port = self.ports_allocator.allocate()
452

    
453
    def _free_ports(self):
454
        # TODO: stop_session
455
        for port in [self.recv_video_port, self.recv_audio_port, self.recv_midi_port]:
456
            try:
457
                self.ports_allocator.free(port)
458
            except ports.PortsAllocatorError, e:
459
                print(e)
460

    
461
    def save_configuration(self):
462
        """
463
        Saves the configuration to a file.
464
        Reads the widget value prior to do it.
465
        """
466
        self.gui._gather_configuration() # need to get the value of the configuration widgets.
467
        self.config.save()
468
        self.address_book.save() # addressbook values are already stored.
469

    
470
    # --------------------------- network receives ------------
471

    
472
    def _check_protocol_version(self, message):
473
        """
474
        Checks if the remote peer's SIC protocol matches.
475
        @param message: dict messages received in an INVITE or ACCEPT SIC message. 
476
        @rtype: bool
477
        """
478
        # TODO: break if not compatible in a next release.
479
        if message["protocol"] != self.protocol_version:
480
            print("WARNING: Remote peer uses %s and we use %s." % (message["protocol"], self.protocol_version))
481
            return False
482
        else:
483
            return True
484

    
485
    def handle_invite(self, message, addr):
486
        """
487
        handles the INVITE message. 
488
        Refuses if : 
489
         * jackd is not running
490
         * We already just got an INVITE and didn't answer yet.
491
        """
492
        self.got_bye = False
493
        self._check_protocol_version(message)
494
        
495
        def _on_contact_request_dialog_response(response):
496
            """
497
            User is accepting or declining an offer.
498
            @param result: Answer to the dialog.
499
            """
500
            if response:
501
                self.send_accept(addr)
502
            else:
503
                self.send_refuse_and_disconnect() 
504
        # check if the contact is in the addressbook
505
        contact = self._get_contact_by_addr(addr)
506
        invited_by = addr
507
        send_to_port = message["please_send_to_port"]
508

    
509
        def _simply_refuse():
510
            communication.connect_send_and_disconnect(addr, send_to_port, {'msg':'REFUSE', 'sid':0})
511
        
512
        if contact is not None:
513
            invited_by = contact["name"]
514

    
515
        if self.get_last_message_received() == "INVITE" and self.get_last_message_sent() != "REFUSE": # FIXME: does that cover all cases?
516
            _simply_refuse()
517
            print("REFUSED an INVITE since we already got one from someone else.")
518
            return
519
        
520
        def _check_cb(result):
521
            if not result:
522
                _simply_refuse() # TODO: add reason param: Technical problems.
523
            else:
524
                # FIXME: the copy of dict should be more straightforward.
525
                self.remote_config = {
526
                    "audio": message["audio"],
527
                    "video": message["video"],
528
                    "midi": message["midi"]
529
                    }
530
                connected_deferred = self.client.connect(addr, message["please_send_to_port"])
531
                if contact is not None and contact["auto_accept"]:
532
                    print("Contact %s is on auto_accept. Accepting." % (invited_by))
533
                    def _connected_cb(proto):
534
                        self.send_accept(addr)
535
                    connected_deferred.addCallback(_connected_cb)
536
                else:
537
                    text = _("<b><big>%(invited_by)s is inviting you.</big></b>\n\nDo you accept the connection?" % {"invited_by": invited_by})
538
                    dialog_deferred = self.gui.show_invited_dialog(text)
539
                    dialog_deferred.addCallback(_on_contact_request_dialog_response)
540
        
541
        flight_check_deferred = self.check_if_ready_to_stream(role="answerer")
542
        flight_check_deferred.addCallback(_check_cb)
543
    
544
    def _get_contact_by_addr(self, addr):
545
        """
546
        Returns a contact dict or None if not in the addressbook.
547
        """
548
        ret = None
549
        for contact in self.address_book.contact_list:
550
            if contact["address"] == addr:
551
                ret = contact
552
                break
553
        return ret
554
    
555
    def handle_cancel(self, message, addr):
556
        # If had previously sent ACCEPT and receive CANCEL, abort the session.
557
        if self.get_last_message_sent() == "ACCEPT":
558
            self.cleanup_after_rtp_stream()
559
        contact = self._get_contact_by_addr(addr)
560
        contact_name = ""
561
        if contact is not None:
562
            contact_name = contact["name"]
563
        txt = _("Contact %(name)s invited you but cancelled his invitation.") % {"name": contact_name}
564
        # Turning the reason into readable i18n str.
565
        if message.has_key("reason"):
566
            reason = message["reason"]
567
            if reason == communication.CANCEL_REASON_TIMEOUT:
568
                txt += "\n\n" + _("The invitation expired.")
569
            elif reason == communication.CANCEL_REASON_CANCELLED:
570
                txt += "\n\n" + _("The peer cancelled the invitation.")
571
        self.client.disconnect()
572
        self.gui.invited_dialog.hide()
573
        dialogs.ErrorDialog.create(txt, parent=self.gui.main_window)
574

    
575
    def handle_accept(self, message, addr):
576
        if self.get_last_message_sent() == "CANCEL":
577
            self.send_bye() # If got ACCEPT, but had sent CANCEL, send BYE.
578
        else:
579
            self._check_protocol_version(message)
580
            self.got_bye = False
581
            # TODO: Use session to contain settings and ports
582
            self.gui.hide_calling_dialog()
583
            # FIXME: the copy of dict should be more straightforward
584
            self.remote_config = {
585
                "audio": message["audio"],
586
                "video": message["video"],
587
                "midi": message["midi"]
588
                }
589
            if self.streamer_manager.is_busy():
590
                print("Got ACCEPT but we are busy. This is very strange")
591
                dialogs.ErrorDialog.create(_("Got an acceptation from a remote peer, but a streaming session is already in progress."), parent=self.gui.main_window)
592
            else:
593
                print("Got ACCEPT. Starting streamers as initiator.")
594
                self.start_streamers(addr)
595
                self.send_ack()
596

    
597
    def handle_refuse(self):
598
        """
599
        Got REFUSE
600
        """
601
        self.gui.hide_calling_dialog()
602
        self._free_ports()
603
        text = _("The contact refused to stream with you.\n\nIt may be caused by a ongoing session with an other peer or by technical problems.")
604
        dialogs.ErrorDialog.create(text, parent=self.gui.main_window)
605

    
606
    def handle_ack(self, addr):
607
        """
608
        Got ACK
609
        """
610
        print("Got ACK. Starting streamers as answerer.")
611
        self.start_streamers(addr)
612

    
613
    def handle_bye(self):
614
        """
615
        Got BYE
616
        """
617
        self.got_bye = True
618
        self.stop_streamers()
619
        if self.client.is_connected():
620
            print('disconnecting client and sending BYE')
621
            self.client.send({"msg":"OK", "sid":0})
622
            self.disconnect_client()
623

    
624
    def handle_ok(self):
625
        """
626
        Got OK
627
        """
628
        print("received ok. Everything has an end.")
629
        print('disconnecting client')
630
        self.disconnect_client()
631

    
632
    def on_server_receive_command(self, message, addr):
633
        msg = message["msg"]
634
        print("Got %s from %s" % (msg, addr))
635
        # TODO: use prefixedMethods from twisted.
636
        if msg == "INVITE":
637
            self.handle_invite(message, addr)
638
        elif msg == "CANCEL":
639
            self.handle_cancel(message, addr)
640
        elif msg == "ACCEPT":
641
            self.handle_accept(message, addr)
642
        elif msg == "REFUSE":
643
            self.handle_refuse()
644
        elif msg == "ACK":
645
            self.handle_ack(addr)
646
        elif msg == "BYE":
647
            self.handle_bye()
648
        elif msg == "OK":
649
            self.handle_ok()
650
        else:
651
            print('WARNING: Unexpected message %s' % (msg))
652

    
653
    # -------------------------- actions on streamer manager --------
654

    
655
    def start_streamers(self, addr):
656
        self.streamer_manager.start(addr)
657

    
658
    def stop_streamers(self):
659
        # TODO: return a deferred. 
660
        self.streamer_manager.stop()
661

    
662
    def on_streamers_stopped(self, addr):
663
        """
664
        We call this when all streamers are stopped.
665
        """
666
        print("on_streamers_stopped got called")
667
        self.cleanup_after_rtp_stream()
668

    
669
    # ---------------------- sending messages -----------
670
        
671
    def disconnect_client(self):
672
        """
673
        Disconnects the SIC sender.
674
        @rtype: L{Deferred}
675
        """
676
        def _cb(result, d1):
677
            d1.callback(True)
678
        def _cl(d1):
679
            if self.client.is_connected():
680
                d2 = self.client.disconnect()
681
                d2.addCallback(_cb, d1)
682
            else:
683
                d1.callback(True)
684
        # not sure why to do it in a call later.
685
        if self.client.is_connected():
686
            d = defer.Deferred()
687
            reactor.callLater(0, _cl, d)
688
            return d
689
        else: 
690
            return defer.succeed(True)
691
    
692
    def _get_local_config_message_items(self):
693
        """
694
        Returns a dict with keys 'audio', 'midi' and 'video' to send to remote peer.
695
        @rtype: dict
696
        """
697
        return {
698
            "video": {
699
                "codec": self.config.video_codec,
700
                "bitrate": self.config.video_bitrate, # float Mbps
701
                "port": self.recv_video_port,
702
                "aspect_ratio": self.config.video_aspect_ratio, 
703
                "capture_size": self.config.video_capture_size 
704
                },
705
            "audio": {
706
                "codec": self.config.audio_codec,
707
                "numchannels": self.config.audio_channels,
708
                "port": self.recv_audio_port
709
                },
710
            "midi": {
711
                "port": self.recv_midi_port,
712
                "recv_enabled": self.config.midi_recv_enabled,
713
                "send_enabled": self.config.midi_send_enabled
714
                }
715
            }
716

    
717
    def check_if_ready_to_stream(self, role="offerer"):
718
        """
719
        Does the flight check, checking if ready to stream.
720
        
721
        Checks if ready to stream. 
722
        Will pop up error dialog if there are errors.
723
        Calls the deferred with a result that is True of False.
724
        @rtype: L{Deferred}
725
        @param role: Either "offerer" or "answerer".
726
        """
727
        #TODO: poll X11 devices
728
        #TODO: poll xv extension
729
        deferred = defer.Deferred()
730
        def _callback(result):
731
            # callback for the deferred list created below.
732
            # calls the deferred's callback
733
            if role == "offerer":
734
                error_msg = _("Impossible to invite a contact to start streaming.")
735
            elif role == "answerer":
736
                error_msg = _("Impossible to accept an invitation to stream.")
737
            else:
738
                raise RuntimeError("Invalid role value : %s" % (role))
739
            
740
            x11_displays = [display["name"] for display in self.devices["x11_displays"]]
741
            midi_input_devices = [device["name"] for device in self.devices["midi_input_devices"]]
742
            midi_output_devices = [device["name"] for device in self.devices["midi_output_devices"]]
743
            cameras = self.devices["cameras"].keys()
744
                
745
            if self.config.video_display not in x11_displays: #TODO: do not test if not receiving video
746
                dialogs.ErrorDialog.create(error_msg + "\n\n" + _("The X11 display %(display)s disappeared!") % {"display": self.config.video_display}, parent=self.gui.main_window) # not very likely to happen !
747
                return deferred.callback(False)
748
            
749
            elif self.config.video_source == "v4l2src" and self.parse_v4l2_device_name(self.config.video_device) is None: #TODO: do not test if not sending video
750
                dialogs.ErrorDialog.create(error_msg + "\n\n" + _("The video source %(camera)s disappeared!") % {"camera": self.config.video_source}, parent=self.gui.main_window) 
751
                return deferred.callback(False)
752
                
753
            elif not self.devices["jackd_is_running"]:
754
                # TODO: Actually poll jackd right now.
755
                dialogs.ErrorDialog.create(error_msg + "\n\n" + _("JACK is not running."), parent=self.gui.main_window)
756
                return deferred.callback(False)
757
            
758
            elif self.streamer_manager.is_busy():
759
                dialogs.ErrorDialog.create(error_msg + "\n\n" + _("A streaming session is already in progress."), parent=self.gui.main_window)
760
                deferred.callback(False)
761
            
762
            elif self.config.midi_recv_enabled and self.parse_midi_device_name(self.config.midi_output_device, is_input=False) is None:
763
                dialogs.ErrorDialog.create(error_msg + "\n\n" + _("The MIDI output device %(device)s disappeared!") % {"device": self.config.midi_output_device}, parent=self.gui.main_window)
764
                deferred.callback(False)
765
            
766
            elif self.config.midi_send_enabled and self.parse_midi_device_name(self.config.midi_input_device, is_input=True) is None:
767
                dialogs.ErrorDialog.create(error_msg + "\n\n" + _("The MIDI input device %(device)s disappeared!") % {"device": self.config.midi_input_device}, parent=self.gui.main_window)
768
                deferred.callback(False)
769
            
770
            # "cameras": {}, # dict of dicts (only V4L2 cameras for now)
771
            elif not self.devices["xvideo_is_present"]: #TODO: do not test if not receiving video
772
                dialogs.ErrorDialog.create(error_msg + "\n\n" + _("The X video extension is not present."), parent=self.gui.main_window)
773
                deferred.callback(False)
774
            
775
            else:
776
                deferred.callback(True)
777
        
778
        deferred_list = defer.DeferredList([
779
            self.poll_x11_devices(), 
780
            self.poll_midi_devices(), 
781
            self.poll_xvideo_extension(),
782
            self.poll_camera_devices()
783
            ])
784
        self._poll_jackd() # does not return a deferred for now... called in a looping call.
785
        deferred_list.addCallback(_callback)
786
        return deferred
787

    
788
    def send_invite(self):
789
        """
790
        Does the flight check. If OK, send an INVITE.
791
        """
792
        contact = self.address_book.get_currently_selected_contact()
793
        if contact is None:
794
            dialogs.ErrorDialog.create(_("You must select a contact to invite."), parent=self.gui.main_window)
795
            return  # important
796
        else:
797
            ip = contact["address"]
798
            
799
        def _check_cb(result):
800
            #TODO: use the Deferred it will return
801
            if result:
802
                self.prepare_before_rtp_stream()
803
                msg = {
804
                    "msg":"INVITE",
805
                    "protocol": self.protocol_version,
806
                    "sid":0, 
807
                    "please_send_to_port": self.config.negotiation_port, # FIXME: rename to listening_port
808
                    }
809
                msg.update(self._get_local_config_message_items())
810
                port = self.config.negotiation_port
811
                
812
                def _on_connected(proto):
813
                    self.gui._schedule_inviting_timeout_delayed()
814
                    self.client.send(msg)
815
                    return proto
816
                def _on_error(reason):
817
                    #FIXME: do we need this error dialog?
818
                    exc_type = type(reason.value)
819
                    if exc_type is error.ConnectionRefusedError:
820
                        msg = _("Could not invite contact %(name)s. \n\nScenic is not listening on port %(port)d of host %(ip)s.") % {"ip": ip, "name": contact["name"], "port": port}
821
                    elif exc_type is error.ConnectError:
822
                        msg = _("Could not invite contact %(name)s. \n\nHost %(ip)s is unreachable.") % {"ip": ip, "name": contact["name"]}
823
                    elif exc_type is error.NoRouteError:
824
                        msg = _("Could not invite contact %(name)s. \n\nHost %(ip)s is unreachable.") % {"ip": ip, "name": contact["name"]}
825
                    else:
826
                        msg = _("Could not invite contact %(name)s. \n\nError trying to connect to %(ip)s:%(port)s:\n %(reason)s") % {"ip": ip, "name": contact["name"], "port": port, "reason": reason.value}
827
                    print(msg)
828
                    self.gui.hide_calling_dialog()
829
                    dialogs.ErrorDialog.create(msg, parent=self.gui.main_window)
830
                    return None
831
                   
832
                print("sending %s to %s:%s" % (msg, ip, port))
833
                deferred = self.client.connect(ip, port)
834
                deferred.addCallback(_on_connected).addErrback(_on_error)
835
                self.gui.show_calling_dialog()
836
                # window will be hidden when we receive ACCEPT or REFUSE, or when we cancel
837
            else:
838
                print("Cannot send INVITE.")
839

    
840
        check_deferred = self.check_if_ready_to_stream(role="offerer")
841
        check_deferred.addCallback(_check_cb)
842
    
843
    def send_accept(self, addr):
844
        # UPDATE config once we accept the invitie
845
        #TODO: use the Deferred it will return
846
        self.prepare_before_rtp_stream()
847
        msg = {
848
            "msg":"ACCEPT", 
849
            "protocol": self.protocol_version,
850
            "sid":0,
851
            }
852
        msg.update(self._get_local_config_message_items())
853
        self.client.send(msg)
854

    
855
    def get_last_message_sent(self):
856
        return self.client.last_message_sent
857

    
858
    def get_last_message_received(self):
859
        return self.server.last_message_received
860
    
861
    def send_ack(self):
862
        """
863
        Sends ACK.
864
        INVITE, ACCEPT, ACK
865
        """
866
        self.client.send({"msg":"ACK", "sid":0})
867

    
868
    def send_bye(self):
869
        """
870
        Sends BYE
871
        BYE stops the streaming on the remote host.
872
        """
873
        self.client.send({"msg":"BYE", "sid":0})
874
    
875
    def send_cancel_and_disconnect(self, reason=""):
876
        """
877
        Sends CANCEL
878
        CANCEL cancels the invite on the remote host.
879
        """
880
        #TODO: add reason argument.
881
        #CANCEL_REASON_TIMEOUT = "timed out"
882
        #CANCEL_REASON_CANCELLED = "cancelled"
883
        if self.client.is_connected():
884
            self.client.send({"msg":"CANCEL", "reason": reason, "sid":0})
885
            self.client.disconnect()
886
        self.cleanup_after_rtp_stream()
887
    
888
    def send_refuse_and_disconnect(self):
889
        """
890
        Sends REFUSE 
891
        REFUSE tells the offerer we can't have a session.
892
        """
893
        self.client.send({"msg":"REFUSE", "sid":0})
894
        self.client.disconnect()
895

    
896
    # ------------------- streaming events handlers ----------------
897
    
898
    def on_streamer_state_changed(self, streamer, new_state):
899
        """
900
        Slot for scenic.streamer.StreamerManager.state_changed_signal
901
        """
902
        if new_state in [process.STATE_STOPPED]:
903
            if not self.got_bye:
904
                # got_bye means our peer sent us a BYE, so we shouldn't send one back 
905
                print("Local StreamerManager stopped. Sending BYE")
906
                self.send_bye()
907
            
908
    #def on_connection_error(self, err, msg):
909
    #    """
910
    #    @param err: Exception message.
911
    #    @param msg: Legible message.
912
    #    """
913
    #    self.gui.hide_calling_dialog()
914
    #    text = _("Connection error: %(message)s\n%(error)s") % {"error": err, "message": msg}
915
    #    dialogs.ErrorDialog.create(text, parent=self.gui.main_window)
916