fix stream tags
type
fix waveform crash and bug with initial render
pull/1982/head
Mikael Finstad 2024-04-21 23:44:10 +02:00
rodzic 852a7989ba
commit 666a1c5bd4
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
7 zmienionych plików z 176 dodań i 97 usunięć

Wyświetl plik

@ -251,7 +251,8 @@ export interface FFprobeStreamDisposition {
/** /**
* The "tags" field on an FFprobe response stream object * The "tags" field on an FFprobe response stream object
*/ */
export interface FFprobeStreamTags { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type FFprobeStreamTags = {
/** /**
* The track's language (usually represented using a 3 letter language code, e.g.: "eng") * The track's language (usually represented using a 3 letter language code, e.g.: "eng")
*/ */
@ -283,6 +284,9 @@ export interface FFprobeStreamTags {
comment?: string comment?: string
rotate?: string, rotate?: string,
// https://github.com/mifi/lossless-cut/issues/1530
title?: string,
} }
/** /**

Wyświetl plik

@ -86,7 +86,7 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
import BigWaveform from './components/BigWaveform'; import BigWaveform from './components/BigWaveform';
import isDev from './isDev'; import isDev from './isDev';
import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types'; import { ChromiumHTMLVideoElement, CustomTagsByFile, EdlFileType, FfmpegCommandLog, FilesMeta, FormatTimecode, ParamsByStreamId, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types'; import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
@ -125,9 +125,9 @@ function App() {
const [cutProgress, setCutProgress] = useState<number>(); const [cutProgress, setCutProgress] = useState<number>();
const [startTimeOffset, setStartTimeOffset] = useState(0); const [startTimeOffset, setStartTimeOffset] = useState(0);
const [filePath, setFilePath] = useState<string>(); const [filePath, setFilePath] = useState<string>();
const [externalFilesMeta, setExternalFilesMeta] = useState<Record<string, { streams: FFprobeStream[], formatData: FFprobeFormat, chapters: FFprobeChapter[] }>>({}); const [externalFilesMeta, setExternalFilesMeta] = useState<FilesMeta>({});
const [customTagsByFile, setCustomTagsByFile] = useState({}); const [customTagsByFile, setCustomTagsByFile] = useState<CustomTagsByFile>({});
const [paramsByStreamId, setParamsByStreamId] = useState(new Map()); const [paramsByStreamId, setParamsByStreamId] = useState<ParamsByStreamId>(new Map());
const [detectedFps, setDetectedFps] = useState<number>(); const [detectedFps, setDetectedFps] = useState<number>();
const [mainFileMeta, setMainFileMeta] = useState<{ streams: FFprobeStream[], formatData: FFprobeFormat, chapters: FFprobeChapter[] }>(); const [mainFileMeta, setMainFileMeta] = useState<{ streams: FFprobeStream[], formatData: FFprobeFormat, chapters: FFprobeChapter[] }>();
const [copyStreamIdsByFile, setCopyStreamIdsByFile] = useState<Record<string, Record<string, boolean>>>({}); const [copyStreamIdsByFile, setCopyStreamIdsByFile] = useState<Record<string, Record<string, boolean>>>({});
@ -253,7 +253,7 @@ function App() {
setFfmpegCommandLog((old) => [...old, { command, time: new Date() }]); setFfmpegCommandLog((old) => [...old, { command, time: new Date() }]);
} }
const setCopyStreamIdsForPath = useCallback((path, cb) => { const setCopyStreamIdsForPath = useCallback<Parameters<typeof StreamsSelector>[0]['setCopyStreamIdsForPath']>((path, cb) => {
setCopyStreamIdsByFile((old) => { setCopyStreamIdsByFile((old) => {
const oldIds = old[path] || {}; const oldIds = old[path] || {};
return ({ ...old, [path]: cb(oldIds) }); return ({ ...old, [path]: cb(oldIds) });
@ -262,7 +262,7 @@ function App() {
const toggleSegmentsList = useCallback(() => setShowRightBar((v) => !v), []); const toggleSegmentsList = useCallback(() => setShowRightBar((v) => !v), []);
const toggleCopyStreamId = useCallback((path, index) => { const toggleCopyStreamId = useCallback((path: string, index: number) => {
setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] })); setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] }));
}, [setCopyStreamIdsForPath]); }, [setCopyStreamIdsForPath]);
@ -309,8 +309,8 @@ function App() {
const mainFileFormatData = useMemo(() => mainFileMeta?.formatData, [mainFileMeta?.formatData]); const mainFileFormatData = useMemo(() => mainFileMeta?.formatData, [mainFileMeta?.formatData]);
const mainFileChapters = useMemo(() => mainFileMeta?.chapters, [mainFileMeta?.chapters]); const mainFileChapters = useMemo(() => mainFileMeta?.chapters, [mainFileMeta?.chapters]);
const isCopyingStreamId = useCallback((path, streamId) => ( const isCopyingStreamId = useCallback((path: string | undefined, streamId: number) => (
!!(copyStreamIdsByFile[path] || {})[streamId] !!((path != null && copyStreamIdsByFile[path]) || {})[streamId]
), [copyStreamIdsByFile]); ), [copyStreamIdsByFile]);
const checkCopyingAnyTrackOfType = useCallback((filter: (s: FFprobeStream) => boolean) => mainStreams.some((stream) => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]); const checkCopyingAnyTrackOfType = useCallback((filter: (s: FFprobeStream) => boolean) => mainStreams.some((stream) => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]);
@ -694,6 +694,7 @@ function App() {
const toggleStripStream = useCallback((filter) => { const toggleStripStream = useCallback((filter) => {
const copyingAnyTrackOfType = checkCopyingAnyTrackOfType(filter); const copyingAnyTrackOfType = checkCopyingAnyTrackOfType(filter);
invariant(filePath != null);
setCopyStreamIdsForPath(filePath, (old) => { setCopyStreamIdsForPath(filePath, (old) => {
const newCopyStreamIds = { ...old }; const newCopyStreamIds = { ...old };
mainStreams.forEach((stream) => { mainStreams.forEach((stream) => {
@ -763,7 +764,7 @@ function App() {
const shouldShowWaveform = calcShouldShowWaveform(zoomedDuration); const shouldShowWaveform = calcShouldShowWaveform(zoomedDuration);
const { neighbouringKeyFrames, findNearestKeyFrameTime } = useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream: activeVideoStream, detectedFps, ffmpegExtractWindow }); const { neighbouringKeyFrames, findNearestKeyFrameTime } = useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream: activeVideoStream, detectedFps, ffmpegExtractWindow });
const { waveforms } = useWaveform({ darkMode, filePath, relevantTime, waveformEnabled, audioStream: activeAudioStream, ffmpegExtractWindow, durationSafe }); const { waveforms } = useWaveform({ darkMode, filePath, relevantTime, waveformEnabled, audioStream: activeAudioStream, ffmpegExtractWindow, duration });
const resetMergedOutFileName = useCallback(() => { const resetMergedOutFileName = useCallback(() => {
if (fileFormat == null || filePath == null) return; if (fileFormat == null || filePath == null) return;
@ -1833,19 +1834,23 @@ function App() {
return fileMeta; return fileMeta;
}, [allFilesMeta, setCopyStreamIdsForPath]); }, [allFilesMeta, setCopyStreamIdsForPath]);
const updateStreamParams = useCallback((fileId, streamId, setter) => setParamsByStreamId(produce((draft) => { const updateStreamParams = useCallback<Parameters<typeof StreamsSelector>[0]['updateStreamParams']>((fileId, streamId, setter) => setParamsByStreamId(produce((draft) => {
if (!draft.has(fileId)) draft.set(fileId, new Map()); if (!draft.has(fileId)) draft.set(fileId, new Map());
const fileMap = draft.get(fileId); const fileMap = draft.get(fileId);
if (!fileMap.has(streamId)) fileMap.set(streamId, new Map()); invariant(fileMap != null);
if (!fileMap.has(streamId)) fileMap.set(streamId, {});
setter(fileMap.get(streamId)); const params = fileMap.get(streamId);
invariant(params != null);
setter(params);
})), [setParamsByStreamId]); })), [setParamsByStreamId]);
const addFileAsCoverArt = useCallback(async (path: string) => { const addFileAsCoverArt = useCallback(async (path: string) => {
const fileMeta = await addStreamSourceFile(path); const fileMeta = await addStreamSourceFile(path);
if (!fileMeta) return false; if (!fileMeta) return false;
const firstIndex = fileMeta.streams[0]!.index; const firstIndex = fileMeta.streams[0]!.index;
updateStreamParams(path, firstIndex, (params) => params.set('disposition', 'attached_pic')); // eslint-disable-next-line no-param-reassign
updateStreamParams(path, firstIndex, (params) => { params.disposition = 'attached_pic'; });
return true; return true;
}, [addStreamSourceFile, updateStreamParams]); }, [addStreamSourceFile, updateStreamParams]);
@ -2280,7 +2285,7 @@ function App() {
electron.ipcRenderer.send('setAskBeforeClose', askBeforeClose && isFileOpened); electron.ipcRenderer.send('setAskBeforeClose', askBeforeClose && isFileOpened);
}, [askBeforeClose, isFileOpened]); }, [askBeforeClose, isFileOpened]);
const extractSingleStream = useCallback(async (index) => { const extractSingleStream = useCallback(async (index: number) => {
if (!filePath) return; if (!filePath) return;
if (workingRef.current) return; if (workingRef.current) return;
@ -2596,7 +2601,7 @@ function App() {
</div> </div>
<AnimatePresence> <AnimatePresence>
{showRightBar && isFileOpened && ( {showRightBar && isFileOpened && filePath != null && (
<SegmentList <SegmentList
width={rightBarWidth} width={rightBarWidth}
currentSegIndex={currentSegIndexSafe} currentSegIndex={currentSegIndexSafe}
@ -2726,9 +2731,8 @@ function App() {
<ExportConfirm areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} mergedOutFileName={mergedOutFileName} setMergedOutFileName={setMergedOutFileName} /> <ExportConfirm areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} mergedOutFileName={mergedOutFileName} setMergedOutFileName={setMergedOutFileName} />
<Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} maxWidth={1000}> <Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} maxWidth={1000}>
{mainStreams && ( {mainStreams && filePath != null && (
<StreamsSelector <StreamsSelector
// @ts-expect-error todo
mainFilePath={filePath} mainFilePath={filePath}
mainFileFormatData={mainFileFormatData} mainFileFormatData={mainFileFormatData}
mainFileChapters={mainFileChapters} mainFileChapters={mainFileChapters}
@ -2742,7 +2746,6 @@ function App() {
setCopyStreamIdsForPath={setCopyStreamIdsForPath} setCopyStreamIdsForPath={setCopyStreamIdsForPath}
onExtractAllStreamsPress={extractAllStreams} onExtractAllStreamsPress={extractAllStreams}
onExtractStreamPress={extractSingleStream} onExtractStreamPress={extractSingleStream}
areWeCutting={areWeCutting}
shortestFlag={shortestFlag} shortestFlag={shortestFlag}
setShortestFlag={setShortestFlag} setShortestFlag={setShortestFlag}
nonCopiedExtraStreams={nonCopiedExtraStreams} nonCopiedExtraStreams={nonCopiedExtraStreams}

Wyświetl plik

@ -1,4 +1,4 @@
import { memo, useState, useMemo, useCallback } from 'react'; import { memo, useState, useMemo, useCallback, Dispatch, SetStateAction, CSSProperties, ReactNode, ChangeEventHandler } from 'react';
import { FaImage, FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport } from 'react-icons/fa'; import { FaImage, FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport } from 'react-icons/fa';
import { GoFileBinary } from 'react-icons/go'; import { GoFileBinary } from 'react-icons/go';
@ -14,24 +14,34 @@ import { getStreamFps } from './ffmpeg';
import { deleteDispositionValue } from './util'; import { deleteDispositionValue } from './util';
import { getActiveDisposition, attachedPicDisposition } from './util/streams'; import { getActiveDisposition, attachedPicDisposition } from './util/streams';
import TagEditor from './components/TagEditor'; import TagEditor from './components/TagEditor';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
import { CustomTagsByFile, FilesMeta, FormatTimecode, ParamsByStreamId, StreamParams } from './types';
const dispositionOptions = ['default', 'dub', 'original', 'comment', 'lyrics', 'karaoke', 'forced', 'hearing_impaired', 'visual_impaired', 'clean_effects', 'attached_pic', 'captions', 'descriptions', 'dependent', 'metadata']; const dispositionOptions = ['default', 'dub', 'original', 'comment', 'lyrics', 'karaoke', 'forced', 'hearing_impaired', 'visual_impaired', 'clean_effects', 'attached_pic', 'captions', 'descriptions', 'dependent', 'metadata'];
const unchangedDispositionValue = 'llc_disposition_unchanged'; const unchangedDispositionValue = 'llc_disposition_unchanged';
type UpdateStreamParams = (fileId: string, streamId: number, setter: (a: StreamParams) => void) => void;
const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setCustomTagsByFile, editingTag, setEditingTag }) => { interface EditingStream {
streamId: number;
path: string;
}
const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setCustomTagsByFile, editingTag, setEditingTag }: {
editingFile: string, allFilesMeta: FilesMeta, customTagsByFile: CustomTagsByFile, setCustomTagsByFile: Dispatch<SetStateAction<CustomTagsByFile>>, editingTag: string | undefined, setEditingTag: (tag: string | undefined) => void
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { formatData } = allFilesMeta[editingFile]; const { formatData } = allFilesMeta[editingFile]!;
const existingTags = formatData.tags || {}; const existingTags = formatData.tags || {};
const customTags = customTagsByFile[editingFile] || {}; const customTags = customTagsByFile[editingFile] || {};
const onTagChange = useCallback((tag, value) => { const onTagsChange = useCallback((keyValues: Record<string, string>) => {
setCustomTagsByFile((old) => ({ ...old, [editingFile]: { ...old[editingFile], [tag]: value } })); setCustomTagsByFile((old) => ({ ...old, [editingFile]: { ...old[editingFile], ...keyValues } }));
}, [editingFile, setCustomTagsByFile]); }, [editingFile, setCustomTagsByFile]);
const onTagReset = useCallback((tag) => { const onTagReset = useCallback((tag: string) => {
setCustomTagsByFile((old) => { setCustomTagsByFile((old) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [tag]: deleted, ...rest } = old[editingFile] || {}; const { [tag]: deleted, ...rest } = old[editingFile] || {};
@ -39,13 +49,13 @@ const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setC
}); });
}, [editingFile, setCustomTagsByFile]); }, [editingFile, setCustomTagsByFile]);
return <TagEditor existingTags={existingTags} customTags={customTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagChange={onTagChange} onTagReset={onTagReset} addTagTitle={t('Add metadata')} addTagText={t('Enter metadata key')} />; return <TagEditor existingTags={existingTags} customTags={customTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagsChange={onTagsChange} onTagReset={onTagReset} addTagTitle={t('Add metadata')} addTagText={t('Enter metadata key')} />;
}); });
const getStreamDispositionsObj = (stream) => ((stream && stream.disposition) || {}); const getStreamDispositionsObj = (stream: FFprobeStream) => ((stream && stream.disposition) || {});
function getStreamEffectiveDisposition(paramsByStreamId, fileId, stream) { function getStreamEffectiveDisposition(paramsByStreamId: ParamsByStreamId, fileId: string, stream: FFprobeStream) {
const customDisposition = paramsByStreamId.get(fileId)?.get(stream.index)?.get('disposition'); const customDisposition = paramsByStreamId.get(fileId)?.get(stream.index)?.disposition;
const existingDispositionsObj = getStreamDispositionsObj(stream); const existingDispositionsObj = getStreamDispositionsObj(stream);
if (customDisposition) return customDisposition; if (customDisposition) return customDisposition;
@ -53,19 +63,23 @@ function getStreamEffectiveDisposition(paramsByStreamId, fileId, stream) {
} }
const StreamParametersEditor = ({ stream, streamParams, updateStreamParams }) => { function StreamParametersEditor({ stream, streamParams, updateStreamParams }: {
stream: FFprobeStream, streamParams: StreamParams, updateStreamParams: (setter: (a: StreamParams) => void) => void,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const ui = []; const ui: ReactNode[] = [];
// https://github.com/mifi/lossless-cut/issues/1680#issuecomment-1682915193 // https://github.com/mifi/lossless-cut/issues/1680#issuecomment-1682915193
if (stream.codec_name === 'h264') { if (stream.codec_name === 'h264') {
ui.push( ui.push(
<Checkbox key="bsfH264Mp4toannexb" checked={!!streamParams.get('bsfH264Mp4toannexb')} label={t('Enable "{{filterName}}" bitstream filter.', { filterName: 'h264_mp4toannexb' })} onChange={(e) => updateStreamParams((params) => params.set('bsfH264Mp4toannexb', e.target.checked))} />, // eslint-disable-next-line no-param-reassign
<Checkbox key="bsfH264Mp4toannexb" checked={!!streamParams.bsfH264Mp4toannexb} label={t('Enable "{{filterName}}" bitstream filter.', { filterName: 'h264_mp4toannexb' })} onChange={(e) => updateStreamParams((params) => { params.bsfH264Mp4toannexb = e.target.checked; })} />,
); );
} }
if (stream.codec_name === 'hevc') { if (stream.codec_name === 'hevc') {
ui.push( ui.push(
<Checkbox key="bsfHevcMp4toannexb" checked={!!streamParams.get('bsfHevcMp4toannexb')} label={t('Enable "{{filterName}}" bitstream filter.', { filterName: 'hevc_mp4toannexb' })} onChange={(e) => updateStreamParams((params) => params.set('bsfHevcMp4toannexb', e.target.checked))} />, // eslint-disable-next-line no-param-reassign
<Checkbox key="bsfHevcMp4toannexb" checked={!!streamParams.bsfHevcMp4toannexb} label={t('Enable "{{filterName}}" bitstream filter.', { filterName: 'hevc_mp4toannexb' })} onChange={(e) => updateStreamParams((params) => { params.bsfHevcMp4toannexb = e.target.checked; })} />,
); );
} }
@ -76,34 +90,39 @@ const StreamParametersEditor = ({ stream, streamParams, updateStreamParams }) =>
: t('No editable parameters for this stream.')} : t('No editable parameters for this stream.')}
</div> </div>
); );
}; }
const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, path: editingFile }, setEditingStream, allFilesMeta, paramsByStreamId, updateStreamParams }) => { const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, path: editingFile }, setEditingStream, allFilesMeta, paramsByStreamId, updateStreamParams }: {
editingStream: EditingStream, setEditingStream: Dispatch<SetStateAction<EditingStream | undefined>>, allFilesMeta: FilesMeta, paramsByStreamId: ParamsByStreamId, updateStreamParams: UpdateStreamParams,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [editingTag, setEditingTag] = useState(); const [editingTag, setEditingTag] = useState<string>();
const { streams } = allFilesMeta[editingFile]; const { streams } = allFilesMeta[editingFile]!;
const editingStream = useMemo(() => streams.find((s) => s.index === editingStreamId), [streams, editingStreamId]); const editingStream = useMemo(() => streams.find((s) => s.index === editingStreamId), [streams, editingStreamId]);
const existingTags = useMemo(() => (editingStream && editingStream.tags) || {}, [editingStream]); const existingTags = useMemo(() => (editingStream && editingStream.tags) || {}, [editingStream]);
const streamParams = useMemo(() => paramsByStreamId.get(editingFile)?.get(editingStreamId) ?? new Map(), [editingFile, editingStreamId, paramsByStreamId]); const streamParams = useMemo(() => paramsByStreamId.get(editingFile)?.get(editingStreamId) ?? {}, [editingFile, editingStreamId, paramsByStreamId]);
const customTags = useMemo(() => streamParams.get('customTags') ?? {}, [streamParams]); const customTags = useMemo(() => streamParams.customTags, [streamParams]);
const onTagChange = useCallback((tag, value) => { const onTagsChange = useCallback((keyValues: Record<string, string>) => {
updateStreamParams(editingFile, editingStreamId, (params) => { updateStreamParams(editingFile, editingStreamId, (params) => {
if (!params.has('customTags')) params.set('customTags', {}); // eslint-disable-next-line no-param-reassign
const tags = params.get('customTags'); if (params.customTags == null) params.customTags = {};
const tags = params.customTags;
Object.entries(keyValues).forEach(([tag, value]) => {
tags[tag] = value; tags[tag] = value;
}); });
});
}, [editingFile, editingStreamId, updateStreamParams]); }, [editingFile, editingStreamId, updateStreamParams]);
const onTagReset = useCallback((tag) => { const onTagReset = useCallback((tag: string) => {
updateStreamParams(editingFile, editingStreamId, (params) => { updateStreamParams(editingFile, editingStreamId, (params) => {
if (!params.has('customTags')) return; if (params.customTags == null) return;
// todo // todo
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-dynamic-delete // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-dynamic-delete
delete params.get('customTags')[tag]; delete params.customTags[tag];
}); });
}, [editingFile, editingStreamId, updateStreamParams]); }, [editingFile, editingStreamId, updateStreamParams]);
@ -114,32 +133,34 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
hasCancel={false} hasCancel={false}
isConfirmDisabled={editingTag != null} isConfirmDisabled={editingTag != null}
confirmLabel={t('Done')} confirmLabel={t('Done')}
onCloseComplete={() => setEditingStream()} onCloseComplete={() => setEditingStream(undefined)}
> >
<div style={{ color: 'black' }}> <div style={{ color: 'black' }}>
<Heading>Parameters</Heading> <Heading>Parameters</Heading>
<StreamParametersEditor stream={editingStream} streamParams={streamParams} updateStreamParams={(setter) => updateStreamParams(editingFile, editingStreamId, setter)} /> {editingStream != null && <StreamParametersEditor stream={editingStream} streamParams={streamParams} updateStreamParams={(setter) => updateStreamParams(editingFile, editingStreamId, setter)} />}
<Heading>Tags</Heading> <Heading>Tags</Heading>
<TagEditor existingTags={existingTags} customTags={customTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagChange={onTagChange} onTagReset={onTagReset} addTagTitle={t('Add metadata')} addTagText={t('Enter metadata key')} /> <TagEditor existingTags={existingTags} customTags={customTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagsChange={onTagsChange} onTagReset={onTagReset} addTagTitle={t('Add metadata')} addTagText={t('Enter metadata key')} />
</div> </div>
</Dialog> </Dialog>
); );
}); });
function onInfoClick(json, title) { function onInfoClick(json: unknown, title: string) {
showJson5Dialog({ title, json }); showJson5Dialog({ title, json });
} }
const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams, formatTimecode }) => { const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams, formatTimecode }: {
filePath: string, stream: FFprobeStream, onToggle: (a: number) => void, batchSetCopyStreamIds: (filter: (a: FFprobeStream) => boolean, enabled: boolean) => void, copyStream: boolean, fileDuration: number | undefined, setEditingStream: (a: EditingStream) => void, onExtractStreamPress?: () => void, paramsByStreamId: ParamsByStreamId, updateStreamParams: UpdateStreamParams, formatTimecode: FormatTimecode,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const effectiveDisposition = useMemo(() => getStreamEffectiveDisposition(paramsByStreamId, filePath, stream), [filePath, paramsByStreamId, stream]); const effectiveDisposition = useMemo(() => getStreamEffectiveDisposition(paramsByStreamId, filePath, stream), [filePath, paramsByStreamId, stream]);
const bitrate = parseInt(stream.bit_rate, 10); const bitrate = parseInt(stream.bit_rate!, 10);
const streamDuration = parseInt(stream.duration, 10); const streamDuration = parseInt(stream.duration, 10);
const duration = !Number.isNaN(streamDuration) ? streamDuration : fileDuration; const duration = !Number.isNaN(streamDuration) ? streamDuration : fileDuration;
let Icon; let Icon: typeof FaBan;
let codecTypeHuman; let codecTypeHuman;
// eslint-disable-next-line unicorn/prefer-switch // eslint-disable-next-line unicorn/prefer-switch
if (stream.codec_type === 'audio') { if (stream.codec_type === 'audio') {
@ -170,15 +191,18 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
const onClick = () => onToggle && onToggle(stream.index); const onClick = () => onToggle && onToggle(stream.index);
const onDispositionChange = useCallback((e) => { const onDispositionChange = useCallback<ChangeEventHandler<HTMLSelectElement>>((e) => {
let newDisposition; let newDisposition: string;
if (dispositionOptions.includes(e.target.value)) { if (dispositionOptions.includes(e.target.value)) {
newDisposition = e.target.value; newDisposition = e.target.value;
} else if (e.target.value === deleteDispositionValue) { } else if (e.target.value === deleteDispositionValue) {
newDisposition = deleteDispositionValue; // needs a separate value (not a real disposition) newDisposition = deleteDispositionValue; // needs a separate value (not a real disposition)
} // else unchanged (undefined) } // else unchanged (undefined)
updateStreamParams(filePath, stream.index, (params) => params.set('disposition', newDisposition)); updateStreamParams(filePath, stream.index, (params) => {
// eslint-disable-next-line no-param-reassign
params.disposition = newDisposition;
});
}, [filePath, updateStreamParams, stream.index]); }, [filePath, updateStreamParams, stream.index]);
const codecTag = stream.codec_tag !== '0x0000' && stream.codec_tag_string; const codecTag = stream.codec_tag !== '0x0000' && stream.codec_tag_string;
@ -191,7 +215,7 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
</td> </td>
<td style={{ maxWidth: '3em', overflow: 'hidden' }} title={stream.codec_name}>{stream.codec_name} {codecTag}</td> <td style={{ maxWidth: '3em', overflow: 'hidden' }} title={stream.codec_name}>{stream.codec_name} {codecTag}</td>
<td> <td>
{!Number.isNaN(duration) && `${formatTimecode({ seconds: duration, shorten: true })}`} {duration != null && !Number.isNaN(duration) && `${formatTimecode({ seconds: duration, shorten: true })}`}
{stream.nb_frames != null ? <div>{stream.nb_frames}f</div> : null} {stream.nb_frames != null ? <div>{stream.nb_frames}f</div> : null}
</td> </td>
<td>{!Number.isNaN(bitrate) && (stream.codec_type === 'audio' ? `${Math.round(bitrate / 1000)} kbps` : prettyBytes(bitrate, { bits: true }))}</td> <td>{!Number.isNaN(bitrate) && (stream.codec_type === 'audio' ? `${Math.round(bitrate / 1000)} kbps` : prettyBytes(bitrate, { bits: true }))}</td>
@ -247,7 +271,9 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
); );
}); });
const FileHeading = ({ path, formatData, chapters, onTrashClick, onEditClick, setCopyAllStreams, onExtractAllStreamsPress }) => { function FileHeading({ path, formatData, chapters, onTrashClick, onEditClick, setCopyAllStreams, onExtractAllStreamsPress }: {
path: string, formatData: FFprobeFormat | undefined, chapters?: FFprobeChapter[] | undefined, onTrashClick?: (() => void) | undefined, onEditClick?: (() => void) | undefined, setCopyAllStreams: (a: boolean) => void, onExtractAllStreamsPress?: () => Promise<void>,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -265,11 +291,11 @@ const FileHeading = ({ path, formatData, chapters, onTrashClick, onEditClick, se
{onExtractAllStreamsPress && <IconButton iconSize={16} title={t('Export each track as individual files')} icon={ForkIcon} onClick={onExtractAllStreamsPress} appearance="minimal" />} {onExtractAllStreamsPress && <IconButton iconSize={16} title={t('Export each track as individual files')} icon={ForkIcon} onClick={onExtractAllStreamsPress} appearance="minimal" />}
</div> </div>
); );
}; }
const thStyle = { borderBottom: '1px solid var(--gray6)', paddingBottom: '.5em' }; const thStyle: CSSProperties = { borderBottom: '1px solid var(--gray6)', paddingBottom: '.5em' };
const Thead = () => { function Thead() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<thead style={{ color: 'var(--gray12)', textAlign: 'left', fontSize: '.9em' }}> <thead style={{ color: 'var(--gray12)', textAlign: 'left', fontSize: '.9em' }}>
@ -286,31 +312,50 @@ const Thead = () => {
</tr> </tr>
</thead> </thead>
); );
}; }
const tableStyle = { fontSize: 14, width: '100%', borderCollapse: 'collapse' }; const tableStyle: CSSProperties = { fontSize: 14, width: '100%', borderCollapse: 'collapse' };
const fileStyle = { margin: '1.5em 1em 1.5em 1em', padding: 5, overflowX: 'auto' }; const fileStyle: CSSProperties = { margin: '1.5em 1em 1.5em 1em', padding: 5, overflowX: 'auto' };
const StreamsSelector = memo(({
mainFilePath, mainFileFormatData, mainFileStreams, mainFileChapters, isCopyingStreamId, toggleCopyStreamId, function StreamsSelector({
setCopyStreamIdsForPath, onExtractStreamPress, onExtractAllStreamsPress, allFilesMeta, externalFilesMeta, setExternalFilesMeta, mainFilePath, mainFileFormatData, mainFileStreams, mainFileChapters, isCopyingStreamId, toggleCopyStreamId, setCopyStreamIdsForPath, onExtractStreamPress, onExtractAllStreamsPress, allFilesMeta, externalFilesMeta, setExternalFilesMeta, showAddStreamSourceDialog, shortestFlag, setShortestFlag, nonCopiedExtraStreams, customTagsByFile, setCustomTagsByFile, paramsByStreamId, updateStreamParams, formatTimecode,
showAddStreamSourceDialog, shortestFlag, setShortestFlag, nonCopiedExtraStreams, }: {
customTagsByFile, setCustomTagsByFile, paramsByStreamId, updateStreamParams, mainFilePath: string,
formatTimecode, mainFileFormatData: FFprobeFormat | undefined,
}) => { mainFileStreams: FFprobeStream[],
const [editingFile, setEditingFile] = useState(); mainFileChapters: FFprobeChapter[] | undefined,
const [editingStream, setEditingStream] = useState(); isCopyingStreamId: (path: string | undefined, streamId: number) => boolean,
toggleCopyStreamId: (path: string, index: number) => void,
setCopyStreamIdsForPath: (path: string, cb: (a: Record<string, boolean>) => Record<string, boolean>) => void,
onExtractStreamPress: (index: number) => void,
onExtractAllStreamsPress: () => Promise<void>,
allFilesMeta: FilesMeta,
externalFilesMeta: FilesMeta,
setExternalFilesMeta: Dispatch<SetStateAction<FilesMeta>>,
showAddStreamSourceDialog: () => Promise<void>,
shortestFlag: boolean,
setShortestFlag: Dispatch<SetStateAction<boolean>>,
nonCopiedExtraStreams: FFprobeStream[],
customTagsByFile: CustomTagsByFile,
setCustomTagsByFile: Dispatch<SetStateAction<CustomTagsByFile>>,
paramsByStreamId: ParamsByStreamId,
updateStreamParams: UpdateStreamParams,
formatTimecode: FormatTimecode,
}) {
const [editingFile, setEditingFile] = useState<string>();
const [editingStream, setEditingStream] = useState<EditingStream>();
const [editingTag, setEditingTag] = useState<string>();
const { t } = useTranslation(); const { t } = useTranslation();
const [editingTag, setEditingTag] = useState();
function getFormatDuration(formatData) { function getFormatDuration(formatData: FFprobeFormat | undefined) {
if (!formatData || !formatData.duration) return undefined; if (!formatData || !formatData.duration) return undefined;
const parsed = parseFloat(formatData.duration, 10); const parsed = parseFloat(formatData.duration);
if (Number.isNaN(parsed)) return undefined; if (Number.isNaN(parsed)) return undefined;
return parsed; return parsed;
} }
async function removeFile(path) { function removeFile(path: string) {
setCopyStreamIdsForPath(path, () => ({})); setCopyStreamIdsForPath(path, () => ({}));
setExternalFilesMeta((old) => { setExternalFilesMeta((old) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -319,7 +364,7 @@ const StreamsSelector = memo(({
}); });
} }
async function batchSetCopyStreamIdsForPath(path, streams, filter, enabled) { function batchSetCopyStreamIdsForPath(path: string, streams: FFprobeStream[], filter: (a: FFprobeStream) => boolean, enabled: boolean) {
setCopyStreamIdsForPath(path, (old) => { setCopyStreamIdsForPath(path, (old) => {
const ret = { ...old }; const ret = { ...old };
// eslint-disable-next-line unicorn/no-array-callback-reference // eslint-disable-next-line unicorn/no-array-callback-reference
@ -330,7 +375,7 @@ const StreamsSelector = memo(({
}); });
} }
async function setCopyAllStreamsForPath(path, enabled) { function setCopyAllStreamsForPath(path: string, enabled: boolean) {
setCopyStreamIdsForPath(path, (old) => Object.fromEntries(Object.entries(old).map(([streamId]) => [streamId, enabled]))); setCopyStreamIdsForPath(path, (old) => Object.fromEntries(Object.entries(old).map(([streamId]) => [streamId, enabled])));
} }
@ -354,7 +399,7 @@ const StreamsSelector = memo(({
stream={stream} stream={stream}
copyStream={isCopyingStreamId(mainFilePath, stream.index)} copyStream={isCopyingStreamId(mainFilePath, stream.index)}
onToggle={(streamId) => toggleCopyStreamId(mainFilePath, streamId)} onToggle={(streamId) => toggleCopyStreamId(mainFilePath, streamId)}
batchSetCopyStreamIds={(filter, enabled) => batchSetCopyStreamIdsForPath(mainFilePath, mainFileStreams, filter, enabled)} batchSetCopyStreamIds={(filter: (a: FFprobeStream) => boolean, enabled: boolean) => batchSetCopyStreamIdsForPath(mainFilePath, mainFileStreams, filter, enabled)}
setEditingStream={setEditingStream} setEditingStream={setEditingStream}
fileDuration={getFormatDuration(mainFileFormatData)} fileDuration={getFormatDuration(mainFileFormatData)}
onExtractStreamPress={() => onExtractStreamPress(stream.index)} onExtractStreamPress={() => onExtractStreamPress(stream.index)}
@ -381,7 +426,7 @@ const StreamsSelector = memo(({
stream={stream} stream={stream}
copyStream={isCopyingStreamId(path, stream.index)} copyStream={isCopyingStreamId(path, stream.index)}
onToggle={(streamId) => toggleCopyStreamId(path, streamId)} onToggle={(streamId) => toggleCopyStreamId(path, streamId)}
batchSetCopyStreamIds={(filter, enabled) => batchSetCopyStreamIdsForPath(path, streams, filter, enabled)} batchSetCopyStreamIds={(filter: (a: FFprobeStream) => boolean, enabled: boolean) => batchSetCopyStreamIdsForPath(path, streams, filter, enabled)}
setEditingStream={setEditingStream} setEditingStream={setEditingStream}
fileDuration={getFormatDuration(formatData)} fileDuration={getFormatDuration(formatData)}
paramsByStreamId={paramsByStreamId} paramsByStreamId={paramsByStreamId}
@ -425,10 +470,12 @@ const StreamsSelector = memo(({
isShown={editingFile != null} isShown={editingFile != null}
hasCancel={false} hasCancel={false}
confirmLabel={t('Done')} confirmLabel={t('Done')}
onCloseComplete={() => setEditingFile()} onCloseComplete={() => setEditingFile(undefined)}
isConfirmDisabled={editingTag != null} isConfirmDisabled={editingTag != null}
> >
<EditFileDialog editingFile={editingFile} editingTag={editingTag} setEditingTag={setEditingTag} allFilesMeta={allFilesMeta} customTagsByFile={customTagsByFile} setCustomTagsByFile={setCustomTagsByFile} /> <div style={{ color: 'black' }}>
{editingFile != null && <EditFileDialog editingFile={editingFile} editingTag={editingTag} setEditingTag={setEditingTag} allFilesMeta={allFilesMeta} customTagsByFile={customTagsByFile} setCustomTagsByFile={setCustomTagsByFile} />}
</div>
</Dialog> </Dialog>
{editingStream != null && ( {editingStream != null && (
@ -442,6 +489,6 @@ const StreamsSelector = memo(({
)} )}
</> </>
); );
}); }
export default StreamsSelector; export default memo(StreamsSelector);

Wyświetl plik

@ -560,7 +560,7 @@ export async function selectSegmentsByTagDialog() {
return { tagName: value1, tagValue: value2 }; return { tagName: value1, tagValue: value2 };
} }
export function showJson5Dialog({ title, json }) { export function showJson5Dialog({ title, json }: { title: string, json: unknown }) {
const html = ( const html = (
<SyntaxHighlighter language="javascript" style={syntaxStyle} customStyle={{ textAlign: 'left', maxHeight: 300, overflowY: 'auto', fontSize: 14 }}> <SyntaxHighlighter language="javascript" style={syntaxStyle} customStyle={{ textAlign: 'left', maxHeight: 300, overflowY: 'auto', fontSize: 14 }}>
{JSON5.stringify(json, null, 2)} {JSON5.stringify(json, null, 2)}

Wyświetl plik

@ -11,6 +11,7 @@ import { getSmartCutParams } from '../smartcut';
import { isDurationValid } from '../segments'; import { isDurationValid } from '../segments';
import { FFprobeStream } from '../../../../ffprobe'; import { FFprobeStream } from '../../../../ffprobe';
import { Html5ifyMode } from '../../../../types'; import { Html5ifyMode } from '../../../../types';
import { ParamsByStreamId } from '../types';
const { join, resolve, dirname } = window.require('path'); const { join, resolve, dirname } = window.require('path');
const { pathExists } = window.require('fs-extra'); const { pathExists } = window.require('fs-extra');
@ -181,6 +182,9 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const losslessCutSingle = useCallback(async ({ const losslessCutSingle = useCallback(async ({
keyframeCut: ssBeforeInput, avoidNegativeTs, copyFileStreams, cutFrom, cutTo, chaptersPath, onProgress, outPath, keyframeCut: ssBeforeInput, avoidNegativeTs, copyFileStreams, cutFrom, cutTo, chaptersPath, onProgress, outPath,
videoDuration, rotation, allFilesMeta, outFormat, appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, customTagsByFile, paramsByStreamId, videoTimebase, detectedFps, videoDuration, rotation, allFilesMeta, outFormat, appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, customTagsByFile, paramsByStreamId, videoTimebase, detectedFps,
}: {
keyframeCut: boolean, avoidNegativeTs: boolean, copyFileStreams, cutFrom, cutTo, chaptersPath, onProgress, outPath,
videoDuration, rotation, allFilesMeta, outFormat, appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, customTagsByFile, paramsByStreamId: ParamsByStreamId, videoTimebase, detectedFps,
}) => { }) => {
if (await shouldSkipExistingFile(outPath)) return; if (await shouldSkipExistingFile(outPath)) return;
@ -227,7 +231,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// This function tries to calculate the output stream index needed for -metadata:s:x and -disposition:x arguments // This function tries to calculate the output stream index needed for -metadata:s:x and -disposition:x arguments
// It is based on the assumption that copyFileStreamsFiltered contains the order of the input files (and their respective streams orders) sent to ffmpeg, to hopefully calculate the same output stream index values that ffmpeg does internally. // It is based on the assumption that copyFileStreamsFiltered contains the order of the input files (and their respective streams orders) sent to ffmpeg, to hopefully calculate the same output stream index values that ffmpeg does internally.
// It also takes into account previously added files that have been removed and disabled streams. // It also takes into account previously added files that have been removed and disabled streams.
function mapInputStreamIndexToOutputIndex(inputFilePath, inputFileStreamIndex) { function mapInputStreamIndexToOutputIndex(inputFilePath: string, inputFileStreamIndex: number) {
let streamCount = 0; let streamCount = 0;
// Count copied streams of all files until this input file // Count copied streams of all files until this input file
const foundFile = copyFileStreamsFiltered.find(({ path: path2, streamIds }) => { const foundFile = copyFileStreamsFiltered.find(({ path: path2, streamIds }) => {
@ -254,24 +258,24 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const ret: string[] = []; const ret: string[] = [];
for (const [fileId, fileParams] of paramsByStreamId.entries()) { for (const [fileId, fileParams] of paramsByStreamId.entries()) {
for (const [streamId, streamParams] of fileParams.entries()) { for (const [streamId, streamParams] of fileParams.entries()) {
const outputIndex = mapInputStreamIndexToOutputIndex(fileId, parseInt(streamId, 10)); const outputIndex = mapInputStreamIndexToOutputIndex(fileId, streamId);
if (outputIndex != null) { if (outputIndex != null) {
const disposition = streamParams.get('disposition'); const { disposition } = streamParams;
if (disposition != null) { if (disposition != null) {
// "0" means delete the disposition for this stream // "0" means delete the disposition for this stream
const dispositionArg = disposition === deleteDispositionValue ? '0' : disposition; const dispositionArg = disposition === deleteDispositionValue ? '0' : disposition;
ret.push(`-disposition:${outputIndex}`, String(dispositionArg)); ret.push(`-disposition:${outputIndex}`, String(dispositionArg));
} }
if (streamParams.get('bsfH264Mp4toannexb')) { if (streamParams.bsfH264Mp4toannexb) {
ret.push(`-bsf:${outputIndex}`, String('h264_mp4toannexb')); ret.push(`-bsf:${outputIndex}`, String('h264_mp4toannexb'));
} }
if (streamParams.get('bsfHevcMp4toannexb')) { if (streamParams.bsfHevcMp4toannexb) {
ret.push(`-bsf:${outputIndex}`, String('hevc_mp4toannexb')); ret.push(`-bsf:${outputIndex}`, String('hevc_mp4toannexb'));
} }
// custom stream metadata tags // custom stream metadata tags
const customTags = streamParams.get('customTags'); const { customTags } = streamParams;
if (customTags != null) { if (customTags != null) {
for (const [tag, value] of Object.entries(customTags)) { for (const [tag, value] of Object.entries(customTags)) {
ret.push(`-metadata:s:${outputIndex}`, `${tag}=${value}`); ret.push(`-metadata:s:${outputIndex}`, `${tag}=${value}`);

Wyświetl plik

@ -11,8 +11,8 @@ import { FFprobeStream } from '../../../../ffprobe';
const maxWaveforms = 100; const maxWaveforms = 100;
// const maxWaveforms = 3; // testing // const maxWaveforms = 3; // testing
export default ({ darkMode, filePath, relevantTime, durationSafe, waveformEnabled, audioStream, ffmpegExtractWindow }: { export default ({ darkMode, filePath, relevantTime, duration, waveformEnabled, audioStream, ffmpegExtractWindow }: {
darkMode: boolean, filePath: string | undefined, relevantTime: number, durationSafe: number, waveformEnabled: boolean, audioStream: FFprobeStream | undefined, ffmpegExtractWindow: number, darkMode: boolean, filePath: string | undefined, relevantTime: number, duration: number | undefined, waveformEnabled: boolean, audioStream: FFprobeStream | undefined, ffmpegExtractWindow: number,
}) => { }) => {
const creatingWaveformPromise = useRef<Promise<unknown>>(); const creatingWaveformPromise = useRef<Promise<unknown>>();
const [waveforms, setWaveforms] = useState<RenderableWaveform[]>([]); const [waveforms, setWaveforms] = useState<RenderableWaveform[]>([]);
@ -30,7 +30,7 @@ export default ({ darkMode, filePath, relevantTime, durationSafe, waveformEnable
}, [filePath, audioStream, setWaveforms]); }, [filePath, audioStream, setWaveforms]);
const waveformStartTime = Math.floor(relevantTime / ffmpegExtractWindow) * ffmpegExtractWindow; const waveformStartTime = Math.floor(relevantTime / ffmpegExtractWindow) * ffmpegExtractWindow;
const safeExtractDuration = Math.min(waveformStartTime + ffmpegExtractWindow, durationSafe) - waveformStartTime; const safeExtractDuration = duration != null ? Math.min(waveformStartTime + ffmpegExtractWindow, duration) - waveformStartTime : undefined;
const waveformStartTimeThrottled = useThrottle(waveformStartTime, 1000); const waveformStartTimeThrottled = useThrottle(waveformStartTime, 1000);
@ -39,7 +39,7 @@ export default ({ darkMode, filePath, relevantTime, durationSafe, waveformEnable
(async () => { (async () => {
const alreadyHaveWaveformAtTime = (waveformsRef.current ?? []).some((waveform) => waveform.from === waveformStartTimeThrottled); const alreadyHaveWaveformAtTime = (waveformsRef.current ?? []).some((waveform) => waveform.from === waveformStartTimeThrottled);
const shouldRun = !!filePath && audioStream && waveformEnabled && !alreadyHaveWaveformAtTime && !creatingWaveformPromise.current; const shouldRun = !!filePath && safeExtractDuration != null && audioStream && waveformEnabled && !alreadyHaveWaveformAtTime && !creatingWaveformPromise.current;
if (!shouldRun) return; if (!shouldRun) return;
try { try {
@ -72,9 +72,13 @@ export default ({ darkMode, filePath, relevantTime, durationSafe, waveformEnable
if (w.from !== waveformStartTimeThrottled) { if (w.from !== waveformStartTimeThrottled) {
return w; return w;
} }
// if we don't do this, we get Failed to construct 'Blob': The provided ArrayBufferView value must not be resizable.
const buffer2 = Buffer.allocUnsafe(buffer.length);
buffer.copy(buffer2);
return { return {
...w, ...w,
url: URL.createObjectURL(new Blob([buffer], { type: 'image/png' })), url: URL.createObjectURL(new Blob([buffer2], { type: 'image/png' })),
}; };
})); }));
} catch (err) { } catch (err) {

Wyświetl plik

@ -1,5 +1,6 @@
import type { MenuItem, MenuItemConstructorOptions } from 'electron'; import type { MenuItem, MenuItemConstructorOptions } from 'electron';
import { z } from 'zod'; import { z } from 'zod';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
export interface ChromiumHTMLVideoElement extends HTMLVideoElement { export interface ChromiumHTMLVideoElement extends HTMLVideoElement {
@ -96,3 +97,19 @@ export type UpdateSegAtIndex = (index: number, newProps: Partial<StateSegment>)
export type ContextMenuTemplate = (MenuItemConstructorOptions | MenuItem)[]; export type ContextMenuTemplate = (MenuItemConstructorOptions | MenuItem)[];
export type ExportMode = 'segments_to_chapters' | 'merge' | 'merge+separate' | 'separate'; export type ExportMode = 'segments_to_chapters' | 'merge' | 'merge+separate' | 'separate';
export type FilesMeta = Record<string, {
streams: FFprobeStream[];
formatData: FFprobeFormat;
chapters: FFprobeChapter[];
}>
export type CustomTagsByFile = Record<string, Record<string, string>>;
export interface StreamParams {
customTags?: Record<string, string>,
disposition?: string,
bsfH264Mp4toannexb?: boolean,
bsfHevcMp4toannexb?: boolean,
}
export type ParamsByStreamId = Map<string, Map<number, StreamParams>>;