diff options
Diffstat (limited to 'MediaBrowser.Api/Playback/BaseStreamingService.cs')
| -rw-r--r-- | MediaBrowser.Api/Playback/BaseStreamingService.cs | 251 |
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; } |
