From 250218e2d0c464f05cb8dccddc1b5e90aa6d0401 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 18 Feb 2025 00:28:33 +0800 Subject: [PATCH] Split timeline by byte size closes #1219 --- src/main/menu.ts | 6 +++ src/renderer/src/App.tsx | 7 +-- .../src/components/KeyboardShortcuts.tsx | 4 ++ src/renderer/src/dialogs/index.tsx | 47 ++++++++++++++----- src/renderer/src/hooks/useSegments.ts | 24 +++++++--- src/renderer/src/segments.ts | 9 ++++ types.ts | 2 +- 7 files changed, 78 insertions(+), 21 deletions(-) diff --git a/src/main/menu.ts b/src/main/menu.ts index f94aa701..d40b12d1 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -266,6 +266,12 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: { mainWindow.webContents.send('createFixedDurationSegments'); }, }, + { + label: esc(t('Create fixed byte sized segments')), + click() { + mainWindow.webContents.send('createFixedByteSizedSegments'); + }, + }, { label: esc(t('Create random segments')), click() { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index cf8f63f8..15b99a95 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -335,8 +335,8 @@ function App() { }, [isFileOpened]); const { - cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onMutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, - } = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, durationSafe }); + cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onMutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, + } = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, durationSafe, mainFileMeta }); const { getEdlFilePath, projectFileSavePath, getProjectFileSavePath } = useSegmentsAutoSave({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, customOutDir, cutSegments }); @@ -1993,6 +1993,7 @@ function App() { combineSelectedSegments, createFixedDurationSegments, createNumSegments, + createFixedByteSizedSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, @@ -2049,7 +2050,7 @@ function App() { }; return ret; - }, [toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, increaseRotation, goToTimecode, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, onExportPress, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripThumbnail, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, checkFileOpened, cutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, setCurrentSegIndex, zoomRel, batchFileJump, removeCutSegment, currentSegIndexSafe, cutSegmentsHistory, onLabelSegment, toggleLastCommands, userHtml5ifyCurrentFile, toggleKeyframeCut, setPlaybackVolume, closeFileWithConfirm, openSendReportDialogWithState, detectBlackScenes, detectSilentScenes, detectSceneChanges]); + }, [toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, increaseRotation, goToTimecode, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, onExportPress, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createFixedByteSizedSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripThumbnail, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, checkFileOpened, cutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, setCurrentSegIndex, zoomRel, batchFileJump, removeCutSegment, currentSegIndexSafe, cutSegmentsHistory, onLabelSegment, toggleLastCommands, userHtml5ifyCurrentFile, toggleKeyframeCut, setPlaybackVolume, closeFileWithConfirm, openSendReportDialogWithState, detectBlackScenes, detectSilentScenes, detectSceneChanges]); const getKeyboardAction = useCallback((action: MainKeyboardAction) => mainActions[action], [mainActions]); diff --git a/src/renderer/src/components/KeyboardShortcuts.tsx b/src/renderer/src/components/KeyboardShortcuts.tsx index 22fcf5e9..6e4151fe 100644 --- a/src/renderer/src/components/KeyboardShortcuts.tsx +++ b/src/renderer/src/components/KeyboardShortcuts.tsx @@ -395,6 +395,10 @@ const KeyboardShortcuts = memo(({ name: t('Create num segments'), category: segmentsAndCutpointsCategory, }, + createFixedByteSizedSegments: { + name: t('Create fixed byte sized segments'), + category: segmentsAndCutpointsCategory, + }, createRandomSegments: { name: t('Create random segments'), category: segmentsAndCutpointsCategory, diff --git a/src/renderer/src/dialogs/index.tsx b/src/renderer/src/dialogs/index.tsx index e18bb8d8..80b8d1f8 100644 --- a/src/renderer/src/dialogs/index.tsx +++ b/src/renderer/src/dialogs/index.tsx @@ -235,7 +235,7 @@ export async function createNumSegments(fileDuration: number) { const exampleDuration = '00:00:05.123'; -async function askForSegmentDuration({ fileDuration, inputPlaceholder, parseTimecode }: { +export async function askForSegmentDuration({ fileDuration, inputPlaceholder, parseTimecode }: { fileDuration: number, inputPlaceholder: string, parseTimecode: ParseTimecode, }) { const { value } = await Swal.fire({ @@ -467,19 +467,44 @@ export async function showCleanupFilesDialog(cleanupChoicesIn: CleanupChoicesTyp return undefined; } -export async function createFixedDurationSegments({ fileDuration, inputPlaceholder, parseTimecode }: { - fileDuration: number, inputPlaceholder: string, parseTimecode: ParseTimecode, +function parseBytesHuman(str: string) { + const match = str.replaceAll(/\s/g, '').match(/^(\d+)([gkmt]?)b$/i); + if (!match) return undefined; + const size = parseInt(match[1]!, 10); + const unit = match[2]!.toLowerCase(); + if (unit === 't') return size * 1024 * 1024 * 1024 * 1024; + if (unit === 'g') return size * 1024 * 1024 * 1024; + if (unit === 'm') return size * 1024 * 1024; + if (unit === 'k') return size * 1024; + return size; +} + +export async function createFixedByteSixedSegments({ fileDuration, fileSize }: { + fileDuration: number, fileSize: number, }) { - const segmentDuration = await askForSegmentDuration({ fileDuration, inputPlaceholder, parseTimecode }); - if (segmentDuration == null) return undefined; - const edl: { start: number, end: number }[] = []; - for (let start = 0; start < fileDuration; start += segmentDuration) { - const end = start + segmentDuration; - edl.push({ start, end: end >= fileDuration ? fileDuration : end }); - } - return edl; + const example = '100 MB'; + const { value } = await Swal.fire({ + input: 'text', + showCancelButton: true, + inputValue: example, + inputPlaceholder: example, + text: i18n.t('Divide timeline into a number of segments with an approximate byte size'), + inputValidator: (v) => { + const bytes = parseBytesHuman(v); + if (bytes != null) return undefined; + return i18n.t('Please input a valid size. Example: {{example}}', { example }); + }, + }); + + if (value == null) return undefined; + + const parsed = parseBytesHuman(value); + invariant(parsed != null); + + return fileDuration * (parsed / fileSize); } + export async function createRandomSegments(fileDuration: number) { const response = await askForSegmentsRandomDurationRange(); if (response == null) return undefined; diff --git a/src/renderer/src/hooks/useSegments.ts b/src/renderer/src/hooks/useSegments.ts index 1a8fa7dc..bc946edf 100644 --- a/src/renderer/src/hooks/useSegments.ts +++ b/src/renderer/src/hooks/useSegments.ts @@ -9,19 +9,19 @@ import { detectSceneChanges as ffmpegDetectSceneChanges, readFrames, mapTimesToS import { handleError, shuffleArray } from '../util'; import { errorToast } from '../swal'; import { showParametersDialog } from '../dialogs/parameters'; -import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByExprDialog, mutateSegmentsByExprDialog } from '../dialogs'; -import { createSegment, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, addSegmentColorIndex, filterNonMarkers } from '../segments'; +import { createNumSegments as createNumSegmentsDialog, createFixedByteSixedSegments as createFixedByteSixedSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByExprDialog, mutateSegmentsByExprDialog, askForSegmentDuration } from '../dialogs'; +import { createSegment, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, addSegmentColorIndex, filterNonMarkers, makeDurationSegments } from '../segments'; import { parameters as allFfmpegParameters, FfmpegDialog } from '../ffmpegParameters'; import { maxSegmentsAllowed } from '../util/constants'; import { ParseTimecode, SegmentBase, segmentTagsSchema, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types'; import safeishEval from '../worker/eval'; import { ScopeSegment } from '../../../../types'; -import { FFprobeStream } from '../../../../ffprobe'; +import { FFprobeFormat, FFprobeStream } from '../../../../ffprobe'; const { ffmpeg: { blackDetect, silenceDetect } } = window.require('@electron/remote').require('./index.js'); -function useSegments({ filePath, workingRef, setWorking, setProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, durationSafe }: { +function useSegments({ filePath, workingRef, setWorking, setProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, durationSafe, mainFileMeta }: { filePath?: string | undefined, workingRef: MutableRefObject, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, @@ -37,6 +37,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea parseTimecode: ParseTimecode, appendFfmpegCommandLog: (args: string[]) => void, durationSafe: number, + mainFileMeta: { formatData: FFprobeFormat } | undefined, }) { // Segment related state const segCounterRef = useRef(0); @@ -528,10 +529,20 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea const createFixedDurationSegments = useCallback(async () => { if (!checkFileOpened() || !isDurationValid(duration)) return; - const segments = await createFixedDurationSegmentsDialog({ fileDuration: duration, inputPlaceholder: timecodePlaceholder, parseTimecode }); - if (segments) loadCutSegments(segments); + const segmentDuration = await askForSegmentDuration({ fileDuration: duration, inputPlaceholder: timecodePlaceholder, parseTimecode }); + if (segmentDuration == null) return; + loadCutSegments(makeDurationSegments(segmentDuration, duration)); }, [checkFileOpened, duration, loadCutSegments, parseTimecode, timecodePlaceholder]); + const createFixedByteSizedSegments = useCallback(async () => { + if (!checkFileOpened() || !isDurationValid(duration)) return; + const fileSize = mainFileMeta && parseInt(mainFileMeta.formatData.size, 10); + invariant(fileSize != null && !Number.isNaN(fileSize)); + const segmentDuration = await createFixedByteSixedSegmentsDialog({ fileDuration: duration, fileSize }); + if (segmentDuration == null) return; + loadCutSegments(makeDurationSegments(segmentDuration, duration)); + }, [checkFileOpened, duration, loadCutSegments, mainFileMeta]); + const createRandomSegments = useCallback(async () => { if (!checkFileOpened() || !isDurationValid(duration)) return; const segments = await createRandomSegmentsDialog(duration); @@ -707,6 +718,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, + createFixedByteSizedSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, diff --git a/src/renderer/src/segments.ts b/src/renderer/src/segments.ts index c291edcd..a7e34f55 100644 --- a/src/renderer/src/segments.ts +++ b/src/renderer/src/segments.ts @@ -253,3 +253,12 @@ export const filterNonMarkers = (segment ...rest, end, }] : [])); + +export function makeDurationSegments(segmentDuration: number, fileDuration: number) { + const edl: { start: number, end: number }[] = []; + for (let start = 0; start < fileDuration; start += segmentDuration) { + const end = start + segmentDuration; + edl.push({ start, end: end >= fileDuration ? fileDuration : end }); + } + return edl; +} diff --git a/types.ts b/types.ts index 7fac2b50..b2a58309 100644 --- a/types.ts +++ b/types.ts @@ -1,4 +1,4 @@ -export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'closeActiveScreen' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripThumbnail' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit'; +export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'closeActiveScreen' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | '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' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripThumbnail' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit'; export interface KeyBinding { keys: string,