[rygel] renderer: Implement DIDL_S playlist support



commit 6acf09c0f54f7792e63386a7c7d12ac1440f49ee
Author: Jens Georg <jensg openismus com>
Date:   Mon Nov 19 17:29:33 2012 +0100

    renderer: Implement DIDL_S playlist support

 configure.ac                                       |    2 +-
 data/rygel.conf                                    |    7 +
 data/xml/AVTransport2.xml.in                       |    1 +
 .../rygel-playbin-player.vala                      |    6 +-
 src/librygel-renderer/filelist.am                  |    1 +
 src/librygel-renderer/rygel-av-transport.vala      |  265 +++++++++++++-------
 .../rygel-media-renderer-plugin.vala               |   41 +++
 src/librygel-renderer/rygel-player-controller.vala |  225 +++++++++++++++++
 .../rygel-sink-connection-manager.vala             |   20 +--
 9 files changed, 462 insertions(+), 106 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 5aa6867..acc920b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -34,7 +34,7 @@ VALA_REQUIRED=0.18.0
 VALADOC_REQUIRED=0.2
 GSSDP_REQUIRED=0.13.0
 GUPNP_REQUIRED=0.19.0
-GUPNP_AV_REQUIRED=0.11.3
+GUPNP_AV_REQUIRED=0.11.4
 GUPNP_DLNA_REQUIRED=0.7.0
 GSTREAMER_REQUIRED=1.0
 GSTPBU_REQUIRED=1.0
diff --git a/data/rygel.conf b/data/rygel.conf
index 5d831bf..13c1757 100644
--- a/data/rygel.conf
+++ b/data/rygel.conf
@@ -85,6 +85,13 @@ allow-deletion=true
 # List of active transcoders. To disable one, remove from list.
 transcoders=mp3;lpcm;mp2ts;wmv;aac;avc
 
+# Options that apply to the renderer framework in general
+[Renderer]
+
+# Default showtime in seconds to use for images in playlists if dlna:lifetime
+# is not set. DLNA wants something between 5 and 15 seconds.
+image-timeout = 15
+
 [Tracker]
 enabled=true
 share-pictures=true
diff --git a/data/xml/AVTransport2.xml.in b/data/xml/AVTransport2.xml.in
index 3344a15..fdda8ae 100644
--- a/data/xml/AVTransport2.xml.in
+++ b/data/xml/AVTransport2.xml.in
@@ -281,6 +281,7 @@
          <allowedValueList>
             <allowedValue>ABS_TIME</allowedValue>
             <allowedValue>REL_TIME</allowedValue>
+            <allowedValue>TRACK_NR</allowedValue>
          </allowedValueList>
       </stateVariable>
 
diff --git a/src/librygel-renderer-gst/rygel-playbin-player.vala b/src/librygel-renderer-gst/rygel-playbin-player.vala
index cca07ba..9a6ad26 100644
--- a/src/librygel-renderer-gst/rygel-playbin-player.vala
+++ b/src/librygel-renderer-gst/rygel-playbin-player.vala
@@ -132,6 +132,9 @@ public class Rygel.Playbin.Player : GLib.Object, Rygel.MediaPlayer {
                         this._playback_state = value;
                     }
                 break;
+                case "EOS":
+                    this._playback_state = value;
+                break;
                 default:
                 break;
             }
@@ -163,6 +166,7 @@ public class Rygel.Playbin.Player : GLib.Object, Rygel.MediaPlayer {
                         this.is_live = this.playbin.set_state (State.PAUSED)
                                         == StateChangeReturn.NO_PREROLL;
                         break;
+                    case "EOS":
                     case "PLAYING":
                         // This needs a check if GStreamer and DLNA agree on
                         // the "liveness" of the source (s0/sn increase in
@@ -397,7 +401,7 @@ public class Rygel.Playbin.Player : GLib.Object, Rygel.MediaPlayer {
         case MessageType.EOS:
             if (!this.is_rendering_image ()) {
                 debug ("EOS");
-                this.playback_state = "STOPPED";
+                this.playback_state = "EOS";
             } else {
                 debug ("Content is image, ignoring EOS");
             }
diff --git a/src/librygel-renderer/filelist.am b/src/librygel-renderer/filelist.am
index 5048e79..b1f7888 100644
--- a/src/librygel-renderer/filelist.am
+++ b/src/librygel-renderer/filelist.am
@@ -5,6 +5,7 @@ LIBRYGEL_RENDERER_VAPI_SOURCE_FILES = \
 
 LIBRYGEL_RENDERER_NONVAPI_SOURCE_FILES = \
 	rygel-av-transport.vala \
+	rygel-player-controller.vala \
 	rygel-rendering-control.vala \
 	rygel-sink-connection-manager.vala \
 	rygel-time-utils.vala \
diff --git a/src/librygel-renderer/rygel-av-transport.vala b/src/librygel-renderer/rygel-av-transport.vala
index e7ff5ca..74f5099 100644
--- a/src/librygel-renderer/rygel-av-transport.vala
+++ b/src/librygel-renderer/rygel-av-transport.vala
@@ -35,51 +35,21 @@ internal class Rygel.AVTransport : Service {
                     "urn:schemas-upnp-org:metadata-1-0/AVT/";
 
     private Session session;
+    private string protocol_info;
 
-    // The setters below update the LastChange message
-    private uint _n_tracks = 0;
-    public uint n_tracks {
-        get {
-            return this._n_tracks;
-        }
-
-        set {
-            this._n_tracks = value;
-
-            this.changelog.log ("NumberOfTracks", this._n_tracks.to_string ());
-        }
-    }
-
-    private uint _track = 0;
-    public uint track {
-        get {
-            return this._track;
-        }
+    public string track_metadata {
+        owned get { return this.player.metadata ?? ""; }
 
         set {
-            this._track = value;
-
-            this.changelog.log ("CurrentTrack", this._track.to_string ());
-        }
-    }
-
-    private string _metadata = "";
-    public string metadata {
-        owned get {
-            if (this._metadata != null) {
-                return Markup.escape_text (this._metadata);
+            if (value.has_prefix ("&lt;")) {
+                this.player.metadata = this.unescape (value);
             } else {
-                return "";
+                this.player.metadata = value;
             }
         }
-
-        set {
-            this._metadata = value;
-            this.player.metadata = value;
-        }
     }
 
-    public string uri {
+    public string track_uri {
         owned get {
             if (this.player.uri != null) {
                 return Markup.escape_text (this.player.uri);
@@ -134,10 +104,14 @@ internal class Rygel.AVTransport : Service {
 
     private ChangeLog changelog;
     private MediaPlayer player;
+    private PlayerController controller;
 
     public override void constructed () {
+        var plugin = this.root_device.resource_factory as MediaRendererPlugin;
+
         this.changelog = new ChangeLog (this, LAST_CHANGE_NS);
         this.player = this.get_player ();
+        this.controller = plugin.get_controller ();
 
         query_variable["LastChange"].connect (this.query_last_change_cb);
 
@@ -158,12 +132,18 @@ internal class Rygel.AVTransport : Service {
         action_invoked["Next"].connect (this.next_cb);
         action_invoked["Previous"].connect (this.previous_cb);
 
-        this.player.notify["playback-state"].connect (this.notify_state_cb);
+        this.controller.notify["playback-state"].connect (this.notify_state_cb);
+        this.controller.notify["n-tracks"].connect (this.notify_n_tracks_cb);
+        this.controller.notify["track"].connect (this.notify_track_cb);
+        this.controller.notify["uri"].connect (this.notify_uri_cb);
+        this.controller.notify["metadata"].connect (this.notify_meta_data_cb);
+
         this.player.notify["duration"].connect (this.notify_duration_cb);
-        this.player.notify["uri"].connect (this.notify_uri_cb);
-        this.player.notify["metadata"].connect (this.notify_meta_data_cb);
+        this.player.notify["uri"].connect (this.notify_track_uri_cb);
+        this.player.notify["metadata"].connect (this.notify_track_meta_data_cb);
 
         this.session = new SessionAsync ();
+        this.protocol_info = plugin.get_protocol_info ();
     }
 
     private MediaPlayer get_player () {
@@ -189,14 +169,16 @@ internal class Rygel.AVTransport : Service {
         log.log ("RecordMediumWriteStatus",      "NOT_IMPLEMENTED");
         log.log ("CurrentRecordQualityMode",     "NOT_IMPLEMENTED");
         log.log ("PossibleRecordQualityMode",    "NOT_IMPLEMENTED");
-        log.log ("NumberOfTracks",               this.n_tracks.to_string ());
-        log.log ("CurrentTrack",                 this.track.to_string ());
+        log.log ("NumberOfTracks",               this.controller.n_tracks.to_string ());
+        log.log ("CurrentTrack",                 this.controller.track.to_string ());
         log.log ("CurrentTrackDuration",         this.player.duration_as_str);
         log.log ("CurrentMediaDuration",         this.player.duration_as_str);
-        log.log ("CurrentTrackMetaData",         this.metadata);
-        log.log ("AVTransportURIMetaData",       this.metadata);
-        log.log ("CurrentTrackURI",              this.uri);
-        log.log ("AVTransportURI",               this.uri);
+        log.log ("CurrentTrackMetaData",
+                 Markup.escape_text (this.track_metadata));
+        log.log ("AVTransportURIMetaData",
+                 Markup.escape_text (this.controller.metadata));
+        log.log ("CurrentTrackURI",              this.track_uri);
+        log.log ("AVTransportURI",               this.controller.uri);
         log.log ("NextAVTransportURI",           "NOT_IMPLEMENTED");
         log.log ("NextAVTransportURIMetaData",   "NOT_IMPLEMENTED");
 
@@ -241,6 +223,8 @@ internal class Rygel.AVTransport : Service {
                         typeof (string),
                         out _metadata);
 
+        // remove current playlist handler
+        this.controller.set_playlist (null);
         if (_uri.has_prefix ("http://";) || _uri.has_prefix ("https://";)) {
             var message = new Message ("HEAD", _uri);
             message.request_headers.append ("getContentFeatures.dlna.org",
@@ -256,40 +240,53 @@ internal class Rygel.AVTransport : Service {
                     return;
                 } else {
                     var mime = msg.response_headers.get_one ("Content-Type");
+                    var features = msg.response_headers.get_one
+                                        ("contentFeatures.dlna.org");
+
                     if (mime != null &&
-                        !(mime in this.player.get_mime_types ())) {
+                        !(mime in this.player.get_mime_types () || mime ==
+                            "text/xml")) {
                         action.return_error (714, _("Illegal MIME-type"));
 
                         return;
                     }
-                    this.player.mime_type = mime;
-                    var features = msg.response_headers.get_one
-                                        ("contentFeatures.dlna.org");
 
-                    if (features != null) {
-                        this.player.content_features = features;
+                    this.controller.metadata = _metadata;
+                    this.controller.uri = _uri;
+
+                    if (mime == "text/xml" &&
+                        features.has_prefix ("DLNA.ORG_PN=DIDL_S")) {
+                        // Delay returning the action until we got some
+                        this.handle_playlist.begin (action);
                     } else {
-                        this.player.content_features = "*";
+                        // some other track
+                        this.player.mime_type = mime;
+                        if (features != null) {
+                            this.player.content_features = features;
+                        } else {
+                            this.player.content_features = "*";
+                        }
+
+                        // Track == Media
+                        this.track_metadata = _metadata;
+                        this.track_uri = _uri;
+                        this.controller.n_tracks = 1;
+                        this.controller.track = 1;
+
+                        action.return ();
                     }
-
-                    this.metadata = _metadata;
-                    this.uri = _uri;
-                    this.n_tracks = 1;
-                    this.track = 1;
-
-                    action.return ();
                 }
             });
             this.session.queue_message (message, null);
         } else {
-            this.metadata = _metadata;
-            this.uri = _uri;
+            this.controller.metadata = _metadata;
+            this.controller.uri = _uri;
             if (_uri == "") {
-                this.n_tracks = 0;
-                this.track = 0;
+                this.controller.n_tracks = 0;
+                this.controller.track = 0;
             } else {
-                this.n_tracks = 1;
-                this.track = 1;
+                this.controller.n_tracks = 1;
+                this.controller.track = 1;
             }
 
             action.return ();
@@ -302,18 +299,27 @@ internal class Rygel.AVTransport : Service {
             return;
         }
 
+        string media_duration;
+        if (this.controller.n_tracks > 1) {
+            // We don't know the size of the playlist. Might need change if we
+            // support playlists whose size we know in advance
+            media_duration = "0:00:00";
+        } else {
+            media_duration = this.player.duration_as_str;
+        }
+
         action.set ("NrTracks",
                         typeof (uint),
-                        this.n_tracks,
+                        this.controller.n_tracks,
                     "MediaDuration",
                         typeof (string),
-                        this.player.duration_as_str,
+                        media_duration,
                     "CurrentURI",
                         typeof (string),
-                        this.uri,
+                        this.controller.uri,
                     "CurrentURIMetaData",
                         typeof (string),
-                        this.metadata,
+                        this.controller.metadata,
                     "NextURI",
                         typeof (string),
                         "NOT_IMPLEMENTED",
@@ -339,21 +345,30 @@ internal class Rygel.AVTransport : Service {
             return;
         }
 
+        string media_duration;
+        if (this.controller.n_tracks > 1) {
+            // We don't know the size of the playlist. Might need change if we
+            // support playlists whose size we know in advance
+            media_duration = "0:00:00";
+        } else {
+            media_duration = this.player.duration_as_str;
+        }
+
         action.set ("CurrentType",
                         typeof (string),
                         "NO_MEDIA",
                     "NrTracks",
                         typeof (uint),
-                        this.n_tracks,
+                        this.controller.n_tracks,
                     "MediaDuration",
                         typeof (string),
-                        this.player.duration_as_str,
+                        media_duration,
                     "CurrentURI",
                         typeof (string),
-                        this.uri,
+                        this.controller.uri,
                     "CurrentURIMetaData",
                         typeof (string),
-                        this.metadata,
+                        this.controller.metadata,
                     "NextURI",
                         typeof (string),
                         "NOT_IMPLEMENTED",
@@ -401,16 +416,16 @@ internal class Rygel.AVTransport : Service {
 
         action.set ("Track",
                         typeof (uint),
-                        this.track,
+                        this.controller.track,
                     "TrackDuration",
                         typeof (string),
                         this.player.duration_as_str,
                     "TrackMetaData",
                         typeof (string),
-                        this.metadata,
+                        this.track_metadata,
                     "TrackURI",
                         typeof (string),
-                        this.uri,
+                        this.track_uri,
                     "RelTime",
                         typeof (string),
                         this.player.position_as_str,
@@ -534,6 +549,21 @@ internal class Rygel.AVTransport : Service {
             action.return ();
 
             return;
+        case "TRACK_NR":
+            debug ("Setting track to %s.", target);
+            var track = int.parse (target);
+
+            if (track < 1 || track > this.controller.n_tracks) {
+                action.return_error (711, _("Illegal seek target"));
+
+                return;
+            }
+
+            this.controller.track = track;
+
+            action.return();
+
+            break;
         default:
             action.return_error (710, _("Seek mode not supported"));
 
@@ -542,31 +572,96 @@ internal class Rygel.AVTransport : Service {
     }
 
     private void next_cb (Service service, ServiceAction action) {
-        action.return_error (701, _("Transition not available"));
+        if (this.controller.next ()) {
+            action.return ();
+        } else {
+            action.return_error (711, _("Illegal seek target"));
+        }
     }
 
     private void previous_cb (Service service, ServiceAction action) {
-        action.return_error (701, _("Transition not available"));
+        if (this.controller.previous ()) {
+            action.return ();
+        } else {
+            action.return_error (711, _("Illegal seek target"));
+        }
     }
 
     private void notify_state_cb (Object player, ParamSpec p) {
-        this.changelog.log ("TransportState", this.player.playback_state);
+        var state = this.player.playback_state;
+        this.changelog.log ("TransportState", state);
+    }
+
+    private void notify_n_tracks_cb (Object player, ParamSpec p) {
+        this.changelog.log ("NumberOfTracks",
+                            this.controller.n_tracks.to_string ());
     }
 
-    private void notify_duration_cb (Object player, ParamSpec p) {
+    private void notify_track_cb (Object player, ParamSpec p) {
+        this.changelog.log ("CurrentTrack",
+                            this.controller.track.to_string ());
+    }
+
+   private void notify_duration_cb (Object player, ParamSpec p) {
         this.changelog.log ("CurrentTrackDuration",
                             this.player.duration_as_str);
         this.changelog.log ("CurrentMediaDuration",
                             this.player.duration_as_str);
     }
 
+    private void notify_track_uri_cb (Object player, ParamSpec p) {
+        this.changelog.log ("CurrentTrackURI", this.track_uri);
+    }
+
     private void notify_uri_cb (Object player, ParamSpec p) {
-        this.changelog.log ("CurrentTrackURI", this.uri);
-        this.changelog.log ("AVTransportURI", this.uri);
+        this.changelog.log ("AVTransportURI", this.controller.uri);
+    }
+
+    private void notify_track_meta_data_cb (Object player, ParamSpec p) {
+        this.changelog.log ("CurrentTrackMetaData",
+                            Markup.escape_text (this.track_metadata));
     }
 
     private void notify_meta_data_cb (Object player, ParamSpec p) {
-        this._metadata = this.player.metadata;
-        this.changelog.log ("CurrentTrackMetadata", this.metadata);
+        this.changelog.log ("AVTransportURIMetaData",
+                            Markup.escape_text (this.controller.metadata));
+    }
+
+    private async void handle_playlist (ServiceAction action) {
+        var message = new Message ("GET", this.controller.uri);
+        this.session.queue_message (message, () => {
+            handle_playlist.callback ();
+        });
+        yield;
+
+        if (message.status_code != 200) {
+            action.return_error (716, _("Resource not found"));
+
+            return;
+        }
+
+        unowned string xml_string = (string) message.response_body.data;
+
+        var collection = new MediaCollection.from_string (xml_string);
+        if (collection.get_items ().length () == 0) {
+            // FIXME: Return a more sensible error here.
+            action.return_error (716, _("Resource not found"));
+
+            return;
+        }
+
+        this.controller.set_playlist (collection);
+
+        action.return ();
+    }
+
+    private string unescape (string input) {
+        var result = input.replace ("&quot;", "\"");
+        result = result.replace ("&lt;", "<");
+        result = result.replace ("&gt;", ">");
+        result = result.replace ("&apos;", "'");
+        result = result.replace ("&amp;", "&");
+
+        return result;
     }
 }
diff --git a/src/librygel-renderer/rygel-media-renderer-plugin.vala b/src/librygel-renderer/rygel-media-renderer-plugin.vala
index 872963b..a9e4ca1 100644
--- a/src/librygel-renderer/rygel-media-renderer-plugin.vala
+++ b/src/librygel-renderer/rygel-media-renderer-plugin.vala
@@ -35,6 +35,9 @@ public class Rygel.MediaRendererPlugin : Rygel.Plugin {
                                 BuildConfig.DATA_DIR +
                                 "/xml/MediaRenderer2.xml";
 
+    private string sink_protocol_info;
+    private PlayerController controller;
+
     /**
      * Create an instance of the plugin.
      *
@@ -66,9 +69,47 @@ public class Rygel.MediaRendererPlugin : Rygel.Plugin {
                                      RenderingControl.DESCRIPTION_PATH,
                                      typeof (RenderingControl));
         this.add_resource (resource);
+
+        this.controller = new PlayerController (this.get_player (),
+                                                this.get_protocol_info ());
     }
 
     public virtual MediaPlayer? get_player () {
         return null;
     }
+
+    internal PlayerController get_controller () {
+        return this.controller;
+    }
+
+    public string get_protocol_info () {
+        var player = this.get_player ();
+        if (player == null) {
+            return "";
+        }
+
+        if (this.sink_protocol_info == null) {
+            this.sink_protocol_info = "";
+            var protocols = player.get_protocols ();
+
+            this.sink_protocol_info += "http-get:*:text/xml:DLNA.ORG_PN=DIDL_S,";
+
+            var mime_types = player.get_mime_types ();
+            foreach (var protocol in protocols) {
+                if (protocols[0] != protocol) {
+                    this.sink_protocol_info += ",";
+                }
+
+                foreach (var mime_type in mime_types) {
+                    if (mime_types[0] != mime_type) {
+                        this.sink_protocol_info += ",";
+                    }
+
+                    this.sink_protocol_info += protocol + ":*:" + mime_type + ":*";
+                }
+            }
+        }
+
+        return this.sink_protocol_info;
+    }
 }
diff --git a/src/librygel-renderer/rygel-player-controller.vala b/src/librygel-renderer/rygel-player-controller.vala
new file mode 100644
index 0000000..d1d0a25
--- /dev/null
+++ b/src/librygel-renderer/rygel-player-controller.vala
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2012 Intel Corporation.
+ *
+ * Author: Jens Georg <jensg openismus com>
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using GUPnP;
+
+/**
+ * This class keeps track of global states that are not dependant on the
+ * RygelMediaPlayer.
+ *
+ * These states are:
+ * # URI
+ * # MetaData
+ * # Number of tracks
+ * # Current track
+ * # Playback state
+ *
+ * In case of playlists this class will also control the player. It needs to
+ * proxy the playback state to react on end of item to be able to switch to
+ * the next item.
+ */
+internal class Rygel.PlayerController : Object {
+    private const int DEFAULT_IMAGE_TIMEOUT = 15;
+    private const string CONFIG_SECTION = "Renderer";
+    private const string TIMEOUT_KEY = "image-timeout";
+    private const string DIDL_FRAME_TEMPLATE = "<DIDL-Lite " +
+        "xmlns:dc=\"http://purl.org/dc/elements/1.1/\"; " +
+        "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" " +
+        "xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\" " +
+        "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">" +
+        "%s</DIDL-Lite>";
+
+    /* private (construction) properties */
+    public MediaPlayer player { construct; private get; }
+    public string protocol_info { construct; private get; }
+
+    /* public properties */
+    public string playback_state { get; set; default = "NO_MEDIA_PRESENT"; }
+    public uint n_tracks { get; set; default = 0; }
+    public uint track {
+        get { return this._track; }
+        set { this._track = value; this.apply_track (); }
+        default = 0;
+    }
+    public string uri { get; set; default = ""; }
+    public string metadata {
+        owned get { return this._metadata ?? ""; }
+        set { this._metadata = this.unescape (value); }
+        default = "";
+    }
+
+    private MediaCollection collection;
+    private List<DIDLLiteItem> collection_items;
+    private uint timeout_id;
+    private uint default_image_timeout;
+    private Configuration config;
+
+    // Private property variables
+    private string _metadata;
+    private uint _track;
+
+    public PlayerController (MediaPlayer player, string protocol_info) {
+        Object (player : player, protocol_info : protocol_info);
+    }
+
+    public override void constructed () {
+        this.player.notify["playback-state"].connect (this.notify_state_cb);
+
+        this.config = MetaConfig.get_default ();
+        this.config.setting_changed.connect (this.on_setting_changed);
+        this.default_image_timeout = DEFAULT_IMAGE_TIMEOUT;
+        this.on_setting_changed (CONFIG_SECTION, TIMEOUT_KEY);
+    }
+
+    public bool next () {
+        if (this.track + 1 > this.n_tracks) {
+            return false;
+        }
+
+        this.track++;
+
+        return true;
+    }
+
+    public bool previous () {
+        if (this.track <= 1) {
+            return false;
+        }
+
+        this.track--;
+
+        return true;
+    }
+
+    public void set_playlist (MediaCollection? collection) {
+        this.collection = collection;
+        if (this.timeout_id != 0) {
+            this.timeout_id = 0;
+            Source.remove (this.timeout_id);
+        }
+
+        if (this.collection != null) {
+            this.collection_items = collection.get_items ();
+            this.n_tracks = this.collection_items.length ();
+            this.track = 1;
+        } else {
+            this.collection_items = null;
+        }
+    }
+
+    private void notify_state_cb (Object player, ParamSpec p) {
+        var state = this.player.playback_state;
+        if (state == "EOS") {
+            if (this.collection == null) {
+                // Just move to stop
+                Idle.add (() => {
+                    this.player.playback_state = "STOPPED";
+
+                    return false;
+                });
+
+                return;
+            } else {
+                // Set next playlist item
+                if (!this.next ()) {
+                    // We were at the end of the list; as per DLNA, move to
+                    // STOPPED and let current track be 1.
+                    this.reset ();
+                }
+            }
+        } else {
+            // just forward
+            this.playback_state = state;
+        }
+    }
+
+    private void apply_track () {
+        // We only have something to do here if we have collection items
+        if (this.collection_items != null) {
+            var item = this.collection_items.nth (this.track - 1).data;
+
+            var res = item.get_compat_resource (this.protocol_info, true);
+            this.player.metadata = DIDL_FRAME_TEMPLATE.printf
+                                        (item.get_xml_string ());
+            this.player.uri = res.get_uri ();
+            if (item.upnp_class.has_prefix ("object.item.image") &&
+                this.collection != null) {
+                this.setup_image_timeouts (item.lifetime);
+            }
+        }
+    }
+
+    private void reset () {
+        this.player.playback_state = "STOPPED";
+        this.track = 1;
+    }
+
+    private void setup_image_timeouts (long lifetime) {
+        // For images, we handle the timeout here. Either the item carries a
+        // dlna:lifetime tag, then we use that or we use a default timeout of
+        // 5 minutes.
+        var timeout = this.default_image_timeout;
+        if (lifetime > 0) {
+            timeout = (uint) lifetime;
+        }
+
+        debug ("Item is image, setup timer: %ld", timeout);
+
+        if (this.timeout_id != 0) {
+            Source.remove (this.timeout_id);
+        }
+
+        this.timeout_id = Timeout.add_seconds ((uint) timeout, () => {
+            this.timeout_id = 0;
+            if (!this.next ()) {
+                this.reset ();
+            }
+
+            return false;
+        });
+    }
+
+    private void on_setting_changed (string section, string key) {
+        if (section != CONFIG_SECTION && key != TIMEOUT_KEY) {
+            return;
+        }
+
+        try {
+            this.default_image_timeout = config.get_int (CONFIG_SECTION,
+                                                         TIMEOUT_KEY,
+                                                         0,
+                                                         int.MAX);
+        } catch (Error error) {
+            this.default_image_timeout = DEFAULT_IMAGE_TIMEOUT;
+        }
+
+        debug ("New image timeout: %lu", this.default_image_timeout);
+    }
+
+    private string unescape (string input) {
+        var result = input.replace ("&quot;", "\"");
+        result = result.replace ("&lt;", "<");
+        result = result.replace ("&gt;", ">");
+        result = result.replace ("&apos;", "'");
+        result = result.replace ("&amp;", "&");
+
+        return result;
+    }
+}
diff --git a/src/librygel-renderer/rygel-sink-connection-manager.vala b/src/librygel-renderer/rygel-sink-connection-manager.vala
index cea868c..beb30c0 100644
--- a/src/librygel-renderer/rygel-sink-connection-manager.vala
+++ b/src/librygel-renderer/rygel-sink-connection-manager.vala
@@ -24,8 +24,6 @@
 using GUPnP;
 
 internal class Rygel.SinkConnectionManager : Rygel.ConnectionManager {
-    private MediaPlayer player;
-
     public override void constructed () {
         base.constructed ();
 
@@ -34,22 +32,6 @@ internal class Rygel.SinkConnectionManager : Rygel.ConnectionManager {
         this.direction = "Input";
 
         var plugin = this.root_device.resource_factory as MediaRendererPlugin;
-        this.player = plugin.get_player ();
-        var protocols = this.player.get_protocols ();
-
-        foreach (var protocol in protocols) {
-            if (protocols[0] != protocol) {
-                this.sink_protocol_info += ",";
-            }
-            var mime_types = this.player.get_mime_types ();
-
-            foreach (var mime_type in mime_types) {
-                if (mime_types[0] != mime_type) {
-                    this.sink_protocol_info += ",";
-                }
-
-                this.sink_protocol_info += protocol + ":*:" + mime_type + ":*";
-            }
-        }
+        this.sink_protocol_info = plugin.get_protocol_info ();
     }
 }



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]