fix(playout): silent-audio staging crash, home tiles, channel delete
- playout-stage: skip loudnorm pass 2 when measured_I=-inf (silent or no-audio clip); fall back to plain AAC transcode so staging completes instead of erroring out - screens-home: add Playout tile; replace Premiere panel tile with Downloads tile opening a combined modal (Premiere panel releases + Dragon-ISO link to forge.wilddragon.net/WildDragonLLC/dragon-iso) - screens-playout: add Delete channel button (visible only when stopped); removes channel from list and selects next on confirm Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
12115a053a
commit
f7cf56ae0d
4 changed files with 101 additions and 23 deletions
|
|
@ -18,7 +18,7 @@
|
|||
// Anything that would just say "all clear" is hidden, not rendered.
|
||||
|
||||
function Home({ navigate }) {
|
||||
const [showPremiereDownload, setShowPremiereDownload] = React.useState(false);
|
||||
const [showDownloads, setShowDownloads] = React.useState(false);
|
||||
|
||||
// Pull live counts so the tile subtitles ("34 assets", "0 live", "3 running")
|
||||
// reflect what's actually in the DB right now, not a stale boot-time cache.
|
||||
|
|
@ -64,12 +64,20 @@ function Home({ navigate }) {
|
|||
desc: 'SDI · SRT · RTMP ingest. Start, stop, schedule.',
|
||||
},
|
||||
{
|
||||
id: '__premiere',
|
||||
label: 'Premiere panel',
|
||||
icon: 'editor',
|
||||
id: 'playout',
|
||||
label: 'Playout',
|
||||
icon: 'monitor',
|
||||
tone: 'live',
|
||||
sub: 'Master Control',
|
||||
desc: 'Play assets to SDI, NDI, SRT or RTMP via CasparCG.',
|
||||
},
|
||||
{
|
||||
id: '__downloads',
|
||||
label: 'Downloads',
|
||||
icon: 'download',
|
||||
tone: 'purple',
|
||||
sub: 'v' + ((window.PREMIERE_LATEST || {}).version || '·'),
|
||||
desc: 'Download the Adobe Premiere Pro panel for frame-accurate editing.',
|
||||
sub: 'Premiere panel · Dragon-ISO',
|
||||
desc: 'Download the Premiere Pro panel and Dragon-ISO NDI tools.',
|
||||
},
|
||||
{
|
||||
id: 'jobs',
|
||||
|
|
@ -127,7 +135,7 @@ function Home({ navigate }) {
|
|||
<button
|
||||
key={t.id}
|
||||
className={'launcher-tile tone-' + t.tone}
|
||||
onClick={() => t.id === '__premiere' ? setShowPremiereDownload(true) : navigate(t.id)}
|
||||
onClick={() => t.id === '__downloads' ? setShowDownloads(true) : navigate(t.id)}
|
||||
>
|
||||
<span className="launcher-tile-icon">
|
||||
<Icon name={t.icon} size={26} />
|
||||
|
|
@ -241,39 +249,46 @@ function Home({ navigate }) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showPremiereDownload && <PremiereDownloadModal onClose={() => setShowPremiereDownload(false)} />}
|
||||
{showDownloads && <DownloadsModal onClose={() => setShowDownloads(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Modal listing all Premiere panel downloads (ZXP + Windows installer for
|
||||
// each released version). Sourced from window.PREMIERE_RELEASES, written by
|
||||
// the Settings → SDKs section in screens-admin.jsx.
|
||||
function PremiereDownloadModal({ onClose }) {
|
||||
// Combined downloads modal: Premiere Pro panel + Dragon-ISO.
|
||||
function DownloadsModal({ onClose }) {
|
||||
const releases = (window.PREMIERE_RELEASES || []).slice().sort((a, b) => {
|
||||
// Newest first; fall back to lexicographic compare on version string.
|
||||
const av = String(a.version || ''), bv = String(b.version || '');
|
||||
return bv.localeCompare(av, undefined, { numeric: true });
|
||||
});
|
||||
const latest = window.PREMIERE_LATEST || releases[0] || null;
|
||||
|
||||
const DRAGON_ISO_RELEASES_URL = 'https://forge.wilddragon.net/WildDragonLLC/dragon-iso/releases';
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 560 }}>
|
||||
<div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 580 }}>
|
||||
<div className="modal-head">
|
||||
<div>
|
||||
<div style={{ fontSize: 15, fontWeight: 600 }}>Premiere panel</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 600 }}>Downloads</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 2 }}>
|
||||
Adobe Premiere Pro (UXP) integration. Install the .ccx per workstation via the Adobe UXP Developer Tool, or double-click it with Creative Cloud installed.
|
||||
Premiere Pro panel and Dragon-ISO NDI tools.
|
||||
</div>
|
||||
</div>
|
||||
<button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
{/* ── Premiere panel ── */}
|
||||
<div className="downloads-section-head">
|
||||
<Icon name="editor" size={13} />
|
||||
<span>Premiere Pro panel (UXP)</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 10 }}>
|
||||
Install the .ccx per workstation via the Adobe UXP Developer Tool, or double-click it with Creative Cloud installed.
|
||||
</div>
|
||||
{releases.length === 0 && (
|
||||
<div style={{ padding: '24px 0', textAlign: 'center', color: 'var(--text-3)', fontSize: 12 }}>
|
||||
No releases registered yet. Upload one from Settings → Capture SDKs.
|
||||
<div style={{ padding: '12px 0', color: 'var(--text-3)', fontSize: 12 }}>
|
||||
No releases registered. Upload one from Settings → Capture SDKs.
|
||||
</div>
|
||||
)}
|
||||
{releases.map((rel, i) => (
|
||||
|
|
@ -293,23 +308,39 @@ function PremiereDownloadModal({ onClose }) {
|
|||
<div className="premiere-release-actions">
|
||||
{rel.ccx && (
|
||||
<a href={rel.ccx} download className="btn primary sm">
|
||||
<Icon name="download" />UXP plugin (.ccx)
|
||||
<Icon name="download" size={12} />UXP plugin (.ccx)
|
||||
</a>
|
||||
)}
|
||||
{rel.installer && (
|
||||
<a href={rel.installer} download className="btn ghost sm">
|
||||
<Icon name="download" />Windows installer
|
||||
<Icon name="download" size={12} />Windows installer
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* ── Dragon-ISO ── */}
|
||||
<div className="downloads-section-head" style={{ marginTop: 20 }}>
|
||||
<Icon name="film" size={13} />
|
||||
<span>Dragon-ISO</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 10 }}>
|
||||
NDI ISO recorder for Microsoft Teams. Windows, .NET 8 WPF.
|
||||
</div>
|
||||
<div className="premiere-release">
|
||||
<div className="premiere-release-head">
|
||||
<span className="premiere-release-version mono">Releases</span>
|
||||
</div>
|
||||
<div className="premiere-release-actions">
|
||||
<a href={DRAGON_ISO_RELEASES_URL} target="_blank" rel="noopener noreferrer" className="btn primary sm">
|
||||
<Icon name="download" size={12} />View releases on Forgejo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-foot">
|
||||
<div style={{ flex: 1, fontSize: 11.5, color: 'var(--text-3)' }}>
|
||||
Need help installing? Use the Adobe Extension Manager or UPIA.
|
||||
</div>
|
||||
<button className="btn ghost" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -456,6 +456,13 @@ function ChannelDetail({ channel, onChannelChange }) {
|
|||
const updated = await poFetch('/channels/' + channel.id + '/stop', { method: 'POST' });
|
||||
setCh(updated); onChannelChange(updated);
|
||||
};
|
||||
const deleteChannel = async () => {
|
||||
if (!window.confirm('Delete channel "' + ch.name + '"? This cannot be undone.')) return;
|
||||
try {
|
||||
await poFetch('/channels/' + ch.id, { method: 'DELETE' });
|
||||
onChannelChange({ ...ch, _deleted: true });
|
||||
} catch (e) { alert(e.message); }
|
||||
};
|
||||
|
||||
// engine.currentIndex maps directly to the sorted item position.
|
||||
const activeIndex = (engine && engine.currentIndex >= 0) ? engine.currentIndex : -1;
|
||||
|
|
@ -471,6 +478,9 @@ function ChannelDetail({ channel, onChannelChange }) {
|
|||
{ch.status === 'running'
|
||||
? <button className="btn danger" onClick={stopChannel}>Stop channel</button>
|
||||
: <button className="btn primary" onClick={startChannel}>Start channel</button>}
|
||||
{ch.status !== 'running' && (
|
||||
<button className="btn ghost danger sm" onClick={deleteChannel} title="Delete this channel">Delete</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{ch.error_message && <div className="alert error">{ch.error_message}</div>}
|
||||
|
|
@ -514,6 +524,14 @@ function Playout() {
|
|||
|
||||
const selected = (channels || []).find(c => c.id === selectedId) || null;
|
||||
const onChannelChange = (updated) => {
|
||||
if (updated._deleted) {
|
||||
setChannels(cs => {
|
||||
const next = (cs || []).filter(c => c.id !== updated.id);
|
||||
setSelectedId(next.length ? next[0].id : null);
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
setChannels(cs => (cs || []).map(c => c.id === updated.id ? updated : c));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -156,6 +156,15 @@
|
|||
}
|
||||
.po-pl-total { color: var(--text-2); }
|
||||
|
||||
/* Downloads modal section header */
|
||||
.downloads-section-head {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: 0.06em; color: var(--text-3);
|
||||
padding-bottom: 8px; border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Small button variants reused */
|
||||
.btn.xs { padding: 2px 8px; font-size: 11px; }
|
||||
.btn.sm { padding: 5px 10px; font-size: 12px; }
|
||||
|
|
|
|||
|
|
@ -57,10 +57,30 @@ async function measureLoudness(inputPath) {
|
|||
return JSON.parse(match[0]);
|
||||
}
|
||||
|
||||
function isFiniteLoudness(val) {
|
||||
const n = parseFloat(val);
|
||||
return isFinite(n);
|
||||
}
|
||||
|
||||
async function applyLoudnorm(inputPath, outputPath, m) {
|
||||
// Pass 2: linear normalization using pass 1's measurements. -c:v copy keeps
|
||||
// the video stream intact so we only re-encode audio (target AAC stereo, the
|
||||
// common-denominator CasparCG ffmpeg producer accepts).
|
||||
//
|
||||
// Silent / no-audio clips measure I=-inf which ffmpeg rejects in pass 2.
|
||||
// When any loudnorm measurement is non-finite, fall back to a plain audio
|
||||
// transcode (AAC 192k) with no loudness adjustment — the clip has no
|
||||
// meaningful audio to normalize.
|
||||
const silentOrNoAudio = !isFiniteLoudness(m.input_i) || !isFiniteLoudness(m.input_tp);
|
||||
if (silentOrNoAudio) {
|
||||
console.log(`[playout-stage] loudnorm skip — silent/no audio (I=${m.input_i}), transcoding audio only`);
|
||||
await runFfmpeg([
|
||||
'-hide_banner', '-nostats', '-y', '-i', inputPath,
|
||||
'-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-ar', '48000',
|
||||
outputPath,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
await runFfmpeg([
|
||||
'-hide_banner', '-nostats', '-y', '-i', inputPath,
|
||||
'-af', `loudnorm=I=-23:TP=-1:LRA=11:measured_I=${m.input_i}:measured_TP=${m.input_tp}:measured_LRA=${m.input_lra}:measured_thresh=${m.input_thresh}:offset=${m.target_offset}:linear=true:print_format=summary`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue