add flow steps
This commit is contained in:
+192
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user