fix(asset): show 'Generate proxy' CTA when an asset has a hi-res
master but no browser-playable proxy
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.
This commit is contained in:
parent
1afb150237
commit
7a6296585c
1 changed files with 34 additions and 6 deletions
|
|
@ -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 (
|
||||
<div className="asset-detail fade-in">
|
||||
<div className="asset-detail-header">
|
||||
|
|
@ -273,23 +296,28 @@ function AssetDetail({ asset, onClose }) {
|
|||
<FauxFrame seed={asset.seed || 1} />
|
||||
<div className="scanlines" />
|
||||
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ textAlign: 'center', color: 'var(--text-3)' }}>
|
||||
<div style={{ textAlign: 'center', color: 'var(--text-3)', maxWidth: 360, padding: '0 16px' }}>
|
||||
{streamLoading ? (
|
||||
<div style={{ fontSize: 13 }}>Loading…</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<div style={{ fontSize: 13, marginBottom: 8 }}>{statusMessage}</div>
|
||||
<StatusDot status={asset.status} />
|
||||
{asset.status === 'error' && (
|
||||
{canRetry && (
|
||||
<button
|
||||
className="btn ghost sm"
|
||||
style={{ marginTop: 12, display: 'block', margin: '12px auto 0' }}
|
||||
className="btn primary sm"
|
||||
style={{ marginTop: 14, display: 'inline-flex' }}
|
||||
onClick={retryProcessing}
|
||||
disabled={retrying}
|
||||
>
|
||||
{retrying ? 'Retrying…' : 'Retry processing'}
|
||||
{retrying ? 'Queueing…' : retryLabel}
|
||||
</button>
|
||||
)}
|
||||
{isMissingProxy && (
|
||||
<div style={{ fontSize: 11, color: 'var(--text-4)', marginTop: 10 }}>
|
||||
The hi-res master is in storage. We just need to transcode a browser-playable copy.
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue