diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding/Encoder')
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs | 30 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs | 86 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 159 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs | 14 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs | 59 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/FontConfigLoader.cs | 179 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 365 | ||||
| -rw-r--r-- | MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs | 16 |
8 files changed, 799 insertions, 109 deletions
diff --git a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs index 968d703be..cd1575fa5 100644 --- a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs @@ -6,6 +6,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using System.Threading.Tasks; using CommonIO; namespace MediaBrowser.MediaEncoding.Encoder @@ -16,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { } - protected override string GetCommandLineArguments(EncodingJob state) + protected override Task<string> GetCommandLineArguments(EncodingJob state) { var audioTranscodeParams = new List<string>(); @@ -41,19 +42,38 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - const string vn = " -vn"; - var threads = GetNumberOfThreads(state, false); var inputModifier = GetInputModifier(state); - return string.Format("{0} {1} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1 -y \"{5}\"", + var albumCoverInput = string.Empty; + var mapArgs = string.Empty; + var metadata = string.Empty; + var vn = string.Empty; + + if (!string.IsNullOrWhiteSpace(state.AlbumCoverPath)) + { + albumCoverInput = " -i \"" + state.AlbumCoverPath + "\""; + mapArgs = " -map 0:a -map 1:v -c:v copy"; + metadata = " -metadata:s:v title=\"Album cover\" -metadata:s:v comment=\"Cover(Front)\""; + } + else + { + vn = " -vn"; + } + + var result = string.Format("{0} {1}{6}{7} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{8} -y \"{5}\"", inputModifier, GetInputArgument(state), threads, vn, string.Join(" ", audioTranscodeParams.ToArray()), - state.OutputFilePath).Trim(); + state.OutputFilePath, + albumCoverInput, + mapArgs, + metadata).Trim(); + + return Task.FromResult(result); } protected override string GetOutputFileExtension(EncodingJob state) diff --git a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs index 8c0a7b0b5..32cd950af 100644 --- a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs @@ -42,8 +42,8 @@ namespace MediaBrowser.MediaEncoding.Encoder IFileSystem fileSystem, IIsoManager isoManager, ILibraryManager libraryManager, - ISessionManager sessionManager, - ISubtitleEncoder subtitleEncoder, + ISessionManager sessionManager, + ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager) { MediaEncoder = mediaEncoder; @@ -71,12 +71,7 @@ namespace MediaBrowser.MediaEncoding.Encoder await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false); - var commandLineArgs = GetCommandLineArguments(encodingJob); - - if (GetEncodingOptions().EnableDebugLogging) - { - commandLineArgs = "-loglevel debug " + commandLineArgs; - } + var commandLineArgs = await GetCommandLineArguments(encodingJob).ConfigureAwait(false); var process = new Process { @@ -86,7 +81,7 @@ namespace MediaBrowser.MediaEncoding.Encoder UseShellExecute = false, // Must consume both stdout and stderr or deadlocks may occur - RedirectStandardOutput = true, + //RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, @@ -136,15 +131,15 @@ namespace MediaBrowser.MediaEncoding.Encoder } cancellationToken.Register(() => Cancel(process, encodingJob)); - + // 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 new JobLogger(Logger).StartStreamingLog(encodingJob, process.StandardError.BaseStream, encodingJob.LogFileStream); // Wait for the file to exist before proceeeding - while (!FileSystem.FileExists(encodingJob.OutputFilePath) && !encodingJob.HasExited) + while (!FileSystem.FileExists(encodingJob.OutputFilePath) && !encodingJob.HasExited) { await Task.Delay(100, cancellationToken).ConfigureAwait(false); } @@ -269,11 +264,11 @@ namespace MediaBrowser.MediaEncoding.Encoder return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding"); } - protected abstract string GetCommandLineArguments(EncodingJob job); + protected abstract Task<string> GetCommandLineArguments(EncodingJob job); private string GetOutputFilePath(EncodingJob state) { - var folder = string.IsNullOrWhiteSpace(state.Options.OutputDirectory) ? + var folder = string.IsNullOrWhiteSpace(state.Options.OutputDirectory) ? ConfigurationManager.ApplicationPaths.TranscodingTempPath : state.Options.OutputDirectory; @@ -366,9 +361,22 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <returns>System.String.</returns> protected string GetVideoDecoder(EncodingJob state) { - if (string.Equals(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(GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { switch (state.MediaSource.VideoStream.Codec.ToLower()) { @@ -376,6 +384,7 @@ namespace MediaBrowser.MediaEncoding.Encoder 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; @@ -535,7 +544,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <param name="state">The state.</param> /// <param name="outputVideoCodec">The output video codec.</param> /// <returns>System.String.</returns> - protected string GetGraphicalSubtitleParam(EncodingJob state, string outputVideoCodec) + protected async Task<string> GetGraphicalSubtitleParam(EncodingJob state, string outputVideoCodec) { var outputSizeParam = string.Empty; @@ -544,7 +553,8 @@ namespace MediaBrowser.MediaEncoding.Encoder // Add resolution params, if specified if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue) { - outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"'); + outputSizeParam = await GetOutputSizeParam(state, outputVideoCodec).ConfigureAwait(false); + outputSizeParam = outputSizeParam.TrimEnd('"'); outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase)); } @@ -605,10 +615,10 @@ namespace MediaBrowser.MediaEncoding.Encoder } - // 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 llhq"; } // webm @@ -670,17 +680,20 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrEmpty(state.Options.Profile)) { - param += " -profile:v " + state.Options.Profile; + if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase)) + { + // not supported by h264_omx + param += " -profile:v " + state.Options.Profile; + } } var levelString = state.Options.Level.HasValue ? state.Options.Level.Value.ToString(CultureInfo.InvariantCulture) : null; if (!string.IsNullOrEmpty(levelString)) { - var h264Encoder = EncodingJobFactory.GetH264Encoder(state, GetEncodingOptions()); - - // 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 (levelString) { @@ -716,13 +729,20 @@ namespace MediaBrowser.MediaEncoding.Encoder break; } } - else + else if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase)) { param += " -level " + levelString; } } - 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 GetVideoBitrateParam(EncodingJob state, string videoCodec) @@ -857,7 +877,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <param name="outputVideoCodec">The output video codec.</param> /// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param> /// <returns>System.String.</returns> - protected string GetOutputSizeParam(EncodingJob state, + protected async Task<string> GetOutputSizeParam(EncodingJob state, string outputVideoCodec, bool allowTimeStampCopy = true) { @@ -934,7 +954,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.Options.SubtitleMethod == SubtitleDeliveryMethod.Encode) { - var subParam = GetTextSubtitleParam(state); + var subParam = await GetTextSubtitleParam(state).ConfigureAwait(false); filters.Add(subParam); @@ -957,7 +977,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="state">The state.</param> /// <returns>System.String.</returns> - protected string GetTextSubtitleParam(EncodingJob state) + protected async Task<string> GetTextSubtitleParam(EncodingJob state) { var seconds = Math.Round(TimeSpan.FromTicks(state.Options.StartTimeTicks ?? 0).TotalSeconds); @@ -969,7 +989,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrEmpty(state.SubtitleStream.Language)) { - var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.MediaSource.Protocol, CancellationToken.None).Result; + var charenc = await SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language, state.MediaSource.Protocol, CancellationToken.None).ConfigureAwait(false); if (!string.IsNullOrEmpty(charenc)) { @@ -1002,7 +1022,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // 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 && !GetEncodingOptions().DownMixAudioBoost.Equals(1)) { volParam = ",volume=" + GetEncodingOptions().DownMixAudioBoost.ToString(UsCulture); } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs new file mode 100644 index 000000000..0866bd255 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class EncoderValidator + { + private readonly ILogger _logger; + + public EncoderValidator(ILogger logger) + { + _logger = logger; + } + + public Tuple<List<string>, List<string>> Validate(string encoderPath) + { + _logger.Info("Validating media encoder at {0}", encoderPath); + + var decoders = GetDecoders(encoderPath); + var encoders = GetEncoders(encoderPath); + + _logger.Info("Encoder validation complete"); + + return new Tuple<List<string>, List<string>>(decoders, encoders); + } + + private List<string> GetDecoders(string encoderAppPath) + { + string output = string.Empty; + try + { + output = GetProcessOutput(encoderAppPath, "-decoders"); + } + catch + { + } + + var found = new List<string>(); + var required = new[] + { + "h264_qsv", + "mpeg2_qsv", + "vc1_qsv" + }; + + foreach (var codec in required) + { + var srch = " " + codec + " "; + + if (output.IndexOf(srch, StringComparison.OrdinalIgnoreCase) != -1) + { + _logger.Info("Decoder available: " + codec); + found.Add(codec); + } + } + + return found; + } + + private List<string> GetEncoders(string encoderAppPath) + { + string output = null; + try + { + output = GetProcessOutput(encoderAppPath, "-encoders"); + } + catch + { + } + + var found = new List<string>(); + var required = new[] + { + "libx264", + "libx265", + "mpeg4", + "msmpeg4", + "libvpx", + "libvpx-vp9", + "aac", + "libmp3lame", + "libopus", + "libvorbis", + "srt", + "h264_nvenc", + "h264_qsv", + "ac3" + }; + + output = output ?? string.Empty; + + var index = 0; + + foreach (var codec in required) + { + var srch = " " + codec + " "; + + if (output.IndexOf(srch, StringComparison.OrdinalIgnoreCase) != -1) + { + if (index < required.Length - 1) + { + _logger.Info("Encoder available: " + codec); + } + + found.Add(codec); + } + index++; + } + + return found; + } + + private string GetProcessOutput(string path, string arguments) + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = path, + Arguments = arguments, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + RedirectStandardOutput = true + } + }; + + using (process) + { + process.Start(); + + try + { + return process.StandardOutput.ReadToEnd(); + } + catch + { + _logger.Info("Killing process {0} {1}", path, arguments); + + // Hate having to do this + try + { + process.Kill(); + } + catch (Exception ex1) + { + _logger.ErrorException("Error killing process", ex1); + } + + throw; + } + } + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs index ae676dbc5..490a51128 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs @@ -64,6 +64,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public long? InputFileSize { get; set; } public string OutputAudioSync = "1"; public string OutputVideoSync = "vfr"; + public string AlbumCoverPath { get; set; } public string GetMimeType(string outputPath) { @@ -391,19 +392,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - public bool? IsTargetCabac - { - get - { - if (Options.Static) - { - return VideoStream == null ? null : VideoStream.IsCabac; - } - - return true; - } - } - public int? TargetVideoStreamCount { get diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs index f782fd05f..7c4b7fc2f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs @@ -60,6 +60,14 @@ namespace MediaBrowser.MediaEncoding.Encoder state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); + var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ?? + item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null); + + if (primaryImage != null) + { + state.AlbumCoverPath = primaryImage.Path; + } + var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false); var mediaSource = string.IsNullOrEmpty(request.MediaSourceId) @@ -91,7 +99,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (videoRequest != null) { state.OutputVideoCodec = state.Options.VideoCodec; - state.OutputVideoBitrate = GetVideoBitrateParamValue(state.Options, state.VideoStream); + state.OutputVideoBitrate = GetVideoBitrateParamValue(state.Options, state.VideoStream, state.OutputVideoCodec); if (state.OutputVideoBitrate.HasValue) { @@ -388,7 +396,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return request.AudioChannels; } - private int? GetVideoBitrateParamValue(EncodingJobOptions request, MediaStream videoStream) + private int? GetVideoBitrateParamValue(EncodingJobOptions request, MediaStream videoStream, string outputVideoCodec) { var bitrate = request.VideoBitRate; @@ -413,6 +421,18 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + 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; } @@ -536,13 +556,19 @@ namespace MediaBrowser.MediaEncoding.Encoder internal static string GetH264Encoder(EncodingJob state, EncodingOptions options) { - if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) || + string.Equals(options.HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { - // It's currently failing on live tv - if (state.RunTimeTicks.HasValue) - { - return "h264_qsv"; - } + return "h264_qsv"; + } + + if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + return "h264_nvenc"; + } + if (string.Equals(options.HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase)) + { + return "h264_omx"; } return "libx264"; @@ -575,6 +601,14 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } + if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + if (videoStream.IsAVC.HasValue && !videoStream.IsAVC.Value) + { + return false; + } + } + // If client is requesting a specific video profile, it must match the source if (!string.IsNullOrEmpty(request.Profile)) { @@ -664,14 +698,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - if (request.Cabac.HasValue && request.Cabac.Value) - { - if (videoStream.IsCabac.HasValue && !videoStream.IsCabac.Value) - { - return false; - } - } - return request.EnableAutoStreamCopy; } @@ -773,7 +799,6 @@ namespace MediaBrowser.MediaEncoding.Encoder state.TargetPacketLength, state.TargetTimestamp, state.IsTargetAnamorphic, - state.IsTargetCabac, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, diff --git a/MediaBrowser.MediaEncoding/Encoder/FontConfigLoader.cs b/MediaBrowser.MediaEncoding/Encoder/FontConfigLoader.cs new file mode 100644 index 000000000..d7ef493c2 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/FontConfigLoader.cs @@ -0,0 +1,179 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class FontConfigLoader + { + private readonly IHttpClient _httpClient; + private readonly IApplicationPaths _appPaths; + private readonly ILogger _logger; + private readonly IZipClient _zipClient; + private readonly IFileSystem _fileSystem; + + private readonly string[] _fontUrls = + { + "https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/ARIALUNI.7z" + }; + + public FontConfigLoader(IHttpClient httpClient, IApplicationPaths appPaths, ILogger logger, IZipClient zipClient, IFileSystem fileSystem) + { + _httpClient = httpClient; + _appPaths = appPaths; + _logger = logger; + _zipClient = zipClient; + _fileSystem = fileSystem; + } + + /// <summary> + /// Extracts the fonts. + /// </summary> + /// <param name="targetPath">The target path.</param> + /// <returns>Task.</returns> + public async Task DownloadFonts(string targetPath) + { + try + { + var fontsDirectory = Path.Combine(targetPath, "fonts"); + + _fileSystem.CreateDirectory(fontsDirectory); + + const string fontFilename = "ARIALUNI.TTF"; + + var fontFile = Path.Combine(fontsDirectory, fontFilename); + + if (_fileSystem.FileExists(fontFile)) + { + await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false); + } + else + { + // Kick this off, but no need to wait on it + Task.Run(async () => + { + await DownloadFontFile(fontsDirectory, fontFilename, new Progress<double>()).ConfigureAwait(false); + + await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false); + }); + } + } + catch (HttpException ex) + { + // Don't let the server crash because of this + _logger.ErrorException("Error downloading ffmpeg font files", ex); + } + catch (Exception ex) + { + // Don't let the server crash because of this + _logger.ErrorException("Error writing ffmpeg font files", ex); + } + } + + /// <summary> + /// Downloads the font file. + /// </summary> + /// <param name="fontsDirectory">The fonts directory.</param> + /// <param name="fontFilename">The font filename.</param> + /// <returns>Task.</returns> + private async Task DownloadFontFile(string fontsDirectory, string fontFilename, IProgress<double> progress) + { + var existingFile = Directory + .EnumerateFiles(_appPaths.ProgramDataPath, fontFilename, SearchOption.AllDirectories) + .FirstOrDefault(); + + if (existingFile != null) + { + try + { + _fileSystem.CopyFile(existingFile, Path.Combine(fontsDirectory, fontFilename), true); + return; + } + catch (IOException ex) + { + // Log this, but don't let it fail the operation + _logger.ErrorException("Error copying file", ex); + } + } + + string tempFile = null; + + foreach (var url in _fontUrls) + { + progress.Report(0); + + try + { + tempFile = await _httpClient.GetTempFile(new HttpRequestOptions + { + Url = url, + Progress = progress + + }).ConfigureAwait(false); + + break; + } + catch (Exception ex) + { + // The core can function without the font file, so handle this + _logger.ErrorException("Failed to download ffmpeg font file from {0}", ex, url); + } + } + + if (string.IsNullOrEmpty(tempFile)) + { + return; + } + + Extract7zArchive(tempFile, fontsDirectory); + + try + { + _fileSystem.DeleteFile(tempFile); + } + catch (IOException ex) + { + // Log this, but don't let it fail the operation + _logger.ErrorException("Error deleting temp file {0}", ex, tempFile); + } + } + private void Extract7zArchive(string archivePath, string targetPath) + { + _logger.Info("Extracting {0} to {1}", archivePath, targetPath); + + _zipClient.ExtractAllFrom7z(archivePath, targetPath, true); + } + + /// <summary> + /// Writes the font config file. + /// </summary> + /// <param name="fontsDirectory">The fonts directory.</param> + /// <returns>Task.</returns> + private async Task WriteFontConfigFile(string fontsDirectory) + { + const string fontConfigFilename = "fonts.conf"; + var fontConfigFile = Path.Combine(fontsDirectory, fontConfigFilename); + + if (!_fileSystem.FileExists(fontConfigFile)) + { + var contents = string.Format("<?xml version=\"1.0\"?><fontconfig><dir>{0}</dir><alias><family>Arial</family><prefer>Arial Unicode MS</prefer></alias></fontconfig>", fontsDirectory); + + var bytes = Encoding.UTF8.GetBytes(contents); + + using (var fileStream = _fileSystem.GetFileStream(fontConfigFile, FileMode.Create, FileAccess.Write, + FileShare.Read, true)) + { + await fileStream.WriteAsync(bytes, 0, bytes.Length); + } + } + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 97567db0e..897684b73 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -21,6 +21,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; namespace MediaBrowser.MediaEncoding.Encoder { @@ -64,8 +68,6 @@ namespace MediaBrowser.MediaEncoding.Encoder public string FFProbePath { get; private set; } - public string Version { get; private set; } - protected readonly IServerConfigurationManager ConfigurationManager; protected readonly IFileSystem FileSystem; protected readonly ILiveTvManager LiveTvManager; @@ -75,14 +77,16 @@ namespace MediaBrowser.MediaEncoding.Encoder protected readonly ISessionManager SessionManager; protected readonly Func<ISubtitleEncoder> SubtitleEncoder; protected readonly Func<IMediaSourceManager> MediaSourceManager; + private readonly IHttpClient _httpClient; + private readonly IZipClient _zipClient; private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>(); + private readonly bool _hasExternalEncoder; - public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager) + public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, bool hasExternalEncoder, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager, IHttpClient httpClient, IZipClient zipClient) { _logger = logger; _jsonSerializer = jsonSerializer; - Version = version; ConfigurationManager = configurationManager; FileSystem = fileSystem; LiveTvManager = liveTvManager; @@ -92,19 +96,305 @@ namespace MediaBrowser.MediaEncoding.Encoder SessionManager = sessionManager; SubtitleEncoder = subtitleEncoder; MediaSourceManager = mediaSourceManager; + _httpClient = httpClient; + _zipClient = zipClient; FFProbePath = ffProbePath; FFMpegPath = ffMpegPath; + + _hasExternalEncoder = hasExternalEncoder; } - public void SetAvailableEncoders(List<string> list) + public string EncoderLocationType + { + get + { + if (_hasExternalEncoder) + { + return "External"; + } + + if (string.IsNullOrWhiteSpace(FFMpegPath)) + { + return null; + } + + if (IsSystemInstalledPath(FFMpegPath)) + { + return "System"; + } + + return "Custom"; + } + } + + private bool IsSystemInstalledPath(string path) + { + if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1) + { + return true; + } + + return false; + } + + public async Task Init() + { + InitPaths(); + + if (!string.IsNullOrWhiteSpace(FFMpegPath)) + { + var result = new EncoderValidator(_logger).Validate(FFMpegPath); + + SetAvailableDecoders(result.Item1); + SetAvailableEncoders(result.Item2); + + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + var directory = Path.GetDirectoryName(FFMpegPath); + + if (!string.IsNullOrWhiteSpace(directory) && FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.ProgramDataPath, directory)) + { + await new FontConfigLoader(_httpClient, ConfigurationManager.ApplicationPaths, _logger, _zipClient, + FileSystem).DownloadFonts(directory).ConfigureAwait(false); + } + } + } + } + + private void InitPaths() + { + ConfigureEncoderPaths(); + + if (_hasExternalEncoder) + { + LogPaths(); + return; + } + + // If the path was passed in, save it into config now. + var encodingOptions = GetEncodingOptions(); + var appPath = encodingOptions.EncoderAppPath; + + var valueToSave = FFMpegPath; + + if (!string.IsNullOrWhiteSpace(valueToSave)) + { + // if using system variable, don't save this. + if (IsSystemInstalledPath(valueToSave)) + { + valueToSave = null; + } + } + + if (!string.Equals(valueToSave, appPath, StringComparison.Ordinal)) + { + encodingOptions.EncoderAppPath = valueToSave; + ConfigurationManager.SaveConfiguration("encoding", encodingOptions); + } + } + + public async Task UpdateEncoderPath(string path, string pathType) + { + if (_hasExternalEncoder) + { + return; + } + + Tuple<string, string> newPaths; + + if (string.Equals(pathType, "system", StringComparison.OrdinalIgnoreCase)) + { + path = "ffmpeg"; + + newPaths = TestForInstalledVersions(); + } + else if (string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException("path"); + } + + if (!File.Exists(path) && !Directory.Exists(path)) + { + throw new ResourceNotFoundException(); + } + newPaths = GetEncoderPaths(path); + } + else + { + throw new ArgumentException("Unexpected pathType value"); + } + + if (string.IsNullOrWhiteSpace(newPaths.Item1)) + { + throw new ResourceNotFoundException("ffmpeg not found"); + } + if (string.IsNullOrWhiteSpace(newPaths.Item2)) + { + throw new ResourceNotFoundException("ffprobe not found"); + } + + var config = GetEncodingOptions(); + config.EncoderAppPath = path; + ConfigurationManager.SaveConfiguration("encoding", config); + + Init(); + } + + private void ConfigureEncoderPaths() + { + if (_hasExternalEncoder) + { + return; + } + + var appPath = GetEncodingOptions().EncoderAppPath; + + if (string.IsNullOrWhiteSpace(appPath)) + { + appPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg"); + } + + var newPaths = GetEncoderPaths(appPath); + if (string.IsNullOrWhiteSpace(newPaths.Item1) || string.IsNullOrWhiteSpace(newPaths.Item2)) + { + newPaths = TestForInstalledVersions(); + } + + if (!string.IsNullOrWhiteSpace(newPaths.Item1) && !string.IsNullOrWhiteSpace(newPaths.Item2)) + { + FFMpegPath = newPaths.Item1; + FFProbePath = newPaths.Item2; + } + + LogPaths(); + } + + private Tuple<string, string> GetEncoderPaths(string configuredPath) + { + var appPath = configuredPath; + + if (!string.IsNullOrWhiteSpace(appPath)) + { + if (Directory.Exists(appPath)) + { + return GetPathsFromDirectory(appPath); + } + + if (File.Exists(appPath)) + { + return new Tuple<string, string>(appPath, GetProbePathFromEncoderPath(appPath)); + } + } + + return new Tuple<string, string>(null, null); + } + + private Tuple<string, string> TestForInstalledVersions() + { + string encoderPath = null; + string probePath = null; + + if (TestSystemInstalled("ffmpeg")) + { + encoderPath = "ffmpeg"; + } + if (TestSystemInstalled("ffprobe")) + { + probePath = "ffprobe"; + } + + return new Tuple<string, string>(encoderPath, probePath); + } + + private bool TestSystemInstalled(string app) { + try + { + var startInfo = new ProcessStartInfo + { + FileName = app, + Arguments = "-v", + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }; + using (var process = Process.Start(startInfo)) + { + process.WaitForExit(); + } + + _logger.Debug("System app installed: " + app); + return true; + } + catch + { + _logger.Debug("System app not installed: " + app); + return false; + } + } + + private Tuple<string, string> GetPathsFromDirectory(string path) + { + // Since we can't predict the file extension, first try directly within the folder + // If that doesn't pan out, then do a recursive search + var files = Directory.GetFiles(path); + + var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase)); + var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath)) + { + files = Directory.GetFiles(path, "*", SearchOption.AllDirectories); + + ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrWhiteSpace(ffmpegPath)) + { + ffprobePath = GetProbePathFromEncoderPath(ffmpegPath); + } + } + + return new Tuple<string, string>(ffmpegPath, ffprobePath); + } + + private string GetProbePathFromEncoderPath(string appPath) + { + return Directory.GetFiles(Path.GetDirectoryName(appPath)) + .FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase)); + } + + private void LogPaths() + { + _logger.Info("FFMpeg: {0}", FFMpegPath ?? "not found"); + _logger.Info("FFProbe: {0}", FFProbePath ?? "not found"); + } + + private EncodingOptions GetEncodingOptions() + { + return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding"); + } + + private List<string> _encoders = new List<string>(); + public void SetAvailableEncoders(List<string> list) + { + _encoders = list.ToList(); + //_logger.Info("Supported encoders: {0}", string.Join(",", list.ToArray())); } private List<string> _decoders = new List<string>(); public void SetAvailableDecoders(List<string> list) { _decoders = list.ToList(); + //_logger.Info("Supported decoders: {0}", string.Join(",", list.ToArray())); + } + + public bool SupportsEncoder(string decoder) + { + return _encoders.Contains(decoder, StringComparer.OrdinalIgnoreCase); } public bool SupportsDecoder(string decoder) @@ -112,6 +402,20 @@ namespace MediaBrowser.MediaEncoding.Encoder return _decoders.Contains(decoder, StringComparer.OrdinalIgnoreCase); } + public bool CanEncodeToAudioCodec(string codec) + { + if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase)) + { + codec = "libopus"; + } + else if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + codec = "libmp3lame"; + } + + return SupportsEncoder(codec); + } + /// <summary> /// Gets the encoder path. /// </summary> @@ -195,7 +499,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Must consume both or ffmpeg may hang due to deadlocks. See comments below. RedirectStandardOutput = true, - RedirectStandardError = true, + //RedirectStandardError = true, RedirectStandardInput = true, FileName = FFProbePath, Arguments = string.Format(args, @@ -229,7 +533,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - process.BeginErrorReadLine(); + //process.BeginErrorReadLine(); var result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream); @@ -296,7 +600,7 @@ namespace MediaBrowser.MediaEncoding.Encoder formats.Contains("ts", StringComparer.OrdinalIgnoreCase) || formats.Contains("mpegts", StringComparer.OrdinalIgnoreCase) || formats.Contains("wtv", StringComparer.OrdinalIgnoreCase); - + // If it's mpeg based, assume true if ((videoStream.Codec ?? string.Empty).IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1) { @@ -324,7 +628,7 @@ namespace MediaBrowser.MediaEncoding.Encoder UseShellExecute = false, // Must consume both or ffmpeg may hang due to deadlocks. See comments below. - RedirectStandardOutput = true, + //RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, FileName = FFMpegPath, @@ -355,7 +659,7 @@ namespace MediaBrowser.MediaEncoding.Encoder try { - process.BeginOutputReadLine(); + //process.BeginOutputReadLine(); using (var reader = new StreamReader(process.StandardError.BaseStream)) { @@ -479,18 +783,22 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - public Task<Stream> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken) + public Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken) { return ExtractImage(new[] { path }, imageStreamIndex, MediaProtocol.File, true, null, null, cancellationToken); } - public Task<Stream> ExtractVideoImage(string[] inputFiles, MediaProtocol protocol, Video3DFormat? threedFormat, - TimeSpan? offset, CancellationToken cancellationToken) + public Task<string> ExtractVideoImage(string[] inputFiles, MediaProtocol protocol, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { return ExtractImage(inputFiles, null, protocol, false, threedFormat, offset, cancellationToken); } - private async Task<Stream> ExtractImage(string[] inputFiles, int? imageStreamIndex, MediaProtocol protocol, bool isAudio, + public Task<string> ExtractVideoImage(string[] inputFiles, MediaProtocol protocol, int? imageStreamIndex, CancellationToken cancellationToken) + { + return ExtractImage(inputFiles, imageStreamIndex, protocol, false, null, null, cancellationToken); + } + + private async Task<string> ExtractImage(string[] inputFiles, int? imageStreamIndex, MediaProtocol protocol, bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { var resourcePool = isAudio ? _audioImageResourcePool : _videoImageResourcePool; @@ -524,13 +832,16 @@ namespace MediaBrowser.MediaEncoding.Encoder return await ExtractImageInternal(inputArgument, imageStreamIndex, protocol, threedFormat, offset, false, resourcePool, cancellationToken).ConfigureAwait(false); } - private async Task<Stream> ExtractImageInternal(string inputPath, int? imageStreamIndex, MediaProtocol protocol, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, SemaphoreSlim resourcePool, CancellationToken cancellationToken) + private async Task<string> ExtractImageInternal(string inputPath, int? imageStreamIndex, MediaProtocol protocol, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, SemaphoreSlim resourcePool, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) { throw new ArgumentNullException("inputPath"); } + var tempExtractPath = Path.Combine(ConfigurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); + Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); + // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar then scale to width 600. // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar var vf = "scale=600:trunc(600/dar/2)*2"; @@ -563,8 +874,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case. - var args = useIFrame ? string.Format("-i {0}{3} -threads 1 -v quiet -vframes 1 -vf \"{2},thumbnail=30\" -f image2 \"{1}\"", inputPath, "-", vf, mapArg) : - string.Format("-i {0}{3} -threads 1 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, "-", vf, mapArg); + var args = useIFrame ? string.Format("-i {0}{3} -threads 1 -v quiet -vframes 1 -vf \"{2},thumbnail=30\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg) : + string.Format("-i {0}{3} -threads 1 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg); var probeSize = GetProbeSizeArgument(new[] { inputPath }, protocol); @@ -588,8 +899,6 @@ namespace MediaBrowser.MediaEncoding.Encoder Arguments = args, WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false, - RedirectStandardOutput = true, - RedirectStandardError = true, RedirectStandardInput = true } }; @@ -602,20 +911,10 @@ namespace MediaBrowser.MediaEncoding.Encoder bool ranToCompletion; - var memoryStream = new MemoryStream(); - try { StartProcess(processWrapper); -#pragma warning disable 4014 - // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - process.StandardOutput.BaseStream.CopyToAsync(memoryStream); -#pragma warning restore 4014 - - // MUST read both stdout and stderr asynchronously or a deadlock may occurr - process.BeginErrorReadLine(); - ranToCompletion = process.WaitForExit(10000); if (!ranToCompletion) @@ -630,11 +929,10 @@ namespace MediaBrowser.MediaEncoding.Encoder } var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; + var file = new FileInfo(tempExtractPath); - if (exitCode == -1 || memoryStream.Length == 0) + if (exitCode == -1 || !file.Exists || file.Length == 0) { - memoryStream.Dispose(); - var msg = string.Format("ffmpeg image extraction failed for {0}", inputPath); _logger.Error(msg); @@ -642,8 +940,7 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new ApplicationException(msg); } - memoryStream.Position = 0; - return memoryStream; + return tempExtractPath; } } diff --git a/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs index 41bfb3b96..82a966821 100644 --- a/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs @@ -7,6 +7,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using System; using System.IO; +using System.Threading.Tasks; using CommonIO; namespace MediaBrowser.MediaEncoding.Encoder @@ -17,7 +18,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { } - protected override string GetCommandLineArguments(EncodingJob state) + protected override async Task<string> GetCommandLineArguments(EncodingJob state) { // Get the output codec name var videoCodec = EncodingJobFactory.GetVideoEncoder(state, GetEncodingOptions()); @@ -36,12 +37,14 @@ namespace MediaBrowser.MediaEncoding.Encoder var inputModifier = GetInputModifier(state); + var videoArguments = await GetVideoArguments(state, videoCodec).ConfigureAwait(false); + return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -threads {5} {6}{7} -y \"{8}\"", inputModifier, GetInputArgument(state), keyFrame, GetMapArgs(state), - GetVideoArguments(state, videoCodec), + videoArguments, threads, GetAudioArguments(state), format, @@ -55,7 +58,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <param name="state">The state.</param> /// <param name="videoCodec">The video codec.</param> /// <returns>System.String.</returns> - private string GetVideoArguments(EncodingJob state, string videoCodec) + private async Task<string> GetVideoArguments(EncodingJob state, string videoCodec) { var args = "-codec:v:0 " + videoCodec; @@ -73,8 +76,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (state.VideoStream != null && IsH264(state.VideoStream) && - (string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) || isOutputMkv)) + if (state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { args += " -bsf:v h264_mp4toannexb"; } @@ -92,7 +94,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Add resolution params, if specified if (!hasGraphicalSubs) { - args += GetOutputSizeParam(state, videoCodec); + args += await GetOutputSizeParam(state, videoCodec).ConfigureAwait(false); } var qualityParam = GetVideoQualityParam(state, videoCodec); @@ -105,7 +107,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // This is for internal graphical subs if (hasGraphicalSubs) { - args += GetGraphicalSubtitleParam(state, videoCodec); + args += await GetGraphicalSubtitleParam(state, videoCodec).ConfigureAwait(false); } return args; |
