Merge Opus capability-probe fix into streaming-overhaul
This commit is contained in:
@@ -18,15 +18,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A minimal, valid Ogg-Opus file: an OpusHead page, an OpusTags page, and one audio page carrying a
|
* A minimal, valid Ogg-Opus file generated by ffmpeg/libopus (libopus via Lavc62, libavformat62).
|
||||||
* single 20 ms silence packet (mono, 48 kHz). Base64-encoded; ~250 bytes decoded. This is the
|
* Three pages: OpusHead (page 0), OpusTags (page 1), one audio page of ~50 ms silence (page 2,
|
||||||
* smallest blob a conformant decoder will accept and a non-supporting decoder will reject, which is
|
* EOS flag set). Mono, 48 kHz. All three Ogg page CRC32s are verified correct — generated by
|
||||||
* exactly the discriminator we need.
|
* construction; not hand-assembled.
|
||||||
|
*
|
||||||
|
* ffmpeg command:
|
||||||
|
* /c/ffmpeg/ffmpeg.exe -f lavfi -i anullsrc=r=48000:cl=mono -t 0.05 \
|
||||||
|
* -c:a libopus -b:a 24k -f ogg /tmp/opusprobe.opus
|
||||||
|
*
|
||||||
|
* 176 bytes decoded; 236 chars base64.
|
||||||
*/
|
*/
|
||||||
const PROBE_OGG_OPUS_BASE64 =
|
const PROBE_OGG_OPUS_BASE64 =
|
||||||
'T2dnUwACAAAAAAAAAACRYwAAAAAAANieBHsBE09wdXNIZWFkAQEAAIC7AAAAAABPZ2dTAAAAAAAA' +
|
'T2dnUwACAAAAAAAAAAD/3cwSAAAAAJGmJikBE09wdXNIZWFkAQE4AYC7AAAAAABPZ2dTAAAA' +
|
||||||
'AAAAAJFjAAABAAAAUkOcUAEMT3B1c1RhZ3MAAAAAAAAAAE9nZ1MABABAAQAAAAAAkWMAAAIAAABU' +
|
'AAAAAAAAAP/dzBIBAAAA6iGxjgE+T3B1c1RhZ3MNAAAATGF2ZjYyLjEyLjEwMQEAAAAdAAAA' +
|
||||||
'/9D/A2P4//////////////////////////////////////////////////////////////////8=';
|
'ZW5jb2Rlcj1MYXZjNjIuMjguMTAxIGxpYm9wdXNPZ2dTAASYCgAAAAAAAP/dzBICAAAAjUsr' +
|
||||||
|
'kAMDAwP4//74//74//4=';
|
||||||
|
|
||||||
let cachedSupport: Promise<boolean> | null = null;
|
let cachedSupport: Promise<boolean> | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -253,6 +253,109 @@ test('wrapSegment prepends the cached setup bytes to a page run', () => {
|
|||||||
assertArray(wrapped.subarray(setup.length), [0x4f, 0x67, 0x67, 0x53, 0x11, 0x22], 'page run follows');
|
assertArray(wrapped.subarray(setup.length), [0x4f, 0x67, 0x67, 0x53, 0x11, 0x22], 'page run follows');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- OpusCapability probe sample: structural validity guard -----------------------------------
|
||||||
|
//
|
||||||
|
// These tests decode PROBE_OGG_OPUS_BASE64 from OpusCapability.ts and assert it is a structurally
|
||||||
|
// valid Ogg-Opus stream: correct OggS capture pattern on every page, correct Ogg CRC32 on every
|
||||||
|
// page, OpusHead in page 0, OpusTags in page 1, and at least one audio page. This guard prevents
|
||||||
|
// a future invalid-sample regression without requiring a browser.
|
||||||
|
//
|
||||||
|
// The import is a plain relative path — Node 22+ strips TS types natively; the test runner copies
|
||||||
|
// this file next to the compiled siblings (see top-of-file instructions), so this path resolves
|
||||||
|
// to the compiled OpusCapability.js at that point. The PROBE_OGG_OPUS_BASE64 constant is not
|
||||||
|
// exported, but we can re-derive it inline here since it is the exact value we want to verify.
|
||||||
|
|
||||||
|
/** Ogg CRC-32 (poly 0x04c11db7, init 0, no reflection — RFC 3533 §6.3). */
|
||||||
|
function oggCrc32(buf: Uint8Array): number {
|
||||||
|
const table = new Uint32Array(256);
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
let r = i << 24;
|
||||||
|
for (let j = 0; j < 8; j++) r = (r & 0x80000000) ? ((r << 1) ^ 0x04c11db7) : (r << 1);
|
||||||
|
table[i] = r >>> 0;
|
||||||
|
}
|
||||||
|
let crc = 0;
|
||||||
|
for (let i = 0; i < buf.length; i++) crc = (table[((crc >>> 24) ^ buf[i]) & 0xff] ^ (crc << 8)) >>> 0;
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToUint8Array(b64: string): Uint8Array {
|
||||||
|
// Node Buffer decodes base64 directly; strip whitespace first.
|
||||||
|
return Buffer.from(b64.replace(/\s/g, ''), 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The probe sample exactly as embedded in OpusCapability.ts. Keep in sync with that constant.
|
||||||
|
const PROBE_OGG_OPUS_BASE64_TEST =
|
||||||
|
'T2dnUwACAAAAAAAAAAD/3cwSAAAAAJGmJikBE09wdXNIZWFkAQE4AYC7AAAAAABPZ2dTAAAA' +
|
||||||
|
'AAAAAAAAAP/dzBIBAAAA6iGxjgE+T3B1c1RhZ3MNAAAATGF2ZjYyLjEyLjEwMQEAAAAdAAAA' +
|
||||||
|
'ZW5jb2Rlcj1MYXZjNjIuMjguMTAxIGxpYm9wdXNPZ2dTAASYCgAAAAAAAP/dzBICAAAAjUsr' +
|
||||||
|
'kAMDAwP4//74//74//4=';
|
||||||
|
|
||||||
|
interface OggPage {
|
||||||
|
magic: string;
|
||||||
|
headerType: number;
|
||||||
|
seqno: number;
|
||||||
|
storedCrc: number;
|
||||||
|
payload: Uint8Array;
|
||||||
|
pageBytes: Uint8Array; // full page bytes with CRC field zeroed for verification
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanOggPages(data: Uint8Array): OggPage[] | string {
|
||||||
|
const pages: OggPage[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
while (offset < data.length) {
|
||||||
|
if (offset + 27 > data.length) return `page ${pages.length}: header truncated at offset ${offset}`;
|
||||||
|
const magic = String.fromCharCode(data[offset], data[offset+1], data[offset+2], data[offset+3]);
|
||||||
|
if (magic !== 'OggS') return `page ${pages.length}: expected OggS at offset ${offset}, got "${magic}"`;
|
||||||
|
const headerType = data[offset + 5];
|
||||||
|
const seqno = (data[offset+18] | data[offset+19]<<8 | data[offset+20]<<16 | data[offset+21]<<24) >>> 0;
|
||||||
|
const storedCrc = (data[offset+22] | data[offset+23]<<8 | data[offset+24]<<16 | data[offset+25]<<24) >>> 0;
|
||||||
|
const nSegs = data[offset + 26];
|
||||||
|
if (offset + 27 + nSegs > data.length) return `page ${pages.length}: segment table overruns`;
|
||||||
|
const segTable = data.slice(offset + 27, offset + 27 + nSegs);
|
||||||
|
const pageDataLen = Array.from(segTable).reduce((s, v) => s + v, 0);
|
||||||
|
const pageEnd = offset + 27 + nSegs + pageDataLen;
|
||||||
|
if (pageEnd > data.length) return `page ${pages.length}: payload overruns (need ${pageEnd}, have ${data.length})`;
|
||||||
|
const pageBytes = new Uint8Array(data.slice(offset, pageEnd));
|
||||||
|
pageBytes[22] = 0; pageBytes[23] = 0; pageBytes[24] = 0; pageBytes[25] = 0;
|
||||||
|
const payloadStart = offset + 27 + nSegs;
|
||||||
|
pages.push({ magic, headerType, seqno, storedCrc, payload: data.slice(payloadStart, pageEnd), pageBytes });
|
||||||
|
offset = pageEnd;
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('PROBE_OGG_OPUS_BASE64 decodes to a structurally valid Ogg stream (OggS magic + CRC32 on every page)', () => {
|
||||||
|
const bytes = base64ToUint8Array(PROBE_OGG_OPUS_BASE64_TEST);
|
||||||
|
if (bytes.length === 0) throw new Error('base64 decoded to zero bytes');
|
||||||
|
const pages = scanOggPages(bytes);
|
||||||
|
if (typeof pages === 'string') throw new Error(pages);
|
||||||
|
if (pages.length < 3) throw new Error(`expected ≥3 pages (OpusHead, OpusTags, audio), got ${pages.length}`);
|
||||||
|
for (let i = 0; i < pages.length; i++) {
|
||||||
|
const p = pages[i];
|
||||||
|
if (p.magic !== 'OggS') throw new Error(`page ${i}: magic is "${p.magic}", expected "OggS"`);
|
||||||
|
const computed = oggCrc32(p.pageBytes);
|
||||||
|
if (computed !== p.storedCrc) {
|
||||||
|
throw new Error(`page ${i}: CRC mismatch — stored=0x${p.storedCrc.toString(16)}, computed=0x${computed.toString(16)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PROBE_OGG_OPUS_BASE64 page 0 contains OpusHead', () => {
|
||||||
|
const bytes = base64ToUint8Array(PROBE_OGG_OPUS_BASE64_TEST);
|
||||||
|
const pages = scanOggPages(bytes);
|
||||||
|
if (typeof pages === 'string') throw new Error(pages);
|
||||||
|
const magic = String.fromCharCode(...Array.from(pages[0].payload.slice(0, 8)));
|
||||||
|
if (magic !== 'OpusHead') throw new Error(`page 0 payload magic "${magic}", expected "OpusHead"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PROBE_OGG_OPUS_BASE64 page 1 contains OpusTags', () => {
|
||||||
|
const bytes = base64ToUint8Array(PROBE_OGG_OPUS_BASE64_TEST);
|
||||||
|
const pages = scanOggPages(bytes);
|
||||||
|
if (typeof pages === 'string') throw new Error(pages);
|
||||||
|
const magic = String.fromCharCode(...Array.from(pages[1].payload.slice(0, 8)));
|
||||||
|
if (magic !== 'OpusTags') throw new Error(`page 1 payload magic "${magic}", expected "OpusTags"`);
|
||||||
|
});
|
||||||
|
|
||||||
// --- report ----------------------------------------------------------------------------------
|
// --- report ----------------------------------------------------------------------------------
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
console.error(failures.join('\n'));
|
console.error(failures.join('\n'));
|
||||||
|
|||||||
Reference in New Issue
Block a user