fix(video): direct S3 signed URL for streaming + proxy bitrate 1.5Mbps

- 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.
This commit is contained in:
Zac Gaetano 2026-05-26 16:57:37 +00:00
parent a03dd36f11
commit 37247fdfea
2 changed files with 16 additions and 18 deletions

View file

@ -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); }
});

View file

@ -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,