diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-08-05 19:59:24 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-08-05 19:59:24 -0400 |
| commit | 3ba6364f259ea43979a88b2a83d64292119057dc (patch) | |
| tree | 5663fb9c54387e92a09d219488ad1af9276232aa | |
| parent | 7e25c857a551ce06025b3b85996aef7ed3c6571e (diff) | |
fixes #887 - Support ttml subtitle output
35 files changed, 333 insertions, 96 deletions
diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index 3e692cb22..f7696e963 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,8 @@ using MediaBrowser.Model.Providers; using ServiceStack; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -69,7 +69,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 { /// <summary> @@ -132,6 +133,7 @@ namespace MediaBrowser.Api.Subtitles request.Index, request.Format, request.StartPositionTicks, + null, CancellationToken.None).ConfigureAwait(false); } diff --git a/MediaBrowser.Api/SystemService.cs b/MediaBrowser.Api/SystemService.cs index e31e66d19..259b1d892 100644 --- a/MediaBrowser.Api/SystemService.cs +++ b/MediaBrowser.Api/SystemService.cs @@ -71,6 +71,8 @@ namespace MediaBrowser.Api /// Initializes a new instance of the <see cref="SystemService" /> class. /// </summary> /// <param name="appHost">The app host.</param> + /// <param name="appPaths">The application paths.</param> + /// <param name="fileSystem">The file system.</param> /// <exception cref="System.ArgumentNullException">jsonSerializer</exception> public SystemService(IServerApplicationHost appHost, IApplicationPaths appPaths, IFileSystem fileSystem) { diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 32d3dd5c8..d3085cb68 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<string>(); } + public override bool SupportsAddingToPlaylist + { + get { return LocationType == LocationType.FileSystem && RunTimeTicks.HasValue; } + } + /// <summary> /// Gets or sets a value indicating whether this instance has embedded image. /// </summary> diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 695b1fd57..152d76782 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<Guid>(); } + 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 1544da7bc..de527b68b 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<BaseItem> ActualChildren { get diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index bce9da4d1..f1dc56ac6 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; } + } + /// <summary> /// 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 a476f555f..fdffa60d0 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -52,6 +52,14 @@ namespace MediaBrowser.Controller.Entities public List<ItemImageInfo> ImageInfos { get; set; } + public virtual bool SupportsAddingToPlaylist + { + get + { + return false; + } + } + /// <summary> /// Gets a value indicating whether this instance is in mixed folder. /// </summary> diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 3977d869c..b82a400fe 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 27ca8b18d..856ed4fdf 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 5685edc81..ff4c5dd90 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -55,6 +55,11 @@ namespace MediaBrowser.Controller.Entities LinkedAlternateVersions = new List<LinkedChild>(); } + 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 49061e05c..2af37e84d 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -46,5 +46,11 @@ namespace MediaBrowser.Controller /// </summary> /// <value>The server identifier.</value> string ServerId { get; } + + /// <summary> + /// Gets the name of the friendly. + /// </summary> + /// <value>The name of the friendly.</value> + string FriendlyName { get; } } } diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index 541dfd226..d8d836597 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 6e9bcef2e..9e32fc32b 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="inputFormat">The input format.</param> /// <param name="outputFormat">The output format.</param> /// <param name="startTimeTicks">The start time ticks.</param> + /// <param name="endTimeTicks">The end time ticks.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{Stream}.</returns> Task<Stream> ConvertSubtitles( @@ -20,6 +21,7 @@ namespace MediaBrowser.Controller.MediaEncoding string inputFormat, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken); /// <summary> @@ -30,6 +32,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param> /// <param name="outputFormat">The output format.</param> /// <param name="startTimeTicks">The start time ticks.</param> + /// <param name="endTimeTicks">The end time ticks.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{Stream}.</returns> Task<Stream> GetSubtitles(string itemId, @@ -37,6 +40,7 @@ namespace MediaBrowser.Controller.MediaEncoding int subtitleStreamIndex, string outputFormat, long startTimeTicks, + long? endTimeTicks, CancellationToken cancellationToken); /// <summary> diff --git a/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs b/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs index 796ccb004..83d7f322d 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/Windows81Profile.cs b/MediaBrowser.Dlna/Profiles/Windows81Profile.cs index 921019f8e..029fefc64 100644 --- a/MediaBrowser.Dlna/Profiles/Windows81Profile.cs +++ b/MediaBrowser.Dlna/Profiles/Windows81Profile.cs @@ -155,6 +155,13 @@ namespace MediaBrowser.Dlna.Profiles } }; + ExternalSubtitleProfiles = new[] + { + new SubtitleProfile + { + Format = "ttml" + } + }; } } } diff --git a/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml b/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml index 5b5125b30..5859b2439 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml @@ -69,9 +69,7 @@ </CodecProfiles> <ResponseProfiles /> <SoftSubtitleProfiles> - <SubtitleProfile> - <Format>srt</Format> - </SubtitleProfile> + <SubtitleProfile format="srt" /> </SoftSubtitleProfiles> <ExternalSubtitleProfiles /> </Profile>
\ 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 209c029b5..28f170bc0 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml @@ -107,9 +107,7 @@ </ResponseProfile> </ResponseProfiles> <SoftSubtitleProfiles> - <SubtitleProfile> - <Format>smi</Format> - </SubtitleProfile> + <SubtitleProfile format="smi" /> </SoftSubtitleProfiles> <ExternalSubtitleProfiles /> </Profile>
\ 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 61ee59549..1847d844f 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml @@ -63,5 +63,7 @@ </CodecProfiles> <ResponseProfiles /> <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> + <ExternalSubtitleProfiles> + <SubtitleProfile format="ttml" /> + </ExternalSubtitleProfiles> </Profile>
\ No newline at end of file diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index 83bc6a49e..c7f974200 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 abc7e3b3f..1541c2176 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 /// <returns>Task.</returns> public void Save(IHasMetadata item, CancellationToken cancellationToken) { + var playlist = (Playlist)item; + var builder = new StringBuilder(); builder.Append("<Item>"); - XmlSaverHelpers.AddCommonNodes((Playlist)item, builder); + if (!string.IsNullOrEmpty(playlist.OwnerUserId)) + { + builder.Append("<OwnerUserId>" + SecurityElement.Escape(playlist.OwnerUserId) + "</OwnerUserId>"); + } + + if (!string.IsNullOrEmpty(playlist.PlaylistMediaType)) + { + builder.Append("<PlaylistMediaType>" + SecurityElement.Escape(playlist.PlaylistMediaType) + "</PlaylistMediaType>"); + } + + XmlSaverHelpers.AddCommonNodes(playlist, builder); builder.Append("</Item>"); var xmlFilePath = GetSavePath(item); - XmlSaverHelpers.Save(builder, xmlFilePath, new List<string> { }); + XmlSaverHelpers.Save(builder, xmlFilePath, new List<string> + { + "OwnerUserId", + "PlaylistMediaType" + + }); } /// <summary> diff --git a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs index a007a95cf..0801b7358 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("<Type>" + SecurityElement.Escape(link.ItemType) + "</Type>"); + if (!string.IsNullOrWhiteSpace(link.ItemType)) + { + builder.Append("<Type>" + SecurityElement.Escape(link.ItemType) + "</Type>"); + } if (link.ItemYear.HasValue) { builder.Append("<Year>" + SecurityElement.Escape(link.ItemYear.Value.ToString(UsCulture)) + "</Year>"); } - builder.Append("<Path>" + SecurityElement.Escape((link.Path ?? string.Empty)) + "</Path>"); + if (!string.IsNullOrWhiteSpace(link.Path)) + { + builder.Append("<Path>" + SecurityElement.Escape((link.Path)) + "</Path>"); + } builder.Append("</" + singularNodeName + ">"); } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index f11c261e2..12a03a876 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -66,6 +66,7 @@ <Compile Include="Subtitles\SsaParser.cs" /> <Compile Include="Subtitles\SubtitleEncoder.cs" /> <Compile Include="Subtitles\SubtitleTrackInfo.cs" /> + <Compile Include="Subtitles\TtmlWriter.cs" /> <Compile Include="Subtitles\VttWriter.cs" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index ab9cd546a..82e331dd8 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<Stream> 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 000000000..a937175f0 --- /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("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); + writer.WriteLine("<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:tts=\"http://www.w3.org/2006/04/ttaf1#styling\" lang=\"no\">"); + + writer.WriteLine("<head>"); + writer.WriteLine("<styling>"); + writer.WriteLine("<style id=\"italic\" tts:fontStyle=\"italic\" />"); + writer.WriteLine("<style id=\"left\" tts:textAlign=\"left\" />"); + writer.WriteLine("<style id=\"center\" tts:textAlign=\"center\" />"); + writer.WriteLine("<style id=\"right\" tts:textAlign=\"right\" />"); + writer.WriteLine("</styling>"); + writer.WriteLine("</head>"); + + writer.WriteLine("<body>"); + writer.WriteLine("<div>"); + + foreach (var trackEvent in info.TrackEvents) + { + var text = trackEvent.Text; + + text = Regex.Replace(text, @"\\N", "<br/>", RegexOptions.IgnoreCase); + + writer.WriteLine("<p begin=\"{0}\" dur=\"{1}\">{2}</p>", + trackEvent.StartPositionTicks, + (trackEvent.EndPositionTicks - trackEvent.StartPositionTicks), + text); + } + + writer.WriteLine("</div>"); + writer.WriteLine("</body>"); + + writer.WriteLine("</tt>"); + } + } + + private string FormatTime(long ticks) + { + var time = TimeSpan.FromTicks(ticks); + + return string.Format(@"{0:hh\:mm\:ss\,fff}", time); + } + } +} diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 6e929564e..22e817de1 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -2,6 +2,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; using System; using System.Collections.Generic; @@ -9,7 +10,7 @@ namespace MediaBrowser.Model.Dlna { public class StreamBuilder { - private string[] _serverTextSubtitleOutputs = new string[] { "srt", "vtt" }; + private readonly string[] _serverTextSubtitleOutputs = { "srt", "vtt", "ttml" }; public StreamInfo BuildAudioItem(AudioOptions options) { @@ -158,7 +159,7 @@ namespace MediaBrowser.Model.Dlna if (all) { - playlistItem.IsDirectStream = true; + playlistItem.PlayMethod = PlayMethod.DirectStream; playlistItem.Container = item.Container; return playlistItem; @@ -179,7 +180,7 @@ namespace MediaBrowser.Model.Dlna if (transcodingProfile != null) { - playlistItem.IsDirectStream = false; + playlistItem.PlayMethod = PlayMethod.Transcode; playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength; playlistItem.Container = transcodingProfile.Container; @@ -252,12 +253,12 @@ namespace MediaBrowser.Model.Dlna if (directPlay != null) { - playlistItem.IsDirectStream = true; + playlistItem.PlayMethod = PlayMethod.DirectStream; playlistItem.Container = item.Container; if (subtitleStream != null) { - playlistItem.SubtitleDeliveryMethod = GetDirectStreamSubtitleDeliveryMethod(subtitleStream, options); + playlistItem.SubtitleDeliveryMethod = GetSubtitleDeliveryMethod(subtitleStream, options); } return playlistItem; @@ -279,10 +280,10 @@ namespace MediaBrowser.Model.Dlna { if (subtitleStream != null) { - playlistItem.SubtitleDeliveryMethod = GetTranscodedSubtitleDeliveryMethod(subtitleStream, options); + playlistItem.SubtitleDeliveryMethod = GetSubtitleDeliveryMethod(subtitleStream, options); } - playlistItem.IsDirectStream = false; + playlistItem.PlayMethod = PlayMethod.Transcode; playlistItem.Container = transcodingProfile.Container; playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength; playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; @@ -499,9 +500,9 @@ namespace MediaBrowser.Model.Dlna return false; } - SubtitleDeliveryMethod subtitleMethod = GetDirectStreamSubtitleDeliveryMethod(subtitleStream, options); + SubtitleDeliveryMethod subtitleMethod = GetSubtitleDeliveryMethod(subtitleStream, options); - if (subtitleMethod != SubtitleDeliveryMethod.External && subtitleMethod != SubtitleDeliveryMethod.Direct) + if (subtitleMethod != SubtitleDeliveryMethod.External && subtitleMethod != SubtitleDeliveryMethod.Embed) { return false; } @@ -510,34 +511,7 @@ namespace MediaBrowser.Model.Dlna return IsAudioEligibleForDirectPlay(item, maxBitrate); } - private SubtitleDeliveryMethod GetDirectStreamSubtitleDeliveryMethod(MediaStream subtitleStream, - VideoOptions options) - { - if (subtitleStream.IsTextSubtitleStream) - { - string subtitleFormat = NormalizeSubtitleFormat(subtitleStream.Codec); - - bool supportsDirect = ContainsSubtitleFormat(options.Profile.SoftSubtitleProfiles, new[] { subtitleFormat }); - - if (supportsDirect) - { - return SubtitleDeliveryMethod.Direct; - } - - // See if the device can retrieve the subtitles externally - bool supportsSubsExternally = options.Context == EncodingContext.Streaming && - ContainsSubtitleFormat(options.Profile.ExternalSubtitleProfiles, _serverTextSubtitleOutputs); - - if (supportsSubsExternally) - { - return SubtitleDeliveryMethod.External; - } - } - - return SubtitleDeliveryMethod.Encode; - } - - private SubtitleDeliveryMethod GetTranscodedSubtitleDeliveryMethod(MediaStream subtitleStream, + private SubtitleDeliveryMethod GetSubtitleDeliveryMethod(MediaStream subtitleStream, VideoOptions options) { if (subtitleStream.IsTextSubtitleStream) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 8bf9f2a04..4f02d6450 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -3,6 +3,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; using System; using System.Collections.Generic; @@ -15,7 +16,7 @@ namespace MediaBrowser.Model.Dlna { public string ItemId { get; set; } - public bool IsDirectStream { get; set; } + public PlayMethod PlayMethod { get; set; } public DlnaProfileType MediaType { get; set; } @@ -59,6 +60,7 @@ namespace MediaBrowser.Model.Dlna public MediaSourceInfo MediaSource { get; set; } public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } + public string SubtitleFormat { get; set; } public string MediaSourceId { @@ -68,6 +70,11 @@ namespace MediaBrowser.Model.Dlna } } + public bool IsDirectStream + { + get { return PlayMethod == PlayMethod.DirectStream; } + } + public string ToUrl(string baseUrl) { return ToDlnaUrl(baseUrl); @@ -124,6 +131,32 @@ namespace MediaBrowser.Model.Dlna return string.Format("Params={0}", string.Join(";", list.ToArray())); } + public string ToSubtitleUrl(string baseUrl) + { + if (SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) + { + return null; + } + + if (!SubtitleStreamIndex.HasValue) + { + return null; + } + + // HLS will preserve timestamps so we can just grab the full subtitle stream + long startPositionTicks = StringHelper.EqualsIgnoreCase(Protocol, "hls") + ? 0 + : StartPositionTicks; + + return string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", + baseUrl, + ItemId, + MediaSourceId, + StringHelper.ToStringCultureInvariant(SubtitleStreamIndex.Value), + StringHelper.ToStringCultureInvariant(startPositionTicks), + SubtitleFormat); + } + /// <summary> /// Returns the audio stream that will be used /// </summary> @@ -437,16 +470,12 @@ namespace MediaBrowser.Model.Dlna /// </summary> Encode = 0, /// <summary> - /// Internal format is supported natively - /// </summary> - Direct = 1, - /// <summary> /// The embed /// </summary> - Embed = 2, + Embed = 1, /// <summary> /// The external /// </summary> - External = 3 + External = 2 } } diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs index ec38d96ae..7aa5e1771 100644 --- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs +++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs @@ -1,8 +1,13 @@ - +using System.Xml.Serialization; + namespace MediaBrowser.Model.Dlna { public class SubtitleProfile { + [XmlAttribute("format")] public string Format { get; set; } + + [XmlAttribute("protocol")] + public string Protocol { get; set; } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 9581b5740..32bf4b816 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -526,6 +526,12 @@ namespace MediaBrowser.Model.Dto } /// <summary> + /// Gets or sets a value indicating whether [supports playlists]. + /// </summary> + /// <value><c>true</c> if [supports playlists]; otherwise, <c>false</c>.</value> + public bool SupportsPlaylists { get; set; } + + /// <summary> /// Determines whether the specified type is type. /// </summary> /// <param name="type">The type.</param> @@ -632,12 +638,6 @@ namespace MediaBrowser.Model.Dto public string MediaType { get; set; } /// <summary> - /// Gets or sets the overview HTML. - /// </summary> - /// <value>The overview HTML.</value> - public string OverviewHtml { get; set; } - - /// <summary> /// Gets or sets the end date. /// </summary> /// <value>The end date.</value> diff --git a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs index 7e9cdf53f..60b0bb54d 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs @@ -8,5 +8,6 @@ public const string VTT = "vtt"; public const string SUB = "sub"; public const string SMI = "smi"; + public const string TTML = "ttml"; } }
\ No newline at end of file diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 5fb7ef11b..a1c229f9b 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -147,6 +147,7 @@ <Compile Include="Photos\PhotoHelper.cs" /> <Compile Include="Photos\PhotoMetadataService.cs" /> <Compile Include="Photos\PhotoProvider.cs" /> + <Compile Include="Playlists\PlaylistMetadataService.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Manager\ProviderUtils.cs" /> <Compile Include="Studios\StudiosImageProvider.cs" /> diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs new file mode 100644 index 000000000..d7615eb02 --- /dev/null +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -0,0 +1,47 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Providers.Manager; +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Playlists +{ + class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo> + { + public PlaylistMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem) + : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem) + { + } + + /// <summary> + /// Merges the specified source. + /// </summary> + /// <param name="source">The source.</param> + /// <param name="target">The target.</param> + /// <param name="lockedFields">The locked fields.</param> + /// <param name="replaceData">if set to <c>true</c> [replace data].</param> + /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param> + protected override void MergeData(Playlist source, Playlist target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + + if (replaceData || string.IsNullOrEmpty(target.PlaylistMediaType)) + { + target.PlaylistMediaType = source.PlaylistMediaType; + } + + if (replaceData || string.IsNullOrEmpty(target.OwnerUserId)) + { + target.OwnerUserId = source.OwnerUserId; + } + + if (mergeMetadataSettings) + { + target.LinkedChildren = source.LinkedChildren; + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index e3a386841..ace794b19 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -112,6 +112,8 @@ namespace MediaBrowser.Server.Implementations.Dto var dto = new BaseItemDto(); + dto.SupportsPlaylists = item.SupportsAddingToPlaylist; + if (fields.Contains(ItemFields.People)) { AttachPeople(dto, item); @@ -849,17 +851,7 @@ namespace MediaBrowser.Server.Implementations.Dto if (fields.Contains(ItemFields.Overview)) { - // TODO: Remove this after a while, since it's been moved to the providers - if (item is MusicArtist) - { - var strippedOverview = string.IsNullOrEmpty(item.Overview) ? item.Overview : item.Overview.StripHtml(); - - dto.Overview = strippedOverview; - } - else - { - dto.Overview = item.Overview; - } + dto.Overview = item.Overview; } if (fields.Contains(ItemFields.ShortOverview)) diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 251acb02d..8ffe8aa63 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -12,7 +12,6 @@ using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; diff --git a/MediaBrowser.Server.Implementations/Udp/UdpServer.cs b/MediaBrowser.Server.Implementations/Udp/UdpServer.cs index e4626bec6..9028b540f 100644 --- a/MediaBrowser.Server.Implementations/Udp/UdpServer.cs +++ b/MediaBrowser.Server.Implementations/Udp/UdpServer.cs @@ -124,7 +124,7 @@ namespace MediaBrowser.Server.Implementations.Udp { Address = serverAddress, Id = _appHost.ServerId, - Name = _appHost.Name + Name = _appHost.FriendlyName }; await SendAsync(Encoding.UTF8.GetBytes(_json.SerializeToString(response)), endpoint); diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 993cc4e1a..a4ac7e0ba 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -1058,10 +1058,20 @@ namespace MediaBrowser.ServerApplication SupportsAutoRunAtStartup = SupportsAutoRunAtStartup, TranscodingTempPath = ApplicationPaths.TranscodingTempPath, IsRunningAsService = IsRunningAsService, - ServerName = string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.ServerName) ? Environment.MachineName : ServerConfigurationManager.Configuration.ServerName + ServerName = FriendlyName }; } + public string FriendlyName + { + get + { + return string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.ServerName) + ? Environment.MachineName + : ServerConfigurationManager.Configuration.ServerName; + } + } + public int HttpServerPort { get { return ServerConfigurationManager.Configuration.HttpServerPortNumber; } |
