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> /// 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); }
/// <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); }
/// <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()}"; } }