From a49f56767756fa629f7f754d26927c1e7d05d8a3 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Mon, 20 Jul 2009 19:11:07 +0800 Subject: [PATCH] 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(-) create mode 100644 src/rygel/rygel-seek.vala 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 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 (); 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) + * + * + * 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; -- 2.7.4