mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-21 18:02:35 +01:00
parent
0e5e7dffea
commit
94a43986cb
@ -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(() => {
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user