Files
appRobotWebcam/src/fmp4Parser.js
2026-06-07 17:00:43 +02:00

74 lines
2.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 };