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:
parent
a03dd36f11
commit
37247fdfea
2 changed files with 16 additions and 18 deletions
|
|
@ -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); }
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue