/** * One run of the ffmpeg program (ffmpeg.mjs) with an input file and an * output that streams out into Blob pieces. Used by worker.mjs in the * browser, and by the site's tests in Node (where the input is bytes rather * than a File, since WORKERFS needs a browser worker). * * Written for johnmcm.com (2026); part of the module published at /source/ffmpeg/. */ /** Collects what ffmpeg writes, in order, with later overwrites of earlier bytes kept aside. */ export class Sink { constructor(chunk = 8 << 20) { this.parts = []; this.flushed = 0; this.buf = new Uint8Array(chunk); this.len = 0; this.size = 0; this.patches = []; } write(data, pos) { if (pos > this.size) this.append(new Uint8Array(pos - this.size)); if (pos === this.size) { this.append(data); return; } // Rewriting bytes already written: in the buffer still, change them there; otherwise remember the change. const end = pos + data.length; const overlap = Math.min(end, this.size) - pos; if (pos >= this.flushed) { this.buf.set(data.subarray(0, overlap), pos - this.flushed); } else { this.patches.push({ pos, data: data.slice(0, overlap) }); } if (end > this.size) this.append(data.subarray(overlap)); } append(data) { let at = 0; while (at < data.length) { const n = Math.min(data.length - at, this.buf.length - this.len); this.buf.set(data.subarray(at, at + n), this.len); this.len += n; at += n; this.size += n; if (this.len === this.buf.length) this.flush(); } } flush() { if (!this.len) return; this.parts.push(new Blob([this.buf.slice(0, this.len)])); this.flushed += this.len; this.len = 0; } blob(type) { this.flush(); let blob = new Blob(this.parts, { type }); for (const p of this.patches) { blob = new Blob([blob.slice(0, p.pos), p.data, blob.slice(p.pos + p.data.length)], { type }); } return blob; } } /** * Runs ffmpeg once in a fresh instance `ff` (from createFFmpeg). The input is * at /in/: a File mounted with WORKERFS (read in place), or bytes. The * output, if named, is /out/. Resolves { code, blob, size }. */ export function run(ff, { args, name, file, bytes, output, type }) { const FS = ff.FS; FS.mkdir('/in'); if (file) FS.mount(FS.filesystems.WORKERFS, { files: [file] }, '/in'); else FS.writeFile(`/in/${name}`, bytes); FS.mkdir('/out'); const sink = new Sink(); if (output) { const dev = FS.makedev(64, 0); FS.registerDevice(dev, { open() {}, close() {}, read() { return 0; }, write(stream, buffer, offset, length, position) { sink.write(buffer.subarray(offset, offset + length), typeof position === 'number' ? position : stream.position); return length; }, llseek(stream, offset, whence) { const pos = whence === 0 ? offset : whence === 1 ? stream.position + offset : sink.size + offset; if (pos < 0) throw new FS.ErrnoError(28); return pos; }, }); FS.mkdev(`/out/${output}`, dev); } let code; try { code = ff.callMain(args); } catch (err) { // exit() inside ffmpeg ends the run by throwing; the exit status says how it went. if (typeof err?.status === 'number') code = err.status; else throw err; } return { code: code ?? 0, blob: output ? sink.blob(type || '') : null, size: sink.size }; }