public CropParameters CalculateCropParameters(Dimensions sourceDimensions, Dimensions storageDimensions, CropParameters autocropParameters, double?aspectRatio, int divisor) { var sampleAspectRatio = VideoUtility.GetSampleAspectRatio(sourceDimensions, storageDimensions); var start = autocropParameters?.Start ?? new Coordinate <int>(0, 0); int targetX = (int)Math.Round(start.X * sampleAspectRatio); int actualX = targetX; int targetY = start.Y; int actualY = targetY; Dimensions size = autocropParameters?.Size ?? sourceDimensions; int targetHeight = size.Height; var heightMethod = targetHeight < sourceDimensions.Height ? EstimationMethod.Floor : EstimationMethod.Round; int actualHeight = GetClosestValue(targetHeight, divisor, heightMethod); if (actualHeight < targetHeight) { actualY += (int)Math.Ceiling((targetHeight - actualHeight) / 2d); } int targetWidth = size.Width; if (aspectRatio.HasValue) { targetWidth = VideoUtility.GetWidth(actualHeight, aspectRatio.Value); } var widthMethod = !aspectRatio.HasValue && size.Width < sourceDimensions.Width ? EstimationMethod.Floor : EstimationMethod.Round; int actualWidth = GetClosestValue(targetWidth, divisor, widthMethod); if (aspectRatio.HasValue) { actualWidth = (int)Math.Round(actualWidth / sampleAspectRatio); } if (actualWidth > size.Width) { actualWidth = GetClosestValue(targetWidth, divisor, EstimationMethod.Floor); if (aspectRatio.HasValue) { actualWidth = (int)Math.Round(actualWidth / sampleAspectRatio); } } if (actualWidth < size.Width) { actualX += (int)Math.Floor((size.Width - actualWidth) / 2d); } return(new CropParameters() { Start = new Coordinate <int>(actualX, actualY), Size = new Dimensions(actualWidth, actualHeight) }); }
CropParameters Parse(string outputData) { CropParameters result = null; int x, y, width, height; var match = Regex.Match(outputData, $"crop=(?<{nameof(width)}>\\d+):(?<{nameof(height)}>\\d+):(?<{nameof(x)}>\\d+):(?<{nameof(y)}>\\d+)"); if (match.Success && int.TryParse(match.Groups[nameof(x)].Value, out x) && int.TryParse(match.Groups[nameof(y)].Value, out y) && int.TryParse(match.Groups[nameof(width)].Value, out width) && int.TryParse(match.Groups[nameof(height)].Value, out height)) { result = new CropParameters() { Start = new Coordinate <int>(x, y), Size = new Dimensions(width, height) }; } else { Trace.WriteLine("No crop data was found."); } return(result); }
protected virtual IFilter GetCropFilter(CropParameters parameters) { return(new Filter("crop") { Options = new Option[] { Option.FromValue(parameters.Size.Width), Option.FromValue(parameters.Size.Height), Option.FromValue(parameters.Start.X), Option.FromValue(parameters.Start.Y) } }); }
public void CalculatesCropParametersFor16x9WithHorizontalBars() { var calculator = new TranscodeCalculator(); var sourceDimensions = new Dimensions(1920, 1080); var autocropParameters = new CropParameters() { Size = new Dimensions(1920, 804), Start = new Coordinate <int>(0, 138) }; var result = calculator.CalculateCropParameters(sourceDimensions, sourceDimensions, autocropParameters, 16 / 9d, 8); Assert.IsNotNull(result); Assert.AreEqual(new Dimensions(1424, 800), result.Size); Assert.AreEqual(new Coordinate <int>(248, 140), result.Start); }
public void CalculatesCropParametersWhenNoChangesAreRequired() { var calculator = new TranscodeCalculator(); var sourceDimensions = new Dimensions(1920, 1080); var autocropParameters = new CropParameters() { Size = new Dimensions(1440, 1080), Start = new Coordinate <int>(0, 240) }; var result = calculator.CalculateCropParameters(sourceDimensions, sourceDimensions, autocropParameters, 4 / 3d, 8); Assert.IsNotNull(result); Assert.AreEqual(autocropParameters.Size, result.Size); Assert.AreEqual(autocropParameters.Start, result.Start); }
public void CalculatesCropParametersFor21x9WithHorizontalBars() { var calculator = new TranscodeCalculator(); var sourceDimensions = new Dimensions(3840, 2160); var autocropParameters = new CropParameters() { Size = new Dimensions(3840, 1632), Start = new Coordinate <int>(0, 264) }; var result = calculator.CalculateCropParameters(sourceDimensions, sourceDimensions, autocropParameters, 21 / 9d, 16); Assert.IsNotNull(result); Assert.AreEqual(new Dimensions(3808, 1632), result.Size); Assert.AreEqual(new Coordinate <int>(16, 264), result.Start); }
public void CalculatesCropParametersFor3x2WithVerticalBars() { var calculator = new TranscodeCalculator(); var sourceDimensions = new Dimensions(1920, 1080); var autocropParameters = new CropParameters() { Size = new Dimensions(1620, 1080), Start = new Coordinate <int>(148, 0) }; var result = calculator.CalculateCropParameters(sourceDimensions, sourceDimensions, autocropParameters, null, 8); Assert.IsNotNull(result); Assert.AreEqual(new Dimensions(1616, 1080), result.Size); Assert.AreEqual(new Coordinate <int>(150, 0), result.Start); }
public void CalculateCropParametersDoesNotExceedSourceDimensions() { var calculator = new TranscodeCalculator(); var sourceDimensions = new Dimensions(3840, 2160); var autocropParameters = new CropParameters() { Size = new Dimensions(3840, 1606), Start = new Coordinate <int>(0, 278) }; var result = calculator.CalculateCropParameters(sourceDimensions, sourceDimensions, autocropParameters, null, 8); Assert.IsNotNull(result); Assert.AreEqual(new Dimensions(3840, 1600), result.Size); Assert.AreEqual(new Coordinate <int>(0, 281), result.Start); }
public void CalculatesCropParametersWhenNoChangesAreRequiredForAnamorphic() { var calculator = new TranscodeCalculator(); var sourceDimensions = new Dimensions(853, 480); var storageDimensions = new Dimensions(720, 480); var autocropParameters = new CropParameters() { Size = new Dimensions(720, 464), Start = new Coordinate <int>(0, 6) }; var result = calculator.CalculateCropParameters(sourceDimensions, storageDimensions, autocropParameters, null, 8); Assert.IsNotNull(result); Assert.AreEqual(new Dimensions(720, 464), result.Size); Assert.AreEqual(new Coordinate <int>(0, 6), result.Start); }
public void CalculatesCropParametersFor16x9WithBarsAndAnamorphicSource() { var calculator = new TranscodeCalculator(); var sourceDimensions = new Dimensions(853, 480); var storageDimensions = new Dimensions(720, 480); var autocropParameters = new CropParameters() { Size = new Dimensions(720, 464), Start = new Coordinate <int>(0, 6) }; var result = calculator.CalculateCropParameters(sourceDimensions, storageDimensions, autocropParameters, 16 / 9d, 8); Assert.IsNotNull(result); Assert.AreEqual(new Dimensions(696, 464), result.Size); Assert.AreEqual(new Coordinate <int>(12, 6), result.Start); }
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); }
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 }