This commit is contained in:
chk
2026-06-07 17:00:43 +02:00
parent 39fa6d07f5
commit d9cfa7e974
13 changed files with 744 additions and 62 deletions

73
test/fmp4Parser.test.js Normal file
View File

@@ -0,0 +1,73 @@
'use strict';
const { Fmp4Parser } = require('../src/fmp4Parser');
// Baut eine ISO-BMFF-Box: [4B size][4B type][payload]. size deckt size+type ab.
function box(type, payload = Buffer.alloc(0)) {
const head = Buffer.alloc(8);
head.writeUInt32BE(8 + payload.length, 0);
head.write(type, 4, 'latin1');
return Buffer.concat([head, payload]);
}
// Typischer FFmpeg-fMP4-Stream: ftyp moov | (moof mdat)…
function ftyp() { return box('ftyp', Buffer.from('isom')); }
function moov() { return box('moov', Buffer.from([1, 2, 3, 4])); }
function frag(n) { return Buffer.concat([box('moof', Buffer.from([n])), box('mdat', Buffer.from([n, n, n]))]); }
describe('Fmp4Parser', () => {
test('Init-Segment = ftyp + moov, einmalig', () => {
const inits = []; const segs = [];
const p = new Fmp4Parser({ onInit: (b) => inits.push(b), onSegment: (b) => segs.push(b) });
p.push(Buffer.concat([ftyp(), moov()]));
expect(inits).toHaveLength(1);
expect(inits[0].equals(Buffer.concat([ftyp(), moov()]))).toBe(true);
expect(segs).toHaveLength(0);
});
test('Fragmente = moof + mdat', () => {
const inits = []; const segs = [];
const p = new Fmp4Parser({ onInit: (b) => inits.push(b), onSegment: (b) => segs.push(b) });
p.push(Buffer.concat([ftyp(), moov(), frag(1), frag(2)]));
expect(inits).toHaveLength(1);
expect(segs).toHaveLength(2);
expect(segs[0].equals(frag(1))).toBe(true);
expect(segs[1].equals(frag(2))).toBe(true);
});
test('über beliebige Chunk-Grenzen gesplittet', () => {
const inits = []; const segs = [];
const p = new Fmp4Parser({ onInit: (b) => inits.push(b), onSegment: (b) => segs.push(b) });
const stream = Buffer.concat([ftyp(), moov(), frag(1), frag(2), frag(3)]);
for (let i = 0; i < stream.length; i += 3) p.push(stream.subarray(i, i + 3)); // 3-Byte-Häppchen
expect(inits).toHaveLength(1);
expect(segs).toHaveLength(3);
expect(segs[2].equals(frag(3))).toBe(true);
});
test('Byte-für-Byte (Extremfall)', () => {
const inits = []; const segs = [];
const p = new Fmp4Parser({ onInit: (b) => inits.push(b), onSegment: (b) => segs.push(b) });
const stream = Buffer.concat([ftyp(), moov(), frag(7)]);
for (const byte of stream) p.push(Buffer.from([byte]));
expect(inits).toHaveLength(1);
expect(segs).toHaveLength(1);
expect(segs[0].equals(frag(7))).toBe(true);
});
test('64-Bit largesize (size==1) wird verstanden', () => {
const inits = [];
const p = new Fmp4Parser({ onInit: (b) => inits.push(b) });
// ftyp normal, moov als 64-Bit-Box
const payload = Buffer.from([9, 9]);
const head = Buffer.alloc(16);
head.writeUInt32BE(1, 0); // size == 1 → largesize folgt
head.write('moov', 4, 'latin1');
head.writeUInt32BE(0, 8); // largesize high
head.writeUInt32BE(16 + payload.length, 12); // largesize low
const moov64 = Buffer.concat([head, payload]);
p.push(Buffer.concat([ftyp(), moov64]));
expect(inits).toHaveLength(1);
expect(inits[0].equals(Buffer.concat([ftyp(), moov64]))).toBe(true);
});
});

71
test/hwencode.test.js Normal file
View File

@@ -0,0 +1,71 @@
'use strict';
const { resolveHwenc, mseCodecString, h264LiveArgs } = require('../src/hwencode');
describe('resolveHwenc', () => {
test('intel → vaapi am Default-Renderknoten', () => {
expect(resolveHwenc({ vendor: 'intel' })).toMatchObject({ encoder: 'vaapi', device: '/dev/dri/renderD128' });
});
test('amd → vaapi (gleicher Codepfad)', () => {
expect(resolveHwenc({ vendor: 'amd' }).encoder).toBe('vaapi');
});
test('none → libx264 (Software-Fallback)', () => {
expect(resolveHwenc({ vendor: 'none' }).encoder).toBe('libx264');
});
test('HWENC überschreibt Vendor', () => {
expect(resolveHwenc({ vendor: 'amd', encoder: 'qsv' }).encoder).toBe('qsv');
});
test('eigener Device-Pfad', () => {
expect(resolveHwenc({ vendor: 'intel', device: '/dev/dri/renderD129' }).device).toBe('/dev/dri/renderD129');
});
test('unbekannter Encoder wirft', () => {
expect(() => resolveHwenc({ encoder: 'nvenc' })).toThrow();
});
});
describe('mseCodecString', () => {
test('main/3.1', () => expect(mseCodecString('main', '1F')).toBe('avc1.4D401F'));
test('high', () => expect(mseCodecString('high', '1F')).toBe('avc1.64001F'));
test('constrained_baseline', () => expect(mseCodecString('constrained_baseline', '1F')).toBe('avc1.42E01F'));
test('unbekannt → main', () => expect(mseCodecString('xyz', '1F')).toBe('avc1.4D401F'));
});
describe('h264LiveArgs', () => {
const base = { device: '/dev/video0', size: '640x480', fps: 30 };
test('VAAPI: hwupload + h264_vaapi + fragmentiertes mp4 auf pipe:1', () => {
const { args, useFd3 } = h264LiveArgs({ ...base, hwenc: resolveHwenc({ vendor: 'intel' }), jpegSnapshots: false });
const s = args.join(' ');
expect(s).toContain('-vaapi_device /dev/dri/renderD128');
expect(s).toContain('format=nv12,hwupload');
expect(s).toContain('-c:v h264_vaapi');
expect(s).toContain('-movflags +frag_keyframe+empty_moov+default_base_moof');
expect(args[args.length - 1]).toBe('pipe:1');
expect(useFd3).toBe(false);
});
test('mit Snapshot-Nebenausgang: split + pipe:3 + useFd3', () => {
const { args, useFd3 } = h264LiveArgs({ ...base, hwenc: resolveHwenc({ vendor: 'amd' }), jpegSnapshots: true });
const s = args.join(' ');
expect(s).toContain('-filter_complex');
expect(s).toContain('split=2');
expect(s).toContain('-c:v mjpeg');
expect(args[args.length - 1]).toBe('pipe:3');
expect(useFd3).toBe(true);
});
test('libx264-Software-Fallback', () => {
const { args } = h264LiveArgs({ ...base, hwenc: resolveHwenc({ vendor: 'none' }), jpegSnapshots: false });
const s = args.join(' ');
expect(s).toContain('-c:v libx264');
expect(s).toContain('-tune zerolatency');
expect(s).not.toContain('hwupload');
});
test('GOP default ~2×fps, überschreibbar', () => {
const def = h264LiveArgs({ ...base, hwenc: resolveHwenc({}), jpegSnapshots: false });
expect(def.args[def.args.indexOf('-g') + 1]).toBe('60');
const custom = h264LiveArgs({ ...base, gop: 15, hwenc: resolveHwenc({}), jpegSnapshots: false });
expect(custom.args[custom.args.indexOf('-g') + 1]).toBe('15');
});
});