[rygel] Add support for time-based seeking in transcoded streams.



commit a49f56767756fa629f7f754d26927c1e7d05d8a3
Author: James Henstridge <james jamesh id au>
Date:   Mon Jul 20 19:11:07 2009 +0800

    Add support for time-based seeking in transcoded streams.
    
    * Transcoded resources now set the DLNA operation to TIMESEEK.
    * The Rygel.Seek class has been moved out into its own file, and now
      includes code to represent time based ranges.
    * The Rygel.Seek class includes routines for parsing both byte ranges
      (from the standard Range HTTP header), and time ranges (from the
      non-standard TimeSeekRange.dlna.org header).
    * For transcoded streams, a TimeSeekRange.dlna.org response header is
      generated if the request included one.
    * LiveResponse seeks the pipeline to the appropriate starting point
      before playback.

 src/rygel/Makefile.am              |    3 +
 src/rygel/rygel-http-request.vala  |   99 +++------------------
 src/rygel/rygel-http-response.vala |   36 --------
 src/rygel/rygel-live-response.vala |   23 +++++-
 src/rygel/rygel-seek.vala          |  169 ++++++++++++++++++++++++++++++++++++
 src/rygel/rygel-transcoder.vala    |    2 +-
 6 files changed, 207 insertions(+), 125 deletions(-)
---
diff --git a/src/rygel/Makefile.am b/src/rygel/Makefile.am
index ad0c7de..e43a93c 100644
--- a/src/rygel/Makefile.am
+++ b/src/rygel/Makefile.am
@@ -52,6 +52,7 @@ BUILT_SOURCES = rygel-1.0.vapi \
 		rygel-http-server.c \
 		rygel-state-machine.c \
 		rygel-http-request.c \
+		rygel-seek.c \
 		rygel-http-response.c \
 		rygel-live-response.c \
 		rygel-seekable-response.c \
@@ -95,6 +96,7 @@ rygel_SOURCES = $(VAPI_SOURCE_FILES) \
 		rygel-http-server.c \
 		rygel-state-machine.c \
 		rygel-http-request.c \
+		rygel-seek.c \
 		rygel-http-response.c \
 		rygel-live-response.c \
 		rygel-seekable-response.c \
@@ -150,6 +152,7 @@ VAPI_SOURCE_FILES = rygel-configuration.vala \
 		    rygel-http-server.vala \
 		    rygel-state-machine.vala \
 		    rygel-http-request.vala \
+		    rygel-seek.vala \
 		    rygel-http-response.vala \
 		    rygel-live-response.vala \
 		    rygel-seekable-response.vala \
diff --git a/src/rygel/rygel-http-request.vala b/src/rygel/rygel-http-request.vala
index e579b0f..ffd0772 100644
--- a/src/rygel/rygel-http-request.vala
+++ b/src/rygel/rygel-http-request.vala
@@ -49,7 +49,8 @@ internal class Rygel.HTTPRequest : GLib.Object, Rygel.StateMachine {
     private string item_id;
     private Transcoder transcoder;
     private MediaItem item;
-    private Seek seek;
+    private Seek byte_range;
+    private Seek time_range;
 
     private Cancellable cancellable;
 
@@ -100,7 +101,8 @@ internal class Rygel.HTTPRequest : GLib.Object, Rygel.StateMachine {
         var response = new LiveResponse (this.server,
                                          this.msg,
                                          "RygelLiveResponse",
-                                         src);
+                                         src,
+                                         this.time_range);
         this.response = response;
         response.completed += on_response_completed;
 
@@ -111,7 +113,7 @@ internal class Rygel.HTTPRequest : GLib.Object, Rygel.StateMachine {
         var response = new SeekableResponse (this.server,
                                              this.msg,
                                              uri,
-                                             this.seek,
+                                             this.byte_range,
                                              size);
         this.response = response;
         response.completed += on_response_completed;
@@ -125,7 +127,8 @@ internal class Rygel.HTTPRequest : GLib.Object, Rygel.StateMachine {
 
     private void handle_item_request () {
         try {
-            this.parse_range ();
+            this.byte_range = Seek.from_byte_range(this.msg);
+            this.time_range = Seek.from_time_range(this.msg);
         } catch (Error error) {
             this.handle_error (error);
             return;
@@ -158,6 +161,7 @@ internal class Rygel.HTTPRequest : GLib.Object, Rygel.StateMachine {
         if (this.transcoder != null) {
             this.msg.response_headers.append ("Content-Type",
                                               this.transcoder.mime_type);
+            this.time_range.add_response_header(this.msg);
             return;
         }
 
@@ -171,24 +175,15 @@ internal class Rygel.HTTPRequest : GLib.Object, Rygel.StateMachine {
         }
 
         if (this.item.size > 0) {
-            int64 first_byte;
-            int64 last_byte;
+            Seek seek;
 
-            if (this.seek != null) {
-                first_byte = this.seek.start;
-                last_byte = this.seek.stop;
+            if (this.byte_range != null) {
+                seek = this.byte_range;
             } else {
-                first_byte = 0;
-                last_byte = this.item.size - 1;
+                seek = new Seek (Format.BYTES, 0, this.item.size - 1);
             }
 
-            // Content-Range: bytes START_BYTE-STOP_BYTE/TOTAL_LENGTH
-            var content_range = "bytes " +
-                                first_byte.to_string () + "-" +
-                                last_byte.to_string () + "/" +
-                                this.item.size.to_string ();
-            this.msg.response_headers.append ("Content-Range", content_range);
-            this.msg.response_headers.append ("Accept-Ranges", "bytes");
+            seek.add_response_header (this.msg, this.item.size);
         }
     }
 
@@ -237,74 +232,6 @@ internal class Rygel.HTTPRequest : GLib.Object, Rygel.StateMachine {
         this.serve_uri (uri, this.item.size);
     }
 
-    private void parse_range () throws HTTPRequestError {
-            string range;
-            string[] range_tokens;
-
-            range = this.msg.request_headers.get ("Range");
-            if (range == null) {
-                return;
-            }
-
-            // We have a Range header. Parse.
-            if (!range.has_prefix ("bytes=")) {
-                throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
-                                                       range);
-            }
-
-            range_tokens = range.offset (6).split ("-", 2);
-
-            if (range_tokens[0] == null || range_tokens[1] == null) {
-                throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
-                                                       range);
-            }
-
-            this.seek = new Seek (Format.BYTES, 0, this.item.size - 1);
-
-            // Get first byte position
-            string first_byte = range_tokens[0];
-            if (first_byte[0].isdigit ()) {
-                this.seek.start = first_byte.to_int64 ();
-            } else if (first_byte  != "") {
-                throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
-                                                       range);
-            }
-
-            // Get last byte position if specified
-            string last_byte = range_tokens[1];
-            if (last_byte[0].isdigit ()) {
-                this.seek.stop = last_byte.to_int64 ();
-            } else if (last_byte  != "") {
-                throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
-                                                       range);
-            }
-
-            if (this.item.size > 0) {
-                // shouldn't go beyond actual length of media
-                if (this.seek.start > this.item.size ||
-                    this.seek.length > this.item.size) {
-                    throw new HTTPRequestError.OUT_OF_RANGE (
-                            "Range '%s' not setsifiable", range);
-                }
-
-                // No need to seek if whole stream is requested
-                if (this.seek.start == 0 &&
-                    this.seek.length == this.item.size) {
-                    this.seek == null;
-                    return;
-                }
-            } else if (this.seek.start == 0) {
-                // Might be an attempt to get the size, in which case it's not
-                // an error. Just don't seek.
-                this.seek == null;
-                return;
-            } else {
-                throw new HTTPRequestError.UNACCEPTABLE (
-                            "Partial download not applicable for item %s",
-                            this.item.id);
-            }
-    }
-
     private void on_item_found (GLib.Object source_object,
                                 AsyncResult res) {
         var container = (MediaContainer) source_object;
diff --git a/src/rygel/rygel-http-response.vala b/src/rygel/rygel-http-response.vala
index efbd72c..c7c2583 100644
--- a/src/rygel/rygel-http-response.vala
+++ b/src/rygel/rygel-http-response.vala
@@ -81,39 +81,3 @@ internal abstract class Rygel.HTTPResponse : GLib.Object, Rygel.StateMachine {
         this.completed ();
     }
 }
-
-internal class Rygel.Seek : GLib.Object {
-    public Format format { get; private set; }
-
-    private int64 _start;
-    public int64 start {
-        get {
-            return this._start;
-        }
-        set {
-            this._start = value;
-            this.length = stop - start + 1;
-        }
-    }
-
-    private int64 _stop;
-    public int64 stop {
-        get {
-            return this._stop;
-        }
-        set {
-            this._stop = value;
-            this.length = stop - start + 1;
-        }
-    }
-
-    public int64 length { get; private set; }
-
-    public Seek (Format format,
-                 int64  start,
-                 int64  stop) {
-        this.format = format;
-        this.start = start;
-        this.stop = stop;
-    }
-}
diff --git a/src/rygel/rygel-live-response.vala b/src/rygel/rygel-live-response.vala
index 0e5c0b5..c842159 100644
--- a/src/rygel/rygel-live-response.vala
+++ b/src/rygel/rygel-live-response.vala
@@ -39,10 +39,13 @@ internal class Rygel.LiveResponse : Rygel.HTTPResponse {
 
     private AsyncQueue<Buffer> buffers;
 
+    private Seek time_range;
+
     public LiveResponse (Soup.Server  server,
                          Soup.Message msg,
                          string       name,
-                         Element      src) throws Error {
+                         Element      src,
+                         Seek?        time_range) throws Error {
         base (server, msg, false);
 
         this.msg.response_headers.set_encoding (Soup.Encoding.EOF);
@@ -50,12 +53,28 @@ internal class Rygel.LiveResponse : Rygel.HTTPResponse {
         this.buffers = new AsyncQueue<Buffer> ();
 
         this.prepare_pipeline (name, src);
+        this.time_range = time_range;
     }
 
     public override void run (Cancellable? cancellable) {
         base.run (cancellable);
 
-        // Go to PAUSED first
+        // Only bother attempting to seek if the offset is greater than zero.
+        if (this.time_range != null && this.time_range.start > 0) {
+            this.pipeline.set_state (State.PAUSED);
+            this.pipeline.get_state (null, null, -1);
+            if (!this.pipeline.seek (
+                    1.0, Format.TIME, SeekFlags.FLUSH,
+                    Gst.SeekType.SET, this.time_range.start,
+                    this.time_range.stop > 0 ? Gst.SeekType.SET :
+                    Gst.SeekType.NONE, this.time_range.stop)) {
+                warning ("Failed to seek to offset %lld",
+                         this.time_range.start);
+                this.end(false,
+                         Soup.KnownStatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
+                return;
+            }
+        }
         this.pipeline.set_state (State.PLAYING);
     }
 
diff --git a/src/rygel/rygel-seek.vala b/src/rygel/rygel-seek.vala
new file mode 100644
index 0000000..7962489
--- /dev/null
+++ b/src/rygel/rygel-seek.vala
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2008 Nokia Corporation.
+ *
+ * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
+ *                               <zeeshan ali nokia 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 Gst;
+
+internal class Rygel.Seek : GLib.Object {
+    public Format format { get; private set; }
+
+    public int64 start { get; private set; }
+    public int64 stop { get; private set; }
+
+    public int64 length {
+        get {
+            return this.stop + 1 - this.start;
+        }
+    }
+
+    public Seek (Format format,
+                 int64  start,
+                 int64  stop) {
+        this.format = format;
+        this.start = start;
+        this.stop = stop;
+    }
+
+    public static Seek? from_byte_range(Soup.Message msg)
+            throws HTTPRequestError {
+        string range, pos;
+        string[] range_tokens;
+        int64 start = 0, stop = -1;
+
+        range = msg.request_headers.get ("Range");
+        if (range == null) {
+            return null;
+        }
+
+        // We have a Range header. Parse.
+        if (!range.has_prefix ("bytes=")) {
+            throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                      range);
+        }
+
+        range_tokens = range.offset (6).split ("-", 2);
+        if (range_tokens[0] == null || range_tokens[1] == null) {
+            throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                      range);
+        }
+
+        // Get first byte position
+        pos = range_tokens[0];
+        if (pos[0].isdigit ()) {
+            start = pos.to_int64 ();
+        } else if (pos  != "") {
+            throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                      range);
+        }
+
+        // Get last byte position if specified
+        pos = range_tokens[1];
+        if (pos[0].isdigit ()) {
+            stop = pos.to_int64 ();
+            if (stop < start) {
+                throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                          range);
+            }
+        } else if (pos  != "") {
+            throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                      range);
+        }
+
+        return new Seek (Format.BYTES, start, stop);
+    }
+
+    public static Seek? from_time_range (Soup.Message msg)
+            throws HTTPRequestError {
+        string range, time;
+        string[] range_tokens;
+        int64 start = 0, stop = -1;
+
+        range = msg.request_headers.get ("TimeSeekRange.dlna.org");
+        if (range == null) {
+            return null;
+        }
+
+        if (!range.has_prefix ("npt=")) {
+            throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                      range);
+        }
+
+        range_tokens = range.offset (4).split ("-", 2);
+        if (range_tokens[0] == null || range_tokens[1] == null) {
+            throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                      range);
+        }
+
+        // Get start time
+        time = range_tokens[0];
+        if (time[0].isdigit()) {
+            start = (int64)(time.to_double() * SECOND);
+        } else if (time != "") {
+            throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                      range);
+        }
+
+        // Get end time
+        time = range_tokens[1];
+        if (time[0].isdigit()) {
+            stop = (int64)(time.to_double() * SECOND);
+            if (stop < start) {
+                throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                          range);
+            }
+        } else if (time != "") {
+            throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'",
+                                                      range);
+        }
+
+        return new Seek (Format.TIME, start, stop);
+    }
+
+    public void add_response_header (Soup.Message msg, int64 length=-1) {
+        string value;
+
+        if (this.format == Format.TIME) {
+            // TimeSeekRange.dlna.org: npt=START_TIME-END_TIME
+            value = "npt=%g-".printf((double)this.start / SECOND);
+            if (this.stop > 0) {
+                value += "%g".printf((double)this.stop / SECOND);
+            }
+            msg.response_headers.append ("TimeSeekRange.dlna.org", value);
+        } else {
+            // Content-Range: bytes START_BYTE-STOP_BYTE/TOTAL_LENGTH
+            value = "bytes " + this.start.to_string() + "-";
+            if (this.stop >= 0) {
+                int64 end_point = this.stop;
+
+                if (length > 0) {
+                    end_point = int64.min(end_point, length - 1);
+                }
+                value += end_point.to_string();
+            }
+            if (length > 0) {
+                value += "/" + length.to_string();
+            }
+            msg.response_headers.append ("Content-Range", value);
+            msg.response_headers.append ("Accept-Ranges", "bytes");
+        }
+    }
+}
diff --git a/src/rygel/rygel-transcoder.vala b/src/rygel/rygel-transcoder.vala
index ea54231..e4f3585 100644
--- a/src/rygel/rygel-transcoder.vala
+++ b/src/rygel/rygel-transcoder.vala
@@ -78,7 +78,7 @@ internal abstract class Rygel.Transcoder : GLib.Object {
         res.dlna_profile = this.dlna_profile;
         res.dlna_conversion = DLNAConversion.TRANSCODED;
         res.dlna_flags = DLNAFlags.STREAMING_TRANSFER_MODE;
-        res.dlna_operation = DLNAOperation.NONE;
+        res.dlna_operation = DLNAOperation.TIMESEEK;
         res.size = -1;
 
         return res;



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