[rygel] server,media-engines: Refactor seek handling



commit 65bab73cd2cad36156e4b9f609031e47b5c76c44
Author: Jens Georg <mail jensge org>
Date:   Thu Feb 12 22:32:34 2015 +0100

    server,media-engines: Refactor seek handling
    
    Code based on Cablelabs's CVP-2 implementation.

 src/librygel-server/filelist.am                    |    8 +-
 src/librygel-server/rygel-data-sink.vala           |   12 +-
 src/librygel-server/rygel-data-source.vala         |   31 ++-
 .../rygel-http-byte-seek-request.vala              |  148 +++++++++++++
 .../rygel-http-byte-seek-response.vala             |   77 +++++++
 src/librygel-server/rygel-http-byte-seek.vala      |   98 ---------
 src/librygel-server/rygel-http-get-handler.vala    |   29 +++-
 src/librygel-server/rygel-http-get.vala            |  121 +++++++----
 .../rygel-http-identity-handler.vala               |   87 --------
 src/librygel-server/rygel-http-request.vala        |   10 +-
 .../rygel-http-resource-handler.vala               |   14 ++
 .../rygel-http-response-element.vala               |   44 ++++
 src/librygel-server/rygel-http-response.vala       |   12 +-
 src/librygel-server/rygel-http-seek.vala           |   93 ++-------
 .../rygel-http-subtitle-handler.vala               |    4 +
 .../rygel-http-thumbnail-handler.vala              |    4 +
 .../rygel-http-time-seek-request.vala              |  211 ++++++++++++++++++
 .../rygel-http-time-seek-response.vala             |  224 ++++++++++++++++++++
 src/librygel-server/rygel-http-time-seek.vala      |  179 ----------------
 src/librygel-server/rygel-media-container.vala     |   12 +-
 .../gstreamer/rygel-gst-data-source.vala           |   76 +++++--
 .../gstreamer/rygel-gst-media-engine.vala          |    4 +-
 src/media-engines/gstreamer/rygel-gst-sink.vala    |    8 +-
 .../simple/rygel-simple-data-source.vala           |   38 +++--
 24 files changed, 1000 insertions(+), 544 deletions(-)
---
diff --git a/src/librygel-server/filelist.am b/src/librygel-server/filelist.am
index 9d85d41..ef4d309 100644
--- a/src/librygel-server/filelist.am
+++ b/src/librygel-server/filelist.am
@@ -37,19 +37,21 @@ LIBRYGEL_SERVER_NONVAPI_SOURCE_FILES = \
        rygel-content-directory.vala \
        rygel-dbus-thumbnailer.vala \
        rygel-engine-loader.vala \
+       rygel-http-byte-seek-request.vala \
+       rygel-http-byte-seek-response.vala \
        rygel-free-desktop-interfaces.vala \
-       rygel-http-byte-seek.vala \
        rygel-http-get-handler.vala \
        rygel-http-get.vala \
        rygel-http-thumbnail-handler.vala \
        rygel-http-subtitle-handler.vala \
-       rygel-http-identity-handler.vala \
        rygel-http-item-uri.vala \
        rygel-http-post.vala \
        rygel-http-request.vala \
        rygel-http-response.vala \
+       rygel-http-response-element.vala \
        rygel-http-server.vala \
-       rygel-http-time-seek.vala \
+       rygel-http-time-seek-request.vala \
+       rygel-http-time-seek-response.vala \
        rygel-http-resource-handler.vala \
        rygel-import-resource.vala \
        rygel-object-creator.vala \
diff --git a/src/librygel-server/rygel-data-sink.vala b/src/librygel-server/rygel-data-sink.vala
index 9ff7d51..eb206ed 100644
--- a/src/librygel-server/rygel-data-sink.vala
+++ b/src/librygel-server/rygel-data-sink.vala
@@ -1,7 +1,9 @@
 /*
  * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
  *
  * Author: Jens Georg <jensg openismus com>
+ *         Craig Pratt <craig ecaspia com>
  *
  * This file is part of Rygel.
  *
@@ -40,7 +42,7 @@ internal class Rygel.DataSink : Object {
     public DataSink (DataSource source,
                      Server     server,
                      Message    message,
-                     HTTPSeek?  offsets) {
+                     HTTPSeekRequest?  offsets) {
         this.source = source;
         this.server = server;
         this.message = message;
@@ -49,10 +51,12 @@ internal class Rygel.DataSink : Object {
         this.bytes_sent = 0;
         this.max_bytes = int64.MAX;
         if (offsets != null &&
-            offsets is HTTPByteSeek) {
-            this.max_bytes = offsets.length;
+            offsets is HTTPByteSeekRequest &&
+            ((offsets as HTTPByteSeekRequest).range_length != HTTPSeekRequest.UNSPECIFIED)) {
+            this.max_bytes = (offsets as HTTPByteSeekRequest).range_length;
         }
-
+        debug ("Setting max_bytes to %s", (this.max_bytes == int64.MAX)
+                                          ? "MAX" : this.max_bytes.to_string());
         this.source.data_available.connect (this.on_data_available);
         this.message.wrote_chunk.connect (this.on_wrote_chunk);
     }
diff --git a/src/librygel-server/rygel-data-source.vala b/src/librygel-server/rygel-data-source.vala
index 437f550..d663ff8 100644
--- a/src/librygel-server/rygel-data-source.vala
+++ b/src/librygel-server/rygel-data-source.vala
@@ -1,7 +1,9 @@
 /*
  * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
  *
  * Author: Jens Georg <jensg openismus com>
+ *         Craig Pratt <craig ecaspia com>
  *
  * This file is part of Rygel.
  *
@@ -22,7 +24,7 @@
 
 public errordomain Rygel.DataSourceError {
     GENERAL,
-    SEEK_FAILED
+    SEEK_FAILED,
 }
 
 /**
@@ -36,7 +38,8 @@ public errordomain Rygel.DataSourceError {
  * to Rygel which adds them to the response it sends to the original HTTP
  * request received from the client.
  *
- * The data source is responsible for providing the streamable byte-stream
+ * The data source is responsible for providing response header information
+ * describing the content being produced and a streamable byte-stream
  * via its data_available signal. End-of-stream is signalled by the 
  * done signal, while errors are signalled by the error signal.
  *
@@ -58,14 +61,28 @@ public errordomain Rygel.DataSourceError {
  */
 public interface Rygel.DataSource : GLib.Object {
     /**
+     * Preroll the data with the given seek
+     *
+     * @param seek    optional seek/range specifier
+    *
+     * @return List of HTTPResponseElements appropriate for the content request and
+     *         optional seek (e.g. Content-Range, TimeSeekRange.dlna.org,
+     *         etc) or null/empty list if none are appropriate. Note: the list will
+     *         be processed in-order by the caller.
+     *
+     * @throws Error if anything goes wrong while prerolling the stream.
+     *         Throws DataSourceError.SEEK_FAILED if a seek method is not supported or the
+     *         range is not fulfillable.
+     */
+    public abstract Gee.List<HTTPResponseElement> ? preroll (HTTPSeekRequest? seek)
+       throws Error;
+
+    /**
      * Start producing the data.
      *
-     * @param offsets optional limits of the stream for partial streaming
-     * @throws Error if anything goes wrong while starting the stream. Throws
-     * DataSourceError.SEEK_FAILED if a seek method is not supported or the
-     * range is not fulfillable.
+     * @throws Error if anything goes wrong while starting the stream.
      */
-    public abstract void start (HTTPSeek? offsets) throws Error;
+    public abstract void start () throws Error;
 
     /**
      * Temporarily stop data generation.
diff --git a/src/librygel-server/rygel-http-byte-seek-request.vala 
b/src/librygel-server/rygel-http-byte-seek-request.vala
new file mode 100644
index 0000000..38969f9
--- /dev/null
+++ b/src/librygel-server/rygel-http-byte-seek-request.vala
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2009 Nokia Corporation.
+ * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
+ *
+ * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
+ *                               <zeeshan ali nokia com>
+ *         Jens Georg <jensg openismus com>
+ *         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.
+ *
+ * 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;
+
+public class Rygel.HTTPByteSeekRequest : Rygel.HTTPSeekRequest {
+    /**
+     * The start of the range in bytes
+     */
+    public int64 start_byte { get; set; }
+
+    /**
+     * The end of the range in bytes (inclusive)
+     */
+    public int64 end_byte { get; set; }
+
+    /**
+     * The length of the range in bytes
+     */
+    public int64 range_length { get; private set; }
+
+    /**
+     * The length of the resource in bytes
+     */
+    public int64 total_size { get; set; }
+
+
+    public HTTPByteSeekRequest (HTTPGet request) throws HTTPSeekRequestError,
+                                                 HTTPRequestError {
+        base ();
+        unowned string range = request.msg.request_headers.get_one ("Range");
+        if (range == null) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ("Range header not present");
+        }
+
+        int64 start_byte, end_byte, total_size;
+
+        // The size (entity body size) may not be known up-front (especially for live sources)
+        total_size = request.handler.get_resource_size ();
+        if (total_size < 0) {
+            total_size = UNSPECIFIED;
+        }
+
+        // Note: DLNA restricts the syntax on the Range header (see DLNA 7.5.4.3.2.22.3)
+        //       And we need to retain the concept of an "open range" ("bytes=DIGITS-")
+        //       since the interpretation/legality varies based on the context
+        //       (e.g. DLNA 7.5.4.3.2.19.2, 7.5.4.3.2.20.1, 7.5.4.3.2.20.3)
+        if (!range.has_prefix ("bytes=")) {
+            throw new HTTPSeekRequestError.INVALID_RANGE
+                          ("Invalid Range value (missing 'bytes=' field): '%s'", range);
+        }
+
+        var parsed_range = range.substring (6);
+        if (!parsed_range.contains ("-")) {
+            throw new HTTPSeekRequestError.INVALID_RANGE
+                          ("Invalid Range request with no '-': '%s'", range);
+        }
+
+        var range_tokens = parsed_range.split ("-", 2);
+
+        if (!int64.try_parse (strip_leading_zeros(range_tokens[0]), out start_byte)) {
+            throw new HTTPSeekRequestError.INVALID_RANGE
+                          ("Invalid Range start value: '%s'", range);
+        }
+
+        if ((total_size != UNSPECIFIED) && (start_byte >= total_size)) {
+            throw new HTTPSeekRequestError.OUT_OF_RANGE
+                          ("Range start value %lld is larger than content size %lld: '%s'",
+                          start_byte, total_size, range);
+        }
+
+        if (range_tokens[1] == null || (range_tokens[1].length == 0)) {
+            end_byte = UNSPECIFIED;
+            range_length = UNSPECIFIED;
+        } else {
+            if (!int64.try_parse (strip_leading_zeros(range_tokens[1]), out end_byte)) {
+                throw new HTTPSeekRequestError.INVALID_RANGE
+                              ("Invalid Range end value: '%s'", range);
+            }
+            if (end_byte < start_byte) {
+                throw new HTTPSeekRequestError.INVALID_RANGE
+                              ("Range end value %lld is smaller than range start value %lld: '%s'",
+                              end_byte, start_byte, range);
+            }
+            if ((total_size != UNSPECIFIED) && (end_byte >= total_size)) {
+                end_byte = total_size - 1;
+            }
+            range_length = end_byte - start_byte + 1; // range is inclusive
+        }
+        this.start_byte = start_byte;
+        this.end_byte = end_byte;
+        this.total_size = total_size;
+    }
+
+    public static bool supported (HTTPGet request) {
+        bool force_seek = false;
+
+        try {
+            var hack = ClientHacks.create (request.msg);
+            force_seek = hack.force_seek ();
+        } catch (Error error) { }
+
+        return force_seek || request.handler.supports_byte_seek ();
+    }
+
+    public static bool requested (HTTPGet request) {
+        return (request.msg.request_headers.get_one ("Range") != null);
+    }
+
+    // Leading "0"s cause try_parse() to assume the value is octal (see Vala bug 656691)
+    //  So we strip them off before passing to int64.try_parse()
+    private static string strip_leading_zeros (string number_string) {
+        int i=0;
+        while ((number_string[i] == '0') && (i < number_string.length)) {
+            i++;
+        }
+        if (i == 0) {
+            return number_string;
+        } else {
+            return number_string[i:number_string.length];
+        }
+    }
+
+}
diff --git a/src/librygel-server/rygel-http-byte-seek-response.vala 
b/src/librygel-server/rygel-http-byte-seek-response.vala
new file mode 100644
index 0000000..459b2d9
--- /dev/null
+++ b/src/librygel-server/rygel-http-byte-seek-response.vala
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2009 Nokia Corporation.
+ * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
+ *
+ * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
+ *                               <zeeshan ali nokia com>
+ *         Jens Georg <jensg openismus com>
+ *         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.
+ *
+ * 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;
+
+public class Rygel.HTTPByteSeekResponse : Rygel.HTTPResponseElement {
+    /**
+     * The start of the range in bytes
+     */
+    public int64 start_byte { get; set; }
+
+    /**
+     * The end of the range in bytes (inclusive)
+     */
+    public int64 end_byte { get; set; }
+
+    /**
+     * The length of the range in bytes
+     */
+    public int64 range_length { get; private set; }
+
+    /**
+     * The length of the resource in bytes
+     */
+    public int64 total_size { get; set; }
+
+    public HTTPByteSeekResponse (int64 start_byte, int64 end_byte, int64 total_size) {
+        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;
+    }
+
+    public HTTPByteSeekResponse.from_request (HTTPByteSeekRequest request) {
+        this.start_byte = request.start_byte;
+        this.end_byte = request.end_byte;
+        this.range_length = request.range_length;
+        this.total_size = request.total_size;
+    }
+
+    public override void add_response_headers (Rygel.HTTPRequest request) {
+        // Content-Range: bytes START_BYTE-END_BYTE/TOTAL_LENGTH (or "*")
+        request.msg.response_headers.set_content_range ( this.start_byte, this.end_byte,
+                                                         this.total_size );
+        request.msg.response_headers.append ("Accept-Ranges", "bytes");
+        request.msg.response_headers.set_content_length (range_length);
+    }
+
+    public override string to_string () {
+        return ("HTTPByteSeekResponse(bytes=%lld-%lld/%lld (%lld bytes))"
+                .printf (this.start_byte, this.end_byte, this.total_size, this.total_size));
+    }
+}
diff --git a/src/librygel-server/rygel-http-get-handler.vala b/src/librygel-server/rygel-http-get-handler.vala
index 37e1aa0..20dee38 100644
--- a/src/librygel-server/rygel-http-get-handler.vala
+++ b/src/librygel-server/rygel-http-get-handler.vala
@@ -1,9 +1,11 @@
 /*
  * Copyright (C) 2008-2010 Nokia Corporation.
  * Copyright (C) 2010 Andreas Henriksson <andreas fatal se>
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
  *
  * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
  *                               <zeeshan ali nokia com>
+ *         Craig Pratt <craig ecaspia com>
  *
  * This file is part of Rygel.
  *
@@ -27,7 +29,7 @@ using GUPnP;
 /**
  * HTTP GET request handler interface.
  */
-internal abstract class Rygel.HTTPGetHandler: GLib.Object {
+public abstract class Rygel.HTTPGetHandler: GLib.Object {
     protected const string TRANSFER_MODE_HEADER = "transferMode.dlna.org";
 
     protected const string TRANSFER_MODE_STREAMING = "Streaming";
@@ -36,7 +38,6 @@ internal abstract class Rygel.HTTPGetHandler: GLib.Object {
 
     public Cancellable cancellable { get; set; }
 
-    // Add response headers.
     /**
      * Invokes the handler to add response headers to/for the given HTTP request
      */
@@ -78,7 +79,29 @@ internal abstract class Rygel.HTTPGetHandler: GLib.Object {
      */
     public abstract int64 get_resource_size ();
 
-    // Create an HTTPResponse object that will render the body.
+    /**
+     * Returns the resource duration (in microseconds) or -1 if not known.
+     */
+    public virtual int64 get_resource_duration () {
+        return -1;
+    }
+
+    /**
+     * Returns true if the handler supports full random-access byte seek.
+     */
+    public virtual bool supports_byte_seek () {
+        return false;
+    }
+
+    /**
+     * Returns true if the handler supports full random-access time seek.
+     */
+    public virtual bool supports_time_seek () {
+        return false;
+    }
+    /**
+     * Create an HTTPResponse object that will render the body.
+     */
     public abstract HTTPResponse render_body (HTTPGet request)
                                               throws HTTPRequestError;
 
diff --git a/src/librygel-server/rygel-http-get.vala b/src/librygel-server/rygel-http-get.vala
index dbde59c..79adfbb 100644
--- a/src/librygel-server/rygel-http-get.vala
+++ b/src/librygel-server/rygel-http-get.vala
@@ -31,12 +31,12 @@
 /**
  * Responsible for handling HTTP GET & HEAD client requests.
  */
-internal class Rygel.HTTPGet : HTTPRequest {
+public class Rygel.HTTPGet : HTTPRequest {
     private const string TRANSFER_MODE_HEADER = "transferMode.dlna.org";
 
+    public HTTPSeekRequest seek;
     public Thumbnail thumbnail;
     public Subtitle subtitle;
-    public HTTPSeek seek;
 
     private int thumbnail_index;
     private int subtitle_index;
@@ -82,10 +82,6 @@ internal class Rygel.HTTPGet : HTTPRequest {
                                                     this.cancellable);
         }
 
-        if (this.handler == null) {
-            this.handler = new HTTPIdentityHandler (this.cancellable);
-        }
-
         { // Check the transfer mode
             var transfer_mode = this.msg.request_headers.get_one (TRANSFER_MODE_HEADER);
 
@@ -157,39 +153,67 @@ internal class Rygel.HTTPGet : HTTPRequest {
     }
 
     private async void handle_item_request () throws Error {
-        var need_time_seek = HTTPTimeSeek.needed (this);
-        var requested_time_seek = HTTPTimeSeek.requested (this);
-        var need_byte_seek = HTTPByteSeek.needed (this);
-        var requested_byte_seek = HTTPByteSeek.requested (this);
-
-        if ((requested_time_seek && !need_time_seek) ||
-            (requested_byte_seek && !need_byte_seek)) {
-            throw new HTTPRequestError.UNACCEPTABLE ("Invalid seek request");
+        var supports_time_seek = HTTPTimeSeekRequest.supported (this);
+        var requested_time_seek = HTTPTimeSeekRequest.requested (this);
+        var supports_byte_seek = HTTPByteSeekRequest.supported (this);
+        var requested_byte_seek = HTTPByteSeekRequest.requested (this);
+
+        if (requested_byte_seek) {
+            if (!supports_byte_seek) {
+                throw new HTTPRequestError.UNACCEPTABLE ( "Byte seek not supported for "
+                                                          + this.uri.to_string () );
+            }
+        } else if (requested_time_seek) {
+            if (!supports_time_seek) {
+                throw new HTTPRequestError.UNACCEPTABLE ( "Time seek not supported for "
+                                                          + this.uri.to_string () );
+            }
         }
 
         try {
-            if (need_time_seek && requested_time_seek) {
-                this.seek = new HTTPTimeSeek (this);
-            } else if (need_byte_seek && requested_byte_seek) {
-                this.seek = new HTTPByteSeek (this);
-            }
-        } catch (HTTPSeekError error) {
-            this.server.unpause_message (this.msg);
-
-            if (error is HTTPSeekError.INVALID_RANGE) {
-                this.end (Soup.Status.BAD_REQUEST);
-            } else if (error is HTTPSeekError.OUT_OF_RANGE) {
-                this.end (Soup.Status.REQUESTED_RANGE_NOT_SATISFIABLE);
+            // Order is intentional here
+            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);
+                this.seek = byte_seek;
+            } else if (supports_time_seek && requested_time_seek) {
+                // Assert: speed_request has been checked/processed
+                var time_seek = new HTTPTimeSeekRequest (this);
+                debug ("Processing " + time_seek.to_string ());
+                this.seek = time_seek;
             } else {
-                throw error;
+                this.seek = null;
             }
-
+        } catch (HTTPSeekRequestError error) {
+            warning ("Caught HTTPSeekRequestError: " + error.message);
+            this.server.unpause_message (this.msg);
+            this.end (error.code, error.message); // All seek error codes are Soup.Status codes
             return;
-        }
+         }
 
         // Add headers
         this.handler.add_response_headers (this);
 
+        var response = this.handler.render_body (this);
+
+        // Have the response process the seek/speed request
+        try {
+            var responses = response.preroll ();
+
+            // Incorporate the prerolled responses
+            if (responses != null) {
+                foreach (var response_elem in responses) {
+                    response_elem.add_response_headers (this);
+                }
+            }
+        } catch (HTTPSeekRequestError error) {
+            warning ("Caught HTTPSeekRequestError on preroll: " + error.message);
+            this.server.unpause_message (this.msg);
+            this.end (error.code, error.message); // All seek error codes are Soup.Status codes
+            return;
+        }
+
         // Determine the size value
         int64 response_size;
         {
@@ -213,13 +237,6 @@ internal class Rygel.HTTPGet : HTTPRequest {
             // size will factor into other logic below...
         }
 
-        // Add general headers
-        if (this.msg.request_headers.get_one ("Range") != null) {
-            this.msg.set_status (Soup.Status.PARTIAL_CONTENT);
-        } else {
-            this.msg.set_status (Soup.Status.OK);
-        }
-
         // Determine the transfer mode encoding
         {
             Soup.Encoding response_body_encoding;
@@ -241,6 +258,36 @@ internal class Rygel.HTTPGet : HTTPRequest {
             this.msg.response_headers.set_encoding (response_body_encoding);
         }
 
+        // Determine the Vary header (if not HTTP 1.0)
+        {
+            // Per DLNA 7.5.4.3.2.35.4, the Vary header needs to include the timeseek
+            // header if it is supported for the resource/uri
+            if (supports_time_seek) {
+                if (this.msg.get_http_version () != Soup HTTPVersion  1_0) {
+                    var vary_header = new StringBuilder
+                                             (this.msg.response_headers.get_list ("Vary"));
+                    if (supports_time_seek) {
+                        if (vary_header.len > 0) {
+                            vary_header.append (",");
+                        }
+                        vary_header.append (HTTPTimeSeekRequest.TIMESEEKRANGE_HEADER);
+                    }
+                    this.msg.response_headers.replace ("Vary", vary_header.str);
+                }
+            }
+        }
+
+        // Determine the status code
+        {
+            int response_code;
+            if (this.msg.response_headers.get_one ("Content-Range") != null) {
+                response_code = Soup.Status.PARTIAL_CONTENT;
+            } else {
+                response_code = Soup.Status.OK;
+            }
+            this.msg.set_status (response_code);
+        }
+
         if (msg.get_http_version () == Soup HTTPVersion  1_0) {
             // Set the response version to HTTP 1.1 (see DLNA 7.5.4.3.2.7.2)
             msg.set_http_version (Soup HTTPVersion  1_1);
@@ -259,8 +306,6 @@ internal class Rygel.HTTPGet : HTTPRequest {
             return;
         }
 
-        var response = this.handler.render_body (this);
-
         yield response.run ();
 
         this.end (Soup.Status.NONE);
diff --git a/src/librygel-server/rygel-http-request.vala b/src/librygel-server/rygel-http-request.vala
index 389d3c0..243705c 100644
--- a/src/librygel-server/rygel-http-request.vala
+++ b/src/librygel-server/rygel-http-request.vala
@@ -110,12 +110,16 @@ public abstract class Rygel.HTTPRequest : GLib.Object, Rygel.StateMachine {
             status = Soup.Status.NOT_FOUND;
         }
 
-        this.end (status);
+        this.end (status, error.message);
     }
 
-    protected void end (uint status) {
+    protected void end (uint status, string ? reason = null) {
         if (status != Soup.Status.NONE) {
-            this.msg.set_status (status);
+            if (reason == null) {
+                this.msg.set_status (status);
+            } else {
+                this.msg.set_status_full (status, reason);
+            }
         }
 
         this.completed ();
diff --git a/src/librygel-server/rygel-http-resource-handler.vala 
b/src/librygel-server/rygel-http-resource-handler.vala
index 965ab64..51a49a4 100644
--- a/src/librygel-server/rygel-http-resource-handler.vala
+++ b/src/librygel-server/rygel-http-resource-handler.vala
@@ -93,4 +93,18 @@ internal class Rygel.HTTPMediaResourceHandler : HTTPGetHandler {
     public override int64 get_resource_size () {
         return media_resource.size;
     }
+
+    public override int64 get_resource_duration () {
+        return media_resource.duration * TimeSpan.SECOND;
+    }
+
+    public override bool supports_byte_seek () {
+        return media_resource.supports_arbitrary_byte_seek ()
+               || media_resource.supports_limited_byte_seek ();
+    }
+
+    public override bool supports_time_seek () {
+        return media_resource.supports_arbitrary_time_seek ()
+               || media_resource.supports_limited_time_seek ();
+    }
 }
diff --git a/src/librygel-server/rygel-http-response-element.vala 
b/src/librygel-server/rygel-http-response-element.vala
new file mode 100644
index 0000000..0ac57cf
--- /dev/null
+++ b/src/librygel-server/rygel-http-response-element.vala
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+/**
+ * This abstract class represents an entity that can contribute response headers to a
+ * HTTP request.
+ */
+public abstract class Rygel.HTTPResponseElement : GLib.Object {
+    // For designating fields that are unset
+    public static const int64 UNSPECIFIED = -1;
+
+    /**
+     * Set the type-appropriate headers on the associated HTTP Message
+     */
+    public abstract void add_response_headers (Rygel.HTTPRequest request);
+
+    public abstract string to_string ();
+}
diff --git a/src/librygel-server/rygel-http-response.vala b/src/librygel-server/rygel-http-response.vala
index 7cfa7bf..3845689 100644
--- a/src/librygel-server/rygel-http-response.vala
+++ b/src/librygel-server/rygel-http-response.vala
@@ -1,10 +1,12 @@
 /*
  * Copyright (C) 2008-2012 Nokia Corporation.
  * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
  *
  * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
  *                               <zeeshan ali nokia com>
  *         Jens Georg <jensg openismus com>
+ *         Craig Pratt <craig ecaspia com>
  *
  * This file is part of Rygel.
  *
@@ -25,13 +27,13 @@
 
 using Soup;
 
-internal class Rygel.HTTPResponse : GLib.Object, Rygel.StateMachine {
+public class Rygel.HTTPResponse : GLib.Object, Rygel.StateMachine {
     public unowned Soup.Server server { get; private set; }
     public Soup.Message msg;
 
     public Cancellable cancellable { get; set; }
 
-    public HTTPSeek seek;
+    public HTTPSeekRequest seek;
 
     private SourceFunc run_continue;
     private int _priority = -1;
@@ -99,10 +101,14 @@ internal class Rygel.HTTPResponse : GLib.Object, Rygel.StateMachine {
         }
     }
 
+    public Gee.List<HTTPResponseElement> ? preroll () throws Error {
+        return this.src.preroll (this.seek);
+    }
+
     public async void run () {
         this.run_continue = run.callback;
         try {
-            this.src.start (this.seek);
+            this.src.start ();
         } catch (Error error) {
             Idle.add (() => {
                 this.end (false, Status.NONE);
diff --git a/src/librygel-server/rygel-http-seek.vala b/src/librygel-server/rygel-http-seek.vala
index a7c66ea..95860f3 100644
--- a/src/librygel-server/rygel-http-seek.vala
+++ b/src/librygel-server/rygel-http-seek.vala
@@ -1,10 +1,12 @@
 /*
  * Copyright (C) 2008-2009 Nokia Corporation.
  * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
  *
  * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
  *                               <zeeshan ali nokia com>
  *         Jens Georg <jensg openismus com>
+ *         Craig Pratt <craig ecaspia com>
  *
  * This file is part of Rygel.
  *
@@ -23,89 +25,22 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
 
-public errordomain Rygel.HTTPSeekError {
+/**
+ * Various errors that can be thrown when attempting to seek into a stream.
+ *
+ * Note: All codes must be set to Soup.Status codes
+ */
+public errordomain Rygel.HTTPSeekRequestError {
     INVALID_RANGE = Soup.Status.BAD_REQUEST,
+    BAD_REQUEST = Soup.Status.BAD_REQUEST,
     OUT_OF_RANGE = Soup.Status.REQUESTED_RANGE_NOT_SATISFIABLE,
 }
 
-public enum Rygel.HTTPSeekType {
-    BYTE,
-    TIME
-}
-
 /**
- * HTTPSeek is an abstract representation of a ranged HTTP request.
- *
- * It can be one of:
- *
- *  - The classic Range request (seek_type == HTTPSeekType.BYTE), with start and stop in bytes.
- *  - The DLNA-Specific "TimeSeekRange.dlna.org" request (seek_type == HTTPSeekType.TIME) with start and 
stop in microseconds.
+ * HTTPSeekRequest is an abstract base for a variety of seek request types.
  */
-public abstract class Rygel.HTTPSeek : GLib.Object {
-
-    /**
-     * Identifies whether this is a class Range request or a DLNA-specific
-     * "TimeSeekRange.dlna.org" request.
-     */
-    public HTTPSeekType seek_type { get; protected set; }
-    public Soup.Message msg { get; private set; }
-
-    /**
-     * The start of the range as a number of bytes (classic) or as microseconds 
-     * (DLNA-specific). See seek_type.
-     */
-    public int64 start { get; private set; }
-
-    /**
-     * The end of the range as a number of bytes (classic) or as microseconds 
-     * (DLNA-specific). See seek_type.
-     */
-    public int64 stop { get; private set; }
-
-    /**
-     * Either 1 byte (classic) or as 1000 G_TIME_SPAN_MILLISECOND microseconds 
-     * (DLNA-specific). See seek_type.
-     */
-    public int64 step { get; private set; }
-
-    /**
-     * The length of the range as a number of bytes (classic) or as microseconds 
-     * (DLNA-specific). See seek_type.
-     */
-    public int64 length { get; private set; }
-
-    /**
-     * The length of the media file as a number of bytes (classic) or as microseconds 
-     * (DLNA-specific). See seek_type.
-     */
-    public int64 total_length { get; private set; }
-
-    public HTTPSeek (Soup.Message msg,
-                     int64        start,
-                     int64        stop,
-                     int64        step,
-                     int64        total_length) throws HTTPSeekError {
-        this.msg = msg;
-        this.start = start;
-        this.stop = stop;
-        this.length = length;
-        this.total_length = total_length;
-
-        if (start < 0 || start >= total_length) {
-            throw new HTTPSeekError.OUT_OF_RANGE (_("Out Of Range Start '%ld'"),
-                                                  start);
-        }
-        if (stop < 0 || stop >= total_length) {
-            throw new HTTPSeekError.OUT_OF_RANGE (_("Out Of Range Stop '%ld'"),
-                                                  stop);
-        }
-
-        if (length > 0) {
-            this.stop = stop.clamp (start + 1, length - 1);
-        }
-
-        this.length = stop + step - start;
-    }
-
-    public abstract void add_response_headers ();
+public abstract class Rygel.HTTPSeekRequest : GLib.Object {
+    // For designating fields that are unset
+    public static const int64 UNSPECIFIED = -1;
+    // Note: -1 is significant in that libsoup also uses it to designate an "unknown" value
 }
diff --git a/src/librygel-server/rygel-http-subtitle-handler.vala 
b/src/librygel-server/rygel-http-subtitle-handler.vala
index d011587..cb27d73 100644
--- a/src/librygel-server/rygel-http-subtitle-handler.vala
+++ b/src/librygel-server/rygel-http-subtitle-handler.vala
@@ -92,4 +92,8 @@ internal class Rygel.HTTPSubtitleHandler : Rygel.HTTPGetHandler {
     public override int64 get_resource_size () {
         return subtitle.size;
     }
+
+    public override bool supports_byte_seek () {
+        return true;
+    }
 }
diff --git a/src/librygel-server/rygel-http-thumbnail-handler.vala 
b/src/librygel-server/rygel-http-thumbnail-handler.vala
index 9a4975f..cc23e71 100644
--- a/src/librygel-server/rygel-http-thumbnail-handler.vala
+++ b/src/librygel-server/rygel-http-thumbnail-handler.vala
@@ -93,4 +93,8 @@ internal class Rygel.HTTPThumbnailHandler : Rygel.HTTPGetHandler {
     public override int64 get_resource_size () {
         return thumbnail.size;
     }
+
+    public override bool supports_byte_seek () {
+        return true;
+    }
 }
diff --git a/src/librygel-server/rygel-http-time-seek-request.vala 
b/src/librygel-server/rygel-http-time-seek-request.vala
new file mode 100644
index 0000000..70fceb0
--- /dev/null
+++ b/src/librygel-server/rygel-http-time-seek-request.vala
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2009 Nokia Corporation.
+ * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
+ *
+ * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
+ *                               <zeeshan ali nokia com>
+ *         Jens Georg <jensg openismus com>
+ *         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.
+ *
+ * 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.
+ */
+
+/**
+ * This class represents a DLNA TimeSeekRange request.
+ *
+ * A TimeSeekRange request can only have a time range ("npt=start-end").
+ */
+public class Rygel.HTTPTimeSeekRequest : Rygel.HTTPSeekRequest {
+    public static const string TIMESEEKRANGE_HEADER = "TimeSeekRange.dlna.org";
+    /**
+     * Requested range start time, in microseconds
+     */
+    public int64 start_time;
+
+    /**
+     * Requested range end time, in microseconds
+     */
+    public int64 end_time;
+
+    /**
+     * Requested range duration, in microseconds
+     */
+    public int64 range_duration;
+
+    /**
+     * The total duration of the resource, in microseconds
+     */
+    public int64 total_duration;
+
+    /**
+     * Create a HTTPTimeSeekRequest corresponding with a HTTPGet that contains a
+     * TimeSeekRange.dlna.org header value.
+     *
+     * Note: This constructor will check the syntax of the request (per DLNA 7.5.4.3.2.24.3)
+     *       as well as perform some range validation. If the provided request is associated
+     *       with a handler that can provide content duration, the start and end time will
+     *       be checked for out-of-bounds conditions.
+     * @param request The HTTP GET/HEAD request
+     */
+    internal HTTPTimeSeekRequest (HTTPGet request)
+            throws HTTPSeekRequestError {
+        base ();
+
+        this.total_duration = request.handler.get_resource_duration ();
+        if (this.total_duration <= 0) {
+            this.total_duration = UNSPECIFIED;
+        }
+
+        var range = request.msg.request_headers.get_one (TIMESEEKRANGE_HEADER);
+
+        if (range == null) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ("%s not present",
+                                                          TIMESEEKRANGE_HEADER);
+        }
+
+        if (!range.has_prefix ("npt=")) {
+            throw new HTTPSeekRequestError.INVALID_RANGE
+                          ("Invalid %s value (missing npt field): '%s'",
+                          TIMESEEKRANGE_HEADER, range);
+        }
+
+        var parsed_range = range.substring (4);
+        if (!parsed_range.contains ("-")) {
+            throw new HTTPSeekRequestError.INVALID_RANGE
+                          ("Invalid %s request with no '-': '%s'",
+                          TIMESEEKRANGE_HEADER, range);
+        }
+
+        var range_tokens = parsed_range.split ("-", 2);
+
+        int64 start = UNSPECIFIED;
+        if (!parse_npt_time (range_tokens[0], ref start)) {
+            throw new HTTPSeekRequestError.INVALID_RANGE
+                          ("Invalid %s value (no start): '%s'",
+                          TIMESEEKRANGE_HEADER, range);
+        }
+
+        this.start_time = start;
+
+        // Look for an end time
+        int64 end = UNSPECIFIED;
+        if (parse_npt_time (range_tokens[1], ref end)) {
+            // The end time was specified in the npt ("start-end")
+            // Check for valid range
+            {
+                this.end_time = end;
+
+                this.range_duration =  this.end_time - this.start_time;
+                // At positive rate, start < end
+                if (this.range_duration <= 0) { // See DLNA 7.5.4.3.2.24.12
+                    throw new HTTPSeekRequestError.INVALID_RANGE
+                                  ("Invalid %s value (start time after end time - forward scan): '%s'",
+                                   TIMESEEKRANGE_HEADER, range);
+                }
+            }
+        } else { // End time not specified in the npt field ("start-")
+            // See DLNA 7.5.4.3.2.24.4
+            this.end_time = UNSPECIFIED; // Will indicate "end/beginning of binary"
+            if (this.total_duration == UNSPECIFIED) {
+                this.range_duration = UNSPECIFIED;
+            } else {
+                this.range_duration = this.total_duration - this.start_time;
+            }
+        }
+    }
+
+    public string to_string () {
+        return ("HTTPTimeSeekRequest (npt=%lld-%s)".printf (this.start_time,
+                                                           (this.end_time != UNSPECIFIED
+                                                            ? this.end_time.to_string()
+                                                            : "*") ) );
+    }
+
+    /**
+     * Return true if time-seek is supported.
+     *
+     * This method utilizes elements associated with the request to determine if a
+     * TimeSeekRange request is supported for the given request/resource.
+     */
+    public static bool supported (HTTPGet request) {
+        bool force_seek = false;
+
+        try {
+            var hack = ClientHacks.create (request.msg);
+            force_seek = hack.force_seek ();
+        } catch (Error error) { }
+
+        return force_seek || request.handler.supports_time_seek ();
+    }
+
+    /**
+     * Return true of the HTTPGet contains a TimeSeekRange request.
+     */
+    public static bool requested (HTTPGet request) {
+        return (request.msg.request_headers.get_one (TIMESEEKRANGE_HEADER) != null);
+    }
+
+    // Parses npt times in the format of '417.33' and returns the time in microseconds
+    private static bool parse_npt_seconds (string range_token,
+                                           ref int64 value) {
+        if (range_token[0].isdigit ()) {
+            value = (int64) (double.parse (range_token) * TimeSpan.SECOND);
+        } else {
+            return false;
+        }
+        return true;
+    }
+
+    // Parses npt times in the format of '10:19:25.7' and returns the time in microseconds
+    private static bool parse_npt_time (string? range_token,
+                                        ref int64 value) {
+        if (range_token == null) {
+            return false;
+        }
+
+        if (range_token.index_of (":") == -1) {
+            return parse_npt_seconds (range_token, ref value);
+        }
+        // parse_seconds has a ':' in it...
+        int64 seconds_sum = 0;
+        int time_factor = 0;
+        string[] time_tokens;
+
+        seconds_sum = 0;
+        time_factor = 3600;
+
+        time_tokens = range_token.split (":", 3);
+        if (time_tokens[0] == null ||
+            time_tokens[1] == null ||
+            time_tokens[2] == null) {
+            return false;
+        }
+
+        foreach (string time in time_tokens) {
+            if (time[0].isdigit ()) {
+                seconds_sum += (int64) ((double.parse (time) * TimeSpan.SECOND) * time_factor);
+            } else {
+                return false;
+            }
+            time_factor /= 60;
+        }
+        value = seconds_sum;
+
+        return true;
+    }
+}
diff --git a/src/librygel-server/rygel-http-time-seek-response.vala 
b/src/librygel-server/rygel-http-time-seek-response.vala
new file mode 100644
index 0000000..3e26e67
--- /dev/null
+++ b/src/librygel-server/rygel-http-time-seek-response.vala
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2013  Cable Television Laboratories, Inc.
+ * Contact: http://www.cablelabs.com/
+ *
+ * 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.
+ */
+
+public class Rygel.HTTPTimeSeekResponse : Rygel.HTTPResponseElement {
+    /**
+     * Effective range start time, in microseconds
+     */
+    public int64 start_time { get; private set; }
+
+    /**
+     * Effective range end time, in microseconds
+     */
+    public int64 end_time { get; private set; }
+
+    /**
+     * Effective range duration, in microseconds
+     */
+    public int64 range_duration { get; private set; }
+
+    /**
+     * The total duration of the resource, in microseconds
+     */
+    public int64 total_duration { get; private set; }
+
+    /**
+     * The start of the 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 response length in bytes
+     */
+    public int64 response_length { get; private set; }
+
+    /**
+     * The length of the resource in bytes
+     */
+    public int64 total_size { get; private set; }
+
+    /**
+     * Construct a HTTPTimeSeekResponse with time and byte range
+     *
+     * start_time and start_byte must be specified.
+     *
+     * if total_duration and total_size are UNSPECIFIED, then the content duration/size
+     * will be signaled as unknown ("*")
+     *
+     * if end_time is UNSPECIFIED, then the time range end will be omitted from the
+     * response. If the end_byte is UNSPECIFIED, the entire byte range response will be
+     * omitted. (see DLNA 7.5.4.3.2.24.3)
+     */
+    public HTTPTimeSeekResponse (int64 start_time, int64 end_time, int64 total_duration,
+                                 int64 start_byte, int64 end_byte, int64 total_size) {
+        base ();
+        this.start_time = start_time;
+        this.end_time = end_time;
+        this.total_duration = total_duration;
+
+        this.start_byte = start_byte;
+        this.end_byte = end_byte;
+        this.response_length = (end_byte == UNSPECIFIED) ? UNSPECIFIED
+                                                         : (end_byte - start_byte + 1);
+        this.total_size = total_size;
+    }
+
+    /**
+     * Create a HTTPTimeSeekResponse only containing a time range
+     *
+     * Note: This form is only valid when byte-seek is not supported, according to the
+     * associated resource's ProtocolInfo (see DLNA 7.5.4.3.2.24.5)
+     */
+    public HTTPTimeSeekResponse.time_only (int64 start_time, int64 end_time, int64 total_duration) {
+        base ();
+        this.start_time = start_time;
+        this.end_time = end_time;
+        this.total_duration = total_duration;
+
+        this.start_byte = UNSPECIFIED;
+        this.end_byte = UNSPECIFIED;
+        this.response_length = UNSPECIFIED;
+        this.total_size = UNSPECIFIED;
+    }
+
+    /**
+     * Construct a HTTPTimeSeekResponse with time and byte range and allowing for a
+     * response length override. This is useful when the response body is larger than the
+     * specified byte range from the original content binary.
+     *
+     * start_time and start_byte must be specified.
+     *
+     * If total_duration and total_size are UNSPECIFIED, then the content duration/size
+     * will be signaled as unknown ("*")
+     *
+     * if end_time is UNSPECIFIED, then the time range end will be omitted from the
+     * response. If the end_byte is UNSPECIFIED, the entire byte range response will be
+     * omitted. (see DLNA 7.5.4.3.2.24.3)
+     */
+    public HTTPTimeSeekResponse.with_length (int64 start_time, int64 end_time,
+                                             int64 total_duration,
+                                             int64 start_byte, int64 end_byte,
+                                             int64 total_size,
+                                             int64 response_length) {
+        base ();
+        this.start_time = start_time;
+        this.end_time = end_time;
+        this.total_duration = total_duration;
+
+        this.start_byte = start_byte;
+        this.end_byte = end_byte;
+        this.response_length = response_length;
+        this.total_size = total_size;
+    }
+
+    /**
+     * Create a HTTPTimeSeekResponse from a HTTPTimeSeekRequest
+     *
+     * Note: This form is only valid when byte-seek is not supported, according to the
+     * associated resource's ProtocolInfo (see DLNA 7.5.4.3.2.24.5)
+     */
+    public HTTPTimeSeekResponse.from_request ( HTTPTimeSeekRequest time_seek_request,
+                                              int64 total_duration ) {
+        HTTPTimeSeekResponse.time_only ( time_seek_request.start_time,
+                                         time_seek_request.end_time,
+                                         total_duration );
+    }
+
+    public override void add_response_headers (Rygel.HTTPRequest request) {
+        var response = get_response_string ();
+        if (response != null) {
+            request.msg.response_headers.append (HTTPTimeSeekRequest.TIMESEEKRANGE_HEADER,
+                                                 response);
+            if (this.response_length != UNSPECIFIED) {
+                // Note: Don't use set_content_range () here - we don't want a "Content-range" header
+                request.msg.response_headers.set_content_length (this.response_length);
+            }
+            if (request.msg.get_http_version () == Soup HTTPVersion  1_0) {
+                request.msg.response_headers.replace ("Pragma","no-cache");
+            }
+        }
+    }
+
+    private string? get_response_string () {
+        if (start_time == UNSPECIFIED) {
+            return null;
+        }
+
+        // The response form of TimeSeekRange:
+        //
+        // TimeSeekRange.dlna.org: npt=START_TIME-END_TIME/DURATION bytes=START_BYTE-END_BYTE/LENGTH
+        //
+        // The "bytes=" field can be ommitted in some cases. (e.g. ORG_OP a-val==1, b-val==0)
+        // The DURATION can be "*" in some cases (e.g. for limited-operation mode)
+        // The LENGTH can be "*" in some cases (e.g. for limited-operation mode)
+        // And the entire response header can be ommitted for HEAD requests (see DLNA 7.5.4.3.2.24.2)
+
+        // It's not our job at this level to enforce all the semantics of the TimeSeekRange
+        //  response, as we don't have enough context. Setting up the correct HTTPTimeSeekRequest
+        //  object is the responsibility of the object owner. To form the response, we just
+        //  use what is set.
+
+        var response = new StringBuilder ();
+        response.append ("npt=");
+        response.append_printf ("%.3f-", (double) this.start_time / TimeSpan.SECOND);
+        if (this.end_time != UNSPECIFIED) {
+            response.append_printf ("%.3f", (double) this.end_time / TimeSpan.SECOND);
+        }
+        if (this.total_duration != UNSPECIFIED) {
+            response.append_printf ("/%.3f", (double) this.total_duration / TimeSpan.SECOND);
+        } else {
+            response.append ("/*");
+        }
+
+        if ((this.start_byte != UNSPECIFIED) && (this.end_byte != UNSPECIFIED)) {
+            response.append (" bytes=");
+            response.append (this.start_byte.to_string ());
+            response.append ("-");
+            response.append (this.end_byte.to_string ());
+            response.append ("/");
+            if (this.total_size != UNSPECIFIED) {
+                response.append (this.total_size.to_string ());
+            } else {
+                response.append ("*");
+            }
+        }
+
+        return response.str;
+   }
+
+    public override string to_string () {
+        return ("HTTPTimeSeekResponse (%s)".printf (get_response_string ()));
+    }
+}
diff --git a/src/librygel-server/rygel-media-container.vala b/src/librygel-server/rygel-media-container.vala
index e739ad3..46ff5fd 100644
--- a/src/librygel-server/rygel-media-container.vala
+++ b/src/librygel-server/rygel-media-container.vala
@@ -58,16 +58,22 @@ internal class Rygel.PlaylistDatasource : Rygel.DataSource, Object {
 
     public signal void data_ready ();
 
-    public void start (HTTPSeek? offsets) throws Error {
-        if (offsets != null) {
+    public Gee.List<HTTPResponseElement> ? preroll ( HTTPSeekRequest? seek_request)
+       throws Error {
+        if (seek_request != null) {
             throw new DataSourceError.SEEK_FAILED
                                         (_("Seeking not supported"));
         }
 
+        return null;
+    }
+
+
+    public void start () throws Error {
         if (this.data == null) {
             this.data_ready.connect ( () => {
                 try {
-                    this.start (offsets);
+                    this.start ();
                 } catch (Error error) { }
             });
 
diff --git a/src/media-engines/gstreamer/rygel-gst-data-source.vala 
b/src/media-engines/gstreamer/rygel-gst-data-source.vala
index 944ba82..f49f0ce 100644
--- a/src/media-engines/gstreamer/rygel-gst-data-source.vala
+++ b/src/media-engines/gstreamer/rygel-gst-data-source.vala
@@ -1,7 +1,10 @@
 /*
  * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
  *
  * Author: Jens Georg <jensg openismus com>
+ *         Prasanna Modem <prasanna ecaspia com>
+ *         Craig Pratt <craig ecaspia com>
  *
  * This file is part of Rygel.
  *
@@ -28,12 +31,14 @@ internal errordomain Rygel.GstDataSourceError {
 
 internal class Rygel.GstDataSource : Rygel.DataSource, GLib.Object {
     internal dynamic Element src;
+    internal MediaResource res;
     private Pipeline pipeline;
-    private HTTPSeek seek = null;
+    private HTTPSeekRequest seek = null;
     private GstSink sink;
     private uint bus_watch_id;
 
-    public GstDataSource (string uri) throws Error {
+    public GstDataSource (string uri, MediaResource ? resource) throws Error {
+        this.res = resource;
         this.src = GstUtils.create_source_for_uri (uri);
         if (this.src == null) {
             var msg = _("Could not create GstElement for URI %s");
@@ -56,8 +61,38 @@ internal class Rygel.GstDataSource : Rygel.DataSource, GLib.Object {
         this.src = element;
     }
 
-    public void start (HTTPSeek? offsets) throws Error {
-        this.seek = offsets;
+    public Gee.List<HTTPResponseElement> ? preroll ( HTTPSeekRequest? seek_request)
+       throws Error {
+        var response_list = new Gee.ArrayList<HTTPResponseElement>();
+
+        if (seek_request == null) {
+            debug("No seek requested - sending entire binary");
+        } else if (seek_request is HTTPByteSeekRequest) {
+            var seek_response = new HTTPByteSeekResponse.from_request( seek_request
+                                                                       as HTTPByteSeekRequest );
+            debug("Processing byte seek request for bytes %lld-%lld",
+                    seek_response.start_byte, seek_response.end_byte);
+            response_list.add(seek_response);
+            // Supported - and no reponse values required...
+        } else if (seek is HTTPTimeSeekRequest) {
+            var time_seek = seek_request as HTTPTimeSeekRequest;
+            // Set the effective TimeSeekRange response range to the requested range
+            // TODO: Align this with actual time range being returned
+            var seek_response = new HTTPTimeSeekResponse.from_request(time_seek, res.duration);
+            debug("Processing time seek request for %lldns-%lldns",
+                    seek_response.start_time, seek_response.end_time);
+            response_list.add(seek_response);
+        } else {
+            // Unknown/unsupported seek type
+            throw new DataSourceError.SEEK_FAILED
+                                    (_("HTTPSeekRequest type unsupported"));
+        }
+
+        this.seek = seek_request;
+        return response_list;
+    }
+
+    public void start () throws Error {
         this.prepare_pipeline ("RygelGstDataSource", this.src);
         if (this.seek != null) {
             this.pipeline.set_state (State.PAUSED);
@@ -220,29 +255,35 @@ internal class Rygel.GstDataSource : Rygel.DataSource, GLib.Object {
     }
 
     private bool perform_seek () {
-        if (this.seek != null &&
-            this.seek.length >= this.seek.total_length) {
-            return true;
-        }
-
         var stop_type = Gst.SeekType.NONE;
         Format format;
         var flags = SeekFlags.FLUSH;
         int64 start, stop;
 
-        if (this.seek.seek_type == HTTPSeekType.TIME) {
+        if (this.seek is HTTPTimeSeekRequest) {
+            var time_seek = this.seek as HTTPTimeSeekRequest;
             format = Format.TIME;
             flags |= SeekFlags.KEY_UNIT;
-            start = (this.seek.start) * Gst.USECOND;
-            stop = (this.seek.stop) * Gst.USECOND;
-        } else {
+            start = time_seek.start_time * Gst.USECOND;
+            stop = time_seek.end_time * Gst.USECOND;
+            debug("Performing time-range seek: %lldns to %lldns", start, stop);
+        } else if (this.seek is HTTPByteSeekRequest) {
+            var byte_seek = this.seek as HTTPByteSeekRequest;
+            if (byte_seek.range_length >= byte_seek.total_size) {
+                // How/why would this happen?
+                return true;
+            }
             format = Format.BYTES;
             flags |= SeekFlags.ACCURATE;
-            start = this.seek.start;
-            stop = this.seek.stop;
+            start = byte_seek.start_byte;
+            stop = byte_seek.end_byte;
+            debug("Performing byte-range seek: bytes %lld to %lld", start, stop);
+        } else {
+            this.error (new DataSourceError.SEEK_FAILED (_("Unsupported seek type")));
+            return false;
         }
 
-        if (this.seek.stop > 0) {
+        if (stop > 0) {
             stop_type = Gst.SeekType.SET;
         }
 
@@ -254,8 +295,7 @@ internal class Rygel.GstDataSource : Rygel.DataSource, GLib.Object {
                                  stop_type,
                                  stop + 1)) {
             warning (_("Failed to seek to offsets %lld:%lld"),
-                     this.seek.start,
-                     this.seek.stop);
+                     start, stop);
 
             this.error (new DataSourceError.SEEK_FAILED (_("Failed to seek")));
 
diff --git a/src/media-engines/gstreamer/rygel-gst-media-engine.vala 
b/src/media-engines/gstreamer/rygel-gst-media-engine.vala
index f1ca163..6f96f8f 100644
--- a/src/media-engines/gstreamer/rygel-gst-media-engine.vala
+++ b/src/media-engines/gstreamer/rygel-gst-media-engine.vala
@@ -118,7 +118,7 @@ public class Rygel.GstMediaEngine : Rygel.MediaEngine {
 
     public override DataSource? create_data_source (string uri) {
         try {
-            return new GstDataSource (uri);
+            return new GstDataSource (uri, null);
         } catch (Error error) {
             warning (_("Failed to create GStreamer data source for %s: %s"),
                      uri,
@@ -199,7 +199,7 @@ public class Rygel.GstMediaEngine : Rygel.MediaEngine {
         string source_uri = item.get_primary_uri ();
         debug ("creating data source for %s", source_uri);
 
-        DataSource ds = new GstDataSource (source_uri);
+        DataSource ds = new GstDataSource (source_uri, resource);
         debug ("MediaResource %s, profile %s, mime_type %s", resource.get_name (),
                resource.dlna_profile, resource.mime_type);
         if (resource.dlna_conversion == DLNAConversion.TRANSCODED) {
diff --git a/src/media-engines/gstreamer/rygel-gst-sink.vala b/src/media-engines/gstreamer/rygel-gst-sink.vala
index 1f21a40..c9828c4 100644
--- a/src/media-engines/gstreamer/rygel-gst-sink.vala
+++ b/src/media-engines/gstreamer/rygel-gst-sink.vala
@@ -43,7 +43,7 @@ internal class Rygel.GstSink : Sink {
     private Mutex buffer_mutex = Mutex ();
     private Cond buffer_condition = Cond ();
     private unowned DataSource source;
-    private HTTPSeek offsets;
+    private HTTPSeekRequest offsets;
 
     private bool frozen;
 
@@ -56,7 +56,7 @@ internal class Rygel.GstSink : Sink {
         add_pad_template (template);
     }
 
-    public GstSink (DataSource source, HTTPSeek? offsets) {
+    public GstSink (DataSource source, HTTPSeekRequest? offsets) {
         this.bytes_sent = 0;
         this.max_bytes = int64.MAX;
         this.source = source;
@@ -69,8 +69,8 @@ internal class Rygel.GstSink : Sink {
         this.frozen = false;
 
         if (this.offsets != null) {
-            if (this.offsets.seek_type == HTTPSeekType.BYTE) {
-                this.max_bytes = this.offsets.length;
+            if (this.offsets is HTTPByteSeekRequest) {
+                this.max_bytes = (this.offsets as HTTPByteSeekRequest).total_size;
             }
         }
 
diff --git a/src/media-engines/simple/rygel-simple-data-source.vala 
b/src/media-engines/simple/rygel-simple-data-source.vala
index ec07faa..1700eff 100644
--- a/src/media-engines/simple/rygel-simple-data-source.vala
+++ b/src/media-engines/simple/rygel-simple-data-source.vala
@@ -1,7 +1,9 @@
 /*
  * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
  *
  * Author: Jens Georg <jensg openismus com>
+ *         Craig Pratt <craig ecaspia com>
  *
  * This file is part of Rygel.
  *
@@ -38,7 +40,6 @@ internal class Rygel.SimpleDataSource : DataSource, Object {
     private Posix.off_t last_byte = 0;
     private bool frozen = false;
     private bool stop_thread = false;
-    private HTTPSeek offsets = null;
 
     public SimpleDataSource (string uri) {
         debug ("Creating new data source for %s", uri);
@@ -49,17 +50,32 @@ internal class Rygel.SimpleDataSource : DataSource, Object {
         this.stop ();
     }
 
-    public void start (HTTPSeek? offsets) throws Error {
-        if (offsets != null) {
-            if (offsets.seek_type == HTTPSeekType.TIME) {
-                throw new DataSourceError.SEEK_FAILED
-                                        (_("Time-based seek not supported"));
+    public Gee.List<HTTPResponseElement> ? preroll (HTTPSeekRequest? seek_request)
+       throws Error {
+        var response_list = new Gee.ArrayList<HTTPResponseElement> ();
 
+        if (seek_request != null) {
+            if (!(seek_request is HTTPByteSeekRequest)) {
+                throw new DataSourceError.SEEK_FAILED
+                                        (_("Only byte-based seek supported"));
             }
-        }
 
-        this.offsets = offsets;
+            var byte_seek = seek_request as HTTPByteSeekRequest;
+            this.first_byte = (Posix.off_t) byte_seek.start_byte;
+            this.last_byte = (Posix.off_t) (byte_seek.end_byte + 1);
+            debug ("Processing byte seek request for bytes %lld-%lld of %s",
+                    byte_seek.start_byte, byte_seek.end_byte, this.uri);
+            var seek_response = new HTTPByteSeekResponse.from_request (byte_seek);
+            // Response will just return what was in the request
+            response_list.add (seek_response);
+        } else {
+            this.first_byte = 0;
+            this.last_byte = 0; // Indicates the entire file
+        }
 
+        return response_list;
+    }
+    public void start () throws Error {
         debug ("Starting data source for uri %s", this.uri);
 
         // TODO: Convert to use a thread pool
@@ -109,11 +125,7 @@ internal class Rygel.SimpleDataSource : DataSource, Object {
                                           Posix.strerror (Posix.errno));
             }
 
-            if (this.offsets != null) {
-                this.first_byte = (Posix.off_t) this.offsets.start;
-                this.last_byte = (Posix.off_t) this.offsets.stop + 1;
-            } else {
-                this.first_byte = 0;
+            if (this.last_byte == 0) {
                 this.last_byte = Posix.lseek (fd, 0, Posix.SEEK_END);
                 Posix.lseek (fd, 0, Posix.SEEK_SET);
             }


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