livekit-client 2.17.1 → 2.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +21 -14
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2087 -1920
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +2 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/KeyProvider.d.ts +2 -0
- package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
- package/dist/src/e2ee/events.d.ts +1 -1
- package/dist/src/e2ee/events.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +1 -0
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/index.d.ts +7 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +2 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -4
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts +2 -4
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-track/depacketizer.d.ts +51 -0
- package/dist/src/room/data-track/depacketizer.d.ts.map +1 -0
- package/dist/src/room/data-track/e2ee.d.ts +12 -0
- package/dist/src/room/data-track/e2ee.d.ts.map +1 -0
- package/dist/src/room/data-track/frame.d.ts +7 -0
- package/dist/src/room/data-track/frame.d.ts.map +1 -0
- package/dist/src/room/data-track/handle.d.ts +6 -7
- package/dist/src/room/data-track/handle.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +76 -0
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -0
- package/dist/src/room/data-track/outgoing/errors.d.ts +64 -0
- package/dist/src/room/data-track/outgoing/errors.d.ts.map +1 -0
- package/dist/src/room/data-track/outgoing/pipeline.d.ts +22 -0
- package/dist/src/room/data-track/outgoing/pipeline.d.ts.map +1 -0
- package/dist/src/room/data-track/outgoing/types.d.ts +31 -0
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -0
- package/dist/src/room/data-track/packet/index.d.ts +3 -3
- package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
- package/dist/src/room/data-track/packetizer.d.ts +43 -0
- package/dist/src/room/data-track/packetizer.d.ts.map +1 -0
- package/dist/src/room/data-track/track.d.ts +30 -0
- package/dist/src/room/data-track/track.d.ts.map +1 -0
- package/dist/src/room/data-track/utils.d.ts +34 -2
- package/dist/src/room/data-track/utils.d.ts.map +1 -1
- package/dist/src/room/debounce.d.ts +11 -0
- package/dist/src/room/debounce.d.ts.map +1 -0
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +2 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +0 -2
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +6 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/utils/subscribeToEvents.d.ts +12 -0
- package/dist/src/utils/subscribeToEvents.d.ts.map +1 -0
- package/dist/src/utils/throws.d.ts +4 -2
- package/dist/src/utils/throws.d.ts.map +1 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +2 -0
- package/dist/ts4.2/e2ee/KeyProvider.d.ts +2 -0
- package/dist/ts4.2/e2ee/events.d.ts +1 -1
- package/dist/ts4.2/e2ee/types.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
- package/dist/ts4.2/index.d.ts +7 -3
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/room/PCTransport.d.ts +1 -6
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +2 -4
- package/dist/ts4.2/room/data-track/depacketizer.d.ts +51 -0
- package/dist/ts4.2/room/data-track/e2ee.d.ts +12 -0
- package/dist/ts4.2/room/data-track/frame.d.ts +7 -0
- package/dist/ts4.2/room/data-track/handle.d.ts +6 -7
- package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +77 -0
- package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +64 -0
- package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +22 -0
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +31 -0
- package/dist/ts4.2/room/data-track/packet/index.d.ts +3 -3
- package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -0
- package/dist/ts4.2/room/data-track/track.d.ts +30 -0
- package/dist/ts4.2/room/data-track/utils.d.ts +34 -2
- package/dist/ts4.2/room/debounce.d.ts +11 -0
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/ts4.2/room/track/LocalTrack.d.ts +2 -1
- package/dist/ts4.2/room/types.d.ts +0 -2
- package/dist/ts4.2/room/utils.d.ts +6 -1
- package/dist/ts4.2/utils/subscribeToEvents.d.ts +12 -0
- package/dist/ts4.2/utils/throws.d.ts +4 -2
- package/package.json +4 -5
- package/src/e2ee/E2eeManager.ts +9 -5
- package/src/e2ee/KeyProvider.ts +10 -1
- package/src/e2ee/events.ts +1 -1
- package/src/e2ee/types.ts +1 -0
- package/src/e2ee/worker/ParticipantKeyHandler.ts +7 -4
- package/src/e2ee/worker/e2ee.worker.ts +20 -10
- package/src/index.ts +15 -5
- package/src/logger.ts +1 -0
- package/src/room/PCTransport.ts +2 -1
- package/src/room/PCTransportManager.ts +27 -9
- package/src/room/RTCEngine.ts +13 -2
- package/src/room/Room.ts +11 -5
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -25
- package/src/room/data-stream/incoming/StreamReader.ts +56 -73
- package/src/room/data-track/depacketizer.test.ts +442 -0
- package/src/room/data-track/depacketizer.ts +298 -0
- package/src/room/data-track/e2ee.ts +14 -0
- package/src/room/data-track/frame.ts +8 -0
- package/src/room/data-track/handle.test.ts +1 -1
- package/src/room/data-track/handle.ts +9 -14
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +392 -0
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +302 -0
- package/src/room/data-track/outgoing/errors.ts +157 -0
- package/src/room/data-track/outgoing/pipeline.ts +76 -0
- package/src/room/data-track/outgoing/types.ts +37 -0
- package/src/room/data-track/packet/index.test.ts +9 -9
- package/src/room/data-track/packet/index.ts +11 -9
- package/src/room/data-track/packet/serializable.ts +1 -1
- package/src/room/data-track/packetizer.test.ts +131 -0
- package/src/room/data-track/packetizer.ts +132 -0
- package/src/room/data-track/track.ts +50 -0
- package/src/room/data-track/utils.test.ts +27 -1
- package/src/room/data-track/utils.ts +125 -5
- package/src/room/debounce.ts +115 -0
- package/src/room/events.ts +1 -1
- package/src/room/participant/LocalParticipant.ts +2 -0
- package/src/room/track/LocalAudioTrack.ts +10 -10
- package/src/room/track/LocalTrack.ts +14 -5
- package/src/room/track/LocalVideoTrack.ts +1 -1
- package/src/room/track/RemoteVideoTrack.ts +1 -1
- package/src/room/types.ts +0 -2
- package/src/room/utils.ts +7 -2
- package/src/utils/subscribeToEvents.ts +63 -0
- package/src/utils/throws.ts +3 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { LivekitReasonedError } from '../../errors';
|
|
2
|
+
import { DataTrackPacketizerError, DataTrackPacketizerReason } from '../packetizer';
|
|
3
|
+
|
|
4
|
+
export enum DataTrackPublishErrorReason {
|
|
5
|
+
/**
|
|
6
|
+
* Local participant does not have permission to publish data tracks.
|
|
7
|
+
*
|
|
8
|
+
* Ensure the participant's token contains the `canPublishData` grant.
|
|
9
|
+
*/
|
|
10
|
+
NotAllowed = 0,
|
|
11
|
+
|
|
12
|
+
/** A track with the same name is already published by the local participant. */
|
|
13
|
+
DuplicateName = 1,
|
|
14
|
+
|
|
15
|
+
/** Request to publish the track took long to complete. */
|
|
16
|
+
Timeout = 2,
|
|
17
|
+
|
|
18
|
+
/** No additional data tracks can be published by the local participant. */
|
|
19
|
+
LimitReached = 3,
|
|
20
|
+
|
|
21
|
+
/** Cannot publish data track when the room is disconnected. */
|
|
22
|
+
Disconnected = 4,
|
|
23
|
+
|
|
24
|
+
// NOTE: this was introduced by web / there isn't a corresponding case in the rust version.
|
|
25
|
+
Cancelled = 5,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class DataTrackPublishError<
|
|
29
|
+
Reason extends DataTrackPublishErrorReason,
|
|
30
|
+
> extends LivekitReasonedError<Reason> {
|
|
31
|
+
readonly name = 'DataTrackPublishError';
|
|
32
|
+
|
|
33
|
+
reason: Reason;
|
|
34
|
+
|
|
35
|
+
reasonName: string;
|
|
36
|
+
|
|
37
|
+
constructor(message: string, reason: Reason, options?: { cause?: unknown }) {
|
|
38
|
+
super(21, message, options);
|
|
39
|
+
this.reason = reason;
|
|
40
|
+
this.reasonName = DataTrackPublishErrorReason[reason];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static notAllowed() {
|
|
44
|
+
return new DataTrackPublishError(
|
|
45
|
+
'Data track publishing unauthorized',
|
|
46
|
+
DataTrackPublishErrorReason.NotAllowed,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static duplicateName() {
|
|
51
|
+
return new DataTrackPublishError(
|
|
52
|
+
'Track name already taken',
|
|
53
|
+
DataTrackPublishErrorReason.DuplicateName,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static timeout() {
|
|
58
|
+
return new DataTrackPublishError(
|
|
59
|
+
'Publish data track timed-out',
|
|
60
|
+
DataTrackPublishErrorReason.Timeout,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static limitReached() {
|
|
65
|
+
return new DataTrackPublishError(
|
|
66
|
+
'Data track publication limit reached',
|
|
67
|
+
DataTrackPublishErrorReason.LimitReached,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static disconnected() {
|
|
72
|
+
return new DataTrackPublishError('Room disconnected', DataTrackPublishErrorReason.Disconnected);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// NOTE: this was introduced by web / there isn't a corresponding case in the rust version.
|
|
76
|
+
static cancelled() {
|
|
77
|
+
return new DataTrackPublishError(
|
|
78
|
+
'Publish data track cancelled by caller',
|
|
79
|
+
DataTrackPublishErrorReason.Cancelled,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export enum DataTrackPushFrameErrorReason {
|
|
85
|
+
/** Track is no longer published. */
|
|
86
|
+
TrackUnpublished = 0,
|
|
87
|
+
/** Frame was dropped. */
|
|
88
|
+
// NOTE: this should become a web specific error, the rust version of this "dropped" error means
|
|
89
|
+
// something different and will be renamed to "QueueFull".
|
|
90
|
+
Dropped = 1,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class DataTrackPushFrameError<
|
|
94
|
+
Reason extends DataTrackPushFrameErrorReason,
|
|
95
|
+
> extends LivekitReasonedError<Reason> {
|
|
96
|
+
readonly name = 'DataTrackPushFrameError';
|
|
97
|
+
|
|
98
|
+
reason: Reason;
|
|
99
|
+
|
|
100
|
+
reasonName: string;
|
|
101
|
+
|
|
102
|
+
constructor(message: string, reason: Reason, options?: { cause?: unknown }) {
|
|
103
|
+
super(22, message, options);
|
|
104
|
+
this.reason = reason;
|
|
105
|
+
this.reasonName = DataTrackPushFrameErrorReason[reason];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static trackUnpublished() {
|
|
109
|
+
return new DataTrackPushFrameError(
|
|
110
|
+
'Track is no longer published',
|
|
111
|
+
DataTrackPushFrameErrorReason.TrackUnpublished,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static dropped(cause: unknown) {
|
|
116
|
+
return new DataTrackPushFrameError('Frame was dropped', DataTrackPushFrameErrorReason.Dropped, {
|
|
117
|
+
cause,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export enum DataTrackOutgoingPipelineErrorReason {
|
|
123
|
+
Packetizer = 0,
|
|
124
|
+
Encryption = 1,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class DataTrackOutgoingPipelineError<
|
|
128
|
+
Reason extends DataTrackOutgoingPipelineErrorReason,
|
|
129
|
+
> extends LivekitReasonedError<Reason> {
|
|
130
|
+
readonly name = 'DataTrackOutgoingPipelineError';
|
|
131
|
+
|
|
132
|
+
reason: Reason;
|
|
133
|
+
|
|
134
|
+
reasonName: string;
|
|
135
|
+
|
|
136
|
+
constructor(message: string, reason: Reason, options?: { cause?: unknown }) {
|
|
137
|
+
super(21, message, options);
|
|
138
|
+
this.reason = reason;
|
|
139
|
+
this.reasonName = DataTrackOutgoingPipelineErrorReason[reason];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static packetizer(cause: DataTrackPacketizerError<DataTrackPacketizerReason.MtuTooShort>) {
|
|
143
|
+
return new DataTrackOutgoingPipelineError(
|
|
144
|
+
'Error packetizing frame',
|
|
145
|
+
DataTrackOutgoingPipelineErrorReason.Packetizer,
|
|
146
|
+
{ cause },
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static encryption(cause: unknown) {
|
|
151
|
+
return new DataTrackOutgoingPipelineError(
|
|
152
|
+
'Error encrypting frame',
|
|
153
|
+
DataTrackOutgoingPipelineErrorReason.Encryption,
|
|
154
|
+
{ cause },
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Throws } from '../../../utils/throws';
|
|
2
|
+
import { type EncryptedPayload, type EncryptionProvider } from '../e2ee';
|
|
3
|
+
import { type DataTrackFrame } from '../frame';
|
|
4
|
+
import { DataTrackPacket } from '../packet';
|
|
5
|
+
import { DataTrackE2eeExtension } from '../packet/extensions';
|
|
6
|
+
import DataTrackPacketizer, { DataTrackPacketizerError } from '../packetizer';
|
|
7
|
+
import type { DataTrackInfo } from '../track';
|
|
8
|
+
import { DataTrackOutgoingPipelineError, DataTrackOutgoingPipelineErrorReason } from './errors';
|
|
9
|
+
|
|
10
|
+
type Options = {
|
|
11
|
+
info: DataTrackInfo;
|
|
12
|
+
encryptionProvider: EncryptionProvider | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Processes outgoing frames into final packets for distribution to the SFU. */
|
|
16
|
+
export default class DataTrackOutgoingPipeline {
|
|
17
|
+
private encryptionProvider: EncryptionProvider | null;
|
|
18
|
+
|
|
19
|
+
private packetizer: DataTrackPacketizer;
|
|
20
|
+
|
|
21
|
+
/** Maximum transmission unit (MTU) of the transport. */
|
|
22
|
+
private static TRANSPORT_MTU_BYTES = 16_000;
|
|
23
|
+
|
|
24
|
+
constructor(options: Options) {
|
|
25
|
+
this.encryptionProvider = options.encryptionProvider;
|
|
26
|
+
this.packetizer = new DataTrackPacketizer(
|
|
27
|
+
options.info.pubHandle,
|
|
28
|
+
DataTrackOutgoingPipeline.TRANSPORT_MTU_BYTES,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
*processFrame(
|
|
33
|
+
frame: DataTrackFrame,
|
|
34
|
+
): Throws<
|
|
35
|
+
Generator<DataTrackPacket>,
|
|
36
|
+
| DataTrackOutgoingPipelineError<DataTrackOutgoingPipelineErrorReason.Packetizer>
|
|
37
|
+
| DataTrackOutgoingPipelineError<DataTrackOutgoingPipelineErrorReason.Encryption>
|
|
38
|
+
> {
|
|
39
|
+
const encryptedFrame = this.encryptIfNeeded(frame);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
yield* this.packetizer.packetize(encryptedFrame);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof DataTrackPacketizerError) {
|
|
45
|
+
throw DataTrackOutgoingPipelineError.packetizer(error);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
encryptIfNeeded(
|
|
52
|
+
frame: DataTrackFrame,
|
|
53
|
+
): Throws<
|
|
54
|
+
DataTrackFrame,
|
|
55
|
+
DataTrackOutgoingPipelineError<DataTrackOutgoingPipelineErrorReason.Encryption>
|
|
56
|
+
> {
|
|
57
|
+
if (!this.encryptionProvider) {
|
|
58
|
+
return frame;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let encryptedResult: EncryptedPayload;
|
|
62
|
+
try {
|
|
63
|
+
encryptedResult = this.encryptionProvider.encrypt(frame.payload);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
throw DataTrackOutgoingPipelineError.encryption(err);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
frame.payload = encryptedResult.payload;
|
|
69
|
+
frame.extensions.e2ee = new DataTrackE2eeExtension(
|
|
70
|
+
encryptedResult.keyIndex,
|
|
71
|
+
encryptedResult.iv,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return frame;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type DataTrackHandle } from '../handle';
|
|
2
|
+
import { type DataTrackInfo } from '../track';
|
|
3
|
+
import { type DataTrackPublishError, type DataTrackPublishErrorReason } from './errors';
|
|
4
|
+
|
|
5
|
+
/** Options for publishing a data track. */
|
|
6
|
+
export type DataTrackOptions = {
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Encodes whether a data track publish request to the SFU has been successful or not. */
|
|
11
|
+
export type SfuPublishResponseResult =
|
|
12
|
+
| { type: 'ok'; data: DataTrackInfo }
|
|
13
|
+
| {
|
|
14
|
+
type: 'error';
|
|
15
|
+
error:
|
|
16
|
+
| DataTrackPublishError<DataTrackPublishErrorReason.NotAllowed>
|
|
17
|
+
| DataTrackPublishError<DataTrackPublishErrorReason.DuplicateName>
|
|
18
|
+
| DataTrackPublishError<DataTrackPublishErrorReason.LimitReached>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Request sent to the SFU to publish a track. */
|
|
22
|
+
export type OutputEventSfuPublishRequest = {
|
|
23
|
+
handle: DataTrackHandle;
|
|
24
|
+
name: string;
|
|
25
|
+
usesE2ee: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Request sent to the SFU to unpublish a track. */
|
|
29
|
+
export type OutputEventSfuUnpublishRequest = {
|
|
30
|
+
handle: DataTrackHandle;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Serialized packets are ready to be sent over the transport. */
|
|
34
|
+
export type OutputEventPacketsAvailable = {
|
|
35
|
+
bytes: Uint8Array;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
};
|
|
@@ -24,7 +24,7 @@ describe('DataTrackPacket', () => {
|
|
|
24
24
|
|
|
25
25
|
const payloadBytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
26
26
|
|
|
27
|
-
const packet = new DataTrackPacket(header, payloadBytes
|
|
27
|
+
const packet = new DataTrackPacket(header, payloadBytes);
|
|
28
28
|
|
|
29
29
|
expect(packet.toBinaryLengthBytes()).toStrictEqual(22);
|
|
30
30
|
expect(packet.toBinary()).toStrictEqual(
|
|
@@ -70,7 +70,7 @@ describe('DataTrackPacket', () => {
|
|
|
70
70
|
|
|
71
71
|
const payloadBytes = new Uint8Array(32).fill(0xfa);
|
|
72
72
|
|
|
73
|
-
const packet = new DataTrackPacket(header, payloadBytes
|
|
73
|
+
const packet = new DataTrackPacket(header, payloadBytes);
|
|
74
74
|
|
|
75
75
|
expect(packet.toBinaryLengthBytes()).toStrictEqual(78);
|
|
76
76
|
expect(packet.toBinary()).toStrictEqual(
|
|
@@ -176,7 +176,7 @@ describe('DataTrackPacket', () => {
|
|
|
176
176
|
|
|
177
177
|
const payloadBytes = new Uint8Array(32).fill(0xfa);
|
|
178
178
|
|
|
179
|
-
const packet = new DataTrackPacket(header, payloadBytes
|
|
179
|
+
const packet = new DataTrackPacket(header, payloadBytes);
|
|
180
180
|
|
|
181
181
|
expect(packet.toBinaryLengthBytes()).toStrictEqual(66);
|
|
182
182
|
expect(packet.toBinary()).toStrictEqual(
|
|
@@ -264,7 +264,7 @@ describe('DataTrackPacket', () => {
|
|
|
264
264
|
timestamp: DataTrackTimestamp.fromRtpTicks(104),
|
|
265
265
|
});
|
|
266
266
|
const payloadBytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
267
|
-
const packet = new DataTrackPacket(header, payloadBytes
|
|
267
|
+
const packet = new DataTrackPacket(header, payloadBytes);
|
|
268
268
|
|
|
269
269
|
const twoByteLongDataView = new DataView(new ArrayBuffer(2));
|
|
270
270
|
expect(() => packet.toBinaryInto(twoByteLongDataView)).toThrow('Buffer cannot fit header');
|
|
@@ -278,7 +278,7 @@ describe('DataTrackPacket', () => {
|
|
|
278
278
|
timestamp: DataTrackTimestamp.fromRtpTicks(104),
|
|
279
279
|
});
|
|
280
280
|
const payloadBytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
281
|
-
const packet = new DataTrackPacket(header, payloadBytes
|
|
281
|
+
const packet = new DataTrackPacket(header, payloadBytes);
|
|
282
282
|
|
|
283
283
|
const fourteenByteLongDataView = new DataView(
|
|
284
284
|
new ArrayBuffer(14 /* 12 byte header + 2 extra bytes */),
|
|
@@ -333,7 +333,7 @@ describe('DataTrackPacket', () => {
|
|
|
333
333
|
userTimestamp: null,
|
|
334
334
|
},
|
|
335
335
|
},
|
|
336
|
-
payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9])
|
|
336
|
+
payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
|
337
337
|
});
|
|
338
338
|
});
|
|
339
339
|
|
|
@@ -406,7 +406,7 @@ describe('DataTrackPacket', () => {
|
|
|
406
406
|
userTimestamp: null,
|
|
407
407
|
},
|
|
408
408
|
},
|
|
409
|
-
payload: new Uint8Array([])
|
|
409
|
+
payload: new Uint8Array([]),
|
|
410
410
|
});
|
|
411
411
|
});
|
|
412
412
|
|
|
@@ -563,7 +563,7 @@ describe('DataTrackPacket', () => {
|
|
|
563
563
|
|
|
564
564
|
const payloadBytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
565
565
|
|
|
566
|
-
const encodedPacket = new DataTrackPacket(header, payloadBytes
|
|
566
|
+
const encodedPacket = new DataTrackPacket(header, payloadBytes);
|
|
567
567
|
|
|
568
568
|
expect(encodedPacket.toBinaryLengthBytes()).toStrictEqual(21);
|
|
569
569
|
expect(encodedPacket.toBinary()).toStrictEqual(
|
|
@@ -608,7 +608,7 @@ describe('DataTrackPacket', () => {
|
|
|
608
608
|
userTimestamp: null,
|
|
609
609
|
},
|
|
610
610
|
},
|
|
611
|
-
payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9])
|
|
611
|
+
payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
|
612
612
|
});
|
|
613
613
|
});
|
|
614
614
|
});
|
|
@@ -126,7 +126,7 @@ export class DataTrackPacketHeader extends Serializable {
|
|
|
126
126
|
dataView.setUint8(byteIndex, 0); // Reserved
|
|
127
127
|
byteIndex += U8_LENGTH_BYTES;
|
|
128
128
|
|
|
129
|
-
dataView.setUint16(byteIndex, this.trackHandle
|
|
129
|
+
dataView.setUint16(byteIndex, this.trackHandle);
|
|
130
130
|
byteIndex += U16_LENGTH_BYTES;
|
|
131
131
|
dataView.setUint16(byteIndex, this.sequence.value);
|
|
132
132
|
byteIndex += U16_LENGTH_BYTES;
|
|
@@ -277,10 +277,10 @@ export class DataTrackPacketHeader extends Serializable {
|
|
|
277
277
|
toJSON() {
|
|
278
278
|
return {
|
|
279
279
|
marker: this.marker,
|
|
280
|
-
trackHandle: this.trackHandle
|
|
280
|
+
trackHandle: this.trackHandle,
|
|
281
281
|
sequence: this.sequence.value,
|
|
282
282
|
frameNumber: this.frameNumber.value,
|
|
283
|
-
timestamp: this.timestamp.
|
|
283
|
+
timestamp: this.timestamp.asTicks(),
|
|
284
284
|
extensions: this.extensions.toJSON(),
|
|
285
285
|
};
|
|
286
286
|
}
|
|
@@ -302,9 +302,9 @@ export enum FrameMarker {
|
|
|
302
302
|
export class DataTrackPacket extends Serializable {
|
|
303
303
|
header: DataTrackPacketHeader;
|
|
304
304
|
|
|
305
|
-
payload:
|
|
305
|
+
payload: Uint8Array;
|
|
306
306
|
|
|
307
|
-
constructor(header: DataTrackPacketHeader, payload:
|
|
307
|
+
constructor(header: DataTrackPacketHeader, payload: Uint8Array) {
|
|
308
308
|
super();
|
|
309
309
|
this.header = header;
|
|
310
310
|
this.payload = payload;
|
|
@@ -323,9 +323,8 @@ export class DataTrackPacket extends Serializable {
|
|
|
323
323
|
throw DataTrackSerializeError.tooSmallForPayload();
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
dataView.setUint8(byteIndex, payloadBytes[index]);
|
|
326
|
+
for (let index = 0; index < this.payload.length; index += 1) {
|
|
327
|
+
dataView.setUint8(byteIndex, this.payload[index]);
|
|
329
328
|
byteIndex += U8_LENGTH_BYTES;
|
|
330
329
|
}
|
|
331
330
|
|
|
@@ -352,7 +351,10 @@ export class DataTrackPacket extends Serializable {
|
|
|
352
351
|
dataView.byteOffset + dataView.byteLength,
|
|
353
352
|
);
|
|
354
353
|
|
|
355
|
-
return [new DataTrackPacket(header, payload), dataView.byteLength] as [
|
|
354
|
+
return [new DataTrackPacket(header, new Uint8Array(payload)), dataView.byteLength] as [
|
|
355
|
+
DataTrackPacket,
|
|
356
|
+
number,
|
|
357
|
+
];
|
|
356
358
|
}
|
|
357
359
|
|
|
358
360
|
toJSON() {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { DataTrackFrame } from './frame';
|
|
4
|
+
import { DataTrackHandle } from './handle';
|
|
5
|
+
import { FrameMarker } from './packet';
|
|
6
|
+
import { DataTrackExtensions } from './packet/extensions';
|
|
7
|
+
import DataTrackPacketizer from './packetizer';
|
|
8
|
+
import { DataTrackTimestamp } from './utils';
|
|
9
|
+
|
|
10
|
+
describe('DataTrackPacketizer', () => {
|
|
11
|
+
it('should packetize a large payload properly', () => {
|
|
12
|
+
const packetizer = new DataTrackPacketizer(DataTrackHandle.fromNumber(1), 100);
|
|
13
|
+
const packets = Array.from(
|
|
14
|
+
packetizer.packetize(
|
|
15
|
+
{
|
|
16
|
+
payload: new Uint8Array(300).fill(0xbe),
|
|
17
|
+
extensions: new DataTrackExtensions(),
|
|
18
|
+
},
|
|
19
|
+
{ now: DataTrackTimestamp.fromRtpTicks(1804548298) },
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(packets.map((packet) => packet.toJSON())).toStrictEqual([
|
|
24
|
+
{
|
|
25
|
+
header: {
|
|
26
|
+
extensions: {
|
|
27
|
+
e2ee: null,
|
|
28
|
+
userTimestamp: null,
|
|
29
|
+
},
|
|
30
|
+
frameNumber: 0,
|
|
31
|
+
marker: FrameMarker.Start,
|
|
32
|
+
sequence: 0,
|
|
33
|
+
timestamp: 1804548298,
|
|
34
|
+
trackHandle: 1,
|
|
35
|
+
},
|
|
36
|
+
payload: new Uint8Array(88).fill(0xbe),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
header: {
|
|
40
|
+
extensions: {
|
|
41
|
+
e2ee: null,
|
|
42
|
+
userTimestamp: null,
|
|
43
|
+
},
|
|
44
|
+
frameNumber: 0,
|
|
45
|
+
marker: FrameMarker.Inter,
|
|
46
|
+
sequence: 1,
|
|
47
|
+
timestamp: 1804548298,
|
|
48
|
+
trackHandle: 1,
|
|
49
|
+
},
|
|
50
|
+
payload: new Uint8Array(88).fill(0xbe),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
header: {
|
|
54
|
+
extensions: {
|
|
55
|
+
e2ee: null,
|
|
56
|
+
userTimestamp: null,
|
|
57
|
+
},
|
|
58
|
+
frameNumber: 0,
|
|
59
|
+
marker: FrameMarker.Inter,
|
|
60
|
+
sequence: 2,
|
|
61
|
+
timestamp: 1804548298,
|
|
62
|
+
trackHandle: 1,
|
|
63
|
+
},
|
|
64
|
+
payload: new Uint8Array(88).fill(0xbe),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
header: {
|
|
68
|
+
extensions: {
|
|
69
|
+
e2ee: null,
|
|
70
|
+
userTimestamp: null,
|
|
71
|
+
},
|
|
72
|
+
frameNumber: 0,
|
|
73
|
+
marker: FrameMarker.Final,
|
|
74
|
+
sequence: 3,
|
|
75
|
+
timestamp: 1804548298,
|
|
76
|
+
trackHandle: 1,
|
|
77
|
+
},
|
|
78
|
+
payload: new Uint8Array(36 /* 300 total bytes - (88 bytes * 3 full length packets) */).fill(
|
|
79
|
+
0xbe,
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it.each([
|
|
86
|
+
[0, 1_024, 'zero payload'],
|
|
87
|
+
[128, 1_024, 'single packet'],
|
|
88
|
+
[20_480, 1_024, 'multi packet'],
|
|
89
|
+
[40_960, 16_000, 'multi packet mtu 16000'],
|
|
90
|
+
])('should test packetizer edge cases', (payloadSizeBytes, mtuSizeBytes, label) => {
|
|
91
|
+
const packetizer = new DataTrackPacketizer(DataTrackHandle.fromNumber(1), mtuSizeBytes);
|
|
92
|
+
|
|
93
|
+
const frame: DataTrackFrame = {
|
|
94
|
+
payload: new Uint8Array(payloadSizeBytes).fill(0xab),
|
|
95
|
+
extensions: new DataTrackExtensions(),
|
|
96
|
+
};
|
|
97
|
+
const packets = Array.from(
|
|
98
|
+
packetizer.packetize(frame, {
|
|
99
|
+
now: DataTrackTimestamp.fromRtpTicks(0),
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (packets.length === 0) {
|
|
104
|
+
expect(payloadSizeBytes, `${label}: Should be no packets for zero payload`).toStrictEqual(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let index = 0;
|
|
108
|
+
for (const packet of packets) {
|
|
109
|
+
const packetHeaderJson = packet.header.toJSON();
|
|
110
|
+
expect(packetHeaderJson.marker).toStrictEqual(
|
|
111
|
+
DataTrackPacketizer.computeFrameMarker(index, packets.length),
|
|
112
|
+
);
|
|
113
|
+
expect(packetHeaderJson.frameNumber).toStrictEqual(0);
|
|
114
|
+
expect(packetHeaderJson.trackHandle).toStrictEqual(1);
|
|
115
|
+
expect(packetHeaderJson.sequence).toStrictEqual(index);
|
|
116
|
+
expect(packetHeaderJson.extensions).toStrictEqual(frame.extensions.toJSON());
|
|
117
|
+
index += 1;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it.each([
|
|
122
|
+
[0, 1, FrameMarker.Single],
|
|
123
|
+
[0, 10, FrameMarker.Start],
|
|
124
|
+
[4, 10, FrameMarker.Inter],
|
|
125
|
+
[9, 10, FrameMarker.Final],
|
|
126
|
+
])('should test frame marker utility function', (index, packetCount, expectedMarker) => {
|
|
127
|
+
expect(DataTrackPacketizer.computeFrameMarker(index, packetCount)).toStrictEqual(
|
|
128
|
+
expectedMarker,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Throws } from '../../utils/throws';
|
|
2
|
+
import { LivekitReasonedError } from '../errors';
|
|
3
|
+
import { type DataTrackFrame } from './frame';
|
|
4
|
+
import { DataTrackHandle } from './handle';
|
|
5
|
+
import { DataTrackPacket, DataTrackPacketHeader, FrameMarker } from './packet';
|
|
6
|
+
import { DataTrackClock, DataTrackTimestamp, WrapAroundUnsignedInt } from './utils';
|
|
7
|
+
|
|
8
|
+
type PacketizeOptions = {
|
|
9
|
+
/** "now" timestamp to use as a base when generating new packet timestamps. If not specified,
|
|
10
|
+
* defaults to the return value of {@link DataTrackClock#now}. */
|
|
11
|
+
now?: DataTrackTimestamp<90_000>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class DataTrackPacketizerError<
|
|
15
|
+
Reason extends DataTrackPacketizerReason,
|
|
16
|
+
> extends LivekitReasonedError<Reason> {
|
|
17
|
+
readonly name = 'DataTrackPacketizerError';
|
|
18
|
+
|
|
19
|
+
reason: Reason;
|
|
20
|
+
|
|
21
|
+
reasonName: string;
|
|
22
|
+
|
|
23
|
+
constructor(message: string, reason: Reason, options?: { cause?: unknown }) {
|
|
24
|
+
super(19, message, options);
|
|
25
|
+
this.reason = reason;
|
|
26
|
+
this.reasonName = DataTrackPacketizerReason[reason];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static mtuTooShort() {
|
|
30
|
+
return new DataTrackPacketizerError(
|
|
31
|
+
'MTU is too short to send frame',
|
|
32
|
+
DataTrackPacketizerReason.MtuTooShort,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export enum DataTrackPacketizerReason {
|
|
38
|
+
MtuTooShort = 0,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A packetizer takes a {@link DataTrackFrame} as input and generates a series
|
|
42
|
+
* of {@link DataTrackPacket}s for transmission to other clients over webrtc. */
|
|
43
|
+
export default class DataTrackPacketizer {
|
|
44
|
+
private handle: DataTrackHandle;
|
|
45
|
+
|
|
46
|
+
private mtuSizeBytes: number;
|
|
47
|
+
|
|
48
|
+
private sequence = WrapAroundUnsignedInt.u16(0);
|
|
49
|
+
|
|
50
|
+
private frameNumber = WrapAroundUnsignedInt.u16(0);
|
|
51
|
+
|
|
52
|
+
private clock = DataTrackClock.rtpStartingNow(DataTrackTimestamp.rtpRandom());
|
|
53
|
+
|
|
54
|
+
constructor(trackHandle: DataTrackHandle, mtuSizeBytes: number) {
|
|
55
|
+
this.handle = trackHandle;
|
|
56
|
+
this.mtuSizeBytes = mtuSizeBytes;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @internal */
|
|
60
|
+
static computeFrameMarker(index: number, packetCount: number) {
|
|
61
|
+
if (packetCount <= 1) {
|
|
62
|
+
return FrameMarker.Single;
|
|
63
|
+
}
|
|
64
|
+
if (index === 0) {
|
|
65
|
+
return FrameMarker.Start;
|
|
66
|
+
} else if (index === packetCount - 1) {
|
|
67
|
+
return FrameMarker.Final;
|
|
68
|
+
} else {
|
|
69
|
+
return FrameMarker.Inter;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Generates a series of packets for the specified {@link DataTrackPacketizerFrame}.
|
|
74
|
+
*
|
|
75
|
+
* NOTE: The return value of this function is a generator, so it can be lazily ran if desired,
|
|
76
|
+
* or converted to an array with {@link Array.from}.
|
|
77
|
+
*/
|
|
78
|
+
*packetize(
|
|
79
|
+
frame: DataTrackFrame,
|
|
80
|
+
options?: PacketizeOptions,
|
|
81
|
+
): Throws<
|
|
82
|
+
Generator<DataTrackPacket>,
|
|
83
|
+
DataTrackPacketizerError<DataTrackPacketizerReason.MtuTooShort>
|
|
84
|
+
> {
|
|
85
|
+
const frameNumber = this.frameNumber.getThenIncrement();
|
|
86
|
+
const headerParams = {
|
|
87
|
+
marker: FrameMarker.Inter,
|
|
88
|
+
trackHandle: this.handle,
|
|
89
|
+
sequence: WrapAroundUnsignedInt.u16(0),
|
|
90
|
+
frameNumber,
|
|
91
|
+
timestamp: options?.now ?? this.clock.now(),
|
|
92
|
+
extensions: frame.extensions,
|
|
93
|
+
};
|
|
94
|
+
const headerSerializedLengthBytes = new DataTrackPacketHeader(
|
|
95
|
+
headerParams,
|
|
96
|
+
).toBinaryLengthBytes();
|
|
97
|
+
if (headerSerializedLengthBytes >= this.mtuSizeBytes) {
|
|
98
|
+
throw DataTrackPacketizerError.mtuTooShort();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const maxPayloadSizeBytes = this.mtuSizeBytes - headerSerializedLengthBytes;
|
|
102
|
+
|
|
103
|
+
const packetCount = Math.ceil(frame.payload.byteLength / maxPayloadSizeBytes);
|
|
104
|
+
|
|
105
|
+
for (
|
|
106
|
+
let index = 0, indexBytes = 0;
|
|
107
|
+
indexBytes < frame.payload.byteLength;
|
|
108
|
+
[index, indexBytes] = [index + 1, indexBytes + maxPayloadSizeBytes]
|
|
109
|
+
) {
|
|
110
|
+
const sequence = this.sequence.getThenIncrement();
|
|
111
|
+
const packetHeader = new DataTrackPacketHeader({
|
|
112
|
+
...headerParams,
|
|
113
|
+
marker: DataTrackPacketizer.computeFrameMarker(index, packetCount),
|
|
114
|
+
sequence,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const packetPayloadLengthBytes = Math.min(
|
|
118
|
+
// All but the last packet will be max length ...
|
|
119
|
+
maxPayloadSizeBytes,
|
|
120
|
+
// ... and the last packet will be as long as it needs to be to finish out the buffer.
|
|
121
|
+
frame.payload.byteLength - indexBytes,
|
|
122
|
+
);
|
|
123
|
+
const packetPayload = new Uint8Array(
|
|
124
|
+
frame.payload.buffer,
|
|
125
|
+
frame.payload.byteOffset + indexBytes,
|
|
126
|
+
packetPayloadLengthBytes,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
yield new DataTrackPacket(packetHeader, packetPayload);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|