livekit-client
Version:
JavaScript/TypeScript client SDK for LiveKit
605 lines (537 loc) • 16.5 kB
text/typescript
/* 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]),
});
});
});
});