rygel r415 - in trunk/src: plugins/test rygel



Author: zeeshanak
Date: Wed Jan  7 12:56:08 2009
New Revision: 415
URL: http://svn.gnome.org/viewvc/rygel?rev=415&view=rev

Log:
Implement partial download (Range header).

Seems to work pretty well against PS3 but I did observe stream hanging
once.

Modified:
   trunk/src/plugins/test/rygel-test-item.vala
   trunk/src/rygel/rygel-gst-stream.vala
   trunk/src/rygel/rygel-streamer.vala

Modified: trunk/src/plugins/test/rygel-test-item.vala
==============================================================================
--- trunk/src/plugins/test/rygel-test-item.vala	(original)
+++ trunk/src/plugins/test/rygel-test-item.vala	Wed Jan  7 12:56:08 2009
@@ -72,7 +72,7 @@
             Element src = this.create_gst_source ();
             // Ask streamer to handle the stream for us but use our source in
             // the pipeline.
-            streamer.stream_from_gst_source (src, stream);
+            streamer.stream_from_gst_source (src, stream, null);
         } catch (Error error) {
             critical ("Error in attempting to start streaming %s: %s",
                       path,

Modified: trunk/src/rygel/rygel-gst-stream.vala
==============================================================================
--- trunk/src/rygel/rygel-gst-stream.vala	(original)
+++ trunk/src/rygel/rygel-gst-stream.vala	Wed Jan  7 12:56:08 2009
@@ -41,14 +41,18 @@
 
     private AsyncQueue<Buffer> buffers;
 
+    private Event seek_event;
+
     public GstStream (Stream  stream,
                       string  name,
-                      Element src) throws Error {
+                      Element src,
+                      Event?  seek_event) throws Error {
         this.stream = stream;
         this.name = name;
+        this.seek_event = seek_event;
         this.buffers = new AsyncQueue<Buffer> ();
 
-        this.stream.accept ();
+        this.stream.accept (seek != null);
         this.prepare_pipeline (src);
     }
 
@@ -197,6 +201,16 @@
 
         if (message.type == MessageType.EOS) {
             ret = false;
+        } else if (message.type == MessageType.STATE_CHANGED &&
+                   this.seek_event != null) {
+            State new_state;
+
+            message.parse_state_changed (null, out new_state, null);
+            if (new_state == State.PAUSED || new_state == State.PLAYING) {
+                // Time to shove-in the pending seek event
+                this.send_event (this.seek_event);
+                this.seek_event = null;
+            }
         } else {
             GLib.Error err;
             string err_msg;

Modified: trunk/src/rygel/rygel-streamer.vala
==============================================================================
--- trunk/src/rygel/rygel-streamer.vala	(original)
+++ trunk/src/rygel/rygel-streamer.vala	Wed Jan  7 12:56:08 2009
@@ -1,8 +1,10 @@
 /*
- * Copyright (C) 2008 Nokia Corporation, all rights reserved.
+ * Copyright (C) 2008, 2009 Nokia Corporation, all rights reserved.
+ * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
  *
  * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
  *                               <zeeshan ali nokia com>
+ *         Jorn Baayen <jorn baayen gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,6 +27,12 @@
 
 using Gee;
 using Gst;
+using GUPnP;
+
+public errordomain Rygel.StreamerError {
+    INVALID_RANGE = Soup.KnownStatusCode.BAD_REQUEST,
+    OUT_OF_RANGE = Soup.KnownStatusCode.REQUESTED_RANGE_NOT_SATISFIABLE
+}
 
 public class Rygel.Streamer : GLib.Object {
     private const string SERVER_PATH_PREFIX = "/RygelStreamer";
@@ -62,10 +70,12 @@
     }
 
     public void stream_from_gst_source (Element# src,
-                                        Stream   stream) throws Error {
-        GstStream gst_stream;
-
-        gst_stream = new GstStream (stream, "RygelGstStream", src);
+                                        Stream   stream,
+                                        Event?   seek_event) throws Error {
+        GstStream gst_stream = new GstStream (stream,
+                                              "RygelGstStream",
+                                              src,
+                                              seek_event);
 
         gst_stream.set_state (State.PLAYING);
         stream.eos += on_eos;
@@ -128,8 +138,26 @@
             return;
         }
 
+        size_t offset = 0;
+        size_t length = item.res.size;
+        bool got_range;
+
+        try {
+            got_range = this.parse_range (msg, out offset, out length);
+        } catch (StreamerError err) {
+            warning ("%s", err.message);
+            msg.set_status (err.code);
+            return;
+        }
+
+        if (item.res.size == -1 && got_range) {
+            warning ("Partial download not applicable for item %s", item_id);
+            msg.set_status (Soup.KnownStatusCode.NOT_ACCEPTABLE);
+            return;
+        }
+
         // Add headers
-        this.add_item_headers (msg, item);
+        this.add_item_headers (msg, item, got_range, offset, length);
 
         if (msg.method == "HEAD") {
             // Only headers requested, no need to stream contents
@@ -138,9 +166,9 @@
         }
 
         if (item.upnp_class == MediaItem.IMAGE_CLASS) {
-            this.handle_interactive_item (msg, item);
+            this.handle_interactive_item (msg, item, got_range, offset, length);
         } else {
-            this.handle_streaming_item (msg, item);
+            this.handle_streaming_item (msg, item, got_range, offset, length);
         }
     }
 
@@ -165,7 +193,10 @@
     }
 
     private void add_item_headers (Soup.Message msg,
-                                   MediaItem    item) {
+                                   MediaItem    item,
+                                   bool         partial_content,
+                                   size_t       offset,
+                                   size_t       length) {
         if (item.res.mime_type != null) {
             msg.response_headers.append ("Content-Type", item.res.mime_type);
         }
@@ -174,10 +205,27 @@
             msg.response_headers.append ("Content-Length",
                                          item.res.size.to_string ());
         }
+
+        if (DLNAOperation.RANGE in item.res.dlna_operation) {
+            msg.response_headers.append ("Accept-Ranges", "bytes");
+        }
+
+        if (partial_content) {
+            // Content-Range: bytes OFFSET-LENGTH/TOTAL_LENGTH
+            var content_range = "bytes " +
+                                offset.to_string () + "-" +
+                                (length - 1).to_string () + "/" +
+                                item.res.size.to_string ();
+
+            msg.response_headers.append ("Content-Range", content_range);
+        }
     }
 
     private void handle_streaming_item (Soup.Message msg,
-                                        MediaItem    item) {
+                                        MediaItem    item,
+                                        bool         partial_content,
+                                        size_t       offset,
+                                        size_t       length) {
         string uri = item.res.uri;
 
         // Create to Gst source that can handle the URI
@@ -195,8 +243,21 @@
         // create a stream for it
         var stream = new Stream (this.context.server, msg);
         try {
+            // Create the seek event if needed
+            Event seek_event = null;
+
+            if (partial_content) {
+                seek_event = new Event.seek (1.0,
+                                             Format.BYTES,
+                                             SeekFlags.FLUSH,
+                                             Gst.SeekType.SET,
+                                             offset,
+                                             Gst.SeekType.SET,
+                                             length);
+            }
+
             // Then attach the gst source to stream we are good to go
-            this.stream_from_gst_source (src, stream);
+            this.stream_from_gst_source (src, stream, seek_event);
         } catch (Error error) {
             critical ("Error in attempting to start streaming %s: %s",
                       uri,
@@ -205,17 +266,20 @@
     }
 
     private void handle_interactive_item (Soup.Message msg,
-                                          MediaItem    item) {
+                                          MediaItem    item,
+                                          bool         partial_content,
+                                          size_t       offset,
+                                          size_t       length) {
         string uri = item.res.uri;
 
         File file = File.new_for_uri (uri);
 
         string contents;
-        size_t length;
+        size_t file_length;
         try {
            file.load_contents (null,
                                out contents,
-                               out length,
+                               out file_length,
                                null);
         } catch (Error error) {
             warning ("Failed to load contents from URI: %s: %s\n",
@@ -225,10 +289,82 @@
             return;
         }
 
-        msg.set_status (Soup.KnownStatusCode.OK);
+        assert (offset <= file_length);
+        assert (length <= file_length);
+
+        if (partial_content) {
+            msg.set_status (Soup.KnownStatusCode.PARTIAL_CONTENT);
+        } else {
+            msg.set_status (Soup.KnownStatusCode.OK);
+        }
+
         msg.response_body.append (Soup.MemoryUse.COPY,
-                                  contents,
+                                  contents.offset ((long) offset),
                                   length);
     }
+
+    /* Parses the HTTP Range header on @message and sets:
+     *
+     * @offset to the requested offset (left unchanged if none specified),
+     * @length to the requested length (left unchanged if none specified).
+     *
+     * Both @offset and @length are expected to be initialised to their default
+     * values. Throws a #StreamerError in case of error.
+     *
+     * Returns %true a range header was found, false otherwise. */
+    private bool parse_range (Soup.Message message,
+                              out size_t   offset,
+                              out size_t   length)
+                              throws StreamerError {
+            string range;
+            string[] range_tokens;
+
+            range = message.request_headers.get ("Range");
+            if (range == null) {
+                return false;
+            }
+
+            // We have a Range header. Parse.
+            if (!range.has_prefix ("bytes=")) {
+                throw new StreamerError.INVALID_RANGE ("Invalid Range '%s'",
+                                                       range);
+            }
+
+            range_tokens = range.offset (6).split ("-", 2);
+
+            if (range_tokens[0] == null || range_tokens[1] == null) {
+                throw new StreamerError.INVALID_RANGE ("Invalid Range '%s'",
+                                                       range);
+            }
+
+            // Get first byte position
+            string first_byte = range_tokens[0];
+            if (first_byte[0].isdigit ()) {
+                offset = first_byte.to_long ();
+            } else if (first_byte  != "") {
+                throw new StreamerError.INVALID_RANGE ("Invalid Range '%s'",
+                                                       range);
+            }
+
+            // Save the actual length
+            size_t actual_length = length;
+
+            // Get last byte position if specified
+            string last_byte = range_tokens[1];
+            if (last_byte[0].isdigit ()) {
+                length = last_byte.to_long ();
+            } else if (last_byte  != "") {
+                throw new StreamerError.INVALID_RANGE ("Invalid Range '%s'",
+                                                       range);
+            }
+
+            // Offset shouldn't go beyond actual length of media
+            if (offset > actual_length || length > actual_length) {
+                throw new StreamerError.OUT_OF_RANGE (
+                                    "Range '%s' not setsifiable", range);
+            }
+
+            return true;
+        }
 }
 



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