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>
165 lines
8.0 KiB
HTML
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>
|