1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 19:52:44 +01:00

improve renderibng performance #1881

This commit is contained in:
Mikael Finstad 2024-02-10 18:22:05 +08:00
parent d5fbac6f11
commit 18785e1f88
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
8 changed files with 146 additions and 70 deletions

View File

@ -31,6 +31,38 @@ const zoomOptions = Array(13).fill().map((unused, z) => 2 ** z);
const leftRightWidth = 100;
const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) => {
const { t } = useTranslation();
const onYinYangClick = useCallback(() => {
setInvertCutSegments(v => {
const newVal = !v;
if (newVal) toast.fire({ title: t('When you export, selected segments on the timeline will be REMOVED - the surrounding areas will be KEPT') });
else toast.fire({ title: t('When you export, selected segments on the timeline will be KEPT - the surrounding areas will be REMOVED.') });
return newVal;
});
}, [setInvertCutSegments, t]);
return (
<div style={{ marginLeft: 5 }}>
<motion.div
style={{ width: 24, height: 24 }}
animate={{ rotateX: invertCutSegments ? 0 : 180 }}
transition={{ duration: 0.3 }}
>
<FaYinYang
size={24}
role="button"
title={invertCutSegments ? t('Discard selected segments') : t('Keep selected segments')}
style={{ color: invertCutSegments ? primaryTextColor : undefined }}
onClick={onYinYangClick}
/>
</motion.div>
</div>
);
});
const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart }) => {
const { t } = useTranslation();
const { getSegColor } = useSegColors();
@ -179,15 +211,6 @@ const BottomBar = memo(({
const { invertCutSegments, setInvertCutSegments, simpleMode, toggleSimpleMode, exportConfirmEnabled } = useUserSettings();
const onYinYangClick = useCallback(() => {
setInvertCutSegments(v => {
const newVal = !v;
if (newVal) toast.fire({ title: t('When you export, selected segments on the timeline will be REMOVED - the surrounding areas will be KEPT') });
else toast.fire({ title: t('When you export, selected segments on the timeline will be KEPT - the surrounding areas will be REMOVED.') });
return newVal;
});
}, [setInvertCutSegments, t]);
const rotationStr = `${rotation}°`;
useEffect(() => {
@ -365,21 +388,7 @@ const BottomBar = memo(({
{!simpleMode && (
<>
<div style={{ marginLeft: 5 }}>
<motion.div
style={{ width: 24, height: 24 }}
animate={{ rotateX: invertCutSegments ? 0 : 180 }}
transition={{ duration: 0.3 }}
>
<FaYinYang
size={24}
role="button"
title={invertCutSegments ? t('Discard selected segments') : t('Keep selected segments')}
style={{ color: invertCutSegments ? primaryTextColor : undefined }}
onClick={onYinYangClick}
/>
</motion.div>
</div>
<InvertCutModeButton invertCutSegments={invertCutSegments} setInvertCutSegments={setInvertCutSegments} />
<div role="button" style={{ marginRight: 5, marginLeft: 10 }} title={t('Zoom')} onClick={timelineToggleComfortZoom}>{Math.floor(zoom)}x</div>

View File

@ -24,8 +24,7 @@ const buttonBaseStyle = {
const neutralButtonColor = 'var(--gray8)';
const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onEditSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => {
const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateSegOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onEditSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => {
const { t } = useTranslation();
const { getSegColor } = useSegColors();
@ -33,15 +32,18 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
const contextMenuTemplate = useMemo(() => {
if (invertCutSegments) return [];
const updateOrder = (dir) => updateSegOrder(index, index + dir);
return [
{ label: t('Jump to start time'), click: jumpSegStart },
{ label: t('Jump to end time'), click: jumpSegEnd },
{ label: t('Jump to start time'), click: () => jumpSegStart(index) },
{ label: t('Jump to end time'), click: () => jumpSegEnd(index) },
{ type: 'separator' },
{ label: t('Add segment'), click: addSegment },
{ label: t('Label segment'), click: onLabelPress },
{ label: t('Remove segment'), click: onRemovePress },
{ label: t('Label segment'), click: () => onLabelPress(index) },
{ label: t('Remove segment'), click: () => onRemovePress(index) },
{ label: t('Duplicate segment'), click: () => onDuplicateSegmentClick(seg) },
{ type: 'separator' },
@ -60,7 +62,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
{ type: 'separator' },
{ label: t('Change segment order'), click: onReorderPress },
{ label: t('Change segment order'), click: () => onReorderPress(index) },
{ label: t('Increase segment order'), click: () => updateOrder(1) },
{ label: t('Decrease segment order'), click: () => updateOrder(-1) },
@ -69,7 +71,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
{ label: t('Segment tags'), click: () => onEditSegmentTags(index) },
{ label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg.segId]) },
];
}, [invertCutSegments, t, jumpSegStart, jumpSegEnd, addSegment, onLabelPress, onRemovePress, onLabelSelectedSegments, onRemoveSelected, onReorderPress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, updateOrder, onEditSegmentTags, index, onExtractSegmentFramesAsImages]);
}, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]);
useContextMenu(ref, contextMenuTemplate);
@ -95,10 +97,10 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
const timeStr = useMemo(() => `${formatTimecode({ seconds: seg.start })} - ${formatTimecode({ seconds: seg.end })}`, [seg.start, seg.end, formatTimecode]);
function onDoubleClick() {
const onDoubleClick = useCallback(() => {
if (invertCutSegments) return;
jumpSegStart();
}
jumpSegStart(index);
}, [index, invertCutSegments, jumpSegStart]);
const durationMsFormatted = Math.floor(durationMs);
const frameCount = getFrameCount(duration);
@ -114,14 +116,18 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
const tags = useMemo(() => getSegmentTags(seg), [seg]);
const maybeOnClick = useCallback(() => !invertCutSegments && onClick(index), [index, invertCutSegments, onClick]);
const motionStyle = useMemo(() => ({ originY: 0, margin: '5px 0', background: 'var(--gray2)', border: isActive ? '1px solid var(--gray10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }), [isActive]);
return (
<motion.div
ref={ref}
role="button"
onClick={() => !invertCutSegments && onClick(index)}
onClick={maybeOnClick}
onDoubleClick={onDoubleClick}
layout
style={{ originY: 0, margin: '5px 0', background: 'var(--gray2)', border: isActive ? '1px solid var(--gray10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }}
style={motionStyle}
initial={{ scaleY: 0 }}
animate={{ scaleY: 1, opacity: !selected && !invertCutSegments ? 0.5 : undefined }}
exit={{ scaleY: 0 }}
@ -171,7 +177,7 @@ const SegmentList = memo(({
const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments;
const sortableList = segments.map((seg) => ({ id: seg.segId, seg }));
const sortableList = useMemo(() => segments.map((seg) => ({ id: seg.segId, seg })), [segments]);
const setSortableList = useCallback((newList) => {
if (isEqual(segments.map((s) => s.segId), newList.map((l) => l.id))) return; // No change
@ -189,7 +195,7 @@ const SegmentList = memo(({
}
}
async function onReorderSegs(index) {
const onReorderSegs = useCallback(async (index) => {
if (apparentCutSegments.length < 2) return;
const { value } = await Swal.fire({
title: `${t('Change order of segment')} ${index + 1}`,
@ -207,7 +213,7 @@ const SegmentList = memo(({
const newOrder = parseInt(value, 10);
updateSegOrder(index, newOrder - 1);
}
}
}, [apparentCutSegments.length, t, updateSegOrder]);
function renderFooter() {
const getButtonColor = (seg) => getSegColor(seg).desaturate(0.3).lightness(darkMode ? 45 : 55).string();
@ -340,12 +346,12 @@ const SegmentList = memo(({
onClick={onSegClick}
addSegment={addSegment}
onRemoveSelected={onRemoveSelected}
onRemovePress={() => removeCutSegment(index)}
onReorderPress={() => onReorderSegs(index)}
onLabelPress={() => onLabelSegment(index)}
jumpSegStart={() => jumpSegStart(index)}
jumpSegEnd={() => jumpSegEnd(index)}
updateOrder={(dir) => updateSegOrder(index, index + dir)}
onRemovePress={removeCutSegment}
onReorderPress={onReorderSegs}
onLabelPress={onLabelSegment}
jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd}
updateSegOrder={updateSegOrder}
getFrameCount={getFrameCount}
formatTimecode={formatTimecode}
currentSegIndex={currentSegIndex}

View File

@ -52,6 +52,11 @@ const CommandedTime = memo(({ commandedTimePercent }) => {
);
});
const timelineHeight = 36;
const timeWrapperStyle = { position: 'absolute', height: timelineHeight, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' };
const timeStyle = { background: 'rgba(0,0,0,0.4)', borderRadius: 3, padding: '2px 4px', color: 'rgba(255, 255, 255, 0.8)' };
const Timeline = memo(({
durationSafe, startTimeOffset, playerTime, commandedTime, relevantTime,
zoom, neighbouringKeyFrames, seekAbs, apparentCutSegments,
@ -62,8 +67,6 @@ const Timeline = memo(({
}) => {
const { t } = useTranslation();
const timelineHeight = 36;
const { invertCutSegments } = useUserSettings();
const timelineScrollerRef = useRef();
@ -328,8 +331,8 @@ const Timeline = memo(({
</div>
</div>
<div style={{ position: 'absolute', height: timelineHeight, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
<div style={{ background: 'rgba(0,0,0,0.4)', borderRadius: 3, padding: '2px 4px', color: 'rgba(255, 255, 255, 0.8)' }}>
<div style={timeWrapperStyle}>
<div style={timeStyle}>
{formatTimeAndFrames(displayTime)}{isZoomed ? ` ${displayTimePercent}` : ''}
</div>
</div>

View File

@ -1,8 +1,9 @@
import React, { memo, useCallback } from 'react';
import { IoIosSettings } from 'react-icons/io';
import { FaLock, FaUnlock } from 'react-icons/fa';
import { IconButton, Button, CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui';
import { IconButton, CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import Button from './components/Button';
import ExportModeButton from './components/ExportModeButton';
@ -11,6 +12,9 @@ import { primaryTextColor, controlsBackground, darkModeTransition } from './colo
import useUserSettings from './hooks/useUserSettings';
const outFmtStyle = { height: 20, maxWidth: 100 };
const exportModeStyle = { flexGrow: 0, flexBasis: 140 };
const TopMenu = memo(({
filePath, fileFormat, copyAnyAudioTrack, toggleStripAudio,
renderOutFmt, numStreamsToCopy, numStreamsTotal, setStreamsSelectorShown, toggleSettings,
@ -35,17 +39,20 @@ const TopMenu = memo(({
>
{filePath && (
<>
<Button height={20} iconBefore={ListIcon} onClick={withBlur(() => setStreamsSelectorShown(true))}>
<Button onClick={withBlur(() => setStreamsSelectorShown(true))}>
<ListIcon size="1em" verticalAlign="middle" marginRight=".3em" />
{t('Tracks')} ({numStreamsToCopy}/{numStreamsTotal})
</Button>
<Button
iconBefore={copyAnyAudioTrack ? VolumeUpIcon : VolumeOffIcon}
height={20}
title={copyAnyAudioTrack ? t('Keep audio tracks') : t('Discard audio tracks')}
onClick={withBlur(toggleStripAudio)}
>
{copyAnyAudioTrack ? t('Keep audio') : t('Discard audio')}
{copyAnyAudioTrack ? (
<><VolumeUpIcon size="1em" verticalAlign="middle" marginRight=".3em" />{t('Keep audio')}</>
) : (
<><VolumeOffIcon size="1em" verticalAlign="middle" marginRight=".3em" />{t('Discard audio')}</>
)}
</Button>
</>
)}
@ -53,35 +60,34 @@ const TopMenu = memo(({
<div style={{ flexGrow: 1 }} />
{showClearWorkingDirButton && (
<IconButton
intent="danger"
icon={CrossIcon}
height={20}
<CrossIcon
role="button"
tabIndex={0}
style={{ width: 20 }}
onClick={withBlur(clearOutDir)}
title={t('Clear working directory')}
/>
)}
<Button
height={20}
onClick={withBlur(changeOutDir)}
title={customOutDir}
paddingLeft={showClearWorkingDirButton ? 4 : undefined}
style={{ paddingLeft: showClearWorkingDirButton ? 4 : undefined }}
>
{customOutDir ? t('Working dir set') : t('Working dir unset')}
</Button>
{filePath && (
<>
{renderOutFmt({ height: 20, maxWidth: 100 })}
{renderOutFmt(outFmtStyle)}
{!simpleMode && (isCustomFormatSelected || outFormatLocked) && renderFormatLock()}
<ExportModeButton selectedSegments={selectedSegments} style={{ flexGrow: 0, flexBasis: 140 }} />
<ExportModeButton selectedSegments={selectedSegments} style={exportModeStyle} />
</>
)}
<IoIosSettings size={24} role="button" onClick={toggleSettings} style={{ verticalAlign: 'middle', marginLeft: 5 }} />
<IoIosSettings size={24} role="button" onClick={toggleSettings} style={{ marginLeft: 5 }} />
</div>
);
});

10
src/components/Button.jsx Normal file
View File

@ -0,0 +1,10 @@
import React, { memo } from 'react';
import styles from './Button.module.css';
const Button = memo(({ type = 'button', ...props }) => (
// eslint-disable-next-line react/jsx-props-no-spreading, react/button-has-type
<button className={styles.button} type={type} {...props} />
));
export default Button;

View File

@ -0,0 +1,12 @@
.button {
appearance: none;
font: inherit;
line-height: 140%;
font-size: .8em;
background-color: var(--gray3);
color: var(--gray12);
border-radius: .3em;
padding: 0 .5em 0 .3em;
outline: .05em solid var(--gray8);
border: .05em solid var(--gray7);
}

View File

@ -1,5 +1,6 @@
import dataUriToBuffer from 'data-uri-to-buffer';
import pMap from 'p-map';
import { useCallback } from 'react';
import { getSuffixedOutPath, getOutDir, transferTimestamps, getSuffixedFileName, getOutPath, escapeRegExp, fsOperationWithRetry } from '../util';
import { getNumDigits } from '../segments';
@ -23,7 +24,7 @@ function getFrameFromVideo(video, format, quality) {
}
export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
async function captureFramesRange({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }) {
const captureFramesRange = useCallback(async ({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }) => {
const getSuffix = (prefix) => `${prefix}.${captureFormat}`;
if (!outputTimestamps) {
@ -67,9 +68,9 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
}, { concurrency: 1 });
return outPaths[0];
}
}, [formatTimecode]);
async function captureFrameFromFfmpeg({ customOutDir, filePath, fromTime, captureFormat, quality }) {
const captureFrameFromFfmpeg = useCallback(async ({ customOutDir, filePath, fromTime, captureFormat, quality }) => {
const time = formatTimecode({ seconds: fromTime, fileNameFriendly: true });
const nameSuffix = `${time}.${captureFormat}`;
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix });
@ -77,9 +78,9 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
await transferTimestamps({ inPath: filePath, outPath, cutFrom: fromTime, treatOutputFileModifiedTimeAsStart });
return outPath;
}
}, [formatTimecode, treatOutputFileModifiedTimeAsStart]);
async function captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, quality }) {
const captureFrameFromTag = useCallback(async ({ customOutDir, filePath, currentTime, captureFormat, video, quality }) => {
const buf = getFrameFromVideo(video, captureFormat, quality);
const ext = mime.extension(buf.type);
@ -90,7 +91,7 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
await transferTimestamps({ inPath: filePath, outPath, cutFrom: currentTime, treatOutputFileModifiedTimeAsStart });
return outPath;
}
}, [formatTimecode, treatOutputFileModifiedTimeAsStart]);
return {
captureFramesRange,

View File

@ -0,0 +1,29 @@
import React from 'react';
// https://stackoverflow.com/questions/64997362/how-do-i-see-what-props-have-changed-in-react
export default function useWhatChanged(props) {
// cache the last set of props
const prev = React.useRef(props);
React.useEffect(() => {
// check each prop to see if it has changed
const changed = Object.entries(props).reduce((a, [key, prop]) => {
if (prev.current[key] === prop) return a;
return {
...a,
[key]: {
prev: prev.current[key],
next: prop,
},
};
}, {});
if (Object.keys(changed).length > 0) {
console.group('Props That Changed');
console.log(changed);
console.groupEnd();
}
prev.current = props;
}, [props]);
}