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: 7974 $" 
 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.SystemClock() 268 # It doesn't matter too much what this basetime is, so long as we know 269 # the value. 270 self.basetime = clock.get_time() 271 272 # We force usage of the system clock. 273 pipeline.use_clock(clock) 274 # Now we disable default basetime distribution 275 pipeline.set_new_stream_time(gst.CLOCK_TIME_NONE) 276 # And we choose our own basetime... 277 self.debug("Setting basetime of %d", self.basetime) 278 pipeline.set_base_time(self.basetime)
279
280 - def timeReport(self):
281 ts = self.pipeline.get_clock().get_time() 282 self.debug("Pipeline clock is now at %d -> %s", ts, _tsToString(ts)) 283 reactor.callLater(10, self.timeReport)
284
285 - def getCurrentPosition(self):
286 return self.pipeline.query_position(gst.FORMAT_TIME)[0]
287
288 - def scheduleItem(self, item):
289 """ 290 Schedule a given playlist item in our playback compositions. 291 """ 292 start = item.timestamp - self.basetime 293 self.debug("Starting item %s at %d seconds from start: %s", item.uri, 294 start/gst.SECOND, _tsToString(item.timestamp)) 295 296 # If we schedule things to start before the current pipeline position, 297 # gnonlin will adjust this to start now. However, it does this 298 # separately for audio and video, so we start from different points, 299 # thus we're out of sync. 300 # So, always start slightly in the future... 5 seconds seems to work 301 # fine in practice. 302 now = self.getCurrentPosition() 303 neareststarttime = now + 5 * gst.SECOND 304 305 if start < neareststarttime: 306 if start + item.duration < neareststarttime: 307 self.debug("Item too late; skipping entirely") 308 return False 309 else: 310 change = neareststarttime - start 311 self.debug("Starting item with offset %d", change) 312 item.duration -= change 313 item.offset += change 314 start = neareststarttime 315 316 end = start + item.duration 317 timeuntilend = end - now 318 # After the end time, remove this item from the composition, otherwise 319 # it will continue to use huge gobs of memory and lots of threads. 320 reactor.callLater(timeuntilend/gst.SECOND + 5, 321 self.unscheduleItem, item) 322 323 if self._hasVideo and item.hasVideo: 324 self.debug("Adding video source with start %d, duration %d, " 325 "offset %d", start, item.duration, item.offset) 326 vsrc = file_gnl_src(None, item.uri, self.videocaps, 327 start, item.duration, item.offset, 0) 328 self.videocomp.add(vsrc) 329 self._vsrcs[item] = vsrc 330 if self._hasAudio and item.hasAudio: 331 self.debug("Adding audio source with start %d, duration %d, " 332 "offset %d", start, item.duration, item.offset) 333 asrc = file_gnl_src(None, item.uri, self.audiocaps, 334 start, item.duration, item.offset, 0) 335 self.audiocomp.add(asrc) 336 self._asrcs[item] = asrc 337 self.debug("Done scheduling: start at %s, end at %s", 338 _tsToString(start + self.basetime), 339 _tsToString(start + self.basetime + item.duration)) 340 341 self.uiState.append("playlist", (item.timestamp, 342 item.uri, 343 item.duration, 344 item.offset, 345 item.hasAudio, 346 item.hasVideo)) 347 return True
348
349 - def unscheduleItem(self, item):
350 self.debug("Unscheduling item at uri %s", item.uri) 351 if self._hasVideo and item.hasVideo and item in self._vsrcs: 352 vsrc = self._vsrcs.pop(item) 353 self.videocomp.remove(vsrc) 354 vsrc.set_state(gst.STATE_NULL) 355 if self._hasAudio and item.hasAudio and item in self._asrcs: 356 asrc = self._asrcs.pop(item) 357 self.audiocomp.remove(asrc) 358 asrc.set_state(gst.STATE_NULL) 359 for entry in self.uiState.get("playlist"): 360 if entry[0] == item.timestamp: 361 self.uiState.remove("playlist", entry)
362
363 - def adjustItemScheduling(self, item):
364 if self._hasVideo and item.hasVideo: 365 vsrc = self._vsrcs[item] 366 vsrc.props.start = item.timestamp - self.basetime 367 vsrc.props.duration = item.duration 368 vsrc.props.media_duration = item.duration 369 if self._hasAudio and item.hasAudio: 370 asrc = self._asrcs[item] 371 asrc.props.start = item.timestamp - self.basetime 372 asrc.props.duration = item.duration 373 asrc.props.media_duration = item.duration
374
375 - def addPlaylist(self, data):
376 self.playlistparser.parseData(data)
377
378 - def create_pipeline(self):
379 props = self.config['properties'] 380 381 self._playlistfile = props.get('playlist', None) 382 self._playlistdirectory = props.get('playlist-directory', None) 383 self._baseDirectory = props.get('base-directory', None) 384 385 self._width = props.get('width', 320) 386 self._height = props.get('height', 240) 387 self._framerate = props.get('framerate', (15, 1)) 388 self._samplerate = props.get('samplerate', 44100) 389 self._channels = props.get('channels', 2) 390 391 self._hasAudio = props.get('audio', True) 392 self._hasVideo = props.get('video', True) 393 394 pipeline = self._buildPipeline() 395 self._setupClock(pipeline) 396 397 self._createDefaultSources(props) 398 399 return pipeline
400
401 - def _watchDirectory(self, dir):
402 self.debug("Watching directory %s", dir) 403 self._filesAdded = {} 404 405 self._directoryWatcher = watcher.DirectoryWatcher(dir) 406 self._directoryWatcher.subscribe(fileChanged=self._watchFileChanged, 407 fileDeleted=self._watchFileDeleted) 408 409 # in the start call watcher should find all the existing 410 # files, so we block discovery while the watcher starts 411 self.playlistparser.blockDiscovery() 412 try: 413 self._directoryWatcher.start() 414 finally: 415 self.playlistparser.unblockDiscovery()
416
417 - def _watchFileDeleted(self, file):
418 self.debug("File deleted: %s", file) 419 if file in self._filesAdded: 420 self.playlistparser.playlist.removeItems(file) 421 self._filesAdded.pop(file) 422 423 self._cleanMessage(file)
424
425 - def _cleanMessage(self, file):
426 # There's no message removal API! We have to do this instead. Ick? 427 msgid = ("playlist-parse-error", file) 428 for m in self.state.get('messages'): 429 if m.id == msgid: 430 self.state.remove('messages', m)
431
432 - def _watchFileChanged(self, file):
433 self.debug("File changed: %s", file) 434 if file in self._filesAdded: 435 self.debug("Removing existing items for changed playlist") 436 self.playlistparser.playlist.removeItems(file) 437 438 self._filesAdded[file] = None 439 self._cleanMessage(file) 440 try: 441 self.debug("Parsing file: %s", file) 442 self.playlistparser.parseFile(file, piid=file) 443 except fxml.ParserError, e: 444 self.warning("Failed to parse playlist file: %r", e) 445 # Since this isn't done directly via the remote method, add a 446 # message so people can find out that it failed... 447 # Use a tuple including the filename to identify the warning, so we 448 # can add/remove one per file 449 msgid = ("playlist-parse-error", file) 450 self.addMessage( 451 messages.Warning(T_(N_( 452 "Failed to parse a playlist from file %s: %s" % 453 (file, e))), mid=msgid))
454
455 - def do_check(self):
456 457 def check_gnl(element): 458 exists = gstreamer.element_factory_exists(element) 459 if not exists: 460 m = messages.Error(T_(N_( 461 "%s is missing. Make sure your gnonlin " 462 "installation is complete."), element)) 463 documentation.messageAddGStreamerInstall(m) 464 self.debug(m) 465 self.addMessage(m)
466 467 for el in ["gnlsource", "gnlcomposition"]: 468 check_gnl(el) 469
470 - def do_setup(self):
471 playlist = playlistparser.Playlist(self) 472 self.playlistparser = playlistparser.PlaylistXMLParser(playlist) 473 if self._baseDirectory: 474 self.playlistparser.setBaseDirectory(self._baseDirectory) 475 476 if self._playlistfile: 477 try: 478 self.playlistparser.parseFile(self._playlistfile) 479 except fxml.ParserError, e: 480 self.warning("Failed to parse playlist file: %r", e) 481 482 if self._playlistdirectory: 483 self._watchDirectory(self._playlistdirectory) 484 485 reactor.callLater(10, self.timeReport)
486