|
|
|
|
@ -13,15 +13,28 @@ import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegmen
|
|
|
|
|
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, addSegmentColorIndex } from '../segments';
|
|
|
|
|
import * as ffmpegParameters from '../ffmpeg-parameters';
|
|
|
|
|
import { maxSegmentsAllowed } from '../util/constants';
|
|
|
|
|
import { ParseTimecode, SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
|
|
|
|
|
import { ApparentCutSegment, ParseTimecode, SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
|
|
|
|
|
import safeishEval from '../worker/eval';
|
|
|
|
|
import { ScopeSegment } from '../../../../types';
|
|
|
|
|
import { FFprobeStream } from '../../../../ffprobe';
|
|
|
|
|
|
|
|
|
|
const { ffmpeg: { blackDetect, silenceDetect } } = window.require('@electron/remote').require('./index.js');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode }: {
|
|
|
|
|
filePath?: string | undefined, workingRef: MutableRefObject<boolean>, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, setCutProgress: (a: number | undefined) => void, videoStream, duration?: number | undefined, getRelevantTime: () => number, maxLabelLength: number, checkFileOpened: () => boolean, invertCutSegments: boolean, segmentsToChaptersOnly: boolean, timecodePlaceholder: string, parseTimecode: ParseTimecode,
|
|
|
|
|
filePath?: string | undefined,
|
|
|
|
|
workingRef: MutableRefObject<boolean>,
|
|
|
|
|
setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void,
|
|
|
|
|
setCutProgress: (a: number | undefined) => void,
|
|
|
|
|
videoStream: FFprobeStream | undefined,
|
|
|
|
|
duration?: number | undefined,
|
|
|
|
|
getRelevantTime: () => number,
|
|
|
|
|
maxLabelLength: number,
|
|
|
|
|
checkFileOpened: () => boolean,
|
|
|
|
|
invertCutSegments: boolean,
|
|
|
|
|
segmentsToChaptersOnly: boolean,
|
|
|
|
|
timecodePlaceholder: string,
|
|
|
|
|
parseTimecode: ParseTimecode,
|
|
|
|
|
}) {
|
|
|
|
|
// Segment related state
|
|
|
|
|
const segCounterRef = useRef(0);
|
|
|
|
|
@ -165,7 +178,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
loadCutSegments(newSegments, true);
|
|
|
|
|
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, filePath, loadCutSegments, videoStream]);
|
|
|
|
|
|
|
|
|
|
const removeSegments = useCallback((removeSegmentIds) => {
|
|
|
|
|
const removeSegments = useCallback((removeSegmentIds: string[]) => {
|
|
|
|
|
setCutSegments((existingSegments) => {
|
|
|
|
|
if (existingSegments.length === 1 && existingSegments[0]!.start == null && existingSegments[0]!.end == null) {
|
|
|
|
|
return existingSegments; // We are at initial segment, nothing more we can do (it cannot be removed)
|
|
|
|
|
@ -181,7 +194,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
});
|
|
|
|
|
}, [clearSegCounter, createInitialCutSegments, setCutSegments]);
|
|
|
|
|
|
|
|
|
|
const removeCutSegment = useCallback((index) => {
|
|
|
|
|
const removeCutSegment = useCallback((index: number) => {
|
|
|
|
|
removeSegments([cutSegments[index]!.segId]);
|
|
|
|
|
}, [cutSegments, removeSegments]);
|
|
|
|
|
|
|
|
|
|
@ -247,9 +260,9 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
updateSegAtIndex(currentSegIndexSafe, { [type]: Math.min(Math.max(time, 0), duration) });
|
|
|
|
|
}, [currentSegIndexSafe, getSegApparentEnd, currentCutSeg, duration, updateSegAtIndex]);
|
|
|
|
|
|
|
|
|
|
const modifySelectedSegmentTimes = useCallback(async (transformSegment, concurrency = 5) => {
|
|
|
|
|
const modifySelectedSegmentTimes = useCallback(async (transformSegment: <T extends ApparentCutSegment>(s: T) => Promise<T> | T, concurrency = 5) => {
|
|
|
|
|
if (duration == null) throw new Error();
|
|
|
|
|
const clampValue = (val) => Math.min(Math.max(val, 0), duration);
|
|
|
|
|
const clampValue = (val: number) => Math.min(Math.max(val, 0), duration);
|
|
|
|
|
|
|
|
|
|
let newSegments = await pMap(apparentCutSegments, async (segment) => {
|
|
|
|
|
if (!isSegmentSelected(segment)) return segment; // pass thru non-selected segments
|
|
|
|
|
@ -287,13 +300,13 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
await modifySelectedSegmentTimes(async (segment) => {
|
|
|
|
|
const newSegment = { ...segment };
|
|
|
|
|
|
|
|
|
|
async function align(key) {
|
|
|
|
|
const align = async (key: 'start' | 'end') => {
|
|
|
|
|
const time = newSegment[key];
|
|
|
|
|
if (filePath == null) throw new Error();
|
|
|
|
|
const keyframe = await findKeyframeNearTime({ filePath, streamIndex: videoStream.index, time, mode });
|
|
|
|
|
if (keyframe == null) throw new Error(`Cannot find any keyframe within 60 seconds of frame ${time}`);
|
|
|
|
|
newSegment[key] = keyframe;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if (startOrEnd.includes('start')) await align('start');
|
|
|
|
|
if (startOrEnd.includes('end')) await align('end');
|
|
|
|
|
return newSegment;
|
|
|
|
|
@ -305,7 +318,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
}
|
|
|
|
|
}, [filePath, videoStream, modifySelectedSegmentTimes, setWorking, workingRef]);
|
|
|
|
|
|
|
|
|
|
const updateSegOrder = useCallback((index, newOrder) => {
|
|
|
|
|
const updateSegOrder = useCallback((index: number, newOrder: number) => {
|
|
|
|
|
if (newOrder > cutSegments.length - 1 || newOrder < 0) return;
|
|
|
|
|
const newSegments = [...cutSegments];
|
|
|
|
|
const removedSeg = newSegments.splice(index, 1)[0];
|
|
|
|
|
@ -315,7 +328,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
setCurrentSegIndex(newOrder);
|
|
|
|
|
}, [cutSegments, setCurrentSegIndex, setCutSegments]);
|
|
|
|
|
|
|
|
|
|
const updateSegOrders = useCallback((newOrders) => {
|
|
|
|
|
const updateSegOrders = useCallback((newOrders: string[]) => {
|
|
|
|
|
const newSegments = sortBy(cutSegments, (seg) => newOrders.indexOf(seg.segId));
|
|
|
|
|
const newCurrentSegIndex = newOrders.indexOf(currentCutSeg.segId);
|
|
|
|
|
setCutSegments(newSegments);
|
|
|
|
|
@ -351,7 +364,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
}
|
|
|
|
|
}, [currentCutSeg.start, currentCutSeg.end, getRelevantTime, duration, cutSegments, createIndexedSegment, setCutSegments, setCurrentSegIndex]);
|
|
|
|
|
|
|
|
|
|
const duplicateSegment = useCallback((segment) => {
|
|
|
|
|
const duplicateSegment = useCallback((segment: Pick<StateSegment, 'start' | 'end'> & Partial<Pick<StateSegment, 'name'>>) => {
|
|
|
|
|
try {
|
|
|
|
|
// Cannot duplicate if seg is not finished
|
|
|
|
|
if (segment.start === undefined && segment.end === undefined) return;
|
|
|
|
|
@ -411,7 +424,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
}
|
|
|
|
|
}, [checkFileOpened, getRelevantTime, setCutTime]);
|
|
|
|
|
|
|
|
|
|
const onLabelSegment = useCallback(async (index) => {
|
|
|
|
|
const onLabelSegment = useCallback(async (index: number) => {
|
|
|
|
|
const { name } = cutSegments[index]!;
|
|
|
|
|
const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength });
|
|
|
|
|
if (value != null) updateSegAtIndex(index, { name: value });
|
|
|
|
|
@ -438,7 +451,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
const segment = cutSegments[firstSegmentAtCursorIndex];
|
|
|
|
|
if (segment == null) throw new Error();
|
|
|
|
|
|
|
|
|
|
const getNewName = (oldName, suffix) => oldName && `${segment.name} ${suffix}`;
|
|
|
|
|
const getNewName = (oldName: string, suffix: string) => oldName && `${segment.name} ${suffix}`;
|
|
|
|
|
|
|
|
|
|
const firstPart = createIndexedSegment({ segment: { name: getNewName(segment.name, '1'), start: segment.start, end: relevantTime }, incrementCount: false });
|
|
|
|
|
const secondPart = createIndexedSegment({ segment: { name: getNewName(segment.name, '2'), start: relevantTime, end: segment.end }, incrementCount: true });
|
|
|
|
|
@ -544,8 +557,8 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
|
|
|
|
|
|
|
|
|
|
const removeSelectedSegments = useCallback(() => removeSegments(selectedSegmentsRaw.map((seg) => seg.segId)), [removeSegments, selectedSegmentsRaw]);
|
|
|
|
|
|
|
|
|
|
const selectOnlySegment = useCallback((seg) => setDeselectedSegmentIds(Object.fromEntries(cutSegments.filter((s) => s.segId !== seg.segId).map((s) => [s.segId, true]))), [cutSegments]);
|
|
|
|
|
const toggleSegmentSelected = useCallback((seg) => setDeselectedSegmentIds((existing) => ({ ...existing, [seg.segId]: !existing[seg.segId] })), []);
|
|
|
|
|
const selectOnlySegment = useCallback((seg: Pick<StateSegment, 'segId'>) => setDeselectedSegmentIds(Object.fromEntries(cutSegments.filter((s) => s.segId !== seg.segId).map((s) => [s.segId, true]))), [cutSegments]);
|
|
|
|
|
const toggleSegmentSelected = useCallback((seg: Pick<StateSegment, 'segId'>) => setDeselectedSegmentIds((existing) => ({ ...existing, [seg.segId]: !existing[seg.segId] })), []);
|
|
|
|
|
const deselectAllSegments = useCallback(() => setDeselectedSegmentIds(Object.fromEntries(cutSegments.map((s) => [s.segId, true]))), [cutSegments]);
|
|
|
|
|
const invertSelectedSegments = useCallback(() => setDeselectedSegmentIds((existing) => Object.fromEntries(cutSegments.map((s) => [s.segId, !existing[s.segId]]))), [cutSegments]);
|
|
|
|
|
const selectAllSegments = useCallback(() => setDeselectedSegmentIds({}), []);
|
|
|
|
|
|