fixes #887 - Support ttml subtitle output
This commit is contained in:
parent
7e25c857a5
commit
3ba6364f25
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -21,6 +21,11 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
SoundtrackIds = new List<Guid>();
|
||||
}
|
||||
|
||||
public override bool SupportsAddingToPlaylist
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public MusicArtist MusicArtist
|
||||
{
|
||||
|
|
|
@ -26,6 +26,11 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
}
|
||||
}
|
||||
|
||||
public override bool SupportsAddingToPlaylist
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
protected override IEnumerable<BaseItem> ActualChildren
|
||||
{
|
||||
get
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -29,6 +29,11 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
}
|
||||
}
|
||||
|
||||
public override bool SupportsAddingToPlaylist
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public override bool IsPreSorted
|
||||
{
|
||||
|
|
|
@ -39,6 +39,11 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
DisplaySpecialsWithSeasons = true;
|
||||
}
|
||||
|
||||
public override bool SupportsAddingToPlaylist
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public override bool IsPreSorted
|
||||
{
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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('.');
|
||||
}
|
||||
|
||||
|
|
|
@ -155,6 +155,13 @@ namespace MediaBrowser.Dlna.Profiles
|
|||
}
|
||||
};
|
||||
|
||||
ExternalSubtitleProfiles = new[]
|
||||
{
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "ttml"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,9 +69,7 @@
|
|||
</CodecProfiles>
|
||||
<ResponseProfiles />
|
||||
<SoftSubtitleProfiles>
|
||||
<SubtitleProfile>
|
||||
<Format>srt</Format>
|
||||
</SubtitleProfile>
|
||||
<SubtitleProfile format="srt" />
|
||||
</SoftSubtitleProfiles>
|
||||
<ExternalSubtitleProfiles />
|
||||
</Profile>
|
|
@ -107,9 +107,7 @@
|
|||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SoftSubtitleProfiles>
|
||||
<SubtitleProfile>
|
||||
<Format>smi</Format>
|
||||
</SubtitleProfile>
|
||||
<SubtitleProfile format="smi" />
|
||||
</SoftSubtitleProfiles>
|
||||
<ExternalSubtitleProfiles />
|
||||
</Profile>
|
|
@ -63,5 +63,7 @@
|
|||
</CodecProfiles>
|
||||
<ResponseProfiles />
|
||||
<SoftSubtitleProfiles />
|
||||
<ExternalSubtitleProfiles />
|
||||
<ExternalSubtitleProfiles>
|
||||
<SubtitleProfile format="ttml" />
|
||||
</ExternalSubtitleProfiles>
|
||||
</Profile>
|
|
@ -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())
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 + ">");
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
59
MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
Normal file
59
MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -525,6 +525,12 @@ namespace MediaBrowser.Model.Dto
|
|||
return IsType(type.Name);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
@ -631,12 +637,6 @@ namespace MediaBrowser.Model.Dto
|
|||
/// <value>The type of the media.</value>
|
||||
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>
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
public const string VTT = "vtt";
|
||||
public const string SUB = "sub";
|
||||
public const string SMI = "smi";
|
||||
public const string TTML = "ttml";
|
||||
}
|
||||
}
|
|
@ -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" />
|
||||
|
|
47
MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
Normal file
47
MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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; }
|
||||
|
|
Loading…
Reference in New Issue
Block a user