mirror of
https://github.com/storytold/filmcraft.git
synced 2026-11-12 00:42:23 -06:00
- eframe web runner on WebGPU (WebGL2 fallback; GPU compositor on WebGPU, CPU on WebGL2). - fs: virtual file table of File/Blob handles, 1 MiB chunked async range reads with an LRU cache and read-ahead (pending + retry instead of blocking), downloads for written files, /opfs/ paths written to OPFS. - import: File System Access picker / <input type=file>, drag-and-drop anywhere, container index prefetch (moov wherever it is), retried imports; project open/save. - recovery: OPFS auto-save snapshot while dirty, media copies, reopened on next visit. - audio: AudioWorklet output fed ahead from the UI thread; played-frame audio clock. - webcodecs: H.264/HEVC/VP9/AV1 MP4/MOV decoded by the browser VideoDecoder via a reader opener (fallback to our decoders on any error), probed with isConfigSupported. - api: window.filmcraft (execute/request/commands/screenshot/importFiles/importUrl/ openProject/files/readFile/info). - cargo xtask web [--dev] [--serve PORT] (wasm-bindgen CLI); xtask wasm also checks filmcraft-ui-egui and filmcraft-web; layer L6 for the web app. - Original web icon (favicon.svg) with attribution sidecar; docs/web.md; headless-Chrome smoke test (tests/smoke.mjs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60 lines
1.8 KiB
JavaScript
60 lines
1.8 KiB
JavaScript
// FilmCraft web audio output (AudioWorklet). The UI thread mixes audio ahead and posts
|
|
// interleaved stereo Float32Array blocks; this processor plays them in order and reports how many
|
|
// frames it actually played (with its currentTime), which drives the playback clock.
|
|
class FilmcraftOutput extends AudioWorkletProcessor {
|
|
constructor() {
|
|
super();
|
|
this.queue = [];
|
|
this.offset = 0;
|
|
this.gen = 0;
|
|
this.played = 0;
|
|
this.quanta = 0;
|
|
this.port.onmessage = (e) => {
|
|
const m = e.data;
|
|
if (m.type === "reset") {
|
|
this.gen = m.gen;
|
|
this.queue = [];
|
|
this.offset = 0;
|
|
this.played = 0;
|
|
this.port.postMessage({ gen: this.gen, played: 0, time: currentTime });
|
|
} else if (m.type === "data" && m.gen === this.gen) {
|
|
this.queue.push(m.samples);
|
|
}
|
|
};
|
|
}
|
|
|
|
process(_inputs, outputs) {
|
|
const out = outputs[0];
|
|
const left = out[0];
|
|
const right = out[1] || out[0];
|
|
const n = left.length;
|
|
let i = 0;
|
|
while (i < n && this.queue.length) {
|
|
const b = this.queue[0];
|
|
const k = Math.min(b.length / 2 - this.offset, n - i);
|
|
for (let j = 0; j < k; j++) {
|
|
left[i + j] = b[2 * (this.offset + j)];
|
|
right[i + j] = b[2 * (this.offset + j) + 1];
|
|
}
|
|
i += k;
|
|
this.offset += k;
|
|
if (this.offset * 2 >= b.length) {
|
|
this.queue.shift();
|
|
this.offset = 0;
|
|
}
|
|
}
|
|
// only real frames advance the clock; a starved queue plays silence and holds it
|
|
this.played += i;
|
|
for (; i < n; i++) {
|
|
left[i] = 0;
|
|
right[i] = 0;
|
|
}
|
|
if (++this.quanta % 4 === 0) {
|
|
this.port.postMessage({ gen: this.gen, played: this.played, time: currentTime });
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
registerProcessor("filmcraft-output", FilmcraftOutput);
|