diff --git a/src/TimelineSeg.jsx b/src/TimelineSeg.jsx
index 6fd764e9..7ca78371 100644
--- a/src/TimelineSeg.jsx
+++ b/src/TimelineSeg.jsx
@@ -5,12 +5,11 @@ const { formatDuration } = require('./util');
const TimelineSeg = ({
- isCutRangeValid, duration: durationRaw, cutStartTime, cutEndTime, apparentCutStart,
- apparentCutEnd, isActive, segNum, onSegClick, color,
+ isCutRangeValid, duration, apparentCutStart, apparentCutEnd, isActive, segNum,
+ onSegClick, color,
}) => {
- const duration = durationRaw || 1;
- const cutSectionWidth = `${((apparentCutEnd - apparentCutStart) / duration) * 100}%`;
const markerWidth = 4;
+ const cutSectionWidth = `${Math.max(((apparentCutEnd - apparentCutStart) / duration) * 100, 1)}%`;
const startTimePos = `${(apparentCutStart / duration) * 100}%`;
const markerBorder = isActive ? `2px solid ${color.lighten(0.5).string()}` : undefined;
@@ -30,29 +29,39 @@ const TimelineSeg = ({
borderBottomRightRadius: markerBorderRadius,
};
+ const wrapperStyle = {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: startTimePos,
+ width: cutSectionWidth,
+ display: 'flex',
+ background: backgroundColor,
+ originX: 0,
+ borderRadius: markerBorderRadius,
+ };
+
const onThisSegClick = () => onSegClick(segNum);
return (
- {cutStartTime !== undefined && (
-
- )}
- {isCutRangeValid && (cutStartTime !== undefined || cutEndTime !== undefined) && (
-
- )}
- {cutEndTime !== undefined && (
+
+
+ apparentCutStart && formatDuration({ seconds: apparentCutEnd - apparentCutStart })}
+ >
+ {segNum + 1}
+
+ {apparentCutEnd > apparentCutStart && (
)}
diff --git a/src/ffmpeg.js b/src/ffmpeg.js
index 39d28eee..599cd3bd 100644
--- a/src/ffmpeg.js
+++ b/src/ffmpeg.js
@@ -63,16 +63,16 @@ function handleProgress(process, cutDuration, onProgress) {
}
async function cut({
- filePath, format, cutFrom, cutTo, cutToApparent, videoDuration, rotation,
+ filePath, format, cutFrom, cutTo, videoDuration, rotation,
onProgress, copyStreamIds, keyframeCut, outPath,
}) {
- console.log('Cutting from', cutFrom, 'to', cutToApparent);
+ console.log('Cutting from', cutFrom, 'to', cutTo);
- const cutDuration = cutToApparent - cutFrom;
+ const cutDuration = cutTo - cutFrom;
// https://github.com/mifi/lossless-cut/issues/50
const cutFromArgs = cutFrom === 0 ? [] : ['-ss', cutFrom];
- const cutToArgs = cutTo === undefined || cutTo === videoDuration ? [] : ['-t', cutDuration];
+ const cutToArgs = cutTo === videoDuration ? [] : ['-t', cutDuration];
const inputCutArgs = keyframeCut ? [
...cutFromArgs,
@@ -133,9 +133,9 @@ async function cutMultiple({
let i = 0;
// eslint-disable-next-line no-restricted-syntax,no-unused-vars
- for (const { cutFrom, cutTo, cutToApparent } of segments) {
+ for (const { cutFrom, cutTo } of segments) {
const ext = path.extname(filePath) || `.${format}`;
- const cutSpecification = `${formatDuration({ seconds: cutFrom, fileNameFriendly: true })}-${formatDuration({ seconds: cutToApparent, fileNameFriendly: true })}`;
+ const cutSpecification = `${formatDuration({ seconds: cutFrom, fileNameFriendly: true })}-${formatDuration({ seconds: cutTo, fileNameFriendly: true })}`;
const outPath = getOutPath(customOutDir, filePath, `${cutSpecification}${ext}`);
@@ -151,7 +151,6 @@ async function cutMultiple({
keyframeCut,
cutFrom,
cutTo,
- cutToApparent,
// eslint-disable-next-line no-loop-func
onProgress: progress => onSingleProgress(i, progress),
});
diff --git a/src/main.css b/src/main.css
index af026a53..ae1ca39c 100644
--- a/src/main.css
+++ b/src/main.css
@@ -61,12 +61,6 @@ input, button, textarea, :focus {
text-align: center;
}
-#current-time-display {
- text-align: center;
- color: rgba(255, 255, 255, 0.8);
- padding: .5em;
-}
-
.timeline-wrapper {
width: 100%;
position: relative;
diff --git a/src/renderer.jsx b/src/renderer.jsx
index 06511f85..bcf7fad5 100644
--- a/src/renderer.jsx
+++ b/src/renderer.jsx
@@ -9,6 +9,7 @@ import { Popover, Button } from 'evergreen-ui';
import fromPairs from 'lodash/fromPairs';
import clamp from 'lodash/clamp';
import clone from 'lodash/clone';
+import sortBy from 'lodash/sortBy';
import HelpSheet from './HelpSheet';
import TimelineSeg from './TimelineSeg';
@@ -113,6 +114,13 @@ const App = memo(() => {
setCopyStreamIds(v => ({ ...v, [index]: !v[index] }));
}
+ function toggleMute() {
+ setMuted((v) => {
+ if (!v) toast.fire({ title: 'Muted preview (note that exported file will not be affected)' });
+ return !v;
+ });
+ }
+
function seekAbs(val) {
const video = videoRef.current;
if (val == null || Number.isNaN(val)) return;
@@ -167,6 +175,71 @@ const App = memo(() => {
const frameRenderEnabled = !!(rotationPreviewRequested || dummyVideoPath);
+ // Because segments could have undefined start / end (meaning extend to start of timeline or end duration)
+ function getSegApparentStart(time) {
+ if (time !== undefined) return time;
+ return 0;
+ }
+
+ const getSegApparentEnd = useCallback((time) => {
+ if (time !== undefined) return time;
+ if (duration !== undefined) return duration;
+ return 0; // Haven't gotten duration yet
+ }, [duration]);
+
+ const apparentCutSegments = cutSegments.map(cutSegment => ({
+ ...cutSegment,
+ start: getSegApparentStart(cutSegment.start),
+ end: getSegApparentEnd(cutSegment.end),
+ }));
+
+ const invalidSegUuids = apparentCutSegments
+ .filter(cutSegment => cutSegment.start >= cutSegment.end)
+ .map(cutSegment => cutSegment.uuid);
+
+ const haveInvalidSegs = invalidSegUuids.length > 0;
+
+ const inverseCutSegments = (() => {
+ if (haveInvalidSegs) return undefined;
+ if (apparentCutSegments.length < 1) return undefined;
+
+ const sorted = sortBy(apparentCutSegments, 'start');
+
+ const foundOverlap = sorted.some((cutSegment, i) => {
+ if (i === 0) return false;
+ return sorted[i - 1].end > cutSegment.start;
+ });
+
+ if (foundOverlap) return undefined;
+
+ const ret = [];
+
+ if (sorted[0].start > 0) {
+ ret.push({
+ start: 0,
+ end: sorted[0].start,
+ });
+ }
+
+ sorted.forEach((cutSegment, i) => {
+ if (i === 0) return;
+ ret.push({
+ start: sorted[i - 1].end,
+ end: cutSegment.start,
+ });
+ });
+
+ const last = sorted[sorted.length - 1];
+ if (last.end < duration) {
+ ret.push({
+ start: last.end,
+ end: duration,
+ });
+ }
+
+ return ret;
+ })();
+
const setCutTime = useCallback((type, time) => {
const cloned = clone(cutSegments);
cloned[currentSeg][type] = time;
@@ -180,12 +253,9 @@ const App = memo(() => {
const getCutSeg = useCallback((i) => cutSegments[i !== undefined ? i : currentSeg],
[currentSeg, cutSegments]);
- const getCutStartTime = useCallback((i) => getCutSeg(i).start, [getCutSeg]);
- const getCutEndTime = useCallback((i) => getCutSeg(i).end, [getCutSeg]);
-
const addCutSegment = useCallback(() => {
- const cutStartTime = getCutStartTime();
- const cutEndTime = getCutEndTime();
+ const cutStartTime = getCutSeg().start;
+ const cutEndTime = getCutSeg().end;
if (cutStartTime === undefined && cutEndTime === undefined) return;
@@ -204,7 +274,7 @@ const App = memo(() => {
setCutSegments(cutSegmentsNew);
setCurrentSeg(currentSegNew);
}, [
- getCutEndTime, getCutStartTime, cutSegments, currentTime, duration,
+ getCutSeg, cutSegments, currentTime, duration,
]);
const setCutStart = useCallback(() => {
@@ -286,18 +356,12 @@ const App = memo(() => {
setRotationPreviewRequested(true);
}
- const getApparentCutStartTime = useCallback((i) => {
- const cutStartTime = getCutStartTime(i);
- if (cutStartTime !== undefined) return cutStartTime;
- return 0;
- }, [getCutStartTime]);
+ const getApparentCutStartTimeOrCurrent = useCallback((i) => getSegApparentStart(getCutSeg(i).start), [getCutSeg]);
- const getApparentCutEndTime = useCallback((i) => {
- const cutEndTime = getCutEndTime(i);
- if (cutEndTime !== undefined) return cutEndTime;
- if (duration !== undefined) return duration;
- return 0; // Haven't gotten duration yet
- }, [getCutEndTime, duration]);
+ const getApparentCutEndTimeOrCurrent = useCallback((i) => {
+ const cutEndTime = getCutSeg(i).end;
+ return getSegApparentEnd(cutEndTime);
+ }, [getCutSeg, getSegApparentEnd]);
const offsetCurrentTime = (currentTime || 0) + startTimeOffset;
@@ -343,8 +407,8 @@ const App = memo(() => {
setCutSegments(cutSegmentsNew);
}, [currentSeg, cutSegments]);
- const jumpCutStart = () => seekAbs(getApparentCutStartTime());
- const jumpCutEnd = () => seekAbs(getApparentCutEndTime());
+ const jumpCutStart = () => seekAbs(getApparentCutStartTimeOrCurrent());
+ const jumpCutEnd = () => seekAbs(getApparentCutEndTimeOrCurrent());
function handleTap(e) {
const target = timelineWrapperRef.current;
@@ -366,8 +430,10 @@ const App = memo(() => {
}, [playing]);
async function deleteSourceClick() {
+ if (!filePath) return;
+
// eslint-disable-next-line no-alert
- if (working || !window.confirm('Are you sure you want to move the source file to trash?')) return;
+ if (working || !window.confirm(`Are you sure you want to move the source file to trash? ${filePath}`)) return;
try {
setWorking(true);
@@ -381,19 +447,13 @@ const App = memo(() => {
}
}
- const isCutRangeValid = useCallback((i) => getApparentCutStartTime(i) < getApparentCutEndTime(i),
- [getApparentCutStartTime, getApparentCutEndTime]);
-
const cutClick = useCallback(async () => {
if (working) {
errorToast('I\'m busy');
return;
}
- const cutStartTime = getCutStartTime();
- const cutEndTime = getCutEndTime();
-
- if (!(isCutRangeValid() || cutEndTime === undefined || cutStartTime === undefined)) {
+ if (haveInvalidSegs) {
errorToast('Start time must be before end time');
return;
}
@@ -402,9 +462,8 @@ const App = memo(() => {
setWorking(true);
const segments = cutSegments.map((seg, i) => ({
- cutFrom: getApparentCutStartTime(i),
- cutTo: getCutEndTime(i),
- cutToApparent: getApparentCutEndTime(i),
+ cutFrom: getApparentCutStartTimeOrCurrent(i),
+ cutTo: getApparentCutEndTimeOrCurrent(i),
}));
const outFiles = await ffmpeg.cutMultiple({
@@ -444,9 +503,9 @@ const App = memo(() => {
setWorking(false);
}
}, [
- effectiveRotation, getApparentCutStartTime, getApparentCutEndTime, getCutEndTime,
- getCutStartTime, isCutRangeValid, working, cutSegments, duration, filePath, keyframeCut,
- autoMerge, customOutDir, fileFormat, copyStreamIds,
+ effectiveRotation, getApparentCutStartTimeOrCurrent, getApparentCutEndTimeOrCurrent,
+ working, cutSegments, duration, filePath, keyframeCut,
+ autoMerge, customOutDir, fileFormat, copyStreamIds, haveInvalidSegs,
]);
function showUnsupportedFileMessage() {
@@ -499,6 +558,7 @@ const App = memo(() => {
return ret;
}, [getHtml5ifiedPath]);
+
const load = useCallback(async (fp, html5FriendlyPathRequested) => {
console.log('Load', { fp, html5FriendlyPathRequested });
if (working) {
@@ -737,7 +797,7 @@ const App = memo(() => {
seekAbs(rel);
};
- const cutTime = type === 'start' ? getApparentCutStartTime() : getApparentCutEndTime();
+ const cutTime = type === 'start' ? getApparentCutStartTimeOrCurrent() : getApparentCutEndTimeOrCurrent();
return (
{
const durationSafe = duration || 1;
const currentTimePos = currentTime !== undefined && `${(currentTime / durationSafe) * 100}%`;
- const segColor = getCutSeg().color;
+ const segColor = (getCutSeg() || {}).color;
const segBgColor = segColor.alpha(0.5).string();
const jumpCutButtonStyle = {
@@ -797,17 +857,20 @@ const App = memo(() => {
Output format (default autodetected) |
{renderOutFmt()} |
+
- | In timecode show |
+ Output directory |
+ {customOutDir}
|
+
| Auto merge segments to one file after export? |
@@ -847,24 +910,23 @@ const App = memo(() => {
|
- | Output directory |
-
- {customOutDir}
+ Snapshot capture format
+ |
+
+ {renderCaptureFormatButton()}
|
+ | In timecode show |
- Snapshot capture format
- |
-
- {renderCaptureFormatButton()}
+
|
@@ -950,21 +1012,21 @@ const App = memo(() => {
)}
{working && (
-
-
- {cutProgress != null && (
-
- {`${Math.floor(cutProgress * 100)} %`}
-
- )}
-
+
+
+ {cutProgress != null && (
+
+ {`${Math.floor(cutProgress * 100)} %`}
+
+ )}
+
)}
- {/* eslint-disable jsx-a11y/media-has-caption */}
+ {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
- {/* eslint-enable jsx-a11y/media-has-caption */}
{rotationPreviewRequested && (
{
title="Mute preview? (will not affect output)"
size={30}
role="button"
- onClick={() => setMuted(v => !v)}
+ onClick={toggleMute}
/>
)}
@@ -1029,18 +1090,39 @@ const App = memo(() => {
color={seg.color}
onSegClick={currentSegNew => setCurrentSeg(currentSegNew)}
isActive={i === currentSeg}
- isCutRangeValid={isCutRangeValid(i)}
duration={durationSafe}
- cutStartTime={getCutStartTime(i)}
- cutEndTime={getCutEndTime(i)}
- apparentCutStart={getApparentCutStartTime(i)}
- apparentCutEnd={getApparentCutEndTime(i)}
+ apparentCutStart={getApparentCutStartTimeOrCurrent(i)}
+ apparentCutEnd={getApparentCutEndTimeOrCurrent(i)}
/>
))}
-
-
setTimecodeShowFrames(v => !v)}>
+
+ {inverseCutSegments && inverseCutSegments.map((seg) => (
+
+ ))}
+
+
+
+ setTimecodeShowFrames(v => !v)} style={{ background: 'rgba(0,0,0,0.5)', borderRadius: 3, padding: '2px 4px' }}>
{formatTimecode(offsetCurrentTime)}
@@ -1049,6 +1131,14 @@ const App = memo(() => {
+
+
{
title="Jump to end of video"
onClick={() => seekAbs(durationSafe)}
/>
-
-
-
-
{
size={30}
style={{ margin: '0 5px', color: 'white' }}
role="button"
- title="Add cut segment"
+ title="Add segment"
onClick={addCutSegment}
/>
@@ -1164,6 +1238,14 @@ const App = memo(() => {
/>
+
+
{renderCaptureFormatButton()}