diff --git a/src/librygel-server/filelist.am b/src/librygel-server/filelist.am
index fcc0bbc..03bc0fc 100644
--- a/src/librygel-server/filelist.am
+++ b/src/librygel-server/filelist.am
@@ -38,18 +38,22 @@ LIBRYGEL_SERVER_NONVAPI_SOURCE_FILES = \
 	rygel-content-directory.vala \
 	rygel-dbus-thumbnailer.vala \
 	rygel-engine-loader.vala \
-	rygel-http-byte-seek.vala \
+	rygel-http-byte-seek-request.vala \
+	rygel-http-byte-seek-response.vala \
 	rygel-http-get-handler.vala \
 	rygel-http-get.vala \
-	rygel-http-identity-handler.vala \
-	rygel-http-playlist-handler.vala \
+	rygel-http-thumbnail-handler.vala \
+	rygel-http-subtitle-handler.vala \
+	rygel-playlist-data-source.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-transcode-handler.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 \
 	rygel-reference-creator.vala \
@@ -77,4 +81,8 @@ LIBRYGEL_SERVER_NONVAPI_SOURCE_FILES = \
 	rygel-wmp-hacks.vala \
 	rygel-xbmc-hacks.vala \
 	rygel-xbox-hacks.vala \
-	rygel-data-sink.vala
+	rygel-data-sink.vala \
+	rygel-playspeed.vala \
+	rygel-playspeed-request.vala \
+	rygel-playspeed-response.vala \
+	rygel-dtcp-cleartext-byte-seek.vala
diff --git a/src/librygel-server/rygel-data-sink.vala b/src/librygel-server/rygel-data-sink.vala
index 9ff7d51..4bc9725 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,8 +51,8 @@ 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) {
+            this.max_bytes = (offsets as HTTPByteSeekRequest).range_length;
         }
 
         this.source.data_available.connect (this.on_data_available);
diff --git a/src/librygel-server/rygel-data-source.vala b/src/librygel-server/rygel-data-source.vala
index 437f550..fd01612 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,8 @@
 
 public errordomain Rygel.DataSourceError {
     GENERAL,
-    SEEK_FAILED
+    SEEK_FAILED,
+    PLAYSPEED_FAILED
 }
 
 /**
@@ -36,7 +39,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 +62,32 @@ public errordomain Rygel.DataSourceError {
  */
 public interface Rygel.DataSource : GLib.Object {
     /**
-     * Start producing the data.
+     * Preroll the data with the given seek and playspeed.
+     *
+     * @param seek    optional seek/range specifier
+     * @param playspeed optional playback rate specifier. This will only be provided
+     *                  when a scaled rate is requested (the speed will not be 0.0 or 1.0)
      *
-     * @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.
+     * @return List of HTTPResponseElements appropriate for the content request and
+     *         optional seek/playspeed (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.
+     *         Throws PLAYSPEED_FAILED if the rate is not supported or fulfillable.
+     */
+    public abstract Gee.List<HTTPResponseElement> ? preroll (HTTPSeekRequest? seek,
+                                                             PlaySpeedRequest? playspeed)
+       throws Error;
+
+    /**
+     * Start producing the data.
+     * 
+     * @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-dtcp-cleartext-byte-seek.vala b/src/librygel-server/rygel-dtcp-cleartext-byte-seek.vala
new file mode 100644
index 0000000..911a39c
--- /dev/null
+++ b/src/librygel-server/rygel-dtcp-cleartext-byte-seek.vala
@@ -0,0 +1,217 @@
+/* 
+ * 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 static const string DTCP_CLEARTEXT_RANGE_REQUEST_HEADER = "Range.dtcp.com";
+
+public class Rygel.DTCPCleartextByteSeekRequest : Rygel.HTTPSeekRequest {
+    /**
+     * 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 DTCPCleartextByteSeekRequest (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_CLEARTEXT_RANGE_REQUEST_HEADER);
+
+        if (range == null) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ( "%s request header not present",
+                                                           DTCP_CLEARTEXT_RANGE_REQUEST_HEADER );
+        }
+
+        if (!range.has_prefix ("bytes")) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ( "Invalid %s value (missing bytes field): '%s'",
+                                                           DTCP_CLEARTEXT_RANGE_REQUEST_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'",
+                                                           DTCP_CLEARTEXT_RANGE_REQUEST_HEADER,
+                                                           range );
+        }
+
+        if (!int64.try_parse (range_tokens[0], out start) || (start < 0)) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ( "Invalid %s range start: '%s'",
+                                                           DTCP_CLEARTEXT_RANGE_REQUEST_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_CLEARTEXT_RANGE_REQUEST_HEADER,
+                                                               range );
+            }
+            // valid end range specified
+        }
+
+        if ((end != UNSPECIFIED) && (start > end)) {
+            throw new HTTPSeekRequestError.INVALID_RANGE ( "Invalid %s range - start > end: '%s'",
+                                                           DTCP_CLEARTEXT_RANGE_REQUEST_HEADER,
+                                                           range );
+        }
+
+        if ((total_size != UNSPECIFIED) && (start > total_size-1)) {
+            throw new HTTPSeekRequestError.OUT_OF_RANGE ( "Invalid %s range - start > length: '%s'",
+                                                           DTCP_CLEARTEXT_RANGE_REQUEST_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_CLEARTEXT_RANGE_REQUEST_HEADER) != null);
+    }
+}
+
+public static const string DTCP_CLEARTEXT_RANGE_RESPONSE_HEADER = "Content-Range.dtcp.com";
+
+public class Rygel.DTCPCleartextByteSeekResponse : Rygel.HTTPResponseElement {
+    /**
+     * 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 DTCPCleartextByteSeekResponse (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;
+        this.encrypted_length = UNSPECIFIED;
+    }
+
+    public DTCPCleartextByteSeekResponse.from_request (DTCPCleartextByteSeekRequest 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;
+        this.encrypted_length = UNSPECIFIED;
+    }
+
+    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_CLEARTEXT_RANGE_RESPONSE_HEADER, response);
+        }
+        if (this.encrypted_length != UNSPECIFIED) {
+            request.msg.response_headers.set_content_length (this.encrypted_length);
+        }
+    }
+
+    public override string to_string () {
+        return ("DTCPCleartextByteSeekResponse(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-byte-seek-request.vala b/src/librygel-server/rygel-http-byte-seek-request.vala
new file mode 100644
index 0000000..a7d065a
--- /dev/null
+++ b/src/librygel-server/rygel-http-byte-seek-request.vala
@@ -0,0 +1,119 @@
+/*
+ * 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 ();
+        Soup.Range[] ranges;
+        int64 start, stop;
+        unowned string range = request.msg.request_headers.get_one ("Range");
+        string range_header_str = null;
+
+        int64 total_size = request.handler.get_resource_size ();
+        // TODO: Deal with resources that support Range but don't have fixed/known sizes
+
+        // Check if Range is present in the header
+        if (range == null) {
+            start = 0;
+            stop = total_size -1;
+        } else {
+            range_header_str = range;
+            if (request.msg.request_headers.get_ranges (total_size,
+                                                        out ranges)) {
+                // TODO: Somehow deal with multipart/byterange properly
+                //       (not legal in DLNA per 7.5.4.3.2.22.3)
+                start = ranges[0].start;
+                stop = ranges[0].end;
+                // TODO: For live/in-progress sources, we need to differentiate between "x-" and
+                //       "x-y" (and cases where y>total_size) (see DLNA 7.5.4.3.2.19.2)
+                //       We can't tell the difference with get_ranges()...
+            } else {
+                throw new HTTPSeekRequestError.INVALID_RANGE (_("Invalid Range '%s'"),
+                                                              range_header_str);
+            }
+        }
+
+        if (start > total_size-1) {
+            throw new HTTPSeekRequestError.OUT_OF_RANGE (_("Invalid Range '%s'"),
+                                                         range_header_str);
+        }
+
+        if (stop < start) {
+            throw new HTTPSeekRequestError.INVALID_RANGE (_("Invalid Range '%s'"),
+                                                          range_header_str);
+        }
+
+        if ((total_size > 0) && (stop > total_size-1)) {
+            // Per RFC 2616, the range end can be beyond the total length. And Soup doesn't clamp...
+            stop = total_size-1;
+        }
+
+        this.start_byte = start;
+        this.end_byte = stop;
+        this.range_length = stop-start+1; // +1, since range is inclusive
+        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);
+    }
+}
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..5c8603f
--- /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)"
+                .printf (this.start_byte, this.end_byte, this.total_size));
+    }
+}
diff --git a/src/librygel-server/rygel-http-byte-seek.vala b/src/librygel-server/rygel-http-byte-seek.vala
deleted file mode 100644
index 590a1ae..0000000
--- a/src/librygel-server/rygel-http-byte-seek.vala
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2009 Nokia Corporation.
- * Copyright (C) 2012 Intel Corporation.
- *
- * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
- *                               <zeeshan.ali@nokia.com>
- *         Jens Georg <jensg@openismus.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.
- */
-
-internal class Rygel.HTTPByteSeek : Rygel.HTTPSeek {
-    public HTTPByteSeek (HTTPGet request) throws HTTPSeekError {
-        Soup.Range[] ranges;
-        int64 start = 0, total_length;
-        unowned string range = request.msg.request_headers.get_one ("Range");
-
-        if (request.thumbnail != null) {
-            total_length = request.thumbnail.size;
-        } else if (request.subtitle != null) {
-            total_length = request.subtitle.size;
-        } else {
-            total_length = (request.object as MediaItem).size;
-        }
-        var stop = total_length - 1;
-
-        if (range != null) {
-            if (request.msg.request_headers.get_ranges (total_length,
-                                                        out ranges)) {
-                // TODO: Somehow deal with multipart/byterange properly
-                start = ranges[0].start;
-                stop = ranges[0].end;
-            } else {
-                // Range header was present but invalid
-                throw new HTTPSeekError.INVALID_RANGE (_("Invalid Range '%s'"),
-                                                       range);
-            }
-
-            if (start > stop) {
-                throw new HTTPSeekError.INVALID_RANGE (_("Invalid Range '%s'"),
-                                                       range);
-            }
-        }
-
-        base (request.msg, start, stop, 1, total_length);
-        this.seek_type = HTTPSeekType.BYTE;
-    }
-
-    public static bool needed (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.object is MediaContainer) && ((request.object as MediaItem).size > 0 &&
-                request.handler is HTTPIdentityHandler) ||
-               (request.thumbnail != null &&
-                request.thumbnail.size > 0) ||
-               (request.subtitle != null && request.subtitle.size > 0));
-    }
-
-    public static bool requested (HTTPGet request) {
-        return request.msg.request_headers.get_one ("Range") != null;
-    }
-
-    public override void add_response_headers () {
-        // Content-Range: bytes START_BYTE-STOP_BYTE/TOTAL_LENGTH
-        var range = "bytes ";
-        unowned Soup.MessageHeaders headers = this.msg.response_headers;
-
-        if (this.msg.request_headers.get_one ("Range") != null) {
-            headers.append ("Accept-Ranges", "bytes");
-
-            range += this.start.to_string () + "-" +
-                     this.stop.to_string () + "/" +
-                     this.total_length.to_string ();
-            headers.append ("Content-Range", range);
-        }
-
-        headers.set_content_length (this.length);
-    }
-}
diff --git a/src/librygel-server/rygel-http-get-handler.vala b/src/librygel-server/rygel-http-get-handler.vala
index df0603f..8625e3f 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,68 +29,89 @@ using GUPnP;
 /**
  * HTTP GET request handler interface.
  */
-internal abstract class Rygel.HTTPGetHandler: GLib.Object {
+public abstract class Rygel.HTTPGetHandler: GLib.Object {
     private const string TRANSFER_MODE_HEADER = "transferMode.dlna.org";
 
+    protected const string TRANSFER_MODE_STREAMING = "Streaming";
+    protected const string TRANSFER_MODE_INTERACTIVE = "Interactive";
+    protected const string TRANSFER_MODE_BACKGROUND = "Background";
+
     public Cancellable cancellable { get; set; }
 
-    // Add response headers.
+    /**
+     * Invokes the handler to add response headers to/for the given HTTP request
+     */
     public virtual void add_response_headers (HTTPGet request)
                                               throws HTTPRequestError {
         var mode = request.msg.request_headers.get_one (TRANSFER_MODE_HEADER);
-        if (mode != null) {
-            // FIXME: Is it OK to just copy the value of this header from
-            // request to response? All we do to entertain this header is to
-            // set the priority of IO operations.
-            request.msg.response_headers.append (TRANSFER_MODE_HEADER, mode);
-        }
-
-        // Yes, I know this is not the ideal code to just get a specific
-        // string for an HTTP header but if you think you can come-up with
-        // something better, be my guest and provide a patch.
-        var didl_writer = new GUPnP.DIDLLiteWriter (null);
-        var didl_item = didl_writer.add_item ();
-        try {
-            var resource = this.add_resource (didl_item, request);
-            if (resource != null) {
-                var tokens = resource.protocol_info.to_string ().split (":", 4);
-                assert (tokens.length == 4);
 
-                request.msg.response_headers.append ("contentFeatures.dlna.org",
-                                                     tokens[3]);
-            }
-        } catch (Error err) {
-            warning ("Received request for 'contentFeatures.dlna.org' but " +
-                       "failed to provide the value in response headers");
+        // Per DLNA 7.5.4.3.2.33.2, if the transferMode header is empty it
+        // must be treated as Streaming mode or Interactive, depending upon the content
+        if (mode == null) {
+            request.msg.response_headers.append (TRANSFER_MODE_HEADER,
+                                                 get_default_transfer_mode ());
+        } else {
+            request.msg.response_headers.append (TRANSFER_MODE_HEADER, mode);
         }
 
         // Handle Samsung DLNA TV proprietary subtitle headers
         if (request.msg.request_headers.get_one ("getCaptionInfo.sec") != null
+            && (request.object is VideoItem)
             && (request.object as VideoItem).subtitles.size > 0) {
                 var caption_uri = request.http_server.create_uri_for_item
-                                        (request.object as MediaItem,
+                                        (request.object,
+                                         (request.object as VideoItem).get_extension (),
                                          -1,
                                          0, // FIXME: offer first subtitle only?
-                                         null,
                                          null);
 
                 request.msg.response_headers.append ("CaptionInfo.sec",
                                                      caption_uri);
         }
+    }
 
-        request.msg.response_headers.append ("Connection", "close");
+    /**
+     * Returns the default transfer mode for the handler.
+     * The default is "Interactive"
+     */
+    public virtual string get_default_transfer_mode () {
+        return TRANSFER_MODE_INTERACTIVE; // Considering this the default
     }
 
-    public virtual bool knows_size (HTTPGet request) {
+    /**
+     * Returns true if the handler supports the given transfer mode, false otherwise.
+     */
+    public abstract bool supports_transfer_mode (string mode);
+
+    /**
+     * Returns the resource size or -1 if not known.
+     */
+    public abstract int64 get_resource_size ();
+
+    /**
+     * 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;
     }
 
-    // Create an HTTPResponse object that will render the body.
+    /**
+     * 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;
-
-    protected abstract DIDLLiteResource add_resource (DIDLLiteObject didl_object,
-                                                      HTTPGet      request)
-                                                      throws Error;
-
 }
diff --git a/src/librygel-server/rygel-http-get.vala b/src/librygel-server/rygel-http-get.vala
index 95be210..da44136 100644
--- a/src/librygel-server/rygel-http-get.vala
+++ b/src/librygel-server/rygel-http-get.vala
@@ -2,11 +2,14 @@
  * Copyright (C) 2008-2010 Nokia Corporation.
  * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
  * Copyright (C) 2012 Intel Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
  *
  * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
  *                               <zeeshan.ali@nokia.com>
  *         Jorn Baayen <jorn.baayen@gmail.com>
  *         Jens Georg <jensg@openismus.com>
+ *         Craig Pratt <craig@ecaspia.com>
+ *         Parthiban Balasubramanian <P.Balasubramanian-contractor@cablelabs.com>
  *
  * This file is part of Rygel.
  *
@@ -28,55 +31,66 @@
 /**
  * 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";
+    private const string SERVER_NAME = "CVP2-RI-DMS";
 
-    public Thumbnail thumbnail;
-    public Subtitle subtitle;
-    public HTTPSeek seek;
-
-    private int thumbnail_index;
-    private int subtitle_index;
+    public HTTPSeekRequest seek;
+    public PlaySpeedRequest speed_request;
 
     public HTTPGetHandler handler;
 
-    public HTTPGet (HTTPServer   http_server,
+    internal HTTPGet (HTTPServer   http_server,
                     Soup.Server  server,
                     Soup.Message msg) {
         base (http_server, server, msg);
-
-        this.thumbnail_index = -1;
-        this.subtitle_index = -1;
     }
 
     protected override async void handle () throws Error {
-        var header = this.msg.request_headers.get_one
-                                        ("getcontentFeatures.dlna.org");
-
         /* We only entertain 'HEAD' and 'GET' requests */
-        if ((this.msg.method != "HEAD" && this.msg.method != "GET") ||
-            (header != null && header != "1")) {
-            throw new HTTPRequestError.BAD_REQUEST (_("Invalid Request"));
+        if (!(this.msg.method == "HEAD" || this.msg.method == "GET")) {
+            throw new HTTPRequestError.BAD_REQUEST
+                          (_("Invalid Request (only GET and HEAD supported)"));
         }
 
-        if (uri.transcode_target != null) {
-            var transcoder = this.http_server.get_transcoder
-                                        (uri.transcode_target);
-            this.handler = new HTTPTranscodeHandler (transcoder,
-                                                     this.cancellable);
+        { /* Check for proper content feature request */
+            var cf_header = "getcontentFeatures.dlna.org";
+            var cf_val = this.msg.request_headers.get_one (cf_header);
+
+            if (cf_val != null && cf_val != "1") {
+                throw new HTTPRequestError.BAD_REQUEST (_(cf_header + " must be 1"));
+            }
         }
 
-        if (uri.playlist_format != null &&
-            HTTPPlaylistHandler.is_supported (uri.playlist_format)) {
-            this.handler = new HTTPPlaylistHandler (uri.playlist_format,
+        if (uri.resource_name != null) {
+            this.handler = new HTTPMediaResourceHandler (this.object,
+                                                         uri.resource_name,
+                                                         this.cancellable);
+        } else if (uri.thumbnail_index >= 0) {
+            this.handler = new HTTPThumbnailHandler (this.object as MediaFileItem,
+                                                     uri.thumbnail_index,
+                                                     this.cancellable);
+        } else if (uri.subtitle_index >= 0) {
+            this.handler = new HTTPSubtitleHandler (this.object as MediaFileItem,
+                                                    uri.subtitle_index,
                                                     this.cancellable);
+        } else {
+            throw new HTTPRequestError.NOT_FOUND ("No handler found for '%s'",
+                                                  uri.to_string ());
         }
 
-        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);
+
+            if (transfer_mode == null) {
+                transfer_mode = this.handler.get_default_transfer_mode ();
+            }
 
-        this.ensure_correct_mode ();
+            if (! this.handler.supports_transfer_mode (transfer_mode)) {
+                throw new HTTPRequestError.UNACCEPTABLE ("%s transfer mode not supported for '%s'",
+                                                        transfer_mode, uri.to_string ());
+            }
+        }
 
         yield this.handle_item_request ();
     }
@@ -84,79 +98,109 @@ internal class Rygel.HTTPGet : HTTPRequest {
     protected override async void find_item () throws Error {
         yield base.find_item ();
 
-        // No need to do anything here, will be done in PlaylistHandler
-        if (this.object is MediaContainer) {
-            return;
-        }
-
-        if (unlikely ((this.object as MediaItem).place_holder)) {
-            throw new HTTPRequestError.NOT_FOUND ("Item '%s' is empty",
+        if (unlikely ((this.object is MediaFileItem)
+                      && (this.object as MediaFileItem).place_holder)) {
+            throw new HTTPRequestError.NOT_FOUND ("MediaFileItem '%s' is empty",
                                                   this.object.id);
         }
 
         if (this.hack != null) {
             this.hack.apply (this.object);
         }
+    }
 
-        if (this.uri.thumbnail_index >= 0) {
-            if (this.object is MusicItem) {
-                var music = this.object as MusicItem;
-                this.thumbnail = music.album_art;
-
-                return;
-            } else if (this.object is VisualItem) {
-                var visual = this.object as VisualItem;
-                if (this.uri.thumbnail_index < visual.thumbnails.size) {
-                    this.thumbnail = visual.thumbnails.get
-                                            (this.uri.thumbnail_index);
-
-                    return;
-                }
+    private async void handle_item_request () throws Error {
+        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);
+        var supports_cleartext_seek = DTCPCleartextByteSeekRequest.supported (this);
+        var requested_cleartext_seek = DTCPCleartextByteSeekRequest.requested (this);
+
+        // 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 () );
             }
-
-            throw new HTTPRequestError.NOT_FOUND
-                                        ("No Thumbnail available for item '%s",
-                                         this.object.id);
-        }
-
-        if (this.uri.subtitle_index >= 0 && this.object is VideoItem) {
-            var video = this.object as VideoItem;
-
-            if (this.uri.subtitle_index < video.subtitles.size) {
-                this.subtitle = video.subtitles.get (this.uri.subtitle_index);
-
-                return;
+            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 () );
+            }
+        } else if (requested_time_seek) {
+            if (!supports_time_seek) {
+                throw new HTTPRequestError.UNACCEPTABLE ( "Time seek not supported for "
+                                                          + this.uri.to_string () );
             }
-
-            throw new HTTPRequestError.NOT_FOUND
-                                        ("No subtitles available for item '%s",
-                                         this.object.id);
         }
-    }
 
-    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");
+        // Check for DLNA PlaySpeed request only if Range or Range.dtcp.com is not
+        // in the request. DLNA 7.5.4.3.3.19.2, DLNA Link Protection : 7.6.4.4.2.12
+        // (is 7.5.4.3.3.19.2 compatible with the use case in 7.5.4.3.2.24.5?)
+        // Note: We need to check the speed first since direction factors into validating
+        //       the time-seek request
+        try {
+            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 ());
+                if (this.speed_request.speed.is_normal_rate ()) {
+                    // This is not a scaled-rate request. Treat it as if it wasn't even there
+                    this.speed_request = null;
+                }
+            } else {
+                this.speed_request = null;
+            }
+        } catch (PlaySpeedError error) {
+            this.server.unpause_message (this.msg);
+            if (error is PlaySpeedError.INVALID_SPEED_FORMAT) {
+                this.end (Soup.Status.BAD_REQUEST);
+                // Per DLNA 7.5.4.3.3.16.3
+            } else if (error is PlaySpeedError.SPEED_NOT_PRESENT) {
+                this.end (Soup.Status.NOT_ACCEPTABLE);
+                 // Per DLNA 7.5.4.3.3.16.5
+            } else {
+                throw error;
+            }
+            debug ("Error processing PlaySpeed: %s", error.message);
+            return;
         }
 
         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);
+            // Order is intentional here
+            if (supports_cleartext_seek && requested_cleartext_seek) {
+                var cleartext_seek = new DTCPCleartextByteSeekRequest (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);
+                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, ((this.speed_request == null) ? null
+                                                                : this.speed_request.speed) );
+                debug ("Processing time seek request (time %lldns to %lldns)",
+                       time_seek.start_time, time_seek.end_time);
+                this.seek = time_seek;
+            } else {
+                this.seek = null;
             }
-        } catch (HTTPSeekError error) {
+        } catch (HTTPSeekRequestError error) {
+            warning ("Caught HTTPSeekRequestError: " + error.message);
             this.server.unpause_message (this.msg);
 
-            if (error is HTTPSeekError.INVALID_RANGE) {
+            if (error is HTTPSeekRequestError.INVALID_RANGE) {
                 this.end (Soup.Status.BAD_REQUEST);
-            } else if (error is HTTPSeekError.OUT_OF_RANGE) {
+            } else if (error is HTTPSeekRequestError.OUT_OF_RANGE) {
                 this.end (Soup.Status.REQUESTED_RANGE_NOT_SATISFIABLE);
             } else {
                 throw error;
@@ -168,22 +212,72 @@ internal class Rygel.HTTPGet : HTTPRequest {
         // Add headers
         this.handler.add_response_headers (this);
 
-        // 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);
+        this.msg.response_headers.append ("Server",SERVER_NAME);
+
+        HTTPResponse response = this.handler.render_body (this);
+
+        // Have the response process the seek/speed request
+        var responses = response.preroll ();
+
+        // Incorporate the prerolled responses
+        if (responses != null) {
+            foreach (var response_elem in responses) {
+                response_elem.add_response_headers (this);
+            }
         }
 
-        if (this.handler.knows_size (this)) {
-            this.msg.response_headers.set_encoding (Soup.Encoding.CONTENT_LENGTH);
-        } else {
-            this.msg.response_headers.set_encoding (Soup.Encoding.EOF);
+        // Determine the size value
+        int64 response_size;
+        {
+            if (this.seek != null) {
+                // The response element for seeks is responsible for setting the appropriate
+                //  length if/when appropriate. So it should already be set (if it can be set)
+                response_size = this.msg.response_headers.get_content_length ();
+            } else {
+                // If a seek wasn't included, we'll be returning the entire resource binary
+                response_size = this.handler.get_resource_size ();
+                this.msg.response_headers.set_content_length (response_size);
+            }
+            // size will factor into other logic below...
+        }
+
+        // Determine the transfer mode encoding
+        {
+            Soup.Encoding response_body_encoding;
+            // See DLNA 7.5.4.3.2.15 for requirements
+            if ((this.speed_request != null)
+                && (this.msg.get_http_version () != Soup.HTTPVersion.@1_0) ) {
+                // We'll want the option to insert PlaySpeed position information
+                //  whether or not we know the length (see DLNA 7.5.4.3.3.17)
+                response_body_encoding = Soup.Encoding.CHUNKED;
+            } else if (response_size > 0) {
+                // TODO: Incorporate ChunkEncodingMode.dlna.org request into this block
+                response_body_encoding = Soup.Encoding.CONTENT_LENGTH;
+            } else { // Response size is 0
+                if (this.msg.get_http_version () == Soup.HTTPVersion.@1_0) {
+                    // Can't send the length and can't send chunked (in HTTP 1.0)...
+                    response_body_encoding = Soup.Encoding.EOF;
+                } else {
+                    response_body_encoding = Soup.Encoding.CHUNKED;
+                }
+            }
+            this.msg.response_headers.set_encoding (response_body_encoding);
+        }
+
+        // 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);
         }
 
         debug ("Following HTTP headers appended to response:");
         this.msg.response_headers.foreach ((name, value) => {
-            debug ("%s : %s", name, value);
+            debug ("    %s : %s", name, value);
         });
 
         if (this.msg.method == "HEAD") {
@@ -193,42 +287,8 @@ internal class Rygel.HTTPGet : HTTPRequest {
             return;
         }
 
-        var response = this.handler.render_body (this);
-
         yield response.run ();
 
         this.end (Soup.Status.NONE);
     }
-
-    private void ensure_correct_mode () throws HTTPRequestError {
-        var mode = this.msg.request_headers.get_one (TRANSFER_MODE_HEADER);
-        var correct = true;
-
-        switch (mode) {
-        case "Streaming":
-            correct = (!(this.handler is HTTPPlaylistHandler)) && (
-                      (this.handler is HTTPTranscodeHandler ||
-                      ((this.object as MediaItem).streamable () &&
-                       this.subtitle == null &&
-                       this.thumbnail == null)));
-
-            break;
-        case "Interactive":
-            correct = (this.handler is HTTPIdentityHandler &&
-                      ((!(this.object as MediaItem).is_live_stream () &&
-                       !(this.object as MediaItem).streamable ()) ||
-                       (this.subtitle != null ||
-                        this.thumbnail != null))) ||
-                      this.handler is HTTPPlaylistHandler;
-
-            break;
-        }
-
-        if (!correct) {
-            throw new HTTPRequestError.UNACCEPTABLE
-                                        ("%s mode not supported for '%s'",
-                                         mode,
-                                         this.object.id);
-        }
-    }
 }
diff --git a/src/librygel-server/rygel-http-identity-handler.vala b/src/librygel-server/rygel-http-identity-handler.vala
deleted file mode 100644
index 215a6ea..0000000
--- a/src/librygel-server/rygel-http-identity-handler.vala
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2008, 2009 Nokia Corporation.
- * Copyright (C) 2012 Intel Corporation.
- *
- * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
- *                               <zeeshan.ali@nokia.com>
- *         Jens Georg <jensg@openismus.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;
-
-// An HTTP request handler that passes the item content through as is.
-internal class Rygel.HTTPIdentityHandler : Rygel.HTTPGetHandler {
-
-    public HTTPIdentityHandler (Cancellable? cancellable) {
-        this.cancellable = cancellable;
-    }
-
-    public override void add_response_headers (HTTPGet request)
-                                               throws HTTPRequestError {
-        if (request.subtitle != null) {
-           request.msg.response_headers.append ("Content-Type",
-                                                request.subtitle.mime_type);
-        } else if (request.thumbnail != null) {
-            request.msg.response_headers.append ("Content-Type",
-                                                 request.thumbnail.mime_type);
-        } else {
-            request.msg.response_headers.append ("Content-Type",
-                                                 (request.object as MediaItem).mime_type);
-        }
-
-        if (request.seek != null) {
-            request.seek.add_response_headers ();
-        } else {
-            var size = this.get_size (request);
-
-            if (size > 0) {
-                request.msg.response_headers.set_content_length (size);
-            }
-        }
-
-        // Chain-up
-        base.add_response_headers (request);
-    }
-
-    public override HTTPResponse render_body (HTTPGet request)
-                                              throws HTTPRequestError {
-        try {
-            return this.render_body_real (request);
-        } catch (Error err) {
-            throw new HTTPRequestError.NOT_FOUND (err.message);
-        }
-    }
-
-    public override bool knows_size (HTTPGet request) {
-        var size = this.get_size (request);
-
-        return (request.seek != null && request.seek is HTTPByteSeek) ||
-                    size > 0;
-    }
-
-    protected override DIDLLiteResource add_resource
-                                        (DIDLLiteObject didl_object,
-                                         HTTPGet      request)
-                                        throws Error {
-        var protocol = request.http_server.get_protocol ();
-
-        if (request.thumbnail != null) {
-            return request.thumbnail.add_resource (didl_object as DIDLLiteItem,
-                                                   protocol);
-        } else {
-            return request.object.add_resource (didl_object, null, protocol);
-        }
-    }
-
-    private HTTPResponse render_body_real (HTTPGet request) throws Error {
-        DataSource src;
-        var engine = MediaEngine.get_default ();
-
-        if (request.subtitle != null) {
-            src = engine.create_data_source (request.subtitle.uri);
-        } else if (request.thumbnail != null) {
-            src = engine.create_data_source (request.thumbnail.uri);
-        } else {
-            src = (request.object as MediaItem).create_stream_source
-                                        (request.http_server.context.host_ip);
-        }
-
-        if (src == null) {
-            throw new HTTPRequestError.NOT_FOUND (_("Not found"));
-        }
-
-        return new HTTPResponse (request, this, src);
-    }
-
-    private int64 get_size (HTTPGet request) {
-        if (request.subtitle != null) {
-            return request.subtitle.size;
-        }
-
-        if (request.thumbnail != null) {
-            return request.thumbnail.size;
-        }
-
-        return (request.object as MediaItem).size;
-    }
-}
diff --git a/src/librygel-server/rygel-http-item-uri.vala b/src/librygel-server/rygel-http-item-uri.vala
index 2b92237..6b603dc 100644
--- a/src/librygel-server/rygel-http-item-uri.vala
+++ b/src/librygel-server/rygel-http-item-uri.vala
@@ -1,10 +1,12 @@
 /*
  * Copyright (C) 2009 Jens Georg <mail@jensge.org>.
  * Copyright (C) 2010 Nokia Corporation.
+ * Copyright (C) 2013 Cable Television Laboratories, Inc.
  *
  * Authors: Jens Georg <mail@jensge.org>
  *          Zeeshan Ali (Khattak) <zeeshan.ali@nokia.com>
  *                                <zeeshanak@gnome.org>
+ *          Craig Pratt <craig@ecaspia.com>
  *
  * This file is part of Rygel.
  *
@@ -25,12 +27,11 @@
 
 using Gee;
 
-internal class Rygel.HTTPItemURI : Object {
+public class Rygel.HTTPItemURI : Object {
     public string item_id { get; set; }
     public int thumbnail_index { get; set; default = -1; }
     public int subtitle_index { get; set; default = -1; }
-    public string? transcode_target { get; set; default = null; }
-    public string? playlist_format { get; set; default = null; }
+    public string? resource_name { get; set; default = null; }
     public unowned HTTPServer http_server { get; set; }
 
     private string real_extension;
@@ -46,77 +47,18 @@ internal class Rygel.HTTPItemURI : Object {
         }
     }
 
-    public static HashMap<string, string> mime_to_ext;
-
     public HTTPItemURI (MediaObject object,
                         HTTPServer http_server,
+                        string     extension,
                         int        thumbnail_index = -1,
                         int        subtitle_index = -1,
-                        string?    transcode_target = null,
-                        string?    playlist_format = null) {
+                        string?    resource_name = null) {
         this.item_id = object.id;
         this.thumbnail_index = thumbnail_index;
         this.subtitle_index = subtitle_index;
-        this.transcode_target = transcode_target;
         this.http_server = http_server;
-        this.playlist_format = playlist_format;
-        this.extension = "";
-
-        if (!(object is MediaItem)) {
-            return;
-        }
-
-        var item = object as MediaItem;
-        if (thumbnail_index > -1) {
-            if (item is VisualItem) {
-                var thumbnails = (item as VisualItem).thumbnails;
-
-                if (thumbnails.size > thumbnail_index) {
-                    this.extension = thumbnails[thumbnail_index].file_extension;
-                }
-            } else if (item is MusicItem) {
-                var album_art = (item as MusicItem).album_art;
-
-                if (album_art != null) {
-                    this.extension = album_art.file_extension;
-                }
-            }
-        } else if (subtitle_index > -1) {
-            if (item is VideoItem) {
-                var subtitles = (item as VideoItem).subtitles;
-
-                if (subtitles.size > subtitle_index) {
-                    this.extension = subtitles[subtitle_index].caption_type;
-                }
-            }
-        } else if (transcode_target != null) {
-            try {
-                var tc = this.http_server.get_transcoder (transcode_target);
-
-                this.extension = tc.extension;
-            } catch (Error error) {}
-        }
-
-        if (this.extension == "") {
-            string uri_extension = "";
-
-            foreach (string uri_string in item.uris) {
-                string basename = Path.get_basename (uri_string);
-                int dot_index = basename.last_index_of(".");
-
-                if (dot_index > -1) {
-                    uri_extension = basename.substring (dot_index + 1);
-
-                    break;
-                }
-            }
-
-            if (uri_extension == "") {
-                this.extension = this.ext_from_mime_type (item.mime_type);
-            } else {
-                this.extension = uri_extension;
-            }
-        }
+        this.extension = extension;
+        this.resource_name = resource_name;
     }
 
     // Base 64 Encoding with URL and Filename Safe Alphabet
@@ -141,7 +83,6 @@ internal class Rygel.HTTPItemURI : Object {
         // do not decode the path here as it may contain encoded slashes
         this.thumbnail_index = -1;
         this.subtitle_index = -1;
-        this.transcode_target = null;
         this.http_server = http_server;
         this.extension = "";
 
@@ -169,23 +110,15 @@ internal class Rygel.HTTPItemURI : Object {
                     StringBuilder builder = new StringBuilder ();
                     builder.append ((string) data);
                     this.item_id = builder.str;
-
-                    break;
-                case "tr":
-                    this.transcode_target = Soup.URI.decode (parts[i + 1]);
-
                     break;
                 case "th":
                     this.thumbnail_index = int.parse (parts[i + 1]);
-
                     break;
                 case "sub":
                     this.subtitle_index = int.parse (parts[i + 1]);
-
                     break;
-                case "pl":
-                    this.playlist_format = Soup.URI.decode (parts[i + 1]);
-
+                case "res":
+                    this.resource_name = Soup.URI.decode (parts[i + 1]);
                     break;
                 default:
                     break;
@@ -197,7 +130,7 @@ internal class Rygel.HTTPItemURI : Object {
         }
     }
 
-    public string to_string() {
+    public string to_string () {
         // there seems to be a problem converting strings properly to arrays
         // you need to call to_utf8() and assign it to a variable to make it
         // work properly
@@ -206,16 +139,13 @@ internal class Rygel.HTTPItemURI : Object {
         var escaped = Uri.escape_string (data, "", true);
         string path = "/i/" + escaped;
 
-        if (this.transcode_target != null) {
-            escaped = Uri.escape_string (this.transcode_target, "", true);
-            path += "/tr/" + escaped;
+        if (this.resource_name != null) {
+            escaped = Uri.escape_string (this.resource_name, "", true);
+            path += "/res/" + escaped;
         } else if (this.thumbnail_index >= 0) {
             path += "/th/" + this.thumbnail_index.to_string ();
         } else if (this.subtitle_index >= 0) {
             path += "/sub/" + this.subtitle_index.to_string ();
-        } else if (this.playlist_format != null) {
-            path += "/pl/" + Uri.escape_string
-                                        (this.playlist_format, "", true);
         }
         path += this.extension;
 
@@ -228,41 +158,4 @@ internal class Rygel.HTTPItemURI : Object {
                                           this.http_server.path_root,
                                           path);
     }
-
-    private string ext_from_mime_type (string mime_type) {
-        if (mime_to_ext == null) {
-            mime_to_ext = new HashMap<string, string> ();
-            // videos
-            string[] videos = {"mpeg", "webm", "ogg"};
-
-            foreach (string video in videos) {
-                mime_to_ext.set ("video/" + video, video);
-            }
-            mime_to_ext.set ("video/x-matroska", "mkv");
-
-            // audios
-            mime_to_ext.set ("audio/x-wav", "wav");
-            mime_to_ext.set ("audio/x-matroska", "mka");
-
-            // images
-            string[] images = {"jpeg", "png"};
-
-            foreach (string image in images) {
-                mime_to_ext.set ("image/" + image, image);
-            }
-
-            // texts
-            mime_to_ext.set ("text/srt", "srt");
-            mime_to_ext.set ("text/xml", "xml");
-
-            // applications? (can be either video or audio?);
-            mime_to_ext.set ("application/ogg", "ogg");
-        }
-
-        if (HTTPItemURI.mime_to_ext.has_key (mime_type)) {
-            return mime_to_ext.get (mime_type);
-        }
-
-        return "";
-    }
 }
diff --git a/src/librygel-server/rygel-http-playlist-handler.vala b/src/librygel-server/rygel-http-playlist-handler.vala
deleted file mode 100644
index 6a8d5aa..0000000
--- a/src/librygel-server/rygel-http-playlist-handler.vala
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2012 Intel Corporation.
- *
- * Author: Jens Georg <jensg@openismus.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;
-
-/**
- * Implementation of RygelDataSource to serve generated playlists to a client.
- */
-internal class Rygel.PlaylistDatasource : Rygel.DataSource, Object {
-    private MediaContainer container;
-    private uint8[] data;
-    private HTTPServer server;
-    private ClientHacks hacks;
-    private SerializerType playlist_type;
-
-    public PlaylistDatasource (SerializerType playlist_type,
-                               MediaContainer container,
-                               HTTPServer     server,
-                               ClientHacks?   hacks) {
-        this.playlist_type = playlist_type;
-        this.container = container;
-        this.server = server;
-        this.hacks = hacks;
-        this.generate_data.begin ();
-    }
-
-    public signal void data_ready ();
-
-    public void start (HTTPSeek? offsets) throws Error {
-        if (offsets != null) {
-            throw new DataSourceError.SEEK_FAILED
-                                        (_("Seeking not supported"));
-        }
-
-        if (this.data == null) {
-            this.data_ready.connect ( () => {
-                try {
-                    this.start (offsets);
-                } catch (Error error) { }
-            });
-
-            return;
-        }
-
-        Idle.add ( () => {
-            this.data_available (this.data);
-            this.done ();
-
-            return false;
-        });
-    }
-
-    public void freeze () { }
-
-    public void thaw () { }
-
-    public void stop () { }
-
-    public async void generate_data () {
-        try {
-            var sort_criteria = this.container.sort_criteria;
-            var count = this.container.child_count;
-
-            var children = yield this.container.get_children (0,
-                                                              count,
-                                                              sort_criteria,
-                                                              null);
-
-            if (children != null) {
-                var serializer = new Serializer (this.playlist_type);
-                children.serialize (serializer, this.server, this.hacks);
-
-                var xml = serializer.get_string ();
-
-                this.data = xml.data;
-                this.data_ready ();
-            } else {
-                this.error (new DataSourceError.GENERAL
-                                        (_("Failed to generate playlist")));
-            }
-        } catch (Error error) {
-            warning ("Could not generate playlist: %s", error.message);
-            this.error (error);
-        }
-    }
-}
-
-/**
- * RygelHTTPPlaylistHandler implements a special handler for generating XML
- * playlists (DIDL_S format as defined by DLNA) on-the-fly.
- */
-internal class Rygel.HTTPPlaylistHandler : Rygel.HTTPGetHandler {
-    private SerializerType playlist_type;
-
-    public static bool is_supported (string playlist_format) {
-        return playlist_format == "DIDL_S" || playlist_format == "M3U";
-    }
-
-    public HTTPPlaylistHandler (string playlist_format,
-                                Cancellable? cancellable) {
-        if (playlist_format == "DIDL_S") {
-            this.playlist_type = SerializerType.DIDL_S;
-        } else if (playlist_format == "M3U") {
-            this.playlist_type = SerializerType.M3UEXT;
-        }
-
-        this.cancellable = cancellable;
-    }
-
-    public override void add_response_headers (HTTPGet request)
-                                               throws HTTPRequestError {
-        // TODO: Why do we use response_headers.append instead of set_content_type
-        switch (this.playlist_type) {
-            case SerializerType.DIDL_S:
-                request.msg.response_headers.append ("Content-Type",
-                                                     "text/xml");
-                break;
-            case SerializerType.M3UEXT:
-                request.msg.response_headers.append ("ContentType",
-                                                     "audio/x-mpegurl");
-                break;
-            default:
-                assert_not_reached ();
-        }
-
-        base.add_response_headers (request);
-    }
-
-    public override HTTPResponse render_body (HTTPGet request)
-                                              throws HTTPRequestError {
-        try {
-            var source = new PlaylistDatasource
-                                        (this.playlist_type,
-                                         request.object as MediaContainer,
-                                         request.http_server,
-                                         request.hack);
-
-            return new HTTPResponse (request, this, source);
-        } catch (Error error) {
-            throw new HTTPRequestError.NOT_FOUND (error.message);
-        }
-    }
-
-    protected override DIDLLiteResource add_resource
-                                        (DIDLLiteObject didl_object,
-                                         HTTPGet        request) {
-        var protocol = request.http_server.get_protocol ();
-
-        try {
-            var res = request.object.add_resource (didl_object, null, protocol);
-
-            // set DLNA profile to get proper contentFeatures header
-            if (this.playlist_type == SerializerType.DIDL_S) {
-                res.protocol_info.dlna_profile = "DIDL_S";
-            }
-
-            return res;
-        } catch (Error error) {
-            return null as DIDLLiteResource;
-        }
-    }
-}
diff --git a/src/librygel-server/rygel-http-resource-handler.vala b/src/librygel-server/rygel-http-resource-handler.vala
new file mode 100644
index 0000000..bdd3fa9
--- /dev/null
+++ b/src/librygel-server/rygel-http-resource-handler.vala
@@ -0,0 +1,129 @@
+/* 
+ * 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;
+
+/**
+ * The HTTP handler for HTTP ContentResource requests.
+ */
+internal class Rygel.HTTPMediaResourceHandler : HTTPGetHandler {
+    private MediaObject media_object;
+    private string media_resource_name;
+    public MediaResource media_resource;
+
+    public HTTPMediaResourceHandler (MediaObject media_object,
+                                     string media_resource_name,
+                                     Cancellable? cancellable) throws HTTPRequestError
+    {
+        this.media_object = media_object;
+        this.cancellable = cancellable;
+        this.media_resource_name = media_resource_name;
+        foreach (var resource in media_object.get_resource_list ()) {
+            if (resource.get_name () == media_resource_name) {
+                this.media_resource
+                    = new MediaResource.from_resource (resource.get_name (),
+                                                       resource);
+            }
+        }
+        if (this.media_resource == null) {
+            throw new HTTPRequestError.NOT_FOUND ("MediaResource %s not found",
+                                                  media_resource_name);
+        }
+    }
+
+    public override void add_response_headers (HTTPGet request)
+                                               throws HTTPRequestError {
+        request.http_server.set_resource_delivery_options (media_resource);
+
+        request.msg.response_headers.append ("Content-Type",
+                                             this.media_resource.mime_type);
+        // Determine cache control
+        if (media_resource.is_link_protection_enabled ()) {
+            if (request.msg.get_http_version () == Soup.HTTPVersion.@1_1) {
+                request.msg.response_headers.append ("Cache-control","no-cache");
+            }
+            request.msg.response_headers.append ("Pragma","no-cache");
+        }
+
+        // Add contentFeatures.dlna.org
+        var protocol_info = media_resource.get_protocol_info ();
+        if (protocol_info != null) {
+            var pi_fields = protocol_info.to_string ().split (":", 4);
+            if (pi_fields[3] != null) {
+                request.msg.response_headers.append ("contentFeatures.dlna.org",
+                                                     pi_fields[3]);
+            }
+        }
+
+        // Chain-up
+        base.add_response_headers (request);
+    }
+
+    public override string get_default_transfer_mode () {
+        // Per DLNA 7.5.4.3.2.33.2, the assumed transfer mode is based on the content type
+        // "Streaming" for AV content and "Interactive" for all others
+        return media_resource.get_default_transfer_mode ();
+    }
+
+    public override bool supports_transfer_mode (string mode) {
+        return media_resource.supports_transfer_mode (mode);
+    }
+
+    public override HTTPResponse render_body (HTTPGet request)
+                                              throws HTTPRequestError {
+        try {
+            var src = request.object.create_stream_source_for_resource
+                                    (request, this.media_resource);
+            if (src == null) {
+                throw new HTTPRequestError.NOT_FOUND
+                              (_("Couldn't create data source for %s"),
+                               this.media_resource.get_name ());
+            }
+            return new HTTPResponse (request, this, src);
+        } catch (Error err) {
+            throw new HTTPRequestError.NOT_FOUND (err.message);
+        }
+    }
+
+    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 ();
+    }
+
+    public override bool supports_time_seek () {
+        return media_resource.supports_arbitrary_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..531ac1c
--- /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..ddc6546 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,14 @@
 
 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;
+    public PlaySpeedRequest speed;
 
     private SourceFunc run_continue;
     private int _priority = -1;
@@ -69,6 +72,7 @@ internal class Rygel.HTTPResponse : GLib.Object, Rygel.StateMachine {
         this.msg = request.msg;
         this.cancellable = request_handler.cancellable;
         this.seek = request.seek;
+        this.speed = request.speed_request;
         this.src = src;
         this.sink = new DataSink (this.src, this.server, this.msg, this.seek);
         this.src.done.connect ( () => {
@@ -99,10 +103,14 @@ internal class Rygel.HTTPResponse : GLib.Object, Rygel.StateMachine {
         }
     }
 
+    public Gee.List<HTTPResponseElement> ? preroll () throws Error {
+        return this.src.preroll (this.seek, this.speed);
+    }
+
     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..5a7a987 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,17 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
 
-public errordomain Rygel.HTTPSeekError {
+public errordomain Rygel.HTTPSeekRequestError {
     INVALID_RANGE = 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.
+ * HTTPSeekRequest is an abstract base for a variety of seek request types.
  *
- * 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.
  */
-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
new file mode 100644
index 0000000..faa6c49
--- /dev/null
+++ b/src/librygel-server/rygel-http-subtitle-handler.vala
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ *
+ * 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;
+
+internal class Rygel.HTTPSubtitleHandler : Rygel.HTTPGetHandler {
+    private MediaFileItem media_item;
+    private int subtitle_index;
+    public Subtitle subtitle;
+
+    public HTTPSubtitleHandler (MediaFileItem media_item,
+                                int subtitle_index,
+                                Cancellable? cancellable) throws HTTPRequestError {
+        this.media_item = media_item;
+        this.subtitle_index = subtitle_index;
+        this.cancellable = cancellable;
+
+        if (subtitle_index >= 0 && media_item is VideoItem) {
+            var video_item = media_item as VideoItem;
+
+            if (subtitle_index < video_item.subtitles.size) {
+                this.subtitle = video_item.subtitles.get (subtitle_index);
+            }
+        }
+
+        if (this.subtitle == null) {
+            throw new HTTPRequestError.NOT_FOUND ("Subtitle index %d not found for item '%s",
+                                                  subtitle_index, media_item.id);
+        }
+    }
+
+    public override bool supports_transfer_mode (string mode) {
+        // Support interactive and background transfers only
+        return (mode != TRANSFER_MODE_STREAMING);
+    }
+
+    public override void add_response_headers (HTTPGet request)
+                                               throws HTTPRequestError {
+        // Add Content-Type
+        request.msg.response_headers.append ("Content-Type", subtitle.mime_type);
+
+        // Add contentFeatures.dlna.org
+
+        // This is functionally equivalent to how contentFeatures was formed via the
+        //  (deprecated) HTTPIdentityHandler
+        MediaResource res = this.media_item.get_resource_list ().get (0);
+        string protocol_info = res.get_protocol_info ().to_string ();
+        var pi_fields = protocol_info.split (":", 4);
+        request.msg.response_headers.append ("contentFeatures.dlna.org", pi_fields[3]);
+
+        // Chain-up
+        base.add_response_headers (request);
+    }
+
+    public override HTTPResponse render_body (HTTPGet request)
+                                              throws HTTPRequestError {
+        DataSource src;
+        try {
+            var engine = MediaEngine.get_default ();
+            src = engine.create_data_source_for_uri (this.subtitle.uri);
+
+            return new HTTPResponse (request, this, src);
+        } catch (Error err) {
+            throw new HTTPRequestError.NOT_FOUND (err.message);
+        }
+    }
+
+    public override int64 get_resource_size () {
+        return subtitle.size;
+    }
+}
diff --git a/src/librygel-server/rygel-http-thumbnail-handler.vala b/src/librygel-server/rygel-http-thumbnail-handler.vala
new file mode 100644
index 0000000..74863cc
--- /dev/null
+++ b/src/librygel-server/rygel-http-thumbnail-handler.vala
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ *
+ * 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;
+
+internal class Rygel.HTTPThumbnailHandler : Rygel.HTTPGetHandler {
+    private MediaFileItem media_item;
+    private int thumbnail_index;
+    private Thumbnail thumbnail;
+
+    public HTTPThumbnailHandler (MediaFileItem media_item,
+                                 int thumbnail_index,
+                                 Cancellable? cancellable) throws HTTPRequestError 
+    {
+        this.media_item = media_item;
+        this.thumbnail_index = thumbnail_index;
+        this.cancellable = cancellable;
+
+        if (media_item is MusicItem) {
+            var music_item = media_item as MusicItem;
+            this.thumbnail = music_item.album_art;
+        } else if (media_item is VisualItem) {
+            var visual_item = media_item as VisualItem;
+            if (thumbnail_index < visual_item.thumbnails.size) {
+                this.thumbnail = visual_item.thumbnails.get (thumbnail_index);
+            }
+        }
+        if (this.thumbnail == null) {
+            throw new HTTPRequestError.NOT_FOUND ("Thumbnail index %d not found for item '%s",
+                                                  thumbnail_index, media_item.id);
+        }
+    }
+
+    public override bool supports_transfer_mode (string mode) {
+        // Support interactive and background transfers only
+        return (mode != TRANSFER_MODE_STREAMING);
+    }
+
+    public override void add_response_headers (HTTPGet request)
+                                               throws HTTPRequestError {
+        // Add Content-Type
+        request.msg.response_headers.append ("Content-Type", thumbnail.mime_type);
+
+        // Add contentFeatures.dlna.org
+        MediaResource res = this.thumbnail.get_resource (request.http_server.get_protocol ());
+        string protocol_info = res.get_protocol_info ().to_string ();
+        var pi_fields = protocol_info.split (":", 4);
+        request.msg.response_headers.append ("contentFeatures.dlna.org", pi_fields[3]);
+
+        // Chain-up
+        base.add_response_headers (request);
+    }
+
+    public override HTTPResponse render_body (HTTPGet request)
+                                              throws HTTPRequestError {
+        DataSource src;
+        try {
+            var engine = MediaEngine.get_default ();
+            src = engine.create_data_source_for_uri (this.thumbnail.uri);
+
+            return new HTTPResponse (request, this, src);
+        } catch (Error err) {
+            throw new HTTPRequestError.NOT_FOUND (err.message);
+        }
+    }
+
+    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..d35d6d7
--- /dev/null
+++ b/src/librygel-server/rygel-http-time-seek-request.vala
@@ -0,0 +1,242 @@
+/*
+ * 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.
+ */
+
+public static const string TIMESEEKRANGE_HEADER = "TimeSeekRange.dlna.org";
+
+/**
+ * This class represents a DLNA TimeSeekRange request and response.
+ *
+ * A TimeSeekRange request can only have a time range ("npt=start-end"). But a
+ * TimeSeekRange response may have a time range ("npt=start-end/duration" and
+ * a byte range ("bytes=") corresponding with the actual time/data range.
+ *
+ * Note that DLNA requires both "npt=" and "bytes=" to be set if both
+ * data- and time-based seek are supported. (see DLNA 7.5.4.3.2.24.5)
+ */
+public class Rygel.HTTPTimeSeekRequest : Rygel.HTTPSeekRequest {
+    /**
+     * 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.
+     *
+     * @param request The HTTP GET/HEAD request
+     * @param positive_rate Indicates if playback is in the positive or negative direction
+     */
+    internal HTTPTimeSeekRequest (HTTPGet request, PlaySpeed ? speed)
+            throws HTTPSeekRequestError {
+        base ();
+
+        bool positive_rate = (speed == null) || speed.is_positive ();
+        bool trick_mode = (speed != null) && !speed.is_normal_rate ();
+
+        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);
+        }
+
+        // Check for out-of-bounds range start and clamp it in if in trick/scan mode
+        if ((this.total_duration != UNSPECIFIED) && (start > this.total_duration)) {
+            if (trick_mode && !positive_rate) { // Per DLNA 7.5.4.3.2.24.4
+                this.start_time = this.total_duration;
+            } else { // See DLNA 7.5.4.3.2.24.8
+                throw new HTTPSeekRequestError.OUT_OF_RANGE
+                              ("Invalid %s start time %lldns is beyond the content duration of %lldns",
+                               TIMESEEKRANGE_HEADER, start, this.total_duration);
+            }
+        } else { // Nothing to check it against - just store it
+            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
+            if (positive_rate) {
+                // Check for out-of-bounds range end or fence it in
+                if ((this.total_duration != UNSPECIFIED) && (end > this.total_duration)) {
+                    if (trick_mode) { // Per DLNA 7.5.4.3.2.24.4
+                        this.end_time = this.total_duration;
+                    } else { // Per DLNA 7.5.4.3.2.24.8
+                        throw new HTTPSeekRequestError.OUT_OF_RANGE
+                                      ("Invalid %s end time %lldns is beyond the content duration of %lldns",
+                                       TIMESEEKRANGE_HEADER, end,this.total_duration);
+                    }
+                } else {
+                    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 { // Negative rate
+                // Note: start_time has already been checked/clamped
+                this.end_time = end;
+                this.range_duration = this.start_time - this.end_time;
+                // At negative 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 before end time - reverse 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 (positive_rate) {
+                this.range_duration = this.total_duration - this.start_time;
+            } else { // Negative rate
+                this.range_duration = this.start_time; // Going backward from start to 0
+            }
+        }
+    }
+
+    /**
+     * 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..8c91d85
--- /dev/null
+++ b/src/librygel-server/rygel-http-time-seek-response.vala
@@ -0,0 +1,175 @@
+/*
+ * 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 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; private set; }
+
+    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.range_length = 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.range_length = UNSPECIFIED;
+        this.total_size = UNSPECIFIED;
+    }
+
+    /**
+     * 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 (TIMESEEKRANGE_HEADER, response);
+            if (this.start_byte != 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.range_length);
+            }
+        }
+    }
+
+    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);
+        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) {
+            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-http-time-seek.vala b/src/librygel-server/rygel-http-time-seek.vala
deleted file mode 100644
index 675d4e1..0000000
--- a/src/librygel-server/rygel-http-time-seek.vala
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2009 Nokia Corporation.
- * Copyright (C) 2012 Intel Corporation.
- *
- * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
- *                               <zeeshan.ali@nokia.com>
- *         Jens Georg <jensg@openismus.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.
- */
-
-internal class Rygel.HTTPTimeSeek : Rygel.HTTPSeek {
-    public HTTPTimeSeek (HTTPGet request) throws HTTPSeekError {
-        string range;
-        string[] range_tokens;
-        int64 start = 0;
-        int64 duration = (request.object as AudioItem).duration * TimeSpan.SECOND;
-        int64 stop = duration - TimeSpan.MILLISECOND;
-        int64 parsed_value = 0;
-        bool parsing_start = true;
-
-        range = request.msg.request_headers.get_one ("TimeSeekRange.dlna.org");
-
-        if (range != null) {
-            if (!range.has_prefix ("npt=")) {
-                throw new HTTPSeekError.INVALID_RANGE ("Invalid Range '%s'",
-                                                       range);
-            }
-
-            range_tokens = range.substring (4).split ("-", 2);
-            if (range_tokens[0] == null ||
-                // Start token of the range must be provided
-                range_tokens[0] == "" ||
-                range_tokens[1] == null) {
-                throw new HTTPSeekError.INVALID_RANGE (_("Invalid Range '%s'"),
-                                                       range);
-            }
-
-            foreach (string range_token in range_tokens) {
-                if (range_token == "") {
-                    continue;
-                }
-
-                if (range_token.index_of (":") == -1) {
-                    if (!parse_seconds (range_token, ref parsed_value)) {
-                        throw new HTTPSeekError.INVALID_RANGE
-                                            (_("Invalid Range '%s'"),
-                                               range);
-                    }
-                } else {
-                    if (!parse_time (range_token,
-                                     ref parsed_value)) {
-                        throw new HTTPSeekError.INVALID_RANGE
-                                            (_("Invalid Range '%s'"),
-                                               range);
-                    }
-                }
-
-                if (parsing_start) {
-                    parsing_start = false;
-                    start = parsed_value;
-                } else {
-                    stop = parsed_value;
-                }
-            }
-
-            if (start > stop) {
-                throw new HTTPSeekError.INVALID_RANGE
-                                    (_("Invalid Range '%s'"),
-                                       range);
-            }
-        }
-
-        base (request.msg, start, stop - 1, TimeSpan.MILLISECOND, duration);
-        this.seek_type = HTTPSeekType.TIME;
-    }
-
-    public static bool needed (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.object is AudioItem &&
-               (request.object as AudioItem).duration > 0 &&
-               (request.handler is HTTPTranscodeHandler ||
-                (request.thumbnail == null &&
-                 request.subtitle == null &&
-                 (request.object as MediaItem).is_live_stream ())));
-    }
-
-    public static bool requested (HTTPGet request) {
-        return request.msg.request_headers.get_one ("TimeSeekRange.dlna.org") !=
-               null;
-    }
-
-    public override void add_response_headers () {
-        // TimeSeekRange.dlna.org: npt=START_TIME-END_TIME/DURATION
-        double start = (double) this.start / TimeSpan.SECOND;
-        double stop = (double) this.stop / TimeSpan.SECOND;
-        double total = (double) this.total_length / TimeSpan.SECOND;
-
-        var start_str = new char[double.DTOSTR_BUF_SIZE];
-        var stop_str = new char[double.DTOSTR_BUF_SIZE];
-        var total_str = new char[double.DTOSTR_BUF_SIZE];
-
-        var range = "npt=" + start.format (start_str, "%.3f") + "-" +
-                             stop.format (stop_str, "%.3f") + "/" +
-                             total.format (total_str, "%.3f");
-
-        this.msg.response_headers.append ("TimeSeekRange.dlna.org", range);
-    }
-
-    // Parses npt times in the format of '417.33'
-    private static bool parse_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'
-    private static bool parse_time (string    range_token,
-                                    ref int64 value) {
-        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-transcode-handler.vala b/src/librygel-server/rygel-http-transcode-handler.vala
deleted file mode 100644
index da21a06..0000000
--- a/src/librygel-server/rygel-http-transcode-handler.vala
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2009 Nokia Corporation.
- * Copyright (C) 2012 Intel Corporation.
- *
- * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
- *                               <zeeshan.ali@nokia.com>
- *         Jens Georg <jensg@openismus.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;
-
-/**
- * The handler for HTTP transcoding requests.
- */
-internal class Rygel.HTTPTranscodeHandler : HTTPGetHandler {
-    private Transcoder transcoder;
-
-    public HTTPTranscodeHandler (Transcoder   transcoder,
-                                 Cancellable? cancellable) {
-        this.transcoder = transcoder;
-        this.cancellable = cancellable;
-    }
-
-    public override void add_response_headers (HTTPGet request)
-                                               throws HTTPRequestError {
-        request.msg.response_headers.append ("Content-Type",
-                                             this.transcoder.mime_type);
-        if (request.seek != null) {
-            request.seek.add_response_headers ();
-        }
-
-        // Chain-up
-        base.add_response_headers (request);
-    }
-
-    public override HTTPResponse render_body (HTTPGet request)
-                                              throws HTTPRequestError {
-        var item = request.object as MediaItem;
-        var src = item.create_stream_source
-                                        (request.http_server.context.host_ip);
-        if (src == null) {
-            throw new HTTPRequestError.NOT_FOUND (_("Not found"));
-        }
-
-        try {
-            src = this.transcoder.create_source (item, src);
-
-            return new HTTPResponse (request, this, src);
-        } catch (GLib.Error err) {
-            throw new HTTPRequestError.NOT_FOUND (err.message);
-        }
-    }
-
-    protected override DIDLLiteResource add_resource
-                                        (DIDLLiteObject didl_object,
-                                         HTTPGet      request)
-                                        throws Error {
-        return this.transcoder.add_resource (didl_object as DIDLLiteItem,
-                                             request.object as MediaItem,
-                                             request.http_server);
-    }
-}
diff --git a/src/librygel-server/rygel-playlist-data-source.vala b/src/librygel-server/rygel-playlist-data-source.vala
new file mode 100644
index 0000000..b0b03bb
--- /dev/null
+++ b/src/librygel-server/rygel-playlist-data-source.vala
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ *
+ * 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;
+
+/**
+ * Implementation of RygelDataSource to serve generated playlists to a client.
+ */
+internal class Rygel.PlaylistDatasource : Rygel.DataSource, Object {
+    private MediaContainer container;
+    private uint8[] data;
+    private HTTPServer server;
+    private ClientHacks hacks;
+    private SerializerType playlist_type;
+
+    public PlaylistDatasource (SerializerType playlist_type,
+                               MediaContainer container,
+                               HTTPServer     server,
+                               ClientHacks?   hacks) {
+        this.playlist_type = playlist_type;
+        this.container = container;
+        this.server = server;
+        this.hacks = hacks;
+        this.generate_data.begin ();
+    }
+
+    public signal void data_ready ();
+
+    public Gee.List<HTTPResponseElement> ? preroll ( HTTPSeekRequest? seek_request,
+                                                     PlaySpeedRequest? playspeed_request)
+       throws Error {
+        if (seek_request != null) {
+            throw new DataSourceError.SEEK_FAILED
+                                        (_("Seeking not supported"));
+        }
+
+        if (playspeed_request != null) {
+            throw new DataSourceError.PLAYSPEED_FAILED
+                                    (_("Speed not supported"));
+        }
+
+        return null;
+    }
+
+    public void start () throws Error {
+        if (this.data == null) {
+            this.data_ready.connect ( () => {
+                try {
+                    this.start ();
+                } catch (Error error) { }
+            });
+
+            return;
+        }
+
+        Idle.add ( () => {
+            this.data_available (this.data);
+            this.done ();
+
+            return false;
+        });
+    }
+
+    public void freeze () { }
+
+    public void thaw () { }
+
+    public void stop () { }
+
+    public async void generate_data () {
+        try {
+            var sort_criteria = this.container.sort_criteria;
+            var count = this.container.child_count;
+
+            var children = yield this.container.get_children (0,
+                                                              count,
+                                                              sort_criteria,
+                                                              null);
+
+            if (children != null) {
+                var serializer = new Serializer (this.playlist_type);
+                children.serialize (serializer, this.server, this.hacks);
+
+                var xml = serializer.get_string ();
+
+                this.data = xml.data;
+                this.data_ready ();
+            } else {
+                this.error (new DataSourceError.GENERAL
+                                        (_("Failed to generate playlist")));
+            }
+        } catch (Error error) {
+            warning ("Could not generate playlist: %s", error.message);
+            this.error (error);
+        }
+    }
+}
diff --git a/src/librygel-server/rygel-playspeed-request.vala b/src/librygel-server/rygel-playspeed-request.vala
new file mode 100644
index 0000000..144e0fa
--- /dev/null
+++ b/src/librygel-server/rygel-playspeed-request.vala
@@ -0,0 +1,106 @@
+/* 
+ * 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.
+ */
+
+public errordomain Rygel.PlaySpeedError {
+    INVALID_SPEED_FORMAT,
+    SPEED_NOT_PRESENT
+}
+
+public static const string PLAYSPEED_HEADER = "PlaySpeed.dlna.org";
+
+/**
+ * This class represents a DLNA PlaySpeed request (PlaySpeed.dlna.org)
+ */
+public class Rygel.PlaySpeedRequest : GLib.Object {
+    public PlaySpeed speed { get; private set; }
+
+    internal static bool requested (HTTPGet request) {
+        return request.msg.request_headers.get_one (PLAYSPEED_HEADER) != null;
+    }
+
+    public PlaySpeedRequest (int numerator, uint denominator) {
+        base ();
+        this.speed = new PlaySpeed (numerator, denominator);
+    }
+
+    public PlaySpeedRequest.from_string (string speed) throws PlaySpeedError {
+        base ();
+        this.speed = new PlaySpeed.from_string (speed);
+    }
+
+    internal PlaySpeedRequest.from_request (Rygel.HTTPGet request) throws PlaySpeedError {
+        base ();
+        // Format: PlaySpeed.dlna.org: speed=<rate>
+        string speed_string = request.msg.request_headers.get_one (PLAYSPEED_HEADER);
+
+        if (speed_string == null) {
+            throw new PlaySpeedError.SPEED_NOT_PRESENT ("Could not find %s",
+                                                        PLAYSPEED_HEADER);
+        }
+
+        var elements = speed_string.split ("=");
+
+        if ((elements.length != 2) || (elements[0] != "speed")) {
+            throw new PlaySpeedError.INVALID_SPEED_FORMAT ("ill-formed value for "
+                                                           + PLAYSPEED_HEADER + ": "
+                                                           + speed_string );
+        }
+
+        speed = new PlaySpeed.from_string (elements[1]);
+
+        // Normal rate is always valid. Just check for valid scaled rate
+        if (!speed.is_normal_rate ()) {
+            // Validate if playspeed is listed in the protocolInfo
+            if (request.handler is HTTPMediaResourceHandler) {
+                MediaResource resource = (request.handler as HTTPMediaResourceHandler)
+                                         .media_resource;
+                var speeds = resource.play_speeds;
+                bool found_speed = false;
+                foreach (var speed in speeds) {
+                    var cur_speed = new PlaySpeedRequest.from_string (speed);
+                    if (this.equals (cur_speed)) {
+                        found_speed = true;
+                        break;
+                    }
+                }
+                if (!found_speed) {
+                    throw new PlaySpeedError
+                              .SPEED_NOT_PRESENT ("Unknown playspeed requested (%s)",
+                                                  speed_string);
+                }
+            }
+        }
+    }
+
+    public bool equals (PlaySpeedRequest that) {
+        if (that == null) return false;
+
+        return (this.speed.equals (that.speed));
+    }
+}
diff --git a/src/librygel-server/rygel-playspeed-response.vala b/src/librygel-server/rygel-playspeed-response.vala
new file mode 100644
index 0000000..6981bdf
--- /dev/null
+++ b/src/librygel-server/rygel-playspeed-response.vala
@@ -0,0 +1,84 @@
+/* 
+ * 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.
+ */
+
+public static const string FRAMERATE_HEADER = "FrameRateInTrickMode.dlna.org";
+
+/**
+ * This class represents a DLNA PlaySpeed response (PlaySpeed.dlna.org)
+ */
+public class Rygel.PlaySpeedResponse : Rygel.HTTPResponseElement {
+    PlaySpeed speed;
+    public static const int NO_FRAMERATE = -1;
+
+    /**
+     * The framerate supported for the given rate, in frames per second
+     */
+    public int framerate;
+
+    public PlaySpeedResponse (int numerator, uint denominator, int framerate) {
+        base ();
+        this.speed = new PlaySpeed (numerator, denominator);
+        this.framerate = NO_FRAMERATE;
+    }
+
+    public PlaySpeedResponse.from_speed (PlaySpeed speed, int framerate)
+       throws PlaySpeedError {
+        base ();
+        this.speed = speed;
+        this.framerate = framerate;
+    }
+
+    public PlaySpeedResponse.from_string (string speed, int framerate)
+       throws PlaySpeedError {
+        base ();
+        this.speed = new PlaySpeed.from_string (speed);
+        this.framerate = NO_FRAMERATE;
+    }
+
+    public bool equals (PlaySpeedRequest that) {
+        if (that == null) return false;
+
+        return (this.speed.equals (that.speed));
+    }
+
+    public override void add_response_headers (Rygel.HTTPRequest request) {
+        // Format: PlaySpeed.dlna.org: speed=<rate>
+        request.msg.response_headers.append (PLAYSPEED_HEADER, "speed=" + this.speed.to_string ());
+        if (this.framerate > 0) {
+            // Format: FrameRateInTrickMode.dlna.org: rate=<framerate>
+            request.msg.response_headers.append ( FRAMERATE_HEADER,
+                                                  "rate=" + this.framerate.to_string () );
+        }
+    }
+
+    public override string to_string () {
+        return ("PlaySpeedResponse(speed=%s, framerate=%d)"
+                .printf (this.speed.to_string (), this.framerate));
+    }
+}
diff --git a/src/librygel-server/rygel-playspeed.vala b/src/librygel-server/rygel-playspeed.vala
new file mode 100644
index 0000000..f1f0dfa
--- /dev/null
+++ b/src/librygel-server/rygel-playspeed.vala
@@ -0,0 +1,101 @@
+/* 
+ * 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 is a container for a PlaySpeed value.
+ * 
+ * A Playspeed can be positive or negative whole numbers or fractions.
+ * e.g. "2". "1/2", "-1/4"
+ */
+public class Rygel.PlaySpeed {
+    public int numerator; // Sign of the speed will be attached to the numerator
+    public uint denominator;
+
+    public PlaySpeed (int numerator, uint denominator) {
+        this.numerator = numerator;
+        this.denominator = denominator;
+    }
+
+    public PlaySpeed.from_string (string speed) throws PlaySpeedError {
+        parse (speed);
+    }
+
+    public bool equals (PlaySpeed that) {
+        if (that == null) return false;
+
+        return ( (this.numerator == that.numerator)
+                 && (this.denominator == that.denominator) );
+    }
+
+    public bool is_positive () {
+        return (this.numerator > 0);
+    }
+
+    public bool is_normal_rate () {
+        return (this.numerator == 1) && (this.denominator == 1);
+    }
+
+    public string to_string () {
+        if (this.denominator == 1) {
+            return numerator.to_string ();
+        } else {
+            return this.numerator.to_string () + "/" + this.denominator.to_string ();
+        }
+    }
+
+    public float to_float () {
+        return (float)numerator/denominator;
+    }
+
+    public double to_double () {
+        return (double)numerator/denominator;
+    }
+
+    private void parse (string speed) throws PlaySpeedError {
+        if (! ("/" in speed)) {
+            this.numerator = int.parse (speed);
+            this.denominator = 1;
+        } else {
+            var elements = speed.split ("/");
+            if (elements.length != 2) {
+                throw new PlaySpeedError.INVALID_SPEED_FORMAT ("Missing/extra numerator/denominator");
+            }
+            this.numerator = int.parse (elements[0]);
+            this.denominator = int.parse (elements[1]);
+        }
+        // "0" isn't a valid numerator or denominator (and parse returns "0" on parse error)
+        if (this.numerator == 0) {
+            throw new PlaySpeedError.INVALID_SPEED_FORMAT ("Invalid numerator in speed: " + speed);
+        }
+        if (this.denominator <= 0) {
+            throw new PlaySpeedError.INVALID_SPEED_FORMAT ("Invalid denominator in speed: " + speed);
+        }
+    }
+}
+
diff --git a/src/plugins/gst-launch/rygel-gst-launch-audio-item.vala b/src/plugins/gst-launch/rygel-gst-launch-audio-item.vala
index ed696f8..9d24e21 100644
--- a/src/plugins/gst-launch/rygel-gst-launch-audio-item.vala
+++ b/src/plugins/gst-launch/rygel-gst-launch-audio-item.vala
@@ -42,7 +42,8 @@ public class Rygel.GstLaunch.AudioItem : Rygel.AudioItem, Item {
         this.launch_line = launch_line;
     }
 
-    public override DataSource? create_stream_source (string? host_ip) {
+    public override DataSource? create_stream_source_for_resource (HTTPRequest request,
+                                                                   MediaResource resource) {
         return this.create_source ();
     }
 }
diff --git a/src/plugins/gst-launch/rygel-gst-launch-item.vala b/src/plugins/gst-launch/rygel-gst-launch-item.vala
index 965ef92..7a8626c 100644
--- a/src/plugins/gst-launch/rygel-gst-launch-item.vala
+++ b/src/plugins/gst-launch/rygel-gst-launch-item.vala
@@ -26,13 +26,13 @@
 /**
  * Item that serves data from a gst-launch commandline.
  */
-public interface Rygel.GstLaunch.Item : Rygel.MediaItem {
+public interface Rygel.GstLaunch.Item : Rygel.MediaFileItem {
     public abstract string launch_line { get; protected set; }
 
     protected DataSource? create_source () {
         var engine = MediaEngine.get_default ();
 
-        return engine.create_data_source ("gst-launch://" + this.launch_line);
+        return engine.create_data_source_for_uri ("gst-launch://" + this.launch_line);
     }
 }
 
diff --git a/src/plugins/gst-launch/rygel-gst-launch-video-item.vala b/src/plugins/gst-launch/rygel-gst-launch-video-item.vala
index be29c61..90c4554 100644
--- a/src/plugins/gst-launch/rygel-gst-launch-video-item.vala
+++ b/src/plugins/gst-launch/rygel-gst-launch-video-item.vala
@@ -42,7 +42,8 @@ public class Rygel.GstLaunch.VideoItem : Rygel.VideoItem, Item {
         this.launch_line = launch_line;
     }
 
-    public override DataSource? create_stream_source (string? host_ip) {
+    public override DataSource? create_stream_source_for_resource (HTTPRequest request,
+                                                                   MediaResource resource) {
         return this.create_source ();
     }
 }
