pull/3020/merge
Nexusnui 7 days ago committed by GitHub
commit 58e9998ce6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -160,6 +160,8 @@
"Create segments from keyframes": "Create segments from keyframes",
"Create segments from subtitles": "Create segments from subtitles",
"Crop": "Crop",
"Crop Values with a lower or equal height than this will be part of a segment. Is not used if 0. If width and height are 0 every crop change indicates a new segment": "Crop Values with a lower or equal height than this will be part of a segment. Is not used if 0. If width and height are 0 every crop change indicates a new segment",
"Crop Values with a lower or equal width than this will be part of a segment. Is not used if 0. If width and height are 0 every crop change indicates a new segment": "Crop Values with a lower or equal width than this will be part of a segment. Is not used if 0. If width and height are 0 every crop change indicates a new segment",
"CSV files": "CSV files",
"Ctrl": "Ctrl",
"CUE files": "CUE files",
@ -190,9 +192,11 @@
"Depending on your specific file/player, you may have to try different options for best results.": "Depending on your specific file/player, you may have to try different options for best results.",
"Deselect all segments": "Deselect all segments",
"Detect black scenes": "Detect black scenes",
"Detect cropped scenes": "Detect cropped scenes",
"Detect scene changes": "Detect scene changes",
"Detect silent scenes": "Detect silent scenes",
"Detecting black scenes": "Detecting black scenes",
"Detecting cropped scenes": "Detecting cropped scenes",
"Detecting scene changes": "Detecting scene changes",
"Detecting silent scenes": "Detecting silent scenes",
"Disable <1>merge options</1>": "Disable <1>merge options</1>",
@ -304,6 +308,7 @@
"Failed to convert file. Try a different conversion": "Failed to convert file. Try a different conversion.",
"Failed to convert files:": "Failed to convert files:",
"Failed to detect black scenes": "Failed to detect black scenes",
"Failed to detect cropped scenes": "Failed to detect cropped scenes",
"Failed to detect scene changes": "Failed to detect scene changes",
"Failed to detect silent scenes": "Failed to detect silent scenes",
"Failed to download URL": "Failed to download URL",
@ -688,9 +693,12 @@
"Set custom start offset/timecode": "Set custom start offset/timecode",
"Set custom start time offset": "Set custom start time offset",
"Set file modification date/time of output files to:": "Set file modification date/time of output files to:",
"Set higher black value threshold, which can be optionally specified from nothing (0) to everything (255 for 8-bit based formats). An intensity value greater to the set value is considered non-black. It defaults to 24. You can also specify a value between 0.0 and 1.0 which will be scaled depending on the bitdepth of the pixel format. ": "Set higher black value threshold, which can be optionally specified from nothing (0) to everything (255 for 8-bit based formats). An intensity value greater to the set value is considered non-black. It defaults to 24. You can also specify a value between 0.0 and 1.0 which will be scaled depending on the bitdepth of the pixel format. ",
"Set minimum duration in seconds that is required to create a new segment.": "Set minimum duration in seconds that is required to create a new segment.",
"Set minimum silence duration that will be converted into a segment.": "Set minimum silence duration that will be converted into a segment.",
"Set noise tolerance. Can be specified in dB (in case \"dB\" is appended to the specified value) or amplitude ratio. Default is -60dB, or 0.001.": "Set noise tolerance. Can be specified in dB (in case \"dB\" is appended to the specified value) or amplitude ratio. Default is -60dB, or 0.001.",
"Set output rotation. Current: ": "Set output rotation. Current: ",
"Set the counter that determines after how many frames cropdetect will reset the previously detected largest video area and start over to detect the current optimal crop area. Default ffmpeg value is 0, but for detecting segments it is >=1. This can be useful when channel logos distort the video area. 0 indicates never reset, and returns the largest area encountered during playback. ": "Set the counter that determines after how many frames cropdetect will reset the previously detected largest video area and start over to detect the current optimal crop area. Default ffmpeg value is 0, but for detecting segments it is >=1. This can be useful when channel logos distort the video area. 0 indicates never reset, and returns the largest area encountered during playback. ",
"Set the minimum detected black duration expressed in seconds. It must be a non-negative floating point number.": "Set the minimum detected black duration expressed in seconds. It must be a non-negative floating point number.",
"Set the threshold for considering a picture \"black\".": "Set the threshold for considering a picture \"black\".",
"Set the threshold for considering a pixel \"black\".": "Set the threshold for considering a pixel \"black\".",

@ -1,6 +1,6 @@
import type { SupportedLanguage } from './i18n.ts';
export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'captureSnapshotToClipboard' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'removeCurrentCutpoint' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'selectSegmentsAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'jumpSeekFirstSegment' | 'jumpSeekPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'jumpSeekLastSegment' | 'jumpSeekNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'makeCursorTimeZero' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'togglePlaySelectedSegments' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'readAllKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createFixedByteSizedSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'decimate' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripVideo' | 'toggleStripSubtitle' | 'toggleStripThumbnail' | 'toggleStripCurrentFilter' | 'toggleStripAll' | 'toggleDarkMode' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit' | 'selectAllMarkers' | 'generateOverviewWaveform' | 'selectSegmentsByLabel' | 'selectSegmentsByExpr' | 'labelSelectedSegments' | 'mutateSegmentsByExpr';
export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'captureSnapshotToClipboard' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'removeCurrentCutpoint' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'selectSegmentsAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'jumpSeekFirstSegment' | 'jumpSeekPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'jumpSeekLastSegment' | 'jumpSeekNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'detectCroppedScenes' |'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'makeCursorTimeZero' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'togglePlaySelectedSegments' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'readAllKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createFixedByteSizedSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'decimate' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripVideo' | 'toggleStripSubtitle' | 'toggleStripThumbnail' | 'toggleStripCurrentFilter' | 'toggleStripAll' | 'toggleDarkMode' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit' | 'selectAllMarkers' | 'generateOverviewWaveform' | 'selectSegmentsByLabel' | 'selectSegmentsByExpr' | 'labelSelectedSegments' | 'mutateSegmentsByExpr';
export interface KeyBinding {
keys: string,

@ -472,6 +472,145 @@ export async function silenceDetect({ streamId, filterOptions, ...rest }: {
});
}
export async function cropDetect({ filePath, streamId, limit, reset_count, minSegmentDuration, width, height, onProgress, onSegmentDetected, from, to, ffmpegHwaccel }: {
filePath: string,
streamId: number | undefined
limit: number | string,
reset_count: number | string,
minSegmentDuration: string,
width: string,
height: string,
onProgress: (p: number) => void,
onSegmentDetected: (p: DetectedSegment) => void,
from: number,
to: number,
ffmpegHwaccel: FfmpegHwAccel,
}) {
const args = [
'-hide_banner',
...getHwaccelArgs(ffmpegHwaccel),
...getInputSeekArgs({ filePath, from, to }),
'-map', streamId != null ? `0:${streamId}` : 'v:0',
'-filter:v', `cropdetect=reset_count=${reset_count}:limit=${limit}:round=1`,
'-f', 'null', '-',
];
const process = runFfmpegProcess(args, { buffer: false });
handleProgress(process, to - from, onProgress);
assert(process.stdout != null);
const rl = readline.createInterface({ input: process.stderr});
const minDuration = parseFloat(minSegmentDuration);
const maxWidth = parseInt(width);
const maxHeight = parseInt(height);
let lastTime: number = 0;
let currentStartTime: number = 0;
let currentEndTime: number = 0;
if (maxWidth == 0 && maxHeight == 0){
let nextStartTime: number = 0;
let currentCrop: string ="StartValue";
let lastCrop: string | undefined;
let setNextSegmnent: boolean = false;
rl.on('line', (line) => {
// cropdetect output => match[1]=pts_time,match[2]=crop values, match[3]=w, match[4]=h, match[5]=x, match[6]=y
const match = line.match(/\s+t:([\d.]+)\s+limit:[\d.]+\s+crop=((\d+):(\d+):(\d+):(\d+))$/);
if (!match) return;
const time = parseFloat(match[1]!);
const crop = match[2]!;
if (!Number.isNaN(time)) {
if (setNextSegmnent && time > lastTime) {
// Next segment was reset in previous iteration and current segment was extended
nextStartTime = time;
setNextSegmnent = false;
} else if (lastCrop != null && time > lastTime && crop != lastCrop) {
if (currentCrop == "StartValue") {
// First Crop Change
currentEndTime = lastTime;
nextStartTime = time;
currentCrop = lastCrop;
}else if (lastTime - nextStartTime >= minDuration){
// Next Segment is at least the min duration required for a segment
// Current Segment will be created and next segmnent will be set to the be the current one
onSegmentDetected({ start: from + currentStartTime, end: from + currentEndTime });
currentStartTime = nextStartTime;
currentEndTime = lastTime;
nextStartTime = time;
} else if (crop == currentCrop){
// Next Segment is not long enough and the crop values are the same as the current segment
// Reset next Segment
currentEndTime = time;
nextStartTime = 0;
setNextSegmnent = true;
}
// If none of the cases is true the next segment will be extended to the next crop change
}
lastTime = time;
lastCrop = crop;
}
});
await process;
// Create Last 1-2 Segments
if(!setNextSegmnent && nextStartTime > 0 && currentStartTime != null && lastTime != null && lastTime - nextStartTime >= minDuration){
onSegmentDetected({ start: from + currentStartTime!, end: from + currentEndTime! });
onSegmentDetected({ start: from + nextStartTime!, end: from + lastTime! });
}else if(currentStartTime != null && lastTime != null) {
onSegmentDetected({ start: from + currentStartTime!, end: from + lastTime! });
}
} else {
let skippedStart: number = 0;
let skipping: boolean = false;
rl.on('line', (line) => {
// cropdetect output => match[1]=pts_time,match[2]=crop values, match[3]=w, match[4]=h, match[5]=x, match[6]=y
const match = line.match(/\s+t:([\d.]+)\s+limit:[\d.]+\s+crop=((\d+):(\d+):(\d+):(\d+))$/);
if (!match) return;
const time = parseFloat(match[1]!);
const widthTest = maxWidth == 0 || parseInt(match[3]!) - maxWidth <= 0;
const heightTest = maxHeight == 0 || parseInt(match[4]!) - maxHeight <= 0;
if (!Number.isNaN(time)) {
if (time > lastTime){
if(widthTest && heightTest) {
if (skipping){
if (lastTime-skippedStart > minDuration){
// Skipped duration is long enough to be left out
if (currentEndTime - currentStartTime >= minDuration){
// Current Segment is long enough
onSegmentDetected({ start: from + currentStartTime, end: from + currentEndTime });
}
currentStartTime = time;
}
skipping = false;
}
currentEndTime = time;
} else {
if (!skipping){
skipping = true;
skippedStart = time;
}
}
}
lastTime = time;
}
});
await process;
// Create Last Segment
if(currentStartTime != null && currentEndTime != null && currentEndTime - currentStartTime >= minDuration) {
onSegmentDetected({ start: from + currentStartTime, end: from + currentEndTime });
}
}
return { ffmpegArgs: args };
}
function getQualityOpts({ captureFormat, quality }: { captureFormat: CaptureFormat, quality: number }) {
if (captureFormat === 'jpeg') return ['-q:v', String(getFfmpegJpegQuality(quality))];
if (captureFormat === 'webp') return ['-q:v', String(Math.max(0, Math.min(100, Math.round(quality * 100))))];

@ -422,6 +422,12 @@ export default ({ app, mainWindow, newVersion, isStoreBuild, openExternal }: {
mainWindow.webContents.send('detectSceneChanges');
},
},
{
label: esc(t('Detect cropped scenes')),
click() {
mainWindow.webContents.send('detectCroppedScenes');
},
},
{
label: esc(t('Read all keyframes')),
click() {

@ -352,7 +352,7 @@ function App() {
}, [isFileOpened]);
const {
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, modifySelectedSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, getSegEstimatedSize, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, clearSegColorCounter, loadCutSegments, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, segmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, maybeCreateFullLengthSegment, currentCutSegOrWholeTimeline, segColorCounter,
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, detectCroppedScenes, removeSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, modifySelectedSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, getSegEstimatedSize, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, clearSegColorCounter, loadCutSegments, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, segmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, maybeCreateFullLengthSegment, currentCutSegOrWholeTimeline, segColorCounter,
} = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta: mainFileMeta?.ffprobeMeta, seekAbs, activeVideoStreamIndex, activeAudioStreamIndexes, handleError, showGenericDialog, simpleMode, ffmpegHwaccel });
const { getEdlFilePath, projectFileSavePath, getProjectFileSavePath } = useSegmentsAutoSave({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, customOutDir, cutSegments });
@ -2181,6 +2181,7 @@ function App() {
detectBlackScenes,
detectSilentScenes,
detectSceneChanges,
detectCroppedScenes,
readAllKeyframes,
createSegmentsFromKeyframes,
toggleWaveformMode,
@ -2200,7 +2201,7 @@ function App() {
};
return ret;
}, [togglePlaySelectedSegments, toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, captureSnapshotToClipboard, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, increaseRotation, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, toggleLastCommands, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createFixedByteSizedSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripVideo, toggleStripSubtitle, toggleStripThumbnail, toggleStripAll, toggleDarkMode, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, tryDecimate, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, detectBlackScenes, detectSilentScenes, detectSceneChanges, readAllKeyframes, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, selectAllMarkers, selectSegmentsByLabel, selectSegmentsByExpr, labelSelectedSegments, mutateSegmentsByExpr, toggleKeyboardShortcuts, generateOverviewWaveform, checkFileOpened, cutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, goToTimecode, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, zoomRel, batchFileJump, removeSegment, currentSegIndexSafe, cutSegmentsHistory, labelSegment, onExportPress, userHtml5ifyCurrentFile, toggleKeyframeCut, applyEnabledStreamsFilter, setPlaybackVolume, commandedTimeRef, closeFileWithConfirm, openSendReportDialogWithState]);
}, [togglePlaySelectedSegments, toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, captureSnapshotToClipboard, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, increaseRotation, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, toggleLastCommands, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createFixedByteSizedSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripVideo, toggleStripSubtitle, toggleStripThumbnail, toggleStripAll, toggleDarkMode, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, tryDecimate, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, detectBlackScenes, detectSilentScenes, detectSceneChanges, detectCroppedScenes, readAllKeyframes, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, selectAllMarkers, selectSegmentsByLabel, selectSegmentsByExpr, labelSelectedSegments, mutateSegmentsByExpr, toggleKeyboardShortcuts, generateOverviewWaveform, checkFileOpened, cutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, goToTimecode, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, zoomRel, batchFileJump, removeSegment, currentSegIndexSafe, cutSegmentsHistory, labelSegment, onExportPress, userHtml5ifyCurrentFile, toggleKeyframeCut, applyEnabledStreamsFilter, setPlaybackVolume, commandedTimeRef, closeFileWithConfirm, openSendReportDialogWithState]);
const getKeyboardAction = useCallback((action: MainKeyboardAction) => mainActions[action], [mainActions]);

@ -472,6 +472,10 @@ const KeyboardShortcuts = memo(({
name: t('Detect scene changes'),
category: segmentsAndCutpointsCategory,
},
detectCroppedScenes: {
name: t('Detect cropped scenes'),
category: segmentsAndCutpointsCategory,
},
shuffleSegments: {
name: t('Shuffle segments order'),
category: segmentsAndCutpointsCategory,

@ -40,6 +40,28 @@ const parametersRaw = {
hint: () => i18n.t('Minimum change between two frames to be considered a new scene. A value between 0.3 and 0.5 is generally a sane choice.'),
},
},
cropdetect: {
limit: {
value: '24',
hint: () => i18n.t('Set higher black value threshold, which can be optionally specified from nothing (0) to everything (255 for 8-bit based formats). An intensity value greater to the set value is considered non-black. It defaults to 24. You can also specify a value between 0.0 and 1.0 which will be scaled depending on the bitdepth of the pixel format. '),
},
reset_count: {
value: '1',
hint: () => i18n.t('Set the counter that determines after how many frames cropdetect will reset the previously detected largest video area and start over to detect the current optimal crop area. Default ffmpeg value is 0, but for detecting segments it is >=1. This can be useful when channel logos distort the video area. 0 indicates never reset, and returns the largest area encountered during playback. '),
},
minSegmentDuration: {
value: '1',
hint: () => i18n.t('Set minimum duration in seconds that is required to create a new segment.'),
},
width: {
value: '0',
hint: () => i18n.t('Crop Values with a lower or equal width than this will be part of a segment. Is not used if 0. If width and height are 0 every crop change indicates a new segment'),
},
height: {
value: '0',
hint: () => i18n.t('Crop Values with a lower or equal height than this will be part of a segment. Is not used if 0. If width and height are 0 every crop change indicates a new segment'),
},
},
};
export type FfmpegDialog = keyof typeof parametersRaw;

@ -35,7 +35,7 @@ import type { FfmpegHwAccel } from '../../../common/types';
import mainApi from '../mainApi';
const remote = window.require('@electron/remote');
const { ffmpeg: { blackDetect, silenceDetect } } = remote.require('./index.js');
const { ffmpeg: { blackDetect, silenceDetect, cropDetect } } = remote.require('./index.js');
type ParameterDialogParameters = Record<string, string>;
@ -350,6 +350,27 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
await detectSegments({ name: 'sceneChanges', workingText: i18n.t('Detecting scene changes'), errorText: i18n.t('Failed to detect scene changes'), fn: async (onSegmentDetected) => ffmpegDetectSceneChanges({ filePath, streamId: activeVideoStreamIndex, minChange, onProgress: setProgress, onSegmentDetected, from: start, to: end, ffmpegHwaccel }) });
}, [activeVideoStreamIndex, currentCutSegOrWholeTimeline, deleteCurrentCutSeg, detectSegments, ffmpegHwaccel, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress, showParametersDialog]);
const detectCroppedScenes = useCallback(async () => {
const { start, end } = currentCutSegOrWholeTimeline;
deleteCurrentCutSeg();
const dialogType = 'cropdetect';
const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#cropdetect' });
if (parameters == null) return;
setFfmpegParametersForDialog(dialogType, parameters);
invariant(filePath != null)
const limit = parameters['limit'];
const reset_count = parameters['reset_count'];
const minSegmentDuration = parameters['minSegmentDuration'];
const width = parameters['width'];
const height = parameters['height'];
invariant(limit != null);
invariant(reset_count != null);
invariant(minSegmentDuration != null);
invariant(width != null);
invariant(height != null);
await detectSegments({ name: 'croppedScenes', workingText: i18n.t('Detecting cropped scenes'), errorText: i18n.t('Failed to detect cropped scenes'), fn: async (onSegmentDetected) => cropDetect({ filePath, streamId: activeVideoStreamIndex, limit, reset_count, minSegmentDuration, width, height , onProgress: setProgress, onSegmentDetected, from: start, to: end, ffmpegHwaccel }) });
}, [activeVideoStreamIndex, currentCutSegOrWholeTimeline, deleteCurrentCutSeg, detectSegments, ffmpegHwaccel, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress, showParametersDialog]);
const createSegmentsFromKeyframes = useCallback(async () => {
const { start, end } = currentCutSegOrWholeTimeline;
deleteCurrentCutSeg();
@ -967,6 +988,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
detectBlackScenes,
detectSilentScenes,
detectSceneChanges,
detectCroppedScenes,
removeSegment,
invertAllSegments,
fillSegmentsGaps,

Loading…
Cancel
Save