lossless-cut/src/hooks/useSegments.ts

562 wiersze
26 KiB
TypeScript

import { useCallback, useRef, useMemo, useState, MutableRefObject } from 'react';
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
import i18n from 'i18next';
import pMap from 'p-map';
import sortBy from 'lodash/sortBy';
import { detectSceneChanges as ffmpegDetectSceneChanges, readFrames, mapTimesToSegments, findKeyframeNearTime } from '../ffmpeg';
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, selectSegmentsByTagDialog } from '../dialogs';
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2 } from '../segments';
import * as ffmpegParameters from '../ffmpeg-parameters';
import { maxSegmentsAllowed } from '../util/constants';
import { StateSegment } from '../types';
const remote = window.require('@electron/remote');
const { blackDetect, silenceDetect } = remote.require('./ffmpeg');
export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly }: {
filePath?: string, workingRef: MutableRefObject<boolean>, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, setCutProgress: (a: number | undefined) => void, videoStream, duration?: number, getRelevantTime: () => number, maxLabelLength: number, checkFileOpened: () => boolean, invertCutSegments: boolean, segmentsToChaptersOnly: boolean,
}) => {
// Segment related state
const segCounterRef = useRef(0);
const createIndexedSegment = useCallback(({ segment, incrementCount } = {}) => {
if (incrementCount) segCounterRef.current += 1;
const ret = createSegment({ segColorIndex: segCounterRef.current, ...segment });
return ret;
}, []);
const createInitialCutSegments = useCallback(() => [createIndexedSegment()], [createIndexedSegment]);
const [cutSegments, setCutSegments, cutSegmentsHistory] = useStateWithHistory(
createInitialCutSegments(),
100,
);
const [currentSegIndex, setCurrentSegIndex] = useState(0);
const [deselectedSegmentIds, setDeselectedSegmentIds] = useState({});
const isSegmentSelected = useCallback(({ segId }: { segId: string }) => !deselectedSegmentIds[segId], [deselectedSegmentIds]);
const clearSegCounter = useCallback(() => {
// eslint-disable-next-line no-param-reassign
segCounterRef.current = 0;
}, [segCounterRef]);
const clearSegments = useCallback(() => {
clearSegCounter();
setCutSegments(createInitialCutSegments());
}, [clearSegCounter, createInitialCutSegments, setCutSegments]);
const shuffleSegments = useCallback(() => setCutSegments((oldSegments) => shuffleArray(oldSegments)), [setCutSegments]);
const loadCutSegments = useCallback((edl, append = false) => {
const validEdl = edl.filter((row) => (
(row.start === undefined || row.end === undefined || row.start < row.end)
&& (row.start === undefined || row.start >= 0)
// TODO: Cannot do this because duration is not yet set when loading a file
// && (row.start === undefined || (row.start >= 0 && row.start < duration))
// && (row.end === undefined || row.end < duration)
));
if (validEdl.length === 0) throw new Error(i18n.t('No valid segments found'));
if (!append) clearSegCounter();
if (validEdl.length > maxSegmentsAllowed) throw new Error(i18n.t('Tried to create too many segments (max {{maxSegmentsAllowed}}.)', { maxSegmentsAllowed }));
setCutSegments((existingSegments) => {
const needToAppend = append && existingSegments.length > 1;
let newSegments = validEdl.map((segment, i) => createIndexedSegment({ segment, incrementCount: needToAppend || i > 0 }));
if (needToAppend) newSegments = [...existingSegments, ...newSegments];
return newSegments;
});
}, [clearSegCounter, createIndexedSegment, setCutSegments]);
const detectSegments = useCallback(async ({ name, workingText, errorText, fn }) => {
if (!filePath) return;
if (workingRef.current) return;
try {
setWorking({ text: workingText });
setCutProgress(0);
const newSegments = await fn();
console.log(name, newSegments);
loadCutSegments(newSegments, true);
} catch (err) {
handleError(errorText, err);
} finally {
setWorking(undefined);
setCutProgress(undefined);
}
}, [filePath, workingRef, setWorking, setCutProgress, loadCutSegments]);
const getSegApparentEnd = useCallback((seg) => getSegApparentEnd2(seg, duration), [duration]);
const getApparentCutSegments = useCallback((segments: StateSegment[]) => segments.map((cutSegment) => ({
...cutSegment,
start: getSegApparentStart(cutSegment),
end: getSegApparentEnd(cutSegment),
})), [getSegApparentEnd]);
// These are segments guaranteed to have a start and end time
const apparentCutSegments = useMemo(() => getApparentCutSegments(cutSegments), [cutSegments, getApparentCutSegments]);
const getApparentCutSegmentById = useCallback((id) => apparentCutSegments.find((s) => s.segId === id), [apparentCutSegments]);
const haveInvalidSegs = useMemo(() => apparentCutSegments.some((cutSegment) => cutSegment.start >= cutSegment.end), [apparentCutSegments]);
const currentSegIndexSafe = Math.min(currentSegIndex, cutSegments.length - 1);
const currentCutSeg = useMemo(() => {
const ret = cutSegments[currentSegIndexSafe];
if (ret == null) throw new Error('currentCutSeg was nullish, this shouldn\'t happen');
return ret;
}, [currentSegIndexSafe, cutSegments]);
const currentApparentCutSeg = useMemo(() => {
const ret = apparentCutSegments[currentSegIndexSafe];
if (ret == null) throw new Error('currentApparentCutSeg was nullish, this shouldn\'t happen');
return ret;
}, [apparentCutSegments, currentSegIndexSafe]);
const selectedSegmentsRaw = useMemo(() => apparentCutSegments.filter((segment) => isSegmentSelected(segment)), [apparentCutSegments, isSegmentSelected]);
const detectBlackScenes = useCallback(async () => {
const filterOptions = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.blackdetect(), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#blackdetect' });
if (filterOptions == null) return;
await detectSegments({ name: 'blackScenes', workingText: i18n.t('Detecting black scenes'), errorText: i18n.t('Failed to detect black scenes'), fn: async () => blackDetect({ filePath, filterOptions, onProgress: setCutProgress, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end }) });
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, setCutProgress]);
const detectSilentScenes = useCallback(async () => {
const filterOptions = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.silencedetect(), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#silencedetect' });
if (filterOptions == null) return;
await detectSegments({ name: 'silentScenes', workingText: i18n.t('Detecting silent scenes'), errorText: i18n.t('Failed to detect silent scenes'), fn: async () => silenceDetect({ filePath, filterOptions, onProgress: setCutProgress, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end }) });
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, setCutProgress]);
const detectSceneChanges = useCallback(async () => {
const filterOptions = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.sceneChange() });
if (filterOptions == null) return;
await detectSegments({ name: 'sceneChanges', workingText: i18n.t('Detecting scene changes'), errorText: i18n.t('Failed to detect scene changes'), fn: async () => ffmpegDetectSceneChanges({ filePath, minChange: filterOptions['minChange'], onProgress: setCutProgress, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end }) });
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, setCutProgress]);
const createSegmentsFromKeyframes = useCallback(async () => {
if (!videoStream) return;
const keyframes = (await readFrames({ filePath, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end, streamIndex: videoStream.index })).filter((frame) => frame.keyframe);
const newSegments = mapTimesToSegments(keyframes.map((keyframe) => keyframe.time));
loadCutSegments(newSegments, true);
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, filePath, loadCutSegments, videoStream]);
const removeSegments = useCallback((removeSegmentIds) => {
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)
}
const newSegments = existingSegments.filter((seg) => !removeSegmentIds.includes(seg.segId));
if (newSegments.length === 0) {
// when removing the last segments, we start over
clearSegCounter();
return createInitialCutSegments();
}
return newSegments;
});
}, [clearSegCounter, createInitialCutSegments, setCutSegments]);
const removeCutSegment = useCallback((index) => {
removeSegments([cutSegments[index]!.segId]);
}, [cutSegments, removeSegments]);
const inverseCutSegments = useMemo(() => {
const inverted = !haveInvalidSegs && isDurationValid(duration) ? invertSegments(sortSegments(apparentCutSegments), true, true, duration) : undefined;
return inverted || [];
}, [apparentCutSegments, duration, haveInvalidSegs]);
const invertAllSegments = useCallback(() => {
if (inverseCutSegments.length === 0) {
errorToast(i18n.t('Make sure you have no overlapping segments.'));
return;
}
// don't reset segColorIndex (which represent colors) when inverting
const newInverseCutSegments = inverseCutSegments.map((inverseSegment, index) => createSegment({ ...inverseSegment, segColorIndex: index }));
setCutSegments(newInverseCutSegments);
}, [inverseCutSegments, setCutSegments]);
const fillSegmentsGaps = useCallback(() => {
if (inverseCutSegments.length === 0) {
errorToast(i18n.t('Make sure you have no overlapping segments.'));
return;
}
const newInverseCutSegments = inverseCutSegments.map((inverseSegment) => createIndexedSegment({ segment: inverseSegment, incrementCount: true }));
setCutSegments((existing) => ([...existing, ...newInverseCutSegments]));
}, [createIndexedSegment, inverseCutSegments, setCutSegments]);
const combineOverlappingSegments = useCallback(() => {
setCutSegments((existingSegments) => combineOverlappingSegments2(existingSegments, getSegApparentEnd));
}, [getSegApparentEnd, setCutSegments]);
const combineSelectedSegments = useCallback(() => {
setCutSegments((existingSegments) => combineSelectedSegments2(existingSegments, getSegApparentEnd2, isSegmentSelected));
}, [isSegmentSelected, setCutSegments]);
const updateSegAtIndex = useCallback((index, newProps) => {
if (index < 0) return;
const cutSegmentsNew = [...cutSegments];
cutSegmentsNew.splice(index, 1, { ...cutSegments[index], ...newProps });
setCutSegments(cutSegmentsNew);
}, [setCutSegments, cutSegments]);
const setCutTime = useCallback((type, time) => {
if (!isDurationValid(duration)) return;
const currentSeg = currentCutSeg;
if (type === 'start' && time >= getSegApparentEnd(currentSeg)) {
throw new Error('Start time must precede end time');
}
if (type === 'end' && time <= getSegApparentStart(currentSeg)) {
throw new Error('Start time must precede end time');
}
updateSegAtIndex(currentSegIndexSafe, { [type]: Math.min(Math.max(time, 0), duration) });
}, [currentSegIndexSafe, getSegApparentEnd, currentCutSeg, duration, updateSegAtIndex]);
const modifySelectedSegmentTimes = useCallback(async (transformSegment, concurrency = 5) => {
if (duration == null) throw new Error();
const clampValue = (val) => Math.min(Math.max(val, 0), duration);
let newSegments = await pMap(apparentCutSegments, async (segment) => {
if (!isSegmentSelected(segment)) return segment; // pass thru non-selected segments
const newSegment = await transformSegment(segment);
newSegment.start = clampValue(newSegment.start);
newSegment.end = clampValue(newSegment.end);
return newSegment;
}, { concurrency });
newSegments = newSegments.filter((segment) => segment.end > segment.start);
if (newSegments.length === 0) setCutSegments(createInitialCutSegments());
else setCutSegments(newSegments);
}, [apparentCutSegments, createInitialCutSegments, duration, isSegmentSelected, setCutSegments]);
const shiftAllSegmentTimes = useCallback(async () => {
const shift = await askForShiftSegments();
if (shift == null) return;
const { shiftAmount, shiftKeys } = shift;
await modifySelectedSegmentTimes((segment) => {
const newSegment = { ...segment };
shiftKeys.forEach((key) => {
newSegment[key] += shiftAmount;
});
return newSegment;
});
}, [modifySelectedSegmentTimes]);
const alignSegmentTimesToKeyframes = useCallback(async () => {
if (!videoStream || workingRef.current) return;
try {
const response = await askForAlignSegments();
if (response == null) return;
setWorking({ text: i18n.t('Aligning segments to keyframes') });
const { mode, startOrEnd } = response;
await modifySelectedSegmentTimes(async (segment) => {
const newSegment = { ...segment };
async function align(key) {
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;
});
} catch (err) {
handleError(err);
} finally {
setWorking(undefined);
}
}, [filePath, videoStream, modifySelectedSegmentTimes, setWorking, workingRef]);
const updateSegOrder = useCallback((index, newOrder) => {
if (newOrder > cutSegments.length - 1 || newOrder < 0) return;
const newSegments = [...cutSegments];
const removedSeg = newSegments.splice(index, 1)[0];
if (removedSeg == null) throw new Error();
newSegments.splice(newOrder, 0, removedSeg);
setCutSegments(newSegments);
setCurrentSegIndex(newOrder);
}, [cutSegments, setCurrentSegIndex, setCutSegments]);
const updateSegOrders = useCallback((newOrders) => {
const newSegments = sortBy(cutSegments, (seg) => newOrders.indexOf(seg.segId));
const newCurrentSegIndex = newOrders.indexOf(currentCutSeg.segId);
setCutSegments(newSegments);
if (newCurrentSegIndex >= 0 && newCurrentSegIndex < newSegments.length) setCurrentSegIndex(newCurrentSegIndex);
}, [cutSegments, setCutSegments, currentCutSeg, setCurrentSegIndex]);
const reorderSegsByStartTime = useCallback(() => {
setCutSegments(sortBy(cutSegments, getSegApparentStart));
}, [cutSegments, setCutSegments]);
const addSegment = useCallback(() => {
try {
// Cannot add if prev seg is not finished
if (currentCutSeg.start === undefined && currentCutSeg.end === undefined) return;
const suggestedStart = getRelevantTime();
/* if (keyframeCut) {
const keyframeAlignedStart = getSafeCutTime(suggestedStart, true);
if (keyframeAlignedStart != null) suggestedStart = keyframeAlignedStart;
} */
if (duration == null || suggestedStart >= duration) return;
const cutSegmentsNew = [
...cutSegments,
createIndexedSegment({ segment: { start: suggestedStart }, incrementCount: true }),
];
setCutSegments(cutSegmentsNew);
setCurrentSegIndex(cutSegmentsNew.length - 1);
} catch (err) {
console.error(err);
}
}, [currentCutSeg.start, currentCutSeg.end, getRelevantTime, duration, cutSegments, createIndexedSegment, setCutSegments, setCurrentSegIndex]);
const duplicateSegment = useCallback((segment) => {
try {
// Cannot duplicate if seg is not finished
if (segment.start === undefined && segment.end === undefined) return;
const cutSegmentsNew = [
...cutSegments,
createIndexedSegment({ segment: { start: segment.start, end: segment.end, name: segment.name }, incrementCount: true }),
];
setCutSegments(cutSegmentsNew);
setCurrentSegIndex(cutSegmentsNew.length - 1);
} catch (err) {
console.error(err);
}
}, [createIndexedSegment, cutSegments, setCutSegments]);
const duplicateCurrentSegment = useCallback(() => {
duplicateSegment(currentCutSeg);
}, [currentCutSeg, duplicateSegment]);
const setCutStart = useCallback(() => {
if (!checkFileOpened()) return;
const relevantTime = getRelevantTime();
// https://github.com/mifi/lossless-cut/issues/168
// If current time is after the end of the current segment in the timeline,
// add a new segment that starts at playerTime
if (currentCutSeg.end != null && relevantTime >= currentCutSeg.end) {
addSegment();
} else {
try {
const startTime = relevantTime;
/* if (keyframeCut) {
const keyframeAlignedCutTo = getSafeCutTime(startTime, true);
if (keyframeAlignedCutTo != null) startTime = keyframeAlignedCutTo;
} */
setCutTime('start', startTime);
} catch (err) {
handleError(err);
}
}
}, [checkFileOpened, getRelevantTime, currentCutSeg.end, addSegment, setCutTime]);
const setCutEnd = useCallback(() => {
if (!checkFileOpened()) return;
try {
const endTime = getRelevantTime();
/* if (keyframeCut) {
const keyframeAlignedCutTo = getSafeCutTime(endTime, false);
if (keyframeAlignedCutTo != null) endTime = keyframeAlignedCutTo;
} */
setCutTime('end', endTime);
} catch (err) {
handleError(err);
}
}, [checkFileOpened, getRelevantTime, setCutTime]);
const onLabelSegment = useCallback(async (index) => {
const { name } = cutSegments[index]!;
const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength });
if (value != null) updateSegAtIndex(index, { name: value });
}, [cutSegments, updateSegAtIndex, maxLabelLength]);
const splitCurrentSegment = useCallback(() => {
const relevantTime = getRelevantTime();
const segmentsAtCursorIndexes = findSegmentsAtCursor(apparentCutSegments, relevantTime);
const firstSegmentAtCursorIndex = segmentsAtCursorIndexes[0];
if (firstSegmentAtCursorIndex == null) {
errorToast(i18n.t('No segment to split. Please move cursor over the segment you want to split'));
return;
}
const segment = cutSegments[firstSegmentAtCursorIndex];
if (segment == null) throw new Error();
const getNewName = (oldName, suffix) => 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 });
const newSegments = [...cutSegments];
newSegments.splice(firstSegmentAtCursorIndex, 1, firstPart, secondPart);
setCutSegments(newSegments);
}, [apparentCutSegments, createIndexedSegment, cutSegments, getRelevantTime, setCutSegments]);
const createNumSegments = useCallback(async () => {
if (!checkFileOpened() || !isDurationValid(duration)) return;
const segments = await createNumSegmentsDialog(duration);
if (segments) loadCutSegments(segments);
}, [checkFileOpened, duration, loadCutSegments]);
const createFixedDurationSegments = useCallback(async () => {
if (!checkFileOpened() || !isDurationValid(duration)) return;
const segments = await createFixedDurationSegmentsDialog(duration);
if (segments) loadCutSegments(segments);
}, [checkFileOpened, duration, loadCutSegments]);
const createRandomSegments = useCallback(async () => {
if (!checkFileOpened() || !isDurationValid(duration)) return;
const segments = await createRandomSegmentsDialog(duration);
if (segments) loadCutSegments(segments);
}, [checkFileOpened, duration, loadCutSegments]);
const enableSegments = useCallback((segmentsToEnable) => {
if (segmentsToEnable.length === 0 || segmentsToEnable.length === cutSegments.length) return; // no point
setDeselectedSegmentIds((existing) => {
const ret = { ...existing };
segmentsToEnable.forEach(({ segId }) => { ret[segId] = false; });
return ret;
});
}, [cutSegments.length]);
const onSelectSegmentsByLabel = useCallback(async () => {
const { name } = currentCutSeg;
const value = await selectSegmentsByLabelDialog(name);
if (value == null) return;
const segmentsToEnable = cutSegments.filter((seg) => (seg.name || '') === value);
enableSegments(segmentsToEnable);
}, [currentCutSeg, cutSegments, enableSegments]);
const onSelectSegmentsByTag = useCallback(async () => {
const value = await selectSegmentsByTagDialog();
if (value == null) return;
const { tagName, tagValue } = value;
const segmentsToEnable = cutSegments.filter((seg) => getSegmentTags(seg)[tagName] === tagValue);
enableSegments(segmentsToEnable);
}, [cutSegments, enableSegments]);
const onLabelSelectedSegments = useCallback(async () => {
if (selectedSegmentsRaw.length === 0) return;
const { name } = selectedSegmentsRaw[0]!;
const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength });
if (value == null) return;
setCutSegments((existingSegments) => existingSegments.map((existingSegment) => {
if (selectedSegmentsRaw.some((seg) => seg.segId === existingSegment.segId)) return { ...existingSegment, name: value };
return existingSegment;
}));
}, [maxLabelLength, selectedSegmentsRaw, setCutSegments]);
// Guaranteed to have at least one segment (if user has selected none to export (selectedSegments empty), it makes no sense so select all instead.)
const selectedSegments = useMemo(() => (selectedSegmentsRaw.length > 0 ? selectedSegmentsRaw : apparentCutSegments), [apparentCutSegments, selectedSegmentsRaw]);
// For invertCutSegments we do not support filtering (selecting) segments
const selectedSegmentsOrInverse = useMemo(() => (invertCutSegments ? inverseCutSegments : selectedSegments), [inverseCutSegments, invertCutSegments, selectedSegments]);
const nonFilteredSegmentsOrInverse = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments), [invertCutSegments, inverseCutSegments, apparentCutSegments]);
const segmentsToExport = useMemo(() => {
// segmentsToChaptersOnly is a special mode where all segments will be simply written out as chapters to one file: https://github.com/mifi/lossless-cut/issues/993#issuecomment-1037927595
// Chapters export mode: Emulate a single segment with no cuts (full timeline)
if (segmentsToChaptersOnly) return [{ start: 0, end: getSegApparentEnd({}) }];
return selectedSegmentsOrInverse;
}, [selectedSegmentsOrInverse, getSegApparentEnd, segmentsToChaptersOnly]);
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 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({}), []);
const selectOnlyCurrentSegment = useCallback(() => selectOnlySegment(currentCutSeg), [currentCutSeg, selectOnlySegment]);
const toggleCurrentSegmentSelected = useCallback(() => toggleSegmentSelected(currentCutSeg), [currentCutSeg, toggleSegmentSelected]);
return {
cutSegments,
cutSegmentsHistory,
createSegmentsFromKeyframes,
shuffleSegments,
detectBlackScenes,
detectSilentScenes,
detectSceneChanges,
removeCutSegment,
invertAllSegments,
fillSegmentsGaps,
combineOverlappingSegments,
combineSelectedSegments,
shiftAllSegmentTimes,
alignSegmentTimesToKeyframes,
updateSegOrder,
updateSegOrders,
reorderSegsByStartTime,
addSegment,
duplicateCurrentSegment,
duplicateSegment,
setCutStart,
setCutEnd,
onLabelSegment,
splitCurrentSegment,
createNumSegments,
createFixedDurationSegments,
createRandomSegments,
apparentCutSegments,
getApparentCutSegmentById,
haveInvalidSegs,
currentSegIndexSafe,
currentCutSeg,
currentApparentCutSeg,
inverseCutSegments,
clearSegments,
loadCutSegments,
isSegmentSelected,
selectedSegments,
selectedSegmentsOrInverse,
nonFilteredSegmentsOrInverse,
segmentsToExport,
setCurrentSegIndex,
setDeselectedSegmentIds,
onLabelSelectedSegments,
deselectAllSegments,
selectAllSegments,
selectOnlyCurrentSegment,
toggleCurrentSegmentSelected,
invertSelectedSegments,
removeSelectedSegments,
onSelectSegmentsByLabel,
onSelectSegmentsByTag,
toggleSegmentSelected,
selectOnlySegment,
setCutTime,
updateSegAtIndex,
};
};