Package flumotion :: Package component :: Package consumers :: Package httpstreamer :: Module wizard_gtk
[hide private]

Source Code for Module flumotion.component.consumers.httpstreamer.wizard_gtk

  1  # -*- Mode: Python -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3   
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007,2008,2009 Fluendo, S.L. 
  6  # Copyright (C) 2010,2011 Flumotion Services, S.A. 
  7  # All rights reserved. 
  8  # 
  9  # This file may be distributed and/or modified under the terms of 
 10  # the GNU Lesser General Public License version 2.1 as published by 
 11  # the Free Software Foundation. 
 12  # This file is distributed without any warranty; without even the implied 
 13  # warranty of merchantability or fitness for a particular purpose. 
 14  # See "LICENSE.LGPL" in the source distribution for more information. 
 15  # 
 16  # Headers in this file shall remain intact. 
 17   
 18  """HTTP wizard integration 
 19   
 20  This provides a step which you can chose: 
 21  - http port 
 22  - bandwidth/client limit 
 23  - mount point (eg, the url it will be accessed as) 
 24  - burst on connect 
 25  - cortado java applet 
 26   
 27  A component of type 'http-streamer' will always be created. 
 28  In addition, if you include the java applet, a 'porter' and 
 29  'http-server' will be included to share the port between the streamer 
 30  and the server and to serve an html file plus the java applet itself. 
 31  On the http-server the applet will be provided with help of a plug. 
 32  """ 
 33   
 34  import gettext 
 35  import re 
 36  import os 
 37   
 38  import gobject 
 39  from kiwi.utils import gsignal 
 40  import gtk 
 41  from twisted.internet import defer 
 42  from zope.interface import implements 
 43   
 44  from flumotion.admin.assistant.interfaces import IConsumerPlugin 
 45  from flumotion.admin.assistant.models import Consumer, Porter 
 46  from flumotion.admin.gtk.basesteps import ConsumerStep 
 47  from flumotion.configure import configure 
 48  from flumotion.common import errors, log, messages 
 49  from flumotion.common.i18n import N_, gettexter, ngettext 
 50   
 51  __version__ = "$Rev$" 
 52  _ = gettext.gettext 
 53  T_ = gettexter() 
 54   
 55   
56 -class HTTPStreamer(Consumer):
57 """I am a model representing the configuration file for a 58 HTTP streamer component. 59 @ivar has_client_limit: If a client limit was set 60 @ivar client_limit: The client limit 61 @ivar has_bandwidth_limit: If a bandwidth limit was set 62 @ivar bandwidth_limit: The bandwidth limit 63 @ivar set_hostname: If a hostname was set 64 @ivar hostname: the hostname this will be streamed on 65 @ivar port: The port this server will be listening to 66 """ 67 componentType = 'http-streamer' 68 requiresPorter = True 69 prefix = 'http' 70
71 - def __init__(self):
72 super(HTTPStreamer, self).__init__() 73 74 self.setPorter( 75 Porter(worker=None, port=configure.defaultHTTPStreamPort)) 76 77 self.has_client_limit = False 78 self.client_limit = 1000 79 self.has_bandwidth_limit = False 80 self.bandwidth_limit = 500.0 81 self.set_hostname = False 82 self.hostname = '' 83 self.port = None 84 85 self.properties.burst_on_connect = False
86 87 # Public 88
89 - def getURL(self):
90 """Fetch the url to this stream 91 @returns: the url 92 """ 93 return 'http://%s:%d%s' % ( 94 self.getHostname(), 95 self.getPorter().getPort(), 96 self.properties.mount_point)
97
98 - def getHostname(self):
99 """Fetch the hostname this stream will be published on 100 @returns: the hostname 101 """ 102 return self.hostname
103
104 - def setData(self, model):
105 """ 106 Sets the data from another model so we can reuse it. 107 108 @param model : model to get the data from 109 @type model : L{HTTPStreamer} 110 """ 111 self.has_client_limit = model.has_client_limit 112 self.has_bandwidth_limit = model.has_bandwidth_limit 113 self.client_limit = model.client_limit 114 self.bandwidth_limit = model.bandwidth_limit 115 self.set_hostname = model.set_hostname 116 self.hostname = model.hostname 117 self.properties.burst_on_connect = model.properties.burst_on_connect 118 self.port = model.port
119 120 # Component 121
122 - def getPorter(self):
123 """ 124 Obtains this streamer's porter model. 125 """ 126 porter = Consumer.getPorter(self) 127 porter.worker = self.worker 128 if self.port: 129 porter.properties.port = self.port 130 return porter
131
132 - def getProperties(self):
133 properties = super(HTTPStreamer, self).getProperties() 134 if self.has_bandwidth_limit: 135 properties.bandwidth_limit = int(self.bandwidth_limit * 1e6) 136 if self.has_client_limit: 137 properties.client_limit = self.client_limit 138 139 porter = self.getPorter() 140 hostname = self.getHostname() 141 if hostname and self.set_hostname: 142 properties.hostname = hostname 143 properties.porter_socket_path = porter.getSocketPath() 144 properties.porter_username = porter.getUsername() 145 properties.porter_password = porter.getPassword() 146 properties.type = 'slave' 147 # FIXME: Try to maintain the port empty when we are slave. Needed 148 # for now as the adminwindow tab shows the URL based on this property. 149 properties.port = self.port or self.getPorter().getProperties().port 150 151 return properties
152 153
154 -class HTTPSpecificStep(ConsumerStep):
155 """I am a step of the configuration wizard which allows you 156 to configure a stream to be served over HTTP. 157 """ 158 section = _('Consumption') 159 gladeFile = os.path.join(os.path.dirname(os.path.abspath(__file__)), 160 'wizard.glade') 161
162 - def __init__(self, wizard):
163 self.model = HTTPStreamer() 164 ConsumerStep.__init__(self, wizard)
165
166 - def updateModel(self, model):
167 """ 168 There is a previous httpstreamer step from where the data can be copied 169 It will be copied to the actual model and the advanced 170 tab would be hidden. 171 172 @param model: The previous model we are going to copy. 173 @type model: L{HTTPStreamer} 174 """ 175 self.model.setData(model) 176 self.expander.set_expanded(False) 177 self._proxy2.set_model(self.model)
178 179 # ConsumerStep 180
181 - def getConsumerModel(self):
182 return self.model
183
184 - def getServerConsumers(self):
185 for line in self.plugarea.getEnabledLines(): 186 yield line.getConsumer(self.model, 187 self.wizard.getScenario().getAudioProducer(self.wizard), 188 self.wizard.getScenario().getVideoProducer(self.wizard))
189 190 # WizardStep 191
192 - def setup(self):
193 self.mount_point.data_type = str 194 self.bandwidth_limit.data_type = float 195 self.burst_on_connect.data_type = bool 196 self.client_limit.data_type = int 197 self.port.data_type = int 198 self.hostname.data_type = str 199 200 self.model.properties.mount_point = self._getDefaultMountPath() 201 self._proxy1 = self.add_proxy(self.model.properties, 202 ['mount_point', 'burst_on_connect']) 203 self._proxy2 = self.add_proxy( 204 self.model, ['has_client_limit', 205 'has_bandwidth_limit', 206 'client_limit', 207 'bandwidth_limit', 208 'set_hostname', 209 'hostname', 210 'port']) 211 212 self.client_limit.set_sensitive(self.model.has_client_limit) 213 self.bandwidth_limit.set_sensitive(self.model.has_bandwidth_limit) 214 self.hostname.set_sensitive(self.model.set_hostname) 215 216 self.port.connect('changed', self.on_port_changed) 217 self.mount_point.connect('changed', self.on_mount_point_changed)
218
219 - def workerChanged(self, worker):
220 self.model.worker = worker 221 d = self._runChecks() 222 d.addCallback(self._populatePlugins) 223 return d
224
225 - def getNext(self):
226 227 def setModel(next): 228 if next and next.model.componentType == self.model.componentType: 229 next.updateModel(self.model) 230 return next
231 d = defer.maybeDeferred(ConsumerStep.getNext, self) 232 d.addCallback(setModel) 233 return d
234 235 # Private 236
237 - def _getDefaultMountPath(self):
238 encodingStep = self.wizard.getStep('Encoding') 239 return '/%s-%s/' % (str(encodingStep.getMuxerFormat()), 240 self.getConsumerType(), )
241
242 - def _suggestMountPoint(self, mountPoint):
243 # FIXME: Generalise this method and use the same in f.a.a.save module. 244 # Resolve naming conflicts, using a simple algorithm 245 # First, find all the trailing digits, for instance in 246 # 'audio-producer42' -> '42' 247 mountPoint = mountPoint.rstrip('/') 248 249 pattern = re.compile('(\d*$)') 250 match = pattern.search(mountPoint) 251 trailingDigit = match.group() 252 253 # Now if we had a digit in the end, convert it to 254 # a number and increase it by one and remove the trailing 255 # digits the existing component name 256 if trailingDigit: 257 digit = int(trailingDigit) + 1 258 mountPoint = mountPoint[:-len(trailingDigit)] 259 # No number in the end, use 2 the first one so we end up 260 # with 'audio-producer' and 'audio-producer2' in case of 261 # a simple conflict 262 else: 263 digit = 2 264 return mountPoint + str(digit) + '/'
265
266 - def _populatePlugins(self, canPopulate):
267 if not canPopulate: 268 return 269 270 self.plugarea.clean() 271 272 def gotEntries(entries): 273 log.debug('httpwizard', 'got %r' % (entries, )) 274 for entry in entries: 275 if not self._canAddPlug(entry): 276 continue 277 278 def response(factory, entry): 279 # FIXME: verify that factory implements IHTTPConsumerPlugin 280 plugin = factory(self.wizard) 281 if hasattr(plugin, 'workerChanged'): 282 d = plugin.workerChanged(self.worker) 283 284 def cb(found, plugin, entry): 285 self._addPlug(plugin.getPlugWizard( 286 N_(entry.description)), found)
287 d.addCallback(cb, plugin, entry) 288 else: 289 self._addPlug(plugin.getPlugWizard( 290 N_(entry.description)), True) 291 d = self.wizard.getWizardPlugEntry(entry.componentType) 292 d.addCallback(response, entry) 293 294 d = self.wizard.getWizardEntries(wizardTypes=['http-consumer']) 295 d.addCallbacks(gotEntries) 296
297 - def _canAddPlug(self, entry):
298 # This function filters out entries which are 299 # not matching the accepted media types of the entry 300 muxerTypes = [] 301 audioTypes = [] 302 videoTypes = [] 303 for mediaType in entry.getAcceptedMediaTypes(): 304 kind, name = mediaType.split(':', 1) 305 if kind == 'muxer': 306 muxerTypes.append(name) 307 elif kind == 'video': 308 videoTypes.append(name) 309 elif kind == 'audio': 310 audioTypes.append(name) 311 else: 312 raise AssertionError 313 314 encoding_step = self.wizard.getStep('Encoding') 315 if encoding_step.getMuxerFormat() not in muxerTypes: 316 return False 317 318 audioFormat = encoding_step.getAudioFormat() 319 videoFormat = encoding_step.getVideoFormat() 320 if ((audioFormat and audioFormat not in audioTypes) or 321 (videoFormat and videoFormat not in videoTypes)): 322 return False 323 324 return True
325
326 - def _addPlug(self, plugin, enabled):
327 plugin.setEnabled(enabled) 328 self.plugarea.addLine(plugin)
329
330 - def _runChecks(self):
331 self.wizard.waitForTask('http streamer check') 332 333 def hostnameErrback(failure): 334 failure.trap(errors.RemoteRunError) 335 self.wizard.taskFinished(blockNext=True) 336 return False
337 338 def gotHostname(hostname): 339 self.model.hostname = hostname 340 self._proxy2.update('hostname') 341 self.wizard.taskFinished() 342 return True 343 344 def getHostname(result): 345 if not result: 346 return False 347 348 d = self.wizard.runInWorker( 349 self.worker, 'flumotion.worker.checks.http', 350 'runHTTPStreamerChecks') 351 d.addCallback(gotHostname) 352 d.addErrback(hostnameErrback) 353 return d 354 355 def checkImport(elements): 356 if elements: 357 self.wizard.taskFinished(blockNext=True) 358 return False 359 360 d = self.wizard.requireImport( 361 self.worker, 'twisted.web', projectName='Twisted project', 362 projectURL='http://www.twistedmatrix.com/') 363 d.addCallback(getHostname) 364 return d 365 366 # first check elements 367 d = self.wizard.requireElements(self.worker, 'multifdsink') 368 d.addCallback(checkImport) 369 return d 370
371 - def _checkMountPoint(self, port=None, worker=None, 372 mount_point=None, need_fix=False):
373 """ 374 Checks whether the provided mount point is available with the 375 current configuration (port, worker). It can provide a valid 376 mountpoint if it is required with need_fix=True. 377 378 @param port : The port the streamer is going to be listening. 379 @type port : int 380 @param worker : The worker the streamer will be running. 381 @type worker : str 382 @param mount_point : The desired mount point. 383 @type mount_point : str 384 @param need_fix : Whether the method should search for a valid 385 mount_point if the provided one is not. 386 @type need_fix : bool 387 388 @returns : True if the mount_point can be used, False if it is in use. 389 @rtype : bool 390 """ 391 self.wizard.clear_msg('http-streamer-mountpoint') 392 393 port = port or self.model.port 394 worker = worker or self.model.worker 395 mount_point = mount_point or self.model.properties.mount_point 396 397 self.wizard.waitForTask('http-streamer-mountpoint') 398 399 if self.wizard.addMountPoint(worker, port, mount_point, 400 self.getConsumerType()): 401 self.wizard.taskFinished() 402 return True 403 else: 404 if need_fix: 405 while not self.wizard.addMountPoint(worker, port, 406 mount_point, 407 self.getConsumerType()): 408 mount_point=self._suggestMountPoint(mount_point) 409 410 self.model.properties.mount_point = mount_point 411 self._proxy1.update('mount_point') 412 self.wizard.taskFinished() 413 return True 414 415 message = messages.Error(T_(N_( 416 "The mount point %s is already being used for worker %s and " 417 "port %s. Please correct this to be able to go forward."), 418 mount_point, worker, port)) 419 message.id = 'http-streamer-mountpoint' 420 self.wizard.add_msg(message) 421 self.wizard.taskFinished(True) 422 return False
423 424 # Callbacks 425
426 - def on_mount_point_changed(self, entry):
427 if not entry.get_text(): 428 self.wizard.clear_msg('http-streamer-mountpoint') 429 message = messages.Error(T_(N_( 430 "Mountpoint cannot be left empty.\n" 431 "Fill the text field with a correct mount point to" 432 "be able to go forward."))) 433 message.id = 'http-streamer-mountpoint' 434 self.wizard.add_msg(message) 435 self.wizard.blockNext(True) 436 else: 437 self._checkMountPoint(mount_point=entry.get_text())
438
439 - def on_has_client_limit_toggled(self, cb):
440 self.client_limit.set_sensitive(cb.get_active())
441
442 - def on_has_bandwidth_limit_toggled(self, cb):
443 self.bandwidth_limit.set_sensitive(cb.get_active())
444
445 - def on_set_hostname__toggled(self, cb):
446 self.hostname.set_sensitive(cb.get_active())
447
448 - def on_port_changed(self, widget):
449 if widget.get_text().isdigit(): 450 self._checkMountPoint(port=int(widget.get_text()))
451 452
453 -class HTTPBothStep(HTTPSpecificStep):
454 name = 'HTTPStreamerBoth' 455 title = _('HTTP Streamer (Audio and Video)') 456 sidebarName = _('HTTP Audio/Video') 457 docSection = 'help-configuration-assistant-http-streaming-both' 458 docAnchor = '' 459 docVersion = 'local' 460 461 # ConsumerStep 462
463 - def getConsumerType(self):
464 return 'audio-video'
465 466
467 -class HTTPAudioStep(HTTPSpecificStep):
468 name = 'HTTPStreamerAudio' 469 title = _('HTTP Streamer (Audio Only)') 470 sidebarName = _('HTTP Audio') 471 docSection = 'help-configuration-assistant-http-streaming-audio-only' 472 docAnchor = '' 473 docVersion = 'local' 474 475 # ConsumerStep 476
477 - def getConsumerType(self):
478 return 'audio'
479 480
481 -class HTTPVideoStep(HTTPSpecificStep):
482 name = 'HTTPStreamerVideo' 483 title = _('HTTP Streamer (Video Only)') 484 sidebarName = _('HTTP Video') 485 docSection = 'help-configuration-assistant-http-streaming-video-only' 486 docAnchor = '' 487 docVersion = 'local' 488 489 # ConsumerStep 490
491 - def getConsumerType(self):
492 return 'video'
493 494
495 -class HTTPGenericStep(HTTPSpecificStep):
496 name = 'HTTPStreamerGeneric' 497 title = _('HTTP Streamer (Generic)') 498 sidebarName = _('HTTP Generic') 499 docSection = 'help-configuration-assistant-http-streaming-generic' 500 docAnchor = '' 501 docVersion = 'local' 502
503 - def __init__(self, wizard, type):
504 self._consumertype = type 505 HTTPSpecificStep.__init__(self, wizard)
506 507 # ConsumerStep 508
509 - def getConsumerType(self):
510 return self._consumertype
511 512
513 -class HTTPStreamerWizardPlugin(object):
514 implements(IConsumerPlugin) 515
516 - def __init__(self, wizard):
517 self.wizard = wizard
518
519 - def getConsumptionStep(self, type):
520 if type == 'video': 521 return HTTPVideoStep(self.wizard) 522 elif type == 'audio': 523 return HTTPAudioStep(self.wizard) 524 elif type == 'audio-video': 525 return HTTPBothStep(self.wizard) 526 else: 527 return HTTPGenericStep(self.wizard, type)
528