rx-player
Version:
Canal+ HTML5 Video Player
450 lines (392 loc) • 15.4 kB
text/typescript
import { describe, it, expect, vi, afterEach } from "vitest";
import assert from "../../../../utils/assert";
import { concat } from "../../../../utils/byte_parsing";
import {
boxTypeToFourCC,
getBox,
getBoxContent,
getBoxesContent,
getBoxOffsets,
getChildBox,
getNextBoxOffsets,
getUuidContent,
} from "../get_box";
const logWarn = vi.hoisted(() => vi.fn());
vi.mock("../../../../log", () => ({
default: { warn: logWarn, debug: vi.fn(), error: vi.fn(), info: vi.fn() },
}));
/**
* Writes a 32-bit big-endian integer into `buf` at `offset`.
*/
function writeBe4(buf: Uint8Array, offset: number, value: number): void {
buf[offset] = (value >>> 24) & 0xff;
buf[offset + 1] = (value >>> 16) & 0xff;
buf[offset + 2] = (value >>> 8) & 0xff;
buf[offset + 3] = value & 0xff;
}
/**
* Writes a 64-bit big-endian integer (as two 32-bit halves) into `buf` at
* `offset`. Sufficient for box sizes well within the 32-bit range.
*/
function writeBe8(buf: Uint8Array, offset: number, hi: number, lo: number): void {
writeBe4(buf, offset, hi);
writeBe4(buf, offset + 4, lo);
}
/** Box name constants (big-endian ASCII) */
const MOOV = 0x6d6f6f76;
const MDAT = 0x6d646174;
const FTYP = 0x66747970;
const FREE = 0x66726565;
const UUID = 0x75756964;
/**
* Builds a minimal ISOBMFF box:
* [4 bytes size][4 bytes name][...content]
*
* Total size = 8 + content.length
*/
function makeBox(name: number, content: Uint8Array = new Uint8Array(0)): Uint8Array {
const size = 8 + content.length;
const buf = new Uint8Array(size);
writeBe4(buf, 0, size);
writeBe4(buf, 4, name);
buf.set(content, 8);
return buf;
}
/**
* Builds an extended-size (size === 1) box:
* [4 bytes: 1][4 bytes name][8 bytes largeSize][...content]
*
* Total size = 16 + content.length
*/
function makeLargeBox(name: number, content: Uint8Array = new Uint8Array(0)): Uint8Array {
const totalSize = 16 + content.length;
const buf = new Uint8Array(totalSize);
writeBe4(buf, 0, 1); // size === 1 → extended
writeBe4(buf, 4, name);
writeBe8(buf, 8, 0, totalSize); // 64-bit size (fits in lower 32 bits)
buf.set(content, 16);
return buf;
}
/**
* Builds a uuid box:
* [4 bytes size][4 bytes "uuid"][16 bytes uuid][...content]
*/
function makeUuidBox(
id1: number,
id2: number,
id3: number,
id4: number,
content: Uint8Array = new Uint8Array(0),
): Uint8Array {
const size = 8 + 16 + content.length;
const buf = new Uint8Array(size);
writeBe4(buf, 0, size);
writeBe4(buf, 4, UUID);
writeBe4(buf, 8, id1);
writeBe4(buf, 12, id2);
writeBe4(buf, 16, id3);
writeBe4(buf, 20, id4);
buf.set(content, 24);
return buf;
}
describe("isobmff - boxTypeToFourCC", () => {
afterEach(() => {
vi.resetAllMocks();
});
it("should convert 'moov' integer to its fourCC string", () => {
expect(boxTypeToFourCC(0x6d6f6f76)).toBe("moov");
});
it("should convert 'mdat' integer to its fourCC string", () => {
expect(boxTypeToFourCC(0x6d646174)).toBe("mdat");
});
it("should convert 'pssh' integer to its fourCC string", () => {
expect(boxTypeToFourCC(0x70737368)).toBe("pssh");
});
it("should correctly extract all four distinct bytes", () => {
// 'A'=0x41 'B'=0x42 'C'=0x43 'D'=0x44
expect(boxTypeToFourCC(0x41424344)).toBe("ABCD");
});
it("should return four identical characters when all bytes are the same", () => {
expect(boxTypeToFourCC(0x61616161)).toBe("aaaa");
});
it("should return four null characters for 0", () => {
expect(boxTypeToFourCC(0x00000000)).toBe("\0\0\0\0");
});
it("should handle the maximum 32-bit unsigned integer", () => {
expect(boxTypeToFourCC(0xffffffff)).toBe("\xff\xff\xff\xff");
});
});
describe("isobmff - getBoxOffsets", () => {
afterEach(() => {
vi.resetAllMocks();
});
it("should return null when the buffer is too small to hold any box header", () => {
expect(getBoxOffsets(new Uint8Array(7), MOOV)).toBeNull();
});
it("should return null when the box name does not match", () => {
const buf = makeBox(FTYP, new Uint8Array(4));
expect(getBoxOffsets(buf, MOOV)).toBeNull();
});
it("should find a simple box and return correct offsets", () => {
const content = new Uint8Array([1, 2, 3, 4]);
const buf = makeBox(MOOV, content);
// [0, 8, 12] → start=0, contentStart=8, end=12
expect(getBoxOffsets(buf, MOOV)).toEqual([0, 8, 12]);
});
it("should skip earlier boxes and find the requested one", () => {
const first = makeBox(FTYP, new Uint8Array(4));
const second = makeBox(MOOV, new Uint8Array(8));
const buf = concat(first, second);
const result = getBoxOffsets(buf, MOOV);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
expect(result[0]).toBe(first.length);
expect(result[2]).toBe(buf.length);
});
it("should handle an extended-size (size === 1) box", () => {
const content = new Uint8Array([0xaa, 0xbb]);
const buf = makeLargeBox(MOOV, content);
// start=0, contentStart=16, end=18
expect(getBoxOffsets(buf, MOOV)).toEqual([0, 16, 18]);
});
it("should treat size === 0 as extending to the end of the buffer", () => {
const buf = makeBox(MOOV, new Uint8Array(4));
writeBe4(buf, 0, 0); // overwrite size with 0
const result = getBoxOffsets(buf, MOOV);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
expect(result[2]).toBe(buf.length);
});
it("should return null when an extended-size header cannot fit in the buffer", () => {
// Write size=1 (extended) but only 12 bytes total — not enough for the 8-byte largeSize field
const buf = new Uint8Array(12);
writeBe4(buf, 0, 1);
writeBe4(buf, 4, MOOV);
expect(getBoxOffsets(buf, MOOV)).toBeNull();
});
it("should add 16 bytes to the content offset for a uuid box", () => {
const content = new Uint8Array(4);
const buf = makeUuidBox(0x01020304, 0x05060708, 0x090a0b0c, 0x0d0e0f10, content);
const result = getBoxOffsets(buf, UUID);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
// uuid header: 8 (size+name) + 16 (uuid bytes) = content starts at 24
expect(result[1]).toBe(24);
});
});
describe("isobmff - getBoxContent", () => {
afterEach(() => {
vi.resetAllMocks();
});
it("should return null when the box is not present", () => {
const buf = makeBox(FTYP, new Uint8Array(4));
expect(getBoxContent(buf, MOOV)).toBeNull();
});
it("should return only the content bytes (excluding size and name)", () => {
const content = new Uint8Array([10, 20, 30]);
const buf = makeBox(MDAT, content);
const result = getBoxContent(buf, MDAT);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
expect(Array.from(result)).toEqual([10, 20, 30]);
});
it("should return an empty Uint8Array for a box with no content", () => {
const buf = makeBox(FTYP);
const result = getBoxContent(buf, FTYP);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
expect(result.length).toBe(0);
});
});
describe("isobmff - getBoxesContent", () => {
afterEach(() => {
vi.resetAllMocks();
});
it("should return an empty array when no matching box exists", () => {
const buf = makeBox(FTYP, new Uint8Array(4));
expect(getBoxesContent(buf, MOOV)).toEqual([]);
});
it("should return all occurrences of a repeated box, in order", () => {
const first = makeBox(FREE, new Uint8Array([1]));
const second = makeBox(FREE, new Uint8Array([2]));
const third = makeBox(FREE, new Uint8Array([3]));
const buf = concat(first, second, third);
const result = getBoxesContent(buf, FREE);
expect(result).toHaveLength(3);
expect(Array.from(result[0])).toEqual([1]);
expect(Array.from(result[1])).toEqual([2]);
expect(Array.from(result[2])).toEqual([3]);
});
it("should return only the matching boxes and skip others", () => {
const a = makeBox(FTYP, new Uint8Array([0xaa]));
const b = makeBox(FREE, new Uint8Array([0xbb]));
const c = makeBox(FTYP, new Uint8Array([0xcc]));
const buf = concat(a, b, c);
const result = getBoxesContent(buf, FTYP);
expect(result).toHaveLength(2);
expect(Array.from(result[0])).toEqual([0xaa]);
expect(Array.from(result[1])).toEqual([0xcc]);
});
});
describe("isobmff - getBox", () => {
afterEach(() => {
vi.resetAllMocks();
});
it("should return null when the box is not found", () => {
const buf = makeBox(FTYP, new Uint8Array(4));
expect(getBox(buf, MOOV)).toBeNull();
});
it("should return the full box bytes — size + name + content", () => {
const content = new Uint8Array([0xde, 0xad]);
const fullBox = makeBox(MDAT, content);
const result = getBox(fullBox, MDAT);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
expect(Array.from(result)).toEqual(Array.from(fullBox));
});
it("should return only the matching box when preceded by another box", () => {
const before = makeBox(FTYP, new Uint8Array(4));
const target = makeBox(MOOV, new Uint8Array([0xff]));
const buf = concat(before, target);
const result = getBox(buf, MOOV);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
expect(Array.from(result)).toEqual(Array.from(target));
});
});
describe("isobmff - getChildBox", () => {
afterEach(() => {
vi.resetAllMocks();
});
it("should return null when an empty childNames array is provided and the buffer is non-empty", () => {
// With no children to traverse, the loop never executes and currBox is
// returned as-is. Verify the returned reference equals the input.
const buf = makeBox(MOOV, new Uint8Array(4));
const result = getChildBox(buf, []);
assert(result !== null);
expect(Array.from(result)).toEqual(Array.from(buf));
});
it("should return null when the first child is not found", () => {
const buf = makeBox(FTYP, new Uint8Array(4));
expect(getChildBox(buf, [MOOV])).toBeNull();
});
it("should return null when an intermediate child is not found", () => {
// moov → ftyp exists, but moov → ftyp → mdat does not
const innerContent = makeBox(FTYP, new Uint8Array(2));
const outer = makeBox(MOOV, innerContent);
expect(getChildBox(outer, [MOOV, FTYP, MDAT])).toBeNull();
});
it("should traverse a chain of nested boxes and return the innermost content", () => {
const payload = new Uint8Array([0x42]);
const innermost = makeBox(MDAT, payload);
const middle = makeBox(FTYP, innermost);
const outer = makeBox(MOOV, middle);
const result = getChildBox(outer, [MOOV, FTYP, MDAT]);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
expect(Array.from(result)).toEqual([0x42]);
});
});
describe("isobmff - getNextBoxOffsets", () => {
afterEach(() => {
vi.resetAllMocks();
});
it("should return null and warn when the buffer is shorter than 8 bytes", () => {
const result = getNextBoxOffsets(new Uint8Array(4));
expect(result).toBeNull();
expect(logWarn).toHaveBeenCalledOnce();
});
it("should return correct offsets for a simple box", () => {
const content = new Uint8Array([1, 2]);
const buf = makeBox(MOOV, content);
// start=0, contentStart=8, end=10
expect(getNextBoxOffsets(buf)).toEqual([0, 8, 10]);
});
it("should handle an extended-size box correctly", () => {
const content = new Uint8Array([0xab]);
const buf = makeLargeBox(FTYP, content);
expect(getNextBoxOffsets(buf)).toEqual([0, 16, 17]);
});
it("should treat size === 0 as extending to the end of the buffer", () => {
const buf = makeBox(MOOV, new Uint8Array(6));
writeBe4(buf, 0, 0);
const result = getNextBoxOffsets(buf);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
expect(result[2]).toBe(buf.length);
});
it("should return null and warn when an extended-size header cannot fit in the buffer", () => {
const buf = new Uint8Array(12);
writeBe4(buf, 0, 1);
writeBe4(buf, 4, MOOV);
const result = getNextBoxOffsets(buf);
expect(result).toBeNull();
expect(logWarn).toHaveBeenCalledOnce();
});
it("should add 16 bytes to the content offset for a uuid box", () => {
const content = new Uint8Array(2);
const buf = makeUuidBox(0x01020304, 0x05060708, 0x090a0b0c, 0x0d0e0f10, content);
const result = getNextBoxOffsets(buf);
expect(result).not.toBeNull();
assert(result !== null); // For TypeScript
expect(result[1]).toBe(24);
});
it("should always return 0 as the first element (start byte)", () => {
const buf = makeBox(FREE, new Uint8Array(4));
const result = getNextBoxOffsets(buf);
assert(result !== null);
expect(result[0]).toBe(0);
});
});
describe("isobmff - getUuidContent", () => {
afterEach(() => {
vi.resetAllMocks();
});
const ID1 = 0x01020304;
const ID2 = 0x05060708;
const ID3 = 0x090a0b0c;
const ID4 = 0x0d0e0f10;
it("should return undefined when no uuid box is present", () => {
const buf = makeBox(MOOV, new Uint8Array(4));
expect(getUuidContent(buf, ID1, ID2, ID3, ID4)).toBeUndefined();
});
it("should return undefined when the uuid identifiers do not match", () => {
const buf = makeUuidBox(0xdeadbeef, ID2, ID3, ID4, new Uint8Array(2));
expect(getUuidContent(buf, ID1, ID2, ID3, ID4)).toBeUndefined();
});
it("should return the content bytes when the uuid matches exactly", () => {
const payload = new Uint8Array([0xca, 0xfe]);
const buf = makeUuidBox(ID1, ID2, ID3, ID4, payload);
const result = getUuidContent(buf, ID1, ID2, ID3, ID4);
expect(result).not.toBeUndefined();
assert(result !== undefined); // For TypeScript
expect(Array.from(result)).toEqual([0xca, 0xfe]);
});
it("should skip non-uuid boxes and find the matching uuid box", () => {
const payload = new Uint8Array([0xff]);
const nonUuid = makeBox(MOOV, new Uint8Array(4));
const uuidBox = makeUuidBox(ID1, ID2, ID3, ID4, payload);
const buf = concat(nonUuid, uuidBox);
const result = getUuidContent(buf, ID1, ID2, ID3, ID4);
expect(result).not.toBeUndefined();
assert(result !== undefined); // For TypeScript
expect(Array.from(result)).toEqual([0xff]);
});
it("should return undefined when uuid header is present but the 16-byte id cannot fit in the buffer", () => {
// Build a box that claims to be uuid but is too short to hold its 16-byte identifier
const buf = new Uint8Array(16);
writeBe4(buf, 0, 16);
writeBe4(buf, 4, UUID);
// Only 8 bytes remain — not enough for the full 16-byte uuid identifier
expect(getUuidContent(buf, ID1, ID2, ID3, ID4)).toBeUndefined();
});
it("should handle a size === 0 uuid box that extends to end of buffer", () => {
const payload = new Uint8Array([0x11, 0x22]);
const uuidBox = makeUuidBox(ID1, ID2, ID3, ID4, payload);
writeBe4(uuidBox, 0, 0); // size = 0 → extends to end
const result = getUuidContent(uuidBox, ID1, ID2, ID3, ID4);
expect(result).not.toBeUndefined();
assert(result !== undefined); // For TypeScript
expect(Array.from(result)).toEqual([0x11, 0x22]);
});
});