dev - hls

This commit is contained in:
Victor
2025-09-17 13:51:02 +02:00
parent 2d4642891d
commit 12f87f0c92
6 changed files with 443 additions and 139 deletions
+161
View File
@@ -0,0 +1,161 @@
const { onObjectFinalized } = require('firebase-functions/v2/storage');
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const path = require('path');
const os = require('os');
const fs = require('fs');
// These deps must be installed in functions/: ffmpeg-static, fluent-ffmpeg
let ffmpeg;
let ffmpegPath;
try {
ffmpeg = require('fluent-ffmpeg');
ffmpegPath = require('ffmpeg-static');
ffmpeg.setFfmpegPath(ffmpegPath);
} catch (e) {
console.log(
'ffmpeg modules not installed yet — deploy after npm i',
e?.message
);
}
const bucket = admin.storage().bucket();
exports.transcodePlaybackToHLS = onObjectFinalized(
{
region: 'europe-west1',
memory: '2GiB',
timeoutSeconds: 540,
cpu: 2,
},
async (event) => {
const object = event.data;
const filePath = object?.name || ''; // e.g., musics/{projectId}/source.mp4
const contentType = object?.contentType || '';
try {
// Only process our target path + video files
const match = filePath.match(/^musics\/(.+?)\/source\.mp4$/i);
if (!match || !contentType.startsWith('video/')) {
return;
}
const projectId = match[1];
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), `hls_${projectId}_`)
);
const localSource = path.join(tempDir, 'source.mp4');
const hlsDir = path.join(tempDir, 'hls');
fs.mkdirSync(hlsDir);
// Download source
await bucket.file(filePath).download({ destination: localSource });
if (!ffmpeg) {
console.warn('ffmpeg not available; skip transcode');
return;
}
// Transcode to HLS (single rendition for simplicity)
const masterPath = path.join(hlsDir, 'master.m3u8');
const segmentPattern = path.join(hlsDir, 'segment_%03d.ts');
await new Promise((resolve, reject) => {
ffmpeg(localSource)
.outputOptions([
'-profile:v baseline',
'-level 3.0',
'-start_number 0',
'-hls_time 4',
'-hls_list_size 0',
'-f hls',
`-hls_segment_filename ${segmentPattern}`,
])
.output(masterPath)
.on('end', resolve)
.on('error', reject)
.run();
});
// Upload all files in hlsDir to Storage under musics/{projectId}/hls/
const fileNames = fs.readdirSync(hlsDir);
const destPrefix = `musics/${projectId}/hls/`;
// Upload all segments first
const uploads = fileNames
.filter((n) => n.endsWith('.ts'))
.map((name) => {
const local = path.join(hlsDir, name);
const dest = `${destPrefix}${name}`;
return bucket.upload(local, {
destination: dest,
metadata: {
contentType: 'video/MP2T',
cacheControl: 'public, max-age=3600',
},
});
});
await Promise.all(uploads);
// Generate signed URLs for segments and rewrite playlist with absolute URLs
const segmentNames = fileNames.filter((n) => n.endsWith('.ts'));
const signedSegments = {};
await Promise.all(
segmentNames.map(async (name) => {
const file = bucket.file(`${destPrefix}${name}`);
const [url] = await file.getSignedUrl({
action: 'read',
expires: Date.now() + 1000 * 60 * 60 * 24 * 365,
});
signedSegments[name] = url;
})
);
// Rewrite master playlist to use absolute signed segment URLs
let masterContent = fs.readFileSync(masterPath, 'utf8');
masterContent = masterContent
.split(/\r?\n/)
.map((line) =>
line.endsWith('.ts') && signedSegments[line]
? signedSegments[line]
: line
)
.join('\n');
const rewrittenMasterLocal = path.join(hlsDir, 'master.abs.m3u8');
fs.writeFileSync(rewrittenMasterLocal, masterContent, 'utf8');
// Upload rewritten playlist
await bucket.upload(rewrittenMasterLocal, {
destination: `${destPrefix}master.m3u8`,
metadata: {
contentType: 'application/vnd.apple.mpegurl',
cacheControl: 'public, max-age=3600',
},
});
// Compute signed URL for playlist
const [signedUrl] = await bucket
.file(`${destPrefix}master.m3u8`)
.getSignedUrl({
action: 'read',
expires: Date.now() + 1000 * 60 * 60 * 24 * 365,
});
// Update Firestore project doc with playbackUrl
await admin.firestore().collection('projects').doc(projectId).set(
{
playbackUrl: signedUrl,
playbackStatus: 'ready',
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
// Cleanup tmp dir
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (e) {}
} catch (e) {
console.error('HLS transcode error', e);
}
}
);