/** * @description Detect black bars in video for cropping using ffmpeg cropdetect filter. Analyzes 9 segments of the video (excluding first and last segments from 11 total) and saves crop parameters to Variables.CropParam if black bars are detected by at least 65% of analyzed frames. * @help Dependencies: FFmpeg. If crop parameters are found, they are saved to Variables.CropParam in format "crop=width:height:x:y". Executes ffmpeg cropdetect on multiple video segments for accurate detection. * @author Generated for FileFlows * @param {int} AnalysisFrames Number of frames to analyze per segment (default: 25) * @param {string} CropdetectLimit Cropdetect threshold (0.0-1.0, default: 0.24) * @param {int} CropdetectRound Cropdetect rounding (default: 2) * @param {int} MinCropDifference Minimum pixel difference to consider cropping (default: 10) * @param {int} ConsistencyThreshold Percentage of frames that must have same crop (1-100, default: 65) * @output No black bars detected * @output Black bars detected, crop parameters saved to Variables.CropParam */ // Get ffmpeg tool path globally let ffmpeg = Flow.GetToolPath('ffmpeg'); if (!ffmpeg) { Logger.ELog("FFmpeg tool not found"); Flow.Fail("FFmpeg tool not found. Please configure FFmpeg in the system settings."); } // Get ffprobe tool path globally let ffprobe = Flow.GetToolPath('ffprobe'); if (!ffprobe) { Logger.ELog("FFprobe tool not found"); Flow.Fail("FFprobe tool not found. Please configure FFprobe in the system settings."); } function Script(AnalysisFrames, CropdetectLimit, CropdetectRound, MinCropDifference, ConsistencyThreshold) { // Check if ffmpeg is available if (!ffmpeg) { return -1; } // Set default values AnalysisFrames = AnalysisFrames || 25; CropdetectLimit = CropdetectLimit || "0.24"; CropdetectRound = CropdetectRound || 2; MinCropDifference = MinCropDifference || 10; ConsistencyThreshold = ConsistencyThreshold || 65; // Get the input file path from Flow.WorkingFile var fi = FileInfo(Flow.WorkingFile); Logger.ILog(`Starting black bar detection for: ${fi.Name}`); // Get video duration let duration = getVideoDuration(fi.FullName); if (duration <= 0) { Logger.ELog("Could not determine video duration"); return -1; } Logger.ILog(`Video duration: ${duration} seconds`); // Calculate segment times (11 segments total, use middle 9) let totalSegments = 11; let segmentDuration = duration / totalSegments; let analysisSegments = []; // Skip first and last segment (segments 2-10, 0-indexed: 1-9) for (let i = 1; i <= 9; i++) { let startTime = i * segmentDuration; analysisSegments.push(startTime); } Logger.ILog("Analyzing " + analysisSegments.length + " segments with " + AnalysisFrames + " frames each (consistency threshold: " + ConsistencyThreshold + "%)"); // Use global variables that get updated from every segment const globalCropFrequencyMap = {}; let totalCropMatches = 0; let originalWidth = 0; let originalHeight = 0; // Create global counter object to pass by reference let globalCounters = { totalMatches: 0 }; // Analyze all segments together using the original approach for (let i = 0; i < analysisSegments.length; i++) { let startTime = analysisSegments[i]; Logger.ILog("Analyzing segment " + (i + 1) + "/" + analysisSegments.length + " at " + startTime.toFixed(2) + "s"); let segmentResult = analyzeCropDetectOriginal(fi.FullName, startTime, AnalysisFrames, CropdetectLimit, CropdetectRound, globalCropFrequencyMap, globalCounters); if (segmentResult) { // Store original dimensions (should be consistent across segments) if (segmentResult.originalWidth && segmentResult.originalHeight) { originalWidth = segmentResult.originalWidth; originalHeight = segmentResult.originalHeight; } } Flow.PartPercentageUpdate(((i + 1) / analysisSegments.length) * 100); } // Get the final total from the global counter totalCropMatches = globalCounters.totalMatches; if (totalCropMatches === 0) { Logger.ILog("No crop data detected in any segment"); return 1; } if (totalCropMatches < 75 ) { Logger.ILog("Error counting crop matches. Total < 75 lines."); return -1; } Logger.ILog(`Total crop line matches found: ${totalCropMatches}`); // Logger.ILog(`Global crop frequency map: ${JSON.stringify(globalCropFrequencyMap)}`); // Find the most consistent crop value let finalCrop = findMostConsistentCrop(globalCropFrequencyMap, totalCropMatches, ConsistencyThreshold, MinCropDifference, originalWidth, originalHeight); if (finalCrop) { let cropParam = `crop=${finalCrop.width}:${finalCrop.height}:${finalCrop.x}:${finalCrop.y},`; Variables.CropParam = cropParam; Logger.ILog(`Black bars detected! Crop parameters: ${cropParam}`); Flow.AdditionalInfoRecorder("Crop", cropParam, 1000); Flow.AdditionalInfoRecorder("Original", `${originalWidth}x${originalHeight}`, 1000); Flow.AdditionalInfoRecorder("Cropped", `${finalCrop.width}x${finalCrop.height}`, 1000); return 2; } else { Logger.ILog(`No consistent black bars detected (need ${(ConsistencyThreshold)}% consistency)`); return 1; } } function getVideoDuration(inputFile) { var executeArgs = new ExecuteArgs(); executeArgs.command = ffprobe; executeArgs.argumentList = [ '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', inputFile ]; let duration = 0; executeArgs.add_Output((line) => { // ffprobe outputs duration as a decimal number (seconds) let durationValue = parseFloat(line.trim()); if (!isNaN(durationValue) && durationValue > 0) { duration = durationValue; } }); let result = Flow.Execute(executeArgs); if (result.exitCode !== 0) { Logger.WLog("FFprobe failed to get video duration"); return 0; } return duration; } function analyzeCropDetectOriginal(inputFile, startTime, frames, limit, round, globalCropFrequencyMap, globalCounters) { const executeArgs = new ExecuteArgs(); executeArgs.command = ffmpeg; executeArgs.argumentList = [ '-ss', startTime.toString(), '-i', inputFile, '-vframes', frames.toString(), '-vf', 'cropdetect=' + limit + ':' + round + ':0', '-f', 'null', '-' ]; let originalWidth = 0; let originalHeight = 0; executeArgs.add_Error(function(line) { const cropMatch = line.match(/crop=(\d+):(\d+):(\d+):(\d+)/); if (cropMatch) { const cropKey = cropMatch[0]; // e.g., "crop=3840:2160:60:40" globalCropFrequencyMap[cropKey] = (globalCropFrequencyMap[cropKey] || 0) + 1; globalCounters.totalMatches++; // Increment global counter } const streamMatch = line.match(/Stream.*Video.*?(\d+)x(\d+)/); if (streamMatch) { originalWidth = parseInt(streamMatch[1]); originalHeight = parseInt(streamMatch[2]); } }); const result = Flow.Execute(executeArgs); if (result.exitCode !== 0) { Logger.WLog("FFmpeg cropdetect failed for segment at " + startTime + "s"); return null; } return { originalWidth: originalWidth, originalHeight: originalHeight }; } function findMostConsistentCrop(globalCropFrequencyMap, totalMatches, consistencyThreshold, minDifference, originalWidth, originalHeight) { if (totalMatches === 0 || Object.keys(globalCropFrequencyMap).length === 0) { return null; } const requiredMatches = Math.ceil(totalMatches * (consistencyThreshold / 100)); Logger.ILog("Looking for crop with at least " + requiredMatches + " matches out of " + totalMatches + " total (" + consistencyThreshold + "% threshold)"); // Find the crop with the highest frequency let bestCropKey = null; let bestFrequency = 0; for (let cropKey in globalCropFrequencyMap) { let frequency = globalCropFrequencyMap[cropKey]; let percentage = ((frequency / totalMatches) * 100); Logger.ILog("Crop " + cropKey + ": " + frequency + " matches (" + percentage.toFixed(1) + "%)"); // Error check: if percentage > 100%, something is wrong if (percentage > 100) { Logger.ELog("Error: Crop frequency percentage is " + percentage.toFixed(1) + "% which is > 100%. Data corruption detected."); return -1; } if (frequency > bestFrequency) { bestCropKey = cropKey; bestFrequency = frequency; } } // Check if the best crop meets the consistency threshold if (bestFrequency < requiredMatches) { Logger.ILog(`Best crop ${bestCropKey} has ${bestFrequency} matches, but need ${requiredMatches} minimum`); return null; } // Parse the crop parameters const cropMatch = bestCropKey.match(/crop=(\d+):(\d+):(\d+):(\d+)/); if (!cropMatch) { Logger.ELog(`Failed to parse crop parameters from ${bestCropKey}`); return null; } const cropData = { width: parseInt(cropMatch[1]), height: parseInt(cropMatch[2]), x: parseInt(cropMatch[3]), y: parseInt(cropMatch[4]) }; // Check if cropping is actually needed (significant difference from original) if (originalWidth && originalHeight) { let widthDiff = originalWidth - cropData.width; let heightDiff = originalHeight - cropData.height; Logger.ILog(`Original dimensions: ${originalWidth}x${originalHeight}`); Logger.ILog(`Crop dimensions: ${cropData.width}x${cropData.height}`); Logger.ILog(`Dimension differences: width=${widthDiff}, height=${heightDiff} (threshold=${minDifference})`); if (widthDiff < minDifference && heightDiff < minDifference) { Logger.ILog(`Crop difference too small (${widthDiff}x${heightDiff}), skipping crop`); return null; } } Logger.ILog(`Consistent crop found: ${bestCropKey} with ${bestFrequency}/${totalMatches} matches (${((bestFrequency / totalMatches) * 100).toFixed(1)}%)`); return cropData; }