public async Task <bool> 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)); } var job = new FFmpegJob() { HideBanner = true, StartTime = TimeSpan.FromMilliseconds(mediaInfo.Duration.TotalMilliseconds / 2), InputFileName = mediaInfo.FileName, FrameCount = FRAME_COUNT, Filters = new IFilter[] { new Filter("idet") } }; var arguments = _argumentGenerator.GenerateArguments(job); FrameStatistics statistics = null; try { var processResult = await _processRunner.Run(_ffmpegFileName, arguments, _timeout); //The interlace detection data is written to standard error. if (!string.IsNullOrWhiteSpace(processResult.ErrorData)) { statistics = Parse(processResult.ErrorData); } 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); } return(statistics != null && (statistics.TffCount + statistics.BffCount) > (statistics.ProgressiveCount + statistics.UndeterminedCount)); }
public void GenerateArgumentsCallsReflector() { var job = new FFmpegJob() { InputFileName = "test" }; _generator.GenerateArguments(job); _reflector.Received().Reflect(job); }
public void GenerateArgumentsUsesPlaceholderForMissingOutput() { var job = new FFmpegJob() { InputFileName = "test" }; string actual = _generator.GenerateArguments(job); Assert.IsNotNull(actual); Assert.IsTrue(actual.EndsWith($"-f null -", StringComparison.InvariantCulture), "The result did not end with the output placeholder."); }
public async Task DetectGeneratesArguments() { FFmpegJob job = null; _argumentGenerator.When(x => x.GenerateArguments(Arg.Any <FFmpegJob>())) .Do(x => job = x[0] as FFmpegJob); await _detector.Detect(_mediaInfo); Assert.IsNotNull(job); Assert.AreEqual(TimeSpan.FromHours(1), job.StartTime); Assert.AreEqual(_mediaInfo.FileName, job.InputFileName); Assert.AreEqual(100, job.FrameCount); Assert.AreEqual(1, job.Filters?.Count); Assert.AreEqual("idet", (job.Filters[0] as Filter).Name); }
public void GenerateArgumentsUsesArgumentConverter() { var job = new FFmpegJob() { InputFileName = "/Users/fred/Movies/movie.mkv" }; _reflector.Reflect(job).Returns(new ArgumentProperty[] { new ArgumentProperty() { Value = job.InputFileName, Converter = _mockConverter } }); string actual = _generator.GenerateArguments(job); Assert.IsNotNull(actual); Assert.IsTrue(actual.Contains(job.InputFileName), "The result did not contain the converted value."); }
public void GenerateArgumentsUsesArgumentName() { var job = new FFmpegJob() { InputFileName = "test" }; _reflector.Reflect(job).Returns(new ArgumentProperty[] { new ArgumentProperty() { ArgumentName = "-f", Converter = _mockConverter } }); string actual = _generator.GenerateArguments(job); Assert.IsNotNull(actual); Assert.IsTrue(actual.Contains("-f"), "The result did not contain the argument name."); }
public string GenerateArguments(FFmpegJob job) { if (job == null) { throw new ArgumentNullException(nameof(job)); } if (string.IsNullOrWhiteSpace(job.InputFileName)) { throw new ArgumentException($"{nameof(job)}.{nameof(job.InputFileName)} is null or empty.", nameof(job)); } IEnumerable <ArgumentProperty> properties = _reflector.Reflect(job); var builder = new StringBuilder(); foreach (var property in properties) { if (builder.Length > 0) { builder.Append(" "); } string argument = property?.Converter?.Convert(property?.ArgumentName, property?.Value); if (!string.IsNullOrWhiteSpace(argument)) { builder.Append(argument); } } if (string.IsNullOrWhiteSpace(job.OutputFileName) && string.IsNullOrWhiteSpace(job.Format)) { // Some jobs like crop or interlace detection won't write to a file, // but FFmpeg still requires the output argument builder.Append(" -f null -"); } return(builder?.ToString()); }
public void Setup() { _ffmpegFileName = "usr/sbin/ffmpeg"; _process = Substitute.For <IProcess>(); _argumentGenerator = Substitute.For <IFFmpegArgumentGenerator>(); _configManager = Substitute.For <IConfigManager <FFmpegConfig> >(); _transcoder = new MediaTranscoder(_ffmpegFileName, () => _process, _configManager, _argumentGenerator); _argumentGenerator.When(x => x.GenerateArguments(Arg.Any <FFmpegJob>())) .Do(x => _ffmpegJob = x[0] as FFmpegJob); _videoSource = new VideoStreamInfo() { Index = 0 }; _videoOutput = new VideoOutputStream() { SourceStreamIndex = 0 }; _transcodeJob = new TranscodeJob() { SourceInfo = new MediaInfo() { FileName = "source", Streams = new List <StreamInfo>() { _videoSource } }, OutputFileName = "destination", Streams = new List <OutputStream>() { _videoOutput } }; _ffmpegJob = null; }
public void GenerateArgumentsDelimitsArguments() { var job = new FFmpegJob() { InputFileName = "/Users/fred/Movies/source.mkv", OutputFileName = "/Users/fred/Movies/destination.m4v", Format = "mp4" }; _reflector.Reflect(job).Returns(new ArgumentProperty[] { new ArgumentProperty() { ArgumentName = "-i", Value = job.InputFileName, Converter = _mockConverter }, new ArgumentProperty() { ArgumentName = "-f", Value = job.Format, Converter = _mockConverter }, new ArgumentProperty() { ArgumentName = "-o", Value = job.OutputFileName, Converter = _mockConverter } }); string expected = $"-i {job.InputFileName} -f {job.Format} -o {job.OutputFileName}"; string actual = _generator.GenerateArguments(job); Assert.AreEqual(expected, actual); }
public void GenerateArgumentsThrowsForEmptyInputFileName() { var job = new FFmpegJob(); _generator.GenerateArguments(job); }
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 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); }