Example #1
0
        /// <summary>
        /// Generate single encoded video stream based on JOB and OUTPUT DEFINITION
        /// </summary>
        /// <param name="v">Defines the video stream being generated</param>
        /// <param name="j">The overall job for this encode run</param>
        /// <param name="xEncoder">libx264 or libx265</param>
        public void EncodeX(VideoOutputDefinition v, VideoEncodeJob j, string xEncoder)
        {
            MediaProperties ma = j.InputMedia[0];
            MediaStream     vs = ma.Streams[ma.FirstVideoIndex];

            //implicitly set frame rate.
            //To do: this needs to be rationals, not floats
            var rateString = vs.VideoFrameRate > 0 ? $"-r {((j.MatchRate != 0) ? j.MatchRate : vs.VideoFrameRate)}" : String.Empty;

            //Select proper prefix based on input media.
            //MPEG files require a specific syntax on the input to prevent time-code issues.
            var FFRoot = this.MpegFiles.Contains(Path.GetExtension(ma.MediaFile).ToLower()) ? this.FFRootCmdMpg : this.FFRootCmdDefault;

            FilterChain f = this.ConfigureFilters(v, j);

            for (int pass = 1; pass <= v.NumPasses; pass++)
            {
                var FFCmd = $"{FFRoot} -i {j.InputMedia[0].MediaFile} {f.GetVideoFilters()} " +
                            $"-pix_fmt {this.PixelFormatMap[j.PixelFmt]} " +
                            $"-preset {this.EncoderPresetMap[j.Preset]} " +
                            $"-an -c:v lib{xEncoder} " +
                            $"-metadata:s:v:0 language={j.Language} " +
                            //ToDo: this needs to be a rational like "24000/1001" to prevent rate drift during sitching or concetnation.
                            $"{rateString} " +
                            $"{this.BuildX26xEncOptions(v, j, pass, xEncoder)} " +
                            $"{Path.Combine(j.OutputFolder, v.OutputFileName)}";
                //Run
                this.utils.LogProxy($"\r\nSTARTING {xEncoder} Encode pass {pass} of {v.NumPasses}", Utilities.LogLevel.Info);
                this.ExecuteFFMpeg_LogProgress(FFCmd, ma.MediaDuration);
            }
        }
        private string RunScan(MediaProperties ma, FilterChain fc, int startSeconds = 0, int scanDuration = 0)
        {
            var videoStream = ma.Streams.FirstOrDefault(m => m.StreamType == StreamType.Video);

            if (scanDuration == 0)
            {
                scanDuration = Convert.ToInt32(ma.MediaDuration);
            }

            var scanCmd =
                $" -ss {startSeconds} -y -i {ma.MediaFile}  -pix_fmt yuv420p -t {scanDuration} {fc.GetVideoFilters()} -an -f null NUL";

            return(this.ExecuteFFMpeg_LogProgress(scanCmd, scanDuration).StdErr);
        }
        ///  <summary>
        ///  Parse the scan results to detect combed video or film content...
        ///  </summary>
        /// <param name="ma"> MediaAttributes.</param>
        /// <param name="scanData">Results of FFMPEG run</param>
        private void ParseCombScanData(MediaProperties ma, string scanData)
        {
            string[] sfd = null;
            string[] mfd = null;
            string[] rfd = null;
            var      fieldMatchFailures = 0;

            string[] scanDataLines = scanData.Split(new[]
            {
                "\n",
                "\r\n"
            }, StringSplitOptions.RemoveEmptyEntries);

            //Extract field and frame info
            foreach (string s in scanDataLines)
            {
                if (s.Contains("Repeated Fields"))
                {
                    //remove dupe spaces
                    string t = Regex.Replace(s, @"\s+", " ").Replace(@": ", @":");
                    rfd = t.Split(' ');
                }

                if (s.Contains("Single frame detection"))
                {
                    //remove dupe spaces
                    string t = Regex.Replace(s, @"\s+", " ").Replace(@": ", @":");
                    sfd = t.Split(' ');
                }

                if (s.Contains("Multi frame detection"))
                {
                    //remove dupe spaces
                    string t = Regex.Replace(s, @"\s+", " ").Replace(@": ", @":");
                    t   = t.Replace(": ", ":");
                    mfd = t.Split(' ');
                }

                if (s.Contains("still interlaced"))
                {
                    fieldMatchFailures++;
                }
            }

            if (sfd != null && mfd != null)
            {
                this.DoFrameAndFieldAnalysis(ma, mfd, rfd, fieldMatchFailures);
            }
        }
        /// <summary>
        /// Scan media, looking for combing artifacts.
        /// Used for guiding de-interlacing and inverse-telecine filter selection.
        /// </summary>
        /// <param name="ma"> MediaAttributes.</param>
        /// <param name="startSeconds"></param>
        /// <param name="scanDuration"></param>
        public void DetectCombing(MediaProperties ma, int startSeconds = 0, int scanDuration = 0)
        {
            //Settings for detecting combing and telecine patterns.
            var filterIdet       = "idet";
            var filterFieldMatch = "fieldmatch=order=auto:combmatch=full:cthresh=12";

            var filterChain = new FilterChain();

            filterChain.AddFilter(filterIdet);
            filterChain.AddFilter(filterFieldMatch);

            this.utils.LogProxy($"Starting combing/telecine detection...", Utilities.LogLevel.Info);
            var scanData = this.RunScan(ma, filterChain, startSeconds, scanDuration);

            this.ParseCombScanData(ma, scanData);
        }
Example #5
0
        /// <summary>
        /// Derives x264/x265 options from JOB and OUTPUT DEFINITION
        /// This only supports the parameters that are common between x264 and x265 and relies in their shared syntax.
        /// VideoOutputDefinition.EncoderOptionOverride.Add() is used for unique encoder specific overrides.
        /// </summary>
        /// <param name="v">Defines the video stream being generated</param>
        /// <param name="j">The overall job for this encode run</param>
        /// <param name="pass">pass number of multi pass encode</param>
        /// <param name="xEncoder">encoder to use</param>
        protected string BuildX26xEncOptions(VideoOutputDefinition v, VideoEncodeJob j, int pass, string xEncoder)
        {
            MediaProperties ma        = j.InputMedia[0];
            MediaStream     vs        = ma.Streams[ma.FirstVideoIndex];
            var             statsFile = $"{Path.GetFileNameWithoutExtension(v.OutputFileName)}_STATS";

            //Scene detection breaks GOP alignment across streams. Disable by default.
            //May be re-enabled at the stream level, for example for DONWLOAD encodes.
            var sceneDetection = v.AllowSceneDetection ? "" : "scenecut=0:";

            //If job does not have color space for output implicitly set, derive it from source media.
            var ColorArgs = (j.ColorSpec == VideoEncodeJob.OutputColorSpec.Unknown) ? this.getX26xColorSpaceArgs(vs) : this.ColorSpaceMap[j.ColorSpec];

            var args = $"-{xEncoder}-params \"" +
                       $"bitrate={v.TargetBitrate}:" +
                       $"vbv-maxrate={v.PeakBitrate}:" +
                       $"vbv-bufsize={v.VBVBufferSize}:" +
                       $"min-keyint={Convert.ToInt32(j.GopLengthSeconds * vs.VideoFrameRate)}:" +
                       $"keyint={Convert.ToInt32(j.GopLengthSeconds * vs.VideoFrameRate)}:" +
                       $"rc-lookahead={j.LookAheadFrames}:" +
                       "open-gop=0:" +  //GOPS must always be closed
                       $"{sceneDetection}" +
                       $"pass={pass}:" +
                       $"stats=\"{statsFile}\":" +
                       $"{ColorArgs}";

            //handle HDR settings.
            //HDR is only supported when the output is HEVC

            if (xEncoder == "x265" && j.HdrMasteringDisplayPrimaries != null)
            {
                args += $":{this.Getx26xHdrString(j)}";
            }

            foreach (string overrideOption in v.EncoderOptionOverride)
            {
                args += $":{overrideOption}";
            }

            args += "\"";

            return(args);
        }
        /// <summary>
        /// Adds input media to job
        /// </summary>
        /// <param name="m">MediaAttributes object describing a single input</param>
        public void AddInputMedia(MediaProperties m)
        {
            //Only one source per video encode job is permitted...
            if (this.InputMedia.Count == 1)
            {
                this.utils.LogProxy($"Video encoding jobs may have only one input...",
                                    Utilities.LogLevel.AppError);
            }

            //If present, fetch HDR data from source and add it to job
            //But only if it is not already set.
            //IE: trust delivered side-car metadata over embedded metadata

            if (m.HasHDR && this.HdrMasteringDisplayPrimaries == null)
            {
                this.HdrMasteringDisplayPrimaries = m.Streams[m.FirstVideoIndex].MasteringDisplayPrimaries;
                this.HdrMasteringDisplayLuminance = m.Streams[m.FirstVideoIndex].MasteringDisplayLuminance;
                this.HdrCea8613HdrData            = m.Streams[m.FirstVideoIndex].Cea8613HdrData;
            }

            this.InputMedia.Add(m);
        }
        ///  <summary>
        ///  Parse the scan results to detect combed video or film content...
        ///  </summary>
        /// <param name="ma"> MediaAttributes.</param>
        /// <param name="mfd">Multi-frame detection data from FFMPEG IDET filter run</param>
        /// <param name="rfd">Repeat fields data from FFMPEG IDET filter run</param>
        /// <param name="fieldMatchFailures">Field match failures from FIELDMATCH filter run</param>
        private void DoFrameAndFieldAnalysis(MediaProperties ma, string[] mfd, string[] rfd, int fieldMatchFailures)
        {
            //Detection settings
            //If Progressive frame count falls below this level, combing
            //is assumed...
            decimal progressiveFrameThreshold = 95;
            decimal ProgFramePercentage       = 0;
            decimal RepeatFieldPercentage     = 0;
            decimal TfFtoBffRatio             = 0;

            decimal mfdTff = 1, mfdBff = 1, mfdProg = 1;
            decimal rfdNone = 1, rfdTop = 1, rfdBottom = 1;

            foreach (string s in mfd)

            {
                if (s.Contains("TFF"))
                {
                    mfdTff = Convert.ToDecimal(s.Split(':')[2]);
                }
                if (s.Contains("BFF"))
                {
                    mfdBff = Convert.ToDecimal(s.Split(':')[1]);
                }
                if (s.Contains("Progressive"))
                {
                    mfdProg = Convert.ToDecimal(s.Split(':')[1]);
                }
            }

            foreach (string s in rfd)
            {
                if (s.Contains("Neither"))
                {
                    rfdNone = Convert.ToDecimal(s.Split(':')[2]);
                }
                if (s.Contains("Top"))
                {
                    rfdTop = Convert.ToDecimal(s.Split(':')[1]);
                }
                if (s.Contains("Bottom"))
                {
                    rfdBottom = Convert.ToDecimal(s.Split(':')[1]);
                }
            }

            //total number of identified frames.
            decimal processedFrames = rfdBottom + rfdNone + rfdTop;

            this.utils.LogProxy($"Total Scanned Frames (from IDET): {processedFrames}", Utilities.LogLevel.Info);
            this.utils.LogProxy($"Fieldmatch Failures: {fieldMatchFailures}", Utilities.LogLevel.Info);

            //Ratio of frames with repeated fields to the total number of frames identified
            RepeatFieldPercentage = ((rfdTop + rfdBottom) / processedFrames) * 100;
            this.utils.LogProxy($"RepeatFieldPercentage : {RepeatFieldPercentage:N2}", Utilities.LogLevel.Info);

            //Ratio of detected progressive frames to total frames identified
            ProgFramePercentage = (mfdProg / processedFrames) * 100;
            this.utils.LogProxy($"Progressive frame percentage : {ProgFramePercentage:N2}", Utilities.LogLevel.Info);

            //Compute ratio of TFF vs BFF frames.
            if (mfdTff == 0 || mfdBff == 0)
            {
                TfFtoBffRatio = 0;
            }
            else
            {
                TfFtoBffRatio = Math.Max(mfdTff, mfdBff) / Math.Min(mfdTff, mfdBff);
            }

            this.utils.LogProxy($"TFF:BFF ratio : {mfdTff}:{mfdBff}", Utilities.LogLevel.Info);

            //Once the detected progressive frame count falls below a threshold assume interlaced
            if (ProgFramePercentage < progressiveFrameThreshold)
            {
                ma.HasCombing = true;
                this.utils.LogProxy($"Combing detected...", Utilities.LogLevel.Info);
            }

            //If content is combed (low progressive frame count) and there are
            //repeated fields, video probably has some telecine content
            if (ma.HasCombing && (RepeatFieldPercentage > Convert.ToDecimal(5)))
            {
                this.utils.LogProxy($"Telecine content detected...", Utilities.LogLevel.Info);
                ma.HasTelecine = true;
            }

            //If fieldmatch failures are very low, indicating either clean progressive media
            //OR a clean telecine AND the progressive frame percentage is very low THEN
            //flag media a cleanly telecined.
            if (fieldMatchFailures / (double)processedFrames < 0.01 && ProgFramePercentage < 1)
            {
                ma.IsPureFilm = true;
                this.utils.LogProxy($"Clean Telecine content detected...", Utilities.LogLevel.Info);
            }

            ma.IsPureVideo      = (ma.HasCombing && !ma.HasTelecine);
            ma.IsMixedFilmVideo = (ma.HasCombing && ma.HasTelecine && !ma.IsPureFilm);

            this.utils.LogProxy($"PureVideo:{ma.IsPureVideo}", Utilities.LogLevel.Info);
            this.utils.LogProxy($"Mixed Film and Video:{ma.IsMixedFilmVideo}", Utilities.LogLevel.Info);

            //If media is combed but the TFF:BFF ratio approaches 1:1 this could indicate
            //a potentially bad delivery.
            if (ma.HasCombing && (TfFtoBffRatio < Convert.ToDecimal(1.25) && TfFtoBffRatio > Convert.ToDecimal(0.75)))
            {
                ma.BadDelivery = true;
                this.utils.LogProxy($"Ratio of TFF and BFF frames of {TfFtoBffRatio:F}:1 indicates possibly poor delivery.", Utilities.LogLevel.Warning);
            }
        }
Example #8
0
        /// <summary>
        /// Manages filters used to process video.
        /// Scaling
        /// De-interlacing
        /// Cropping
        /// Matching to master (for stitching scenarios, IE: dub cards)
        /// Burning in sub-titles.
        /// </summary>
        /// <param name="outputVideo">Defines the video stream being generated</param>
        /// <param name="videoEncodeJob">The overall job for this encode run</param>
        protected FilterChain ConfigureFilters(VideoOutputDefinition outputVideo, VideoEncodeJob videoEncodeJob)
        {
            //Video stream
            MediaProperties videoProperties = videoEncodeJob.InputMedia[0];
            MediaStream     inputVideo      = videoProperties.Streams[videoProperties.FirstVideoIndex];
            FilterChain     filters         = new FilterChain();

            //Derived from desired width, source aspect and cropping options.
            var TargetOutputHeight = 0;

            if (!String.IsNullOrEmpty(videoProperties.FFCropFilter) && videoEncodeJob.AutoCrop)
            {
                //must crop before any scaling happens...
                filters.AddFilter(videoProperties.FFCropFilter);

                if (inputVideo.VideoPixelAspect != 1)
                {
                    inputVideo.SquareVideoWidth  = Convert.ToInt32(videoProperties.cropValue.XExtent * inputVideo.VideoPixelAspect);
                    inputVideo.SquareVideoHeight = videoProperties.cropValue.YExtent;
                }
                else
                {
                    inputVideo.SquareVideoWidth  = videoProperties.cropValue.XExtent;
                    inputVideo.SquareVideoHeight = videoProperties.cropValue.YExtent;
                }
            }
            else
            {
                if (inputVideo.VideoPixelAspect != 1)
                {
                    inputVideo.SquareVideoWidth = Convert.ToInt32(inputVideo.VideoWidth * inputVideo.VideoPixelAspect);
                }
            }

            //De-interlace filter behavior.
            //Must happen before any scaling operations.
            //Only invoke on HD and SD at non-film frame rates
            if (videoProperties.HasCombing && inputVideo.SquareVideoWidth <= 1920 && inputVideo.VideoFrameRate > 24)
            {
                if (videoEncodeJob.DeintOverride != VideoEncodeJob.DeinterlaceOverride.None)
                {
                    //The job manager may desire to override behavior.
                    //For example, in the event where the main feature is cleanly telecined film
                    //but the credits are combed video. HBO LatAm does this.
                    filters.AddFilter(this.DeinterlaceOverrideMap[videoEncodeJob.DeintOverride]);
                }
                else if (videoProperties.IsMixedFilmVideo)
                {
                    //Typical: 30i animation with some telecine segments.
                    //Preserve original frame rate. Inverse telecine film.
                    //De-comb video. There will be judder in the film segments.
                    filters.AddFilter(FFBase.DeintFilter_VideoBias);
                }
                else if (videoProperties.IsPureFilm)
                {
                    //Old telecined content. Rare.
                    filters.AddFilter(FFBase.DeintFilter_PureTelecine);
                }
                else if (videoProperties.IsPureVideo)
                {
                    //Talk shows. Next day TV. Old sitcoms.
                    //This filter setting is slow, but yields good results.
                    filters.AddFilter(FFBase.DeintFilter_PureVideoSD);
                }
            }

            if (videoEncodeJob.MatchWidth != 0 && videoEncodeJob.MatchHeight != 0)
            {
                //Job indicates matching to master source.
                filters.AddFilter(this.GetScaleMatchFilter(inputVideo.VideoPixelAspect == 1, videoEncodeJob.MatchWidth, videoEncodeJob.MatchHeight));
                TargetOutputHeight = outputVideo.Width * videoEncodeJob.MatchHeight / videoEncodeJob.MatchWidth;
                TargetOutputHeight = (TargetOutputHeight % 2 == 0) ? TargetOutputHeight : ++TargetOutputHeight;
                filters.AddFilter($"scale={outputVideo.Width}:{TargetOutputHeight}");
            }
            else if (outputVideo.Width != inputVideo.SquareVideoWidth || inputVideo.VideoPixelAspect != 1)
            {
                //Input is either non-square or does not match desired output
                TargetOutputHeight = outputVideo.Width * inputVideo.SquareVideoHeight / inputVideo.SquareVideoWidth;
                TargetOutputHeight = (TargetOutputHeight % 2 == 0) ? TargetOutputHeight : ++TargetOutputHeight;
                filters.AddFilter($"scale={outputVideo.Width}:{TargetOutputHeight}");
            }

            //Set source pixel aspect
            filters.AddFilter("setsar=1/1");

            //Sub titles go last to ensure some degree of readability on low res streams
            if (!String.IsNullOrEmpty(videoEncodeJob.BurnSubs))
            {
                //Need to escape characters so the ASS filter will be happy.
                videoEncodeJob.BurnSubs = videoEncodeJob.BurnSubs.Replace(@"\", @"\\");
                videoEncodeJob.BurnSubs = videoEncodeJob.BurnSubs.Replace(@":", @"\:");

                filters.AddFilter($"ass='{videoEncodeJob.BurnSubs}'");
            }

            return(filters);
        }
Example #9
0
        /// <summary>
        /// Scan media, finding black bars and selecting the most common result.
        /// Used for automated cropping
        /// </summary>
        /// <param name="ma"> MediaAttributes.</param>
        /// <param name="startSeconds"></param>
        /// <param name="scanDuration"></param>
        public void DetectLetterbox(MediaProperties ma, int startSeconds = 0, int scanDuration = 0)
        {
            var filterCropdetect = "cropdetect=0.1:2:0";
            var filterChain      = new FilterChain();
            var videoStream      = ma.Streams.FirstOrDefault(m => m.StreamType == StreamType.Video);

            filterChain.AddFilter(filterCropdetect);

            this.utils.LogProxy($"Starting letterbox detection...", Utilities.LogLevel.Info);
            var scanData = this.RunScan(ma, filterChain, startSeconds, scanDuration);

            ma.cropValue = this.GetCommonCropValue(scanData);

            int bottomCrop = videoStream.VideoHeight - ma.cropValue.YExtent - ma.cropValue.YOffset;

            //If the bars at the top differ in size from those at the bottom
            //then re-scan because this is generally the result of noise at the top
            //of the video frame that breaks bar detection.
            if (Math.Abs(bottomCrop - ma.cropValue.YOffset) > 16)
            {
                filterChain.DeleteAll();

                //Different masking values are used for suspected PAL and NSTC content.
                //The crop filter syntax is counter-intuitive. It describes the visible region of media
                //IE: crop=width:height:x-offset:y-offset <- this is the size of the output. Not the mask.
                if (videoStream.VideoHeight < 650 && videoStream.VideoHeight > 525)
                {
                    filterChain.AddFilter($"crop={videoStream.VideoWidth.ToString()}:{videoStream.VideoHeight - 32}:0:{32}");
                }
                else if (videoStream.VideoHeight <= 525)
                {
                    filterChain.AddFilter($"crop={videoStream.VideoWidth.ToString()}:{videoStream.VideoHeight - 16}:0:{16}");
                }

                filterChain.AddFilter(filterCropdetect);

                this.utils.LogProxy($"Starting second letterbox detection scan due to detected noise...",
                                    Utilities.LogLevel.Info);
                scanData = this.RunScan(ma, filterChain, startSeconds, scanDuration);

                ma.cropValue = this.GetCommonCropValue(scanData);

                if (videoStream.VideoHeight < 650 && videoStream.VideoHeight > 525)
                {
                    ma.cropValue.YOffset += 32;
                }
                else if (videoStream.VideoHeight <= 525)
                {
                    ma.cropValue.YOffset += 16;
                }
            }

            //Only crop if we have a minimum of letterbox coverage
            if (Math.Abs(ma.cropValue.YExtent - videoStream.VideoHeight) > 16)
            {
                ma.HasLetterbox = true;
                //Crop to even values...
                if (ma.cropValue.YExtent % 2 != 0)
                {
                    ma.cropValue.YExtent--;
                }

                //Save pre-built crop filter for later use if desired.
                ma.FFCropFilter = $"crop={videoStream.VideoWidth.ToString()}:{ma.cropValue.YExtent.ToString()}:0:{ma.cropValue.YOffset.ToString()}";
            }
        }