diff options
author | Jens Georg <mail@jensge.org> | 2015-02-12 22:32:34 +0100 |
---|---|---|
committer | Jens Georg <mail@jensge.org> | 2015-02-17 01:05:14 +0100 |
commit | 65bab73cd2cad36156e4b9f609031e47b5c76c44 (patch) | |
tree | 119c740f48c35b9dac11576a677e1a45079014f8 /src/librygel-server | |
parent | a58008ef4e7b8afdfe088273ac51d05782615567 (diff) | |
download | rygel-65bab73cd2cad36156e4b9f609031e47b5c76c44.tar.gz |
server,media-engines: Refactor seek handling
Code based on Cablelabs's CVP-2 implementation.
Diffstat (limited to 'src/librygel-server')
20 files changed, 911 insertions, 507 deletions
diff --git a/src/librygel-server/filelist.am b/src/librygel-server/filelist.am index 9d85d41d..ef4d3093 100644 --- a/src/librygel-server/filelist.am +++ b/src/librygel-server/filelist.am @@ -37,19 +37,21 @@ LIBRYGEL_SERVER_NONVAPI_SOURCE_FILES = \ rygel-content-directory.vala \ rygel-dbus-thumbnailer.vala \ rygel-engine-loader.vala \ + rygel-http-byte-seek-request.vala \ + rygel-http-byte-seek-response.vala \ rygel-free-desktop-interfaces.vala \ - rygel-http-byte-seek.vala \ rygel-http-get-handler.vala \ rygel-http-get.vala \ rygel-http-thumbnail-handler.vala \ rygel-http-subtitle-handler.vala \ - rygel-http-identity-handler.vala \ rygel-http-item-uri.vala \ rygel-http-post.vala \ rygel-http-request.vala \ rygel-http-response.vala \ + rygel-http-response-element.vala \ rygel-http-server.vala \ - rygel-http-time-seek.vala \ + rygel-http-time-seek-request.vala \ + rygel-http-time-seek-response.vala \ rygel-http-resource-handler.vala \ rygel-import-resource.vala \ rygel-object-creator.vala \ diff --git a/src/librygel-server/rygel-data-sink.vala b/src/librygel-server/rygel-data-sink.vala index 9ff7d51a..eb206ed7 100644 --- a/src/librygel-server/rygel-data-sink.vala +++ b/src/librygel-server/rygel-data-sink.vala @@ -1,7 +1,9 @@ /* * Copyright (C) 2012 Intel Corporation. + * Copyright (C) 2013 Cable Television Laboratories, Inc. * * Author: Jens Georg <jensg@openismus.com> + * Craig Pratt <craig@ecaspia.com> * * This file is part of Rygel. * @@ -40,7 +42,7 @@ internal class Rygel.DataSink : Object { public DataSink (DataSource source, Server server, Message message, - HTTPSeek? offsets) { + HTTPSeekRequest? offsets) { this.source = source; this.server = server; this.message = message; @@ -49,10 +51,12 @@ internal class Rygel.DataSink : Object { this.bytes_sent = 0; this.max_bytes = int64.MAX; if (offsets != null && - offsets is HTTPByteSeek) { - this.max_bytes = offsets.length; + offsets is HTTPByteSeekRequest && + ((offsets as HTTPByteSeekRequest).range_length != HTTPSeekRequest.UNSPECIFIED)) { + this.max_bytes = (offsets as HTTPByteSeekRequest).range_length; } - + debug ("Setting max_bytes to %s", (this.max_bytes == int64.MAX) + ? "MAX" : this.max_bytes.to_string()); this.source.data_available.connect (this.on_data_available); this.message.wrote_chunk.connect (this.on_wrote_chunk); } diff --git a/src/librygel-server/rygel-data-source.vala b/src/librygel-server/rygel-data-source.vala index 437f5509..d663ff85 100644 --- a/src/librygel-server/rygel-data-source.vala +++ b/src/librygel-server/rygel-data-source.vala @@ -1,7 +1,9 @@ /* * Copyright (C) 2012 Intel Corporation. + * Copyright (C) 2013 Cable Television Laboratories, Inc. * * Author: Jens Georg <jensg@openismus.com> + * Craig Pratt <craig@ecaspia.com> * * This file is part of Rygel. * @@ -22,7 +24,7 @@ public errordomain Rygel.DataSourceError { GENERAL, - SEEK_FAILED + SEEK_FAILED, } /** @@ -36,7 +38,8 @@ public errordomain Rygel.DataSourceError { * to Rygel which adds them to the response it sends to the original HTTP * request received from the client. * - * The data source is responsible for providing the streamable byte-stream + * The data source is responsible for providing response header information + * describing the content being produced and a streamable byte-stream * via its data_available signal. End-of-stream is signalled by the * done signal, while errors are signalled by the error signal. * @@ -58,14 +61,28 @@ public errordomain Rygel.DataSourceError { */ public interface Rygel.DataSource : GLib.Object { /** + * Preroll the data with the given seek + * + * @param seek optional seek/range specifier + * + * @return List of HTTPResponseElements appropriate for the content request and + * optional seek (e.g. Content-Range, TimeSeekRange.dlna.org, + * etc) or null/empty list if none are appropriate. Note: the list will + * be processed in-order by the caller. + * + * @throws Error if anything goes wrong while prerolling the stream. + * Throws DataSourceError.SEEK_FAILED if a seek method is not supported or the + * range is not fulfillable. + */ + public abstract Gee.List<HTTPResponseElement> ? preroll (HTTPSeekRequest? seek) + throws Error; + + /** * Start producing the data. * - * @param offsets optional limits of the stream for partial streaming - * @throws Error if anything goes wrong while starting the stream. Throws - * DataSourceError.SEEK_FAILED if a seek method is not supported or the - * range is not fulfillable. + * @throws Error if anything goes wrong while starting the stream. */ - public abstract void start (HTTPSeek? offsets) throws Error; + public abstract void start () throws Error; /** * Temporarily stop data generation. diff --git a/src/librygel-server/rygel-http-byte-seek-request.vala b/src/librygel-server/rygel-http-byte-seek-request.vala new file mode 100644 index 00000000..38969f99 --- /dev/null +++ b/src/librygel-server/rygel-http-byte-seek-request.vala @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2009 Nokia Corporation. + * Copyright (C) 2012 Intel Corporation. + * Copyright (C) 2013 Cable Television Laboratories, Inc. + * + * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org> + * <zeeshan.ali@nokia.com> + * Jens Georg <jensg@openismus.com> + * Craig Pratt <craig@ecaspia.com> + * + * This file is part of Rygel. + * + * Rygel is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Rygel is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using GUPnP; + +public class Rygel.HTTPByteSeekRequest : Rygel.HTTPSeekRequest { + /** + * The start of the range in bytes + */ + public int64 start_byte { get; set; } + + /** + * The end of the range in bytes (inclusive) + */ + public int64 end_byte { get; set; } + + /** + * The length of the range in bytes + */ + public int64 range_length { get; private set; } + + /** + * The length of the resource in bytes + */ + public int64 total_size { get; set; } + + + public HTTPByteSeekRequest (HTTPGet request) throws HTTPSeekRequestError, + HTTPRequestError { + base (); + unowned string range = request.msg.request_headers.get_one ("Range"); + if (range == null) { + throw new HTTPSeekRequestError.INVALID_RANGE ("Range header not present"); + } + + int64 start_byte, end_byte, total_size; + + // The size (entity body size) may not be known up-front (especially for live sources) + total_size = request.handler.get_resource_size (); + if (total_size < 0) { + total_size = UNSPECIFIED; + } + + // Note: DLNA restricts the syntax on the Range header (see DLNA 7.5.4.3.2.22.3) + // And we need to retain the concept of an "open range" ("bytes=DIGITS-") + // since the interpretation/legality varies based on the context + // (e.g. DLNA 7.5.4.3.2.19.2, 7.5.4.3.2.20.1, 7.5.4.3.2.20.3) + if (!range.has_prefix ("bytes=")) { + throw new HTTPSeekRequestError.INVALID_RANGE + ("Invalid Range value (missing 'bytes=' field): '%s'", range); + } + + var parsed_range = range.substring (6); + if (!parsed_range.contains ("-")) { + throw new HTTPSeekRequestError.INVALID_RANGE + ("Invalid Range request with no '-': '%s'", range); + } + + var range_tokens = parsed_range.split ("-", 2); + + if (!int64.try_parse (strip_leading_zeros(range_tokens[0]), out start_byte)) { + throw new HTTPSeekRequestError.INVALID_RANGE + ("Invalid Range start value: '%s'", range); + } + + if ((total_size != UNSPECIFIED) && (start_byte >= total_size)) { + throw new HTTPSeekRequestError.OUT_OF_RANGE + ("Range start value %lld is larger than content size %lld: '%s'", + start_byte, total_size, range); + } + + if (range_tokens[1] == null || (range_tokens[1].length == 0)) { + end_byte = UNSPECIFIED; + range_length = UNSPECIFIED; + } else { + if (!int64.try_parse (strip_leading_zeros(range_tokens[1]), out end_byte)) { + throw new HTTPSeekRequestError.INVALID_RANGE + ("Invalid Range end value: '%s'", range); + } + if (end_byte < start_byte) { + throw new HTTPSeekRequestError.INVALID_RANGE + ("Range end value %lld is smaller than range start value %lld: '%s'", + end_byte, start_byte, range); + } + if ((total_size != UNSPECIFIED) && (end_byte >= total_size)) { + end_byte = total_size - 1; + } + range_length = end_byte - start_byte + 1; // range is inclusive + } + this.start_byte = start_byte; + this.end_byte = end_byte; + this.total_size = total_size; + } + + public static bool supported (HTTPGet request) { + bool force_seek = false; + + try { + var hack = ClientHacks.create (request.msg); + force_seek = hack.force_seek (); + } catch (Error error) { } + + return force_seek || request.handler.supports_byte_seek (); + } + + public static bool requested (HTTPGet request) { + return (request.msg.request_headers.get_one ("Range") != null); + } + + // Leading "0"s cause try_parse() to assume the value is octal (see Vala bug 656691) + // So we strip them off before passing to int64.try_parse() + private static string strip_leading_zeros (string number_string) { + int i=0; + while ((number_string[i] == '0') && (i < number_string.length)) { + i++; + } + if (i == 0) { + return number_string; + } else { + return number_string[i:number_string.length]; + } + } + +} diff --git a/src/librygel-server/rygel-http-byte-seek-response.vala b/src/librygel-server/rygel-http-byte-seek-response.vala new file mode 100644 index 00000000..459b2d94 --- /dev/null +++ b/src/librygel-server/rygel-http-byte-seek-response.vala @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2009 Nokia Corporation. + * Copyright (C) 2012 Intel Corporation. + * Copyright (C) 2013 Cable Television Laboratories, Inc. + * + * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org> + * <zeeshan.ali@nokia.com> + * Jens Georg <jensg@openismus.com> + * Craig Pratt <craig@ecaspia.com> + * + * This file is part of Rygel. + * + * Rygel is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Rygel is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using GUPnP; + +public class Rygel.HTTPByteSeekResponse : Rygel.HTTPResponseElement { + /** + * The start of the range in bytes + */ + public int64 start_byte { get; set; } + + /** + * The end of the range in bytes (inclusive) + */ + public int64 end_byte { get; set; } + + /** + * The length of the range in bytes + */ + public int64 range_length { get; private set; } + + /** + * The length of the resource in bytes + */ + public int64 total_size { get; set; } + + public HTTPByteSeekResponse (int64 start_byte, int64 end_byte, int64 total_size) { + this.start_byte = start_byte; + this.end_byte = end_byte; + this.range_length = end_byte-start_byte+1; // +1, since range is inclusive + this.total_size = total_size; + } + + public HTTPByteSeekResponse.from_request (HTTPByteSeekRequest request) { + this.start_byte = request.start_byte; + this.end_byte = request.end_byte; + this.range_length = request.range_length; + this.total_size = request.total_size; + } + + public override void add_response_headers (Rygel.HTTPRequest request) { + // Content-Range: bytes START_BYTE-END_BYTE/TOTAL_LENGTH (or "*") + request.msg.response_headers.set_content_range ( this.start_byte, this.end_byte, + this.total_size ); + request.msg.response_headers.append ("Accept-Ranges", "bytes"); + request.msg.response_headers.set_content_length (range_length); + } + + public override string to_string () { + return ("HTTPByteSeekResponse(bytes=%lld-%lld/%lld (%lld bytes))" + .printf (this.start_byte, this.end_byte, this.total_size, this.total_size)); + } +} diff --git a/src/librygel-server/rygel-http-byte-seek.vala b/src/librygel-server/rygel-http-byte-seek.vala deleted file mode 100644 index df1f2cc2..00000000 --- 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 MediaFileItem).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 MediaFileItem).size > 0 && - request.handler is HTTPIdentityHandler) || - (request.handler is HTTPThumbnailHandler) || - (request.handler is HTTPSubtitleHandler)); - } - - 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 37e1aa0c..20dee38c 100644 --- a/src/librygel-server/rygel-http-get-handler.vala +++ b/src/librygel-server/rygel-http-get-handler.vala @@ -1,9 +1,11 @@ /* * Copyright (C) 2008-2010 Nokia Corporation. * Copyright (C) 2010 Andreas Henriksson <andreas@fatal.se> + * Copyright (C) 2013 Cable Television Laboratories, Inc. * * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org> * <zeeshan.ali@nokia.com> + * Craig Pratt <craig@ecaspia.com> * * This file is part of Rygel. * @@ -27,7 +29,7 @@ using GUPnP; /** * HTTP GET request handler interface. */ -internal abstract class Rygel.HTTPGetHandler: GLib.Object { +public abstract class Rygel.HTTPGetHandler: GLib.Object { protected const string TRANSFER_MODE_HEADER = "transferMode.dlna.org"; protected const string TRANSFER_MODE_STREAMING = "Streaming"; @@ -36,7 +38,6 @@ internal abstract class Rygel.HTTPGetHandler: GLib.Object { public Cancellable cancellable { get; set; } - // Add response headers. /** * Invokes the handler to add response headers to/for the given HTTP request */ @@ -78,7 +79,29 @@ internal abstract class Rygel.HTTPGetHandler: GLib.Object { */ public abstract int64 get_resource_size (); - // Create an HTTPResponse object that will render the body. + /** + * Returns the resource duration (in microseconds) or -1 if not known. + */ + public virtual int64 get_resource_duration () { + return -1; + } + + /** + * Returns true if the handler supports full random-access byte seek. + */ + public virtual bool supports_byte_seek () { + return false; + } + + /** + * Returns true if the handler supports full random-access time seek. + */ + public virtual bool supports_time_seek () { + return false; + } + /** + * Create an HTTPResponse object that will render the body. + */ public abstract HTTPResponse render_body (HTTPGet request) throws HTTPRequestError; diff --git a/src/librygel-server/rygel-http-get.vala b/src/librygel-server/rygel-http-get.vala index dbde59cc..79adfbb8 100644 --- a/src/librygel-server/rygel-http-get.vala +++ b/src/librygel-server/rygel-http-get.vala @@ -31,12 +31,12 @@ /** * Responsible for handling HTTP GET & HEAD client requests. */ -internal class Rygel.HTTPGet : HTTPRequest { +public class Rygel.HTTPGet : HTTPRequest { private const string TRANSFER_MODE_HEADER = "transferMode.dlna.org"; + public HTTPSeekRequest seek; public Thumbnail thumbnail; public Subtitle subtitle; - public HTTPSeek seek; private int thumbnail_index; private int subtitle_index; @@ -82,10 +82,6 @@ internal class Rygel.HTTPGet : HTTPRequest { this.cancellable); } - if (this.handler == null) { - this.handler = new HTTPIdentityHandler (this.cancellable); - } - { // Check the transfer mode var transfer_mode = this.msg.request_headers.get_one (TRANSFER_MODE_HEADER); @@ -157,39 +153,67 @@ internal class Rygel.HTTPGet : HTTPRequest { } private async void handle_item_request () throws Error { - var need_time_seek = HTTPTimeSeek.needed (this); - var requested_time_seek = HTTPTimeSeek.requested (this); - var need_byte_seek = HTTPByteSeek.needed (this); - var requested_byte_seek = HTTPByteSeek.requested (this); - - if ((requested_time_seek && !need_time_seek) || - (requested_byte_seek && !need_byte_seek)) { - throw new HTTPRequestError.UNACCEPTABLE ("Invalid seek request"); + var supports_time_seek = HTTPTimeSeekRequest.supported (this); + var requested_time_seek = HTTPTimeSeekRequest.requested (this); + var supports_byte_seek = HTTPByteSeekRequest.supported (this); + var requested_byte_seek = HTTPByteSeekRequest.requested (this); + + if (requested_byte_seek) { + if (!supports_byte_seek) { + throw new HTTPRequestError.UNACCEPTABLE ( "Byte seek not supported for " + + this.uri.to_string () ); + } + } else if (requested_time_seek) { + if (!supports_time_seek) { + throw new HTTPRequestError.UNACCEPTABLE ( "Time seek not supported for " + + this.uri.to_string () ); + } } try { - if (need_time_seek && requested_time_seek) { - this.seek = new HTTPTimeSeek (this); - } else if (need_byte_seek && requested_byte_seek) { - this.seek = new HTTPByteSeek (this); - } - } catch (HTTPSeekError error) { - this.server.unpause_message (this.msg); - - if (error is HTTPSeekError.INVALID_RANGE) { - this.end (Soup.Status.BAD_REQUEST); - } else if (error is HTTPSeekError.OUT_OF_RANGE) { - this.end (Soup.Status.REQUESTED_RANGE_NOT_SATISFIABLE); + // Order is intentional here + if (supports_byte_seek && requested_byte_seek) { + var byte_seek = new HTTPByteSeekRequest (this); + debug ("Processing byte range request (bytes %lld to %lld)", + byte_seek.start_byte, byte_seek.end_byte); + this.seek = byte_seek; + } else if (supports_time_seek && requested_time_seek) { + // Assert: speed_request has been checked/processed + var time_seek = new HTTPTimeSeekRequest (this); + debug ("Processing " + time_seek.to_string ()); + this.seek = time_seek; } else { - throw error; + this.seek = null; } - + } catch (HTTPSeekRequestError error) { + warning ("Caught HTTPSeekRequestError: " + error.message); + this.server.unpause_message (this.msg); + this.end (error.code, error.message); // All seek error codes are Soup.Status codes return; - } + } // Add headers this.handler.add_response_headers (this); + var response = this.handler.render_body (this); + + // Have the response process the seek/speed request + try { + var responses = response.preroll (); + + // Incorporate the prerolled responses + if (responses != null) { + foreach (var response_elem in responses) { + response_elem.add_response_headers (this); + } + } + } catch (HTTPSeekRequestError error) { + warning ("Caught HTTPSeekRequestError on preroll: " + error.message); + this.server.unpause_message (this.msg); + this.end (error.code, error.message); // All seek error codes are Soup.Status codes + return; + } + // Determine the size value int64 response_size; { @@ -213,13 +237,6 @@ internal class Rygel.HTTPGet : HTTPRequest { // size will factor into other logic below... } - // Add general headers - if (this.msg.request_headers.get_one ("Range") != null) { - this.msg.set_status (Soup.Status.PARTIAL_CONTENT); - } else { - this.msg.set_status (Soup.Status.OK); - } - // Determine the transfer mode encoding { Soup.Encoding response_body_encoding; @@ -241,6 +258,36 @@ internal class Rygel.HTTPGet : HTTPRequest { this.msg.response_headers.set_encoding (response_body_encoding); } + // Determine the Vary header (if not HTTP 1.0) + { + // Per DLNA 7.5.4.3.2.35.4, the Vary header needs to include the timeseek + // header if it is supported for the resource/uri + if (supports_time_seek) { + if (this.msg.get_http_version () != Soup.HTTPVersion.@1_0) { + var vary_header = new StringBuilder + (this.msg.response_headers.get_list ("Vary")); + if (supports_time_seek) { + if (vary_header.len > 0) { + vary_header.append (","); + } + vary_header.append (HTTPTimeSeekRequest.TIMESEEKRANGE_HEADER); + } + this.msg.response_headers.replace ("Vary", vary_header.str); + } + } + } + + // Determine the status code + { + int response_code; + if (this.msg.response_headers.get_one ("Content-Range") != null) { + response_code = Soup.Status.PARTIAL_CONTENT; + } else { + response_code = Soup.Status.OK; + } + this.msg.set_status (response_code); + } + if (msg.get_http_version () == Soup.HTTPVersion.@1_0) { // Set the response version to HTTP 1.1 (see DLNA 7.5.4.3.2.7.2) msg.set_http_version (Soup.HTTPVersion.@1_1); @@ -259,8 +306,6 @@ internal class Rygel.HTTPGet : HTTPRequest { return; } - var response = this.handler.render_body (this); - yield response.run (); this.end (Soup.Status.NONE); diff --git a/src/librygel-server/rygel-http-identity-handler.vala b/src/librygel-server/rygel-http-identity-handler.vala deleted file mode 100644 index 19e8a218..00000000 --- a/src/librygel-server/rygel-http-identity-handler.vala +++ /dev/null @@ -1,87 +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 { - { - request.msg.response_headers.append ("Content-Type", - (request.object as MediaFileItem).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 supports_transfer_mode (string mode) { - return true; - } - - public override int64 get_resource_size () { - return -1; - } - - private HTTPResponse render_body_real (HTTPGet request) throws Error { - var src = (request.object as MediaFileItem).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) { - return (request.object as MediaFileItem).size; - } -} diff --git a/src/librygel-server/rygel-http-request.vala b/src/librygel-server/rygel-http-request.vala index 389d3c09..243705c1 100644 --- a/src/librygel-server/rygel-http-request.vala +++ b/src/librygel-server/rygel-http-request.vala @@ -110,12 +110,16 @@ public abstract class Rygel.HTTPRequest : GLib.Object, Rygel.StateMachine { status = Soup.Status.NOT_FOUND; } - this.end (status); + this.end (status, error.message); } - protected void end (uint status) { + protected void end (uint status, string ? reason = null) { if (status != Soup.Status.NONE) { - this.msg.set_status (status); + if (reason == null) { + this.msg.set_status (status); + } else { + this.msg.set_status_full (status, reason); + } } this.completed (); diff --git a/src/librygel-server/rygel-http-resource-handler.vala b/src/librygel-server/rygel-http-resource-handler.vala index 965ab648..51a49a47 100644 --- a/src/librygel-server/rygel-http-resource-handler.vala +++ b/src/librygel-server/rygel-http-resource-handler.vala @@ -93,4 +93,18 @@ internal class Rygel.HTTPMediaResourceHandler : HTTPGetHandler { public override int64 get_resource_size () { return media_resource.size; } + + public override int64 get_resource_duration () { + return media_resource.duration * TimeSpan.SECOND; + } + + public override bool supports_byte_seek () { + return media_resource.supports_arbitrary_byte_seek () + || media_resource.supports_limited_byte_seek (); + } + + public override bool supports_time_seek () { + return media_resource.supports_arbitrary_time_seek () + || media_resource.supports_limited_time_seek (); + } } diff --git a/src/librygel-server/rygel-http-response-element.vala b/src/librygel-server/rygel-http-response-element.vala new file mode 100644 index 00000000..0ac57cf8 --- /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 7cfa7bf9..38456893 100644 --- a/src/librygel-server/rygel-http-response.vala +++ b/src/librygel-server/rygel-http-response.vala @@ -1,10 +1,12 @@ /* * Copyright (C) 2008-2012 Nokia Corporation. * Copyright (C) 2012 Intel Corporation. + * Copyright (C) 2013 Cable Television Laboratories, Inc. * * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org> * <zeeshan.ali@nokia.com> * Jens Georg <jensg@openismus.com> + * Craig Pratt <craig@ecaspia.com> * * This file is part of Rygel. * @@ -25,13 +27,13 @@ using Soup; -internal class Rygel.HTTPResponse : GLib.Object, Rygel.StateMachine { +public class Rygel.HTTPResponse : GLib.Object, Rygel.StateMachine { public unowned Soup.Server server { get; private set; } public Soup.Message msg; public Cancellable cancellable { get; set; } - public HTTPSeek seek; + public HTTPSeekRequest seek; private SourceFunc run_continue; private int _priority = -1; @@ -99,10 +101,14 @@ internal class Rygel.HTTPResponse : GLib.Object, Rygel.StateMachine { } } + public Gee.List<HTTPResponseElement> ? preroll () throws Error { + return this.src.preroll (this.seek); + } + public async void run () { this.run_continue = run.callback; try { - this.src.start (this.seek); + this.src.start (); } catch (Error error) { Idle.add (() => { this.end (false, Status.NONE); diff --git a/src/librygel-server/rygel-http-seek.vala b/src/librygel-server/rygel-http-seek.vala index a7c66ea0..95860f39 100644 --- a/src/librygel-server/rygel-http-seek.vala +++ b/src/librygel-server/rygel-http-seek.vala @@ -1,10 +1,12 @@ /* * Copyright (C) 2008-2009 Nokia Corporation. * Copyright (C) 2012 Intel Corporation. + * Copyright (C) 2013 Cable Television Laboratories, Inc. * * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org> * <zeeshan.ali@nokia.com> * Jens Georg <jensg@openismus.com> + * Craig Pratt <craig@ecaspia.com> * * This file is part of Rygel. * @@ -23,89 +25,22 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -public errordomain Rygel.HTTPSeekError { +/** + * Various errors that can be thrown when attempting to seek into a stream. + * + * Note: All codes must be set to Soup.Status codes + */ +public errordomain Rygel.HTTPSeekRequestError { INVALID_RANGE = Soup.Status.BAD_REQUEST, + BAD_REQUEST = Soup.Status.BAD_REQUEST, OUT_OF_RANGE = Soup.Status.REQUESTED_RANGE_NOT_SATISFIABLE, } -public enum Rygel.HTTPSeekType { - BYTE, - TIME -} - /** - * HTTPSeek is an abstract representation of a ranged HTTP request. - * - * It can be one of: - * - * - The classic Range request (seek_type == HTTPSeekType.BYTE), with start and stop in bytes. - * - The DLNA-Specific "TimeSeekRange.dlna.org" request (seek_type == HTTPSeekType.TIME) with start and stop in microseconds. + * HTTPSeekRequest is an abstract base for a variety of seek request types. */ -public abstract class Rygel.HTTPSeek : GLib.Object { - - /** - * Identifies whether this is a class Range request or a DLNA-specific - * "TimeSeekRange.dlna.org" request. - */ - public HTTPSeekType seek_type { get; protected set; } - public Soup.Message msg { get; private set; } - - /** - * The start of the range as a number of bytes (classic) or as microseconds - * (DLNA-specific). See seek_type. - */ - public int64 start { get; private set; } - - /** - * The end of the range as a number of bytes (classic) or as microseconds - * (DLNA-specific). See seek_type. - */ - public int64 stop { get; private set; } - - /** - * Either 1 byte (classic) or as 1000 G_TIME_SPAN_MILLISECOND microseconds - * (DLNA-specific). See seek_type. - */ - public int64 step { get; private set; } - - /** - * The length of the range as a number of bytes (classic) or as microseconds - * (DLNA-specific). See seek_type. - */ - public int64 length { get; private set; } - - /** - * The length of the media file as a number of bytes (classic) or as microseconds - * (DLNA-specific). See seek_type. - */ - public int64 total_length { get; private set; } - - public HTTPSeek (Soup.Message msg, - int64 start, - int64 stop, - int64 step, - int64 total_length) throws HTTPSeekError { - this.msg = msg; - this.start = start; - this.stop = stop; - this.length = length; - this.total_length = total_length; - - if (start < 0 || start >= total_length) { - throw new HTTPSeekError.OUT_OF_RANGE (_("Out Of Range Start '%ld'"), - start); - } - if (stop < 0 || stop >= total_length) { - throw new HTTPSeekError.OUT_OF_RANGE (_("Out Of Range Stop '%ld'"), - stop); - } - - if (length > 0) { - this.stop = stop.clamp (start + 1, length - 1); - } - - this.length = stop + step - start; - } - - public abstract void add_response_headers (); +public abstract class Rygel.HTTPSeekRequest : GLib.Object { + // For designating fields that are unset + public static const int64 UNSPECIFIED = -1; + // Note: -1 is significant in that libsoup also uses it to designate an "unknown" value } diff --git a/src/librygel-server/rygel-http-subtitle-handler.vala b/src/librygel-server/rygel-http-subtitle-handler.vala index d011587b..cb27d73a 100644 --- a/src/librygel-server/rygel-http-subtitle-handler.vala +++ b/src/librygel-server/rygel-http-subtitle-handler.vala @@ -92,4 +92,8 @@ internal class Rygel.HTTPSubtitleHandler : Rygel.HTTPGetHandler { public override int64 get_resource_size () { return subtitle.size; } + + public override bool supports_byte_seek () { + return true; + } } diff --git a/src/librygel-server/rygel-http-thumbnail-handler.vala b/src/librygel-server/rygel-http-thumbnail-handler.vala index 9a4975f6..cc23e715 100644 --- a/src/librygel-server/rygel-http-thumbnail-handler.vala +++ b/src/librygel-server/rygel-http-thumbnail-handler.vala @@ -93,4 +93,8 @@ internal class Rygel.HTTPThumbnailHandler : Rygel.HTTPGetHandler { public override int64 get_resource_size () { return thumbnail.size; } + + public override bool supports_byte_seek () { + return true; + } } diff --git a/src/librygel-server/rygel-http-time-seek-request.vala b/src/librygel-server/rygel-http-time-seek-request.vala new file mode 100644 index 00000000..70fceb02 --- /dev/null +++ b/src/librygel-server/rygel-http-time-seek-request.vala @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2009 Nokia Corporation. + * Copyright (C) 2012 Intel Corporation. + * Copyright (C) 2013 Cable Television Laboratories, Inc. + * + * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org> + * <zeeshan.ali@nokia.com> + * Jens Georg <jensg@openismus.com> + * Craig Pratt <craig@ecaspia.com> + * + * This file is part of Rygel. + * + * Rygel is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Rygel is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * This class represents a DLNA TimeSeekRange request. + * + * A TimeSeekRange request can only have a time range ("npt=start-end"). + */ +public class Rygel.HTTPTimeSeekRequest : Rygel.HTTPSeekRequest { + public static const string TIMESEEKRANGE_HEADER = "TimeSeekRange.dlna.org"; + /** + * Requested range start time, in microseconds + */ + public int64 start_time; + + /** + * Requested range end time, in microseconds + */ + public int64 end_time; + + /** + * Requested range duration, in microseconds + */ + public int64 range_duration; + + /** + * The total duration of the resource, in microseconds + */ + public int64 total_duration; + + /** + * Create a HTTPTimeSeekRequest corresponding with a HTTPGet that contains a + * TimeSeekRange.dlna.org header value. + * + * Note: This constructor will check the syntax of the request (per DLNA 7.5.4.3.2.24.3) + * as well as perform some range validation. If the provided request is associated + * with a handler that can provide content duration, the start and end time will + * be checked for out-of-bounds conditions. + * @param request The HTTP GET/HEAD request + */ + internal HTTPTimeSeekRequest (HTTPGet request) + throws HTTPSeekRequestError { + base (); + + this.total_duration = request.handler.get_resource_duration (); + if (this.total_duration <= 0) { + this.total_duration = UNSPECIFIED; + } + + var range = request.msg.request_headers.get_one (TIMESEEKRANGE_HEADER); + + if (range == null) { + throw new HTTPSeekRequestError.INVALID_RANGE ("%s not present", + TIMESEEKRANGE_HEADER); + } + + if (!range.has_prefix ("npt=")) { + throw new HTTPSeekRequestError.INVALID_RANGE + ("Invalid %s value (missing npt field): '%s'", + TIMESEEKRANGE_HEADER, range); + } + + var parsed_range = range.substring (4); + if (!parsed_range.contains ("-")) { + throw new HTTPSeekRequestError.INVALID_RANGE + ("Invalid %s request with no '-': '%s'", + TIMESEEKRANGE_HEADER, range); + } + + var range_tokens = parsed_range.split ("-", 2); + + int64 start = UNSPECIFIED; + if (!parse_npt_time (range_tokens[0], ref start)) { + throw new HTTPSeekRequestError.INVALID_RANGE + ("Invalid %s value (no start): '%s'", + TIMESEEKRANGE_HEADER, range); + } + + this.start_time = start; + + // Look for an end time + int64 end = UNSPECIFIED; + if (parse_npt_time (range_tokens[1], ref end)) { + // The end time was specified in the npt ("start-end") + // Check for valid range + { + this.end_time = end; + + this.range_duration = this.end_time - this.start_time; + // At positive rate, start < end + if (this.range_duration <= 0) { // See DLNA 7.5.4.3.2.24.12 + throw new HTTPSeekRequestError.INVALID_RANGE + ("Invalid %s value (start time after end time - forward scan): '%s'", + TIMESEEKRANGE_HEADER, range); + } + } + } else { // End time not specified in the npt field ("start-") + // See DLNA 7.5.4.3.2.24.4 + this.end_time = UNSPECIFIED; // Will indicate "end/beginning of binary" + if (this.total_duration == UNSPECIFIED) { + this.range_duration = UNSPECIFIED; + } else { + this.range_duration = this.total_duration - this.start_time; + } + } + } + + public string to_string () { + return ("HTTPTimeSeekRequest (npt=%lld-%s)".printf (this.start_time, + (this.end_time != UNSPECIFIED + ? this.end_time.to_string() + : "*") ) ); + } + + /** + * Return true if time-seek is supported. + * + * This method utilizes elements associated with the request to determine if a + * TimeSeekRange request is supported for the given request/resource. + */ + public static bool supported (HTTPGet request) { + bool force_seek = false; + + try { + var hack = ClientHacks.create (request.msg); + force_seek = hack.force_seek (); + } catch (Error error) { } + + return force_seek || request.handler.supports_time_seek (); + } + + /** + * Return true of the HTTPGet contains a TimeSeekRange request. + */ + public static bool requested (HTTPGet request) { + return (request.msg.request_headers.get_one (TIMESEEKRANGE_HEADER) != null); + } + + // Parses npt times in the format of '417.33' and returns the time in microseconds + private static bool parse_npt_seconds (string range_token, + ref int64 value) { + if (range_token[0].isdigit ()) { + value = (int64) (double.parse (range_token) * TimeSpan.SECOND); + } else { + return false; + } + return true; + } + + // Parses npt times in the format of '10:19:25.7' and returns the time in microseconds + private static bool parse_npt_time (string? range_token, + ref int64 value) { + if (range_token == null) { + return false; + } + + if (range_token.index_of (":") == -1) { + return parse_npt_seconds (range_token, ref value); + } + // parse_seconds has a ':' in it... + int64 seconds_sum = 0; + int time_factor = 0; + string[] time_tokens; + + seconds_sum = 0; + time_factor = 3600; + + time_tokens = range_token.split (":", 3); + if (time_tokens[0] == null || + time_tokens[1] == null || + time_tokens[2] == null) { + return false; + } + + foreach (string time in time_tokens) { + if (time[0].isdigit ()) { + seconds_sum += (int64) ((double.parse (time) * TimeSpan.SECOND) * time_factor); + } else { + return false; + } + time_factor /= 60; + } + value = seconds_sum; + + return true; + } +} diff --git a/src/librygel-server/rygel-http-time-seek-response.vala b/src/librygel-server/rygel-http-time-seek-response.vala new file mode 100644 index 00000000..3e26e67f --- /dev/null +++ b/src/librygel-server/rygel-http-time-seek-response.vala @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2013 Cable Television Laboratories, Inc. + * Contact: http://www.cablelabs.com/ + * + * Author: Craig Pratt <craig@ecaspia.com> + * + * This file is part of Rygel. + * + * Rygel is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CABLE TELEVISION LABORATORIES + * INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +public class Rygel.HTTPTimeSeekResponse : Rygel.HTTPResponseElement { + /** + * Effective range start time, in microseconds + */ + public int64 start_time { get; private set; } + + /** + * Effective range end time, in microseconds + */ + public int64 end_time { get; private set; } + + /** + * Effective range duration, in microseconds + */ + public int64 range_duration { get; private set; } + + /** + * The total duration of the resource, in microseconds + */ + public int64 total_duration { get; private set; } + + /** + * The start of the range in bytes + */ + public int64 start_byte { get; private set; } + + /** + * The end of the range in bytes (inclusive) + */ + public int64 end_byte { get; private set; } + + /** + * The response length in bytes + */ + public int64 response_length { get; private set; } + + /** + * The length of the resource in bytes + */ + public int64 total_size { get; private set; } + + /** + * Construct a HTTPTimeSeekResponse with time and byte range + * + * start_time and start_byte must be specified. + * + * if total_duration and total_size are UNSPECIFIED, then the content duration/size + * will be signaled as unknown ("*") + * + * if end_time is UNSPECIFIED, then the time range end will be omitted from the + * response. If the end_byte is UNSPECIFIED, the entire byte range response will be + * omitted. (see DLNA 7.5.4.3.2.24.3) + */ + public HTTPTimeSeekResponse (int64 start_time, int64 end_time, int64 total_duration, + int64 start_byte, int64 end_byte, int64 total_size) { + base (); + this.start_time = start_time; + this.end_time = end_time; + this.total_duration = total_duration; + + this.start_byte = start_byte; + this.end_byte = end_byte; + this.response_length = (end_byte == UNSPECIFIED) ? UNSPECIFIED + : (end_byte - start_byte + 1); + this.total_size = total_size; + } + + /** + * Create a HTTPTimeSeekResponse only containing a time range + * + * Note: This form is only valid when byte-seek is not supported, according to the + * associated resource's ProtocolInfo (see DLNA 7.5.4.3.2.24.5) + */ + public HTTPTimeSeekResponse.time_only (int64 start_time, int64 end_time, int64 total_duration) { + base (); + this.start_time = start_time; + this.end_time = end_time; + this.total_duration = total_duration; + + this.start_byte = UNSPECIFIED; + this.end_byte = UNSPECIFIED; + this.response_length = UNSPECIFIED; + this.total_size = UNSPECIFIED; + } + + /** + * Construct a HTTPTimeSeekResponse with time and byte range and allowing for a + * response length override. This is useful when the response body is larger than the + * specified byte range from the original content binary. + * + * start_time and start_byte must be specified. + * + * If total_duration and total_size are UNSPECIFIED, then the content duration/size + * will be signaled as unknown ("*") + * + * if end_time is UNSPECIFIED, then the time range end will be omitted from the + * response. If the end_byte is UNSPECIFIED, the entire byte range response will be + * omitted. (see DLNA 7.5.4.3.2.24.3) + */ + public HTTPTimeSeekResponse.with_length (int64 start_time, int64 end_time, + int64 total_duration, + int64 start_byte, int64 end_byte, + int64 total_size, + int64 response_length) { + base (); + this.start_time = start_time; + this.end_time = end_time; + this.total_duration = total_duration; + + this.start_byte = start_byte; + this.end_byte = end_byte; + this.response_length = response_length; + this.total_size = total_size; + } + + /** + * Create a HTTPTimeSeekResponse from a HTTPTimeSeekRequest + * + * Note: This form is only valid when byte-seek is not supported, according to the + * associated resource's ProtocolInfo (see DLNA 7.5.4.3.2.24.5) + */ + public HTTPTimeSeekResponse.from_request ( HTTPTimeSeekRequest time_seek_request, + int64 total_duration ) { + HTTPTimeSeekResponse.time_only ( time_seek_request.start_time, + time_seek_request.end_time, + total_duration ); + } + + public override void add_response_headers (Rygel.HTTPRequest request) { + var response = get_response_string (); + if (response != null) { + request.msg.response_headers.append (HTTPTimeSeekRequest.TIMESEEKRANGE_HEADER, + response); + if (this.response_length != UNSPECIFIED) { + // Note: Don't use set_content_range () here - we don't want a "Content-range" header + request.msg.response_headers.set_content_length (this.response_length); + } + if (request.msg.get_http_version () == Soup.HTTPVersion.@1_0) { + request.msg.response_headers.replace ("Pragma","no-cache"); + } + } + } + + private string? get_response_string () { + if (start_time == UNSPECIFIED) { + return null; + } + + // The response form of TimeSeekRange: + // + // TimeSeekRange.dlna.org: npt=START_TIME-END_TIME/DURATION bytes=START_BYTE-END_BYTE/LENGTH + // + // The "bytes=" field can be ommitted in some cases. (e.g. ORG_OP a-val==1, b-val==0) + // The DURATION can be "*" in some cases (e.g. for limited-operation mode) + // The LENGTH can be "*" in some cases (e.g. for limited-operation mode) + // And the entire response header can be ommitted for HEAD requests (see DLNA 7.5.4.3.2.24.2) + + // It's not our job at this level to enforce all the semantics of the TimeSeekRange + // response, as we don't have enough context. Setting up the correct HTTPTimeSeekRequest + // object is the responsibility of the object owner. To form the response, we just + // use what is set. + + var response = new StringBuilder (); + response.append ("npt="); + response.append_printf ("%.3f-", (double) this.start_time / TimeSpan.SECOND); + if (this.end_time != UNSPECIFIED) { + response.append_printf ("%.3f", (double) this.end_time / TimeSpan.SECOND); + } + if (this.total_duration != UNSPECIFIED) { + response.append_printf ("/%.3f", (double) this.total_duration / TimeSpan.SECOND); + } else { + response.append ("/*"); + } + + if ((this.start_byte != UNSPECIFIED) && (this.end_byte != UNSPECIFIED)) { + response.append (" bytes="); + response.append (this.start_byte.to_string ()); + response.append ("-"); + response.append (this.end_byte.to_string ()); + response.append ("/"); + if (this.total_size != UNSPECIFIED) { + response.append (this.total_size.to_string ()); + } else { + response.append ("*"); + } + } + + return response.str; + } + + public override string to_string () { + return ("HTTPTimeSeekResponse (%s)".printf (get_response_string ())); + } +} diff --git a/src/librygel-server/rygel-http-time-seek.vala b/src/librygel-server/rygel-http-time-seek.vala deleted file mode 100644 index 397d8315..00000000 --- a/src/librygel-server/rygel-http-time-seek.vala +++ /dev/null @@ -1,179 +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; - -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; - } - - private static bool is_transcoder (HTTPGetHandler handler) { - return (handler is HTTPMediaResourceHandler) && - ((handler as HTTPMediaResourceHandler).media_resource.dlna_conversion == - DLNAConversion.TRANSCODED); - } - - 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 && - (is_transcoder (request.handler) || - (!(request.handler is HTTPThumbnailHandler) && - !(request.handler is HTTPSubtitleHandler) && - (request.object as MediaFileItem).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-media-container.vala b/src/librygel-server/rygel-media-container.vala index e739ad39..46ff5fd3 100644 --- a/src/librygel-server/rygel-media-container.vala +++ b/src/librygel-server/rygel-media-container.vala @@ -58,16 +58,22 @@ internal class Rygel.PlaylistDatasource : Rygel.DataSource, Object { public signal void data_ready (); - public void start (HTTPSeek? offsets) throws Error { - if (offsets != null) { + public Gee.List<HTTPResponseElement> ? preroll ( HTTPSeekRequest? seek_request) + throws Error { + if (seek_request != null) { throw new DataSourceError.SEEK_FAILED (_("Seeking not supported")); } + return null; + } + + + public void start () throws Error { if (this.data == null) { this.data_ready.connect ( () => { try { - this.start (offsets); + this.start (); } catch (Error error) { } }); |