rx-player
Version:
Canal+ HTML5 Video Player
283 lines (243 loc) • 9.27 kB
text/typescript
import { describe, it, expect } from "vitest";
import removeDolbyVisionConfigData from "../remove_dolby_vision_config_data";
/** Write a big-endian uint32 into a DataView */
function writeU32(view: DataView, offset: number, value: number): void {
view.setUint32(offset, value, false);
}
/**
* Build a minimal ISOBMFF box:
* [size u32][type u32][...payload]
*/
function buildBox(
type: number,
payload: Uint8Array<ArrayBuffer>,
): Uint8Array<ArrayBuffer> {
const buf = new Uint8Array(8 + payload.length);
const view = new DataView(buf.buffer);
writeU32(view, 0, buf.length);
writeU32(view, 4, type);
buf.set(payload, 8);
return buf;
}
/**
* Concatenate multiple Uint8Arrays into one.
*/
function concat(...arrays: Array<Uint8Array<ArrayBuffer>>): Uint8Array<ArrayBuffer> {
const total = arrays.reduce((n, a) => n + a.length, 0);
const out = new Uint8Array(total);
let offset = 0;
for (const a of arrays) {
out.set(a, offset);
offset += a.length;
}
return out;
}
// Box type constants
const MOOV = 0x6d6f6f76;
const TRAK = 0x7472616b;
const MDIA = 0x6d646961;
const MINF = 0x6d696e66;
const STBL = 0x7374626c;
const STSD = 0x73747364;
const HVC1 = 0x68766331;
const AVC1 = 0x61766331;
const AV01 = 0x61763031;
const DVCC = 0x64766343;
const DVVC = 0x64767643;
const DVWC = 0x64767743;
const FREE = 0x66726565;
/**
* Build a minimal dvcC/dvvC/dvwC payload (5 bytes of fields + 19 reserved).
* Values are representative of a real Profile 8 / level 6 / HDR10-compat record.
*/
function buildDvBox(type: number): Uint8Array<ArrayBuffer> {
const payload = new Uint8Array(24);
const view = new DataView(payload.buffer);
payload[0] = 1; // dv_version_major
payload[1] = 0; // dv_version_minor
// dv_profile=8 (7 bits), dv_level=6 (6 bits), rpu=1, el=0, bl=1
// => 0b_0001000_000110_1_0_1 packed into 16 bits = 0x10_35
view.setUint16(2, 0x1035, false);
// dv_bl_signal_compatibility_id=1 (HDR10) in top 4 bits => 0x10
payload[4] = 0x10;
// bytes 5–23: reserved zeros (already zero)
return buildBox(type, payload);
}
/**
* Build a minimal stsd FullBox wrapping a codec box that contains a DV config box.
* stsd FullBox header: version(1) + flags(3) + entry_count(4) = 8 bytes before children.
*/
function buildStsd(codecType: number, dvBoxType: number): Uint8Array<ArrayBuffer> {
const dvBox = buildDvBox(dvBoxType);
// A real codec box has extra fields before its children, but for our purposes
// the parser only cares about finding the dv child box inside it.
// We add 8 bytes of dummy codec-box fields (e.g. reserved + data-ref-index).
const codecPayload = concat(new Uint8Array(8), dvBox);
const codecBox = buildBox(codecType, codecPayload);
// stsd FullBox header: 4 bytes (version+flags) + 4 bytes (entry_count=1)
const stsdHeader = new Uint8Array(8);
new DataView(stsdHeader.buffer).setUint32(4, 1, false); // entry_count = 1
const stsdPayload = concat(stsdHeader, codecBox);
return buildBox(STSD, stsdPayload);
}
/**
* Wrap an stsd box in the full stbl > minf > mdia > trak chain.
*/
function buildTrakFromStsd(stsd: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> {
const stbl = buildBox(STBL, stsd);
const minf = buildBox(MINF, stbl);
const mdia = buildBox(MDIA, minf);
return buildBox(TRAK, mdia);
}
/**
* Wrap a trak (or multiple traks) in a moov box.
*/
function buildMoov(...traks: Array<Uint8Array<ArrayBuffer>>): Uint8Array<ArrayBuffer> {
return buildBox(MOOV, concat(...traks));
}
/**
* Find the byte offset of a box type within a buffer using a flat byte-by-byte
* scan. This finds boxes at any nesting depth.
* Returns -1 if not found.
*/
function findBoxOffset(buf: Uint8Array, type: number): number {
for (let i = 0; i + 8 <= buf.length; i++) {
const t = new DataView(buf.buffer, buf.byteOffset + i + 4, 4).getUint32(0, false);
if (t === type) {
return i;
}
}
return -1;
}
describe("removeDolbyVisionConfigData", () => {
describe("when there is no moov box", () => {
it("does nothing and does not throw", () => {
const data = buildBox(0x66726565 /* free */, new Uint8Array(16));
const before = data.slice();
removeDolbyVisionConfigData(data);
expect(data).toEqual(before);
});
});
describe("when moov has no trak", () => {
it("does nothing and does not throw", () => {
const data = buildMoov(buildBox(0x75647461 /* udta */, new Uint8Array(4)));
const before = data.slice();
removeDolbyVisionConfigData(data);
expect(data).toEqual(before);
});
});
describe("when trak has no dvcC/dvvC/dvwC", () => {
it("leaves all box types untouched", () => {
const stsd = buildStsd(HVC1, 0x68766343 /* hvcC — not a DV box */);
const data = buildMoov(buildTrakFromStsd(stsd));
const before = data.slice();
removeDolbyVisionConfigData(data);
expect(data).toEqual(before);
});
});
describe("dvcC replacement", () => {
it("replaces dvcC type bytes with 'free'", () => {
const stsd = buildStsd(HVC1, DVCC);
const data = buildMoov(buildTrakFromStsd(stsd));
removeDolbyVisionConfigData(data);
const dvOffset = findBoxOffset(data, DVCC);
expect(dvOffset).toBe(-1); // no longer found as dvcC
// The box that was dvcC should now read as `free`
const freeOffset = findBoxOffset(data, FREE);
expect(freeOffset).toBeGreaterThan(-1);
});
it("preserves the box size field", () => {
const stsd = buildStsd(HVC1, DVCC);
const data = buildMoov(buildTrakFromStsd(stsd));
const dvOffset = findBoxOffset(data, DVCC);
const sizeBefore = new DataView(
data.buffer,
data.byteOffset + dvOffset,
4,
).getUint32(0, false);
removeDolbyVisionConfigData(data);
// find the free box at the same position
const sizeAfter = new DataView(
data.buffer,
data.byteOffset + dvOffset,
4,
).getUint32(0, false);
expect(sizeAfter).toBe(sizeBefore);
});
it("preserves the payload bytes after replacing the type", () => {
const stsd = buildStsd(HVC1, DVCC);
const data = buildMoov(buildTrakFromStsd(stsd));
const dvOffset = findBoxOffset(data, DVCC);
const payloadBefore = data.slice(dvOffset + 8);
removeDolbyVisionConfigData(data);
const payloadAfter = data.slice(dvOffset + 8);
expect(payloadAfter).toEqual(payloadBefore);
});
});
describe("dvvC replacement", () => {
it("replaces dvvC type bytes with 'free'", () => {
const stsd = buildStsd(HVC1, DVVC);
const data = buildMoov(buildTrakFromStsd(stsd));
removeDolbyVisionConfigData(data);
expect(findBoxOffset(data, DVVC)).toBe(-1);
expect(findBoxOffset(data, FREE)).toBeGreaterThan(-1);
});
});
describe("dvwC replacement", () => {
it("replaces dvwC type bytes with 'free' (AV1 base layer)", () => {
const stsd = buildStsd(AV01, DVWC);
const data = buildMoov(buildTrakFromStsd(stsd));
removeDolbyVisionConfigData(data);
expect(findBoxOffset(data, DVWC)).toBe(-1);
expect(findBoxOffset(data, FREE)).toBeGreaterThan(-1);
});
});
describe("multiple trak boxes", () => {
it("replaces dvcC in all trak boxes", () => {
const stsd1 = buildStsd(HVC1, DVCC);
const stsd2 = buildStsd(AVC1, DVCC);
const data = buildMoov(buildTrakFromStsd(stsd1), buildTrakFromStsd(stsd2));
removeDolbyVisionConfigData(data);
// No dvcC should remain anywhere in the buffer
expect(findBoxOffset(data, DVCC)).toBe(-1);
});
it("only touches the DV track and leaves a non-DV trak intact", () => {
const stsdWithDv = buildStsd(HVC1, DVCC);
const stsdWithoutDv = buildStsd(AVC1, 0x61766343 /* avcC */);
const data = buildMoov(
buildTrakFromStsd(stsdWithDv),
buildTrakFromStsd(stsdWithoutDv),
);
const before = data.slice();
removeDolbyVisionConfigData(data);
// The avcC box should be completely untouched
const avcCOffset = findBoxOffset(data, 0x61766343);
const avcCOffsetBefore = findBoxOffset(before, 0x61766343);
expect(avcCOffset).toBe(avcCOffsetBefore);
});
});
describe("mixed dvcC and dvvC in the same segment", () => {
it("replaces both", () => {
const stsd1 = buildStsd(HVC1, DVCC);
const stsd2 = buildStsd(HVC1, DVVC);
const data = buildMoov(buildTrakFromStsd(stsd1), buildTrakFromStsd(stsd2));
removeDolbyVisionConfigData(data);
expect(findBoxOffset(data, DVCC)).toBe(-1);
expect(findBoxOffset(data, DVVC)).toBe(-1);
});
});
describe("empty segment", () => {
it("does not throw on an empty buffer", () => {
const data = new Uint8Array(0);
expect(() => removeDolbyVisionConfigData(data)).not.toThrow();
});
});
describe("truncated box", () => {
it("does not throw on a truncated moov", () => {
const full = buildMoov(buildTrakFromStsd(buildStsd(HVC1, DVCC)));
// Truncate to just the moov header — incomplete content
const truncated = full.slice(0, 10);
expect(() => removeDolbyVisionConfigData(truncated)).not.toThrow();
});
});
});