show estimated segment file size

closes #2630
pull/2640/head
Mikael Finstad 8 months ago
parent 251120f048
commit 251120dd0c
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -109,7 +109,7 @@
"nanoid": "^5.0.9", "nanoid": "^5.0.9",
"p-map": "^5.5.0", "p-map": "^5.5.0",
"p-retry": "^6.2.0", "p-retry": "^6.2.0",
"pretty-bytes": "^6.0.0", "pretty-bytes": "^7.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^16.2.4", "react-i18next": "^16.2.4",

@ -325,7 +325,7 @@ function App() {
}, [isFileOpened]); }, [isFileOpened]);
const { const {
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, clearSegColorCounter, loadCutSegments, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, segmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, maybeCreateFullLengthSegment, currentCutSegOrWholeTimeline, segColorCounter, cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, getSegEstimatedSize, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, clearSegColorCounter, loadCutSegments, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, segmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, maybeCreateFullLengthSegment, currentCutSegOrWholeTimeline, segColorCounter,
} = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta, seekAbs, activeVideoStreamIndex, activeAudioStreamIndexes, handleError, showGenericDialog }); } = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta, seekAbs, activeVideoStreamIndex, activeAudioStreamIndexes, handleError, showGenericDialog });
const { getEdlFilePath, projectFileSavePath, getProjectFileSavePath } = useSegmentsAutoSave({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, customOutDir, cutSegments }); const { getEdlFilePath, projectFileSavePath, getProjectFileSavePath } = useSegmentsAutoSave({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, customOutDir, cutSegments });
@ -2541,6 +2541,7 @@ function App() {
setEditingSegmentTags={setEditingSegmentTags} setEditingSegmentTags={setEditingSegmentTags}
setEditingSegmentTagsSegmentIndex={setEditingSegmentTagsSegmentIndex} setEditingSegmentTagsSegmentIndex={setEditingSegmentTagsSegmentIndex}
onEditSegmentTags={onEditSegmentTags} onEditSegmentTags={onEditSegmentTags}
getSegEstimatedSize={getSegEstimatedSize}
/> />
)} )}
</AnimatePresence> </AnimatePresence>

@ -9,6 +9,7 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import prettyBytes from 'pretty-bytes';
import useContextMenu from './hooks/useContextMenu'; import useContextMenu from './hooks/useContextMenu';
import useUserSettings from './hooks/useUserSettings'; import useUserSettings from './hooks/useUserSettings';
@ -62,6 +63,7 @@ const Segment = memo(({
onExtractSelectedSegmentsFramesAsImages, onExtractSelectedSegmentsFramesAsImages,
onInvertSelectedSegments, onInvertSelectedSegments,
onDuplicateSegmentClick, onDuplicateSegmentClick,
getSegEstimatedSize,
}: { }: {
seg: StateSegment | InverseCutSegment, seg: StateSegment | InverseCutSegment,
index: number, index: number,
@ -93,6 +95,7 @@ const Segment = memo(({
onExtractSelectedSegmentsFramesAsImages: () => void, onExtractSelectedSegmentsFramesAsImages: () => void,
onInvertSelectedSegments: UseSegments['invertSelectedSegments'], onInvertSelectedSegments: UseSegments['invertSelectedSegments'],
onDuplicateSegmentClick: UseSegments['duplicateSegment'], onDuplicateSegmentClick: UseSegments['duplicateSegment'],
getSegEstimatedSize: UseSegments['getSegEstimatedSize'],
}) => { }) => {
const { invertCutSegments, darkMode } = useUserSettings(); const { invertCutSegments, darkMode } = useUserSettings();
const { t } = useTranslation(); const { t } = useTranslation();
@ -149,6 +152,7 @@ const Segment = memo(({
useContextMenu(ref, contextMenuTemplate); useContextMenu(ref, contextMenuTemplate);
const duration = useMemo(() => (seg.end == null ? undefined : seg.end - seg.start), [seg]); const duration = useMemo(() => (seg.end == null ? undefined : seg.end - seg.start), [seg]);
const estimatedSize = useMemo(() => getSegEstimatedSize(seg), [getSegEstimatedSize, seg]);
const timeStr = useMemo(() => ( const timeStr = useMemo(() => (
seg.end == null seg.end == null
@ -249,25 +253,32 @@ const Segment = memo(({
{...sortable.listeners} {...sortable.listeners}
role="button" role="button"
tabIndex={-1} tabIndex={-1}
style={{ cursor, color: 'var(--gray-12)', marginBottom: duration != null ? 3 : undefined, display: 'flex', alignItems: 'center', height: 16 }} style={{ cursor, color: 'var(--gray-12)', marginBottom: duration != null ? '.1em' : undefined, display: 'flex', alignItems: 'center', height: '1em' }}
onClick={handleDraggableClick} onClick={handleDraggableClick}
> >
{renderNumber()} {renderNumber()}
<span style={{ cursor, fontSize: Math.min(310 / timeStr.length, 12), whiteSpace: 'nowrap' }}>{timeStr}</span> <span style={{ cursor, fontSize: `${Math.min(1, 26 / timeStr.length) * 0.75}em`, whiteSpace: 'nowrap' }}>{timeStr}</span>
</div> </div>
{'name' in seg && seg.name && <span style={{ fontSize: 12, color: primaryTextColor, marginRight: '.3em' }}>{seg.name}</span>} {'name' in seg && seg.name && <span style={{ fontSize: '.75em', color: primaryTextColor, marginRight: '.3em' }}>{seg.name}</span>}
{Object.entries(tags).map(([name, value]) => ( {Object.entries(tags).map(([name, value]) => (
<span style={{ fontSize: 11, backgroundColor: 'var(--gray-5)', color: 'var(--gray-12)', borderRadius: '.4em', padding: '0 .2em', marginRight: '.1em' }} key={name}>{name}:<b>{value}</b></span> <span style={{ fontSize: '.7em', backgroundColor: 'var(--gray-5)', color: 'var(--gray-12)', borderRadius: '.4em', padding: '0 .2em', marginRight: '.1em' }} key={name}>{name}:<b>{value}</b></span>
))} ))}
{duration != null && ( {duration != null && (
<> <>
<div style={{ fontSize: 13 }}> <div style={{ fontSize: '.75em' }}>
{t('Duration')} {formatTimecode({ seconds: duration, shorten: true })} {t('Duration')} {formatTimecode({ seconds: duration, shorten: true })}
</div> </div>
<div style={{ fontSize: 12 }}> <div style={{ fontSize: '.75em' }}>
<Trans>{{ durationMsFormatted: Math.floor(duration * 1000) }} ms, {{ frameCount: (duration && getFrameCount(duration)) ?? '?' }} frames</Trans> <Trans>{{ durationMsFormatted: Math.floor(duration * 1000) }} ms</Trans>
<span>, <Trans>{{ frameCount: (duration && getFrameCount(duration)) ?? '?' }} frames</Trans></span>
{estimatedSize != null && (
<span style={{ fontSize: '.9em' }}>
, ~{prettyBytes(estimatedSize, { space: false, maximumFractionDigits: 1, minimumFractionDigits: 0 })}
</span>
)}
</div> </div>
</> </>
)} )}
@ -321,6 +332,7 @@ function SegmentList({
setEditingSegmentTags, setEditingSegmentTags,
setEditingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex,
onEditSegmentTags, onEditSegmentTags,
getSegEstimatedSize,
}: { }: {
width: number, width: number,
formatTimecode: FormatTimecode, formatTimecode: FormatTimecode,
@ -361,6 +373,7 @@ function SegmentList({
setEditingSegmentTags: Dispatch<SetStateAction<SegmentTags | undefined>>, setEditingSegmentTags: Dispatch<SetStateAction<SegmentTags | undefined>>,
setEditingSegmentTagsSegmentIndex: Dispatch<SetStateAction<number | undefined>>, setEditingSegmentTagsSegmentIndex: Dispatch<SetStateAction<number | undefined>>,
onEditSegmentTags: (index: number) => void, onEditSegmentTags: (index: number) => void,
getSegEstimatedSize: UseSegments['getSegEstimatedSize'],
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { getSegColor, nextSegColorIndex } = useSegColors(); const { getSegColor, nextSegColorIndex } = useSegColors();
@ -474,7 +487,7 @@ function SegmentList({
)} )}
</div> </div>
<div style={{ padding: '5px 10px', boxSizing: 'border-box', borderBottom: '1px solid var(--gray-6)', borderTop: '1px solid var(--gray-6)', display: 'flex', justifyContent: 'space-between', fontSize: 13 }}> <div style={{ padding: '5px 10px', boxSizing: 'border-box', borderBottom: '1px solid var(--gray-6)', borderTop: '1px solid var(--gray-6)', display: 'flex', justifyContent: 'space-between', fontSize: '.8em' }}>
<div>{t('Segments total:')}</div> <div>{t('Segments total:')}</div>
<div>{formatTimecode({ seconds: segmentsTotal })}</div> <div>{formatTimecode({ seconds: segmentsTotal })}</div>
</div> </div>
@ -586,6 +599,7 @@ function SegmentList({
onSelectAllMarkers={onSelectAllMarkers} onSelectAllMarkers={onSelectAllMarkers}
onInvertSelectedSegments={onInvertSelectedSegments} onInvertSelectedSegments={onInvertSelectedSegments}
onDuplicateSegmentClick={onDuplicateSegmentClick} onDuplicateSegmentClick={onDuplicateSegmentClick}
getSegEstimatedSize={getSegEstimatedSize}
/> />
); );
} }

@ -9,7 +9,7 @@ import { FaLink } from 'react-icons/fa';
import TextInput from '../components/TextInput'; import TextInput from '../components/TextInput';
import { detectSceneChanges as ffmpegDetectSceneChanges, readFrames, mapTimesToSegments, findKeyframeNearTime } from '../ffmpeg'; import { detectSceneChanges as ffmpegDetectSceneChanges, readFrames, mapTimesToSegments, findKeyframeNearTime } from '../ffmpeg';
import { shuffleArray } from '../util'; import { getFileSize, shuffleArray } from '../util';
import { errorToast } from '../swal'; import { errorToast } from '../swal';
import { createNumSegments as createNumSegmentsDialog, createFixedByteSixedSegments as createFixedByteSixedSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, askForSegmentDuration, toastError } from '../dialogs'; import { createNumSegments as createNumSegmentsDialog, createFixedByteSixedSegments as createFixedByteSixedSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, askForSegmentDuration, toastError } from '../dialogs';
import { createSegment, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, addSegmentColorIndex, filterNonMarkers, makeDurationSegments, isInitialSegment } from '../segments'; import { createSegment, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, addSegmentColorIndex, filterNonMarkers, makeDurationSegments, isInitialSegment } from '../segments';
@ -725,13 +725,22 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
const createFixedByteSizedSegments = useCallback(async () => { const createFixedByteSizedSegments = useCallback(async () => {
if (!checkFileOpened() || !isDurationValid(fileDuration)) return; if (!checkFileOpened() || !isDurationValid(fileDuration)) return;
const fileSize = mainFileMeta && parseInt(mainFileMeta.formatData.size, 10); invariant(mainFileMeta != null);
invariant(fileSize != null && !Number.isNaN(fileSize)); const fileSize = getFileSize(mainFileMeta.formatData);
invariant(fileSize != null);
const segmentDuration = await createFixedByteSixedSegmentsDialog({ fileDuration, fileSize }); const segmentDuration = await createFixedByteSixedSegmentsDialog({ fileDuration, fileSize });
if (segmentDuration == null) return; if (segmentDuration == null) return;
loadCutSegments({ segments: makeDurationSegments(segmentDuration, fileDuration), append: true, clampDuration: fileDuration }); loadCutSegments({ segments: makeDurationSegments(segmentDuration, fileDuration), append: true, clampDuration: fileDuration });
}, [checkFileOpened, fileDuration, loadCutSegments, mainFileMeta]); }, [checkFileOpened, fileDuration, loadCutSegments, mainFileMeta]);
const getSegEstimatedSize = useCallback((segment: Pick<StateSegment, 'start' | 'end'>) => {
if (mainFileMeta == null || !isDurationValid(fileDuration) || segment.end == null) return undefined;
const fileSize = getFileSize(mainFileMeta.formatData);
if (fileSize == null) return undefined;
const segDuration = segment.end - segment.start;
return Math.round((segDuration / fileDuration) * fileSize);
}, [fileDuration, mainFileMeta]);
const createRandomSegments = useCallback(async () => { const createRandomSegments = useCallback(async () => {
if (!checkFileOpened() || currentCutSegOrWholeTimeline.duration <= 0) return; if (!checkFileOpened() || currentCutSegOrWholeTimeline.duration <= 0) return;
const segments = await createRandomSegmentsDialog(currentCutSegOrWholeTimeline.duration); const segments = await createRandomSegmentsDialog(currentCutSegOrWholeTimeline.duration);
@ -978,6 +987,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
createFixedDurationSegments, createFixedDurationSegments,
createFixedByteSizedSegments, createFixedByteSizedSegments,
createRandomSegments, createRandomSegments,
getSegEstimatedSize,
haveInvalidSegs, haveInvalidSegs,
currentSegIndexSafe, currentSegIndexSafe,
currentCutSeg, currentCutSeg,

@ -11,6 +11,7 @@ import { ffmpegExtractWindow } from './util/constants';
import { appName } from '../../main/common'; import { appName } from '../../main/common';
import { Html5ifyMode } from '../../common/types'; import { Html5ifyMode } from '../../common/types';
import { UserFacingError } from '../errors'; import { UserFacingError } from '../errors';
import { FFprobeFormat } from '../../common/ffprobe';
const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path'); const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path');
const fsExtra = window.require('fs-extra'); const fsExtra = window.require('fs-extra');
@ -398,6 +399,12 @@ export function getImportProjectType(filePath: string) {
return edlFormatForExtension[matchingExt]; return edlFormatForExtension[matchingExt];
} }
export function getFileSize(formatData: FFprobeFormat) {
const fileSize = parseInt(formatData.size, 10);
if (Number.isNaN(fileSize)) return undefined;
return fileSize;
}
export const calcShouldShowWaveform = (zoomedDuration: number | undefined) => (zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8); export const calcShouldShowWaveform = (zoomedDuration: number | undefined) => (zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8);
export const calcShouldShowKeyframes = (zoomedDuration: number | undefined) => (zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8); export const calcShouldShowKeyframes = (zoomedDuration: number | undefined) => (zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8);

@ -9152,7 +9152,7 @@ __metadata:
nanoid: "npm:^5.0.9" nanoid: "npm:^5.0.9"
p-map: "npm:^5.5.0" p-map: "npm:^5.5.0"
p-retry: "npm:^6.2.0" p-retry: "npm:^6.2.0"
pretty-bytes: "npm:^6.0.0" pretty-bytes: "npm:^7.1.0"
react: "npm:^18.2.0" react: "npm:^18.2.0"
react-dom: "npm:^18.2.0" react-dom: "npm:^18.2.0"
react-i18next: "npm:^16.2.4" react-i18next: "npm:^16.2.4"
@ -10815,10 +10815,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pretty-bytes@npm:^6.0.0": "pretty-bytes@npm:^7.1.0":
version: 6.0.0 version: 7.1.0
resolution: "pretty-bytes@npm:6.0.0" resolution: "pretty-bytes@npm:7.1.0"
checksum: 10/0bb9f95e617236404b29a8392c6efd82d65805f622f5e809ecd70068102be857d4e3276c86d2a32fa2ef851cc29472e380945dab7bec83ec79bd57a19a10faf7 checksum: 10/75fb34fba44596dc69983039fce9288f3b247b513aba2d6dfe74727505fe43199fcd7a1cee95dbd5be56a431d30fbdb4d24d3a62b9cf22bb63fa706b81c2e540
languageName: node languageName: node
linkType: hard linkType: hard

Loading…
Cancel
Save