music-video-gen/flow-state/index.html
Dejvino e5484c9c38 Export: fall back to VP9/AV1 when H.264 unavailable and warn about software encoding
Firefox on Linux exposes VideoEncoder but reports every avc1.* config as
unsupported (OpenH264 is decode-only), so pickVideoConfig returned null and
export threw "no supported H.264 configuration found". Probe VP9/AV1 families
after H.264 and pick the first profile that isConfigSupported and does not
reorder (B-frame) — same 12-frame ordering test. Map the chosen family to
mp4-muxer's video codec (avc/vp9/av1).

Before any rendering, probe the codec for the chosen preset and show a
modal when we fell off H.264 onto a software VP9/AV1 path — that's what the
user felt as "super slow". The modal names the codec, explains render vs
encode cost, suggests Chromium/Chrome for hardware H.264 (or 720p/lower
preset), and offers Continue anyway / Cancel (Esc/backdrop also cancel) plus
"Don't warn again this session". Injected pick is threaded through
Exporter.export({ videoPick }) and exportSegment so we don't probe twice.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 18:17:53 +02:00

165 lines
8.0 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>flow-state</title>
<link rel="stylesheet" href="/src/ui/style.css">
</head>
<body>
<div id="app">
<div id="stage">
<canvas id="canvas"></canvas>
<div id="overlay">
<div id="dropzone">
<div class="dz-inner">
<div class="dz-title">flow-state</div>
<div class="dz-sub">drop an audio file, or click to choose</div>
<div class="dz-hint">mp3 · flac · wav · ogg</div>
</div>
</div>
</div>
<div id="hud" hidden></div>
<div id="toast" hidden></div>
<div id="export-warn" hidden>
<div class="ew-backdrop"></div>
<div class="ew-box" role="dialog" aria-modal="true" aria-labelledby="ew-title">
<div id="ew-title" class="ew-title">Software encoding — export will be slow</div>
<div id="ew-body" class="ew-body"></div>
<div class="ew-actions">
<button id="ew-cancel">Cancel</button>
<button id="ew-continue" class="primary">Continue anyway</button>
</div>
<label class="ew-opt"><input type="checkbox" id="ew-dontask"> Don't warn again this session</label>
</div>
</div>
</div>
<div id="transport">
<div id="timeline">
<canvas id="timeline-canvas"></canvas>
</div>
<div id="controls">
<button id="btn-play" title="Play / pause (space)"></button>
<button id="btn-prev-section" title="Previous section (←)"></button>
<button id="btn-next-section" title="Next section (→)"></button>
<button id="btn-loop" title="Loop current section (L)"></button>
<span id="time-display">0:00 / 0:00</span>
<!-- The section readout is the row's only flexible item: it
absorbs all the free space and centres its own text, which
is what the two spacers used to do. They competed with it
for that space and squeezed it to nothing. -->
<span id="section-display"></span>
<label class="ctl">quality
<select id="sel-quality">
<option value="draft">draft</option>
<option value="full" selected>full</option>
</select>
</label>
<button id="btn-reroll" title="New seed for the whole track">reroll</button>
<button id="btn-reroll-section" title="New scene for this section only">reroll section</button>
<button id="btn-lock" title="Lock this section against rerolls">lock</button>
<button id="btn-hud" title="Toggle debug HUD (D)">hud</button>
<button id="btn-osd" title="Toggle the title plate in the corner (O)">osd</button>
<button id="btn-segment" title="Render 20s around the playhead at export quality">test render</button>
<button id="btn-export" class="primary" title="Export the full video">export</button>
</div>
</div>
<aside id="panel">
<div id="track-header">
<div class="th-top-row">
<div class="th-info">
<span id="th-label" class="th-label">no track loaded</span>
<span id="th-name" class="th-name" hidden>no track</span>
</div>
<button id="btn-change-track" class="th-btn" title="Choose or drop an audio file">choose track</button>
</div>
<div id="th-progress" class="th-progress" hidden>
<div id="th-step" class="th-step">decoding…</div>
<div class="an-bar"><div id="th-fill" class="an-fill"></div></div>
</div>
</div>
<div id="panel-tabs">
<button data-tab="look" class="active">look</button>
<button data-tab="scene">scene</button>
<button data-tab="post">post</button>
<button data-tab="export">export</button>
</div>
<div id="panel-body"></div>
</aside>
</div>
<input type="file" id="file-input" accept="audio/*" hidden>
<audio id="audio" hidden></audio>
<div id="boot-error" hidden>
<div class="be-inner">
<div class="be-title">the app did not start</div>
<div class="be-body">
<p>Every control is inert because <code>main.js</code> never ran — the
markup you are looking at is the static page. Nothing is wrong with
your audio file.</p>
<pre id="be-detail"></pre>
<p class="be-hint">A module that fails to load takes the whole graph
with it, and the usual cause is a fetch that hit the dev server while
it was restarting. A hard reload normally fixes it; if it does not,
restart <code>npm run dev</code>.</p>
</div>
</div>
</div>
<script>
// BOOT GUARD. Loaded before the module and deliberately not a module
// itself, so it survives whatever kills the module graph.
//
// A module that fails to load — a 404, a MIME error, a syntax error, a
// dev server caught mid-restart — aborts main.js silently. The page
// still renders, every button is still there, and not one of them is
// wired to anything. That failure cost a real debugging session: the UI
// looked correct and simply did nothing, which is the most expensive
// way for a front end to break.
(function () {
var shown = false;
function show(detail) {
if (shown) return;
shown = true;
var box = document.getElementById('boot-error');
var pre = document.getElementById('be-detail');
if (pre) pre.textContent = detail || 'no detail available';
if (box) box.hidden = false;
}
// A module that never executes never sets this.
window.addEventListener('error', function (e) {
// Module load failures arrive as an error event on the script
// element rather than as a window error with a message.
if (e && e.target && e.target.tagName === 'SCRIPT') {
show('failed to load: ' + (e.target.src || 'unknown module') +
'\n\nOpen the console — the first "Loading failed for the ' +
'module" line names the file that actually broke.');
} else if (e && e.message) {
show(e.message + (e.filename ? '\n at ' + e.filename + ':' + e.lineno : ''));
}
}, true);
window.addEventListener('unhandledrejection', function (e) {
var r = e && e.reason;
show('unhandled rejection during startup:\n' + ((r && r.stack) || r));
});
// The backstop for the case no error event fires at all: if the app
// has not announced itself in a few seconds, it is not going to.
setTimeout(function () {
if (!window.__FLOW_STATE_READY__) {
show('main.js did not finish starting within 8 seconds and no ' +
'error was reported.\n\nCheck the console for the first red ' +
'line — a failed module import is the usual cause.');
}
}, 8000);
}());
</script>
<script type="module" src="/src/main.js"></script>
</body>
</html>