From 37247fdfeac89a57ee25c232ec5aebcd3c57d0d5 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Tue, 26 May 2026 16:57:37 +0000 Subject: [PATCH] fix(video): direct S3 signed URL for streaming + proxy bitrate 1.5Mbps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /assets/:id/stream now returns a signed S3 URL directly (4h TTL) instead of pointing to the /video pipe endpoint. Browser streams directly from S3 — no Node.js bottleneck, S3 handles range requests natively for smooth seeking. - GET /assets/:id/video now redirects (302) to a signed S3 URL. Belt-and-suspenders: any code still calling /video gets redirected. - proxy.js: default bitrate changed from 10Mbps to 1.5Mbps, audio default from 192kbps to 128kbps. DB settings already updated to 1.5Mbps. Cuts proxy file size ~6x for the same quality content. Existing proxies need re-generation at new bitrate. --- services/mam-api/src/routes/assets.js | 30 +++++++++++++-------------- services/worker/src/workers/proxy.js | 4 ++-- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/services/mam-api/src/routes/assets.js b/services/mam-api/src/routes/assets.js index 848ce0e..1f75042 100644 --- a/services/mam-api/src/routes/assets.js +++ b/services/mam-api/src/routes/assets.js @@ -522,13 +522,15 @@ router.get('/:id/stream', async (req, res, next) => { if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' }); const a = r.rows[0]; if (a.status === 'live') return res.json({ url: `/live/${a.id}/index.m3u8`, type: 'hls', live: true }); - if (a.proxy_s3_key) return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' }); - // Fall back to original for any video file so uploaded/YouTube clips - // show a filmstrip even before the proxy worker finishes (#58) - const orig = a.original_s3_key; const VIDEO_EXTS = ['.mp4', '.mov', '.mxf', '.ts', '.m4v', '.mkv', '.avi', '.webm']; - if (orig && VIDEO_EXTS.some(ext => orig.toLowerCase().endsWith(ext))) { - return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4', source: 'original' }); + // Return signed S3 URL directly — browser talks to S3, no Node proxy bottleneck. + // Sign for 4 hours so the player doesn't expire mid-session. + const key = a.proxy_s3_key || + (a.original_s3_key && VIDEO_EXTS.some(ext => a.original_s3_key.toLowerCase().endsWith(ext)) + ? a.original_s3_key : null); + if (key) { + const signedUrl = await getSignedUrlForObject(key, 14400); + return res.json({ url: signedUrl, type: 'mp4', source: a.proxy_s3_key ? 'proxy' : 'original' }); } return res.json({ url: null, type: null, reason: 'no_proxy', has_source: !!a.original_s3_key }); } catch (err) { next(err); } @@ -560,6 +562,9 @@ router.get('/:id/live-path', async (req, res, next) => { }); // GET /:id/video +// Redirects to a signed S3 URL so the browser streams directly from S3. +// This eliminates the Node.js proxy bottleneck and lets S3 handle range +// requests natively — critical for smooth seeking in the player. router.get('/:id/video', async (req, res, next) => { try { const { id } = req.params; @@ -570,16 +575,9 @@ router.get('/:id/video', async (req, res, next) => { const origIsVideo = a.original_s3_key && VIDEO_EXTS.some(ext => a.original_s3_key.toLowerCase().endsWith(ext)); const key = a.proxy_s3_key || (origIsVideo ? a.original_s3_key : null); if (!key) return res.status(404).json({ error: 'No browser-playable source' }); - const params = { Bucket: getS3Bucket(), Key: key }; - const rangeHeader = req.headers.range; - if (rangeHeader) params.Range = rangeHeader; - const s3Res = await s3Client.send(new GetObjectCommand(params)); - const status = rangeHeader ? 206 : 200; - const headers = { 'Content-Type': 'video/mp4', 'Accept-Ranges': 'bytes', 'Cache-Control': 'no-store' }; - if (s3Res.ContentLength) headers['Content-Length'] = String(s3Res.ContentLength); - if (s3Res.ContentRange) headers['Content-Range'] = s3Res.ContentRange; - res.writeHead(status, headers); - s3Res.Body.pipe(res); + // Sign for 4 hours — enough for an editing session without frequent re-fetches + const url = await getSignedUrlForObject(key, 14400); + res.redirect(302, url); } catch (err) { next(err); } }); diff --git a/services/worker/src/workers/proxy.js b/services/worker/src/workers/proxy.js index 3d4c6ac..1cd7270 100644 --- a/services/worker/src/workers/proxy.js +++ b/services/worker/src/workers/proxy.js @@ -21,10 +21,10 @@ async function loadProxyEncodingSettings() { const gpuEnabled = map.gpu_transcode_enabled === 'true'; const codec = map.gpu_codec || (gpuEnabled ? 'h264_nvenc' : 'libx264'); const preset = map.gpu_preset || (gpuEnabled ? 'p4' : 'fast'); - const bitrateM = parseInt(map.gpu_bitrate_mbps || '10', 10); + const bitrateM = parseFloat(map.gpu_bitrate_mbps || '1.5'); const rcMode = map.gpu_rc_mode || null; const audioCodec = map.gpu_audio_codec || 'aac'; - const audioKbps = parseInt(map.gpu_audio_bitrate_kbps || '192', 10); + const audioKbps = parseInt(map.gpu_audio_bitrate_kbps || '128', 10); return { videoCodec: codec,