From 7a6296585cbf4bf3c2566b7a8f6d39296d88217d Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Sat, 23 May 2026 10:30:42 -0400 Subject: [PATCH] fix(asset): show 'Generate proxy' CTA when an asset has a hi-res master but no browser-playable proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the player's "Retry processing" button only appeared for assets in status='error'. Old recorder captures (e.g. the archived 'sRT Test_…' clips from May) live as status='archived' or 'ready' with original_s3_key set but proxy_s3_key null. The /stream endpoint correctly returned {url: null, reason: 'no_proxy'} for those, but the player just showed "Preview not yet available" with no path forward — which reads as "ingest worked, won't play, no idea why." Two changes: 1) Capture {reason, has_source} from /stream so the UI can tell why playback isn't available. 2) Render a "Generate proxy" button (using the existing POST /assets/:id/retry endpoint, which the backend now accepts for any asset with original_s3_key but no proxy_s3_key) whenever the stream lookup returned no_proxy and the source exists. Original error-status retry path is preserved. Closes the visible half of #1 — the user can now self-recover proxy- less clips from the library without DB surgery. --- services/web-ui/public/screens-asset.jsx | 40 ++++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/services/web-ui/public/screens-asset.jsx b/services/web-ui/public/screens-asset.jsx index 52b20c2..f6f2cae 100644 --- a/services/web-ui/public/screens-asset.jsx +++ b/services/web-ui/public/screens-asset.jsx @@ -27,6 +27,10 @@ function AssetDetail({ asset, onClose }) { // Stream / video state const [streamUrl, setStreamUrl] = React.useState(null); const [streamType, setStreamType] = React.useState(null); + // Why the stream is unavailable: 'no_proxy' (has hi-res source, browser + // can't play it directly) or null (still loading / live / playable). + const [streamReason, setStreamReason] = React.useState(null); + const [streamHasSource, setStreamHasSource] = React.useState(false); const [streamLoading, setStreamLoading] = React.useState(false); const [videoDuration, setVideoDuration] = React.useState(0); const [retrying, setRetrying] = React.useState(false); @@ -40,6 +44,8 @@ function AssetDetail({ asset, onClose }) { if (!assetId) return; setStreamUrl(null); setStreamType(null); + setStreamReason(null); + setStreamHasSource(false); setVideoDuration(0); setCurrentMs(0); setPlaying(false); @@ -49,6 +55,10 @@ function AssetDetail({ asset, onClose }) { if (r && r.url) { setStreamUrl(r.url); setStreamType(r.type || 'mp4'); + } else if (r) { + // {url: null, reason: 'no_proxy', has_source: true|false} + setStreamReason(r.reason || null); + setStreamHasSource(!!r.has_source); } }) .catch(function() {}) @@ -141,7 +151,9 @@ function AssetDetail({ asset, onClose }) { if (retrying) return; setRetrying(true); window.ZAMPP_API.fetch('/assets/' + assetId + '/retry', { method: 'POST' }) - .then(function() { window.alert('Re-queued for processing. Refresh in a moment.'); }) + .then(function() { + window.alert('Re-queued for processing. The proxy worker will pick it up shortly; refresh in a minute to see the player.'); + }) .catch(function(e) { window.alert('Retry failed: ' + (e.message || 'unknown error')); }) .finally(function() { setRetrying(false); }); }; @@ -216,12 +228,23 @@ function AssetDetail({ asset, onClose }) { const visibleComments = comments.filter(function(c) { return showResolved || !c.resolved; }); + // The player overlay text reflects three states: live processing, + // explicit error, "ingested but never proxied" (the SRT-archive case), + // or generic not-ready. + const isMissingProxy = !streamUrl && streamReason === 'no_proxy' && streamHasSource; const statusMessage = asset.status === 'processing' ? 'Processing…' : asset.status === 'live' ? 'Live recording in progress' : asset.status === 'error' ? 'Processing failed' : + isMissingProxy ? 'This clip was ingested but never got a browser-playable proxy.' : 'Preview not yet available'; + // "Retry processing" (for status=error) and "Generate proxy" (for + // status=ready/archived but missing proxy) both call the same /retry + // endpoint. Label depends on what the user is staring at. + const canRetry = (asset.status === 'error') || isMissingProxy; + const retryLabel = isMissingProxy ? 'Generate proxy' : 'Retry processing'; + return (
@@ -273,23 +296,28 @@ function AssetDetail({ asset, onClose }) {
-
+
{streamLoading ? (
Loading…
) : (
{statusMessage}
- {asset.status === 'error' && ( + {canRetry && ( )} + {isMissingProxy && ( +
+ The hi-res master is in storage. We just need to transcode a browser-playable copy. +
+ )}
)}