aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Api/Playback/BaseStreamingService.cs
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Api/Playback/BaseStreamingService.cs')
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs251
1 files changed, 179 insertions, 72 deletions
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
index 44bc95ed1..ce3691095 100644
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs
@@ -286,13 +286,25 @@ namespace MediaBrowser.Api.Playback
protected string GetH264Encoder(StreamState state)
{
- if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ // Only use alternative encoders for video files.
+ // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
+ // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+ if (state.VideoType == VideoType.VideoFile)
{
- // It's currently failing on live tv
- if (state.RunTimeTicks.HasValue)
+ if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{
return "h264_qsv";
}
+
+ if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ {
+ return "h264_nvenc";
+ }
+ if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase))
+ {
+ return "h264_omx";
+ }
}
return "libx264";
@@ -332,10 +344,10 @@ namespace MediaBrowser.Api.Playback
}
- // h264 (libnvenc)
- else if (string.Equals(videoCodec, "libnvenc", StringComparison.OrdinalIgnoreCase))
+ // h264 (h264_nvenc)
+ else if (string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
{
- param = "-preset high-performance";
+ param = "-preset default";
}
// webm
@@ -397,15 +409,18 @@ namespace MediaBrowser.Api.Playback
if (!string.IsNullOrEmpty(state.VideoRequest.Profile))
{
- param += " -profile:v " + state.VideoRequest.Profile;
+ if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase))
+ {
+ // not supported by h264_omx
+ param += " -profile:v " + state.VideoRequest.Profile;
+ }
}
if (!string.IsNullOrEmpty(state.VideoRequest.Level))
{
- var h264Encoder = GetH264Encoder(state);
-
- // h264_qsv and libnvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
- if (String.Equals(h264Encoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || String.Equals(h264Encoder, "libnvenc", StringComparison.OrdinalIgnoreCase))
+ // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
+ if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
{
switch (state.VideoRequest.Level)
{
@@ -441,13 +456,20 @@ namespace MediaBrowser.Api.Playback
break;
}
}
- else
+ else if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase))
{
param += " -level " + state.VideoRequest.Level;
}
}
- return "-pix_fmt yuv420p " + param;
+ if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ {
+ param = "-pix_fmt yuv420p " + param;
+ }
+
+ return param;
}
protected string GetAudioFilterParam(StreamState state, bool isHls)
@@ -460,7 +482,7 @@ namespace MediaBrowser.Api.Playback
// Boost volume to 200% when downsampling from 6ch to 2ch
if (channels.HasValue && channels.Value <= 2)
{
- if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
+ if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5 && !ApiEntryPoint.Instance.GetEncodingOptions().DownMixAudioBoost.Equals(1))
{
volParam = ",volume=" + ApiEntryPoint.Instance.GetEncodingOptions().DownMixAudioBoost.ToString(UsCulture);
}
@@ -563,14 +585,6 @@ namespace MediaBrowser.Api.Playback
filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(ih\\,{0})", maxHeightParam));
}
- if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
- {
- if (filters.Count > 1)
- {
- //filters[filters.Count - 1] += ":flags=fast_bilinear";
- }
- }
-
var output = string.Empty;
if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode)
@@ -614,7 +628,7 @@ namespace MediaBrowser.Api.Playback
if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
{
- var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.MediaSource.Protocol, CancellationToken.None).Result;
+ var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language, state.MediaSource.Protocol, CancellationToken.None).Result;
if (!string.IsNullOrEmpty(charenc))
{
@@ -712,15 +726,16 @@ namespace MediaBrowser.Api.Playback
inputChannels = null;
}
+ int? resultChannels = null;
var codec = outputAudioCodec ?? string.Empty;
if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
{
// wmav2 currently only supports two channel output
- return Math.Min(2, inputChannels ?? 2);
+ resultChannels = Math.Min(2, inputChannels ?? 2);
}
- if (request.MaxAudioChannels.HasValue)
+ else if (request.MaxAudioChannels.HasValue)
{
var channelLimit = codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1
? 2
@@ -732,10 +747,18 @@ namespace MediaBrowser.Api.Playback
}
// If we don't have any media info then limit it to 5 to prevent encoding errors due to asking for too many channels
- return Math.Min(request.MaxAudioChannels.Value, channelLimit);
+ resultChannels = Math.Min(request.MaxAudioChannels.Value, channelLimit);
}
- return request.AudioChannels;
+ if (resultChannels.HasValue && !string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ if (request.TranscodingMaxAudioChannels.HasValue)
+ {
+ resultChannels = Math.Min(request.TranscodingMaxAudioChannels.Value, resultChannels.Value);
+ }
+ }
+
+ return resultChannels ?? request.AudioChannels;
}
/// <summary>
@@ -821,9 +844,22 @@ namespace MediaBrowser.Api.Playback
/// <returns>System.String.</returns>
protected string GetVideoDecoder(StreamState state)
{
- if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ // Only use alternative encoders for video files.
+ // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
+ // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+ if (state.VideoType != VideoType.VideoFile)
+ {
+ return null;
+ }
+
+ if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec))
{
- if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec))
+ if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{
switch (state.MediaSource.VideoStream.Codec.ToLower())
{
@@ -831,6 +867,7 @@ namespace MediaBrowser.Api.Playback
case "h264":
if (MediaEncoder.SupportsDecoder("h264_qsv"))
{
+ // Seeing stalls and failures with decoding. Not worth it compared to encoding.
return "-c:v h264_qsv ";
}
break;
@@ -947,14 +984,24 @@ namespace MediaBrowser.Api.Playback
await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
- var transcodingId = Guid.NewGuid().ToString("N");
- var commandLineArgs = GetCommandLineArguments(outputPath, state, true);
-
- if (ApiEntryPoint.Instance.GetEncodingOptions().EnableDebugLogging)
+ if (state.VideoRequest != null && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
- commandLineArgs = "-loglevel debug " + commandLineArgs;
+ var auth = AuthorizationContext.GetAuthorizationInfo(Request);
+ if (!string.IsNullOrWhiteSpace(auth.UserId))
+ {
+ var user = UserManager.GetUserById(auth.UserId);
+ if (!user.Policy.EnableVideoPlaybackTranscoding)
+ {
+ ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
+
+ throw new ArgumentException("User does not have access to video transcoding");
+ }
+ }
}
+ var transcodingId = Guid.NewGuid().ToString("N");
+ var commandLineArgs = GetCommandLineArguments(outputPath, state, true);
+
var process = new Process
{
StartInfo = new ProcessStartInfo
@@ -963,7 +1010,7 @@ namespace MediaBrowser.Api.Playback
UseShellExecute = false,
// Must consume both stdout and stderr or deadlocks may occur
- RedirectStandardOutput = true,
+ //RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
@@ -995,7 +1042,17 @@ namespace MediaBrowser.Api.Playback
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
Logger.Info(commandLineLogMessage);
- var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, "transcode-" + Guid.NewGuid() + ".txt");
+ var logFilePrefix = "transcode";
+ if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ logFilePrefix = "directstream";
+ }
+ else if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ logFilePrefix = "remux";
+ }
+
+ var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
FileSystem.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
@@ -1020,13 +1077,13 @@ namespace MediaBrowser.Api.Playback
}
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
- process.BeginOutputReadLine();
+ //process.BeginOutputReadLine();
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
- StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream);
+ Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream));
// Wait for the file to exist before proceeeding
- while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
+ while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
{
await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
}
@@ -1066,7 +1123,7 @@ namespace MediaBrowser.Api.Playback
return true;
}
- private async void StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
+ private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
{
try
{
@@ -1172,7 +1229,7 @@ namespace MediaBrowser.Api.Playback
}
}
- private int? GetVideoBitrateParamValue(VideoStreamRequest request, MediaStream videoStream)
+ private int? GetVideoBitrateParamValue(VideoStreamRequest request, MediaStream videoStream, string outputVideoCodec)
{
var bitrate = request.VideoBitRate;
@@ -1197,6 +1254,18 @@ namespace MediaBrowser.Api.Playback
}
}
+ if (bitrate.HasValue)
+ {
+ var inputVideoCodec = videoStream == null ? null : videoStream.Codec;
+ bitrate = ResolutionNormalizer.ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec);
+
+ // If a max bitrate was requested, don't let the scaled bitrate exceed it
+ if (request.VideoBitRate.HasValue)
+ {
+ bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value);
+ }
+ }
+
return bitrate;
}
@@ -1452,10 +1521,7 @@ namespace MediaBrowser.Api.Playback
}
else if (i == 19)
{
- if (videoRequest != null)
- {
- videoRequest.Cabac = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
+ // cabac no longer used
}
else if (i == 20)
{
@@ -1482,6 +1548,13 @@ namespace MediaBrowser.Api.Playback
}
else if (i == 25)
{
+ if (videoRequest != null)
+ {
+ videoRequest.ForceLiveStream = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+ else if (i == 26)
+ {
if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
{
SubtitleDeliveryMethod method;
@@ -1491,6 +1564,17 @@ namespace MediaBrowser.Api.Playback
}
}
}
+ else if (i == 27)
+ {
+ request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture);
+ }
+ else if (i == 28)
+ {
+ if (videoRequest != null)
+ {
+ videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+ }
+ }
}
}
@@ -1582,7 +1666,8 @@ namespace MediaBrowser.Api.Playback
var state = new StreamState(MediaSourceManager, Logger)
{
Request = request,
- RequestedUrl = url
+ RequestedUrl = url,
+ UserAgent = Request.UserAgent
};
//if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
@@ -1595,7 +1680,8 @@ namespace MediaBrowser.Api.Playback
if (!string.IsNullOrWhiteSpace(request.AudioCodec))
{
state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
- state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault();
+ state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToAudioCodec(i))
+ ?? state.SupportedAudioCodecs.FirstOrDefault();
}
var item = LibraryManager.GetItemById(request.Id);
@@ -1649,14 +1735,14 @@ namespace MediaBrowser.Api.Playback
if (videoRequest != null)
{
state.OutputVideoCodec = state.VideoRequest.VideoCodec;
- state.OutputVideoBitrate = GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream);
+ state.OutputVideoBitrate = GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
if (state.OutputVideoBitrate.HasValue)
{
var resolution = ResolutionNormalizer.Normalize(
- state.VideoStream == null ? (int?)null : state.VideoStream.BitRate,
- state.OutputVideoBitrate.Value,
- state.VideoStream == null ? null : state.VideoStream.Codec,
+ state.VideoStream == null ? (int?)null : state.VideoStream.BitRate,
+ state.OutputVideoBitrate.Value,
+ state.VideoStream == null ? null : state.VideoStream.Codec,
state.OutputVideoCodec,
videoRequest.MaxWidth,
videoRequest.MaxHeight);
@@ -1680,12 +1766,25 @@ namespace MediaBrowser.Api.Playback
private void TryStreamCopy(StreamState state, VideoStreamRequest videoRequest)
{
- if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
+ if (state.VideoStream != null && CanStreamCopyVideo(state))
{
state.OutputVideoCodec = "copy";
}
+ else
+ {
+ // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
+ var auth = AuthorizationContext.GetAuthorizationInfo(Request);
+ if (!string.IsNullOrWhiteSpace(auth.UserId))
+ {
+ var user = UserManager.GetUserById(auth.UserId);
+ if (!user.Policy.EnableVideoPlaybackTranscoding)
+ {
+ state.OutputVideoCodec = "copy";
+ }
+ }
+ }
- if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
+ if (state.AudioStream != null && CanStreamCopyAudio(state, state.SupportedAudioCodecs))
{
state.OutputAudioCodec = "copy";
}
@@ -1773,8 +1872,11 @@ namespace MediaBrowser.Api.Playback
state.MediaSource = mediaSource;
}
- protected virtual bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream)
+ protected virtual bool CanStreamCopyVideo(StreamState state)
{
+ var request = state.VideoRequest;
+ var videoStream = state.VideoStream;
+
if (videoStream.IsInterlaced)
{
return false;
@@ -1784,7 +1886,7 @@ namespace MediaBrowser.Api.Playback
{
return false;
}
-
+
// Can't stream copy if we're burning in subtitles
if (request.SubtitleStreamIndex.HasValue)
{
@@ -1794,6 +1896,15 @@ namespace MediaBrowser.Api.Playback
}
}
+ if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ {
+ if (videoStream.IsAVC.HasValue && !videoStream.IsAVC.Value)
+ {
+ Logger.Debug("Cannot stream copy video. Stream is marked as not AVC");
+ return false;
+ }
+ }
+
// Source and target codecs must match
if (!string.Equals(request.VideoCodec, videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
@@ -1805,10 +1916,10 @@ namespace MediaBrowser.Api.Playback
{
if (string.IsNullOrEmpty(videoStream.Profile))
{
- return false;
+ //return false;
}
- if (!string.Equals(request.Profile, videoStream.Profile, StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(videoStream.Profile) && !string.Equals(request.Profile, videoStream.Profile, StringComparison.OrdinalIgnoreCase))
{
var currentScore = GetVideoProfileScore(videoStream.Profile);
var requestedScore = GetVideoProfileScore(request.Profile);
@@ -1884,24 +1995,16 @@ namespace MediaBrowser.Api.Playback
{
if (!videoStream.Level.HasValue)
{
- return false;
+ //return false;
}
- if (videoStream.Level.Value > requestLevel)
+ if (videoStream.Level.HasValue && videoStream.Level.Value > requestLevel)
{
return false;
}
}
}
- if (request.Cabac.HasValue && request.Cabac.Value)
- {
- if (videoStream.IsCabac.HasValue && !videoStream.IsCabac.Value)
- {
- return false;
- }
- }
-
return request.EnableAutoStreamCopy;
}
@@ -1921,8 +2024,11 @@ namespace MediaBrowser.Api.Playback
return Array.FindIndex(list.ToArray(), t => string.Equals(t, profile, StringComparison.OrdinalIgnoreCase));
}
- protected virtual bool CanStreamCopyAudio(VideoStreamRequest request, MediaStream audioStream, List<string> supportedAudioCodecs)
+ protected virtual bool CanStreamCopyAudio(StreamState state, List<string> supportedAudioCodecs)
{
+ var request = state.VideoRequest;
+ var audioStream = state.AudioStream;
+
// Source and target codecs must match
if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase))
{
@@ -2028,7 +2134,6 @@ namespace MediaBrowser.Api.Playback
state.TargetPacketLength,
state.TargetTimestamp,
state.IsTargetAnamorphic,
- state.IsTargetCabac,
state.TargetRefFrames,
state.TargetVideoStreamCount,
state.TargetAudioStreamCount,
@@ -2054,6 +2159,8 @@ namespace MediaBrowser.Api.Playback
if (state.VideoRequest != null)
{
state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
+ state.VideoRequest.ForceLiveStream = transcodingProfile.ForceLiveStream;
+ state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
}
}
}
@@ -2131,7 +2238,6 @@ namespace MediaBrowser.Api.Playback
state.TargetPacketLength,
state.TranscodeSeekInfo,
state.IsTargetAnamorphic,
- state.IsTargetCabac,
state.TargetRefFrames,
state.TargetVideoStreamCount,
state.TargetAudioStreamCount,
@@ -2218,12 +2324,13 @@ namespace MediaBrowser.Api.Playback
if (state.VideoRequest != null)
{
+ // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking
if (string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase) && state.VideoRequest.CopyTimestamps)
{
- inputModifier += " -noaccurate_seek";
+ //inputModifier += " -noaccurate_seek";
}
}
-
+
return inputModifier;
}