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