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 |
|
