Files
Leonxlnx e8c151f089 Replace MediaRecorder video export with WebCodecs + custom WebM muxer
MediaRecorder/captureStream kept crashing the Chrome renderer
(STATUS_BREAKPOINT). Video is now encoded offline with VideoEncoder
(VP9 with VP8 fallback) and muxed by a dependency-free EBML/Matroska
writer. Adds precise export controls: video fps (24/30/60), exact
length (1-8 loops or 5-60s), and a GIF loop-forever/play-once toggle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 00:59:06 +02:00

128 lines
4.4 KiB
JavaScript

/* Minimal dependency-free WebM (Matroska) muxer for WebCodecs output.
Takes encoded VP8/VP9 chunks and produces a playable .webm file. */
var WebMMux = (function () {
function concat(arrays) {
var total = 0;
for (var i = 0; i < arrays.length; i++) total += arrays[i].length;
var out = new Uint8Array(total);
var o = 0;
for (var j = 0; j < arrays.length; j++) { out.set(arrays[j], o); o += arrays[j].length; }
return out;
}
/* EBML element IDs are written verbatim (big-endian) */
function idBytes(id) {
var bytes = [];
while (id > 0) { bytes.unshift(id & 0xff); id = Math.floor(id / 256); }
return new Uint8Array(bytes);
}
/* EBML data-size vint */
function sizeBytes(n) {
if (n < 0x7F) return new Uint8Array([0x80 | n]);
if (n < 0x3FFF) return new Uint8Array([0x40 | (n >> 8), n & 0xff]);
if (n < 0x1FFFFF) return new Uint8Array([0x20 | (n >> 16), (n >> 8) & 0xff, n & 0xff]);
if (n < 0x0FFFFFFF) return new Uint8Array([0x10 | (n >> 24), (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]);
/* up to ~1TB, plenty for any export */
var hi = Math.floor(n / 0x100000000);
return new Uint8Array([0x08 | hi, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
}
function el(id, payload) {
return concat([idBytes(id), sizeBytes(payload.length), payload]);
}
function uintPayload(n) {
var bytes = [];
do { bytes.unshift(n & 0xff); n = Math.floor(n / 256); } while (n > 0);
return new Uint8Array(bytes);
}
function strPayload(s) {
var out = new Uint8Array(s.length);
for (var i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0x7f;
return out;
}
function float8Payload(d) {
var buf = new ArrayBuffer(8);
new DataView(buf).setFloat64(0, d, false);
return new Uint8Array(buf);
}
/* frames: [{ data: Uint8Array, timestampMs: number, key: boolean }] */
function mux(opts) {
var codecId = opts.codecId; // "V_VP8" | "V_VP9"
var w = opts.width, h = opts.height;
var durationMs = opts.durationMs;
var frames = opts.frames;
var header = el(0x1A45DFA3, concat([
el(0x4286, uintPayload(1)), // EBMLVersion
el(0x42F7, uintPayload(1)), // EBMLReadVersion
el(0x42F2, uintPayload(4)), // EBMLMaxIDLength
el(0x42F3, uintPayload(8)), // EBMLMaxSizeLength
el(0x4282, strPayload("webm")), // DocType
el(0x4287, uintPayload(2)), // DocTypeVersion
el(0x4285, uintPayload(2)) // DocTypeReadVersion
]));
var info = el(0x1549A966, concat([
el(0x2AD7B1, uintPayload(1000000)), // TimecodeScale: 1ms
el(0x4489, float8Payload(durationMs)), // Duration
el(0x4D80, strPayload("lumen")), // MuxingApp
el(0x5741, strPayload("lumen")) // WritingApp
]));
var tracks = el(0x1654AE6B, el(0xAE, concat([
el(0xD7, uintPayload(1)), // TrackNumber
el(0x73C5, uintPayload(1)), // TrackUID
el(0x83, uintPayload(1)), // TrackType: video
el(0x9C, uintPayload(0)), // FlagLacing
el(0x86, strPayload(codecId)), // CodecID
el(0xE0, concat([ // Video
el(0xB0, uintPayload(w)), // PixelWidth
el(0xBA, uintPayload(h)) // PixelHeight
]))
])));
/* clusters: start a new one every second (relative timecode is int16 ms) */
var clusters = [];
var blocks = [];
var clusterTc = 0;
function flushCluster() {
if (!blocks.length) return;
clusters.push(el(0x1F43B675, concat(
[el(0xE7, uintPayload(clusterTc))].concat(blocks)
)));
blocks = [];
}
for (var i = 0; i < frames.length; i++) {
var f = frames[i];
var tc = Math.round(f.timestampMs);
if (!blocks.length) clusterTc = tc;
else if (tc - clusterTc > 1000 || (f.key && tc !== clusterTc)) {
flushCluster();
clusterTc = tc;
}
var rel = tc - clusterTc;
var head = new Uint8Array(4);
head[0] = 0x81; // track 1 (vint)
head[1] = (rel >> 8) & 0xff;
head[2] = rel & 0xff;
head[3] = f.key ? 0x80 : 0x00; // flags
blocks.push(el(0xA3, concat([head, f.data]))); // SimpleBlock
}
flushCluster();
var segment = el(0x18538067, concat([info, tracks].concat(clusters)));
return concat([header, segment]);
}
return { mux: mux };
})();