'use strict'; // ── Parser für fragmentiertes MP4 (fMP4) aus FFmpegs `-f mp4 +empty_moov` ───── // FFmpeg schreibt auf pipe:1: // ftyp moov ← Init-Segment (einmal, am Anfang) // moof mdat moof mdat … ← je ein Media-Fragment (moof+mdat) // // MSE im Browser braucht das Init-Segment ZUERST, dann komplette Fragmente. // Ein spät verbundener Client bekommt das gecachte Init-Segment und steigt am // nächsten Fragment ein – deshalb trennen wir hier sauber auf Box-Grenzen. // // Box-Layout (ISO-BMFF): [4B size][4B type][payload]. size deckt size+type ab. // size == 1 → 64-Bit largesize (8B nach dem type). size == 0 → bis EOF (im // Live-Stream nicht zu erwarten; wir warten dann auf mehr Daten). class Fmp4Parser { // onInit(buf) – einmal, sobald ftyp+moov vollständig sind // onSegment(buf) – je ein vollständiges Fragment (moof+mdat) constructor({ onInit, onSegment } = {}) { this.onInit = onInit || (() => {}); this.onSegment = onSegment || (() => {}); this.buf = Buffer.alloc(0); this.gotInit = false; this.initParts = []; // sammelt ftyp…moov this.fragParts = []; // sammelt moof…mdat des aktuellen Fragments } push(chunk) { this.buf = this.buf.length ? Buffer.concat([this.buf, chunk]) : chunk; for (;;) { if (this.buf.length < 8) return; // Header unvollständig let size = this.buf.readUInt32BE(0); let headerLen = 8; if (size === 1) { if (this.buf.length < 16) return; // 64-Bit-Größe: high32 muss 0 sein (Boxen hier sind klein) size = this.buf.readUInt32BE(12); headerLen = 16; } else if (size === 0) { return; // Box bis EOF – im Live-Stream nicht erwartet, auf mehr warten } if (size < headerLen) { // defekt → diese Box-Länge überspringen, resynchronisieren this.buf = this.buf.subarray(headerLen); continue; } if (this.buf.length < size) return; // Box-Body noch nicht komplett const type = this.buf.toString('latin1', 4, 8); const box = this.buf.subarray(0, size); this.buf = this.buf.subarray(size); if (!this.gotInit) { this.initParts.push(box); if (type === 'moov') { const init = Buffer.concat(this.initParts); this.initParts = []; this.gotInit = true; try { this.onInit(init); } catch (_e) { /* Consumer-Fehler ignorieren */ } } continue; } // Media-Phase: Fragment = moof … mdat. Bei mdat ist es vollständig. this.fragParts.push(box); if (type === 'mdat') { const seg = Buffer.concat(this.fragParts); this.fragParts = []; try { this.onSegment(seg); } catch (_e) { /* Consumer-Fehler ignorieren */ } } } } } module.exports = { Fmp4Parser };