livekit-client
Version:
JavaScript/TypeScript client SDK for LiveKit
130 lines (109 loc) • 4.28 kB
text/typescript
import type { Throws } from '@livekit/throws-transformer/throws';
import { LivekitReasonedError } from '../errors';
import { type DataTrackFrameInternal } from './frame';
import { DataTrackHandle } from './handle';
import { DataTrackPacket, DataTrackPacketHeader, FrameMarker } from './packet';
import { DataTrackClock, DataTrackTimestamp, WrapAroundUnsignedInt } from './utils';
type PacketizeOptions = {
/** "now" timestamp to use as a base when generating new packet timestamps. If not specified,
* defaults to the return value of {@link DataTrackClock#now}. */
now?: DataTrackTimestamp<90_000>;
};
export class DataTrackPacketizerError<
Reason extends DataTrackPacketizerReason = DataTrackPacketizerReason,
> extends LivekitReasonedError<Reason> {
readonly name = 'DataTrackPacketizerError';
reason: Reason;
reasonName: string;
constructor(message: string, reason: Reason, options?: { cause?: unknown }) {
super(19, message, options);
this.reason = reason;
this.reasonName = DataTrackPacketizerReason[reason];
}
static mtuTooShort() {
return new DataTrackPacketizerError(
'MTU is too short to send frame',
DataTrackPacketizerReason.MtuTooShort,
);
}
}
export enum DataTrackPacketizerReason {
MtuTooShort = 0,
}
/** A packetizer takes a {@link DataTrackFrameInternal} as input and generates a series
* of {@link DataTrackPacket}s for transmission to other clients over webrtc. */
export default class DataTrackPacketizer {
private handle: DataTrackHandle;
private mtuSizeBytes: number;
private sequence = WrapAroundUnsignedInt.u16(0);
private frameNumber = WrapAroundUnsignedInt.u16(0);
private clock = DataTrackClock.rtpStartingNow(DataTrackTimestamp.rtpRandom());
constructor(trackHandle: DataTrackHandle, mtuSizeBytes: number) {
this.handle = trackHandle;
this.mtuSizeBytes = mtuSizeBytes;
}
/** @internal */
static computeFrameMarker(index: number, packetCount: number) {
if (packetCount <= 1) {
return FrameMarker.Single;
}
if (index === 0) {
return FrameMarker.Start;
} else if (index === packetCount - 1) {
return FrameMarker.Final;
} else {
return FrameMarker.Inter;
}
}
/** Generates a series of packets for the specified {@link DataTrackFrameInternal}.
*
* NOTE: The return value of this function is a generator, so it can be lazily ran if desired,
* or converted to an array with {@link Array.from}.
*/
*packetize(
frame: DataTrackFrameInternal,
options?: PacketizeOptions,
): Throws<Generator<DataTrackPacket>, DataTrackPacketizerError> {
const frameNumber = this.frameNumber.getThenIncrement();
const headerParams = {
marker: FrameMarker.Inter,
trackHandle: this.handle,
sequence: WrapAroundUnsignedInt.u16(0),
frameNumber,
timestamp: options?.now ?? this.clock.now(),
extensions: frame.extensions,
};
const headerSerializedLengthBytes = new DataTrackPacketHeader(
headerParams,
).toBinaryLengthBytes();
if (headerSerializedLengthBytes >= this.mtuSizeBytes) {
throw DataTrackPacketizerError.mtuTooShort();
}
const maxPayloadSizeBytes = this.mtuSizeBytes - headerSerializedLengthBytes;
const packetCount = Math.ceil(frame.payload.byteLength / maxPayloadSizeBytes);
for (
let index = 0, indexBytes = 0;
indexBytes < frame.payload.byteLength;
[index, indexBytes] = [index + 1, indexBytes + maxPayloadSizeBytes]
) {
const sequence = this.sequence.getThenIncrement();
const packetHeader = new DataTrackPacketHeader({
...headerParams,
marker: DataTrackPacketizer.computeFrameMarker(index, packetCount),
sequence,
});
const packetPayloadLengthBytes = Math.min(
// All but the last packet will be max length ...
maxPayloadSizeBytes,
// ... and the last packet will be as long as it needs to be to finish out the buffer.
frame.payload.byteLength - indexBytes,
);
const packetPayload = new Uint8Array(
frame.payload.buffer,
frame.payload.byteOffset + indexBytes,
packetPayloadLengthBytes,
);
yield new DataTrackPacket(packetHeader, packetPayload);
}
}
}