/* eslint-disable @typescript-eslint/no-unused-vars */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DataTrackPacket, DataTrackPacketHeader, FrameMarker } from '.'; import { DataTrackHandle } from '../handle'; import { DataTrackTimestamp, WrapAroundUnsignedInt } from '../utils'; import { EXT_FLAG_SHIFT } from './constants'; import { DataTrackE2eeExtension, DataTrackExtensionTag, DataTrackExtensions, DataTrackUserTimestampExtension, } from './extensions'; describe('DataTrackPacket', () => { describe('Serialization', () => { it('should serialize a single packet', () => { const header = new DataTrackPacketHeader({ marker: FrameMarker.Single, trackHandle: DataTrackHandle.fromNumber(101), sequence: WrapAroundUnsignedInt.u16(102), frameNumber: WrapAroundUnsignedInt.u16(103), timestamp: DataTrackTimestamp.fromRtpTicks(104), }); const payloadBytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); const packet = new DataTrackPacket(header, payloadBytes); expect(packet.toBinaryLengthBytes()).toStrictEqual(22); expect(packet.toBinary()).toStrictEqual( new Uint8Array([ 0x18, // Version 0, single, extension 0, // Reserved 0, // Track handle (big endian) 101, 0, // Sequence (big endian) 102, 0, // Frame number (big endian) 103, 0, // Timestamp (big endian) 0, 0, 104, /* (No extension words value) */ 0, // Payload 1, 2, 3, 4, 5, 6, 7, 8, 9, ]), ); }); it('should serialize a final packet with extensions', () => { const header = new DataTrackPacketHeader({ marker: FrameMarker.Final, trackHandle: DataTrackHandle.fromNumber(0x8811), sequence: WrapAroundUnsignedInt.u16(0x4422), frameNumber: WrapAroundUnsignedInt.u16(0x4411), timestamp: DataTrackTimestamp.fromRtpTicks(0x44221188), extensions: new DataTrackExtensions({ userTimestamp: new DataTrackUserTimestampExtension(0x4411221111118811n), e2ee: new DataTrackE2eeExtension(0xfa, new Uint8Array(12).fill(0x3c)), }), }); const payloadBytes = new Uint8Array(32).fill(0xfa); const packet = new DataTrackPacket(header, payloadBytes); expect(packet.toBinaryLengthBytes()).toStrictEqual(74); expect(packet.toBinary()).toStrictEqual( new Uint8Array([ 0xc, // Version 0, final, extension 0, // Reserved 136, // Track handle (big endian) 17, 68, // Sequence (big endian) 34, 68, // Frame number (big endian) 17, 68, // Timestamp (big endian) 34, 17, 136, 0, // Rtp oriented extension words (big endian) 6, // E2ee extension 1, // ID 1 13, // Length 13 0xfa, // Key index 0x3c, // Iv array 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, // User timestamp extension 2, // ID 2 8, // Length 8 68, // Timestamp value (big endian) 17, 34, 17, 17, 17, 136, 17, 0, // Extension padding 0, 0, 0xfa, // Payload 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, ]), ); }); it('should serialize a start packet with only the e2ee extension', () => { const header = new DataTrackPacketHeader({ marker: FrameMarker.Start, trackHandle: DataTrackHandle.fromNumber(101), sequence: WrapAroundUnsignedInt.u16(102), frameNumber: WrapAroundUnsignedInt.u16(103), timestamp: DataTrackTimestamp.fromRtpTicks(104), extensions: new DataTrackExtensions({ e2ee: new DataTrackE2eeExtension(0xfa, new Uint8Array(12).fill(0x3c)), }), }); const payloadBytes = new Uint8Array(32).fill(0xfa); const packet = new DataTrackPacket(header, payloadBytes); expect(packet.toBinaryLengthBytes()).toStrictEqual(62); expect(packet.toBinary()).toStrictEqual( new Uint8Array([ 0x14, // Version 0, start, extension 0, // Reserved 0, // Track handle (big endian) 101, 0, // Sequence (big endian) 102, 0, // Frame number (big endian) 103, 0, // Timestamp (big endian) 0, 0, 104, 0, // RTP oriented extension words (big endian) 3, // E2ee extension 1, // ID 1 13, // Length 13 0xfa, // Key index 0x3c, // Iv array 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0, // Extension padding 0xfa, // Payload 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, 0xfa, ]), ); }); it('should be unable to serialize a packet header into a DataView which is too small', () => { const header = new DataTrackPacketHeader({ marker: FrameMarker.Single, trackHandle: DataTrackHandle.fromNumber(101), sequence: WrapAroundUnsignedInt.u16(102), frameNumber: WrapAroundUnsignedInt.u16(103), timestamp: DataTrackTimestamp.fromRtpTicks(104), }); const payloadBytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); const packet = new DataTrackPacket(header, payloadBytes); const twoByteLongDataView = new DataView(new ArrayBuffer(2)); expect(() => packet.toBinaryInto(twoByteLongDataView)).toThrow('Buffer cannot fit header'); }); it('should be unable to serialize a packet payload into a DataView which is too small', () => { const header = new DataTrackPacketHeader({ marker: FrameMarker.Single, trackHandle: DataTrackHandle.fromNumber(101), sequence: WrapAroundUnsignedInt.u16(102), frameNumber: WrapAroundUnsignedInt.u16(103), timestamp: DataTrackTimestamp.fromRtpTicks(104), }); const payloadBytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); const packet = new DataTrackPacket(header, payloadBytes); const fourteenByteLongDataView = new DataView( new ArrayBuffer(14 /* 12 byte header + 2 extra bytes */), ); expect(() => packet.toBinaryInto(fourteenByteLongDataView)).toThrow( 'Buffer cannot fit payload', ); }); }); describe('Deserialization', () => { const VALID_PACKET_BYTES = [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]; it('should deserialize a single packet', () => { const [packet, bytes] = DataTrackPacket.fromBinary( new Uint8Array([ 0x18, // Version 0, single, extension 0, // Reserved 0, // Track handle (big endian) 101, 0, // Sequence (big endian) 102, 0, // Frame number (big endian) 103, 0, // Timestamp (big endian) 0, 0, 104, /* (No extension words value) */ 1, // Payload 2, 3, 4, 5, 6, 7, 8, 9, ]), ); expect(bytes).toStrictEqual(21); expect(packet.toJSON()).toStrictEqual({ header: { frameNumber: 103, marker: FrameMarker.Single, sequence: 102, timestamp: 104, trackHandle: 101, extensions: { e2ee: null, userTimestamp: null, }, }, payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]), }); }); it('should fail to deserialize a too short buffer', () => { const packetBytes = new Uint8Array(VALID_PACKET_BYTES); expect(() => DataTrackPacket.fromBinary(packetBytes.slice(0, 5))).toThrow( 'Too short to contain a valid header', ); }); it('should fail to deserialize a packet including extensions but missing the ext words value', () => { const packetBytes = new Uint8Array(VALID_PACKET_BYTES); packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag - should have ext word indicator here expect(() => DataTrackPacket.fromBinary(packetBytes)).toThrow( 'Extension word indicator is missing', ); }); it('should fail to deserialize a packet which overruns headers', () => { const packetBytes = new Uint8Array([ ...VALID_PACKET_BYTES, 0, // Extension word (big endian) 1, ]); packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag - should have ext word indicator here expect(() => DataTrackPacket.fromBinary(packetBytes)).toThrow( 'Header exceeds total packet length', ); }); it('should fail to deserialize a packet with an unsupported version', () => { const packetBytes = new Uint8Array(VALID_PACKET_BYTES); packetBytes[0] = 0x20; // Version 1 (not supported yet) expect(() => DataTrackPacket.fromBinary(packetBytes)).toThrow('Unsupported version 1'); }); it('should deserialize base header', () => { const [packet, bytes] = DataTrackPacket.fromBinary( new Uint8Array([ 0x8, // Version 0, final, extension 0x0, // Reserved 0x88, // Track handle (big endian) 0x11, 0x44, // Sequence (big endian) 0x22, 0x44, // Frame number (big endian) 0x11, 0x44, // Timestamp (big endian) 0x22, 0x11, 0x88, ]), ); expect(bytes).toStrictEqual(12); expect(packet.toJSON()).toStrictEqual({ header: { marker: FrameMarker.Final, trackHandle: 0x8811, sequence: 0x4422, frameNumber: 0x4411, timestamp: 0x44221188, extensions: { e2ee: null, userTimestamp: null, }, }, payload: new Uint8Array([]), }); }); it.each([0, 1, 24])('should skip extension padding', (extensionWords) => { const packetBytes = new Uint8Array([ ...VALID_PACKET_BYTES, 0, // Extension words (big endian) extensionWords, ...new Array((extensionWords + 1) /* RTP oriented extension words */ * 4).fill(0), // Padding ]); packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag const [packet] = DataTrackPacket.fromBinary(packetBytes); expect(new Uint8Array(packet.toJSON().payload).byteLength).toStrictEqual(0); }); it('should deserialize e2ee extension properly', () => { const packetBytes = new Uint8Array([ ...VALID_PACKET_BYTES, 0, // RTP oriented extension words (big endian) 3, // E2ee extension 1, // ID 1 12, // Length 12 0xfa, // Key index 0x3c, // Iv array 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0, // Padding ]); packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag const [packet] = DataTrackPacket.fromBinary(packetBytes); expect(packet.toJSON().header.extensions.e2ee).toStrictEqual({ tag: DataTrackExtensionTag.E2ee, lengthBytes: 13, keyIndex: 0xfa, iv: new Uint8Array(12).fill(0x3c), }); }); it('should deserialize user timestamp extension properly', () => { const packetBytes = new Uint8Array([ ...VALID_PACKET_BYTES, 0, // Extension words (big endian) 2, // User timestamp extension 2, // ID 2 7, // Length 7 0x44, // Timestamp (big endian) 0x11, 0x22, 0x11, 0x11, 0x11, 0x88, 0x11, 0, // Padding 0, ]); packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag const [packet] = DataTrackPacket.fromBinary(packetBytes); expect(packet.toJSON().header.extensions.userTimestamp).toStrictEqual({ tag: DataTrackExtensionTag.UserTimestamp, lengthBytes: 8, timestamp: 0x4411221111118811n, }); }); it('should deserialize unknown extension properly', () => { const packetBytes = new Uint8Array([ ...VALID_PACKET_BYTES, 0, // RTP oriented extension words (big endian) 2, // Unknown / potential future extension 8, // ID 8 6, // Length 6 0x1, // Payload 0x2, 0x3, 0x4, 0x5, 0x6, 0x0, 0x0, // Padding 0x0, 0x0, ]); packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag const [packet] = DataTrackPacket.fromBinary(packetBytes); expect(packet.toJSON().header.extensions).toStrictEqual({ userTimestamp: null, e2ee: null, }); }); it('should ensure extensions are word aligned', () => { const packetBytes = new Uint8Array([ ...VALID_PACKET_BYTES, 0, // RTP oriented extension words (big endian) 0, 0x0, // Padding, missing one byte 0x0, 0x0, ]); packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag expect(() => DataTrackPacket.fromBinary(packetBytes)).toThrow( 'Header exceeds total packet length', ); }); }); describe('Round trip serialization + deserialization', () => { it('should serialize a single packet', () => { const header = new DataTrackPacketHeader({ marker: FrameMarker.Single, trackHandle: DataTrackHandle.fromNumber(101), sequence: WrapAroundUnsignedInt.u16(102), frameNumber: WrapAroundUnsignedInt.u16(103), timestamp: DataTrackTimestamp.fromRtpTicks(104), }); const payloadBytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]); const encodedPacket = new DataTrackPacket(header, payloadBytes); expect(encodedPacket.toBinaryLengthBytes()).toStrictEqual(21); expect(encodedPacket.toBinary()).toStrictEqual( new Uint8Array([ 0x18, // Version 0, single, extension 0, // Reserved 0, // Track handle (big endian) 101, 0, // Sequence (big endian) 102, 0, // Frame number (big endian) 103, 0, // Timestamp (big endian) 0, 0, 104, /* (No extension words value) */ 1, // Payload 2, 3, 4, 5, 6, 7, 8, 9, ]), ); const [decodedPacket, bytes] = DataTrackPacket.fromBinary(encodedPacket.toBinary()); expect(bytes).toStrictEqual(21); expect(decodedPacket.toJSON()).toStrictEqual({ header: { frameNumber: 103, marker: FrameMarker.Single, sequence: 102, timestamp: 104, trackHandle: 101, extensions: { e2ee: null, userTimestamp: null, }, }, payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]), }); }); }); });