diff options
75 files changed, 689 insertions, 201 deletions
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 1963ad10a..9d54458a6 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<MediaSourceInfo> 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 10543351b..42fa63fb7 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<MediaStream> 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 dfb57ef0d..c72ead949 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; } + /// <summary> /// Gets a value indicating whether this instance has fixed resolution. /// </summary> diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index c6f454447..1d3ff939a 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<MediaStream> 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<string>(); PlayableStreamFileNames = new List<string>(); RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + AllMediaStreams = new List<MediaStream>(); } public string InputAudioSync { get; set; } diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs index b4d2e2f0f..2e3d38f46 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 3e692cb22..dc5799239 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 { /// <summary> @@ -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 + { + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [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<string, string>()); + } + 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 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.Common/Net/MimeTypes.cs b/MediaBrowser.Common/Net/MimeTypes.cs index 0740bf6d1..ee3b7dad6 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 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/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index c77fe18c4..eb94b37db 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; } + /// <summary> /// Serves as a cache /// </summary> @@ -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 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.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 2923c11c5..f5939ad96 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -32,9 +32,9 @@ namespace MediaBrowser.Controller.Playlists /// Removes from playlist. /// </summary> /// <param name="playlistId">The playlist identifier.</param> - /// <param name="indeces">The indeces.</param> + /// <param name="entryIds">The entry ids.</param> /// <returns>Task.</returns> - Task RemoveFromPlaylist(string playlistId, IEnumerable<int> indeces); + Task RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds); /// <summary> /// Gets the playlists folder. diff --git a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs index 4eb6baeed..05d822185 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 childrenResult = (await GetChildrenSorted(folder, user, sortCriteria, start, requested).ConfigureAwait(false)); + var item = GetItemFromObjectId(id, user); - 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 ec86f69e7..a5a97567a 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 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/PanasonicVieraProfile.cs b/MediaBrowser.Dlna/Profiles/PanasonicVieraProfile.cs index d12b3598c..533f4ecf4 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 b90c906fb..3dc1967dd 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 921019f8e..20d0834e1 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 b7a6cc19c..efab21021 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Android.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Android.xml @@ -65,6 +65,4 @@ </CodecProfile> </CodecProfiles> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/Default.xml b/MediaBrowser.Dlna/Profiles/Xml/Default.xml index 6aafbe86e..164ea943d 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Default.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Default.xml @@ -35,6 +35,4 @@ <ContainerProfiles /> <CodecProfiles /> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 28fe6e0c9..9a4322b68 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml @@ -39,6 +39,4 @@ <ContainerProfiles /> <CodecProfiles /> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 f0cf1e96c..ee18a5efa 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml @@ -73,6 +73,4 @@ </CodecProfile> </CodecProfiles> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 775c7e466..43516a378 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml @@ -39,6 +39,4 @@ <ContainerProfiles /> <CodecProfiles /> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml b/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml index 1461c2255..1d63d3885 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml @@ -45,6 +45,4 @@ <ContainerProfiles /> <CodecProfiles /> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 5b5125b30..2ae5524fa 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml @@ -68,10 +68,7 @@ </CodecProfile> </CodecProfiles> <ResponseProfiles /> - <SoftSubtitleProfiles> - <SubtitleProfile> - <Format>srt</Format> - </SubtitleProfile> - </SoftSubtitleProfiles> - <ExternalSubtitleProfiles /> + <SubtitleProfiles> + <SubtitleProfile format="srt" method="External" /> + </SubtitleProfiles> </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..b8edd8ff5 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml @@ -106,10 +106,7 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <SoftSubtitleProfiles> - <SubtitleProfile> - <Format>smi</Format> - </SubtitleProfile> - </SoftSubtitleProfiles> - <ExternalSubtitleProfiles /> + <SubtitleProfiles> + <SubtitleProfile format="smi" method="External" /> + </SubtitleProfiles> </Profile>
\ 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 2e32b77c6..c3c13cdc3 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 @@ </CodecProfile> </CodecProfiles> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 87ba6e33b..d48fc34b1 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 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 698bb44b1..319936e0a 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml @@ -107,6 +107,4 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 f07536fcb..541ecf828 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml @@ -110,6 +110,4 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 a99e4fa1e..a99dc6b4e 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml @@ -93,6 +93,4 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 3d4661621..40e55e98a 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml @@ -93,6 +93,4 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 55f89e3eb..4499b905d 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml @@ -93,6 +93,4 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 5d12b65c8..4e0231a53 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml @@ -78,6 +78,4 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <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..dec3d02ef 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Windows 8 RT.xml @@ -62,6 +62,7 @@ </CodecProfile> </CodecProfiles> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> + <SubtitleProfiles> + <SubtitleProfile format="ttml" method="External" /> + </SubtitleProfiles> </Profile>
\ 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 12b7fe9c9..7c3414b25 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Windows Phone.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Windows Phone.xml @@ -76,6 +76,4 @@ </CodecProfile> </CodecProfiles> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 f3e153130..b629fa518 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml @@ -100,6 +100,4 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ 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 7e057a3f9..5f86a94a9 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml @@ -90,6 +90,4 @@ <Conditions /> </ResponseProfile> </ResponseProfiles> - <SoftSubtitleProfiles /> - <ExternalSubtitleProfiles /> </Profile>
\ No newline at end of file diff --git a/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml b/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml index d09911308..40ad2bc78 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml @@ -45,6 +45,4 @@ <ContainerProfiles /> <CodecProfiles /> <ResponseProfiles /> - <SoftSubtitleProfiles /> - <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 46c680730..aef99f0d1 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/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index 5417c5b82..c25e93c38 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -14,12 +14,13 @@ namespace MediaBrowser.Model.Dlna public string BuildImageHeader(string container, int? width, - int? height) + int? height, + bool isDirectStream) { string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetImageOrgOpValue(); // 0 = native, 1 = transcoded - const string orgCi = ";DLNA.ORG_CI=0"; + var orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; DlnaFlags flagValue = DlnaFlags.StreamingTransferMode | DlnaFlags.BackgroundTransferMode | diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 630e7fa60..5366a538c 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -90,8 +90,7 @@ namespace MediaBrowser.Model.Dlna public CodecProfile[] CodecProfiles { get; set; } public ResponseProfile[] ResponseProfiles { get; set; } - public SubtitleProfile[] SoftSubtitleProfiles { get; set; } - public SubtitleProfile[] ExternalSubtitleProfiles { get; set; } + public SubtitleProfile[] SubtitleProfiles { get; set; } public DeviceProfile() { @@ -100,9 +99,6 @@ namespace MediaBrowser.Model.Dlna ResponseProfiles = new ResponseProfile[] { }; CodecProfiles = new CodecProfile[] { }; ContainerProfiles = new ContainerProfile[] { }; - - SoftSubtitleProfiles = new SubtitleProfile[] { }; - ExternalSubtitleProfiles = new SubtitleProfile[] { }; XmlRootAttributes = new XmlAttribute[] { }; diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs index 036c1fc74..9a84eb195 100644 --- a/MediaBrowser.Model/Dlna/DlnaMaps.cs +++ b/MediaBrowser.Model/Dlna/DlnaMaps.cs @@ -48,7 +48,7 @@ orgOp += "0"; // Byte-based seeking only possible when not transcoding - orgOp += "1"; + orgOp += "0"; return orgOp; } diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs index 3b4cc30ac..5101bbe5a 100644 --- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs +++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs @@ -385,7 +385,7 @@ namespace MediaBrowser.Model.Dlna return ResolveImageJPGFormat(width, height); if (StringHelper.EqualsIgnoreCase(container, "png")) - return MediaFormatProfile.PNG_LRG; + return ResolveImagePNGFormat(width, height); if (StringHelper.EqualsIgnoreCase(container, "gif")) return MediaFormatProfile.GIF_LRG; @@ -401,7 +401,7 @@ namespace MediaBrowser.Model.Dlna if (width.HasValue && height.HasValue) { if ((width.Value <= 160) && (height.Value <= 160)) - return MediaFormatProfile.JPEG_SM; + return MediaFormatProfile.JPEG_TN; if ((width.Value <= 640) && (height.Value <= 480)) return MediaFormatProfile.JPEG_SM; @@ -416,5 +416,16 @@ namespace MediaBrowser.Model.Dlna return MediaFormatProfile.JPEG_SM; } + + private MediaFormatProfile ResolveImagePNGFormat(int? width, int? height) + { + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 160) && (height.Value <= 160)) + return MediaFormatProfile.PNG_TN; + } + + return MediaFormatProfile.PNG_LRG; + } } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 6e929564e..72bf88b70 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,41 +511,14 @@ 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) { // See if the device can retrieve the subtitles externally bool supportsSubsExternally = options.Context == EncodingContext.Streaming && - ContainsSubtitleFormat(options.Profile.ExternalSubtitleProfiles, _serverTextSubtitleOutputs); + ContainsSubtitleFormat(options.Profile.SubtitleProfiles, SubtitleDeliveryMethod.External, _serverTextSubtitleOutputs); if (supportsSubsExternally) { @@ -552,7 +526,7 @@ namespace MediaBrowser.Model.Dlna } // See if the device can retrieve the subtitles externally - bool supportsEmbedded = ContainsSubtitleFormat(options.Profile.SoftSubtitleProfiles, _serverTextSubtitleOutputs); + bool supportsEmbedded = ContainsSubtitleFormat(options.Profile.SubtitleProfiles, SubtitleDeliveryMethod.Embed, _serverTextSubtitleOutputs); if (supportsEmbedded) { @@ -573,11 +547,11 @@ namespace MediaBrowser.Model.Dlna return codec; } - private bool ContainsSubtitleFormat(SubtitleProfile[] profiles, string[] formats) + private bool ContainsSubtitleFormat(SubtitleProfile[] profiles, SubtitleDeliveryMethod method, string[] formats) { foreach (SubtitleProfile profile in profiles) { - if (ListHelper.ContainsIgnoreCase(formats, profile.Format)) + if (method == profile.Method && ListHelper.ContainsIgnoreCase(formats, profile.Format)) { return true; } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 8bf9f2a04..563a2f19a 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,55 @@ namespace MediaBrowser.Model.Dlna return string.Format("Params={0}", string.Join(";", list.ToArray())); } + public List<SubtitleStreamInfo> GetExternalSubtitles(string baseUrl) + { + if (string.IsNullOrEmpty(baseUrl)) + { + throw new ArgumentNullException(baseUrl); + } + + List<SubtitleStreamInfo> list = new List<SubtitleStreamInfo>(); + + if (SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) + { + return list; + } + + if (!SubtitleStreamIndex.HasValue) + { + return list; + } + + // HLS will preserve timestamps so we can just grab the full subtitle stream + long startPositionTicks = StringHelper.EqualsIgnoreCase(Protocol, "hls") + ? 0 + : StartPositionTicks; + + string url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", + baseUrl, + ItemId, + MediaSourceId, + StringHelper.ToStringCultureInvariant(SubtitleStreamIndex.Value), + StringHelper.ToStringCultureInvariant(startPositionTicks), + SubtitleFormat); + + foreach (MediaStream stream in MediaSource.MediaStreams) + { + if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) + { + list.Add(new SubtitleStreamInfo + { + Url = url, + IsForced = stream.IsForced, + Language = stream.Language, + Name = stream.Language ?? "Unknown" + }); + } + } + + return list; + } + /// <summary> /// Returns the audio stream that will be used /// </summary> @@ -137,7 +193,7 @@ namespace MediaBrowser.Model.Dlna { foreach (MediaStream i in MediaSource.MediaStreams) { - if (i.Index == AudioStreamIndex.Value && i.Type == MediaStreamType.Audio) + if (i.Index == AudioStreamIndex.Value && i.Type == MediaStreamType.Audio) return i; } return null; @@ -437,16 +493,24 @@ 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, + /// <summary> + /// The HLS + /// </summary> + Hls = 3 + } + + public class SubtitleStreamInfo + { + public string Url { get; set; } + public string Language { get; set; } + public string Name { get; set; } + public bool IsForced { get; set; } } } diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs index ec38d96ae..189515835 100644 --- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs +++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs @@ -1,8 +1,16 @@ - +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; } + + [XmlAttribute("method")] + public SubtitleDeliveryMethod Method { 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 120e90bd2..fb0421678 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..ad2b50365 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; @@ -551,24 +550,28 @@ namespace MediaBrowser.Server.Implementations.LiveTv }; } - if (!string.IsNullOrEmpty(info.Path)) - { - item.Path = info.Path; - } - else if (!string.IsNullOrEmpty(info.Url)) - { - item.Path = info.Url; - } - isNew = true; } item.RecordingInfo = info; item.ServiceName = serviceName; + var originalPath = item.Path; + + if (!string.IsNullOrEmpty(info.Path)) + { + item.Path = info.Path; + } + else if (!string.IsNullOrEmpty(info.Url)) + { + item.Path = info.Url; + } + + var pathChanged = !string.Equals(originalPath, item.Path); + await item.RefreshMetadata(new MetadataRefreshOptions { - ForceSave = isNew + ForceSave = isNew || pathChanged }, cancellationToken); diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json index ad4ba7dc0..1c207b0a2 100644 --- a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json @@ -334,5 +334,6 @@ "OptionNewPlaylist": "New playlist...", "MessageAddedToPlaylistSuccess": "Ok", "ButtonViewSeriesRecording": "View series recording", - "ValueOriginalAirDate": "Original air date: {0}" + "ValueOriginalAirDate": "Original air date: {0}", + "ButtonRemoveFromPlaylist": "Remove from playlist" } diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs index 79b673283..6ab306c0b 100644 --- a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs +++ b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs @@ -190,9 +190,29 @@ namespace MediaBrowser.Server.Implementations.Playlists }, CancellationToken.None).ConfigureAwait(false); } - public Task RemoveFromPlaylist(string playlistId, IEnumerable<int> indeces) + public async Task RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds) { - throw new NotImplementedException(); + var playlist = _libraryManager.GetItemById(playlistId) as Playlist; + + if (playlist == null) + { + throw new ArgumentException("No Playlist exists with the supplied Id"); + } + + var children = playlist.LinkedChildren.ToList(); + + var idList = entryIds.ToList(); + + var removals = children.Where(i => idList.Contains(i.Id)); + + playlist.LinkedChildren = children.Except(removals) + .ToList(); + + await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + await playlist.RefreshMetadata(new MetadataRefreshOptions + { + ForceSave = true + }, CancellationToken.None).ConfigureAwait(false); } public Folder GetPlaylistsFolder(string userId) 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; } diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index ce34a1b30..78218d9ce 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>MediaBrowser.Common.Internal</id> - <version>3.0.422</version> + <version>3.0.423</version> <title>MediaBrowser.Common.Internal</title> <authors>Luke</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,7 +12,7 @@ <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description> <copyright>Copyright © Media Browser 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.422" /> + <dependency id="MediaBrowser.Common" version="3.0.423" /> <dependency id="NLog" version="3.1.0.0" /> <dependency id="SimpleInjector" version="2.5.2" /> <dependency id="sharpcompress" version="0.10.2" /> diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 5f09afa68..2a0fbc9af 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd"> <metadata> <id>MediaBrowser.Common</id> - <version>3.0.422</version> + <version>3.0.423</version> <title>MediaBrowser.Common</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> diff --git a/Nuget/MediaBrowser.Model.Signed.nuspec b/Nuget/MediaBrowser.Model.Signed.nuspec index df036cc98..2724c2088 100644 --- a/Nuget/MediaBrowser.Model.Signed.nuspec +++ b/Nuget/MediaBrowser.Model.Signed.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>MediaBrowser.Model.Signed</id> - <version>3.0.422</version> + <version>3.0.423</version> <title>MediaBrowser.Model - Signed Edition</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index ee54f248b..0f48a1976 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> <id>MediaBrowser.Server.Core</id> - <version>3.0.422</version> + <version>3.0.423</version> <title>Media Browser.Server.Core</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,7 +12,7 @@ <description>Contains core components required to build plugins for Media Browser Server.</description> <copyright>Copyright © Media Browser 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.422" /> + <dependency id="MediaBrowser.Common" version="3.0.423" /> </dependencies> </metadata> <files> |
