add flow steps

This commit is contained in:
2025-10-13 14:15:19 +02:00
parent 707c30817c
commit 63629017d7
5 changed files with 203 additions and 10 deletions
+192 -1
View File
@@ -9,6 +9,8 @@ const os = require("node:os");
const path = require("node:path");
const crypto = require("node:crypto");
const { Buffer } = require("node:buffer");
const { spawn } = require("node:child_process");
const { URL } = require("node:url");
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
@@ -88,6 +90,115 @@ async function convertWebmToMp4(inputPath, outputPath) {
});
}
async function hasAudioStream(inputPath) {
return new Promise((resolve) => {
const ffmpegProcess = spawn(
ffmpegInstaller.path,
["-hide_banner", "-i", inputPath],
{
windowsHide: true,
}
);
let stderr = "";
ffmpegProcess.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
ffmpegProcess.on("error", () => {
resolve(false);
});
ffmpegProcess.on("close", () => {
const hasAudio = /Stream #\d+:\d+.*Audio:/i.test(stderr);
resolve(hasAudio);
});
});
}
async function muxAudioIntoVideo(videoPath, audioPath, outputPath) {
await new Promise((resolve, reject) => {
ffmpeg()
.input(videoPath)
.input(audioPath)
.outputOptions([
"-map",
"0:v:0",
"-map",
"1:a:0",
"-c:v",
"copy",
"-c:a",
"aac",
"-b:a",
"192k",
"-movflags",
"+faststart",
"-shortest",
])
.on("end", resolve)
.on("error", reject)
.save(outputPath);
});
}
async function downloadUriToFile({ uri, tmpDir, bucket, filenameBase }) {
if (!uri) {
throw new Error("URI audio vide");
}
const trimmed = String(uri || "").trim();
if (!trimmed) {
throw new Error("URI audio vide");
}
const uriWithoutParams = trimmed.split("#")[0] || "";
const uriPath = uriWithoutParams.split("?")[0] || "";
const extFromUri = path.extname(uriPath);
let destination = path.join(tmpDir, `${filenameBase}${extFromUri || ""}`);
const ensureExtension = (contentType) => {
if (extFromUri) return destination;
const guessed = guessExtension(contentType);
destination = `${destination}${guessed}`;
return destination;
};
if (/^gs:\/\//i.test(trimmed)) {
const parsed = new URL(trimmed);
const bucketName = parsed.host;
const objectPath = parsed.pathname.replace(/^\/+/, "");
const targetBucket =
bucketName && bucketName !== bucket.name
? admin.storage().bucket(bucketName)
: bucket;
let contentType = "";
try {
const [meta] = await targetBucket.file(objectPath).getMetadata();
contentType = meta?.contentType || "";
} catch (error) {
logger.warn("[uploadProjectMedia] Impossible de récupérer les métadonnées audio", {
uri: trimmed,
error: error?.message || String(error),
});
}
ensureExtension(contentType);
await targetBucket.file(objectPath).download({ destination });
return { filePath: destination, contentType };
}
const downloaded = await downloadFromHttp(trimmed);
ensureExtension(downloaded.contentType);
await fs.writeFile(destination, downloaded.buffer);
return {
filePath: destination,
contentType: downloaded.contentType || "",
};
}
async function ensureProjectOwnership(uid, projectId) {
const snap = await admin
.firestore()
@@ -184,7 +295,8 @@ exports.uploadProjectMedia = onCall(
metadataKeys: metadata ? Object.keys(metadata) : [],
});
const { snap: projectSnap } = await ensureProjectOwnership(uid, projectId);
const { snap: projectSnap, data: projectData } =
await ensureProjectOwnership(uid, projectId);
const bucket = admin.storage().bucket();
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "upload-"));
@@ -289,6 +401,85 @@ exports.uploadProjectMedia = onCall(
});
}
const audioPresent = await hasAudioStream(finalPath);
logger.info("[uploadProjectMedia] Audio stream inspection", {
projectId,
audioPresent,
});
if (!audioPresent) {
const audioCandidates = [
{ field: "playbackAudioUrl", uri: projectData?.playbackAudioUrl },
{ field: "songUrl", uri: projectData?.songUrl },
{ field: "recordingUrl", uri: projectData?.recordingUrl },
].filter(({ uri }) => typeof uri === "string" && uri.trim() !== "");
if (audioCandidates.length === 0) {
throw new HttpsError(
"failed-precondition",
"Aucune source audio disponible pour ce projet"
);
}
let injected = false;
let lastError = null;
for (const candidate of audioCandidates) {
try {
logger.info("[uploadProjectMedia] Tentative d'injection audio", {
projectId,
field: candidate.field,
});
const { filePath: audioPath } = await downloadUriToFile({
uri: candidate.uri,
tmpDir,
bucket,
filenameBase: `audio-${candidate.field}`,
});
const muxedPath = path.join(
tmpDir,
`muxed-${crypto.randomUUID()}.mp4`
);
await muxAudioIntoVideo(finalPath, audioPath, muxedPath);
const hasAudioAfterMux = await hasAudioStream(muxedPath);
if (!hasAudioAfterMux) {
throw new Error(
"Audio absent après tentative d'injection dans la vidéo"
);
}
finalPath = muxedPath;
finalContentType = "video/mp4";
injected = true;
logger.info("[uploadProjectMedia] Audio injecté dans la vidéo", {
projectId,
field: candidate.field,
});
break;
} catch (error) {
lastError = error;
logger.warn("[uploadProjectMedia] Échec d'injection audio", {
projectId,
field: candidate.field,
error: error?.message || String(error),
});
}
}
if (!injected) {
throw new HttpsError(
"internal",
lastError?.message ||
"Échec de l'injection audio dans la vidéo uploadée"
);
}
}
const downloadToken = crypto.randomUUID();
const sanitizedMetadata = sanitizeMetadata(metadata);
// If we are writing the canonical playback file, make sure contentType is correct