fix(mam-api): /stream returns MP4 url + separate hls_url (fixes Premiere import)

The HLS-VOD work made GET /assets/:id/stream return the HLS playlist URL as
`url` whenever hls_s3_key was set. The Premiere plugin's "Import Proxy"
downloads `url` to a file and imports it — so it was saving an .m3u8 playlist
as .mp4, and Premiere rejected it ("unsupported compression type"). This hit
every YouTube asset (all get HLS generated), regardless of codec.

/stream now returns the directly-downloadable MP4 proxy as `url` (type mp4)
and the HLS playlist as a separate `hls_url`. The web player prefers `hls_url`
(so in-browser HLS playback is unchanged), while the already-installed plugin
gets a real MP4 again — no plugin reinstall needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-05-29 21:44:52 -04:00
parent b449ef0ce3
commit 9d6bbf8112
2 changed files with 18 additions and 4 deletions

View file

@ -595,10 +595,19 @@ router.get('/:id/stream', async (req, res, next) => {
if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' }); if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
const a = r.rows[0]; const a = r.rows[0];
if (a.status === 'live') return res.json({ url: `/live/${a.id}/index.m3u8`, type: 'hls', live: true }); if (a.status === 'live') return res.json({ url: `/live/${a.id}/index.m3u8`, type: 'hls', live: true });
// Prefer the HLS rendition for recorded assets — whole-file segment GETs // `url` is the directly-downloadable MP4 proxy; `hls_url` is the HLS
// avoid the RustFS ranged-GET stitching the MP4 /video path has to do. // rendition for in-browser playback (whole-file segment GETs avoid the
// RustFS ranged-GET stitching the MP4 path needs). The Premiere plugin
// downloads `url` to a file and imports it, so `url` must NOT be the
// .m3u8 playlist — Premiere can't import a playlist ("unsupported
// compression type"). The web player prefers `hls_url` when present.
if (a.hls_s3_key) { if (a.hls_s3_key) {
return res.json({ url: `/api/v1/assets/${id}/hls/playlist.m3u8`, type: 'hls', source: 'proxy' }); return res.json({
url: `/api/v1/assets/${id}/video`,
type: 'mp4',
source: a.proxy_s3_key ? 'proxy' : 'original',
hls_url: `/api/v1/assets/${id}/hls/playlist.m3u8`,
});
} }
const VIDEO_EXTS = ['.mp4', '.mov', '.mxf', '.ts', '.m4v', '.mkv', '.avi', '.webm']; const VIDEO_EXTS = ['.mp4', '.mov', '.mxf', '.ts', '.m4v', '.mkv', '.avi', '.webm'];
const key = a.proxy_s3_key || const key = a.proxy_s3_key ||

View file

@ -65,7 +65,12 @@ function AssetDetail({ asset, onClose }) {
setStreamLoading(true); setStreamLoading(true);
window.ZAMPP_API.fetch('/assets/' + assetId + '/stream') window.ZAMPP_API.fetch('/assets/' + assetId + '/stream')
.then(function(r) { .then(function(r) {
if (r && r.url) { if (r && r.hls_url) {
// Prefer HLS for in-browser playback; `url` stays the MP4 proxy
// (used by the Premiere plugin importer + as a fallback).
setStreamUrl(r.hls_url);
setStreamType('hls');
} else if (r && r.url) {
setStreamUrl(r.url); setStreamUrl(r.url);
setStreamType(r.type || 'mp4'); setStreamType(r.type || 'mp4');
} else if (r) { } else if (r) {