diff options
Diffstat (limited to 'MediaBrowser.Api/Playback/BaseStreamingService.cs')
| -rw-r--r-- | MediaBrowser.Api/Playback/BaseStreamingService.cs | 362 |
1 files changed, 291 insertions, 71 deletions
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index ce3691095..4af564a5a 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -22,6 +22,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; namespace MediaBrowser.Api.Playback { @@ -69,6 +71,9 @@ namespace MediaBrowser.Api.Playback protected IZipClient ZipClient { get; private set; } protected IJsonSerializer JsonSerializer { get; private set; } + public static IServerApplicationHost AppHost; + public static IHttpClient HttpClient; + /// <summary> /// Initializes a new instance of the <see cref="BaseStreamingService" /> class. /// </summary> @@ -286,28 +291,46 @@ namespace MediaBrowser.Api.Playback protected string GetH264Encoder(StreamState state) { + var defaultEncoder = "libx264"; + // 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) { - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) || - string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); + var hwType = encodingOptions.HardwareAccelerationType; + + if (string.Equals(hwType, "qsv", StringComparison.OrdinalIgnoreCase) || + string.Equals(hwType, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { - return "h264_qsv"; + return GetAvailableEncoder("h264_qsv", defaultEncoder); } - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(hwType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + return GetAvailableEncoder("h264_nvenc", defaultEncoder); + } + if (string.Equals(hwType, "h264_omx", StringComparison.OrdinalIgnoreCase)) { - return "h264_nvenc"; + return GetAvailableEncoder("h264_omx", defaultEncoder); } - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(encodingOptions.VaapiDevice)) { - return "h264_omx"; + return GetAvailableEncoder("h264_vaapi", defaultEncoder); } } - return "libx264"; + return defaultEncoder; + } + + private string GetAvailableEncoder(string preferredEncoder, string defaultEncoder) + { + if (MediaEncoder.SupportsEncoder(preferredEncoder)) + { + return preferredEncoder; + } + return defaultEncoder; } /// <summary> @@ -409,7 +432,8 @@ namespace MediaBrowser.Api.Playback if (!string.IsNullOrEmpty(state.VideoRequest.Profile)) { - if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { // not supported by h264_omx param += " -profile:v " + state.VideoRequest.Profile; @@ -464,7 +488,8 @@ namespace MediaBrowser.Api.Playback if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase) && !string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + !string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { param = "-pix_fmt yuv420p " + param; } @@ -530,59 +555,97 @@ namespace MediaBrowser.Api.Playback var filters = new List<string>(); - if (state.DeInterlace) + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + filters.Add("format=nv12|vaapi"); + filters.Add("hwupload"); + } + else if (state.DeInterlace && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { filters.Add("yadif=0:-1:0"); } - // If fixed dimensions were supplied - if (request.Width.HasValue && request.Height.HasValue) + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { - var widthParam = request.Width.Value.ToString(UsCulture); - var heightParam = request.Height.Value.ToString(UsCulture); + // Work around vaapi's reduced scaling features + var scaler = "scale_vaapi"; - filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam)); - } + // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions + // (outputWidth, outputHeight). The user may request precise output dimensions or maximum + // output dimensions. Output dimensions are guaranteed to be even. + decimal inputWidth = Convert.ToDecimal(state.VideoStream.Width); + decimal inputHeight = Convert.ToDecimal(state.VideoStream.Height); + decimal outputWidth = request.Width.HasValue ? Convert.ToDecimal(request.Width.Value) : inputWidth; + decimal outputHeight = request.Height.HasValue ? Convert.ToDecimal(request.Height.Value) : inputHeight; + decimal maximumWidth = request.MaxWidth.HasValue ? Convert.ToDecimal(request.MaxWidth.Value) : outputWidth; + decimal maximumHeight = request.MaxHeight.HasValue ? Convert.ToDecimal(request.MaxHeight.Value) : outputHeight; - // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size - else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue) - { - var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); - var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); + if (outputWidth > maximumWidth || outputHeight > maximumHeight) + { + var scale = Math.Min(maximumWidth / outputWidth, maximumHeight / outputHeight); + outputWidth = Math.Min(maximumWidth, Math.Truncate(outputWidth * scale)); + outputHeight = Math.Min(maximumHeight, Math.Truncate(outputHeight * scale)); + } - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); - } + outputWidth = 2 * Math.Truncate(outputWidth / 2); + outputHeight = 2 * Math.Truncate(outputHeight / 2); - // If a fixed width was requested - else if (request.Width.HasValue) + if (outputWidth != inputWidth || outputHeight != inputHeight) + { + filters.Add(string.Format("{0}=w={1}:h={2}", scaler, outputWidth.ToString(UsCulture), outputHeight.ToString(UsCulture))); + } + } + else { - var widthParam = request.Width.Value.ToString(UsCulture); + // If fixed dimensions were supplied + if (request.Width.HasValue && request.Height.HasValue) + { + var widthParam = request.Width.Value.ToString(UsCulture); + var heightParam = request.Height.Value.ToString(UsCulture); - filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam)); - } + filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam)); + } - // If a fixed height was requested - else if (request.Height.HasValue) - { - var heightParam = request.Height.Value.ToString(UsCulture); + // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size + else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue) + { + var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); + var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); - filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam)); - } + filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); + } - // If a max width was requested - else if (request.MaxWidth.HasValue) - { - var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); + // If a fixed width was requested + else if (request.Width.HasValue) + { + var widthParam = request.Width.Value.ToString(UsCulture); - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam)); - } + filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam)); + } - // If a max height was requested - else if (request.MaxHeight.HasValue) - { - var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); + // If a fixed height was requested + else if (request.Height.HasValue) + { + var heightParam = request.Height.Value.ToString(UsCulture); + + filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam)); + } + + // If a max width was requested + else if (request.MaxWidth.HasValue) + { + var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); + + filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam)); + } + + // If a max height was requested + else if (request.MaxHeight.HasValue) + { + var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); - filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(ih\\,{0})", maxHeightParam)); + filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam)); + } } var output = string.Empty; @@ -917,6 +980,15 @@ namespace MediaBrowser.Api.Playback } } + if (state.VideoRequest != null) + { + var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); + if (GetVideoEncoder(state).IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1) + { + arg = "-hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device " + encodingOptions.VaapiDevice + " " + arg; + } + } + return arg.Trim(); } @@ -1042,14 +1114,14 @@ namespace MediaBrowser.Api.Playback var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; Logger.Info(commandLineLogMessage); - var logFilePrefix = "transcode"; + var logFilePrefix = "ffmpeg-transcode"; if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - logFilePrefix = "directstream"; + logFilePrefix = "ffmpeg-directstream"; } else if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - logFilePrefix = "remux"; + logFilePrefix = "ffmpeg-remux"; } var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt"); @@ -1080,7 +1152,7 @@ namespace MediaBrowser.Api.Playback //process.BeginOutputReadLine(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream)); + var task = 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) @@ -1099,28 +1171,30 @@ namespace MediaBrowser.Api.Playback } StartThrottler(state, transcodingJob); + ReportUsage(state); return transcodingJob; } private void StartThrottler(StreamState state, TranscodingJob transcodingJob) { - if (EnableThrottling(state) && state.InputProtocol == MediaProtocol.File && - state.RunTimeTicks.HasValue && - state.VideoType == VideoType.VideoFile && - !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EnableThrottling(state) && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo) - { - transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager); - state.TranscodingThrottler.Start(); - } + transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager); + state.TranscodingThrottler.Start(); } } protected virtual bool EnableThrottling(StreamState state) { - return true; + // do not use throttling with hardware encoders + return state.InputProtocol == MediaProtocol.File && + state.RunTimeTicks.HasValue && + state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && + state.IsInputVideo && + state.VideoType == VideoType.VideoFile && + !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && + string.Equals(GetVideoEncoder(state), "libx264", StringComparison.OrdinalIgnoreCase); } private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target) @@ -1158,6 +1232,7 @@ namespace MediaBrowser.Api.Playback double? percent = null; TimeSpan? transcodingPosition = null; long? bytesTranscoded = null; + int? bitRate = null; var parts = line.Split(' '); @@ -1221,11 +1296,32 @@ namespace MediaBrowser.Api.Playback } } } + else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) + { + var rate = part.Split(new[] { '=' }, 2).Last(); + + int? scale = null; + if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1) + { + scale = 1024; + rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + if (scale.HasValue) + { + float val; + + if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val)) + { + bitRate = (int)Math.Ceiling(val * scale.Value); + } + } + } } if (framerate.HasValue || percent.HasValue) { - ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded); + ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded, bitRate); } } @@ -1548,13 +1644,6 @@ 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; @@ -1564,17 +1653,21 @@ namespace MediaBrowser.Api.Playback } } } - else if (i == 27) + else if (i == 26) { request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture); } - else if (i == 28) + else if (i == 27) { if (videoRequest != null) { videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); } } + else if (i == 28) + { + request.Tag = val; + } } } @@ -1788,6 +1881,19 @@ namespace MediaBrowser.Api.Playback { state.OutputAudioCodec = "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.EnableAudioPlaybackTranscoding) + { + state.OutputAudioCodec = "copy"; + } + } + } } private void AttachMediaSourceInfo(StreamState state, @@ -2159,13 +2265,127 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest != null) { state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps; - state.VideoRequest.ForceLiveStream = transcodingProfile.ForceLiveStream; state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; } } } } + private async void ReportUsage(StreamState state) + { + try + { + await ReportUsageInternal(state).ConfigureAwait(false); + } + catch + { + + } + } + + private Task ReportUsageInternal(StreamState state) + { + if (!ServerConfigurationManager.Configuration.EnableAnonymousUsageReporting) + { + return Task.FromResult(true); + } + + if (!MediaEncoder.IsDefaultEncoderPath) + { + return Task.FromResult(true); + } + + var dict = new Dictionary<string, string>(); + + var outputAudio = GetAudioEncoder(state); + if (!string.IsNullOrWhiteSpace(outputAudio)) + { + dict["outputAudio"] = outputAudio; + } + + var outputVideo = GetVideoEncoder(state); + if (!string.IsNullOrWhiteSpace(outputVideo)) + { + dict["outputVideo"] = outputVideo; + } + + if (ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase) && + ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return Task.FromResult(true); + } + + dict["id"] = AppHost.SystemId; + dict["type"] = state.VideoRequest == null ? "Audio" : "Video"; + + var audioStream = state.AudioStream; + if (audioStream != null && !string.IsNullOrWhiteSpace(audioStream.Codec)) + { + dict["inputAudio"] = audioStream.Codec; + } + + var videoStream = state.VideoStream; + if (videoStream != null && !string.IsNullOrWhiteSpace(videoStream.Codec)) + { + dict["inputVideo"] = videoStream.Codec; + } + + var cert = GetType().Assembly.GetModules().First().GetSignerCertificate(); + if (cert != null) + { + dict["assemblySig"] = cert.GetCertHashString(); + dict["certSubject"] = cert.Subject ?? string.Empty; + dict["certIssuer"] = cert.Issuer ?? string.Empty; + } + else + { + return Task.FromResult(true); + } + + if (state.SupportedAudioCodecs.Count > 0) + { + dict["supportedAudioCodecs"] = string.Join(",", state.SupportedAudioCodecs.ToArray()); + } + + var auth = AuthorizationContext.GetAuthorizationInfo(Request); + + dict["appName"] = auth.Client ?? string.Empty; + dict["appVersion"] = auth.Version ?? string.Empty; + dict["device"] = auth.Device ?? string.Empty; + dict["deviceId"] = auth.DeviceId ?? string.Empty; + dict["context"] = "streaming"; + + //Logger.Info(JsonSerializer.SerializeToString(dict)); + if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList(); + list.Add(outputAudio); + ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray(); + } + + if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList(); + list.Add(outputVideo); + ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray(); + } + + ServerConfigurationManager.SaveConfiguration(); + + //Logger.Info(JsonSerializer.SerializeToString(dict)); + var options = new HttpRequestOptions() + { + Url = "https://mb3admin.com/admin/service/transcoding/report", + CancellationToken = CancellationToken.None, + LogRequest = false, + LogErrors = false + }; + options.RequestContent = JsonSerializer.SerializeToString(dict); + options.RequestContentType = "application/json"; + + return HttpClient.Post(options); + } + /// <summary> /// Adds the dlna headers. /// </summary> |
