[rygel] server: Add support for encrypted content



commit 68329a27c7be10869c71f650f50bdeab3f3f77c2
Author: Jens Georg <mail jensge org>
Date:   Mon Feb 16 23:02:44 2015 +0100

    server: Add support for encrypted content
    
    Code based on Cablelabs*s CVP-2 implementation
    
    Signed-off-by: Jens Georg <mail jensge org>

 src/librygel-server/filelist.am                    |    4 +-
 .../rygel-dtcp-cleartext-request.vala              |  145 +++++++++++++++++++
 .../rygel-dtcp-cleartext-response.vala             |   97 +++++++++++++
 src/librygel-server/rygel-http-get.vala            |   24 +++-
 .../rygel-http-resource-handler.vala               |    9 +-
 src/librygel-server/rygel-http-server.vala         |   14 ++
 src/librygel-server/rygel-media-object.vala        |   28 ++++-
 src/librygel-server/rygel-media-resource.vala      |  150 +++++++++++++-------
 8 files changed, 411 insertions(+), 60 deletions(-)
---
diff --git a/src/librygel-server/filelist.am b/src/librygel-server/filelist.am
index e2ed3b2..f46a0c2 100644
--- a/src/librygel-server/filelist.am
+++ b/src/librygel-server/filelist.am
@@ -84,4 +84,6 @@ LIBRYGEL_SERVER_NONVAPI_SOURCE_FILES = \
        rygel-data-sink.vala \
        rygel-playspeed.vala \
        rygel-playspeed-request.vala \
-       rygel-playspeed-response.vala
+       rygel-playspeed-response.vala \
+       rygel-dtcp-cleartext-request.vala \
+       rygel-dtcp-cleartext-response.vala
diff --git a/src/librygel-server/rygel-dtcp-cleartext-request.vala 
b/src/librygel-server/rygel-dtcp-cleartext-request.vala
new file mode 100644
index 0000000..c116c7f
--- /dev/null
+++ b/src/librygel-server/rygel-dtcp-cleartext-request.vala
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2013  Cable Television Laboratories, Inc.
+ *
+ * Author: Craig Pratt <craig ecaspia com>
+ *
+ * This file is part of Rygel.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CABLE TELEVISION LABORATORIES
+ * INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+using GUPnP;
+
+public class Rygel.DTCPCleartextRequest : Rygel.HTTPSeekRequest {
+    public static const string DTCP_RANGE_HEADER = "Range.dtcp.com";
+
+    /**
+     * The start of the cleartext range in bytes
+     */
+    public int64 start_byte { get; private set; }
+
+    /**
+     * The end of the cleartext range in bytes (inclusive). May be HTTPSeekRequest.UNSPECIFIED
+     */
+    public int64 end_byte { get; private set; }
+
+    /**
+     * The length of the cleartext range in bytes. May be HTTPSeekRequest.UNSPECIFIED
+     */
+    public int64 range_length { get; private set; }
+
+    /**
+     * The length of the cleartext resource in bytes. May be HTTPSeekRequest.UNSPECIFIED
+     */
+    public int64 total_size { get; private set; }
+
+    public DTCPCleartextRequest (HTTPGet request) throws HTTPSeekRequestError,
+                                                         HTTPRequestError {
+        base ();
+
+        int64 start, end, total_size;
+
+        // It's only possible to get the cleartext size from a MediaResource
+        //  (and only if it is link protected)
+        if (request.handler is HTTPMediaResourceHandler) {
+            MediaResource resource = (request.handler as HTTPMediaResourceHandler)
+                                     .media_resource;
+            total_size = resource.cleartext_size;
+            if (total_size <= 0) {
+                // Even if it's a resource and the content is link-protected, it may have an
+                // unknown cleartext size (e.g. if it's live/in-progress content). This doesn't
+                // mean the request is invalid, it just means the total size is non-static
+                total_size = UNSPECIFIED;
+            }
+        } else {
+            total_size = UNSPECIFIED;
+        }
+
+        unowned string range = request.msg.request_headers.get_one (DTCP_RANGE_HEADER);
+
+        if (range == null) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ( "%s request header not present",
+                                                           DTCP_RANGE_HEADER );
+        }
+
+        if (!range.has_prefix ("bytes")) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ( "Invalid %s value (missing bytes field): '%s'",
+                                                           DTCP_RANGE_HEADER, range );
+        }
+
+        var range_tokens = range.substring (6).split ("-", 2); // skip "bytes="
+        if (range_tokens[0].length == 0) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ( "No range start specified: '%s'",
+                                                           range );
+        }
+
+        if (!int64.try_parse (range_tokens[0], out start) || (start < 0)) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ( "Invalid %s range start: '%s'",
+                                                           DTCP_RANGE_HEADER, range );
+        }
+        // valid range start specified
+
+        // Look for a range end...
+        if (range_tokens[1].length == 0) {
+            end = UNSPECIFIED;
+        } else {
+            if (!int64.try_parse (range_tokens[1], out end) || (end <= 0)) {
+                throw new HTTPSeekRequestError.INVALID_RANGE ( "Invalid %s range end: '%s'",
+                                                               DTCP_RANGE_HEADER, range );
+            }
+            // valid end range specified
+        }
+
+        if ((end != UNSPECIFIED) && (start > end)) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ( "Invalid %s range - start > end: '%s'",
+                                                           DTCP_RANGE_HEADER, range );
+        }
+
+        if ((total_size != UNSPECIFIED) && (start > total_size-1)) {
+            throw new HTTPSeekRequestError.OUT_OF_RANGE ( "Invalid %s range - start > length: '%s'",
+                                                           DTCP_RANGE_HEADER, range );
+        }
+
+        if ((total_size != UNSPECIFIED) && (end > total_size-1)) {
+            // It's not clear from the DLNA link protection spec if the range end can be beyond
+            //  the total length. We'll assume RFC 2616 14.35.1 semantics. But note that having
+            //  an end with an unspecified size will be normal for live/in-progress content
+            end = total_size-1;
+        }
+
+        this.start_byte = start;
+        this.end_byte = end;
+        this.range_length = (end == UNSPECIFIED) ? UNSPECIFIED
+                                                : end-start+1; // +1, since range is inclusive
+        this.total_size = total_size;
+    }
+
+    public static bool supported (HTTPGet request) {
+        return (request.handler is HTTPMediaResourceHandler)
+               && (request.handler as HTTPMediaResourceHandler)
+                  .media_resource.is_cleartext_range_support_enabled ();
+    }
+
+    public static bool requested (HTTPGet request) {
+        return (request.msg.request_headers.get_one (DTCP_RANGE_HEADER) != null);
+    }
+}
diff --git a/src/librygel-server/rygel-dtcp-cleartext-response.vala 
b/src/librygel-server/rygel-dtcp-cleartext-response.vala
new file mode 100644
index 0000000..91d055c
--- /dev/null
+++ b/src/librygel-server/rygel-dtcp-cleartext-response.vala
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2013  Cable Television Laboratories, Inc.
+ *
+ * Author: Craig Pratt <craig ecaspia com>
+ *
+ * This file is part of Rygel.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CABLE TELEVISION LABORATORIES
+ * INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+using GUPnP;
+
+public class Rygel.DTCPCleartextResponse : Rygel.HTTPResponseElement {
+    public static const string DTCP_CONTENT_RANGE_HEADER = "Content-Range.dtcp.com";
+
+    /**
+     * The start of the response range in bytes
+     */
+    public int64 start_byte { get; private set; }
+
+    /**
+     * The end of the range in bytes (inclusive)
+     */
+    public int64 end_byte { get; private set; }
+
+    /**
+     * The length of the range in bytes
+     */
+    public int64 range_length { get; private set; }
+
+    /**
+     * The length of the resource in bytes. May be HTTPSeekRequest.UNSPECIFIED
+     */
+    public int64 total_size { get; private set; }
+
+    /**
+     * The encrypted length of the response
+     */
+    public int64 encrypted_length { get; public set;}
+
+    public DTCPCleartextResponse (int64 start_byte, int64 end_byte, int64 total_size,
+                                  int64 encrypted_length = UNSPECIFIED) {
+        this.start_byte = start_byte;
+        this.end_byte = end_byte;
+        this.range_length = end_byte - start_byte + 1; // +1, since range is inclusive
+        this.total_size = total_size;
+        this.encrypted_length = encrypted_length;
+    }
+
+    public DTCPCleartextResponse.from_request (DTCPCleartextRequest request,
+                                               int64 encrypted_length = UNSPECIFIED) {
+        this.start_byte = request.start_byte;
+        this.end_byte = request.end_byte;
+        this.range_length = request.range_length;
+        this.total_size = request.total_size;
+        this.encrypted_length = encrypted_length;
+    }
+
+    public override void add_response_headers (Rygel.HTTPRequest request) {
+        // Content-Range.dtcp.com: bytes START_BYTE-END_BYTE/TOTAL_LENGTH (or "*")
+        if (this.start_byte != UNSPECIFIED) {
+            string response = "bytes " + this.start_byte.to_string ()
+                              + "-" + this.end_byte.to_string () + "/"
+                              + ( (this.total_size == UNSPECIFIED) ? "*"
+                                  : this.total_size.to_string () );
+
+            request.msg.response_headers.append (DTCP_CONTENT_RANGE_HEADER, response);
+        }
+        if (this.encrypted_length != UNSPECIFIED) {
+            request.msg.response_headers.set_content_length (this.encrypted_length);
+        }
+    }
+
+    public override string to_string () {
+        return ("DTCPCleartextResponse(bytes=%lld-%lld/%lld, enc_len=%lld)"
+                .printf (this.start_byte, this.end_byte, this.total_size, this.encrypted_length));
+    }
+}
diff --git a/src/librygel-server/rygel-http-get.vala b/src/librygel-server/rygel-http-get.vala
index 1b759ec..3a49155 100644
--- a/src/librygel-server/rygel-http-get.vala
+++ b/src/librygel-server/rygel-http-get.vala
@@ -118,8 +118,21 @@ public class Rygel.HTTPGet : HTTPRequest {
         var requested_time_seek = HTTPTimeSeekRequest.requested (this);
         var supports_byte_seek = HTTPByteSeekRequest.supported (this);
         var requested_byte_seek = HTTPByteSeekRequest.requested (this);
+        var supports_cleartext_seek = DTCPCleartextRequest.supported (this);
+        var requested_cleartext_seek = DTCPCleartextRequest.requested (this);
 
-        if (requested_byte_seek) {
+        // Order is significant here when the request has more than one seek header
+        if (requested_cleartext_seek) {
+            if (!supports_cleartext_seek) {
+                throw new HTTPRequestError.UNACCEPTABLE ( "Cleartext seek not supported for "
+                                                          + this.uri.to_string () );
+            }
+            if (requested_byte_seek) {
+                    // Per DLNA Link Protection 7.6.4.3.3.9
+                    throw new HTTPRequestError.UNACCEPTABLE ( "Both Cleartext and Range seek requested "
+                                                              + this.uri.to_string ());
+            }
+        } else if (requested_byte_seek) {
             if (!supports_byte_seek) {
                 throw new HTTPRequestError.UNACCEPTABLE ( "Byte seek not supported for "
                                                           + this.uri.to_string () );
@@ -137,7 +150,7 @@ public class Rygel.HTTPGet : HTTPRequest {
         // Note: We need to check the speed first since direction factors into validating
         //       the time-seek request
         try {
-            if ( !requested_byte_seek
+            if ( !(requested_byte_seek || requested_cleartext_seek)
                  && PlaySpeedRequest.requested (this) ) {
                 this.speed_request = new PlaySpeedRequest.from_request (this);
                 debug ("Processing playspeed %s", speed_request.speed.to_string ());
@@ -164,7 +177,12 @@ public class Rygel.HTTPGet : HTTPRequest {
         }
         try {
             // Order is intentional here
-            if (supports_byte_seek && requested_byte_seek) {
+            if (supports_cleartext_seek && requested_cleartext_seek) {
+                var cleartext_seek = new DTCPCleartextRequest (this);
+                debug ("Processing DTCP cleartext byte range request (bytes %lld to %lld)",
+                         cleartext_seek.start_byte, cleartext_seek.end_byte);
+                this.seek = cleartext_seek;
+            } else if (supports_byte_seek && requested_byte_seek) {
                 var byte_seek = new HTTPByteSeekRequest (this);
                 debug ("Processing byte range request (bytes %lld to %lld)",
                        byte_seek.start_byte, byte_seek.end_byte);
diff --git a/src/librygel-server/rygel-http-resource-handler.vala 
b/src/librygel-server/rygel-http-resource-handler.vala
index 2b0b78e..1c8009c 100644
--- a/src/librygel-server/rygel-http-resource-handler.vala
+++ b/src/librygel-server/rygel-http-resource-handler.vala
@@ -46,11 +46,14 @@ internal class Rygel.HTTPMediaResourceHandler : HTTPGetHandler {
     public override void add_response_headers (HTTPGet request)
                                                throws HTTPRequestError {
         request.http_server.set_resource_delivery_options (this.media_resource);
-        request.msg.response_headers.append ("Content-Type",
-                                             this.media_resource.mime_type);
+        var replacements = request.http_server.get_replacements ();
+        var mime_type = MediaObject.apply_replacements
+                                     (replacements,
+                                      this.media_resource.mime_type);
+        request.msg.response_headers.append ("Content-Type", mime_type);
 
         // Add contentFeatures.dlna.org
-        var protocol_info = this.media_resource.get_protocol_info ();
+        var protocol_info = media_resource.get_protocol_info (replacements);
         if (protocol_info != null) {
             var pi_fields = protocol_info.to_string ().split (":", 4);
             if (pi_fields[3] != null) {
diff --git a/src/librygel-server/rygel-http-server.vala b/src/librygel-server/rygel-http-server.vala
index f72c4d8..8f12b7a 100644
--- a/src/librygel-server/rygel-http-server.vala
+++ b/src/librygel-server/rygel-http-server.vala
@@ -6,6 +6,7 @@
  * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
  *                               <zeeshan ali nokia com>
  *         Jens Georg <jensg openismus com>
+ *         Doug Galligan <doug sentosatech com>
  *         Craig Pratt <craig ecaspia com>
  *
  * This file is part of Rygel.
@@ -36,6 +37,7 @@ public class Rygel.HTTPServer : GLib.Object, Rygel.StateMachine {
     public GUPnP.Context context;
     private ArrayList<HTTPRequest> requests;
     private bool locally_hosted;
+    public HashTable<string, string> replacements;
 
     public Cancellable cancellable { get; set; }
 
@@ -53,6 +55,14 @@ public class Rygel.HTTPServer : GLib.Object, Rygel.StateMachine {
                               this.context.host_ip == "127.0.0.1";
 
         this.path_root = "/" + name;
+        this.replacements = new HashTable <string, string> (str_hash, str_equal);
+        this.replacements.insert ("@SERVICE_ADDRESS@",
+                                  this.context.host_ip);
+        this.replacements.insert ("@SERVICE_INTERFACE@",
+                                  this.context.interface);
+        this.replacements.insert ("@SERVICE_PORT@",
+                                  this.context.port.to_string ());
+        this.replacements.insert ("@HOSTNAME@", Environment.get_host_name ());
     }
 
     public async void run () {
@@ -111,6 +121,10 @@ public class Rygel.HTTPServer : GLib.Object, Rygel.StateMachine {
         return new ArrayList<ProtocolInfo>();
     }
 
+    public HashTable<string, string> get_replacements () {
+        return this.replacements;
+    }
+
     public bool is_local () {
         return this.locally_hosted;
     }
diff --git a/src/librygel-server/rygel-media-object.vala b/src/librygel-server/rygel-media-object.vala
index 2eb9279..8523370 100644
--- a/src/librygel-server/rygel-media-object.vala
+++ b/src/librygel-server/rygel-media-object.vala
@@ -252,7 +252,8 @@ public abstract class Rygel.MediaObject : GLib.Object {
     public void serialize_resource_list (DIDLLiteObject didl_object,
                                          HTTPServer     http_server)
                                          throws Error {
-        foreach (var res in this.get_resource_list ()) {
+        var replacements = http_server.get_replacements ();
+        foreach (var res in get_resource_list ()) {
             if (res.uri == null || res.uri == "") {
                 var uri = http_server.create_uri_for_object (this,
                                                              -1,
@@ -266,7 +267,7 @@ public abstract class Rygel.MediaObject : GLib.Object {
                 }
                 var didl_resource = didl_object.add_resource ();
                 http_server.set_resource_delivery_options (res);
-                res.serialize (didl_resource);
+                res.serialize (didl_resource, replacements);
                 res.uri = null;
                 res.import_uri = null;
             } else {
@@ -275,7 +276,7 @@ public abstract class Rygel.MediaObject : GLib.Object {
                     if (protocol != "internal" || http_server.is_local ()) {
                         // Exclude internal resources when request is non-local
                         var didl_resource = didl_object.add_resource ();
-                        res.serialize (didl_resource);
+                        res.serialize (didl_resource, replacements);
                     }
                 } catch (Error e) {
                     warning (_("Could not determine protocol for %s"), res.uri);
@@ -285,6 +286,27 @@ public abstract class Rygel.MediaObject : GLib.Object {
     }
 
     /**
+     * Replace each key in replacement_pairs with its corresponding
+     * value in the source_string and return the result.
+     *
+     * If source_string is null, null is returned.
+     */
+    public static string ? apply_replacements
+                            (HashTable<string, string> replacement_pairs,
+                             string source_string) {
+        if (source_string == null) {
+            return null;
+        }
+        var replaced_string = source_string;
+        replacement_pairs.foreach ((search_string, replacement)
+            => {
+                    replaced_string
+                        = replaced_string.replace (search_string, replacement);
+               } );
+        return replaced_string;
+    }
+
+    /**
      * Create a stream source for the given resource
      */
     public abstract DataSource? create_stream_source_for_resource
diff --git a/src/librygel-server/rygel-media-resource.vala b/src/librygel-server/rygel-media-resource.vala
index 178412b..3b3494d 100644
--- a/src/librygel-server/rygel-media-resource.vala
+++ b/src/librygel-server/rygel-media-resource.vala
@@ -62,9 +62,10 @@ public class Rygel.MediaResource : GLib.Object {
     public DLNAOperation dlna_operation { get; set; default = DLNAOperation.NONE; }
 
     // I know gupnp-av DIDLLiteResource and ProtocolInfo structures have the above fields.
-    //  But both proved to be problematic in their current form. This class can be
-    //  refactored if/when these classes are made more more flexible. For now, this class
-    //  needs to serve the needs of Rygel first and foremost...
+    //  But both proved to be problematic in their current form when used in a variety
+    //  of containers (appears to be issues with reference management causing (incomplete)
+    //  copies of DIDLLiteResource/ProtocolInfo to be made). This class can be refactored
+    //  if/when these classes are made more flexible.
 
     public MediaResource (string name) {
         this.name = name;
@@ -100,44 +101,11 @@ public class Rygel.MediaResource : GLib.Object {
         this.dlna_operation = that.dlna_operation;
     }
 
-    public static string []? copy_speeds (string? [] src) {
-        if (src == null) {
-            return null;
-        }
-        var new_speeds = new string[src.length];
-        int speed_index = 0;
-        foreach (var speed in src) {
-            new_speeds[speed_index++] = speed;
-        }
-
-        return new_speeds;
-    }
-
-    public MediaResource dup () {
-        return new MediaResource.from_resource (this.get_name (), this);
-    }
-
-    public string get_name () {
-        return this.name;
-    }
-
-    private HashMap<string,string> property_table = new HashMap<string,string> ();
-
-    public void set_custom_property (string ? name, string ? value) {
-        property_table.set (name,value);
-    }
-
-    public string get_custom_property (string ? name) {
-        return property_table.get (name);
-    }
-
-    public Set get_custom_property_names () {
-        return property_table.keys;
-    }
-
-    public void apply_didl_lite (DIDLLiteResource didl_resource) {
-        //  Populate the MediaResource from the given DIDLLiteResource
+    public MediaResource.from_didl_lite_resource (string name, DIDLLiteResource didl_resource) {
+        // Create a MediaResource from the given DIDLLiteResource
         // Note: For a DIDLLiteResource, a value of -1/null also signals "not set"
+        this.name = name;
+        // res block
         this.uri = didl_resource.uri;
         this.size = didl_resource.size64;
         this.cleartext_size = didl_resource.cleartext_size;
@@ -149,6 +117,7 @@ public class Rygel.MediaResource : GLib.Object {
         this.height = didl_resource.height;
         this.audio_channels = didl_resource.audio_channels;
         this.sample_freq = didl_resource.sample_freq;
+        // protocol info
         if (didl_resource.protocol_info != null) {
             this.protocol = didl_resource.protocol_info.protocol;
             this.mime_type = didl_resource.protocol_info.mime_type;
@@ -161,10 +130,37 @@ public class Rygel.MediaResource : GLib.Object {
         }
     }
 
-    public DIDLLiteResource serialize (DIDLLiteResource didl_resource) {
-        // Note: For a DIDLLiteResource, a value of -1/null also signals "not set"
-        didl_resource.uri = this.uri;
-        didl_resource.import_uri = this.import_uri;
+    public MediaResource dup () {
+        return new MediaResource.from_resource (this.get_name (), this);
+    }
+
+    public static string []? copy_speeds (string? [] src) {
+        if (src == null) {
+            return null;
+        }
+        var new_speeds = new string[src.length];
+        int speed_index = 0;
+        foreach (var speed in src) {
+            new_speeds[speed_index++] = speed;
+        }
+
+        return new_speeds;
+    }
+
+    public string get_name () {
+        return this.name;
+    }
+
+    public DIDLLiteResource serialize
+                               (DIDLLiteResource didl_resource,
+                                HashTable<string, string> ? replacements) {
+        // Note: For a DIDLLiteResource, a values -1/null also signal "not set"
+        if (replacements == null) {
+            didl_resource.uri = this.uri;
+        } else {
+            didl_resource.uri = MediaObject.apply_replacements (replacements,
+                                                                this.uri);
+        }
         didl_resource.size64 = this.size;
         didl_resource.cleartext_size = this.cleartext_size;
         didl_resource.duration = this.duration;
@@ -191,12 +187,18 @@ public class Rygel.MediaResource : GLib.Object {
         this.play_speeds = copy_speeds (pi.play_speeds);
     }
 
-    public ProtocolInfo get_protocol_info () {
+    public ProtocolInfo get_protocol_info
+                            (HashTable<string, string> ? replacements = null) {
         var new_pi = new ProtocolInfo ();
 
         new_pi.protocol = this.protocol;
         new_pi.network = this.network;
-        new_pi.mime_type = this.mime_type;
+        if (replacements == null) {
+            new_pi.mime_type = this.mime_type;
+        } else {
+            new_pi.mime_type = MediaObject.apply_replacements (replacements,
+                                                               this.mime_type);
+        }
         new_pi.dlna_profile = this.dlna_profile;
         new_pi.dlna_conversion = this.dlna_conversion;
         new_pi.dlna_operation = this.dlna_operation;
@@ -289,8 +291,8 @@ public class Rygel.MediaResource : GLib.Object {
     }
 
     public string to_string () {
-        var strbuf = new StringBuilder ();
-        strbuf.append (name).append_unichar ('(');
+        var strbuf = new StringBuilder (name);
+        strbuf.append_unichar ('(');
         if (this.size >= 0) {
             strbuf.append ("size ").append (this.size.to_string ())
                   .append_unichar (',');
@@ -343,12 +345,60 @@ public class Rygel.MediaResource : GLib.Object {
         strbuf.append ("dlna_profile ")
               .append (this.dlna_profile == null ? "null" : this.dlna_profile)
               .append_unichar (',');
-        strbuf.append_printf ("dlna_flags %.8X,", this.dlna_flags);
+        strbuf.append_printf ("dlna_flags %.8X [", this.dlna_flags);
+        if (is_dlna_protocol_flag_set (DLNAFlags.SENDER_PACED)) {
+            strbuf.append ("sp-flag ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.TIME_BASED_SEEK)) {
+            strbuf.append ("lop-time ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.BYTE_BASED_SEEK)) {
+            strbuf.append ("lop-byte ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.S0_INCREASE)) {
+            strbuf.append ("s0-increase ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.SN_INCREASE)) {
+            strbuf.append ("sn-increase ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.STREAMING_TRANSFER_MODE)) {
+            strbuf.append ("streaming ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.INTERACTIVE_TRANSFER_MODE)) {
+            strbuf.append ("interactive ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.BACKGROUND_TRANSFER_MODE)) {
+            strbuf.append ("background ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.CONNECTION_STALL)) {
+            strbuf.append ("stall ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.DLNA_V15)) {
+            strbuf.append ("v1.5 ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.LINK_PROTECTED_CONTENT)) {
+            strbuf.append ("link-protected ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.CLEARTEXT_BYTESEEK_FULL)) {
+            strbuf.append ("cleartext-full ");
+        }
+        if (is_dlna_protocol_flag_set (DLNAFlags.LOP_CLEARTEXT_BYTESEEK)) {
+            strbuf.append ("cleartext-lop ");
+        }
+        strbuf.overwrite (strbuf.len-1,"],"); // Replace space
+
         if (this.dlna_conversion != DLNAConversion.NONE) {
             strbuf.append_printf ("dlna_conversion %1d,", this.dlna_conversion);
         }
         if (this.dlna_operation != DLNAOperation.NONE) {
-            strbuf.append_printf ("dlna_operation %.2X,", this.dlna_operation);
+            strbuf.append_printf ("dlna_operation %.2X [", this.dlna_operation);
+            if (is_dlna_operation_mode_set (DLNAOperation.RANGE)) {
+                strbuf.append ("byte-seek ");
+            }
+            if (is_dlna_operation_mode_set (DLNAOperation.TIMESEEK)) {
+                strbuf.append ("time-seek ");
+            }
+            strbuf.overwrite (strbuf.len-1,"],"); // Replace space
         }
         if (this.play_speeds != null) {
             strbuf.append ("play_speeds [");


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