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
|
// Stream / video state
|
||||||
const [streamUrl, setStreamUrl] = React.useState(null);
|
const [streamUrl, setStreamUrl] = React.useState(null);
|
||||||
const [streamType, setStreamType] = 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 [streamLoading, setStreamLoading] = React.useState(false);
|
||||||
const [videoDuration, setVideoDuration] = React.useState(0);
|
const [videoDuration, setVideoDuration] = React.useState(0);
|
||||||
const [retrying, setRetrying] = React.useState(false);
|
const [retrying, setRetrying] = React.useState(false);
|
||||||
|
|
@ -40,6 +44,8 @@ function AssetDetail({ asset, onClose }) {
|
||||||
if (!assetId) return;
|
if (!assetId) return;
|
||||||
setStreamUrl(null);
|
setStreamUrl(null);
|
||||||
setStreamType(null);
|
setStreamType(null);
|
||||||
|
setStreamReason(null);
|
||||||
|
setStreamHasSource(false);
|
||||||
setVideoDuration(0);
|
setVideoDuration(0);
|
||||||
setCurrentMs(0);
|
setCurrentMs(0);
|
||||||
setPlaying(false);
|
setPlaying(false);
|
||||||
|
|
@ -49,6 +55,10 @@ function AssetDetail({ asset, onClose }) {
|
||||||
if (r && r.url) {
|
if (r && r.url) {
|
||||||
setStreamUrl(r.url);
|
setStreamUrl(r.url);
|
||||||
setStreamType(r.type || 'mp4');
|
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() {})
|
.catch(function() {})
|
||||||
|
|
@ -141,7 +151,9 @@ function AssetDetail({ asset, onClose }) {
|
||||||
if (retrying) return;
|
if (retrying) return;
|
||||||
setRetrying(true);
|
setRetrying(true);
|
||||||
window.ZAMPP_API.fetch('/assets/' + assetId + '/retry', { method: 'POST' })
|
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')); })
|
.catch(function(e) { window.alert('Retry failed: ' + (e.message || 'unknown error')); })
|
||||||
.finally(function() { setRetrying(false); });
|
.finally(function() { setRetrying(false); });
|
||||||
};
|
};
|
||||||
|
|
@ -216,12 +228,23 @@ function AssetDetail({ asset, onClose }) {
|
||||||
|
|
||||||
const visibleComments = comments.filter(function(c) { return showResolved || !c.resolved; });
|
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 =
|
const statusMessage =
|
||||||
asset.status === 'processing' ? 'Processing…' :
|
asset.status === 'processing' ? 'Processing…' :
|
||||||
asset.status === 'live' ? 'Live recording in progress' :
|
asset.status === 'live' ? 'Live recording in progress' :
|
||||||
asset.status === 'error' ? 'Processing failed' :
|
asset.status === 'error' ? 'Processing failed' :
|
||||||
|
isMissingProxy ? 'This clip was ingested but never got a browser-playable proxy.' :
|
||||||
'Preview not yet available';
|
'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 (
|
return (
|
||||||
<div className="asset-detail fade-in">
|
<div className="asset-detail fade-in">
|
||||||
<div className="asset-detail-header">
|
<div className="asset-detail-header">
|
||||||
|
|
@ -273,23 +296,28 @@ function AssetDetail({ asset, onClose }) {
|
||||||
<FauxFrame seed={asset.seed || 1} />
|
<FauxFrame seed={asset.seed || 1} />
|
||||||
<div className="scanlines" />
|
<div className="scanlines" />
|
||||||
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<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 ? (
|
{streamLoading ? (
|
||||||
<div style={{ fontSize: 13 }}>Loading…</div>
|
<div style={{ fontSize: 13 }}>Loading…</div>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div style={{ fontSize: 13, marginBottom: 8 }}>{statusMessage}</div>
|
<div style={{ fontSize: 13, marginBottom: 8 }}>{statusMessage}</div>
|
||||||
<StatusDot status={asset.status} />
|
<StatusDot status={asset.status} />
|
||||||
{asset.status === 'error' && (
|
{canRetry && (
|
||||||
<button
|
<button
|
||||||
className="btn ghost sm"
|
className="btn primary sm"
|
||||||
style={{ marginTop: 12, display: 'block', margin: '12px auto 0' }}
|
style={{ marginTop: 14, display: 'inline-flex' }}
|
||||||
onClick={retryProcessing}
|
onClick={retryProcessing}
|
||||||
disabled={retrying}
|
disabled={retrying}
|
||||||
>
|
>
|
||||||
{retrying ? 'Retrying…' : 'Retry processing'}
|
{retrying ? 'Queueing…' : retryLabel}
|
||||||
</button>
|
</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>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue