Package flumotion :: Package component :: Package producers :: Package playlist :: Module playlist
[hide private]

Source Code for Module flumotion.component.producers.playlist.playlist

  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 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  import time 
 23   
 24  import gst 
 25  from twisted.internet import defer, reactor 
 26   
 27  from flumotion.common import messages, fxml, gstreamer, documentation 
 28  from flumotion.common.i18n import N_, gettexter 
 29  from flumotion.component import feedcomponent 
 30  from flumotion.component.base import watcher 
 31   
 32  import smartscale 
 33  import singledecodebin 
 34  import playlistparser 
 35   
 36  __version__ = "$Rev: 8791 $" 
 37  T_ = gettexter() 
 38   
 39   
40 -def _tsToString(ts):
41 """ 42 Return a string in local time from a gstreamer timestamp value 43 """ 44 return time.ctime(ts/gst.SECOND)
45 46
47 -def videotest_gnl_src(name, start, duration, priority, pattern=None):
48 src = gst.element_factory_make('videotestsrc') 49 if pattern: 50 src.props.pattern = pattern 51 else: 52 # Set videotestsrc to all black. 53 src.props.pattern = 2 54 gnlsrc = gst.element_factory_make('gnlsource', name) 55 gnlsrc.props.start = start 56 gnlsrc.props.duration = duration 57 gnlsrc.props.media_start = 0 58 gnlsrc.props.media_duration = duration 59 gnlsrc.props.priority = priority 60 gnlsrc.add(src) 61 62 return gnlsrc
63 64
65 -def audiotest_gnl_src(name, start, duration, priority, wave=None):
66 src = gst.element_factory_make('audiotestsrc') 67 if wave: 68 src.props.wave = wave 69 else: 70 # Set audiotestsrc to use silence. 71 src.props.wave = 4 72 gnlsrc = gst.element_factory_make('gnlsource', name) 73 gnlsrc.props.start = start 74 gnlsrc.props.duration = duration 75 gnlsrc.props.media_start = 0 76 gnlsrc.props.media_duration = duration 77 gnlsrc.props.priority = priority 78 gnlsrc.add(src) 79 80 return gnlsrc
81 82
83 -def file_gnl_src(name, uri, caps, start, duration, offset, priority):
84 src = singledecodebin.SingleDecodeBin(caps, uri) 85 gnlsrc = gst.element_factory_make('gnlsource', name) 86 gnlsrc.props.start = start 87 gnlsrc.props.duration = duration 88 gnlsrc.props.media_start = offset 89 gnlsrc.props.media_duration = duration 90 gnlsrc.props.priority = priority 91 gnlsrc.props.caps = caps 92 gnlsrc.add(src) 93 94 return gnlsrc
95 96
97 -class PlaylistProducerMedium(feedcomponent.FeedComponentMedium):
98
99 - def __init__(self, comp):
101
102 - def remote_add_playlist(self, data):
103 self.comp.addPlaylist(data)
104 105
106 -class PlaylistProducer(feedcomponent.FeedComponent):
107 logCategory = 'playlist-prod' 108 componentMediumClass = PlaylistProducerMedium 109
110 - def init(self):
111 self.basetime = -1 112 113 self._hasAudio = True 114 self._hasVideo = True 115 116 # The gnlcompositions for audio and video 117 self.videocomp = None 118 self.audiocomp = None 119 120 self.videocaps = gst.Caps("video/x-raw-yuv;video/x-raw-rgb") 121 self.audiocaps = gst.Caps("audio/x-raw-int;audio/x-raw-float") 122 123 self._vsrcs = {} # { PlaylistItem -> gnlsource } 124 self._asrcs = {} # { PlaylistItem -> gnlsource } 125 126 self.uiState.addListKey("playlist")
127
128 - def _buildAudioPipeline(self, pipeline, src):
129 audiorate = gst.element_factory_make("audiorate") 130 audioconvert = gst.element_factory_make('audioconvert') 131 resampler = 'audioresample' 132 if gstreamer.element_factory_exists('legacyresample'): 133 resampler = 'legacyresample' 134 audioresample = gst.element_factory_make(resampler) 135 outcaps = gst.Caps( 136 "audio/x-raw-int,channels=%d,rate=%d,width=16,depth=16" % 137 (self._channels, self._samplerate)) 138 139 capsfilter = gst.element_factory_make("capsfilter") 140 capsfilter.props.caps = outcaps 141 142 pipeline.add(audiorate, audioconvert, audioresample, capsfilter) 143 src.link(audioconvert) 144 audioconvert.link(audioresample) 145 audioresample.link(audiorate) 146 audiorate.link(capsfilter) 147 148 return capsfilter.get_pad('src')
149
150 - def _buildVideoPipeline(self, pipeline, src):
151 outcaps = gst.Caps( 152 "video/x-raw-yuv,width=%d,height=%d,framerate=%d/%d," 153 "pixel-aspect-ratio=1/1" % 154 (self._width, self._height, self._framerate[0], 155 self._framerate[1])) 156 157 cspace = gst.element_factory_make("ffmpegcolorspace") 158 scaler = smartscale.SmartVideoScale() 159 scaler.set_caps(outcaps) 160 videorate = gst.element_factory_make("videorate") 161 capsfilter = gst.element_factory_make("capsfilter") 162 capsfilter.props.caps = outcaps 163 164 pipeline.add(cspace, scaler, videorate, capsfilter) 165 166 src.link(cspace) 167 cspace.link(scaler) 168 scaler.link(videorate) 169 videorate.link(capsfilter) 170 return capsfilter.get_pad('src')
171
172 - def _buildPipeline(self):
173 pipeline = gst.Pipeline() 174 175 for mediatype in ['audio', 'video']: 176 if (mediatype == 'audio' and not self._hasAudio) or ( 177 mediatype == 'video' and not self._hasVideo): 178 continue 179 180 # For each of audio, video, we build a pipeline that looks roughly 181 # like: 182 # 183 # gnlcomposition ! identity sync=true ! 184 # identity single-segment=true ! audio/video-elements ! sink 185 186 composition = gst.element_factory_make("gnlcomposition", 187 mediatype + "-composition") 188 189 segmentidentity = gst.element_factory_make("identity") 190 segmentidentity.set_property("single-segment", True) 191 segmentidentity.set_property("silent", True) 192 syncidentity = gst.element_factory_make("identity") 193 syncidentity.set_property("silent", True) 194 syncidentity.set_property("sync", True) 195 196 pipeline.add(composition, segmentidentity, syncidentity) 197 198 def _padAddedCb(element, pad, target): 199 self.debug("Pad added, linking") 200 pad.link(target)
201 composition.connect('pad-added', _padAddedCb, 202 syncidentity.get_pad("sink")) 203 syncidentity.link(segmentidentity) 204 205 if mediatype == 'audio': 206 self.audiocomp = composition 207 srcpad = self._buildAudioPipeline(pipeline, segmentidentity) 208 else: 209 self.videocomp = composition 210 srcpad = self._buildVideoPipeline(pipeline, segmentidentity) 211 212 feedername = self.feeders[mediatype].elementName 213 #FIXME: rethink how we expose the feeder pipeline strings 214 feederchunk = \ 215 feedcomponent.ParseLaunchComponent.FEEDER_TMPL \ 216 % {'name': feedername} 217 218 binstr = "bin.("+feederchunk+" )" 219 self.debug("Parse for media composition is %s", binstr) 220 221 bin = gst.parse_launch(binstr) 222 pad = bin.find_unconnected_pad(gst.PAD_SINK) 223 ghostpad = gst.GhostPad(mediatype + "-feederpad", pad) 224 bin.add_pad(ghostpad) 225 226 pipeline.add(bin) 227 srcpad.link(ghostpad) 228 229 return pipeline
230
231 - def _createDefaultSources(self, properties):
232 if self._hasVideo: 233 vsrc = videotest_gnl_src("videotestdefault", 0, 2**63 - 1, 234 2**31 - 1, properties.get('video-pattern', None)) 235 self.videocomp.add(vsrc) 236 237 if self._hasAudio: 238 asrc = audiotest_gnl_src("videotestdefault", 0, 2**63 - 1, 239 2**31 - 1, properties.get('audio-wave', None)) 240 self.audiocomp.add(asrc)
241
242 - def set_master_clock(self, ip, port, base_time):
243 raise NotImplementedError("Playlist producer doesn't support slaving")
244
245 - def provide_master_clock(self, port):
246 # Most of this copied from feedcomponent010, but changed in various 247 # ways. Refactor the base class? 248 if self.medium: 249 ip = self.medium.getIP() 250 else: 251 ip = "127.0.0.1" 252 253 clock = self.pipeline.get_clock() 254 self.clock_provider = gst.NetTimeProvider(clock, None, port) 255 # small window here but that's ok 256 self.clock_provider.set_property('active', False) 257 258 self._master_clock_info = (ip, port, self.basetime) 259 260 return defer.succeed(self._master_clock_info)
261
262 - def get_master_clock(self):
263 return self._master_clock_info
264
265 - def _setupClock(self, pipeline):
266 # Configure our pipeline to use a known basetime and clock. 267 clock = gst.system_clock_obtain() 268 clock.set_property('clock-type', 'realtime') 269 # It doesn't matter too much what this basetime is, so long as we know 270 # the value. 271 self.basetime = clock.get_time() 272 273 # We force usage of the system clock. 274 pipeline.use_clock(clock) 275 # Now we disable default basetime distribution 276 pipeline.set_new_stream_time(gst.CLOCK_TIME_NONE) 277 # And we choose our own basetime... 278 self.debug("Setting basetime of %d", self.basetime) 279 pipeline.set_base_time(self.basetime)
280
281 - def timeReport(self):
282 ts = self.pipeline.get_clock().get_time() 283 self.debug("Pipeline clock is now at %d -> %s", ts, _tsToString(ts)) 284 reactor.callLater(10, self.timeReport)
285
286 - def getCurrentPosition(self):
287 return self.pipeline.query_position(gst.FORMAT_TIME)[0]
288
289 - def scheduleItem(self, item):
290 """ 291 Schedule a given playlist item in our playback compositions. 292 """ 293 start = item.timestamp - self.basetime 294 self.debug("Starting item %s at %d seconds from start: %s", item.uri, 295 start/gst.SECOND, _tsToString(item.timestamp)) 296 297 # If we schedule things to start before the current pipeline position, 298 # gnonlin will adjust this to start now. However, it does this 299 # separately for audio and video, so we start from different points, 300 # thus we're out of sync. 301 # So, always start slightly in the future... 5 seconds seems to work 302 # fine in practice. 303 now = self.getCurrentPosition() 304 neareststarttime = now + 5 * gst.SECOND 305 306 if start < neareststarttime: 307 if start + item.duration < neareststarttime: 308 self.debug("Item too late; skipping entirely") 309 return False 310 else: 311 change = neareststarttime - start 312 self.debug("Starting item with offset %d", change) 313 item.duration -= change 314 item.offset += change 315 start = neareststarttime 316 317 end = start + item.duration 318 timeuntilend = end - now 319 # After the end time, remove this item from the composition, otherwise 320 # it will continue to use huge gobs of memory and lots of threads. 321 reactor.callLater(timeuntilend/gst.SECOND + 5, 322 self.unscheduleItem, item) 323 324 if self._hasVideo and item.hasVideo: 325 self.debug("Adding video source with start %d, duration %d, " 326 "offset %d", start, item.duration, item.offset) 327 vsrc = file_gnl_src(None, item.uri, self.videocaps, 328 start, item.duration, item.offset, 0) 329 self.videocomp.add(vsrc) 330 self._vsrcs[item] = vsrc 331 if self._hasAudio and item.hasAudio: 332 self.debug("Adding audio source with start %d, duration %d, " 333 "offset %d", start, item.duration, item.offset) 334 asrc = file_gnl_src(None, item.uri, self.audiocaps, 335 start, item.duration, item.offset, 0) 336 self.audiocomp.add(asrc) 337 self._asrcs[item] = asrc 338 self.debug("Done scheduling: start at %s, end at %s", 339 _tsToString(start + self.basetime), 340 _tsToString(start + self.basetime + item.duration)) 341 342 self.uiState.append("playlist", (item.timestamp, 343 item.uri, 344 item.duration, 345 item.offset, 346 item.hasAudio, 347 item.hasVideo)) 348 return True
349
350 - def unscheduleItem(self, item):
351 self.debug("Unscheduling item at uri %s", item.uri) 352 if self._hasVideo and item.hasVideo and item in self._vsrcs: 353 vsrc = self._vsrcs.pop(item) 354 self.videocomp.remove(vsrc) 355 vsrc.set_state(gst.STATE_NULL) 356 if self._hasAudio and item.hasAudio and item in self._asrcs: 357 asrc = self._asrcs.pop(item) 358 self.audiocomp.remove(asrc) 359 asrc.set_state(gst.STATE_NULL) 360 for entry in self.uiState.get("playlist"): 361 if entry[0] == item.timestamp: 362 self.uiState.remove("playlist", entry)
363
364 - def adjustItemScheduling(self, item):
365 if self._hasVideo and item.hasVideo: 366 vsrc = self._vsrcs[item] 367 vsrc.props.start = item.timestamp - self.basetime 368 vsrc.props.duration = item.duration 369 vsrc.props.media_duration = item.duration 370 if self._hasAudio and item.hasAudio: 371 asrc = self._asrcs[item] 372 asrc.props.start = item.timestamp - self.basetime 373 asrc.props.duration = item.duration 374 asrc.props.media_duration = item.duration
375
376 - def addPlaylist(self, data):
377 self.playlistparser.parseData(data)
378
379 - def create_pipeline(self):
380 props = self.config['properties'] 381 382 self._playlistfile = props.get('playlist', None) 383 self._playlistdirectory = props.get('playlist-directory', None) 384 self._baseDirectory = props.get('base-directory', None) 385 386 self._width = props.get('width', 320) 387 self._height = props.get('height', 240) 388 self._framerate = props.get('framerate', (15, 1)) 389 self._samplerate = props.get('samplerate', 44100) 390 self._channels = props.get('channels', 2) 391 392 self._hasAudio = props.get('audio', True) 393 self._hasVideo = props.get('video', True) 394 395 pipeline = self._buildPipeline() 396 self._setupClock(pipeline) 397 398 self._createDefaultSources(props) 399 400 return pipeline
401
402 - def _watchDirectory(self, dir):
403 self.debug("Watching directory %s", dir) 404 self._filesAdded = {} 405 406 self._directoryWatcher = watcher.DirectoryWatcher(dir) 407 self._directoryWatcher.subscribe(fileChanged=self._watchFileChanged, 408 fileDeleted=self._watchFileDeleted) 409 410 # in the start call watcher should find all the existing 411 # files, so we block discovery while the watcher starts 412 self.playlistparser.blockDiscovery() 413 try: 414 self._directoryWatcher.start() 415 finally: 416 self.playlistparser.unblockDiscovery()
417
418 - def _watchFileDeleted(self, file):
419 self.debug("File deleted: %s", file) 420 if file in self._filesAdded: 421 self.playlistparser.playlist.removeItems(file) 422 self._filesAdded.pop(file) 423 424 self._cleanMessage(file)
425
426 - def _cleanMessage(self, file):
427 # There's no message removal API! We have to do this instead. Ick? 428 msgid = ("playlist-parse-error", file) 429 for m in self.state.get('messages'): 430 if m.id == msgid: 431 self.state.remove('messages', m)
432
433 - def _watchFileChanged(self, file):
434 self.debug("File changed: %s", file) 435 if file in self._filesAdded: 436 self.debug("Removing existing items for changed playlist") 437 self.playlistparser.playlist.removeItems(file) 438 439 self._filesAdded[file] = None 440 self._cleanMessage(file) 441 try: 442 self.debug("Parsing file: %s", file) 443 self.playlistparser.parseFile(file, piid=file) 444 except fxml.ParserError, e: 445 self.warning("Failed to parse playlist file: %r", e) 446 # Since this isn't done directly via the remote method, add a 447 # message so people can find out that it failed... 448 # Use a tuple including the filename to identify the warning, so we 449 # can add/remove one per file 450 msgid = ("playlist-parse-error", file) 451 self.addMessage( 452 messages.Warning(T_(N_( 453 "Failed to parse a playlist from file %s: %s" % 454 (file, e))), mid=msgid))
455
456 - def do_check(self):
457 458 def check_gnl(element): 459 exists = gstreamer.element_factory_exists(element) 460 if not exists: 461 m = messages.Error(T_(N_( 462 "%s is missing. Make sure your gnonlin " 463 "installation is complete."), element)) 464 documentation.messageAddGStreamerInstall(m) 465 self.debug(m) 466 self.addMessage(m)
467 468 for el in ["gnlsource", "gnlcomposition"]: 469 check_gnl(el) 470
471 - def do_setup(self):
472 playlist = playlistparser.Playlist(self) 473 self.playlistparser = playlistparser.PlaylistXMLParser(playlist) 474 if self._baseDirectory: 475 self.playlistparser.setBaseDirectory(self._baseDirectory) 476 477 if self._playlistfile: 478 try: 479 self.playlistparser.parseFile(self._playlistfile) 480 except fxml.ParserError, e: 481 self.warning("Failed to parse playlist file: %r", e) 482 483 if self._playlistdirectory: 484 self._watchDirectory(self._playlistdirectory) 485 486 reactor.callLater(10, self.timeReport)
487