diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 1963ad10a5..9d54458a6e 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -1605,6 +1605,8 @@ namespace MediaBrowser.Api.Playback { state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true); } + + state.AllMediaStreams = mediaStreams; } private async Task GetChannelMediaInfo(string id, @@ -1640,7 +1642,10 @@ namespace MediaBrowser.Api.Playback // Can't stream copy if we're burning in subtitles if (request.SubtitleStreamIndex.HasValue) { - return false; + if (request.SubtitleMethod == SubtitleDeliveryMethod.Encode) + { + return false; + } } // Source and target codecs must match diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 10543351b9..42fa63fb7a 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -5,6 +5,8 @@ using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using ServiceStack; using System; @@ -18,8 +20,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Api.Playback.Hls { - [Route("/Videos/{Id}/master.m3u8", "GET")] - [Api(Description = "Gets a video stream using HTTP live streaming.")] + [Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")] public class GetMasterHlsVideoStream : VideoStreamRequest { public bool EnableAdaptiveBitrateStreaming { get; set; } @@ -30,8 +31,7 @@ namespace MediaBrowser.Api.Playback.Hls } } - [Route("/Videos/{Id}/main.m3u8", "GET")] - [Api(Description = "Gets a video stream using HTTP live streaming.")] + [Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")] public class GetMainHlsVideoStream : VideoStreamRequest { } @@ -359,7 +359,17 @@ namespace MediaBrowser.Api.Playback.Hls var playlistUrl = (state.RunTimeTicks ?? 0) > 0 ? "main.m3u8" : "live.m3u8"; playlistUrl += queryString; - AppendPlaylist(builder, playlistUrl, totalBitrate); + var request = (GetMasterHlsVideoStream) state.Request; + + var subtitleStreams = state.AllMediaStreams + .Where(i => i.IsTextSubtitleStream) + .ToList(); + + var subtitleGroup = subtitleStreams.Count > 0 && request.SubtitleMethod == SubtitleDeliveryMethod.Hls ? + "subs" : + null; + + AppendPlaylist(builder, playlistUrl, totalBitrate, subtitleGroup); if (EnableAdaptiveBitrateStreaming(state)) { @@ -369,16 +379,52 @@ namespace MediaBrowser.Api.Playback.Hls var variation = GetBitrateVariation(totalBitrate); var newBitrate = totalBitrate - variation; - AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate); + AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate, subtitleGroup); variation *= 2; newBitrate = totalBitrate - variation; - AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate); + AppendPlaylist(builder, playlistUrl.Replace(requestedVideoBitrate.ToString(UsCulture), (requestedVideoBitrate - variation).ToString(UsCulture)), newBitrate, subtitleGroup); + } + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + AddSubtitles(state, subtitleStreams, builder); } return builder.ToString(); } + private void AddSubtitles(StreamState state, IEnumerable subtitles, StringBuilder builder) + { + var selectedIndex = state.SubtitleStream == null ? (int?)null : state.SubtitleStream.Index; + + foreach (var stream in subtitles) + { + const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},URI=\"{3}\",LANGUAGE=\"{4}\""; + + var name = stream.Language; + + var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index; + var isForced = stream.IsForced; + + if (string.IsNullOrWhiteSpace(name)) name = stream.Codec ?? "Unknown"; + + var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}", + state.Request.MediaSourceId, + stream.Index.ToString(UsCulture), + 30.ToString(UsCulture)); + + var line = string.Format(format, + name, + isDefault ? "YES" : "NO", + isForced ? "YES" : "NO", + url, + stream.Language ?? "Unknown"); + + builder.AppendLine(line); + } + } + private bool EnableAdaptiveBitrateStreaming(StreamState state) { var request = state.Request as GetMasterHlsVideoStream; @@ -397,9 +443,16 @@ namespace MediaBrowser.Api.Playback.Hls return state.VideoRequest.VideoBitRate.HasValue; } - private void AppendPlaylist(StringBuilder builder, string url, int bitrate) + private void AppendPlaylist(StringBuilder builder, string url, int bitrate, string subtitleGroup) { - builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + bitrate.ToString(UsCulture)); + var header = "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + bitrate.ToString(UsCulture); + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + header += string.Format(",SUBTITLES=\"{0}\"", subtitleGroup); + } + + builder.AppendLine(header); builder.AppendLine(url); } diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index dfb57ef0d7..c72ead949d 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -1,4 +1,5 @@ -using ServiceStack; +using MediaBrowser.Model.Dlna; +using ServiceStack; namespace MediaBrowser.Api.Playback { @@ -160,6 +161,9 @@ namespace MediaBrowser.Api.Playback [ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Level { get; set; } + [ApiMember(Name = "SubtitleDeliveryMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public SubtitleDeliveryMethod SubtitleMethod { get; set; } + /// /// Gets a value indicating whether this instance has fixed resolution. /// diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index c6f4544477..1d3ff939af 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -38,6 +38,8 @@ namespace MediaBrowser.Api.Playback public string InputContainer { get; set; } + public List AllMediaStreams { get; set; } + public MediaStream AudioStream { get; set; } public MediaStream VideoStream { get; set; } public MediaStream SubtitleStream { get; set; } @@ -78,6 +80,7 @@ namespace MediaBrowser.Api.Playback SupportedAudioCodecs = new List(); PlayableStreamFileNames = new List(); RemoteHttpHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + AllMediaStreams = new List(); } public string InputAudioSync { get; set; } diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs index b4d2e2f0f2..2e3d38f465 100644 --- a/MediaBrowser.Api/PlaylistService.cs +++ b/MediaBrowser.Api/PlaylistService.cs @@ -41,6 +41,9 @@ namespace MediaBrowser.Api { [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public string Id { get; set; } + + [ApiMember(Name = "EntryIds", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] + public string EntryIds { get; set; } } [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")] @@ -122,9 +125,9 @@ namespace MediaBrowser.Api public void Delete(RemoveFromPlaylist request) { - //var task = _playlistManager.RemoveFromPlaylist(request.Id, request.Ids.Split(',').Select(i => new Guid(i))); + var task = _playlistManager.RemoveFromPlaylist(request.Id, request.EntryIds.Split(',')); - //Task.WaitAll(task); + Task.WaitAll(task); } public object Get(GetPlaylistItems request) diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index 3e692cb22f..dc5799239a 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Linq; -using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; @@ -11,6 +9,10 @@ using MediaBrowser.Model.Providers; using ServiceStack; using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -69,7 +71,8 @@ namespace MediaBrowser.Api.Subtitles public string Id { get; set; } } - [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format (vtt).")] + [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")] + [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")] public class GetSubtitle { /// @@ -90,6 +93,29 @@ namespace MediaBrowser.Api.Subtitles [ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public long StartPositionTicks { get; set; } + + [ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public long? EndPositionTicks { get; set; } + } + + [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")] + public class GetSubtitlePlaylist + { + /// + /// Gets or sets the id. + /// + /// The id. + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + + [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string MediaSourceId { get; set; } + + [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] + public int Index { get; set; } + + [ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")] + public int SegmentLength { get; set; } } public class SubtitleService : BaseApiService @@ -105,6 +131,53 @@ namespace MediaBrowser.Api.Subtitles _subtitleEncoder = subtitleEncoder; } + public object Get(GetSubtitlePlaylist request) + { + var item = (Video)_libraryManager.GetItemById(new Guid(request.Id)); + + var mediaSource = item.GetMediaSources(false) + .First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id)); + + var builder = new StringBuilder(); + + var runtime = mediaSource.RunTimeTicks ?? -1; + + if (runtime <= 0) + { + throw new ArgumentException("HLS Subtitles are not supported for this media."); + } + + builder.AppendLine("#EXTM3U"); + builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture)); + builder.AppendLine("#EXT-X-VERSION:3"); + builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); + + long positionTicks = 0; + var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks; + + while (positionTicks < runtime) + { + var remaining = runtime - positionTicks; + var lengthTicks = Math.Min(remaining, segmentLengthTicks); + + builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + + var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); + + var url = string.Format("stream.srt?StartPositionTicks={0}&EndPositionTicks={1}", + positionTicks.ToString(CultureInfo.InvariantCulture), + endPositionTicks.ToString(CultureInfo.InvariantCulture)); + + builder.AppendLine(url); + + positionTicks += segmentLengthTicks; + } + + builder.AppendLine("#EXT-X-ENDLIST"); + + return ResultFactory.GetResult(builder.ToString(), Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary()); + } + public object Get(GetSubtitle request) { if (string.IsNullOrEmpty(request.Format)) @@ -132,6 +205,7 @@ namespace MediaBrowser.Api.Subtitles request.Index, request.Format, request.StartPositionTicks, + request.EndPositionTicks, CancellationToken.None).ConfigureAwait(false); } diff --git a/MediaBrowser.Api/SystemService.cs b/MediaBrowser.Api/SystemService.cs index e31e66d19d..259b1d8921 100644 --- a/MediaBrowser.Api/SystemService.cs +++ b/MediaBrowser.Api/SystemService.cs @@ -71,6 +71,8 @@ namespace MediaBrowser.Api /// Initializes a new instance of the class. /// /// The app host. + /// The application paths. + /// The file system. /// jsonSerializer public SystemService(IServerApplicationHost appHost, IApplicationPaths appPaths, IFileSystem fileSystem) { diff --git a/MediaBrowser.Common/Net/MimeTypes.cs b/MediaBrowser.Common/Net/MimeTypes.cs index 0740bf6d13..ee3b7dad6e 100644 --- a/MediaBrowser.Common/Net/MimeTypes.cs +++ b/MediaBrowser.Common/Net/MimeTypes.cs @@ -236,6 +236,11 @@ namespace MediaBrowser.Common.Net return "text/vtt"; } + if (ext.Equals(".ttml", StringComparison.OrdinalIgnoreCase)) + { + return "application/ttml+xml"; + } + if (ext.Equals(".bif", StringComparison.OrdinalIgnoreCase)) { return "application/octet-stream"; diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 32d3dd5c8c..d3085cb680 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -34,6 +34,11 @@ namespace MediaBrowser.Controller.Entities.Audio Tags = new List(); } + public override bool SupportsAddingToPlaylist + { + get { return LocationType == LocationType.FileSystem && RunTimeTicks.HasValue; } + } + /// /// Gets or sets a value indicating whether this instance has embedded image. /// diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 695b1fd570..152d767823 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -21,6 +21,11 @@ namespace MediaBrowser.Controller.Entities.Audio SoundtrackIds = new List(); } + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + [IgnoreDataMember] public MusicArtist MusicArtist { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 1544da7bc9..de527b68b3 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -26,6 +26,11 @@ namespace MediaBrowser.Controller.Entities.Audio } } + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + protected override IEnumerable ActualChildren { get diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index bce9da4d15..f1dc56ac68 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -18,6 +18,11 @@ namespace MediaBrowser.Controller.Entities.Audio return "MusicGenre-" + Name; } + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + /// /// Returns the folder containing the item. /// If the item is a folder, it returns the folder itself diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a476f555fb..fdffa60d0a 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -52,6 +52,14 @@ namespace MediaBrowser.Controller.Entities public List ImageInfos { get; set; } + public virtual bool SupportsAddingToPlaylist + { + get + { + return false; + } + } + /// /// Gets a value indicating whether this instance is in mixed folder. /// diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index c77fe18c45..eb94b37dbc 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -13,6 +13,9 @@ namespace MediaBrowser.Controller.Entities public string ItemType { get; set; } public int? ItemYear { get; set; } + [IgnoreDataMember] + public string Id { get; set; } + /// /// Serves as a cache /// @@ -27,6 +30,11 @@ namespace MediaBrowser.Controller.Entities Type = LinkedChildType.Manual }; } + + public LinkedChild() + { + Id = Guid.NewGuid().ToString("N"); + } } public enum LinkedChildType diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 3977d869c2..b82a400fe8 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -29,6 +29,11 @@ namespace MediaBrowser.Controller.Entities.TV } } + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + [IgnoreDataMember] public override bool IsPreSorted { diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 27ca8b18da..856ed4fdf2 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -39,6 +39,11 @@ namespace MediaBrowser.Controller.Entities.TV DisplaySpecialsWithSeasons = true; } + public override bool SupportsAddingToPlaylist + { + get { return true; } + } + [IgnoreDataMember] public override bool IsPreSorted { diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 5685edc815..ff4c5dd900 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -55,6 +55,11 @@ namespace MediaBrowser.Controller.Entities LinkedAlternateVersions = new List(); } + public override bool SupportsAddingToPlaylist + { + get { return LocationType == LocationType.FileSystem && RunTimeTicks.HasValue; } + } + [IgnoreDataMember] public int MediaSourceCount { diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 49061e05c7..2af37e84dc 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -46,5 +46,11 @@ namespace MediaBrowser.Controller /// /// The server identifier. string ServerId { get; } + + /// + /// Gets the name of the friendly. + /// + /// The name of the friendly. + string FriendlyName { get; } } } diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index 541dfd2268..d8d836597b 100644 --- a/MediaBrowser.Controller/Library/TVUtils.cs +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -126,7 +126,7 @@ namespace MediaBrowser.Controller.Library { var filename = Path.GetFileName(path); - if (string.Equals(path, "specials", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase)) { return 0; } diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index 6e9bcef2ea..9e32fc32b0 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// The input format. /// The output format. /// The start time ticks. + /// The end time ticks. /// The cancellation token. /// Task{Stream}. Task ConvertSubtitles( @@ -20,6 +21,7 @@ namespace MediaBrowser.Controller.MediaEncoding string inputFormat, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken); /// @@ -30,6 +32,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// Index of the subtitle stream. /// The output format. /// The start time ticks. + /// The end time ticks. /// The cancellation token. /// Task{Stream}. Task GetSubtitles(string itemId, @@ -37,6 +40,7 @@ namespace MediaBrowser.Controller.MediaEncoding int subtitleStreamIndex, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken); /// diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 2923c11c51..f5939ad969 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -32,9 +32,9 @@ namespace MediaBrowser.Controller.Playlists /// Removes from playlist. /// /// The playlist identifier. - /// The indeces. + /// The entry ids. /// Task. - Task RemoveFromPlaylist(string playlistId, IEnumerable indeces); + Task RemoveFromPlaylist(string playlistId, IEnumerable entryIds); /// /// Gets the playlists folder. diff --git a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs index 4eb6baeed2..05d822185f 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs +++ b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs @@ -183,19 +183,34 @@ namespace MediaBrowser.Dlna.ContentDirectory //didl.SetAttribute("xmlns:sec", NS_SEC); result.AppendChild(didl); - var folder = (Folder)GetItemFromObjectId(id, user); + var item = GetItemFromObjectId(id, user); - var childrenResult = (await GetChildrenSorted(folder, user, sortCriteria, start, requested).ConfigureAwait(false)); - - var totalCount = childrenResult.TotalRecordCount; + var totalCount = 0; if (string.Equals(flag, "BrowseMetadata")) { - result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, folder, totalCount, filter)); + var folder = item as Folder; + + if (folder == null) + { + result.DocumentElement.AppendChild(_didlBuilder.GetItemElement(result, item, deviceId, filter)); + } + else + { + var childrenResult = (await GetChildrenSorted(folder, user, sortCriteria, start, requested).ConfigureAwait(false)); + totalCount = childrenResult.TotalRecordCount; + + result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, folder, totalCount, filter)); + } provided++; } else { + var folder = (Folder)item; + + var childrenResult = (await GetChildrenSorted(folder, user, sortCriteria, start, requested).ConfigureAwait(false)); + totalCount = childrenResult.TotalRecordCount; + provided = childrenResult.Items.Length; foreach (var i in childrenResult.Items) diff --git a/MediaBrowser.Dlna/Didl/DidlBuilder.cs b/MediaBrowser.Dlna/Didl/DidlBuilder.cs index ec86f69e79..a5a97567a9 100644 --- a/MediaBrowser.Dlna/Didl/DidlBuilder.cs +++ b/MediaBrowser.Dlna/Didl/DidlBuilder.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Net; +using System.IO; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -13,6 +14,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml; +using MediaBrowser.Common.Extensions; namespace MediaBrowser.Dlna.Didl { @@ -101,7 +103,7 @@ namespace MediaBrowser.Dlna.Didl { var sources = _user == null ? video.GetMediaSources(true).ToList() : video.GetMediaSources(true, _user).ToList(); - streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions + streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions { ItemId = video.Id.ToString("N"), MediaSources = sources, @@ -137,6 +139,23 @@ namespace MediaBrowser.Dlna.Didl { AddVideoResource(container, video, deviceId, filter, contentFeature, streamInfo); } + + foreach (var subtitle in streamInfo.GetExternalSubtitles(_serverAddress)) + { + AddSubtitleElement(container, subtitle); + } + } + + private void AddSubtitleElement(XmlElement container, SubtitleStreamInfo info) + { + var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL); + + res.InnerText = info.Url; + + // TODO: Remove this hard-coding + res.SetAttribute("protocolInfo", "http-get:*:text/srt:*"); + + container.AppendChild(res); } private void AddVideoResource(XmlElement container, Video video, string deviceId, Filter filter, string contentFeatures, StreamInfo streamInfo) @@ -598,9 +617,11 @@ namespace MediaBrowser.Dlna.Didl } AddImageResElement(item, element, 4096, 4096, "jpg"); + AddImageResElement(item, element, 4096, 4096, "png"); AddImageResElement(item, element, 1024, 768, "jpg"); AddImageResElement(item, element, 640, 480, "jpg"); AddImageResElement(item, element, 160, 160, "jpg"); + AddImageResElement(item, element, 160, 160, "png"); } private void AddImageResElement(BaseItem item, XmlElement element, int maxWidth, int maxHeight, string format) @@ -623,7 +644,7 @@ namespace MediaBrowser.Dlna.Didl var width = albumartUrlInfo.Width; var height = albumartUrlInfo.Height; - var contentFeatures = new ContentFeatureBuilder(_profile).BuildImageHeader(format, width, height); + var contentFeatures = new ContentFeatureBuilder(_profile).BuildImageHeader(format, width, height, imageInfo.IsDirectStream); res.SetAttribute("protocolInfo", String.Format( "http-get:*:{0}:{1}", @@ -631,6 +652,14 @@ namespace MediaBrowser.Dlna.Didl contentFeatures )); + res.SetAttribute("colorDepth", "24"); + + if (imageInfo.IsDirectStream) + { + // TODO: Add file size + //res.SetAttribute("size", imageInfo.Size.Value.ToString(_usCulture)); + } + if (width.HasValue && height.HasValue) { res.SetAttribute("resolution", string.Format("{0}x{1}", width.Value, height.Value)); @@ -705,7 +734,8 @@ namespace MediaBrowser.Dlna.Didl Type = type, ImageTag = tag, Width = width, - Height = height + Height = height, + File = imageInfo.Path }; } @@ -717,6 +747,10 @@ namespace MediaBrowser.Dlna.Didl internal int? Width; internal int? Height; + + internal bool IsDirectStream; + + internal string File; } class ImageUrlInfo @@ -741,6 +775,8 @@ namespace MediaBrowser.Dlna.Didl var width = info.Width; var height = info.Height; + info.IsDirectStream = false; + if (width.HasValue && height.HasValue) { var newSize = DrawingUtils.Resize(new ImageSize @@ -752,6 +788,18 @@ namespace MediaBrowser.Dlna.Didl width = Convert.ToInt32(newSize.Width); height = Convert.ToInt32(newSize.Height); + + var inputFormat = (Path.GetExtension(info.File) ?? string.Empty) + .TrimStart('.') + .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase); + + var normalizedFormat = format + .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase); + + if (string.Equals(inputFormat, normalizedFormat, StringComparison.OrdinalIgnoreCase)) + { + info.IsDirectStream = maxWidth >= width.Value && maxHeight >= height.Value; + } } return new ImageUrlInfo diff --git a/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs b/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs index 796ccb004c..83d7f322d1 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs @@ -1,5 +1,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Session; using System; using System.Globalization; using System.IO; @@ -29,7 +30,7 @@ namespace MediaBrowser.Dlna.PlayTo if (directPlay != null) { - playlistItem.StreamInfo.IsDirectStream = true; + playlistItem.StreamInfo.PlayMethod = PlayMethod.DirectStream; playlistItem.StreamInfo.Container = Path.GetExtension(item.Path); return playlistItem; @@ -40,7 +41,7 @@ namespace MediaBrowser.Dlna.PlayTo if (transcodingProfile != null) { - playlistItem.StreamInfo.IsDirectStream = true; + playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode; playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.'); } diff --git a/MediaBrowser.Dlna/Profiles/PanasonicVieraProfile.cs b/MediaBrowser.Dlna/Profiles/PanasonicVieraProfile.cs index d12b3598ca..533f4ecf49 100644 --- a/MediaBrowser.Dlna/Profiles/PanasonicVieraProfile.cs +++ b/MediaBrowser.Dlna/Profiles/PanasonicVieraProfile.cs @@ -193,11 +193,12 @@ namespace MediaBrowser.Dlna.Profiles } }; - SoftSubtitleProfiles = new[] + SubtitleProfiles = new[] { new SubtitleProfile { - Format = "srt" + Format = "srt", + Method = SubtitleDeliveryMethod.External } }; } diff --git a/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs b/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs index b90c906fbc..3dc1967dd9 100644 --- a/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs +++ b/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs @@ -339,11 +339,12 @@ namespace MediaBrowser.Dlna.Profiles } }; - SoftSubtitleProfiles = new[] + SubtitleProfiles = new[] { new SubtitleProfile { - Format = "smi" + Format = "smi", + Method = SubtitleDeliveryMethod.External } }; } diff --git a/MediaBrowser.Dlna/Profiles/Windows81Profile.cs b/MediaBrowser.Dlna/Profiles/Windows81Profile.cs index 921019f8e7..20d0834e1e 100644 --- a/MediaBrowser.Dlna/Profiles/Windows81Profile.cs +++ b/MediaBrowser.Dlna/Profiles/Windows81Profile.cs @@ -155,6 +155,14 @@ namespace MediaBrowser.Dlna.Profiles } }; + SubtitleProfiles = new[] + { + new SubtitleProfile + { + Format = "ttml", + Method = SubtitleDeliveryMethod.External + } + }; } } } diff --git a/MediaBrowser.Dlna/Profiles/Xml/Android.xml b/MediaBrowser.Dlna/Profiles/Xml/Android.xml index b7a6cc19c3..efab21021d 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Android.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Android.xml @@ -65,6 +65,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Default.xml b/MediaBrowser.Dlna/Profiles/Xml/Default.xml index 6aafbe86e0..164ea943df 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Default.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Default.xml @@ -35,6 +35,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml b/MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml index 28fe6e0c9d..9a4322b68f 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml @@ -39,6 +39,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml b/MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml index f0cf1e96c9..ee18a5efa2 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml @@ -73,6 +73,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml b/MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml index 775c7e4661..43516a378d 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml @@ -39,6 +39,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml b/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml index 1461c2255c..1d63d38859 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml @@ -45,6 +45,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml b/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml index 5b5125b30c..2ae5524faa 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml @@ -68,10 +68,7 @@ - - - srt - - - + + + \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml b/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml index 209c029b5d..b8edd8ff51 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml @@ -106,10 +106,7 @@ - - - smi - - - + + + \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml b/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml index 2e32b77c69..c3c13cdc3d 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml @@ -71,6 +71,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml b/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml index 87ba6e33bf..d48fc34b18 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml @@ -99,6 +99,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml index 698bb44b10..319936e0a5 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml @@ -107,6 +107,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml index f07536fcb5..541ecf8286 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml @@ -110,6 +110,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml index a99e4fa1e1..a99dc6b4e6 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml @@ -93,6 +93,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml index 3d4661621b..40e55e98ad 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml @@ -93,6 +93,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml index 55f89e3eb9..4499b905df 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml @@ -93,6 +93,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml b/MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml index 5d12b65c81..4e0231a535 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml @@ -78,6 +78,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml b/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml index 61ee595494..dec3d02ef3 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml @@ -62,6 +62,7 @@ - - + + + \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Windows Phone.xml b/MediaBrowser.Dlna/Profiles/Xml/Windows Phone.xml index 12b7fe9c93..7c3414b25d 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Windows Phone.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Windows Phone.xml @@ -76,6 +76,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml b/MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml index f3e1531300..b629fa5183 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml @@ -100,6 +100,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml b/MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml index 7e057a3f9d..5f86a94a9f 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml @@ -90,6 +90,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml b/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml index d099113081..40ad2bc78a 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml @@ -45,6 +45,4 @@ - - \ No newline at end of file diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index 83bc6a49ee..c7f9742008 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -18,6 +18,20 @@ namespace MediaBrowser.LocalMetadata.Parsers { switch (reader.Name) { + case "OwnerUserId": + { + item.OwnerUserId = reader.ReadElementContentAsString(); + + break; + } + + case "PlaylistMediaType": + { + item.PlaylistMediaType = reader.ReadElementContentAsString(); + + break; + } + case "PlaylistItems": using (var subReader = reader.ReadSubtree()) diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs index abc7e3b3fa..1541c2176b 100644 --- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Entities; +using System.Security; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using System.Collections.Generic; @@ -42,17 +43,34 @@ namespace MediaBrowser.LocalMetadata.Savers /// Task. public void Save(IHasMetadata item, CancellationToken cancellationToken) { + var playlist = (Playlist)item; + var builder = new StringBuilder(); builder.Append(""); - XmlSaverHelpers.AddCommonNodes((Playlist)item, builder); + if (!string.IsNullOrEmpty(playlist.OwnerUserId)) + { + builder.Append("" + SecurityElement.Escape(playlist.OwnerUserId) + ""); + } + + if (!string.IsNullOrEmpty(playlist.PlaylistMediaType)) + { + builder.Append("" + SecurityElement.Escape(playlist.PlaylistMediaType) + ""); + } + + XmlSaverHelpers.AddCommonNodes(playlist, builder); builder.Append(""); var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new List { }); + XmlSaverHelpers.Save(builder, xmlFilePath, new List + { + "OwnerUserId", + "PlaylistMediaType" + + }); } /// diff --git a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs index a007a95cf0..0801b73589 100644 --- a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs +++ b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs @@ -704,7 +704,7 @@ namespace MediaBrowser.LocalMetadata.Savers public static void AddLinkedChildren(Folder item, StringBuilder builder, string pluralNodeName, string singularNodeName) { var items = item.LinkedChildren - .Where(i => i.Type == LinkedChildType.Manual && !string.IsNullOrWhiteSpace(i.ItemName)) + .Where(i => i.Type == LinkedChildType.Manual) .ToList(); if (items.Count == 0) @@ -717,14 +717,20 @@ namespace MediaBrowser.LocalMetadata.Savers { builder.Append("<" + singularNodeName + ">"); - builder.Append("" + SecurityElement.Escape(link.ItemType) + ""); + if (!string.IsNullOrWhiteSpace(link.ItemType)) + { + builder.Append("" + SecurityElement.Escape(link.ItemType) + ""); + } if (link.ItemYear.HasValue) { builder.Append("" + SecurityElement.Escape(link.ItemYear.Value.ToString(UsCulture)) + ""); } - builder.Append("" + SecurityElement.Escape((link.Path ?? string.Empty)) + ""); + if (!string.IsNullOrWhiteSpace(link.Path)) + { + builder.Append("" + SecurityElement.Escape((link.Path)) + ""); + } builder.Append(""); } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 46c680730d..aef99f0d12 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -66,6 +66,7 @@ + diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index ab9cd546aa..82e331dd87 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -48,6 +48,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles string inputFormat, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken) { var ms = new MemoryStream(); @@ -56,6 +57,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { // Return the original without any conversions, if possible if (startTimeTicks == 0 && + !endTimeTicks.HasValue && string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase)) { await stream.CopyToAsync(ms, 81920, cancellationToken).ConfigureAwait(false); @@ -64,7 +66,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { var trackInfo = await GetTrackInfo(stream, inputFormat, cancellationToken).ConfigureAwait(false); - UpdateStartingPosition(trackInfo, startTimeTicks); + FilterEvents(trackInfo, startTimeTicks, endTimeTicks, false); var writer = GetWriter(outputFormat); @@ -81,19 +83,30 @@ namespace MediaBrowser.MediaEncoding.Subtitles return ms; } - private void UpdateStartingPosition(SubtitleTrackInfo track, long startPositionTicks) + private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long? endTimeTicks, bool preserveTimestamps) { - if (startPositionTicks == 0) return; + // Drop subs that are earlier than what we're looking for + track.TrackEvents = track.TrackEvents + .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0) + .ToList(); - foreach (var trackEvent in track.TrackEvents) + if (endTimeTicks.HasValue) { - trackEvent.EndPositionTicks -= startPositionTicks; - trackEvent.StartPositionTicks -= startPositionTicks; + var endTime = endTimeTicks.Value; + + track.TrackEvents = track.TrackEvents + .TakeWhile(i => i.StartPositionTicks <= endTime) + .ToList(); } - track.TrackEvents = track.TrackEvents - .SkipWhile(i => i.StartPositionTicks < 0 || i.EndPositionTicks < 0) - .ToList(); + if (!preserveTimestamps) + { + foreach (var trackEvent in track.TrackEvents) + { + trackEvent.EndPositionTicks -= startPositionTicks; + trackEvent.StartPositionTicks -= startPositionTicks; + } + } } public async Task GetSubtitles(string itemId, @@ -101,6 +114,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles int subtitleStreamIndex, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken) { var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken) @@ -110,7 +124,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { var inputFormat = subtitle.Item2; - return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, cancellationToken).ConfigureAwait(false); + return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, cancellationToken).ConfigureAwait(false); } } @@ -254,6 +268,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles { return new VttWriter(); } + if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase)) + { + return new TtmlWriter(); + } throw new ArgumentException("Unsupported format: " + format); } diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs new file mode 100644 index 0000000000..a937175f01 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + public class TtmlWriter : ISubtitleWriter + { + public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) + { + // Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml + // Parser example: https://github.com/mozilla/popcorn-js/blob/master/parsers/parserTTML/popcorn.parserTTML.js + + using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + { + writer.WriteLine(""); + writer.WriteLine(""); + + writer.WriteLine(""); + writer.WriteLine(""); + writer.WriteLine("