|
@@ -1,5 +1,6 @@
|
|
|
using MediaBrowser.Common.Extensions;
|
|
|
using MediaBrowser.Common.IO;
|
|
|
+using MediaBrowser.Common.Net;
|
|
|
using MediaBrowser.Controller.Channels;
|
|
|
using MediaBrowser.Controller.Configuration;
|
|
|
using MediaBrowser.Controller.Dlna;
|
|
@@ -13,6 +14,7 @@ using MediaBrowser.Controller.Persistence;
|
|
|
using MediaBrowser.Model.Configuration;
|
|
|
using MediaBrowser.Model.Dlna;
|
|
|
using MediaBrowser.Model.Drawing;
|
|
|
+using MediaBrowser.Model.Dto;
|
|
|
using MediaBrowser.Model.Entities;
|
|
|
using MediaBrowser.Model.IO;
|
|
|
using MediaBrowser.Model.Library;
|
|
@@ -72,20 +74,14 @@ namespace MediaBrowser.Api.Playback
|
|
|
protected ILiveTvManager LiveTvManager { get; private set; }
|
|
|
protected IDlnaManager DlnaManager { get; private set; }
|
|
|
protected IChannelManager ChannelManager { get; private set; }
|
|
|
+ protected IHttpClient HttpClient { get; private set; }
|
|
|
|
|
|
/// <summary>
|
|
|
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
|
|
|
/// </summary>
|
|
|
- /// <param name="serverConfig">The server configuration.</param>
|
|
|
- /// <param name="userManager">The user manager.</param>
|
|
|
- /// <param name="libraryManager">The library manager.</param>
|
|
|
- /// <param name="isoManager">The iso manager.</param>
|
|
|
- /// <param name="mediaEncoder">The media encoder.</param>
|
|
|
- /// <param name="dtoService">The dto service.</param>
|
|
|
- /// <param name="fileSystem">The file system.</param>
|
|
|
- /// <param name="itemRepository">The item repository.</param>
|
|
|
- protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IEncodingManager encodingManager, IDlnaManager dlnaManager, IChannelManager channelManager)
|
|
|
+ protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IEncodingManager encodingManager, IDlnaManager dlnaManager, IChannelManager channelManager, IHttpClient httpClient)
|
|
|
{
|
|
|
+ HttpClient = httpClient;
|
|
|
ChannelManager = channelManager;
|
|
|
DlnaManager = dlnaManager;
|
|
|
EncodingManager = encodingManager;
|
|
@@ -483,8 +479,12 @@ namespace MediaBrowser.Api.Playback
|
|
|
/// <param name="state">The state.</param>
|
|
|
/// <param name="outputVideoCodec">The output video codec.</param>
|
|
|
/// <param name="performTextSubtitleConversion">if set to <c>true</c> [perform text subtitle conversion].</param>
|
|
|
+ /// <param name="cancellationToken">The cancellation token.</param>
|
|
|
/// <returns>System.String.</returns>
|
|
|
- protected string GetOutputSizeParam(StreamState state, string outputVideoCodec, bool performTextSubtitleConversion)
|
|
|
+ protected string GetOutputSizeParam(StreamState state,
|
|
|
+ string outputVideoCodec,
|
|
|
+ bool performTextSubtitleConversion,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
{
|
|
|
// http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
|
|
|
|
|
@@ -496,7 +496,7 @@ namespace MediaBrowser.Api.Playback
|
|
|
|
|
|
if (state.SubtitleStream != null && !state.SubtitleStream.IsGraphicalSubtitleStream)
|
|
|
{
|
|
|
- assSubtitleParam = GetTextSubtitleParam(state, performTextSubtitleConversion);
|
|
|
+ assSubtitleParam = GetTextSubtitleParam(state, performTextSubtitleConversion, cancellationToken);
|
|
|
copyTsParam = " -copyts";
|
|
|
}
|
|
|
|
|
@@ -592,11 +592,15 @@ namespace MediaBrowser.Api.Playback
|
|
|
/// </summary>
|
|
|
/// <param name="state">The state.</param>
|
|
|
/// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
|
|
|
+ /// <param name="cancellationToken">The cancellation token.</param>
|
|
|
/// <returns>System.String.</returns>
|
|
|
- protected string GetTextSubtitleParam(StreamState state, bool performConversion)
|
|
|
+ protected string GetTextSubtitleParam(StreamState state,
|
|
|
+ bool performConversion,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
{
|
|
|
- var path = state.SubtitleStream.IsExternal ? GetConvertedAssPath(state.MediaPath, state.SubtitleStream, performConversion) :
|
|
|
- GetExtractedAssPath(state, performConversion);
|
|
|
+ var path = state.SubtitleStream.IsExternal ?
|
|
|
+ GetConvertedAssPath(state.SubtitleStream, performConversion, cancellationToken) :
|
|
|
+ GetExtractedAssPath(state, performConversion, cancellationToken);
|
|
|
|
|
|
if (string.IsNullOrEmpty(path))
|
|
|
{
|
|
@@ -615,8 +619,11 @@ namespace MediaBrowser.Api.Playback
|
|
|
/// </summary>
|
|
|
/// <param name="state">The state.</param>
|
|
|
/// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
|
|
|
+ /// <param name="cancellationToken">The cancellation token.</param>
|
|
|
/// <returns>System.String.</returns>
|
|
|
- private string GetExtractedAssPath(StreamState state, bool performConversion)
|
|
|
+ private string GetExtractedAssPath(StreamState state,
|
|
|
+ bool performConversion,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
{
|
|
|
var path = EncodingManager.GetSubtitleCachePath(state.MediaPath, state.SubtitleStream.Index, ".ass");
|
|
|
|
|
@@ -636,7 +643,7 @@ namespace MediaBrowser.Api.Playback
|
|
|
// See https://lists.ffmpeg.org/pipermail/ffmpeg-cvslog/2013-April/063616.html
|
|
|
var isAssSubtitle = string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
- var task = MediaEncoder.ExtractTextSubtitle(inputPath, type, state.SubtitleStream.Index, isAssSubtitle, path, CancellationToken.None);
|
|
|
+ var task = MediaEncoder.ExtractTextSubtitle(inputPath, type, state.SubtitleStream.Index, isAssSubtitle, path, cancellationToken);
|
|
|
|
|
|
Task.WaitAll(task);
|
|
|
}
|
|
@@ -652,11 +659,13 @@ namespace MediaBrowser.Api.Playback
|
|
|
/// <summary>
|
|
|
/// Gets the converted ass path.
|
|
|
/// </summary>
|
|
|
- /// <param name="mediaPath">The media path.</param>
|
|
|
/// <param name="subtitleStream">The subtitle stream.</param>
|
|
|
/// <param name="performConversion">if set to <c>true</c> [perform conversion].</param>
|
|
|
+ /// <param name="cancellationToken">The cancellation token.</param>
|
|
|
/// <returns>System.String.</returns>
|
|
|
- private string GetConvertedAssPath(string mediaPath, MediaStream subtitleStream, bool performConversion)
|
|
|
+ private string GetConvertedAssPath(MediaStream subtitleStream,
|
|
|
+ bool performConversion,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
{
|
|
|
var path = EncodingManager.GetSubtitleCachePath(subtitleStream.Path, ".ass");
|
|
|
|
|
@@ -668,7 +677,7 @@ namespace MediaBrowser.Api.Playback
|
|
|
|
|
|
Directory.CreateDirectory(parentPath);
|
|
|
|
|
|
- var task = MediaEncoder.ConvertTextSubtitleToAss(subtitleStream.Path, path, subtitleStream.Language, CancellationToken.None);
|
|
|
+ var task = MediaEncoder.ConvertTextSubtitleToAss(subtitleStream.Path, path, subtitleStream.Language, cancellationToken);
|
|
|
|
|
|
Task.WaitAll(task);
|
|
|
}
|
|
@@ -696,7 +705,7 @@ namespace MediaBrowser.Api.Playback
|
|
|
// Add resolution params, if specified
|
|
|
if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
|
|
|
{
|
|
|
- outputSizeParam = GetOutputSizeParam(state, outputVideoCodec, false).TrimEnd('"');
|
|
|
+ outputSizeParam = GetOutputSizeParam(state, outputVideoCodec, false, CancellationToken.None).TrimEnd('"');
|
|
|
outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
|
|
|
}
|
|
|
|
|
@@ -842,7 +851,7 @@ namespace MediaBrowser.Api.Playback
|
|
|
/// <returns>System.String.</returns>
|
|
|
protected string GetInputArgument(StreamState state)
|
|
|
{
|
|
|
- var type = InputType.File;
|
|
|
+ var type = state.IsRemote ? InputType.Url : InputType.File;
|
|
|
|
|
|
var inputPath = new[] { state.MediaPath };
|
|
|
|
|
@@ -862,8 +871,10 @@ namespace MediaBrowser.Api.Playback
|
|
|
/// </summary>
|
|
|
/// <param name="state">The state.</param>
|
|
|
/// <param name="outputPath">The output path.</param>
|
|
|
+ /// <param name="cancellationTokenSource">The cancellation token source.</param>
|
|
|
/// <returns>Task.</returns>
|
|
|
- protected async Task StartFfMpeg(StreamState state, string outputPath)
|
|
|
+ /// <exception cref="System.InvalidOperationException">ffmpeg was not found at + MediaEncoder.EncoderPath</exception>
|
|
|
+ protected async Task StartFfMpeg(StreamState state, string outputPath, CancellationTokenSource cancellationTokenSource)
|
|
|
{
|
|
|
if (!File.Exists(MediaEncoder.EncoderPath))
|
|
|
{
|
|
@@ -874,7 +885,7 @@ namespace MediaBrowser.Api.Playback
|
|
|
|
|
|
if (state.IsInputVideo && state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
|
|
|
{
|
|
|
- state.IsoMount = await IsoManager.Mount(state.MediaPath, CancellationToken.None).ConfigureAwait(false);
|
|
|
+ state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
|
}
|
|
|
|
|
|
var commandLineArgs = GetCommandLineArguments(outputPath, state, true);
|
|
@@ -906,7 +917,7 @@ namespace MediaBrowser.Api.Playback
|
|
|
EnableRaisingEvents = true
|
|
|
};
|
|
|
|
|
|
- ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, state.Request.StartTimeTicks, state.MediaPath, state.Request.DeviceId);
|
|
|
+ ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, state.Request.StartTimeTicks, state.MediaPath, state.Request.DeviceId, cancellationTokenSource);
|
|
|
|
|
|
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
|
|
|
Logger.Info(commandLineLogMessage);
|
|
@@ -918,7 +929,7 @@ namespace MediaBrowser.Api.Playback
|
|
|
state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
|
|
|
|
|
|
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
|
|
- await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length).ConfigureAwait(false);
|
|
|
+ await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
|
|
|
|
process.Exited += (sender, args) => OnFfMpegProcessExited(process, state);
|
|
|
|
|
@@ -946,19 +957,19 @@ namespace MediaBrowser.Api.Playback
|
|
|
// Wait for the file to exist before proceeeding
|
|
|
while (!File.Exists(outputPath))
|
|
|
{
|
|
|
- await Task.Delay(100).ConfigureAwait(false);
|
|
|
+ await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
|
}
|
|
|
|
|
|
// Allow a small amount of time to buffer a little
|
|
|
if (state.IsInputVideo)
|
|
|
{
|
|
|
- await Task.Delay(500).ConfigureAwait(false);
|
|
|
+ await Task.Delay(500, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
|
}
|
|
|
|
|
|
// This is arbitrary, but add a little buffer time when internet streaming
|
|
|
if (state.IsRemote)
|
|
|
{
|
|
|
- await Task.Delay(3000).ConfigureAwait(false);
|
|
|
+ await Task.Delay(3000, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -1050,13 +1061,19 @@ namespace MediaBrowser.Api.Playback
|
|
|
/// <summary>
|
|
|
/// Gets the user agent param.
|
|
|
/// </summary>
|
|
|
- /// <param name="path">The path.</param>
|
|
|
+ /// <param name="state">The state.</param>
|
|
|
/// <returns>System.String.</returns>
|
|
|
- private string GetUserAgentParam(string path)
|
|
|
+ private string GetUserAgentParam(StreamState state)
|
|
|
{
|
|
|
- var useragent = GetUserAgent(path);
|
|
|
+ string useragent;
|
|
|
+ state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
|
|
|
|
|
|
- if (!string.IsNullOrEmpty(useragent))
|
|
|
+ if (string.IsNullOrWhiteSpace(useragent))
|
|
|
+ {
|
|
|
+ useragent = GetUserAgent(state.MediaPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!string.IsNullOrWhiteSpace(useragent))
|
|
|
{
|
|
|
return "-user-agent \"" + useragent + "\"";
|
|
|
}
|
|
@@ -1337,9 +1354,7 @@ namespace MediaBrowser.Api.Playback
|
|
|
state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault();
|
|
|
}
|
|
|
|
|
|
- var item = string.IsNullOrEmpty(request.MediaSourceId) ?
|
|
|
- LibraryManager.GetItemById(request.Id) :
|
|
|
- LibraryManager.GetItemById(request.MediaSourceId);
|
|
|
+ var item = LibraryManager.GetItemById(request.Id);
|
|
|
|
|
|
if (user != null && item.GetPlayAccess(user) != PlayAccess.Full)
|
|
|
{
|
|
@@ -1427,19 +1442,24 @@ namespace MediaBrowser.Api.Playback
|
|
|
}
|
|
|
else if (item is IChannelMediaItem)
|
|
|
{
|
|
|
- var source = await GetChannelMediaInfo(request.Id, CancellationToken.None).ConfigureAwait(false);
|
|
|
+ var source = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false);
|
|
|
state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
|
|
- state.IsRemote = source.IsRemote;
|
|
|
+ state.IsRemote = source.LocationType == LocationType.Remote;
|
|
|
state.MediaPath = source.Path;
|
|
|
state.RunTimeTicks = item.RunTimeTicks;
|
|
|
- mediaStreams = GetMediaStreams(source).ToList();
|
|
|
+ state.RemoteHttpHeaders = source.RequiredHttpHeaders;
|
|
|
+ mediaStreams = source.MediaStreams;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- state.MediaPath = item.Path;
|
|
|
- state.IsRemote = item.LocationType == LocationType.Remote;
|
|
|
+ var mediaSource = string.IsNullOrWhiteSpace(request.MediaSourceId)
|
|
|
+ ? item
|
|
|
+ : LibraryManager.GetItemById(request.MediaSourceId);
|
|
|
+
|
|
|
+ state.MediaPath = mediaSource.Path;
|
|
|
+ state.IsRemote = mediaSource.LocationType == LocationType.Remote;
|
|
|
|
|
|
- var video = item as Video;
|
|
|
+ var video = mediaSource as Video;
|
|
|
|
|
|
if (video != null)
|
|
|
{
|
|
@@ -1461,20 +1481,20 @@ namespace MediaBrowser.Api.Playback
|
|
|
state.InputContainer = video.Container;
|
|
|
}
|
|
|
|
|
|
- var audio = item as Audio;
|
|
|
+ var audio = mediaSource as Audio;
|
|
|
if (audio != null)
|
|
|
{
|
|
|
state.InputContainer = audio.Container;
|
|
|
}
|
|
|
|
|
|
- state.RunTimeTicks = item.RunTimeTicks;
|
|
|
+ state.RunTimeTicks = mediaSource.RunTimeTicks;
|
|
|
}
|
|
|
|
|
|
var videoRequest = request as VideoStreamRequest;
|
|
|
|
|
|
mediaStreams = mediaStreams ?? ItemRepository.GetMediaStreams(new MediaStreamQuery
|
|
|
{
|
|
|
- ItemId = item.Id
|
|
|
+ ItemId = new Guid(string.IsNullOrWhiteSpace(request.MediaSourceId) ? request.Id : request.MediaSourceId)
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
@@ -1545,65 +1565,32 @@ namespace MediaBrowser.Api.Playback
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private IEnumerable<MediaStream> GetMediaStreams(ChannelMediaInfo info)
|
|
|
- {
|
|
|
- var list = new List<MediaStream>();
|
|
|
-
|
|
|
- if (!string.IsNullOrWhiteSpace(info.VideoCodec) &&
|
|
|
- !string.IsNullOrWhiteSpace(info.AudioCodec))
|
|
|
- {
|
|
|
- list.Add(new MediaStream
|
|
|
- {
|
|
|
- Type = MediaStreamType.Video,
|
|
|
- Width = info.Width,
|
|
|
- RealFrameRate = info.Framerate,
|
|
|
- Profile = info.VideoProfile,
|
|
|
- Level = info.VideoLevel,
|
|
|
- Index = -1,
|
|
|
- Height = info.Height,
|
|
|
- Codec = info.VideoCodec,
|
|
|
- BitRate = info.VideoBitrate,
|
|
|
- AverageFrameRate = info.Framerate
|
|
|
- });
|
|
|
-
|
|
|
- list.Add(new MediaStream
|
|
|
- {
|
|
|
- Type = MediaStreamType.Audio,
|
|
|
- Index = -1,
|
|
|
- Codec = info.AudioCodec,
|
|
|
- BitRate = info.AudioBitrate,
|
|
|
- Channels = info.AudioChannels,
|
|
|
- SampleRate = info.AudioSampleRate
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- return list;
|
|
|
- }
|
|
|
-
|
|
|
- private async Task<ChannelMediaInfo> GetChannelMediaInfo(string id, CancellationToken cancellationToken)
|
|
|
+ private async Task<MediaSourceInfo> GetChannelMediaInfo(string id,
|
|
|
+ string mediaSourceId,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
{
|
|
|
var channelMediaSources = await ChannelManager.GetChannelItemMediaSources(id, cancellationToken)
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
var list = channelMediaSources.ToList();
|
|
|
|
|
|
- var preferredWidth = ServerConfigurationManager.Configuration.ChannelOptions.PreferredStreamingWidth;
|
|
|
-
|
|
|
- if (preferredWidth.HasValue)
|
|
|
+ if (!string.IsNullOrWhiteSpace(mediaSourceId))
|
|
|
{
|
|
|
- var val = preferredWidth.Value;
|
|
|
+ var source = list
|
|
|
+ .FirstOrDefault(i => string.Equals(mediaSourceId, i.Id));
|
|
|
|
|
|
- return list
|
|
|
- .OrderBy(i => Math.Abs(i.Width ?? 0 - val))
|
|
|
- .ThenByDescending(i => i.Width ?? 0)
|
|
|
- .ThenBy(list.IndexOf)
|
|
|
- .First();
|
|
|
+ if (source != null)
|
|
|
+ {
|
|
|
+ return source;
|
|
|
+ }
|
|
|
+
|
|
|
+ Logger.Warn("Invalid channel MediaSourceId requested, defaulting to first. Item: {0}. Requested MediaSourceId: {1}.",
|
|
|
+ id,
|
|
|
+ mediaSourceId
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
- return list
|
|
|
- .OrderByDescending(i => i.Width ?? 0)
|
|
|
- .ThenBy(list.IndexOf)
|
|
|
- .First();
|
|
|
+ return list.First();
|
|
|
}
|
|
|
|
|
|
private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream)
|
|
@@ -1932,7 +1919,11 @@ namespace MediaBrowser.Api.Playback
|
|
|
inputModifier += " " + probeSize;
|
|
|
inputModifier = inputModifier.Trim();
|
|
|
|
|
|
- inputModifier += " " + GetUserAgentParam(state.MediaPath);
|
|
|
+ if (state.IsRemote)
|
|
|
+ {
|
|
|
+ inputModifier += " " + GetUserAgentParam(state);
|
|
|
+ }
|
|
|
+
|
|
|
inputModifier = inputModifier.Trim();
|
|
|
|
|
|
inputModifier += " " + GetFastSeekCommandLineParameter(state.Request);
|