/** * Unit tests for SFU utility functions. * * Tests protobuf varint encoding/decoding, packet encode/decode roundtrips, * and audio format conversion (48kHz stereo ↔ 16kHz mono). */ import { describe, expect, it } from "vitest"; import { decodeVarint, encodeVarint, extractPayloadFromProtobuf, encodePayloadToProtobuf, downsample48kStereoTo16kMono, upsample16kMonoTo48kStereo } from "../sfu-utils"; // --- Varint encoding/decoding --- describe("varint encoding", () => { it("encodes single-byte values (0–127)", () => { expect(encodeVarint(0)).toEqual(new Uint8Array([0])); expect(encodeVarint(1)).toEqual(new Uint8Array([1])); expect(encodeVarint(127)).toEqual(new Uint8Array([127])); }); it("encodes multi-byte values (>127)", () => { // 128 = 0x80 → [0x80, 0x01] expect(encodeVarint(128)).toEqual(new Uint8Array([0x80, 0x01])); // 300 = 0x012C → [0xAC, 0x02] expect(encodeVarint(300)).toEqual(new Uint8Array([0xac, 0x02])); }); it("roundtrips through decode", () => { for (const value of [0, 1, 42, 127, 128, 255, 300, 16384, 65535]) { const encoded = encodeVarint(value); const decoded = decodeVarint(encoded, 0); expect(decoded.value).toBe(value); expect(decoded.bytesRead).toBe(encoded.length); } }); it("decodes at arbitrary offsets", () => { // Prefix with 3 garbage bytes, then encode 42 const payload = encodeVarint(42); const buf = new Uint8Array(3 + payload.length); buf[0] = 0xff; buf[1] = 0xff; buf[2] = 0xff; buf.set(payload, 3); const { value, bytesRead } = decodeVarint(buf, 3); expect(value).toBe(42); expect(bytesRead).toBe(1); }); }); // --- Protobuf packet encode/decode --- describe("protobuf packet encode/decode", () => { it("roundtrips payload through encode → decode", () => { const original = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); const encoded = encodePayloadToProtobuf(original); const decoded = extractPayloadFromProtobuf(encoded); expect(decoded).not.toBeNull(); expect(new Uint8Array(decoded!)).toEqual(original); }); it("roundtrips empty payload", () => { const original = new Uint8Array([]); const encoded = encodePayloadToProtobuf(original); const decoded = extractPayloadFromProtobuf(encoded); expect(decoded).not.toBeNull(); expect(decoded!.length).toBe(0); }); it("roundtrips large payload (simulating audio frame)", () => { // 960 samples at 48kHz stereo = 20ms frame = 3840 bytes const original = new Uint8Array(3840); for (let i = 0; i < original.length; i++) { original[i] = i % 256; } const encoded = encodePayloadToProtobuf(original); const decoded = extractPayloadFromProtobuf(encoded); expect(decoded).not.toBeNull(); expect(decoded!.length).toBe(3840); expect(new Uint8Array(decoded!)).toEqual(original); }); it("extracts payload from a packet with other fields", () => { // Manually build a protobuf with: // field 1 (sequenceNumber) = 42, varint // field 2 (timestamp) = 1000, varint // field 5 (payload) = [0xAA, 0xBB] const parts: Uint8Array[] = []; // Field 1, wire type 0: tag = (1 << 3) | 0 = 8 parts.push(encodeVarint(8)); parts.push(encodeVarint(42)); // Field 2, wire type 0: tag = (2 << 3) | 0 = 16 parts.push(encodeVarint(16)); parts.push(encodeVarint(1000)); // Field 5, wire type 2: tag = (5 << 3) | 2 = 42 parts.push(encodeVarint(42)); parts.push(encodeVarint(2)); // length = 2 parts.push(new Uint8Array([0xaa, 0xbb])); const totalLen = parts.reduce((s, p) => s + p.length, 0); const buf = new Uint8Array(totalLen); let offset = 0; for (const part of parts) { buf.set(part, offset); offset += part.length; } const payload = extractPayloadFromProtobuf(buf.buffer); expect(payload).not.toBeNull(); expect(new Uint8Array(payload!)).toEqual(new Uint8Array([0xaa, 0xbb])); }); it("returns null for empty buffer", () => { expect(extractPayloadFromProtobuf(new ArrayBuffer(0))).toBeNull(); }); it("returns null when payload field is missing", () => { // Only field 1 (sequenceNumber) const tag = encodeVarint(8); // field 1, wire type 0 const val = encodeVarint(42); const buf = new Uint8Array(tag.length + val.length); buf.set(tag, 0); buf.set(val, tag.length); expect(extractPayloadFromProtobuf(buf.buffer)).toBeNull(); }); }); // --- Audio conversion --- describe("audio conversion", () => { /** * Helper: create 48kHz stereo PCM buffer with known sample values. * Each stereo sample pair is 4 bytes (L int16 LE + R int16 LE). * For downsampling, we need groups of 3 stereo pairs → 1 mono sample. */ function make48kStereo(monoSamples: number[]): Uint8Array { // Each mono sample becomes 3 stereo pairs (for 3:1 ratio) const buf = new ArrayBuffer(monoSamples.length * 3 * 4); const view = new DataView(buf); for (let i = 0; i < monoSamples.length; i++) { const sample = monoSamples[i]; for (let j = 0; j < 3; j++) { const offset = (i * 3 + j) * 4; view.setInt16(offset, sample, true); // left view.setInt16(offset + 2, sample, true); // right } } return new Uint8Array(buf); } /** Helper: read 16kHz mono PCM samples from ArrayBuffer. */ function read16kMono(buf: ArrayBuffer): number[] { const view = new DataView(buf); const samples: number[] = []; for (let i = 0; i < buf.byteLength; i += 2) { samples.push(view.getInt16(i, true)); } return samples; } /** Helper: create 16kHz mono PCM buffer. */ function make16kMono(samples: number[]): ArrayBuffer { const buf = new ArrayBuffer(samples.length * 2); const view = new DataView(buf); for (let i = 0; i < samples.length; i++) { view.setInt16(i * 2, samples[i], true); } return buf; } /** Helper: read 48kHz stereo samples as [left, right] pairs. */ function read48kStereo(buf: Uint8Array): [number, number][] { const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); const pairs: [number, number][] = []; for (let i = 0; i < buf.byteLength; i += 4) { pairs.push([view.getInt16(i, true), view.getInt16(i + 2, true)]); } return pairs; } describe("downsample48kStereoTo16kMono", () => { it("converts identical L+R channels to mono (average = same value)", () => { const stereo = make48kStereo([1000, -500, 0, 32767]); const mono = downsample48kStereoTo16kMono(stereo); const samples = read16kMono(mono); // Each group of 3 identical stereo pairs → 1 mono sample // With identical L+R, average = original value expect(samples).toEqual([1000, -500, 0, 32767]); }); it("averages left and right channels", () => { // Create stereo with different L and R const buf = new ArrayBuffer(3 * 4); // 3 stereo pairs → 1 output sample const view = new DataView(buf); // Only first pair matters for downsampling (sample at index 0*3 = 0) view.setInt16(0, 100, true); // left view.setInt16(2, 200, true); // right // Fill remaining pairs view.setInt16(4, 100, true); view.setInt16(6, 200, true); view.setInt16(8, 100, true); view.setInt16(10, 200, true); const mono = downsample48kStereoTo16kMono(new Uint8Array(buf)); const samples = read16kMono(mono); expect(samples[0]).toBe(150); // (100 + 200) / 2 }); it("returns empty buffer for empty input", () => { const mono = downsample48kStereoTo16kMono(new Uint8Array(0)); expect(mono.byteLength).toBe(0); }); it("handles input shorter than one output sample", () => { // 2 stereo pairs (need 3 for one output sample) const buf = new Uint8Array(2 * 4); const mono = downsample48kStereoTo16kMono(buf); expect(mono.byteLength).toBe(0); }); }); describe("upsample16kMonoTo48kStereo", () => { it("duplicates mono samples to stereo pairs (3x)", () => { const mono = make16kMono([1000, -500]); const stereo = upsample16kMonoTo48kStereo(mono); const pairs = read48kStereo(stereo); // 2 mono samples → 6 stereo pairs expect(pairs.length).toBe(6); // First 3 pairs should be [1000, 1000] expect(pairs[0]).toEqual([1000, 1000]); expect(pairs[1]).toEqual([1000, 1000]); expect(pairs[2]).toEqual([1000, 1000]); // Next 3 pairs should be [-500, -500] expect(pairs[3]).toEqual([-500, -500]); expect(pairs[4]).toEqual([-500, -500]); expect(pairs[5]).toEqual([-500, -500]); }); it("returns empty buffer for empty input", () => { const stereo = upsample16kMonoTo48kStereo(new ArrayBuffer(0)); expect(stereo.length).toBe(0); }); it("handles single sample", () => { const mono = make16kMono([42]); const stereo = upsample16kMonoTo48kStereo(mono); const pairs = read48kStereo(stereo); expect(pairs.length).toBe(3); for (const [l, r] of pairs) { expect(l).toBe(42); expect(r).toBe(42); } }); }); describe("downsample ↔ upsample roundtrip", () => { it("recovers original samples through upsample → downsample", () => { // Start with 16kHz mono, upsample to 48kHz stereo, downsample back const original = [100, -200, 300, 0, -32768, 32767]; const mono = make16kMono(original); const stereo = upsample16kMonoTo48kStereo(mono); const roundtripped = downsample48kStereoTo16kMono(stereo); const result = read16kMono(roundtripped); expect(result).toEqual(original); }); }); });