* 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.
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 \
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 \
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 \
private string item_id;
private Transcoder transcoder;
private MediaItem item;
- private Seek seek;
+ private Seek byte_range;
+ private Seek time_range;
private Cancellable cancellable;
var response = new LiveResponse (this.server,
this.msg,
"RygelLiveResponse",
- src);
+ src,
+ this.time_range);
this.response = response;
response.completed += on_response_completed;
var response = new SeekableResponse (this.server,
this.msg,
uri,
- this.seek,
+ this.byte_range,
size);
this.response = response;
response.completed += on_response_completed;
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;
if (this.transcoder != null) {
this.msg.response_headers.append ("Content-Type",
this.transcoder.mime_type);
+ this.time_range.add_response_header(this.msg);
return;
}
}
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);
}
}
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;
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;
- }
-}
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);
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);
}
--- /dev/null
+/*
+ * 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");
+ }
+ }
+}
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;