Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
File size: 4,572 Bytes
2cae2a9 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
import { existsSync, promises as fs } from "node:fs"
import os from "node:os"
import path from "node:path"
import { v4 as uuidv4 } from "uuid";
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg";
import { concatenateVideos } from "./concatenateVideos.mts";
import { writeBase64ToFile } from "../files/writeBase64ToFile.mts";
import { getMediaInfo } from "./getMediaInfo.mts";
import { removeTemporaryFiles } from "../files/removeTmpFiles.mts";
import { addBase64Header } from "../base64/addBase64.mts";
type ConcatenateVideoAndMergeAudioOptions = {
output?: string;
audioTracks?: string[]; // base64
audioFilePaths?: string[]; // path
videoTracks?: string[]; // base64
videoFilePaths?: string[]; // path
};
export type ConcatenateVideoAndMergeAudioOutput = {
filepath: string;
durationInSec: number;
}
// note: the audio tracks will be fused together, as in "mixed"
// this return a path to the file
export const concatenateVideosAndMergeAudio = async ({
output,
audioTracks = [],
audioFilePaths = [],
videoTracks = [],
videoFilePaths = []
}: ConcatenateVideoAndMergeAudioOptions): Promise<ConcatenateVideoAndMergeAudioOutput> => {
try {
// Prepare temporary directories
const tempDir = path.join(os.tmpdir(), uuidv4());
await fs.mkdir(tempDir);
let i = 0
for (const track of audioTracks) {
if (!track) { continue }
const audioFilePath = path.join(tempDir, `audio${++i}.wav`);
await writeBase64ToFile(addBase64Header(track, "wav"), audioFilePath);
audioFilePaths.push(audioFilePath);
}
audioFilePaths = audioFilePaths.filter((audio) => existsSync(audio))
// Decode and concatenate base64 video tracks to temporary file
i = 0
for (const track of videoTracks) {
if (!track) { continue }
const videoFilePath = path.join(tempDir, `video${++i}.mp4`);
await writeBase64ToFile(addBase64Header(track, "mp4"), videoFilePath);
videoFilePaths.push(videoFilePath);
}
videoFilePaths = videoFilePaths.filter((video) => existsSync(video))
// The final output file path
const finalOutputFilePath = output ? output : path.join(tempDir, `${uuidv4()}.mp4`);
/*
console.log("DEBUG:", {
tempDir,
audioFilePath,
audioTrack: audioTrack.slice(0, 40),
videoTracks: videoTracks.map(vid => vid.slice(0, 40)),
videoFilePaths,
finalOutputFilePath
})
*/
// console.log("concatenating videos (without audio)..")
const tempFilePath = await concatenateVideos({
videoFilePaths,
})
// console.log("concatenated silent shots to: ", tempFilePath)
// console.log("concatenating video + audio..")
// Add audio to the concatenated video file
const promise = new Promise<ConcatenateVideoAndMergeAudioOutput>((resolve, reject) => {
let cmd = ffmpeg().addInput(tempFilePath.filepath).outputOptions("-c:v copy");
for (const audioFilePath of audioFilePaths) {
cmd = cmd.addInput(audioFilePath);
}
if (audioFilePaths.length) {
// Mix all audio tracks (if there are any) into a single stereo stream
const mixFilter = audioFilePaths.map((_, index) => `[${index + 1}:a]`).join('') + `amix=inputs=${audioFilePaths.length}:duration=first[outa]`;
cmd = cmd
.complexFilter(mixFilter)
.outputOptions([
"-map", "0:v:0", // Maps the video stream from the first input (index 0) as the output video stream
"-map", "[outa]", // Maps the labeled audio output from the complex filter (mixed audio) as the output audio stream
"-c:a aac", // Specifies the audio codec to be AAC (Advanced Audio Coding)
"-shortest" // Ensures the output file's duration equals the shortest input stream's duration
]);
} else {
// If there are no audio tracks, just map the video
cmd = cmd.outputOptions(["-map", "0:v:0"]);
}
cmd = cmd
.on("error", reject)
.on('end', async () => {
try {
const { durationInSec } = await getMediaInfo(finalOutputFilePath);
resolve({ filepath: finalOutputFilePath, durationInSec });
} catch (err) {
reject(err);
}
})
.saveToFile(finalOutputFilePath);
});
const result = await promise;
return result
} catch (error) {
throw new Error(`Failed to assemble video: ${(error as Error).message}`);
} finally {
await removeTemporaryFiles([...videoFilePaths, ...audioFilePaths])
}
}; |