From 63bc34de234634345e2e301cdda4c4d35ef916fd Mon Sep 17 00:00:00 2001 From: Jens Georg Date: Sun, 16 Jun 2013 11:22:31 +0200 Subject: [PATCH] server: Add a M3U resource to containers --- src/librygel-server/filelist.am | 1 + src/librygel-server/rygel-http-get.vala | 6 +- .../rygel-http-playlist-handler.vala | 40 +++++++++-- src/librygel-server/rygel-m3u-playlist.vala | 79 ++++++++++++++++++++++ src/librygel-server/rygel-media-container.vala | 67 +++++++++++------- src/librygel-server/rygel-serializer.vala | 56 +++++++++++---- 6 files changed, 206 insertions(+), 43 deletions(-) create mode 100644 src/librygel-server/rygel-m3u-playlist.vala diff --git a/src/librygel-server/filelist.am b/src/librygel-server/filelist.am index dec2e1e..fcc0bbc 100644 --- a/src/librygel-server/filelist.am +++ b/src/librygel-server/filelist.am @@ -62,6 +62,7 @@ LIBRYGEL_SERVER_NONVAPI_SOURCE_FILES = \ rygel-last-change-obj-mod.vala \ rygel-last-change-st-done.vala \ rygel-last-change.vala \ + rygel-m3u-playlist.vala \ rygel-media-query-action.vala \ rygel-media-receiver-registrar.vala \ rygel-panasonic-hacks.vala \ diff --git a/src/librygel-server/rygel-http-get.vala b/src/librygel-server/rygel-http-get.vala index e977cea..d8882fc 100644 --- a/src/librygel-server/rygel-http-get.vala +++ b/src/librygel-server/rygel-http-get.vala @@ -66,8 +66,10 @@ internal class Rygel.HTTPGet : HTTPRequest { this.cancellable); } - if (uri.playlist_format != null) { - this.handler = new HTTPPlaylistHandler (this.cancellable); + if (uri.playlist_format != null && + HTTPPlaylistHandler.is_supported (uri.playlist_format)) { + this.handler = new HTTPPlaylistHandler (uri.playlist_format, + this.cancellable); } if (this.handler == null) { diff --git a/src/librygel-server/rygel-http-playlist-handler.vala b/src/librygel-server/rygel-http-playlist-handler.vala index 7adf0b7..e2a4e39 100644 --- a/src/librygel-server/rygel-http-playlist-handler.vala +++ b/src/librygel-server/rygel-http-playlist-handler.vala @@ -29,10 +29,13 @@ internal class Rygel.PlaylistDatasource : Rygel.DataSource, Object { private uint8[] data; private HTTPServer server; private ClientHacks hacks; + private SerializerType playlist_type; - public PlaylistDatasource (MediaContainer container, + public PlaylistDatasource (SerializerType playlist_type, + MediaContainer container, HTTPServer server, ClientHacks? hacks) { + this.playlist_type = playlist_type; this.container = container; this.server = server; this.hacks = hacks; @@ -82,7 +85,7 @@ internal class Rygel.PlaylistDatasource : Rygel.DataSource, Object { null); if (children != null) { - var serializer = new Serializer (SerializerType.DIDL_S); + var serializer = new Serializer (this.playlist_type); children.serialize (serializer, this.server, this.hacks); var xml = serializer.get_string (); @@ -105,13 +108,38 @@ internal class Rygel.PlaylistDatasource : Rygel.DataSource, Object { * playlists (DIDL_S format as defined by DLNA) on-the-fly. */ internal class Rygel.HTTPPlaylistHandler : Rygel.HTTPGetHandler { - public HTTPPlaylistHandler (Cancellable? cancellable) { + private SerializerType playlist_type; + + public static bool is_supported (string playlist_format) { + return playlist_format == "DIDL_S" || playlist_format == "M3U"; + } + + public HTTPPlaylistHandler (string playlist_format, + Cancellable? cancellable) { + if (playlist_format == "DIDL_S") { + this.playlist_type = SerializerType.DIDL_S; + } else if (playlist_format == "M3U") { + this.playlist_type = SerializerType.M3UEXT; + } + this.cancellable = cancellable; } public override void add_response_headers (HTTPGet request) throws HTTPRequestError { - request.msg.response_headers.append ("Content-Type", "text/xml"); + // TODO: Why do we use response_headers.append instead of set_content_type + switch (this.playlist_type) { + case SerializerType.DIDL_S: + request.msg.response_headers.append ("Content-Type", + "text/xml"); + break; + case SerializerType.M3UEXT: + request.msg.response_headers.append ("ContentType", + "audio/x-mpegurl"); + break; + default: + assert_not_reached (); + } base.add_response_headers (request); } @@ -120,7 +148,8 @@ internal class Rygel.HTTPPlaylistHandler : Rygel.HTTPGetHandler { throws HTTPRequestError { try { var source = new PlaylistDatasource - (request.object as MediaContainer, + (this.playlist_type, + request.object as MediaContainer, request.http_server, request.hack); @@ -134,7 +163,6 @@ internal class Rygel.HTTPPlaylistHandler : Rygel.HTTPGetHandler { (DIDLLiteObject didl_object, HTTPGet request) { var protocol = request.http_server.get_protocol (); - debug ("=> Protocol of this http server is: %s", protocol); try { return request.object.add_resource (didl_object, null, protocol); diff --git a/src/librygel-server/rygel-m3u-playlist.vala b/src/librygel-server/rygel-m3u-playlist.vala new file mode 100644 index 0000000..ae333dd --- /dev/null +++ b/src/librygel-server/rygel-m3u-playlist.vala @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2013 Jens Georg. + * + * Authors: Jens Georg + * + * 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 Gee; +using GUPnP; + +/** + * Serializer class that serializes to an EXTM3U playlist for use with normal + * media players or UPnP Renderers that don't support DIDL_S. + * + * For the description of the EXTM3U format, see + * http://en.wikipedia.org/wiki/M3U#Extended_M3U_directives + */ +internal class Rygel.M3UPlayList : Object { + private LinkedList items; + + // We need this writer for the namespaces, the document etc. + private DIDLLiteWriter writer; + + public M3UPlayList () { + Object (); + } + + public override void constructed () { + this.items = new LinkedList (); + this.writer = new DIDLLiteWriter (null); + } + + + public DIDLLiteItem? add_item () { + this.items.add (this.writer.add_item ()); + + return this.items.last (); + } + + public string get_string () { + var builder = new StringBuilder ("#EXTM3U\r\n"); + + foreach (var item in this.items) { + var resources = item.get_resources (); + if (resources != null) { + var authors = item.get_artists (); + builder.append_printf ("#EXTINF:%ld,", + resources.data.duration); + if (authors != null) { + builder.append_printf ("%s - ", + authors.data.get_name () ?? + _("Unknown")); + } + + builder.append (item.title ?? _("Unknown")); + builder.append ("\r\n"); + builder.append (resources.data.uri); + builder.append ("\r\n"); + } + } + + return builder.str; + } +} diff --git a/src/librygel-server/rygel-media-container.vala b/src/librygel-server/rygel-media-container.vala index 97ecfb1..4ae62e1 100644 --- a/src/librygel-server/rygel-media-container.vala +++ b/src/librygel-server/rygel-media-container.vala @@ -310,6 +310,15 @@ public abstract class Rygel.MediaContainer : MediaObject { didl_container.restricted = true; } + this.add_resources (http_server, didl_container); + + return didl_container; + } + + internal void add_resources (Rygel.HTTPServer http_server, + DIDLLiteContainer didl_container) + throws Error { + // Add resource with container contents serialized to DIDL_S playlist var uri = new HTTPItemURI (this, http_server, -1, @@ -318,11 +327,24 @@ public abstract class Rygel.MediaContainer : MediaObject { "DIDL_S"); uri.extension = "xml"; - this.add_resource (didl_container, - uri.to_string (), - http_server.get_protocol ()); + var res = this.add_resource (didl_container, + uri.to_string (), + http_server.get_protocol ()); + if (res != null) { + res.protocol_info.mime_type = "text/xml"; + res.protocol_info.dlna_profile = "DIDL_S"; + } - return didl_container; + // Add resource with container contents serialized to M3U playlist + uri = new HTTPItemURI (this, http_server, -1, -1, null, "M3U"); + uri.extension = "m3u"; + + res = this.add_resource (didl_container, + uri.to_string (), + http_server.get_protocol ()); + if (res != null) { + res.protocol_info.mime_type = "audio/x-mpegurl"; + } } internal override DIDLLiteResource add_resource @@ -331,29 +353,28 @@ public abstract class Rygel.MediaContainer : MediaObject { string protocol, string? import_uri = null) throws Error { - if (this.child_count > 0) { - var res = base.add_resource (didl_object, - uri, - protocol, - import_uri); - - if (uri != null) { - res.uri = uri; - } + if (this.child_count <= 0) { + return null as DIDLLiteResource; + } - var protocol_info = new ProtocolInfo (); - protocol_info.mime_type = "text/xml"; - protocol_info.dlna_profile = "DIDL_S"; - protocol_info.protocol = protocol; - protocol_info.dlna_flags = DLNAFlags.DLNA_V15 | - DLNAFlags.CONNECTION_STALL | - DLNAFlags.BACKGROUND_TRANSFER_MODE; - res.protocol_info = protocol_info; + var res = base.add_resource (didl_object, + uri, + protocol, + import_uri); - return res; + if (uri != null) { + res.uri = uri; } - return null as DIDLLiteResource; + var protocol_info = new ProtocolInfo (); + protocol_info.mime_type = ""; + protocol_info.protocol = protocol; + protocol_info.dlna_flags = DLNAFlags.DLNA_V15 | + DLNAFlags.CONNECTION_STALL | + DLNAFlags.BACKGROUND_TRANSFER_MODE; + res.protocol_info = protocol_info; + + return res; } /** diff --git a/src/librygel-server/rygel-serializer.vala b/src/librygel-server/rygel-serializer.vala index e7a411b..0e56022 100644 --- a/src/librygel-server/rygel-serializer.vala +++ b/src/librygel-server/rygel-serializer.vala @@ -23,40 +23,67 @@ using GUPnP; internal enum SerializerType { + /// Normal serialization of container/item using DIDL-Lite GENERIC_DIDL, - DIDL_S + + /// Special version of a DIDL-Lite document for playlists, defined by DLNA + DIDL_S, + + /// M3UEXT format as used by various media players + M3UEXT } +/** + * Proxy class hiding the different serializers (DIDL, DIDL_S, M3U) behind a + * single implementation. + */ internal class Rygel.Serializer : Object { private DIDLLiteWriter writer; private MediaCollection collection; + private M3UPlayList playlist; + + // private properties + public SerializerType serializer_type { construct; private get; } public Serializer (SerializerType type) { - switch (type) { + Object (serializer_type: type); + } + + public override void constructed () { + switch (this.serializer_type) { case SerializerType.GENERIC_DIDL: this.writer = new DIDLLiteWriter (null); break; case SerializerType.DIDL_S: this.collection = new MediaCollection (); break; + case SerializerType.M3UEXT: + this.playlist = new M3UPlayList (); + break; default: assert_not_reached (); } + + base.constructed (); } public DIDLLiteItem? add_item () { - if (writer != null) { - return this.writer.add_item (); - } else { - return this.collection.add_item (); + switch (this.serializer_type) { + case SerializerType.GENERIC_DIDL: + return this.writer.add_item (); + case SerializerType.DIDL_S: + return this.collection.add_item (); + case SerializerType.M3UEXT: + return this.playlist.add_item (); + default: + return null; } } public DIDLLiteContainer? add_container () { - if (writer != null) { + if (this.writer != null) { return this.writer.add_container (); } else { - // MediaCollection does not support this. return null; } } @@ -68,10 +95,15 @@ internal class Rygel.Serializer : Object { } public string get_string () { - if (writer != null) { - return this.writer.get_string (); - } else { - return this.collection.get_string (); + switch (this.serializer_type) { + case SerializerType.GENERIC_DIDL: + return this.writer.get_string (); + case SerializerType.DIDL_S: + return this.collection.get_string (); + case SerializerType.M3UEXT: + return this.playlist.get_string (); + default: + return ""; } } } -- 2.7.4