Skip to content

Commit b0c7dca

Browse files
committed
reuse clipping for merging video/audio tracks, style
1 parent 34db75d commit b0c7dca

File tree

7 files changed

+61
-132
lines changed

7 files changed

+61
-132
lines changed

src/extension/codemic.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -868,12 +868,12 @@ class CodeMic {
868868
return ok;
869869
}
870870

871-
case 'recorder/mergeVideoTracks': {
871+
case 'recorder/mergeVideoAudioTracks': {
872872
await this.context.withProgress(
873-
{ title: `Merging video tracks`, cancellable: true },
873+
{ title: `Merging video/audio tracks`, cancellable: true },
874874
async (progress, abortController) => {
875875
assert(this.session?.isLoaded());
876-
const change = await this.session.editor.mergeVideoTracks(progress, abortController, req.deleteOld);
876+
const change = await this.session.editor.mergeVideoAudioTracks(progress, abortController, req.deleteOld);
877877
if (change) await this.session.rr.enqueueSyncAfterSessionChange(change);
878878
await this.updateFrontend();
879879
},

src/extension/session/session_editor.ts

Lines changed: 43 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -595,124 +595,34 @@ export default class SessionEditor {
595595
});
596596
}
597597

598-
async mergeVideoTracks(
598+
async mergeVideoAudioTracks(
599599
progress: Progress,
600600
abortController: AbortController,
601601
deleteOld?: boolean,
602602
): Promise<t.SessionChange | undefined> {
603603
assert(this.session.isLoaded());
604604

605-
const individualFilesProgressMultiplier = 0.9;
606-
607-
if (this.session.body.videoTracks.length === 0) return;
608-
609-
const tempDir = path.join(this.session.core.dataPath, 'temp');
610-
await fs.promises.rm(tempDir, { recursive: true, force: true });
611-
await fs.promises.mkdir(tempDir, { recursive: true });
612-
613-
const sortedVideoTracks = _.orderBy(this.session.body.videoTracks, t => t.clockRange.start);
614-
const videoFiles: string[] = [];
615-
for (const [i, t] of sortedVideoTracks.entries()) {
616-
if (abortController.signal.aborted) return;
617-
618-
progress.report({ message: t.title });
619-
assert(t.file.type === 'blob');
620-
621-
const startOfNext = sortedVideoTracks[i + 1]?.clockRange.start ?? this.session.head.duration;
622-
const gap = startOfNext - t.clockRange.end;
623-
624-
const origFilePath = path.join(this.session.core.dataPath, 'blobs', t.file.sha1);
625-
const outFilePath = path.join(tempDir, t.title + '-' + (i + 1));
626-
627-
console.log('ffmpeg out: ', outFilePath, 'gap: ', gap);
628-
629-
if (gap > 0) {
630-
const vFilter = gap > 0 ? `tpad=stop_mode=clone:stop_duration=${gap}` : 'null';
631-
const args = [
632-
'-y',
633-
'-i',
634-
origFilePath,
635-
'-filter:v',
636-
vFilter,
637-
'-map',
638-
'0:v',
639-
'-an',
640-
'-c:v',
641-
'libx264',
642-
'-preset',
643-
'ultrafast',
644-
'-movflags',
645-
'+faststart',
646-
'-f',
647-
'mp4',
648-
outFilePath,
649-
];
650-
if (config.debug) {
651-
console.log('ffmpeg ' + args.join(' '));
652-
}
653-
const { stdout, stderr } = await execFile('ffmpeg', args);
654-
if (config.debug && stderr.trim()) console.error(stderr);
655-
videoFiles.push(outFilePath);
656-
} else if (gap < 0) {
657-
throw new Error('Found overlapping videos');
658-
} else {
659-
videoFiles.push(origFilePath);
660-
}
605+
let limitRange: t.ClockRange;
606+
const editorSelection = this.selection?.type === 'editor' && {
607+
start: Math.min(this.selection.focus, this.selection.anchor),
608+
end: Math.max(this.selection.focus, this.selection.anchor),
609+
};
661610

662-
progress.report({
663-
message: t.title,
664-
increment: (1 / sortedVideoTracks.length) * individualFilesProgressMultiplier * 100,
665-
});
611+
if (editorSelection && editorSelection.end > editorSelection.start) {
612+
limitRange = editorSelection;
613+
} else {
614+
limitRange = { start: 0, end: this.session.head.duration };
666615
}
667616

668-
if (abortController.signal.aborted) return;
669-
670-
progress.report({ message: 'Final output' });
671-
672-
const videoFilesStr = videoFiles.map(f => `file ${f}`.replace(/'/g, "'\\''")).join('\n');
673-
const videoFilesListPath = path.join(tempDir, 'list');
674-
await fs.promises.writeFile(videoFilesListPath, videoFilesStr, 'utf8');
675-
const finalOutFilePath = path.join(tempDir, 'final-output.mp4');
676-
677-
const concatArgs = [
678-
'-y',
679-
'-f',
680-
'concat',
681-
'-safe',
682-
'0',
683-
'-i',
684-
videoFilesListPath,
685-
'-an', // no audio
686-
'-c:v',
687-
'libx264', // reencode video with H.264
688-
'-movflags',
689-
'+faststart',
690-
'-f',
691-
'mp4', // force MP4 container
692-
finalOutFilePath,
693-
];
694-
if (config.debug) console.log('ffmpeg ' + concatArgs.join(' '));
695-
await execFile('ffmpeg', concatArgs);
696-
progress.report({ message: 'Done', increment: (1 - individualFilesProgressMultiplier) * 100 });
697-
698-
// TODO use stream.
699-
const finalData = await fs.promises.readFile(finalOutFilePath);
700-
const sha1 = await misc.computeSHA1(finalData);
701-
await this.session.core.copyToBlob(finalOutFilePath, sha1);
702-
await fs.promises.rm(tempDir, { recursive: true, force: true });
703-
704-
const finalVideoTrack: t.VideoTrack = {
705-
id: uuid(),
706-
clockRange: { start: sortedVideoTracks[0].clockRange.start, end: this.session.head.duration },
707-
title: 'Merged videos',
708-
type: 'video',
709-
file: { type: 'blob', sha1 },
710-
};
617+
const finalVideoTrack = await this.mergeAudioAndVideoTracksHelper(progress, abortController, limitRange);
711618

712-
if (abortController.signal.aborted) return;
619+
if (!finalVideoTrack) return;
713620

714621
return this.insertApplySessionPatch({
715-
body: { videoTracks: deleteOld ? [finalVideoTrack] : [...this.session.body.videoTracks, finalVideoTrack] },
622+
body: {
623+
videoTracks: deleteOld ? [finalVideoTrack] : [...this.session.body.videoTracks, finalVideoTrack],
624+
audioTracks: deleteOld ? [] : this.session.body.audioTracks,
625+
},
716626
effects: [{ type: 'media' }],
717627
});
718628
}
@@ -724,6 +634,31 @@ export default class SessionEditor {
724634
const start = Math.min(this.selection.anchor, this.selection.focus);
725635
const end = Math.max(this.selection.anchor, this.selection.focus);
726636
const limitRange: t.ClockRange = { start, end };
637+
638+
const finalVideoTrack = await this.mergeAudioAndVideoTracksHelper(progress, abortController, limitRange);
639+
640+
if (!finalVideoTrack) return;
641+
642+
const change1 = this.insertApplySessionPatch({
643+
head: { isClip: true },
644+
body: { videoTracks: [finalVideoTrack], audioTracks: [] },
645+
effects: [{ type: 'media' }],
646+
});
647+
648+
const mergeRange: t.ClockRange = { start: 0, end: limitRange.start };
649+
const change2 = this.merge(mergeRange, true);
650+
const change3 = this.crop(limitRange.end - limitRange.start, true);
651+
652+
return [change1, change2, change3];
653+
}
654+
655+
async mergeAudioAndVideoTracksHelper(
656+
progress: Progress,
657+
abortController: AbortController,
658+
limitRange: t.ClockRange,
659+
): Promise<t.RangedTrackFile | undefined> {
660+
assert(this.session.isLoaded());
661+
727662
const tempDir = path.join(this.session.core.dataPath, 'temp');
728663
const blobDir = path.join(this.session.core.dataPath, 'blobs');
729664

@@ -750,22 +685,12 @@ export default class SessionEditor {
750685
const finalVideoTrack: t.VideoTrack = {
751686
id: uuid(),
752687
clockRange: { start: limitRange.start, end: limitRange.end },
753-
title: 'Merged videos',
688+
title: 'Merged',
754689
type: 'video',
755690
file: { type: 'blob', sha1 },
756691
};
757692

758-
const change1 = this.insertApplySessionPatch({
759-
head: { isClip: true },
760-
body: { videoTracks: [finalVideoTrack], audioTracks: [] },
761-
effects: [{ type: 'media' }],
762-
});
763-
764-
const mergeRange: t.ClockRange = { start: 0, end: limitRange.start };
765-
const change2 = this.merge(mergeRange, true);
766-
const change3 = this.crop(limitRange.end - limitRange.start, true);
767-
768-
return [change1, change2, change3];
693+
return finalVideoTrack;
769694
}
770695

771696
/**

src/lib/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export type FrontendToBackendReqRes =
7676
| { request: { type: 'recorder/crop'; clock: number; adjustMediaTracks: boolean }; response: OKResponse }
7777
| { request: { type: 'recorder/forkSession'; handle: string; workspace: string }; response: OKResponse }
7878
| { request: { type: 'recorder/makeClip' }; response: OKResponse }
79-
| { request: { type: 'recorder/mergeVideoTracks'; deleteOld?: boolean }; response: OKResponse }
79+
| { request: { type: 'recorder/mergeVideoAudioTracks'; deleteOld?: boolean }; response: OKResponse }
8080
| { request: { type: 'recorder/makeTest' }; response: OKResponse }
8181
| { request: { type: 'getStore' }; response: StoreResponse }
8282
| { request: { type: 'showOpenDialog'; options: OpenDialogOptions }; response: UrisResponse }

src/view/player.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export default class Player extends React.Component<Props> {
193193
clock={clock}
194194
workspaceFocusTimeline={session.workspaceFocusTimeline}
195195
toc={head.toc}
196+
sessionTitle={head.title}
196197
/>
197198
)}
198199
<Section className="main-section">

src/view/progress_bar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type Props = {
1616
clock: number;
1717
workspaceFocusTimeline?: t.Focus[];
1818
toc: t.TocItem[];
19+
sessionTitle: string;
1920
};
2021

2122
type UnderMouse = {
@@ -98,7 +99,9 @@ export default function ProgressBar(props: Props) {
9899
showOnAnchorHover
99100
>
100101
<div className="with-clock">
101-
<div className="truncate">{underMouse?.tocItem?.title || 'Unknown section'}</div>
102+
<div className="truncate">
103+
{underMouse?.tocItem?.title || props.toc.length === 0 ? props.sessionTitle : 'Unknown section'}
104+
</div>
102105
<div>{lib.formatTimeSeconds(underMouse?.clock ?? 0)}</div>
103106
</div>
104107
<div className="truncate">{focusUri || 'Unknown file'}</div>

src/view/recorder.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@
546546
gap: var(--spacing-small);
547547
}
548548

549-
& vscode-button {
549+
& > vscode-button {
550550
margin: 0 0 0 auto;
551551
min-width: var(--popover-button-min-width);
552552
}

src/view/recorder_toolbar.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,16 @@ const RecorderToolbar = memo(
107107
await postMessage({ type: 'recorder/makeTest' });
108108
}
109109

110-
async function mergeVideoTracks() {
111-
await postMessage({ type: 'recorder/mergeVideoTracks' });
110+
async function mergeVideoAudioTracks() {
111+
await postMessage({ type: 'recorder/mergeVideoAudioTracks' });
112112
}
113113

114114
async function makeClip() {
115115
await postMessage({ type: 'recorder/makeClip' });
116116
}
117117

118-
async function mergeAndReplaceVideoTracks() {
119-
await postMessage({ type: 'recorder/mergeVideoTracks', deleteOld: true });
118+
async function mergeAndReplaceVideoAudioTracks() {
119+
await postMessage({ type: 'recorder/mergeVideoAudioTracks', deleteOld: true });
120120
}
121121

122122
async function changeSpeed(factor: number, adjustMediaTracks: boolean) {
@@ -232,16 +232,16 @@ const RecorderToolbar = memo(
232232
onClick: makeClip,
233233
},
234234
config.debug && {
235-
title: 'Merge video tracks',
235+
title: 'Merge video/audio tracks',
236236
icon: 'fa-solid fa-link',
237237
disabled: props.playing || props.recording,
238-
onClick: mergeVideoTracks,
238+
onClick: mergeVideoAudioTracks,
239239
},
240240
config.debug && {
241-
title: 'Merge & replace video tracks',
241+
title: 'Merge & replace video/audio tracks',
242242
icon: 'fa-solid fa-link',
243243
disabled: props.playing || props.recording,
244-
onClick: mergeAndReplaceVideoTracks,
244+
onClick: mergeAndReplaceVideoAudioTracks,
245245
},
246246
config.debug && {
247247
title: 'Make test',

0 commit comments

Comments
 (0)