msmith - in flumotion/trunk: . flumotion/component flumotion/component/producers flumotion/component/producers/playlist

flumotion-commit at lists.fluendo.com flumotion-commit at lists.fluendo.com
Tue May 8 15:49:07 CEST 2007


Author: msmith
Date: Tue May  8 15:49:04 2007
New Revision: 4893

Added:
   flumotion/trunk/flumotion/component/producers/playlist/Makefile.am
   flumotion/trunk/flumotion/component/producers/playlist/__init__.py
   flumotion/trunk/flumotion/component/producers/playlist/playlist.py
   flumotion/trunk/flumotion/component/producers/playlist/playlist.xml
   flumotion/trunk/flumotion/component/producers/playlist/playlistparser.py
   flumotion/trunk/flumotion/component/producers/playlist/singledecodebin.py
   flumotion/trunk/flumotion/component/producers/playlist/smartscale.py
Modified:
   flumotion/trunk/ChangeLog
   flumotion/trunk/configure.ac
   flumotion/trunk/flumotion/component/feedcomponent.py
   flumotion/trunk/flumotion/component/feedcomponent010.py
   flumotion/trunk/flumotion/component/producers/Makefile.am
Log:
        * configure.ac:
        * flumotion/component/feedcomponent.py:
        * flumotion/component/feedcomponent010.py:
        * flumotion/component/producers/Makefile.am:
        * flumotion/component/producers/playlist/Makefile.am:
        * flumotion/component/producers/playlist/__init__.py:
        * flumotion/component/producers/playlist/playlist.py:
        * flumotion/component/producers/playlist/playlist.xml:
        * flumotion/component/producers/playlist/playlistparser.py:
        * flumotion/component/producers/playlist/singledecodebin.py:
        * flumotion/component/producers/playlist/smartscale.py:
          Merge playlist component back to trunk now that the basics are
          functional.



Modified: flumotion/trunk/ChangeLog
==============================================================================
--- flumotion/trunk/ChangeLog	(original)
+++ flumotion/trunk/ChangeLog	Tue May  8 15:49:04 2007
@@ -1,3 +1,19 @@
+2007-05-08  Michael Smith,,,  <set EMAIL_ADDRESS environment variable>
+
+	* configure.ac:
+	* flumotion/component/feedcomponent.py:
+	* flumotion/component/feedcomponent010.py:
+	* flumotion/component/producers/Makefile.am:
+	* flumotion/component/producers/playlist/Makefile.am:
+	* flumotion/component/producers/playlist/__init__.py:
+	* flumotion/component/producers/playlist/playlist.py:
+	* flumotion/component/producers/playlist/playlist.xml:
+	* flumotion/component/producers/playlist/playlistparser.py:
+	* flumotion/component/producers/playlist/singledecodebin.py:
+	* flumotion/component/producers/playlist/smartscale.py:
+	  Merge playlist component back to trunk now that the basics are
+	  functional.
+
 2007-05-08  Thomas Vander Stichele  <thomas at apestaart dot org>
 
 	* flumotion/component/misc/httpfile/file.py (File, File.__init__,

Modified: flumotion/trunk/configure.ac
==============================================================================
--- flumotion/trunk/configure.ac	(original)
+++ flumotion/trunk/configure.ac	Tue May  8 15:49:04 2007
@@ -195,6 +195,7 @@
 flumotion/component/producers/icecast/Makefile
 flumotion/component/producers/ivtv/Makefile
 flumotion/component/producers/pipeline/Makefile
+flumotion/component/producers/playlist/Makefile
 flumotion/component/producers/rtsp/Makefile
 flumotion/component/producers/soundcard/Makefile
 flumotion/component/producers/videotest/Makefile

Modified: flumotion/trunk/flumotion/component/feedcomponent.py
==============================================================================
--- flumotion/trunk/flumotion/component/feedcomponent.py	(original)
+++ flumotion/trunk/flumotion/component/feedcomponent.py	Tue May  8 15:49:04 2007
@@ -275,17 +275,6 @@
 
         try:
             pipeline = gst.parse_launch(self.pipeline_string)
-
-            # Connect to the client-fd-removed signals on each feeder, so we 
-            # can clean up properly on removal.
-            feeder_element_names = map(lambda n: "feeder:" + n, 
-                self.feeder_names)
-            for feeder in feeder_element_names:
-                element = pipeline.get_by_name(feeder)
-                element.connect('client-fd-removed', self.removeClientCallback)
-                self.debug("Connected %s to removeClientCallback", feeder)
-
-            return pipeline
         except gobject.GError, e:
             self.warning('Could not parse pipeline: %s' % e.message)
             m = messages.Error(T_(N_(
@@ -294,6 +283,10 @@
             self.state.append('messages', m)
             raise errors.PipelineParseError(e.message)
 
+        self.connect_feeders(pipeline)
+
+        return pipeline
+
     def set_pipeline(self, pipeline):
         FeedComponent.set_pipeline(self, pipeline)
         self.configure_pipeline(self.pipeline, self.config['properties'])

Modified: flumotion/trunk/flumotion/component/feedcomponent010.py
==============================================================================
--- flumotion/trunk/flumotion/component/feedcomponent010.py	(original)
+++ flumotion/trunk/flumotion/component/feedcomponent010.py	Tue May  8 15:49:04 2007
@@ -584,6 +584,16 @@
         self.debug('parsed feeder config, feeders %r' % self.feeder_names)
         self.state.set('feederNames', self.feeder_names)
 
+    def connect_feeders(self, pipeline):
+        # Connect to the client-fd-removed signals on each feeder, so we 
+        # can clean up properly on removal.
+        feeder_element_names = map(lambda n: "feeder:" + n, 
+            self.feeder_names)
+        for feeder in feeder_element_names:
+            element = pipeline.get_by_name(feeder)
+            element.connect('client-fd-removed', self.removeClientCallback)
+            self.debug("Connected %s to removeClientCallback", feeder)
+
     def get_eater_names(self):
         """
         Return the list of feeder names this component eats from.

Modified: flumotion/trunk/flumotion/component/producers/Makefile.am
==============================================================================
--- flumotion/trunk/flumotion/component/producers/Makefile.am	(original)
+++ flumotion/trunk/flumotion/component/producers/Makefile.am	Tue May  8 15:49:04 2007
@@ -17,6 +17,7 @@
 	icecast \
 	ivtv \
 	pipeline \
+	playlist \
 	rtsp \
 	soundcard \
 	videotest \

Added: flumotion/trunk/flumotion/component/producers/playlist/Makefile.am
==============================================================================
--- (empty file)
+++ flumotion/trunk/flumotion/component/producers/playlist/Makefile.am	Tue May  8 15:49:04 2007
@@ -0,0 +1,10 @@
+include $(top_srcdir)/common/python.mk
+
+component_PYTHON = __init__.py playlist.py singledecodebin.py smartscale.py
+componentdir = $(libdir)/flumotion/python/flumotion/component/producers/playlist
+component_DATA = playlist.xml
+
+clean-local:
+	rm -rf *.pyc *.pyo
+
+EXTRA_DIST = $(component_DATA)

Added: flumotion/trunk/flumotion/component/producers/playlist/__init__.py
==============================================================================
--- (empty file)
+++ flumotion/trunk/flumotion/component/producers/playlist/__init__.py	Tue May  8 15:49:04 2007
@@ -0,0 +1,20 @@
+# -*- Mode: Python -*-
+# vi:si:et:sw=4:sts=4:ts=4
+#
+# Flumotion - a streaming media server
+# Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com).
+# All rights reserved.
+
+# This file may be distributed and/or modified under the terms of
+# the GNU General Public License version 2 as published by
+# the Free Software Foundation.
+# This file is distributed without any warranty; without even the implied
+# warranty of merchantability or fitness for a particular purpose.
+# See "LICENSE.GPL" in the source distribution for more information.
+
+# Licensees having purchased or holding a valid Flumotion Advanced
+# Streaming Server license may use this file in accordance with the
+# Flumotion Advanced Streaming Server Commercial License Agreement.
+# See "LICENSE.Flumotion" in the source distribution for more information.
+
+# Headers in this file shall remain intact.

Added: flumotion/trunk/flumotion/component/producers/playlist/playlist.py
==============================================================================
--- (empty file)
+++ flumotion/trunk/flumotion/component/producers/playlist/playlist.py	Tue May  8 15:49:04 2007
@@ -0,0 +1,279 @@
+# -*- Mode: Python -*-
+# vi:si:et:sw=4:sts=4:ts=4
+#
+# Flumotion - a streaming media server
+# Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com).
+# All rights reserved.
+
+# This file may be distributed and/or modified under the terms of
+# the GNU General Public License version 2 as published by
+# the Free Software Foundation.
+# This file is distributed without any warranty; without even the implied
+# warranty of merchantability or fitness for a particular purpose.
+# See "LICENSE.GPL" in the source distribution for more information.
+
+# Licensees having purchased or holding a valid Flumotion Advanced
+# Streaming Server license may use this file in accordance with the
+# Flumotion Advanced Streaming Server Commercial License Agreement.
+# See "LICENSE.Flumotion" in the source distribution for more information.
+
+# Headers in this file shall remain intact.
+
+import gst
+import gobject
+import time
+
+from twisted.internet import defer
+
+from flumotion.common import errors, messages, log, fxml
+from flumotion.component import feedcomponent
+
+from flumotion.common.messages import N_
+
+import smartscale
+import singledecodebin
+import playlistparser
+
+T_ = messages.gettexter('flumotion')
+
+def videotest_gnl_src(name, start, duration, priority):
+    src = gst.element_factory_make('videotestsrc')
+    # Set videotestsrc to all black.
+    src.props.pattern = 2
+    gnlsrc = gst.element_factory_make('gnlsource', name)
+    gnlsrc.props.start = start
+    gnlsrc.props.duration = duration
+    gnlsrc.props.media_start = 0
+    gnlsrc.props.media_duration = duration
+    gnlsrc.props.priority = priority
+    gnlsrc.add(src)
+
+    return gnlsrc
+
+def audiotest_gnl_src(name, start, duration, priority):
+    src = gst.element_factory_make('audiotestsrc')
+    # Set audiotestsrc to use silence.
+    src.props.wave = 4 
+    gnlsrc = gst.element_factory_make('gnlsource', name)
+    gnlsrc.props.start = start
+    gnlsrc.props.duration = duration
+    gnlsrc.props.media_start = 0
+    gnlsrc.props.media_duration = duration
+    gnlsrc.props.priority = priority
+    gnlsrc.add(src)
+
+    return gnlsrc
+
+def file_gnl_src(name, uri, caps, start, duration, offset, priority):
+    src = singledecodebin.SingleDecodeBin(caps, uri)
+    gnlsrc = gst.element_factory_make('gnlsource', name)
+    gnlsrc.props.start = start
+    gnlsrc.props.duration = duration
+    gnlsrc.props.media_start = offset
+    gnlsrc.props.media_duration = duration
+    gnlsrc.props.priority = priority
+    gnlsrc.add(src)
+
+    return gnlsrc
+
+class PlaylistProducerMedium(feedcomponent.FeedComponentMedium):
+    def __init__(self, comp):
+        feedcomponent.FeedComponentMedium.__init__(self, comp)
+
+    def remote_add_playlist(self, data):
+        self.component.addPlaylist(data)
+
+class PlaylistProducer(feedcomponent.FeedComponent):
+
+    componentMediumClass = PlaylistProducerMedium
+
+    def init(self):
+        self.basetime = -1
+        self.pipeline = None
+
+        # The gnlcompositions for audio and video
+        self.videocomp = None
+        self.audiocomp = None
+
+        self.videocaps = gst.Caps("video/x-raw-yuv;video/x-raw-rgb")
+        self.audiocaps = gst.Caps("audio/x-raw-int;audio/x-raw-float")
+
+    def _buildAudioPipeline(self, pipeline, queue):
+        audiorate = gst.element_factory_make("audiorate")
+        audioconvert = gst.element_factory_make('audioconvert')
+        audioresample = gst.element_factory_make('audioresample')
+        outcaps = gst.Caps("audio/x-raw-int,channels=%d,rate=%d" % 
+            (self._channels, self._samplerate))
+
+        capsfilter = gst.element_factory_make("capsfilter")
+        capsfilter.props.caps = outcaps
+
+        pipeline.add(audiorate, audioconvert, audioresample, capsfilter)
+        queue.link(audioconvert)
+        audioconvert.link(audioresample)
+        audioresample.link(audiorate)
+        audiorate.link(capsfilter)
+
+        return capsfilter.get_pad('src')
+
+    def _buildVideoPipeline(self, pipeline, queue):
+        outcaps = gst.Caps(
+            "video/x-raw-yuv,width=%d,height=%d,framerate=%d/%d,"
+            "pixel-aspect-ratio=1/1" % 
+                (self._width, self._height, self._framerate[0], 
+                 self._framerate[1]))
+
+        cspace = gst.element_factory_make("ffmpegcolorspace")
+        scaler = smartscale.SmartVideoScale()
+        scaler.set_caps(outcaps)
+        videorate = gst.element_factory_make("videorate")
+        capsfilter = gst.element_factory_make("capsfilter")
+        capsfilter.props.caps = outcaps
+
+        pipeline.add(cspace, scaler, videorate, capsfilter)
+
+        queue.link(cspace)
+        cspace.link(scaler)
+        scaler.link(videorate)
+        videorate.link(capsfilter)
+        return capsfilter.get_pad('src')
+
+    def _buildPipeline(self):
+        pipeline = gst.Pipeline()
+
+        for mediatype in ['audio', 'video']:
+            composition = gst.element_factory_make("gnlcomposition", 
+                mediatype + "-composition")
+
+            queue = gst.element_factory_make("queue")
+            identity = gst.element_factory_make("identity")
+            identity.set_property("sync", True)
+            identity.set_property("single-segment", True)
+            identity.set_property("silent", True)
+
+            pipeline.add(composition, identity, queue)
+
+            def _padAddedCb(element, pad, target):
+                self.debug("Pad added, linking")
+                pad.link(target)
+            composition.connect('pad-added', _padAddedCb, 
+                identity.get_pad("sink"))
+            identity.link(queue)
+
+            if mediatype == 'audio':
+                self.audiocomp = composition
+                srcpad = self._buildAudioPipeline(pipeline, queue)
+            else:
+                self.videocomp = composition
+                srcpad = self._buildVideoPipeline(pipeline, queue)
+
+            feedername = 'feeder:%s:%s' % (self.name, mediatype)
+            chunk = self.FEEDER_TMPL % {'name': feedername}
+            binstr = "bin.("+chunk+" )"
+            self.debug("Parse for media composition is %s", binstr)
+
+            bin = gst.parse_launch(binstr)
+            pad = bin.find_unconnected_pad(gst.PAD_SINK)
+            ghostpad = gst.GhostPad(mediatype + "-feederpad", pad)
+            bin.add_pad(ghostpad)
+
+            pipeline.add(bin)
+            srcpad.link(ghostpad)
+
+        return pipeline
+
+    def _createDefaultSources(self):
+        vsrc = videotest_gnl_src("videotestdefault", 0, 2**63 - 1, 
+            2**31 - 1)
+        self.videocomp.add(vsrc)
+
+        asrc = audiotest_gnl_src("videotestdefault", 0, 2**63 - 1, 
+            2**31 - 1)
+        self.audiocomp.add(asrc)
+
+    def _setupClock(self, pipeline):
+        # Configure our pipeline to use a known basetime and clock.
+        clock = gst.SystemClock()
+        # It doesn't matter too much what this basetime is, so long as we know
+        # the value.
+        self.basetime = clock.get_time()
+
+        # We force usage of the system clock.
+        pipeline.use_clock(clock)
+        # Now we disable default basetime distribution
+        pipeline.set_new_stream_time(gst.CLOCK_TIME_NONE)
+        # And we choose our own basetime...
+        self.debug("Setting basetime of %d", self.basetime)
+        pipeline.set_base_time(self.basetime)
+
+    def scheduleItem(self, item):
+        """
+        Schedule a given playlist item in our playback compositions.
+        """
+        start = item.timestamp - self.basetime
+        # This works around a bug in videotestsrc and videorate. TODO! This
+        # can be removed once we upgrade to a future version of gst-plugins-base
+        #start = (start / gst.SECOND) * gst.SECOND
+        self.debug("Starting item %s in %d seconds", item.uri, start/gst.SECOND)
+
+        if start < 0:
+            if start + item.duration < 0:
+                return
+            else:
+                # If we're not too late for part of the file to be playable,
+                # then play it!
+                item.offset = -start
+                item.duration = item.duration + start
+                start = 0
+
+        if item.hasVideo:
+            item.vsrc = file_gnl_src(None, item.uri, self.videocaps,
+                start, item.duration, item.offset, 0)
+            self.videocomp.add(item.vsrc)
+        if item.hasAudio:
+            item.asrc = file_gnl_src(None, item.uri, self.audiocaps,
+                start, item.duration, item.offset, 0)
+            self.audiocomp.add(item.asrc)
+
+    def unscheduleItem(self, item):
+        self.debug("Unscheduling item at uri %s", item.uri)
+        if item.hasVideo:
+            self.videocomp.remove(item.vsrc)
+        if item.hasAudio: 
+            self.audiocomp.remove(item.asrc)
+
+    def addPlaylist(self, data):
+        self.playlist.parseData(data)
+
+    def create_pipeline(self):
+        props = self.config['properties'];
+
+        self._playlistfile = props.get('playlist', None)
+
+        self._width = props.get('width', 320)
+        self._height = props.get('height', 240)
+        self._framerate = props.get('framerate', (15, 1))
+        self._samplerate = props.get('samplerate', 44100)
+        self._channels = props.get('channels', 2)
+
+        pipeline = self._buildPipeline() 
+        self._setupClock(pipeline)
+
+        self._createDefaultSources()
+
+        self.playlist = playlistparser.Playlist(self)
+        try:
+            if self._playlistfile:
+                self.playlist.parseFile(self._playlistfile)
+        except fxml.ParserError, e:
+            self.warning("Failed to parse playlist file: %r", e)
+
+        self.connect_feeders(pipeline)
+        return pipeline
+
+    def do_start(self, clocking):
+        self.link()
+        return defer.succeed(None)
+        
+    def do_stop(self):
+        return feedcomponent.FeedComponent.do_stop(self)

Added: flumotion/trunk/flumotion/component/producers/playlist/playlist.xml
==============================================================================
--- (empty file)
+++ flumotion/trunk/flumotion/component/producers/playlist/playlist.xml	Tue May  8 15:49:04 2007
@@ -0,0 +1,52 @@
+<registry>
+  <components>
+    <component type="playlist-producer"
+               base="flumotion/component/producers/playlist"
+               description="Produces a raw audio/video feed from a playlist of input files, scheduled for particular times.">
+      <source location="flumotion.component.producers.playlist.playlist"/>
+      <feeder name="audio"/>
+      <feeder name="video"/>
+      <entries>
+        <entry type="component" location="playlist.py"
+               function="PlaylistProducer" />
+      </entries>
+
+      <synchronization required="yes" clock-priority="110" />
+
+      <properties>
+        <property name="height" type="int"
+                  description="Scaled output height of video" />
+        <property name="width" type="int"
+                  description="Scaled output width of video" />
+        <property name="framerate" type="fraction"
+                  description="Framerate to output" />
+        <property name="samplerate" type="int"
+                  description="Samplerate to output" />
+        <property name="channels" type="int"
+                  description="Audio channels to output" />
+
+        <property name="playlist" type="string" required="yes"
+                  description="Location of the initial playlist file" />
+      </properties>
+    </component>
+  </components>
+  <bundles>
+
+    <bundle name="playlist-component">
+        <dependencies>
+            <dependency name="component" />
+        </dependencies>
+
+        <directories>
+            <directory name="flumotion/component/producers/playlist">
+                <filename location="__init__.py" />
+                <filename location="smartscale.py" />
+                <filename location="singledecodebin.py" />
+                <filename location="playlist.py" />
+                <filename location="playlistparser.py" />
+            </directory>
+        </directories>
+    </bundle>
+
+  </bundles>
+</registry> 

Added: flumotion/trunk/flumotion/component/producers/playlist/playlistparser.py
==============================================================================
--- (empty file)
+++ flumotion/trunk/flumotion/component/producers/playlist/playlistparser.py	Tue May  8 15:49:04 2007
@@ -0,0 +1,186 @@
+# -*- Mode: Python -*-
+# vi:si:et:sw=4:sts=4:ts=4
+#
+# Flumotion - a streaming media server
+# Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com).
+# All rights reserved.
+
+# This file may be distributed and/or modified under the terms of
+# the GNU General Public License version 2 as published by
+# the Free Software Foundation.
+# This file is distributed without any warranty; without even the implied
+# warranty of merchantability or fitness for a particular purpose.
+# See "LICENSE.GPL" in the source distribution for more information.
+
+# Licensees having purchased or holding a valid Flumotion Advanced
+# Streaming Server license may use this file in accordance with the
+# Flumotion Advanced Streaming Server Commercial License Agreement.
+# See "LICENSE.Flumotion" in the source distribution for more information.
+
+# Headers in this file shall remain intact.
+
+import gst
+import time
+from StringIO import StringIO
+
+from xml.dom import Node
+
+from flumotion.common import log, fxml
+
+import singledecodebin
+
+def file_gnl_src(name, uri, caps, start, duration, offset, priority):
+    src = singledecodebin.SingleDecodeBin(caps, uri)
+    gnlsrc = gst.element_factory_make('gnlsource', name)
+    gnlsrc.props.start = start
+    gnlsrc.props.duration = duration
+    gnlsrc.props.media_start = offset
+    gnlsrc.props.media_duration = duration
+    gnlsrc.props.priority = priority
+    gnlsrc.add(src)
+
+    return gnlsrc
+
+class PlaylistItem(object, log.Loggable):
+    def __init__(self, timestamp, uri, offset, duration):
+        self.timestamp = timestamp
+        self.uri = uri
+        self.offset = offset
+        self.duration = duration
+
+        # Currently always set to true; later this should come from what the
+        # discoverer says.
+        self.hasAudio = True
+        self.hasVideo = True
+
+        # Audio and video gnlsource objects
+        self.vsrc = None
+        self.asrc = None
+
+        self.next = None
+
+class Playlist(object, log.Loggable):
+    def __init__(self, producer):
+        """
+        Create an initially empty playlist
+        """
+        self.items = None # PlaylistItem linked list
+
+        self.producer = producer
+
+    def addItem(self, timestamp, uri, offset, duration):
+        """
+        Add an item to the playlist.
+        The duration of previous and this entry may be adjusted to make it fit.
+        """
+        newitem = PlaylistItem(timestamp, uri, offset, duration)
+        prev = next = None
+        item = self.items
+        while item:
+            if item.timestamp < newitem.timestamp:
+                prev = item
+            elif (not next and item.timestamp > 
+                    newitem.timestamp + newitem.duration):
+                next = item
+                break
+            item = item.next
+
+        if prev and next and prev.next != next:
+            # Then things between prev and next are to be deleted. Do so.
+            cur = prev.next
+            while cur != next:
+                self.producer.unscheduleItem(cur)
+                cur = cur.next
+        elif prev and not next:
+            cur = prev.next
+            while cur:
+                self.producer.unscheduleItem(cur)
+                cur = cur.next
+
+        if prev:
+            prev.next = newitem
+        else:
+            self.items = newitem
+
+        if next:
+            newitem.next = next
+
+        # Duration adjustments -> Reflect into gnonlin timeline
+        if prev and prev.timestamp + prev.duration > newitem.timestamp:
+            prev.duration = newitem.timestamp - prev.timestamp
+            prev.asrc.props.duration = prev.duration
+            prev.vsrc.props.duration = prev.duration
+        if next and timestamp + newitem.duration > next.timestamp:
+            newitem.duration = next.timestamp - newitem.timestamp
+
+        # Then we need to actually add newitem into the gnonlin timeline
+        self.producer.scheduleItem(newitem)
+
+    def expireOldEntries(self):
+        """
+        Delete references to old playlist entries that have passed.
+        TODO: is this needed? It's to save memory, but probably not very much 
+        memory...
+        """
+        pass
+
+    def parseData(self, data):
+        """
+        Parse playlist XML document data
+        """
+        file = StringIO(data)
+        self.parseFile(file)
+
+    def parseFile(self, file):
+        """
+        Parse a playlist file. Adds the contents of the file to the existing 
+        playlist, overwriting any existing entries for the same time period.
+        """
+        parser = fxml.Parser()
+
+        root = parser.getRoot(file)
+
+        node = root.documentElement
+        self.debug("Parsing playlist from file %s", file)
+        if node.nodeName != 'playlist':
+            raise fxml.ParserError("Root node is not 'playlist'")
+
+        for child in node.childNodes:
+            if child.nodeType == Node.ELEMENT_NODE and \
+                    child.nodeName == 'entry':
+                self.debug("Parsing entry")
+                self._parsePlaylistEntry(parser, child)
+
+    def _parsePlaylistEntry(self, parser, entry):
+        # TODO: Once we use the discoverer, we should move duration to optional
+        mandatory = ['filename', 'time', 'duration']
+        optional = ['offset']
+
+        (filename, timestamp, duration, offset) = parser.parseAttributes(
+            entry, mandatory, optional)
+
+        duration = int(float(duration) * gst.SECOND)
+        if offset is None:
+            offset = 0
+        offset = int(offset)
+
+        timestamp = self._parseTimestamp(timestamp)
+
+        uri = 'file://'+filename
+
+        self.debug("Adding item")
+        self.addItem(timestamp, uri, offset, duration)
+
+    def _parseTimestamp(self, ts):
+        # Take TS in YYYY-MM-DDThh:mm:ssZ format, return timestamp in 
+        # nanoseconds since the epoch
+        format = "%Y-%m-%dT%H:%M:%SZ"
+
+        try:
+            timestruct = time.strptime(ts, format)
+
+            return int(time.mktime(timestruct) * gst.SECOND)
+        except ValueError:
+            raise fxml.ParserError("Invalid timestamp %s", ts)
+
+

Added: flumotion/trunk/flumotion/component/producers/playlist/singledecodebin.py
==============================================================================
--- (empty file)
+++ flumotion/trunk/flumotion/component/producers/playlist/singledecodebin.py	Tue May  8 15:49:04 2007
@@ -0,0 +1,308 @@
+# -*- Mode: Python -*-
+# vi:si:et:sw=4:sts=4:ts=4
+#
+# Flumotion - a streaming media server
+# Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com).
+# All rights reserved.
+
+# This file may be distributed and/or modified under the terms of
+# the GNU General Public License version 2 as published by
+# the Free Software Foundation.
+# This file is distributed without any warranty; without even the implied
+# warranty of merchantability or fitness for a particular purpose.
+# See "LICENSE.GPL" in the source distribution for more information.
+
+# Licensees having purchased or holding a valid Flumotion Advanced
+# Streaming Server license may use this file in accordance with the
+# Flumotion Advanced Streaming Server Commercial License Agreement.
+# See "LICENSE.Flumotion" in the source distribution for more information.
+
+# Headers in this file shall remain intact.
+
+
+# Originally part of PiTiVi, 
+# Copyright (C) 2005-2007 Edward Hervey <bilboed at bilboed.com>,
+# Relicensed under the above dual license with his permission.
+
+"""
+Single-stream queue-less decodebin
+"""
+
+import gobject
+import gst
+
+def is_raw(caps):
+    """ returns True if the caps are RAW """
+    rep = caps.to_string()
+    valid = ["video/x-raw", "audio/x-raw", "text/plain", "text/x-pango-markup"]
+    for val in valid:
+        if rep.startswith(val):
+            return True
+    return False
+
+class SingleDecodeBin(gst.Bin):
+
+    __gsttemplates__ = (
+        gst.PadTemplate ("sinkpadtemplate",
+                         gst.PAD_SINK,
+                         gst.PAD_ALWAYS,
+                         gst.caps_new_any()),
+        gst.PadTemplate ("srcpadtemplate",
+                         gst.PAD_SRC,
+                         gst.PAD_SOMETIMES,
+                         gst.caps_new_any())
+        )
+    def __init__(self, caps=None, uri=None, *args, **kwargs):
+        gst.Bin.__init__(self, *args, **kwargs)
+        if not caps:
+            caps = gst.caps_new_any()
+        self.caps = caps
+        self.typefind = gst.element_factory_make("typefind", "internal-typefind")
+        self.add(self.typefind)
+
+        self.uri = uri
+        if self.uri and gst.uri_is_valid(self.uri):
+            self.urisrc = gst.element_make_from_uri(gst.URI_SRC, uri, "urisrc")
+            self.log("created urisrc %s / %r" % (self.urisrc.get_name(),
+                                                 self.urisrc))
+            self.add(self.urisrc)
+            self.urisrc.link(self.typefind)
+        else:
+            self._sinkpad = gst.GhostPad("sink", self.typefind.get_pad("sink"))
+            self._sinkpad.set_active(True)
+            self.add_pad(self._sinkpad)
+
+        self.typefind.connect("have_type", self._typefindHaveTypeCb)
+
+        self._srcpad = None
+
+        self._dynamics = []
+
+        self._validelements = [] #added elements
+
+        self._factories = self._getSortedFactoryList()
+
+
+    ## internal methods
+
+    def _controlDynamicElement(self, element):
+        self.log("element:%s" % element.get_name())
+        self._dynamics.append(element)
+        element.connect("pad-added", self._dynamicPadAddedCb)
+        element.connect("no-more-pads", self._dynamicNoMorePadsCb)
+
+    def _getSortedFactoryList(self):
+        """
+        Returns the list of demuxers, decoders and parsers available, sorted
+        by rank
+        """
+        def myfilter(fact):
+            if fact.get_rank() < 64 :
+                return False
+            klass = fact.get_klass()
+            if not ("Demuxer" in klass or "Decoder" in klass or "Parse" in klass):
+                return False
+            return True
+        reg = gst.registry_get_default()
+        res = [x for x in reg.get_feature_list(gst.ElementFactory) if myfilter(x)]
+        res.sort(lambda a, b: int(b.get_rank() - a.get_rank()))
+        return res
+
+    def _findCompatibleFactory(self, caps):
+        """
+        Returns a list of factories (sorted by rank) which can take caps as
+        input. Returns empty list if none are compatible
+        """
+        self.debug("caps:%s" % caps.to_string())
+        res = []
+        for factory in self._factories:
+            for template in factory.get_static_pad_templates():
+                if template.direction == gst.PAD_SINK:
+                    intersect = caps.intersect(template.static_caps.get())
+                    if not intersect.is_empty():
+                        res.append(factory)
+                        break
+        self.debug("returning %r" % res)
+        return res
+
+    def _closeLink(self, element):
+        """
+        Inspects element and tries to connect something on the srcpads.
+        If there are dynamic pads, it sets up a signal handler to
+        continue autoplugging when they become available.
+        """
+        to_connect = []
+        dynamic = False
+        templates = element.get_pad_template_list()
+        for template in templates:
+            if not template.direction == gst.PAD_SRC:
+                continue
+            if template.presence == gst.PAD_ALWAYS:
+                pad = element.get_pad(template.name_template)
+                to_connect.append(pad)
+            elif template.presence == gst.PAD_SOMETIMES:
+                pad = element.get_pad(template.name_template)
+                if pad:
+                    to_connect.append(pad)
+                else:
+                    dynamic = True
+            else:
+                self.log("Template %s is a request pad, ignoring" % pad.name_template)
+
+        if dynamic:
+            self.debug("%s is a dynamic element" % element.get_name())
+            self._controlDynamicElement(element)
+
+        for pad in to_connect:
+            self._closePadLink(element, pad, pad.get_caps())
+
+    def _tryToLink1(self, source, pad, factories):
+        """
+        Tries to link one of the factories' element to the given pad.
+
+        Returns the element that was successfully linked to the pad.
+        """
+        self.debug("source:%s, pad:%s , factories:%r" % (source.get_name(),
+                                                         pad.get_name(),
+                                                         factories))
+        result = None
+        for factory in factories:
+            element = factory.create()
+            if not element:
+                self.warning("weren't able to create element from %r" % factory)
+                continue
+
+            sinkpad = element.get_pad("sink")
+            if not sinkpad:
+                continue
+
+            self.add(element)
+            try:
+                pad.link(sinkpad)
+            except:
+                element.set_state(gst.STATE_NULL)
+                self.remove(element)
+                continue
+
+            self._closeLink(element)
+            element.set_state(gst.STATE_PAUSED)
+
+            result = element
+            break
+
+        return result
+
+    def _closePadLink(self, element, pad, caps):
+        """
+        Finds the list of elements that could connect to the pad.
+        If the pad has the desired caps, it will create a ghostpad.
+        If no compatible elements could be found, the search will stop.
+        """
+        self.debug("element:%s, pad:%s, caps:%s" % (element.get_name(),
+                                                    pad.get_name(),
+                                                    caps.to_string()))
+        if caps.is_empty():
+            self.log("unknown type")
+            return
+        if caps.is_any():
+            self.log("type is not know yet, waiting")
+            return
+        if caps.intersect(self.caps):
+            # This is the desired caps
+            if not self._srcpad:
+                self._wrapUp(element, pad)
+        elif is_raw(caps):
+            self.log("We hit a raw caps which isn't the wanted one")
+            # FIXME : recursively remove everything until demux/typefind
+
+        else:
+            # Find something
+            if len(caps) > 1:
+                self.log("many possible types, delaying")
+                return
+            facts = self._findCompatibleFactory(caps)
+            if not facts:
+                self.log("unknown type")
+                return
+            self._tryToLink1(element, pad, facts)
+
+    def _wrapUp(self, element, pad):
+        """
+        Ghost the given pad of element.
+        Remove non-used elements.
+        """
+
+        if self._srcpad:
+            return
+        self._markValidElements(element)
+        self._removeUnusedElements(self.typefind)
+        self.log("ghosting pad %s" % pad.get_name)
+        self._srcpad = gst.GhostPad("src", pad)
+        self._srcpad.set_active(True)
+        self.add_pad(self._srcpad)
+        self.post_message(gst.message_new_state_dirty(self))
+
+    def _markValidElements(self, element):
+        """
+        Mark this element and upstreams as valid
+        """
+        self.log("element:%s" % element.get_name())
+        if element == self.typefind:
+            return
+        self._validelements.append(element)
+        # find upstream element
+        pad = list(element.sink_pads())[0]
+        parent = pad.get_peer().get_parent()
+        self._markValidElements(parent)
+
+    def _removeUnusedElements(self, element):
+        """
+        Remove unused elements connected to srcpad(s) of element
+        """
+        self.log("element:%s" % element)
+        for pad in element.src_pads():
+            if pad.is_linked():
+                peer = pad.get_peer().get_parent()
+                self._removeUnusedElements(peer)
+                if not peer in self._validelements:
+                    self.log("removing %s" % peer.get_name())
+                    pad.unlink(pad.get_peer())
+                    peer.set_state(gst.STATE_NULL)
+                    self.remove(peer)
+
+    def _cleanUp(self):
+        self.log("")
+        if self._srcpad:
+            self.remove_pad(self._srcpad)
+        self._srcpad = None
+        for element in self._validelements:
+            element.set_state(gst.STATE_NULL)
+            self.remove(element)
+        self._validelements = []
+
+    ## Overrides
+
+    def do_change_state(self, transition):
+        self.debug("transition:%r" % transition)
+        res = gst.Bin.do_change_state(self, transition)
+        if transition in [gst.STATE_CHANGE_PAUSED_TO_READY, gst.STATE_CHANGE_READY_TO_NULL]:
+            self._cleanUp()
+        return res
+
+    ## Signal callbacks
+
+    def _typefindHaveTypeCb(self, typefind, probability, caps):
+        self.debug("probability:%d, caps:%s" % (probability, caps.to_string()))
+        self._closePadLink(typefind, typefind.get_pad("src"), caps)
+
+    ## Dynamic element Callbacks
+
+    def _dynamicPadAddedCb(self, element, pad):
+        self.log("element:%s, pad:%s" % (element.get_name(), pad.get_name()))
+        if not self._srcpad:
+            self._closePadLink(element, pad, pad.get_caps())
+
+    def _dynamicNoMorePadsCb(self, element):
+        self.log("element:%s" % element.get_name())
+
+gobject.type_register(SingleDecodeBin)

Added: flumotion/trunk/flumotion/component/producers/playlist/smartscale.py
==============================================================================
--- (empty file)
+++ flumotion/trunk/flumotion/component/producers/playlist/smartscale.py	Tue May  8 15:49:04 2007
@@ -0,0 +1,199 @@
+# -*- Mode: Python -*-
+# vi:si:et:sw=4:sts=4:ts=4
+#
+# Flumotion - a streaming media server
+# Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com).
+# All rights reserved.
+
+# This file may be distributed and/or modified under the terms of
+# the GNU General Public License version 2 as published by
+# the Free Software Foundation.
+# This file is distributed without any warranty; without even the implied
+# warranty of merchantability or fitness for a particular purpose.
+# See "LICENSE.GPL" in the source distribution for more information.
+
+# Licensees having purchased or holding a valid Flumotion Advanced
+# Streaming Server license may use this file in accordance with the
+# Flumotion Advanced Streaming Server Commercial License Agreement.
+# See "LICENSE.Flumotion" in the source distribution for more information.
+
+# Headers in this file shall remain intact.
+
+
+# Originally part of PiTiVi, 
+# Copyright (C) 2005-2007 Edward Hervey <bilboed at bilboed.com>,
+# Relicensed under the above dual license with his permission.
+
+"""
+Smart video scaler
+"""
+
+# Algorithm logic
+#
+# PAR is the same in videobox (automatic)
+# DAR is the same in videoscale (We need to make sure)
+#
+# The whole idea is to modify the caps between videobox and videoscale so that
+# the
+
+import gobject
+import gst
+
+class SmartVideoScale(gst.Bin):
+    """
+    Element to do proper videoscale.
+    Keeps Display Aspect Ratio.
+    Adds black borders if needed.
+    """
+
+    def __init__(self):
+        gst.Bin.__init__(self)
+        self.videoscale = gst.element_factory_make("videoscale", "smart-videoscale")
+        # set the scaling method to bilinear (cleaner)
+        # FIXME : we should figure out if better methods are available in the
+        # future, or ask the user which method he wants to use
+        # FIXME : Instead of having the set_caps() method, use proper caps negotiation
+        self.videoscale.props.method = 1
+        self.videobox = gst.element_factory_make("videobox", "smart-videobox")
+        self.capsfilter = gst.element_factory_make("capsfilter", "smart-capsfilter")
+        self.add(self.videoscale, self.capsfilter, self.videobox)
+        gst.element_link_many(self.videoscale, self.capsfilter, self.videobox)
+
+        self._sinkPad = gst.GhostPad("sink", self.videoscale.get_pad("sink"))
+        self._sinkPad.set_active(True)
+        self._srcPad = gst.GhostPad("src", self.videobox.get_pad("src"))
+        self._srcPad.set_active(True)
+
+        self.add_pad(self._sinkPad)
+        self.add_pad(self._srcPad)
+
+        self._sinkPad.set_setcaps_function(self._sinkSetCaps)
+
+
+        # input/output values
+        self.capsin = None
+        self.widthin = -1
+        self.heightin = -1
+        self.parin = gst.Fraction(1,1)
+        self.darin = gst.Fraction(1,1)
+        self.capsout = None
+        self.widthout = -1
+        self.heightout = -1
+        self.parout = gst.Fraction(1,1)
+        self.darout = gst.Fraction(1,1)
+
+    def set_caps(self, caps):
+        """ set the outgoing caps, because gst.BaseTransform is full of CRACK ! """
+        self.widthout, self.heightout, self.parout, self.darout = self._getValuesFromCaps(caps, True)
+
+    def _sinkSetCaps(self, unused_pad, caps):
+        self.log("caps:%s" % caps.to_string())
+        self.widthin, self.heightin, self.parin, self.darin = self._getValuesFromCaps(caps)
+        self._computeAndSetValues()
+        res = self.videoscale.get_pad("sink").set_caps(caps)
+        return res
+
+    def _srcSetCaps(self, unused_pad, caps):
+        self.log("caps:%s" % caps.to_string())
+        self.widthout, self.heightout, self.parout, self.darout = self._getValuesFromCaps(caps)
+        res = self.videobox.get_pad("src").set_caps(caps)
+        if res:
+            self.capsout = caps
+            self._computeAndSetValues()
+        return res
+
+    def _sinkPadCapsNotifyCb(self, pad, unused_prop):
+        caps = pad.get_negotiated_caps()
+        self.log("caps:%r" % caps)
+        self.widthin, self.heightin, self.parin, self.darin = self._getValuesFromCaps(caps)
+        self.capsin = caps
+        self._computeAndSetValues()
+
+    def _srcPadCapsNotifyCb(self, pad, unused_prop):
+        caps = pad.get_negotiated_caps()
+        self.log("caps:%r" % caps)
+        self.widthout, self.heightout, self.parout, self.darout = self._getValuesFromCaps(caps)
+        self.capsout = caps
+        self._computeAndSetValues()
+
+    def _getValuesFromCaps(self, caps, force=False):
+        """
+        returns (width, height, par, dar) from given caps.
+        If caps are None, or not negotiated, it will return
+        (-1, -1, gst.Fraction(1,1), gst.Fraction(1,1))
+        """
+        width = -1
+        height = -1
+        par = gst.Fraction(1,1)
+        dar = gst.Fraction(1,1)
+        if force or (caps and caps.is_fixed()):
+            struc = caps[0]
+            width = struc["width"]
+            height = struc["height"]
+            if struc.has_field('pixel-aspect-ratio'):
+                par = struc['pixel-aspect-ratio']
+            dar = gst.Fraction(width * par.num, height * par.denom)
+        return (width, height, par, dar)
+
+    def _computeAndSetValues(self):
+        """ Calculate the new values to set on capsfilter and videobox. """
+        if self.widthin == -1 or self.heightin == -1 or self.widthout == -1 or self.heightout == -1:
+            # FIXME : should we reset videobox/capsfilter properties here ?
+            self.error("We don't have input and output caps, we can't calculate videobox values")
+            return
+
+        self.log("incoming width/height/PAR/DAR : %d/%d/%r/%r" % (self.widthin, self.heightin,
+                                                                  self.parin, self.darin))
+        self.log("outgoing width/height/PAR/DAR : %d/%d/%r/%r" % (self.widthout, self.heightout,
+                                                                  self.parout, self.darout))
+
+        if self.darin == self.darout:
+            self.log("We have same input and output caps, resetting capsfilter and videobox settings")
+            # same DAR, set inputcaps on capsfilter, reset videobox values
+            caps = gst.caps_new_any()
+            left = 0
+            right = 0
+            top = 0
+            bottom = 0
+        else:
+            par = self.parout
+            dar = self.darin
+            if float(self.darin) > float(self.darout):
+                self.log("incoming DAR is greater that ougoing DAR. Adding top/bottom borders")
+                # width, PAR stays the same as output
+                # calculate newheight = (PAR * widthout) / DAR
+                newheight = (par.num * self.widthout * dar.denom) / (par.denom * dar.num)
+                self.log("newheight should be %d" % newheight)
+                extra = self.heightout - newheight
+                top = extra / 2
+                bottom = extra - top # compensate for odd extra
+                left = right = 0
+                # calculate filter caps
+                astr = "width=%d,height=%d" % (self.widthout, newheight)
+            else:
+                self.log("incoming DAR is smaller than outgoing DAR. Adding left/right borders")
+                # height, PAR stays the same as output
+                # calculate newwidth = (DAR * heightout) / PAR
+                newwidth = (dar.num * self.heightout * par.denom) / (dar.denom * par.num)
+                self.log("newwidth should be %d" % newwidth)
+                extra = self.widthout - newwidth
+                left = extra / 2
+                right = extra - left # compensate for odd extra
+                top = bottom = 0
+                # calculate filter caps
+                astr = "width=%d,height=%d" % (newwidth, self.heightout)
+            caps = gst.caps_from_string("video/x-raw-yuv,%s;video/x-raw-rgb,%s" % (astr, astr))
+
+        # set properties on elements
+        self.debug("About to set left/right/top/bottom : %d/%d/%d/%d" % (-left, -right, -top, -bottom))
+        self.videobox.props.left = -left
+        self.videobox.props.right = -right
+        self.videobox.props.top = -top
+        self.videobox.props.bottom = -bottom
+        self.debug("Settings filter caps %s" % caps.to_string())
+        self.capsfilter.props.caps = caps
+        self.debug("done")
+
+
+
+gobject.type_register(SmartVideoScale)


More information about the flumotion-commit mailing list