diff --git a/app/static/public/js/video.js b/app/static/public/js/video.js
index 2f434e7..729be95 100644
--- a/app/static/public/js/video.js
+++ b/app/static/public/js/video.js
@@ -1954,7 +1954,14 @@
const prefix = ffTaskPrefix('concat');
const segASource = `${prefix}_a_source.mp4`;
const segBSource = `${prefix}_b_source.mp4`;
+ const segAVideo = `${prefix}_a_video.mp4`;
+ const segBVideo = `${prefix}_b_video.mp4`;
+ const segAAudio = `${prefix}_a_audio.m4a`;
+ const segBAudio = `${prefix}_b_audio.m4a`;
+ const listVideo = `${prefix}_video_list.txt`;
+ const listAudio = `${prefix}_audio_list.txt`;
const mergedVideoFile = `${prefix}_merged_video.mp4`;
+ const mergedAudioFile = `${prefix}_merged_audio.m4a`;
const mergedFile = `${prefix}_merged.mp4`;
try {
const trimSeconds = (Math.max(0, lockedTimestampMs) / 1000).toFixed(3);
@@ -1963,7 +1970,46 @@
}
await ffmpegWriteFile(ff, segASource, sourceStable);
await ffmpegWriteFile(ff, segBSource, generatedBuffer);
+
try {
+ await ffmpegExec(
+ ff,
+ [
+ '-y',
+ '-i', segASource,
+ '-t', trimSeconds,
+ '-map', '0:v:0',
+ '-c', 'copy',
+ segAVideo
+ ]
+ );
+ await ffmpegExec(
+ ff,
+ [
+ '-y',
+ '-i', segBSource,
+ '-map', '0:v:0',
+ '-c', 'copy',
+ segBVideo
+ ]
+ );
+ await ffmpegWriteFile(
+ ff,
+ listVideo,
+ new TextEncoder().encode(`file '${segAVideo}'\nfile '${segBVideo}'\n`)
+ );
+ await ffmpegExec(
+ ff,
+ [
+ '-y',
+ '-f', 'concat',
+ '-safe', '0',
+ '-i', listVideo,
+ '-c', 'copy',
+ mergedVideoFile
+ ]
+ );
+ } catch (videoCopyErr) {
await ffmpegExec(
ff,
[
@@ -1981,52 +2027,88 @@
mergedVideoFile
]
);
- } catch (videoFallbackErr) {
- await ffmpegExec(
- ff,
- [
- '-y',
- '-i', segASource,
- '-i', segBSource,
- '-filter_complex',
- `[0:v]trim=end=${trimSeconds},setpts=PTS-STARTPTS[v0];[1:v]setpts=PTS-STARTPTS[v1];[v0][v1]concat=n=2:v=1:a=0[v]`,
- '-map', '[v]',
- '-c:v', 'libx264',
- '-preset', 'ultrafast',
- '-pix_fmt', 'yuv420p',
- '-r', '30',
- mergedVideoFile
- ]
- );
}
+
+ let mergedWithAudio = false;
try {
await ffmpegExec(
ff,
[
'-y',
- '-i', mergedVideoFile,
- '-t', trimSeconds,
'-i', segASource,
- '-map', '0:v',
- '-map', '1:a?',
- '-c:v', 'copy',
- '-c:a', 'aac',
- '-ar', '48000',
- '-ac', '2',
+ '-t', trimSeconds,
+ '-map', '0:a:0',
+ '-c', 'copy',
+ segAAudio
+ ]
+ );
+ await ffmpegExec(
+ ff,
+ [
+ '-y',
+ '-i', segBSource,
+ '-map', '0:a:0',
+ '-c', 'copy',
+ segBAudio
+ ]
+ );
+ await ffmpegWriteFile(
+ ff,
+ listAudio,
+ new TextEncoder().encode(`file '${segAAudio}'\nfile '${segBAudio}'\n`)
+ );
+ await ffmpegExec(
+ ff,
+ [
+ '-y',
+ '-f', 'concat',
+ '-safe', '0',
+ '-i', listAudio,
+ '-c', 'copy',
+ mergedAudioFile
+ ]
+ );
+ await ffmpegExec(
+ ff,
+ [
+ '-y',
+ '-i', mergedVideoFile,
+ '-i', mergedAudioFile,
+ '-map', '0:v:0',
+ '-map', '1:a:0',
+ '-c', 'copy',
'-shortest',
mergedFile
]
);
+ mergedWithAudio = true;
} catch (audioMuxErr) {
- const mergedOnly = await ffmpegReadFile(ff, mergedVideoFile);
- return new Blob([toStableUint8(mergedOnly)], { type: 'video/mp4' });
+ mergedWithAudio = false;
+ }
+ if (!mergedWithAudio) {
+ await ffmpegExec(
+ ff,
+ [
+ '-y',
+ '-i', mergedVideoFile,
+ '-c', 'copy',
+ mergedFile
+ ]
+ );
}
const merged = await ffmpegReadFile(ff, mergedFile);
return new Blob([toStableUint8(merged)], { type: 'video/mp4' });
} finally {
await ffmpegDeleteFileSafe(ff, segASource);
await ffmpegDeleteFileSafe(ff, segBSource);
+ await ffmpegDeleteFileSafe(ff, segAVideo);
+ await ffmpegDeleteFileSafe(ff, segBVideo);
+ await ffmpegDeleteFileSafe(ff, segAAudio);
+ await ffmpegDeleteFileSafe(ff, segBAudio);
+ await ffmpegDeleteFileSafe(ff, listVideo);
+ await ffmpegDeleteFileSafe(ff, listAudio);
await ffmpegDeleteFileSafe(ff, mergedVideoFile);
+ await ffmpegDeleteFileSafe(ff, mergedAudioFile);
await ffmpegDeleteFileSafe(ff, mergedFile);
}
};
diff --git a/app/static/public/pages/video.html b/app/static/public/pages/video.html
index 51f108a..73e5257 100644
--- a/app/static/public/pages/video.html
+++ b/app/static/public/pages/video.html
@@ -318,7 +318,7 @@
-
+