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 bigWaveformEnabled = waveformEnabled && waveformMode === 'big-waveform';
|
||||||
const showThumbnails = thumbnailsEnabled && hasVideo;
|
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 shouldShowKeyframes = keyframesEnabled && hasVideo && calcShouldShowKeyframes(zoomedDuration);
|
||||||
const shouldShowWaveform = calcShouldShowWaveform(zoomedDuration);
|
const shouldShowWaveform = calcShouldShowWaveform(zoomedDuration);
|
||||||
@ -581,9 +581,7 @@ function App() {
|
|||||||
setHideMediaSourcePlayer(false);
|
setHideMediaSourcePlayer(false);
|
||||||
setExportConfirmVisible(false);
|
setExportConfirmVisible(false);
|
||||||
setOutputPlaybackRateState(1);
|
setOutputPlaybackRateState(1);
|
||||||
|
}, [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, cancelRenderThumbnails]);
|
|
||||||
|
|
||||||
|
|
||||||
const showUnsupportedFileMessage = useCallback(() => {
|
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 = [
|
const args = [
|
||||||
'-ss', String(timestamp),
|
'-ss', String(timestamp),
|
||||||
'-i', filePath,
|
'-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' });
|
const blob = new Blob([fixRemoteBuffer(stdout)], { type: 'image/jpeg' });
|
||||||
return URL.createObjectURL(blob);
|
return URL.createObjectURL(blob);
|
||||||
@ -561,24 +561,19 @@ export async function extractSubtitleTrackVtt(filePath: string, streamId: number
|
|||||||
return URL.createObjectURL(blob);
|
return URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderThumbnails({ filePath, from, duration, onThumbnail }: {
|
export async function renderThumbnails({ filePath, from, duration, onThumbnail, signal }: {
|
||||||
filePath: string, from: number, duration: number, onThumbnail: (a: { time: number, url: string }) => void,
|
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 numThumbs = 10;
|
||||||
const startTime = Date.now() / 1000;
|
const thumbTimes = Array.from({ length: numThumbs }).fill(undefined).map((_unused, i) => (from + ((duration * i) / numThumbs)));
|
||||||
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))));
|
|
||||||
// console.log(thumbTimes);
|
// console.log(thumbTimes);
|
||||||
|
|
||||||
await pMap(thumbTimes, async (time) => {
|
await pMap(thumbTimes, async (time) => {
|
||||||
url = await renderThumbnail(filePath, time);
|
const url = await renderThumbnail(filePath, time, signal);
|
||||||
onThumbnail({ time, url });
|
onThumbnail({ time, url });
|
||||||
}, { concurrency: 2 });
|
}, { concurrency: 2 });
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import useDebounceOld from 'react-use/lib/useDebounce'; // Want to phase out this
|
import { useDebounce } from 'use-debounce';
|
||||||
import invariant from 'tiny-invariant';
|
import invariant from 'tiny-invariant';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
|
|
||||||
@ -15,51 +15,48 @@ export default ({ filePath, zoomedDuration, zoomWindowStartTime, showThumbnails
|
|||||||
showThumbnails: boolean,
|
showThumbnails: boolean,
|
||||||
}) => {
|
}) => {
|
||||||
const [thumbnails, setThumbnails] = useState<Thumbnail[]>([]);
|
const [thumbnails, setThumbnails] = useState<Thumbnail[]>([]);
|
||||||
const thumnailsRef = useRef<Thumbnail[]>([]);
|
|
||||||
const thumnailsRenderingPromiseRef = useRef<Promise<void>>();
|
|
||||||
|
|
||||||
function addThumbnail(thumbnail) {
|
const [debounced] = useDebounce({ zoomedDuration, filePath, zoomWindowStartTime, showThumbnails }, 300, {
|
||||||
// console.log('Rendered thumbnail', thumbnail.url);
|
equalityFn: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
||||||
setThumbnails((v) => [...v, thumbnail]);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const [, cancelRenderThumbnails] = useDebounceOld(() => {
|
useEffect(() => {
|
||||||
async function renderThumbnails() {
|
const abortController = new AbortController();
|
||||||
if (!showThumbnails || thumnailsRenderingPromiseRef.current) return;
|
const thumbnails2: Thumbnail[] = [];
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
if (!isDurationValid(debounced.zoomedDuration) || !debounced.showThumbnails) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setThumbnails([]);
|
invariant(debounced.filePath != null);
|
||||||
invariant(filePath != null);
|
invariant(debounced.zoomedDuration != null);
|
||||||
invariant(zoomedDuration != null);
|
|
||||||
const promise = ffmpegRenderThumbnails({ filePath, from: zoomWindowStartTime, duration: zoomedDuration, onThumbnail: addThumbnail });
|
const addThumbnail = (t: Thumbnail) => {
|
||||||
thumnailsRenderingPromiseRef.current = promise;
|
if (abortController.signal.aborted) return; // because the bridge is async
|
||||||
await promise;
|
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) {
|
} catch (err) {
|
||||||
console.error('Failed to render thumbnail', err);
|
if ((err as Error).name !== 'AbortError') {
|
||||||
} finally {
|
console.error('Failed to render thumbnails', err);
|
||||||
thumnailsRenderingPromiseRef.current = undefined;
|
}
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
|
|
||||||
if (isDurationValid(zoomedDuration)) renderThumbnails();
|
return () => {
|
||||||
}, 500, [zoomedDuration, filePath, zoomWindowStartTime, showThumbnails]);
|
abortController.abort();
|
||||||
|
console.log('Cleanup thumbnails', thumbnails2.map((t) => t.time));
|
||||||
// Cleanup removed thumbnails
|
thumbnails2.forEach((thumbnail) => URL.revokeObjectURL(thumbnail.url));
|
||||||
useEffect(() => {
|
setThumbnails([]);
|
||||||
thumnailsRef.current.forEach((thumbnail) => {
|
};
|
||||||
if (!thumbnails.some((nextThumbnail) => nextThumbnail.url === thumbnail.url)) {
|
}, [debounced]);
|
||||||
console.log('Cleanup thumbnail', thumbnail.time);
|
|
||||||
URL.revokeObjectURL(thumbnail.url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
thumnailsRef.current = thumbnails;
|
|
||||||
}, [thumbnails]);
|
|
||||||
|
|
||||||
const thumbnailsSorted = useMemo(() => sortBy(thumbnails, (thumbnail) => thumbnail.time), [thumbnails]);
|
const thumbnailsSorted = useMemo(() => sortBy(thumbnails, (thumbnail) => thumbnail.time), [thumbnails]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
thumbnailsSorted,
|
thumbnailsSorted,
|
||||||
setThumbnails,
|
setThumbnails,
|
||||||
cancelRenderThumbnails,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user