protected override FFmpegJob Map(TranscodeJob job, FFmpegConfig config) { var result = base.Map(job, config); result.Format = GetFormatName(job.Format); result.Metadata = job.Metadata; // This is a workaround for subtitle overlays with MKV reporting an incorrect duration if ((job.Format == ContainerFormat.Mkv) && (job.HardSubtitles?.SourceStreamIndex != null)) { result.Duration = job.SourceInfo.Duration; } var trueHdStreams = from o in job.Streams where o.GetType() == typeof(OutputStream) join s in job.SourceInfo.Streams.OfType <AudioStreamInfo>() on o.SourceStreamIndex equals s.Index where s.Format == AudioFormat.DolbyTrueHd select o; if (trueHdStreams.Any()) { result.MaxMuxingQueueSize = 1024; } return(result); }
protected virtual IList <MappedStream> MapStreams(FFmpegConfig config, TranscodeJob job) { IList <MappedStream> result = new List <MappedStream>(); IDictionary <int, StreamInfo> sourceStreamsByIndex = job.SourceInfo.Streams.ToDictionary(s => s.Index); for (int i = 0; i < job.Streams.Count; i++) { OutputStream outputStream = job.Streams[i]; StreamInfo sourceStream; if (!sourceStreamsByIndex.TryGetValue(outputStream.SourceStreamIndex, out sourceStream)) { throw new ArgumentException( $"{nameof(job.SourceInfo)} does not contain a stream with index {outputStream.SourceStreamIndex}.", nameof(job.SourceInfo)); } MappedStream mappedStream = MapStream(config, sourceStream, outputStream); if (mappedStream != null) { result.Add(mappedStream); } } return(result); }
protected virtual void AddTonemapFilters(IList <IFilter> filters, FFmpegConfig config) { filters.Add(new Filter("zscale") { Options = new Option[] { new Option("t", "linear"), new Option("npl", "100") } }); filters.Add(GetFormatFilter("gbrpf32le")); filters.Add(new Filter("zscale") { Options = new Option[] { new Option("p", "bt709") } }); filters.Add(GetTonemapFilter(config)); filters.Add(new Filter("zscale") { Options = new Option[] { new Option("t", "bt709"), new Option("m", "bt709"), new Option("r", "tv") } }); filters.Add(GetFormatFilter("yuv420p")); }
public object Get() { // RODAR antes: apt-get install ffmpeg FFmpegConfig.SetDirectories("ffmpeg", "ffprobe", "/tmp/"); var location = "input"; var bytes = System.IO.File.ReadAllBytes(location); System.IO.File.WriteAllBytes("/tmp/input.mp4", bytes); var input = FFmpegProcess.GetInfo(location); return(new { FFmpegResult = input, Locations = Directory.GetFiles("/tmp/") }); //if (input.Streams.Any(p => p.codec_type.Equals("video"))) //{ // var destinationthumb = Path.ChangeExtension(file, "jpg"); // FFmpegProcess.GetThumbNail(file, destinationthumb); //} //else if (input.Streams.Any(p => p.codec_type.Equals("audio"))) //{ // Console.WriteLine($"Duration: {input.Format.durationTs}"); //} }
public void Setup() { _fileSystem = Substitute.For <IFileSystem>(); _fileService = Substitute.For <IFile>(); _directoryService = Substitute.For <IDirectory>(); _serializer = Substitute.For <ISerializer <string> >(); _userDirectory = Guid.NewGuid().ToString(); _userFileName = Guid.NewGuid().ToString(); _defaultFileName = Guid.NewGuid().ToString(); _configManager = new FFmpegConfigManager(_fileSystem, _serializer, _defaultFileName, _userFileName); _userConfig = new FFmpegConfig(); _defaultConfig = new FFmpegConfig(); var userText = Guid.NewGuid().ToString(); var defaultText = Guid.NewGuid().ToString(); _fileSystem.File.Returns(_fileService); _fileSystem.Directory.Returns(_directoryService); _fileService.Exists(Arg.Any <string>()).Returns(true); _fileService.ReadAllText(_userFileName).Returns(userText); _fileService.ReadAllText(_defaultFileName).Returns(defaultText); _directoryService.Exists(Arg.Any <string>()).Returns(true); _serializer.Deserialize <FFmpegConfig>(userText).Returns(_userConfig); _serializer.Deserialize <FFmpegConfig>(defaultText).Returns(_defaultConfig); }
protected virtual MappedVideoStream MapVideoStream(FFmpegConfig config, VideoStreamInfo sourceStream, VideoOutputStream outputStream) { return(new MappedVideoStream() { Input = GetStreamInput(sourceStream), }); }
protected virtual MappedStream MapSubtitleStream(FFmpegConfig config, SubtitleStreamInfo sourceStream, SubtitleOutputStream outputStream) { return(new MappedStream(StreamType.Subtitle) { Input = GetStreamInput(sourceStream), Codec = new Codec(GetSubtitleCodecName(config, outputStream.Format)) }); }
protected virtual Codec GetVideoCodec(FFmpegConfig config, VideoStreamInfo sourceStream, VideoOutputStream outputStream) { VideoFormat format = outputStream.Format; VideoCodec codec = config?.Video?.Codecs.GetValueOrDefault(format); string codecName = GetVideoCodecName(format); X26xCodec result = format == VideoFormat.Hevc ? new X265Codec(codecName) : new X26xCodec(codecName); result.Preset = codec?.Preset; result.Crf = outputStream.Quality; if (outputStream.DynamicRange == DynamicRange.High) { if (outputStream.Format != VideoFormat.Hevc) { throw new NotSupportedException($"HDR is not supported with the video format {outputStream.Format}."); } var options = new List <Option>() { new Option("colorprim", "bt2020"), new Option("colormatrix", "bt2020nc"), new Option("transfer", "smpte2084") }; if (outputStream.CopyHdrMetadata) { if (sourceStream.MasterDisplayProperties != null) { var properties = sourceStream.MasterDisplayProperties; var value = string.Format("\"G{0}B{1}R{2}WP{3}L({4},{5})\"", properties.Green, properties.Blue, properties.Red, properties.WhitePoint, properties.Luminance.Max, properties.Luminance.Min); options.Add(new Option("master-display", value)); } if (sourceStream.LightLevelProperties != null) { var properties = sourceStream.LightLevelProperties; options.Add(new Option("max-cll", $"\"{properties.MaxCll},{properties.MaxFall}\"")); } } ((X265Codec)result).Options = options; } return(result); }
protected override MappedVideoStream MapVideoStream(FFmpegConfig config, VideoStreamInfo sourceStream, VideoOutputStream outputStream) { var result = base.MapVideoStream(config, sourceStream, outputStream); result.Codec = GetVideoCodec(config, sourceStream, outputStream); result.Tag = outputStream.Tag; return(result); }
string GetSubtitleCodecName(FFmpegConfig config, SubtitleFormat format) { string result = config?.Subtitles?.Codecs?.GetValueOrDefault(format)?.Name; if (result == null) { throw new NotSupportedException($"The subtitle format {format} is not supported."); } return(result); }
string GetAudioCodecName(FFmpegConfig config, AudioFormat format) { string result = config?.Audio?.Codecs?.GetValueOrDefault(format)?.Name; if (result == null) { throw new NotSupportedException($"The audio format {format} is not supported."); } return(result); }
protected override MappedStream MapStream(FFmpegConfig config, StreamInfo sourceStream, OutputStream outputStream) { var result = base.MapStream(config, sourceStream, outputStream); if (result == null) { switch (outputStream) { case AudioOutputStream audioOutput: if (sourceStream is AudioStreamInfo audioInput) { result = MapAudioStream(config, audioInput, audioOutput); } else { throw GetStreamMismatchException(nameof(sourceStream), nameof(outputStream)); } break; case SubtitleOutputStream subtitleOutput: if (sourceStream is SubtitleStreamInfo subtitleInput) { result = MapSubtitleStream(config, subtitleInput, subtitleOutput); } else { throw GetStreamMismatchException(nameof(sourceStream), nameof(outputStream)); } break; default: result = MapPassthruStream(sourceStream, outputStream); break; } } if (result != null) { result.Metadata = outputStream.Metadata; if (outputStream.IsDefault.HasValue) { result.Disposition = outputStream.IsDefault.Value ? "default" : "0"; } } return(result); }
protected virtual MappedStream MapStream(FFmpegConfig config, StreamInfo sourceStream, OutputStream outputStream) { if (sourceStream is VideoStreamInfo videoSource) { if (outputStream is VideoOutputStream videoOutput) { return(MapVideoStream(config, videoSource, videoOutput)); } throw GetStreamMismatchException(nameof(sourceStream), nameof(outputStream)); } return(null); }
public void MapAddsDenoiseFilter() { var config = new FFmpegConfig() { Video = new VideoConfig() { DenoiseOptions = "hqdn3d" } }; _videoOutput.Denoise = true; var ffmpegJob = _jobRunner.CallMap(_transcodeJob, config); Assert.AreEqual(1, ffmpegJob.Filters?.Count); var filter = ffmpegJob.Filters[0] as CustomFilter; Assert.IsNotNull(filter); Assert.AreEqual(config.Video.DenoiseOptions, filter.Data); }
protected virtual MappedAudioStream MapAudioStream(FFmpegConfig config, AudioStreamInfo sourceStream, AudioOutputStream outputStream) { var result = new MappedAudioStream() { Input = GetStreamInput(sourceStream), Codec = new Codec(GetAudioCodecName(config, outputStream.Format)) }; if (outputStream.Mixdown.HasValue) { result.ChannelCount = AudioUtility.GetChannelCount(outputStream.Mixdown.Value); } if (outputStream.Quality.HasValue) { result.Bitrate = $"{outputStream.Quality:0}k"; } return(result); }
public async Task <CropParameters> Detect(MediaInfo mediaInfo) { if (mediaInfo == null) { throw new ArgumentNullException(nameof(mediaInfo)); } if (string.IsNullOrWhiteSpace(mediaInfo.FileName)) { throw new ArgumentException($"{nameof(mediaInfo)}.FileName must not be empty or whitespace.", nameof(mediaInfo)); } if (mediaInfo.Duration <= TimeSpan.Zero) { throw new ArgumentException($"{nameof(mediaInfo)}.Duration is invalid.", nameof(mediaInfo)); } CropParameters result = null; IEnumerable <double> positions = GetSeekSeconds(mediaInfo.Duration); FFmpegConfig config = _configManager.Config; string options = string.Empty; if (!string.IsNullOrWhiteSpace(config?.Video?.CropDetectOptions)) { options = "=" + config.Video.CropDetectOptions; } var lockTarget = new object(); int?minX = null, minY = null, maxWidth = null, maxHeight = null; var tasks = positions.Select(async seconds => { var job = new FFmpegJob() { HideBanner = true, StartTime = TimeSpan.FromSeconds(seconds), InputFileName = mediaInfo.FileName, FrameCount = 2, Filters = new IFilter[] { new CustomFilter($"cropdetect{options}") } }; var arguments = _argumentGenerator.GenerateArguments(job); try { var processResult = await _processRunner.Run(_ffmpegFileName, arguments, _timeout); //The crop detection data is written to standard error. if (!string.IsNullOrWhiteSpace(processResult.ErrorData)) { var crop = Parse(processResult.ErrorData); if (crop != null) { lock (lockTarget) { minX = minX.HasValue ? Math.Min(crop.Start.X, minX.Value) : crop.Start.X; minY = minY.HasValue ? Math.Min(crop.Start.Y, minY.Value) : crop.Start.Y; maxWidth = maxWidth.HasValue ? Math.Max(crop.Size.Width, maxWidth.Value) : crop.Size.Width; maxHeight = maxHeight.HasValue ? Math.Max(crop.Size.Height, maxHeight.Value) : crop.Size.Height; } } } else { Trace.WriteLine("No ffmpeg data on stderr to parse."); } } catch (ArgumentException ex) { Trace.WriteLine(ex.Message); Debug.WriteLine(ex.StackTrace); } catch (InvalidOperationException ex) { Trace.WriteLine(ex.Message); Debug.WriteLine(ex.StackTrace); } }); await Task.WhenAll(tasks); if (minX.HasValue && minY.HasValue && maxWidth.HasValue && maxHeight.HasValue) { result = new CropParameters() { Start = new Coordinate <int>(minX.Value, minY.Value), Size = new Dimensions(maxWidth.Value, maxHeight.Value) }; } return(result); }
protected override FFmpegJob Map(TranscodeJob job, FFmpegConfig config) { ConfigPassed = config; return(JobToMap ?? base.Map(job, config)); }
public async Task TestDetect() { const string ARGS = "generated arguments"; var processRunner = Substitute.For <IProcessRunner>(); var timeout = TimeSpan.FromMilliseconds(10); var ffmpegFileName = "/usr/sbin/ffmpeg"; var config = new FFmpegConfig(); var configManager = Substitute.For <IConfigManager <FFmpegConfig> >(); var argumentGenerator = Substitute.For <IFFmpegArgumentGenerator>(); var detector = new CropDetector(ffmpegFileName, processRunner, configManager, argumentGenerator, timeout); IList <FFmpegJob> jobs = new List <FFmpegJob>(); configManager.Config = config; argumentGenerator.GenerateArguments(Arg.Any <FFmpegJob>()).Returns(ARGS); argumentGenerator.When(x => x.GenerateArguments(Arg.Any <FFmpegJob>())) .Do(x => jobs.Add(x[0] as FFmpegJob)); #region Test Exceptions await Assert.ThrowsExceptionAsync <ArgumentNullException>(async() => await detector.Detect(null)); await Assert.ThrowsExceptionAsync <ArgumentException>(async() => await detector.Detect(new MediaInfo() { FileName = "test" })); #endregion #region Test video that has bars and exceeds max seek time var mediaInfo = new MediaInfo() { FileName = "/Users/fred/Documents/video.mkv", Duration = TimeSpan.FromMinutes(104) }; var outputs = new string[] { "[Parsed_cropdetect_0 @ 0x7fce49600000] x1:0 x2:3839 y1:277 y2:1882 w:3840 h:1600 x:0 y:280 pts:102 t:0.102000 crop=3840:1600:0:280", "[Parsed_cropdetect_0 @ 0x7f8bf045da80] x1:859 x2:3839 y1:277 y2:1882 w:2976 h:1600 x:862 y:280 pts:120 t:0.120000 crop=2976:1600:862:280", "[Parsed_cropdetect_0 @ 0x7f9437704a00] x1:0 x2:3821 y1:277 y2:1774 w:3808 h:1488 x:8 y:282 pts:97 t:0.097000 crop=3808:1488:8:282", "[Parsed_cropdetect_0 @ 0x7f9032448880] x1:0 x2:3423 y1:277 y2:1882 w:3424 h:1600 x:0 y:280 pts:115 t:0.115000 crop=3424:1600:0:280", "[Parsed_cropdetect_0 @ 0x7ff79975be00] x1:1055 x2:3839 y1:277 y2:1882 w:2784 h:1600 x:1056 y:280 pts:91 t:0.091000 crop=2784:1600:1056:280" }; int i = 0; processRunner.Run(ffmpegFileName, ARGS, timeout) .Returns(new ProcessResult() { ErrorData = outputs[i++] }); CropParameters parameters = await detector.Detect(mediaInfo); Assert.AreEqual(5, jobs.Count); for (i = 0; i < jobs.Count; i++) { var job = jobs[i]; Assert.IsNotNull(job); Assert.IsTrue(job.HideBanner); Assert.AreEqual(2, job.FrameCount); Assert.AreEqual(mediaInfo.FileName, job.InputFileName); Assert.AreEqual(TimeSpan.FromMinutes(i + 1), job.StartTime); Assert.AreEqual("cropdetect", (job.Filters.FirstOrDefault() as CustomFilter)?.Data); } Assert.IsNotNull(parameters); Assert.AreEqual(new Coordinate <int>(0, 280), parameters.Start); Assert.AreEqual(new Dimensions(3840, 1600), parameters.Size); #endregion #region Test video that does not have bars mediaInfo = new MediaInfo() { FileName = "/Users/fred/Documents/video2.mkv", Duration = TimeSpan.FromMinutes(1) }; var output = @" Stream #0:0(eng): Video: hevc (Main 10), yuv420p10le(tv, bt2020nc/bt2020/smpte2084), 3840x2160 [SAR 1:1 DAR 16:9], 23.98 fps, 23.98 tbr, 1k tbn, 23.98 tbc Metadata: BPS-eng : 42940118 DURATION-eng : 00:01:00 NUMBER_OF_FRAMES-eng: 75098 NUMBER_OF_BYTES-eng: 16812194126 SOURCE_ID-eng : 001011 _STATISTICS_WRITING_DATE_UTC-eng: 2019-06-06 00:00:24 _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES SOURCE_ID [Parsed_cropdetect_0 @ 0x7ff6cce00a80] x1:0 x2:3839 y1:0 y2:2159 w:3840 h:2160 x:0 y:0 pts:91 t:0.091000 crop=3840:2160:0:0 frame= 2 fps=0.0 q=-0.0 Lsize=N/A time=00:00:00.63 bitrate=N/A speed=1.87x"; processRunner.Run(ffmpegFileName, ARGS, timeout) .Returns(new ProcessResult() { ErrorData = output }); jobs.Clear(); parameters = await detector.Detect(mediaInfo); Assert.AreEqual(5, jobs.Count); for (i = 0; i < jobs.Count; i++) { var job = jobs[i]; Assert.AreEqual(TimeSpan.FromSeconds((i + 1) * 6), job?.StartTime); } Assert.IsNotNull(parameters); Assert.AreEqual(new Coordinate <int>(0, 0), parameters.Start); Assert.AreEqual(new Dimensions(3840, 2160), parameters.Size); #endregion #region Test that config is used config.Video = new VideoConfig() { CropDetectOptions = "24:16:0" }; processRunner.Run(ffmpegFileName, ARGS, timeout) .Returns(new ProcessResult() { ErrorData = output }); jobs.Clear(); parameters = await detector.Detect(mediaInfo); foreach (var job in jobs) { Assert.AreEqual($"cropdetect={config.Video.CropDetectOptions}", (job?.Filters?.FirstOrDefault() as CustomFilter)?.Data); } #endregion }
protected virtual FFmpegJob Map(TranscodeJob job, FFmpegConfig config) { if (job == null) { throw new ArgumentNullException(nameof(job)); } if (job.SourceInfo == null) { throw new ArgumentException($"{nameof(job)}.{nameof(job.SourceInfo)} is null.", nameof(job)); } if (string.IsNullOrWhiteSpace(job.SourceInfo.FileName)) { throw new ArgumentException( $"{nameof(job)}.{nameof(job.SourceInfo)}.{nameof(job.SourceInfo.FileName)} is null or empty.", nameof(job)); } if (job.SourceInfo.Streams?.Any() != true) { throw new ArgumentException( $"{nameof(job)}.{nameof(job.SourceInfo)}.{nameof(job.SourceInfo.Streams)} is null or empty.", nameof(job)); } if (string.IsNullOrWhiteSpace(job.OutputFileName)) { throw new ArgumentException($"{nameof(job)}.{nameof(job.OutputFileName)} is null or empty.", nameof(job)); } if (job.Streams?.Any() != true) { throw new ArgumentException($"{nameof(job)}.{nameof(job.Streams)} is null or empty.", nameof(job)); } var videoSource = job.SourceInfo.Streams.OfType <VideoStreamInfo>().FirstOrDefault(); if (videoSource == null) { throw new NotSupportedException($"{nameof(job)}.{nameof(job.SourceInfo)} must contain a video stream."); } var result = new FFmpegJob() { HideBanner = true, Overwrite = true, InputFileName = job.SourceInfo.FileName, OutputFileName = job.OutputFileName }; SubtitleInfo subtitleInfo = null; if (job.HardSubtitles != null) { int i = 0; subtitleInfo = job.SourceInfo.Streams.OfType <SubtitleStreamInfo>() .Select(s => new SubtitleInfo() { AbsoluteIndex = s.Index, RelativeIndex = i++, SubtitleType = s.SubtitleType, FileName = job.SourceInfo.FileName }) .FirstOrDefault(s => s.AbsoluteIndex == job.HardSubtitles.SourceStreamIndex); if (subtitleInfo == null) { throw new ArgumentException( $"{nameof(job)}.{nameof(job.HardSubtitles)} contains an invalid index.", nameof(job)); } if (job.HardSubtitles.ForcedOnly) { result.ForcedSubtitlesOnly = true; } result.CanvasSize = videoSource.Dimensions; } result.Streams = MapStreams(config, job); var videoOutput = job.Streams.OfType <VideoOutputStream>() .FirstOrDefault(s => s.SourceStreamIndex == videoSource.Index); if (videoOutput != null) { result.Filters = GetVideoFilters(config, videoSource, videoOutput, subtitleInfo); } return(result); }
protected virtual IFilter GetTonemapFilter(FFmpegConfig config) { return(new CustomFilter($"tonemap={config.Video.TonemapOptions}")); }
protected virtual IFilter GetDenoiseFilter(FFmpegConfig config) { return(new CustomFilter(config.Video.DenoiseOptions)); }
protected virtual IList <IFilter> GetVideoFilters(FFmpegConfig config, VideoStreamInfo sourceStream, VideoOutputStream outputStream, SubtitleInfo subtitleInfo) { var result = new List <IFilter>(); if (subtitleInfo != null) { if (subtitleInfo.SubtitleType == SubtitleType.Graphic) { const string SUB_LABEL = "sub"; const string REF_LABEL = "ref"; result.Add(GetScale2RefFilter(sourceStream, subtitleInfo.AbsoluteIndex, SUB_LABEL, REF_LABEL)); result.Add(GetOverlayFilter(sourceStream, REF_LABEL, SUB_LABEL)); } else { result.Add(GetSubtitlesFilter(subtitleInfo)); } } bool setSampleAspectRatio = false; if ((outputStream.CropParameters != null) && ((outputStream.CropParameters.Size.Width < sourceStream.Dimensions.Width) || (outputStream.CropParameters.Size.Height < sourceStream.Dimensions.Height))) { result.Add(GetCropFilter(outputStream.CropParameters)); setSampleAspectRatio = true; } if (outputStream.ScaledDimensions.HasValue) { result.Add(GetScaleFilter(outputStream.ScaledDimensions.Value)); setSampleAspectRatio = true; } if (setSampleAspectRatio) { result.Add(GetSampleAspectRatioFilter(1, 1)); } if (outputStream.Deinterlace) { result.Add(GetDeinterlaceFilter(config)); } if (outputStream.Denoise) { result.Add(GetDenoiseFilter(config)); } if (outputStream.Tonemap) { AddTonemapFilters(result, config); } return(result); }
public void MapAddsTonemapFilters() { var config = new FFmpegConfig() { Video = new VideoConfig() { TonemapOptions = "hable" } }; _videoOutput.Tonemap = true; var ffmpegJob = _jobRunner.CallMap(_transcodeJob, config); Assert.AreEqual(6, ffmpegJob.Filters?.Count); var filter = ffmpegJob.Filters[0] as Filter; Assert.IsNotNull(filter); Assert.AreEqual("zscale", filter.Name); Assert.AreEqual(2, filter.Options?.Count); var option = filter.Options[0]; Assert.IsNotNull(option); Assert.AreEqual("t", option.Name); Assert.AreEqual("linear", option.Value); option = filter.Options[1]; Assert.IsNotNull(option); Assert.AreEqual("npl", option.Name); Assert.AreEqual("100", option.Value); filter = ffmpegJob.Filters[1] as Filter; Assert.IsNotNull(filter); Assert.AreEqual("format", filter.Name); Assert.AreEqual(1, filter.Options?.Count); option = filter.Options[0]; Assert.IsNotNull(option); Assert.IsNull(option.Name); Assert.AreEqual("gbrpf32le", option.Value); filter = ffmpegJob.Filters[2] as Filter; Assert.IsNotNull(filter); Assert.AreEqual("zscale", filter.Name); Assert.AreEqual(1, filter.Options?.Count); option = filter.Options[0]; Assert.IsNotNull(option); Assert.AreEqual("p", option.Name); Assert.AreEqual("bt709", option.Value); var customfilter = ffmpegJob.Filters[3] as CustomFilter; Assert.IsNotNull(filter); Assert.AreEqual($"tonemap=hable", customfilter.Data); filter = ffmpegJob.Filters[4] as Filter; Assert.IsNotNull(filter); Assert.AreEqual("zscale", filter.Name); Assert.AreEqual(3, filter.Options?.Count); option = filter.Options[0]; Assert.IsNotNull(option); Assert.AreEqual("t", option.Name); Assert.AreEqual("bt709", option.Value); option = filter.Options[1]; Assert.IsNotNull(option); Assert.AreEqual("m", option.Name); Assert.AreEqual("bt709", option.Value); option = filter.Options[2]; Assert.IsNotNull(option); Assert.AreEqual("r", option.Name); Assert.AreEqual("tv", option.Value); filter = ffmpegJob.Filters[5] as Filter; Assert.IsNotNull(filter); Assert.AreEqual("format", filter.Name); Assert.AreEqual(1, filter.Options?.Count); option = filter.Options[0]; Assert.IsNotNull(option); Assert.IsNull(option.Name); Assert.AreEqual("yuv420p", option.Value); }
public FFmpegJob CallMap(TranscodeJob job, FFmpegConfig config) { return(Map(job, config)); }