From ec9c6b677889a9cae11c066f260aa56a2086ea3a Mon Sep 17 00:00:00 2001 From: Andreas Henriksson Date: Thu, 8 Apr 2010 02:19:47 +0300 Subject: [PATCH] core: Add basic support for subtitles For each video item, we check if there is a file in the same directory with the same name but extension "srt" and if so, we serve it using Samsung's custom DIDL-Lite and HTTP extensions. Co-author & reviewer: Zeeshan Ali (Khattak) --- src/rygel/Makefile.am | 2 + src/rygel/rygel-http-byte-seek.vala | 5 +- src/rygel/rygel-http-get-handler.vala | 14 ++++++ src/rygel/rygel-http-get.vala | 5 ++ src/rygel/rygel-http-identity-handler.vala | 14 +++++- src/rygel/rygel-http-item-uri.vala | 10 ++++ src/rygel/rygel-http-server.vala | 26 +++++++++- src/rygel/rygel-media-item.vala | 23 +++++++++ src/rygel/rygel-subtitle-manager.vala | 76 ++++++++++++++++++++++++++++++ src/rygel/rygel-subtitle.vala | 54 +++++++++++++++++++++ src/rygel/rygel-transcode-manager.vala | 1 + src/rygel/rygel-transcoder.vala | 1 + 12 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 src/rygel/rygel-subtitle-manager.vala create mode 100644 src/rygel/rygel-subtitle.vala diff --git a/src/rygel/Makefile.am b/src/rygel/Makefile.am index b093db1..28afb3e 100644 --- a/src/rygel/Makefile.am +++ b/src/rygel/Makefile.am @@ -75,6 +75,8 @@ VAPI_SOURCE_FILES = rygel-configuration.vala \ rygel-media-item.vala \ rygel-thumbnail.vala \ rygel-thumbnailer.vala \ + rygel-subtitle.vala \ + rygel-subtitle-manager.vala \ rygel-browse.vala \ rygel-search.vala \ rygel-xbox-hacks.vala \ diff --git a/src/rygel/rygel-http-byte-seek.vala b/src/rygel/rygel-http-byte-seek.vala index 08257b8..51ea055 100644 --- a/src/rygel/rygel-http-byte-seek.vala +++ b/src/rygel/rygel-http-byte-seek.vala @@ -29,6 +29,8 @@ internal class Rygel.HTTPByteSeek : Rygel.HTTPSeek { if (request.thumbnail != null) { total_length = request.thumbnail.size; + } else if (request.subtitle != null) { + total_length = request.subtitle.size; } else { total_length = request.item.size; } @@ -80,7 +82,8 @@ internal class Rygel.HTTPByteSeek : Rygel.HTTPSeek { public static bool needed (HTTPGet request) { return (request.item.size > 0 && request.handler is HTTPIdentityHandler) || - (request.thumbnail != null && request.thumbnail.size > 0); + (request.thumbnail != null && request.thumbnail.size > 0) || + (request.subtitle != null && request.subtitle.size > 0); } public override void add_response_headers () { diff --git a/src/rygel/rygel-http-get-handler.vala b/src/rygel/rygel-http-get-handler.vala index fe9c354..4beeea5 100644 --- a/src/rygel/rygel-http-get-handler.vala +++ b/src/rygel/rygel-http-get-handler.vala @@ -1,5 +1,6 @@ /* * Copyright (C) 2008-2010 Nokia Corporation. + * Copyright (C) 2010 Andreas Henriksson * * Author: Zeeshan Ali (Khattak) * @@ -56,6 +57,19 @@ internal abstract class Rygel.HTTPGetHandler: GLib.Object { warning ("Received request for 'contentFeatures.dlna.org' but " + "failed to provide the value in response headers"); } + + // Handle Samsung DLNA TV proprietary subtitle headers + if (request.msg.request_headers.get ("getCaptionInfo.sec") != null && + request.item.subtitles.size > 0) { + var caption_uri = request.http_server.create_uri_for_item ( + request.item, + -1, + 0, // FIXME: offer first subtitle only? + null); + + request.msg.response_headers.append ("CaptionInfo.sec", + caption_uri); + } } // Create an HTTPResponse object that will render the body. diff --git a/src/rygel/rygel-http-get.vala b/src/rygel/rygel-http-get.vala index ecd0a07..e3c5ee8 100644 --- a/src/rygel/rygel-http-get.vala +++ b/src/rygel/rygel-http-get.vala @@ -30,9 +30,11 @@ using Gst; */ internal class Rygel.HTTPGet : HTTPRequest { public Thumbnail thumbnail; + public Subtitle subtitle; public HTTPSeek seek; private int thumbnail_index; + private int subtitle_index; public HTTPGetHandler handler; @@ -42,6 +44,7 @@ internal class Rygel.HTTPGet : HTTPRequest { base (http_server, server, msg); this.thumbnail_index = -1; + this.subtitle_index = -1; } protected override async void handle () { @@ -84,6 +87,8 @@ internal class Rygel.HTTPGet : HTTPRequest { if (this.uri.thumbnail_index >= 0) { this.thumbnail = this.item.thumbnails.get ( this.uri.thumbnail_index); + } else if (this.uri.subtitle_index >= 0) { + this.subtitle = this.item.subtitles.get (this.uri.subtitle_index); } } diff --git a/src/rygel/rygel-http-identity-handler.vala b/src/rygel/rygel-http-identity-handler.vala index 9db8625..640b1f5 100644 --- a/src/rygel/rygel-http-identity-handler.vala +++ b/src/rygel/rygel-http-identity-handler.vala @@ -32,7 +32,10 @@ internal class Rygel.HTTPIdentityHandler : Rygel.HTTPGetHandler { public override void add_response_headers (HTTPGet request) throws HTTPRequestError { - if (request.thumbnail != null) { + if (request.subtitle != null) { + request.msg.response_headers.append ("Content-Type", + request.subtitle.mime_type); + } else if (request.thumbnail != null) { request.msg.response_headers.append ("Content-Type", request.thumbnail.mime_type); } else { @@ -70,7 +73,14 @@ internal class Rygel.HTTPIdentityHandler : Rygel.HTTPGetHandler { } private HTTPResponse render_body_real (HTTPGet request) throws Error { - if (request.thumbnail != null) { + if (request.subtitle != null) { + return new SeekableResponse (request.server, + request.msg, + request.subtitle.uri, + request.seek, + request.subtitle.size, + this.cancellable); + } else if (request.thumbnail != null) { return new SeekableResponse (request.server, request.msg, request.thumbnail.uri, diff --git a/src/rygel/rygel-http-item-uri.vala b/src/rygel/rygel-http-item-uri.vala index ca92df4..7cf8980 100644 --- a/src/rygel/rygel-http-item-uri.vala +++ b/src/rygel/rygel-http-item-uri.vala @@ -26,15 +26,18 @@ internal class Rygel.HTTPItemURI : Object { public string item_id; public int thumbnail_index; + public int subtitle_index; public string? transcode_target; public HTTPServer http_server; public HTTPItemURI (string item_id, HTTPServer http_server, int thumbnail_index = -1, + int subtitle_index = -1, string? transcode_target = null) { this.item_id = item_id; this.thumbnail_index = thumbnail_index; + this.subtitle_index = subtitle_index; this.transcode_target = transcode_target; this.http_server = http_server; } @@ -44,6 +47,7 @@ internal class Rygel.HTTPItemURI : Object { throws HTTPRequestError { // do not decode the path here as it may contain encoded slashes this.thumbnail_index = -1; + this.subtitle_index = -1; this.transcode_target = null; this.http_server = http_server; @@ -72,6 +76,10 @@ internal class Rygel.HTTPItemURI : Object { this.thumbnail_index = parts[i + 1].to_int (); break; + case "subtitle": + this.subtitle_index = parts[i + 1].to_int (); + + break; default: break; } @@ -97,6 +105,8 @@ internal class Rygel.HTTPItemURI : Object { path += "/transcoded/" + escaped; } else if (this.thumbnail_index >= 0) { path += "/thumbnail/" + this.thumbnail_index.to_string (); + } else if (this.subtitle_index >= 0) { + path += "/subtitle/" + this.subtitle_index.to_string (); } return this.create_uri_for_path (path); diff --git a/src/rygel/rygel-http-server.vala b/src/rygel/rygel-http-server.vala index 9b46441..a16d904 100644 --- a/src/rygel/rygel-http-server.vala +++ b/src/rygel/rygel-http-server.vala @@ -66,6 +66,23 @@ internal class Rygel.HTTPServer : Rygel.TranscodeManager, Rygel.StateMachine { internal override void add_resources (DIDLLiteItem didl_item, MediaItem item) throws Error { + // Subtitles first + foreach (var subtitle in item.subtitles) { + if (!is_http_uri (subtitle.uri)) { + var uri = subtitle.uri; // Save the original URI + var index = item.subtitles.index_of (subtitle); + + subtitle.uri = this.create_uri_for_item (item, + -1, + index, + null); + subtitle.add_didl_node (didl_item); + + // Now restore the original URI + subtitle.uri = uri; + } + } + if (!this.http_uri_present (item)) { this.add_proxy_resource (didl_item, item); } @@ -78,7 +95,10 @@ internal class Rygel.HTTPServer : Rygel.TranscodeManager, Rygel.StateMachine { var uri = thumbnail.uri; // Save the original URI var index = item.thumbnails.index_of (thumbnail); - thumbnail.uri = this.create_uri_for_item (item, index, null); + thumbnail.uri = this.create_uri_for_item (item, + index, + -1, + null); thumbnail.add_resource (didl_item, this.get_protocol ()); // Now restore the original URI @@ -90,7 +110,7 @@ internal class Rygel.HTTPServer : Rygel.TranscodeManager, Rygel.StateMachine { internal void add_proxy_resource (DIDLLiteItem didl_item, MediaItem item) throws Error { - var uri = this.create_uri_for_item (item, -1, null); + var uri = this.create_uri_for_item (item, -1, -1, null); item.add_resource (didl_item, uri.to_string (), @@ -127,10 +147,12 @@ internal class Rygel.HTTPServer : Rygel.TranscodeManager, Rygel.StateMachine { internal override string create_uri_for_item (MediaItem item, int thumbnail_index, + int subtitle_index, string? transcode_target) { var uri = new HTTPItemURI (item.id, this, thumbnail_index, + subtitle_index, transcode_target); return uri.to_string (); diff --git a/src/rygel/rygel-media-item.vala b/src/rygel/rygel-media-item.vala index 68b9d70..85e0eac 100644 --- a/src/rygel/rygel-media-item.vala +++ b/src/rygel/rygel-media-item.vala @@ -64,6 +64,7 @@ public class Rygel.MediaItem : MediaObject { public int color_depth = -1; public ArrayList thumbnails; + public ArrayList subtitles; internal bool place_holder = false; @@ -77,6 +78,7 @@ public class Rygel.MediaItem : MediaObject { this.upnp_class = upnp_class; this.thumbnails = new ArrayList (); + this.subtitles = new ArrayList (); } // Live media items need to provide a nice working implementation of this @@ -127,6 +129,19 @@ public class Rygel.MediaItem : MediaObject { this.thumbnails.add (thumb); } catch (Error err) {} } + + if (this.upnp_class.has_prefix (MediaItem.VIDEO_CLASS)) { + var subtitle_manager = SubtitleManager.get_default (); + + if (subtitle_manager == null) { + return; + } + + try { + var subtitle = subtitle_manager.get_subtitle (uri); + this.subtitles.add (subtitle); + } catch (Error err) {} + } } internal int compare_transcoders (void *a, void *b) { @@ -140,6 +155,14 @@ public class Rygel.MediaItem : MediaObject { internal void add_resources (DIDLLiteItem didl_item, bool allow_internal) throws Error { + foreach (var subtitle in this.subtitles) { + var protocol = this.get_protocol_for_uri (subtitle.uri); + + if (allow_internal || protocol != "internal") { + subtitle.add_didl_node (didl_item); + } + } + foreach (var uri in this.uris) { var protocol = this.get_protocol_for_uri (uri); diff --git a/src/rygel/rygel-subtitle-manager.vala b/src/rygel/rygel-subtitle-manager.vala new file mode 100644 index 0000000..7ecf86d --- /dev/null +++ b/src/rygel/rygel-subtitle-manager.vala @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008 Zeeshan Ali . + * Copyright (C) 2010 Andreas Henriksson . + * + * Authors: Andreas Henriksson + * Zeeshan Ali (Khattak) + * + * This file is part of Rygel. + * + * Rygel is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Rygel is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +internal errordomain SubtitleManagerError { + NO_SUBTITLE +} + +/** + * Provides subtitles for vidoes. + */ +internal class Rygel.SubtitleManager : GLib.Object { + private static SubtitleManager manager; // Our singleton object + + public static SubtitleManager? get_default () { + if (manager == null) { + manager = new SubtitleManager (); + } + + return manager; + } + + public Subtitle get_subtitle (string uri) throws Error { + var video_file = File.new_for_uri (uri); + + var directory = video_file.get_parent (); + var filename = video_file.get_basename (); + var extension = filename.rchr (-1, '.'); + if (extension != null) { + filename = filename.substring (0, + filename.length - extension.length); + } + // FIXME: foreach ".eng.srt", ".ger.srt", ".srt"... + // FIXME: case insensitive? + filename += ".srt"; + + var srt_file = directory.get_child (filename); + + var info = srt_file.query_info (FILE_ATTRIBUTE_ACCESS_CAN_READ + "," + + FILE_ATTRIBUTE_STANDARD_SIZE, + FileQueryInfoFlags.NONE, + null); + + if (!info.get_attribute_boolean (FILE_ATTRIBUTE_ACCESS_CAN_READ)) { + throw new SubtitleManagerError.NO_SUBTITLE ( + "No subtitle available"); + } + + var subtitle = new Subtitle (); + subtitle.uri = srt_file.get_uri (); + subtitle.size = (long) info.get_attribute_uint64 ( + FILE_ATTRIBUTE_STANDARD_SIZE); + + return subtitle; + } +} diff --git a/src/rygel/rygel-subtitle.vala b/src/rygel/rygel-subtitle.vala new file mode 100644 index 0000000..17d531f --- /dev/null +++ b/src/rygel/rygel-subtitle.vala @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2008 Zeeshan Ali . + * Copyright (C) 2010 Andreas Henriksson + * + * Authors: Andreas Henriksson + * Zeeshan Ali (Khattak) + * + * This file is part of Rygel. + * + * Rygel is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Rygel is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using GUPnP; + +/** + * Represents a subtitle for a video. + */ +public class Rygel.Subtitle { + public string uri; + public string mime_type; + public string caption_type; + + public long size = -1; // Size in bytes + + public Subtitle (string mime_type = "text/plain", + string caption_type = "srt") { + this.mime_type = mime_type; + this.caption_type = caption_type; + } + + internal void add_didl_node (DIDLLiteItem didl_item) { + Xml.Node *item_node = didl_item.xml_node; + Xml.Node *root_node = item_node->doc->get_root_element (); + + weak Xml.Ns sec_ns = root_node->new_ns ("http://www.sec.co.kr/", "sec"); + Xml.Node *sec_node = item_node->new_child (sec_ns, + "CaptionInfoEx", + this.uri); + + sec_node->new_prop ("sec:type", this.caption_type); + } +} diff --git a/src/rygel/rygel-transcode-manager.vala b/src/rygel/rygel-transcode-manager.vala index cedecfb..4fed0fe 100644 --- a/src/rygel/rygel-transcode-manager.vala +++ b/src/rygel/rygel-transcode-manager.vala @@ -58,6 +58,7 @@ internal abstract class Rygel.TranscodeManager : GLib.Object { public abstract string create_uri_for_item (MediaItem item, int thumbnail_index, + int subtitle_index, string? transcode_target); public virtual void add_resources (DIDLLiteItem didl_item, diff --git a/src/rygel/rygel-transcoder.vala b/src/rygel/rygel-transcoder.vala index 16b6ca8..bc3e13a 100644 --- a/src/rygel/rygel-transcoder.vala +++ b/src/rygel/rygel-transcoder.vala @@ -67,6 +67,7 @@ internal abstract class Rygel.Transcoder : GLib.Object { var protocol = manager.get_protocol (); var uri = manager.create_uri_for_item (item, -1, + -1, this.dlna_profile); var res = item.add_resource (didl_item, uri, protocol); res.size = -1; -- 2.7.4