Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
File size: 7,151 Bytes
3165afb 2cae2a9 3165afb 2cae2a9 4cb7ad9 2cae2a9 4cb7ad9 2cae2a9 4cb7ad9 2cae2a9 3165afb 2cae2a9 ccd3eba d66ad20 e9ae5da 3165afb 2cae2a9 ccd3eba d66ad20 3165afb 2cae2a9 e9ae5da ccd3eba 3165afb 2cae2a9 3165afb 2cae2a9 e9ae5da 2cae2a9 e9ae5da 2cae2a9 e9ae5da ccd3eba 4cb7ad9 2cae2a9 e9ae5da 1083ad0 2cae2a9 ccd3eba 2cae2a9 873dc9a ccd3eba 2cae2a9 ccd3eba 2cae2a9 1628a6d e9ae5da 1628a6d ccd3eba 2cae2a9 e9ae5da 1628a6d 2cae2a9 ccd3eba 2cae2a9 e9ae5da 1628a6d 2cae2a9 ccd3eba 2cae2a9 e9ae5da 1628a6d 2cae2a9 ccd3eba 2cae2a9 73a3511 2cae2a9 1083ad0 d5d9687 1083ad0 2cae2a9 d5d9687 1083ad0 2cae2a9 873dc9a 1628a6d 2cae2a9 3165afb 4cb7ad9 2cae2a9 e9ae5da 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 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
import { existsSync } from "node:fs"
import { readFile } from "node:fs/promises"
import path from "node:path"
import { v4 as uuidv4 } from "uuid"
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
import { addBase64Header, extractBase64 } from "@aitube/encoders"
import { getRandomDirectory, removeTemporaryFiles, writeBase64ToFile } from "@aitube/io"
import { getMediaInfo } from "../analyze/getMediaInfo"
import { concatenateVideos } from "./concatenateVideos"
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 = await getRandomDirectory()
if (audioTrack && audioTrack.length > 0) {
const analysis = extractBase64(audioTrack)
// console.log(`concatenateVideosWithAudio: writing down an audio file (${analysis.extension}) from the supplied base64 track`)
audioFilePath = path.join(tempDir, `audio.${analysis.extension}`)
await writeBase64ToFile(addBase64Header(audioTrack, analysis.extension), audioFilePath)
}
// Decode and concatenate base64 video tracks to temporary file
let i = 0
for (const track of videoTracks) {
if (!track) { continue }
// note: here we assume the input video is in mp4
const analysis = extractBase64(audioTrack)
const videoFilePath = path.join(tempDir, `video${++i}.${analysis.extension}`)
// console.log(`concatenateVideosWithAudio: writing down a video file (${analysis.extension}) from the supplied base64 track`)
await writeBase64ToFile(addBase64Header(track, analysis.extension), videoFilePath)
videoFilePaths.push(videoFilePath)
}
videoFilePaths = videoFilePaths.filter((video) => existsSync(video))
// console.log("concatenateVideosWithAudio: concatenating videos (without audio)..")
const tempFilePath = await concatenateVideos({
videoFilePaths,
})
// console.log(`concatenateVideosWithAudio: tempFilePath = ${JSON.stringify(tempFilePath, null, 2)}`)
// Check if the concatenated video has audio or not
const tempMediaInfo = await getMediaInfo(tempFilePath.filepath);
const hasOriginalAudio = tempMediaInfo.hasAudio;
// console.log(`concatenateVideosWithAudio: hasOriginalAudio = ${hasOriginalAudio}`)
const finalOutputFilePath = output || path.join(tempDir, `${uuidv4()}.${format}`);
// console.log(`concatenateVideosWithAudio: finalOutputFilePath = ${finalOutputFilePath}`)
// Begin ffmpeg command configuration
let ffmpegCommand = ffmpeg();
ffmpegCommand = ffmpegCommand.addInput(tempFilePath.filepath);
ffmpegCommand = ffmpegCommand.outputOptions('-loglevel', 'debug');
// If additional audio is provided, add audio to ffmpeg command
if (typeof audioFilePath === "string" && audioFilePath.length > 0) {
// console.log(`concatenateVideosWithAudio: adding an audio file path: ${audioFilePath}`)
ffmpegCommand = ffmpegCommand.addInput(audioFilePath);
// If the input video already has audio, we will mix it with additional audio
if (hasOriginalAudio) {
// console.log(`concatenateVideosWithAudio: case 1: additional audio was provided, and we already have audio: we mix`)
const filterComplex = `
[0:a]volume=${videoTracksVolume}[a0];
[1:a]volume=${audioTrackVolume}[a1];
[a0][a1]amix=inputs=2:duration=shortest[a]
`.trim();
ffmpegCommand = ffmpegCommand.outputOptions([
'-filter_complex', filterComplex,
'-map', '0:v',
'-map', '[a]',
'-c:v', 'copy',
'-c:a', 'aac',
]);
} else {
// console.log(`concatenateVideosWithAudio: case 2: additional audio was provided, but we don't already have audio: we overwrite`)
// If the input video has no audio, just use the additional audio as is
ffmpegCommand = ffmpegCommand.outputOptions([
'-map', '0:v',
'-map', '1:a',
'-c:v', 'copy',
'-c:a', 'aac',
]);
}
} else {
// console.log(`concatenateVideosWithAudio: case 3: no additional audio provided, we leave the audio as-is`)
// If no additional audio is provided, simply copy the video stream
ffmpegCommand = ffmpegCommand.outputOptions([
'-c:v', 'copy',
hasOriginalAudio ? '-c:a copy' : '-an', // If original audio exists, copy it; otherwise, indicate no audio
]);
}
/*
console.log("concatenateVideosWithAudio: DEBUG:", {
videoTracksVolume,
audioTrackVolume,
videoFilePaths,
tempFilePath,
hasOriginalAudio,
// originalAudioVolume,
audioFilePath,
// additionalAudioVolume,
finalOutputFilePath
})
*/
// Set up event handlers for ffmpeg processing
const promise = new Promise<string>((resolve, reject) => {
ffmpegCommand.on('start', function(commandLine) {
console.log('concatenateVideosWithAudio: Spawned Ffmpeg with command: ' + commandLine);
}).on('error', (err) => {
console.error("concatenateVideosWithAudio: error during ffmpeg processing");
console.error(err)
reject(err);
}).on('end', async () => {
// When ffmpeg finishes processing, resolve the promise with file info
try {
if (asBase64) {
try {
const outputBuffer = await 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 {
// console.log(`concatenateVideosWithAudio: deleting ${JSON.stringify([...videoFilePaths].concat(audioFilePath), null, 2)}`)
await removeTemporaryFiles([...videoFilePaths].concat(audioFilePath))
}
}; |