コード例 #1
0
        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)
            });
        }
コード例 #2
0
        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);
        }
コード例 #3
0
 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)
         }
     });
 }
コード例 #4
0
        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);
        }
コード例 #5
0
        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);
        }
コード例 #6
0
        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);
        }
コード例 #7
0
        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);
        }
コード例 #8
0
        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);
        }
コード例 #9
0
        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);
        }
コード例 #10
0
        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);
        }
コード例 #11
0
        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);
        }
コード例 #12
0
        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
        }