1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-21 18:02:35 +01:00

always render all thumbs

but abort processes when params change

#2229
This commit is contained in:
Mikael Finstad 2024-11-01 17:03:05 +08:00
parent 0e5e7dffea
commit 94a43986cb
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
3 changed files with 45 additions and 55 deletions

View File

@ -531,7 +531,7 @@ function App() {
const bigWaveformEnabled = waveformEnabled && waveformMode === 'big-waveform';
const showThumbnails = thumbnailsEnabled && hasVideo;
const { cancelRenderThumbnails, thumbnailsSorted, setThumbnails } = useThumbnails({ filePath, zoomedDuration, zoomWindowStartTime, showThumbnails });
const { thumbnailsSorted, setThumbnails } = useThumbnails({ filePath, zoomedDuration, zoomWindowStartTime, showThumbnails });
const shouldShowKeyframes = keyframesEnabled && hasVideo && calcShouldShowKeyframes(zoomedDuration);
const shouldShowWaveform = calcShouldShowWaveform(zoomedDuration);
@ -581,9 +581,7 @@ function App() {
setHideMediaSourcePlayer(false);
setExportConfirmVisible(false);
setOutputPlaybackRateState(1);
cancelRenderThumbnails();
}, [videoRef, setCommandedTime, setPlaybackRate, setPlaying, playingRef, playbackModeRef, setCompatPlayerEventId, setDuration, cutSegmentsHistory, clearSegments, setFileFormat, setDetectedFileFormat, setCopyStreamIdsByFile, setThumbnails, setDeselectedSegmentIds, setSubtitlesByStreamId, setOutputPlaybackRateState, cancelRenderThumbnails]);
}, [videoRef, setCommandedTime, setPlaybackRate, setPlaying, playingRef, playbackModeRef, setCompatPlayerEventId, setDuration, cutSegmentsHistory, clearSegments, setFileFormat, setDetectedFileFormat, setCopyStreamIdsByFile, setThumbnails, setDeselectedSegmentIds, setSubtitlesByStreamId, setOutputPlaybackRateState]);
const showUnsupportedFileMessage = useCallback(() => {

View File

@ -506,7 +506,7 @@ export async function extractStreams({ filePath, customOutDir, streams, enableOv
];
}
async function renderThumbnail(filePath: string, timestamp: number) {
async function renderThumbnail(filePath: string, timestamp: number, signal: AbortSignal) {
const args = [
'-ss', String(timestamp),
'-i', filePath,
@ -517,7 +517,7 @@ async function renderThumbnail(filePath: string, timestamp: number) {
'-',
];
const { stdout } = await runFfmpeg(args);
const { stdout } = await runFfmpeg(args, { signal });
const blob = new Blob([fixRemoteBuffer(stdout)], { type: 'image/jpeg' });
return URL.createObjectURL(blob);
@ -561,24 +561,19 @@ export async function extractSubtitleTrackVtt(filePath: string, streamId: number
return URL.createObjectURL(blob);
}
export async function renderThumbnails({ filePath, from, duration, onThumbnail }: {
filePath: string, from: number, duration: number, onThumbnail: (a: { time: number, url: string }) => void,
export async function renderThumbnails({ filePath, from, duration, onThumbnail, signal }: {
filePath: string,
from: number,
duration: number,
onThumbnail: (a: { time: number, url: string }) => void,
signal: AbortSignal,
}) {
// Time first render to determine how many to render
const startTime = Date.now() / 1000;
let url = await renderThumbnail(filePath, from);
const endTime = Date.now() / 1000;
onThumbnail({ time: from, url });
// Aim for max 3 sec to render all
const numThumbs = Math.floor(Math.min(Math.max(3 / (endTime - startTime), 3), 10));
// console.log(numThumbs);
const thumbTimes = Array.from({ length: numThumbs - 1 }).fill(undefined).map((_unused, i) => (from + ((duration * (i + 1)) / (numThumbs))));
const numThumbs = 10;
const thumbTimes = Array.from({ length: numThumbs }).fill(undefined).map((_unused, i) => (from + ((duration * i) / numThumbs)));
// console.log(thumbTimes);
await pMap(thumbTimes, async (time) => {
url = await renderThumbnail(filePath, time);
const url = await renderThumbnail(filePath, time, signal);
onThumbnail({ time, url });
}, { concurrency: 2 });
}

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import useDebounceOld from 'react-use/lib/useDebounce'; // Want to phase out this
import { useEffect, useMemo, useState } from 'react';
import { useDebounce } from 'use-debounce';
import invariant from 'tiny-invariant';
import sortBy from 'lodash/sortBy';
@ -15,51 +15,48 @@ export default ({ filePath, zoomedDuration, zoomWindowStartTime, showThumbnails
showThumbnails: boolean,
}) => {
const [thumbnails, setThumbnails] = useState<Thumbnail[]>([]);
const thumnailsRef = useRef<Thumbnail[]>([]);
const thumnailsRenderingPromiseRef = useRef<Promise<void>>();
function addThumbnail(thumbnail) {
// console.log('Rendered thumbnail', thumbnail.url);
setThumbnails((v) => [...v, thumbnail]);
}
const [debounced] = useDebounce({ zoomedDuration, filePath, zoomWindowStartTime, showThumbnails }, 300, {
equalityFn: (a, b) => JSON.stringify(a) === JSON.stringify(b),
});
const [, cancelRenderThumbnails] = useDebounceOld(() => {
async function renderThumbnails() {
if (!showThumbnails || thumnailsRenderingPromiseRef.current) return;
useEffect(() => {
const abortController = new AbortController();
const thumbnails2: Thumbnail[] = [];
(async () => {
if (!isDurationValid(debounced.zoomedDuration) || !debounced.showThumbnails) return;
try {
setThumbnails([]);
invariant(filePath != null);
invariant(zoomedDuration != null);
const promise = ffmpegRenderThumbnails({ filePath, from: zoomWindowStartTime, duration: zoomedDuration, onThumbnail: addThumbnail });
thumnailsRenderingPromiseRef.current = promise;
await promise;
invariant(debounced.filePath != null);
invariant(debounced.zoomedDuration != null);
const addThumbnail = (t: Thumbnail) => {
if (abortController.signal.aborted) return; // because the bridge is async
thumbnails2.push(t);
setThumbnails((v) => [...v, t]);
};
await ffmpegRenderThumbnails({ signal: abortController.signal, filePath: debounced.filePath, from: debounced.zoomWindowStartTime, duration: debounced.zoomedDuration, onThumbnail: addThumbnail });
} catch (err) {
console.error('Failed to render thumbnail', err);
} finally {
thumnailsRenderingPromiseRef.current = undefined;
if ((err as Error).name !== 'AbortError') {
console.error('Failed to render thumbnails', err);
}
}
}
})();
if (isDurationValid(zoomedDuration)) renderThumbnails();
}, 500, [zoomedDuration, filePath, zoomWindowStartTime, showThumbnails]);
// Cleanup removed thumbnails
useEffect(() => {
thumnailsRef.current.forEach((thumbnail) => {
if (!thumbnails.some((nextThumbnail) => nextThumbnail.url === thumbnail.url)) {
console.log('Cleanup thumbnail', thumbnail.time);
URL.revokeObjectURL(thumbnail.url);
}
});
thumnailsRef.current = thumbnails;
}, [thumbnails]);
return () => {
abortController.abort();
console.log('Cleanup thumbnails', thumbnails2.map((t) => t.time));
thumbnails2.forEach((thumbnail) => URL.revokeObjectURL(thumbnail.url));
setThumbnails([]);
};
}, [debounced]);
const thumbnailsSorted = useMemo(() => sortBy(thumbnails, (thumbnail) => thumbnail.time), [thumbnails]);
return {
thumbnailsSorted,
setThumbnails,
cancelRenderThumbnails,
};
};