thumbnailUrl

This commit is contained in:
2025-10-01 10:23:49 +02:00
parent d2607b0755
commit 61f95a15fe
5 changed files with 306 additions and 92 deletions
+119
View File
@@ -0,0 +1,119 @@
const { onObjectFinalized } = require("firebase-functions/v2/storage");
const logger = require("firebase-functions/logger");
const admin = require("firebase-admin");
const ffmpeg = require("fluent-ffmpeg");
const ffmpegInstaller = require("@ffmpeg-installer/ffmpeg");
const fs = require("node:fs/promises");
const os = require("node:os");
const path = require("node:path");
const crypto = require("node:crypto");
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
exports.generateVideoThumbnail = onObjectFinalized(
{
region: "europe-west1",
timeoutSeconds: 180,
memory: "1GiB",
cpu: 1,
},
async (event) => {
const file = event.data || {};
const bucketName = file.bucket;
const objectName = file.name || "";
const contentType = file.contentType || "";
if (!bucketName) return;
if (!contentType.startsWith("video/")) return;
if (!objectName) return;
if (/_thumb9x16\.jpg$/i.test(objectName)) return;
const bucket = admin.storage().bucket(bucketName);
const playbackMatch = objectName.match(/^musics\/([^/]+)\/playback\.mp4$/i);
const projectId = playbackMatch ? playbackMatch[1] : null;
// Use unique folder under /tmp to avoid name collisions
const tmpDir = path.join(
os.tmpdir(),
`thumb-${Date.now()}-${Math.round(Math.random() * 1000)}`
);
const baseName = path.basename(objectName);
const dirName = path.posix.dirname(objectName);
const localVideoPath = path.join(tmpDir, baseName);
const thumbBase = baseName.replace(/\.[^.]+$/, "") + "_thumb9x16.jpg";
const localThumbPath = path.join(tmpDir, thumbBase);
const remoteThumbPath =
dirName && dirName !== "."
? path.posix.join(dirName, thumbBase)
: thumbBase;
try {
await fs.mkdir(tmpDir, { recursive: true });
// Télécharger la vidéo depuis le bucket
await bucket.file(objectName).download({ destination: localVideoPath });
// Extraire 1 frame à 1 seconde, forcer 9:16 (1080x1920) par scale+crop centré
await new Promise((resolve, reject) => {
ffmpeg(localVideoPath)
.inputOptions(["-ss 1"])
.frames(1)
.outputOptions([
"-vf",
"scale='if(gt(a,9/16),1080,-2)':'if(gt(a,9/16),-2,1920)',crop=1080:1920",
"-q:v",
"2",
])
.output(localThumbPath)
.on("end", resolve)
.on("error", reject)
.run();
});
// Upload du thumbnail avec un token de téléchargement public Firebase
const downloadToken = crypto.randomUUID();
await bucket.upload(localThumbPath, {
destination: remoteThumbPath,
metadata: {
contentType: "image/jpeg",
cacheControl: "public, max-age=86400",
metadata: {
original: objectName,
aspect: "9:16",
t: "1s",
firebaseStorageDownloadTokens: downloadToken,
},
},
});
const encodedPath = encodeURIComponent(remoteThumbPath);
const thumbnailUrl = `https://firebasestorage.googleapis.com/v0/b/${bucketName}/o/${encodedPath}?alt=media&token=${downloadToken}`;
if (projectId) {
await admin.firestore().collection("projects").doc(projectId).set(
{
thumbnailUrl,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
}
logger.info("✅ [Thumbnail] Uploaded", {
objectName,
remoteThumbPath,
projectId,
thumbnailUrl,
});
} catch (error) {
logger.error("❌ [Thumbnail] Failed", {
objectName,
error: error?.message || String(error),
});
throw error;
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
}
);