Detect Black Bars
🧩 Syntax:
/**
* @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;
}