Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
File size: 5,363 Bytes
2cae2a9 4cb7ad9 2cae2a9 4cb7ad9 2cae2a9 4cb7ad9 2cae2a9 4cb7ad9 2cae2a9 4cb7ad9 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 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
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";
export type SupportedExportFormat = "mp4" | "webm"
export const defaultExportFormat = "mp4"
export type ConcatenateVideoWithAudioOptions = {
output?: string;
format?: SupportedExportFormat;
audioTrack?: string; // base64
audioFilePath?: string; // path
videoTracks?: string[]; // base64
videoFilePaths?: string[]; // path
videoTracksVolume?: number; // Represents the volume level of the original video track
audioTrackVolume?: number; // Represents the volume level of the additional audio track
asBase64?: boolean;
};
export const concatenateVideosWithAudio = async ({
output,
format = defaultExportFormat,
audioTrack = "",
audioFilePath = "",
videoTracks = [],
videoFilePaths = [],
videoTracksVolume = 0.5, // (1.0 = 100% volume)
audioTrackVolume = 0.5,
asBase64 = false,
}: ConcatenateVideoWithAudioOptions): Promise<string> => {
try {
// Prepare temporary directories
const tempDir = path.join(os.tmpdir(), uuidv4());
await fs.mkdir(tempDir);
if (audioTrack) {
audioFilePath = path.join(tempDir, `audio.wav`);
await writeBase64ToFile(addBase64Header(audioTrack, "wav"), audioFilePath);
}
// Decode and concatenate base64 video tracks to temporary file
let 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))
// console.log("concatenating videos (without audio)..")
const tempFilePath = await concatenateVideos({
videoFilePaths,
})
// Check if the concatenated video has audio or not
const tempMediaInfo = await getMediaInfo(tempFilePath.filepath);
const hasOriginalAudio = tempMediaInfo.hasAudio;
const finalOutputFilePath = output || path.join(tempDir, `${uuidv4()}.${format}`);
// Begin ffmpeg command configuration
let cmd = ffmpeg();
// Add silent concatenated video
cmd = cmd.addInput(tempFilePath.filepath);
// If additional audio is provided, add audio to ffmpeg command
if (audioFilePath) {
cmd = cmd.addInput(audioFilePath);
// If the input video already has audio, we will mix it with additional audio
if (hasOriginalAudio) {
const filterComplex = `
[0:a]volume=${videoTracksVolume}[a0];
[1:a]volume=${audioTrackVolume}[a1];
[a0][a1]amix=inputs=2:duration=shortest[a]
`.trim();
cmd = cmd.outputOptions([
'-filter_complex', filterComplex,
'-map', '0:v',
'-map', '[a]',
'-c:v', 'copy',
'-c:a', 'aac',
]);
} else {
// If the input video has no audio, just use the additional audio as is
cmd = cmd.outputOptions([
'-map', '0:v',
'-map', '1:a',
'-c:v', 'copy',
'-c:a', 'aac',
]);
}
} else {
// If no additional audio is provided, simply copy the video stream
cmd = cmd.outputOptions([
'-c:v', 'copy',
hasOriginalAudio ? '-c:a' : '-an', // If original audio exists, copy it; otherwise, indicate no audio
]);
}
/*
console.log("DEBUG:", {
videoTracksVolume,
audioTrackVolume,
videoFilePaths,
tempFilePath,
hasOriginalAudio,
// originalAudioVolume,
audioFilePath,
// additionalAudioVolume,
finalOutputFilePath
})
*/
// Set up event handlers for ffmpeg processing
const promise = new Promise<string>((resolve, reject) => {
cmd.on('error', (err) => {
console.error(" Error during ffmpeg processing:", err.message);
reject(err);
}).on('end', async () => {
// When ffmpeg finishes processing, resolve the promise with file info
try {
if (asBase64) {
try {
const outputBuffer = await fs.readFile(finalOutputFilePath);
const outputBase64 = addBase64Header(outputBuffer.toString("base64"), format)
resolve(outputBase64);
} catch (error) {
reject(new Error(`Error reading output video file: ${(error as Error).message}`));
}
} else {
resolve(finalOutputFilePath)
}
} catch (err) {
reject(err);
}
}).save(finalOutputFilePath); // Provide the path where to save the file
});
// Wait for ffmpeg to complete the process
const result = await promise;
return result;
} catch (error) {
throw new Error(`Failed to assemble video: ${(error as Error).message}`);
} finally {
await removeTemporaryFiles([...videoFilePaths].concat(audioFilePath))
}
}; |