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,50 @@
|
|
|
1
|
+
import type { DataTrackFrame } from './frame';
|
|
2
|
+
import { type DataTrackHandle } from './handle';
|
|
3
|
+
import type OutgoingDataTrackManager from './outgoing/OutgoingDataTrackManager';
|
|
4
|
+
|
|
5
|
+
export type DataTrackSid = string;
|
|
6
|
+
|
|
7
|
+
/** Information about a published data track. */
|
|
8
|
+
export type DataTrackInfo = {
|
|
9
|
+
sid: DataTrackSid;
|
|
10
|
+
pubHandle: DataTrackHandle;
|
|
11
|
+
name: String;
|
|
12
|
+
usesE2ee: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class LocalDataTrack {
|
|
16
|
+
info: DataTrackInfo;
|
|
17
|
+
|
|
18
|
+
protected manager: OutgoingDataTrackManager;
|
|
19
|
+
|
|
20
|
+
constructor(info: DataTrackInfo, manager: OutgoingDataTrackManager) {
|
|
21
|
+
this.info = info;
|
|
22
|
+
this.manager = manager;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The raw descriptor from the manager containing the internal state for this local track. */
|
|
26
|
+
protected get descriptor() {
|
|
27
|
+
return this.manager.getDescriptor(this.info.pubHandle);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
isPublished() {
|
|
31
|
+
return this.descriptor?.type === 'active';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Try pushing a frame to subscribers of the track.
|
|
35
|
+
*
|
|
36
|
+
* Pushing a frame can fail for several reasons:
|
|
37
|
+
*
|
|
38
|
+
* - The track has been unpublished by the local participant or SFU
|
|
39
|
+
* - The room is no longer connected
|
|
40
|
+
*/
|
|
41
|
+
tryPush(payload: DataTrackFrame['payload']) {
|
|
42
|
+
try {
|
|
43
|
+
return this.manager.tryProcessAndSend(this.info.pubHandle, payload);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// NOTE: wrapping in the bare try/catch like this means that the Throws<...> type doesn't
|
|
46
|
+
// propegate upwards into the public interface.
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { WrapAroundUnsignedInt } from './utils';
|
|
3
|
+
import { U16_MAX_SIZE, WrapAroundUnsignedInt } from './utils';
|
|
4
4
|
|
|
5
5
|
describe('WrapAroundUnsignedInt', () => {
|
|
6
6
|
it('should test initialization + edge cases', () => {
|
|
@@ -25,4 +25,30 @@ describe('WrapAroundUnsignedInt', () => {
|
|
|
25
25
|
n.update((v) => v - 1);
|
|
26
26
|
expect(n.value).toBe(65535);
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
it.each([
|
|
30
|
+
// Happy path
|
|
31
|
+
[5, 10, true],
|
|
32
|
+
[10, 5, false],
|
|
33
|
+
|
|
34
|
+
// Equality cases
|
|
35
|
+
[0, 0, false],
|
|
36
|
+
[7, 7, false],
|
|
37
|
+
[U16_MAX_SIZE + 1, U16_MAX_SIZE + 1, false],
|
|
38
|
+
|
|
39
|
+
// Boundary cases
|
|
40
|
+
[0, 2, true],
|
|
41
|
+
[U16_MAX_SIZE - 1, U16_MAX_SIZE, true],
|
|
42
|
+
|
|
43
|
+
// Wraparound cases
|
|
44
|
+
[1, U16_MAX_SIZE + 1 /* wraps around to 0 */, false],
|
|
45
|
+
[U16_MAX_SIZE + 1 /* wraps around to 0 */, 5, true],
|
|
46
|
+
[2, (U16_MAX_SIZE + 1) * 5 + 3 /* wraps around to 3 */, true],
|
|
47
|
+
[(U16_MAX_SIZE + 1) * 5 + 3 /* wraps around to 3 */, 5, true],
|
|
48
|
+
])('should ensure isBefore works', (first, second, result) => {
|
|
49
|
+
expect(
|
|
50
|
+
WrapAroundUnsignedInt.u16(first).isBefore(WrapAroundUnsignedInt.u16(second)),
|
|
51
|
+
`${first} isBefore ${second} != ${result}`,
|
|
52
|
+
).toStrictEqual(result);
|
|
53
|
+
});
|
|
28
54
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const U16_MAX_SIZE = 0xffff;
|
|
2
|
+
export const U32_MAX_SIZE = 0xffffffff;
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A number of fields withing the data tracks packet specification assume wrap around behavior when
|
|
@@ -15,6 +16,10 @@ export class WrapAroundUnsignedInt<MaxSize extends number> {
|
|
|
15
16
|
return new WrapAroundUnsignedInt(raw, U16_MAX_SIZE);
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
static u32(raw: number) {
|
|
20
|
+
return new WrapAroundUnsignedInt(raw, U32_MAX_SIZE);
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
constructor(raw: number, maxSize: MaxSize) {
|
|
19
24
|
this.value = raw;
|
|
20
25
|
if (raw < 0) {
|
|
@@ -42,31 +47,146 @@ export class WrapAroundUnsignedInt<MaxSize extends number> {
|
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
clone() {
|
|
51
|
+
return new WrapAroundUnsignedInt(this.value, this.maxSize);
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
/** When called, maps the containing value to a new containing value. After mapping, the wrap
|
|
46
|
-
* around external max size bounds are applied. */
|
|
55
|
+
* around external max size bounds are applied. Note that this is a mutative operation. */
|
|
47
56
|
update(updateFn: (value: number) => number) {
|
|
48
57
|
this.value = updateFn(this.value);
|
|
49
58
|
this.clamp();
|
|
50
59
|
}
|
|
60
|
+
|
|
61
|
+
/** Increments the given `n` to the inner value. Note that this is a mutative operation. */
|
|
62
|
+
increment(n = 1) {
|
|
63
|
+
this.update((value) => value + n);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Decrements the given `n` from the inner value. Note that this is a mutative operation. */
|
|
67
|
+
decrement(n = 1) {
|
|
68
|
+
this.update((value) => value - n);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getThenIncrement() {
|
|
72
|
+
const previousValue = this.value;
|
|
73
|
+
this.increment();
|
|
74
|
+
return new WrapAroundUnsignedInt(previousValue, this.maxSize);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Returns true if {@link this} is before the passed other {@link WrapAroundUnsignedInt}. */
|
|
78
|
+
isBefore(other: WrapAroundUnsignedInt<MaxSize>) {
|
|
79
|
+
const a = this.value >>> 0;
|
|
80
|
+
const b = other.value >>> 0;
|
|
81
|
+
const diff = (b - a) >>> 0;
|
|
82
|
+
return diff !== 0 && diff < this.maxSize + 1;
|
|
83
|
+
}
|
|
51
84
|
}
|
|
52
85
|
|
|
53
86
|
export class DataTrackTimestamp<RateInHz extends number> {
|
|
54
87
|
rateInHz: RateInHz;
|
|
55
88
|
|
|
56
|
-
timestamp:
|
|
89
|
+
timestamp: WrapAroundUnsignedInt<typeof U32_MAX_SIZE>;
|
|
57
90
|
|
|
58
91
|
static fromRtpTicks(rtpTicks: number) {
|
|
59
92
|
return new DataTrackTimestamp(rtpTicks, 90_000);
|
|
60
93
|
}
|
|
61
94
|
|
|
62
|
-
|
|
63
|
-
|
|
95
|
+
/** Generates a timestamp initialized to a non cryptographically secure random value, so that
|
|
96
|
+
* different streams are more difficult to correlate in packet capture. */
|
|
97
|
+
static rtpRandom() {
|
|
98
|
+
const randomValue = Math.round(Math.random() * U32_MAX_SIZE);
|
|
99
|
+
return DataTrackTimestamp.fromRtpTicks(randomValue);
|
|
64
100
|
}
|
|
65
101
|
|
|
66
102
|
private constructor(raw: number, rateInHz: RateInHz) {
|
|
67
|
-
this.timestamp = raw;
|
|
103
|
+
this.timestamp = WrapAroundUnsignedInt.u32(raw);
|
|
104
|
+
this.rateInHz = rateInHz;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
asTicks() {
|
|
108
|
+
return this.timestamp.value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
clone() {
|
|
112
|
+
return new DataTrackTimestamp(this.timestamp.value, this.rateInHz);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
wrappingAdd(n: number) {
|
|
116
|
+
this.timestamp.increment(n);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Returns true if {@link this} is before the passed other {@link DataTrackTimestamp}. */
|
|
120
|
+
isBefore(other: DataTrackTimestamp<RateInHz>) {
|
|
121
|
+
return this.timestamp.isBefore(other.timestamp);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class DataTrackClock<RateInHz extends number> {
|
|
126
|
+
epoch: Date;
|
|
127
|
+
|
|
128
|
+
base: DataTrackTimestamp<RateInHz>;
|
|
129
|
+
|
|
130
|
+
previous: DataTrackTimestamp<RateInHz>;
|
|
131
|
+
|
|
132
|
+
rateInHz: RateInHz;
|
|
133
|
+
|
|
134
|
+
private constructor(rateInHz: RateInHz, epoch: Date, base: DataTrackTimestamp<RateInHz>) {
|
|
135
|
+
this.epoch = epoch;
|
|
136
|
+
this.base = base;
|
|
137
|
+
this.previous = base.clone();
|
|
68
138
|
this.rateInHz = rateInHz;
|
|
69
139
|
}
|
|
140
|
+
|
|
141
|
+
static startingNow<RateInHz extends number>(
|
|
142
|
+
base: DataTrackTimestamp<RateInHz>,
|
|
143
|
+
rateInHz: RateInHz,
|
|
144
|
+
) {
|
|
145
|
+
return new DataTrackClock(rateInHz, new Date(), base);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static startingAtTime<RateInHz extends number>(
|
|
149
|
+
epoch: Date,
|
|
150
|
+
base: DataTrackTimestamp<RateInHz>,
|
|
151
|
+
rateInHz: RateInHz,
|
|
152
|
+
) {
|
|
153
|
+
return new DataTrackClock(rateInHz, epoch, base);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static rtpStartingNow(base: DataTrackTimestamp<90_000>) {
|
|
157
|
+
return DataTrackClock.startingNow(base, 90_000);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static rtpStartingAtTime(epoch: Date, base: DataTrackTimestamp<90_000>) {
|
|
161
|
+
return DataTrackClock.startingAtTime(epoch, base, 90_000);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
now(): DataTrackTimestamp<RateInHz> {
|
|
165
|
+
return this.at(new Date());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
at(timestamp: Date) {
|
|
169
|
+
let elapsedMs = timestamp.getTime() - this.epoch.getTime();
|
|
170
|
+
let durationTicks = DataTrackClock.durationInMsToTicks(elapsedMs, this.rateInHz);
|
|
171
|
+
|
|
172
|
+
let result = this.base.clone();
|
|
173
|
+
result.wrappingAdd(durationTicks);
|
|
174
|
+
|
|
175
|
+
// Enforce monotonicity in RTP wraparound space
|
|
176
|
+
if (result.isBefore(this.previous)) {
|
|
177
|
+
result = this.previous;
|
|
178
|
+
}
|
|
179
|
+
this.previous = result.clone();
|
|
180
|
+
return result.clone();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Convert a duration since the epoch into clock ticks. */
|
|
184
|
+
static durationInMsToTicks(durationMilliseconds: number, rateInHz: number) {
|
|
185
|
+
// round(nanos * rate_hz / 1e9)
|
|
186
|
+
let durationNanoseconds = durationMilliseconds * 1e6;
|
|
187
|
+
let ticks = (durationNanoseconds * rateInHz + 500_000_000) / 1_000_000_000;
|
|
188
|
+
return Math.round(ticks);
|
|
189
|
+
}
|
|
70
190
|
}
|
|
71
191
|
|
|
72
192
|
export function coerceToDataView<Input extends DataView | ArrayBuffer | Uint8Array>(
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Originally from ts-debounce (https://github.com/chodorowicz/ts-debounce)
|
|
3
|
+
* with the following license:
|
|
4
|
+
*
|
|
5
|
+
* MIT License
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2017 Jakub Chodorowicz
|
|
8
|
+
*
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
* in the Software without restriction, including without limitation the rights
|
|
12
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
* furnished to do so, subject to the following conditions:
|
|
15
|
+
*
|
|
16
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
* copies or substantial portions of the Software.
|
|
18
|
+
*
|
|
19
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
* SOFTWARE.
|
|
26
|
+
*
|
|
27
|
+
* Modified to use CriticalTimers for reliable timer execution.
|
|
28
|
+
*/
|
|
29
|
+
/* eslint-disable @typescript-eslint/no-this-alias, @typescript-eslint/no-unused-expressions, @typescript-eslint/no-shadow */
|
|
30
|
+
import CriticalTimers from './timers';
|
|
31
|
+
|
|
32
|
+
export type Options<Result> = {
|
|
33
|
+
isImmediate?: boolean;
|
|
34
|
+
maxWait?: number;
|
|
35
|
+
callback?: (data: Result) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface DebouncedFunction<Args extends any[], F extends (...args: Args) => any> {
|
|
39
|
+
(this: ThisParameterType<F>, ...args: Args & Parameters<F>): Promise<ReturnType<F>>;
|
|
40
|
+
cancel: (reason?: any) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface DebouncedPromise<FunctionReturn> {
|
|
44
|
+
resolve: (result: FunctionReturn) => void;
|
|
45
|
+
reject: (reason?: any) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function debounce<Args extends any[], F extends (...args: Args) => any>(
|
|
49
|
+
func: F,
|
|
50
|
+
waitMilliseconds = 50,
|
|
51
|
+
options: Options<ReturnType<F>> = {},
|
|
52
|
+
): DebouncedFunction<Args, F> {
|
|
53
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
54
|
+
const isImmediate = options.isImmediate ?? false;
|
|
55
|
+
const callback = options.callback ?? false;
|
|
56
|
+
const maxWait = options.maxWait;
|
|
57
|
+
let lastInvokeTime = Date.now();
|
|
58
|
+
|
|
59
|
+
let promises: DebouncedPromise<ReturnType<F>>[] = [];
|
|
60
|
+
|
|
61
|
+
function nextInvokeTimeout() {
|
|
62
|
+
if (maxWait !== undefined) {
|
|
63
|
+
const timeSinceLastInvocation = Date.now() - lastInvokeTime;
|
|
64
|
+
|
|
65
|
+
if (timeSinceLastInvocation + waitMilliseconds >= maxWait) {
|
|
66
|
+
return maxWait - timeSinceLastInvocation;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return waitMilliseconds;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const debouncedFunction = function (this: ThisParameterType<F>, ...args: Parameters<F>) {
|
|
74
|
+
const context = this;
|
|
75
|
+
return new Promise<ReturnType<F>>((resolve, reject) => {
|
|
76
|
+
const invokeFunction = function () {
|
|
77
|
+
timeoutId = undefined;
|
|
78
|
+
lastInvokeTime = Date.now();
|
|
79
|
+
if (!isImmediate) {
|
|
80
|
+
const result = func.apply(context, args);
|
|
81
|
+
callback && callback(result);
|
|
82
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: vendored code
|
|
83
|
+
promises.forEach(({ resolve }) => resolve(result));
|
|
84
|
+
promises = [];
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const shouldCallNow = isImmediate && timeoutId === undefined;
|
|
89
|
+
|
|
90
|
+
if (timeoutId !== undefined) {
|
|
91
|
+
CriticalTimers.clearTimeout(timeoutId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
timeoutId = CriticalTimers.setTimeout(invokeFunction, nextInvokeTimeout());
|
|
95
|
+
|
|
96
|
+
if (shouldCallNow) {
|
|
97
|
+
const result = func.apply(context, args);
|
|
98
|
+
callback && callback(result);
|
|
99
|
+
return resolve(result);
|
|
100
|
+
}
|
|
101
|
+
promises.push({ resolve, reject });
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
debouncedFunction.cancel = function (reason?: any) {
|
|
106
|
+
if (timeoutId !== undefined) {
|
|
107
|
+
CriticalTimers.clearTimeout(timeoutId);
|
|
108
|
+
}
|
|
109
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: vendored code
|
|
110
|
+
promises.forEach(({ reject }) => reject(reason));
|
|
111
|
+
promises = [];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return debouncedFunction;
|
|
115
|
+
}
|
package/src/room/events.ts
CHANGED
|
@@ -34,7 +34,7 @@ export enum RoomEvent {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* When disconnected from room. This fires when room.disconnect() is called or
|
|
37
|
-
* when an unrecoverable connection issue had
|
|
37
|
+
* when an unrecoverable connection issue had occurred.
|
|
38
38
|
*
|
|
39
39
|
* DisconnectReason can be used to determine why the participant was disconnected. Notable reasons are
|
|
40
40
|
* - DUPLICATE_IDENTITY: another client with the same identity has joined the room
|
|
@@ -275,6 +275,8 @@ export default class LocalParticipant extends Participant {
|
|
|
275
275
|
|
|
276
276
|
private handleClosing = () => {
|
|
277
277
|
if (this.reconnectFuture) {
|
|
278
|
+
// @throws-transformer ignore - introduced due to adding Throws into Future, investigate this
|
|
279
|
+
// further
|
|
278
280
|
this.reconnectFuture.promise.catch((e) => this.log.warn(e.message, this.logContext));
|
|
279
281
|
this.reconnectFuture?.reject?.(new Error('Got disconnected during reconnection attempt'));
|
|
280
282
|
this.reconnectFuture = undefined;
|
|
@@ -3,7 +3,7 @@ import { TrackEvent } from '../events';
|
|
|
3
3
|
import { computeBitrate, monitorFrequency } from '../stats';
|
|
4
4
|
import type { AudioSenderStats } from '../stats';
|
|
5
5
|
import type { LoggerOptions } from '../types';
|
|
6
|
-
import { isReactNative, isWeb
|
|
6
|
+
import { isReactNative, isWeb } from '../utils';
|
|
7
7
|
import LocalTrack from './LocalTrack';
|
|
8
8
|
import { Track } from './Track';
|
|
9
9
|
import type { AudioCaptureOptions } from './options';
|
|
@@ -74,18 +74,15 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
74
74
|
return this;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const deviceHasChanged =
|
|
78
|
-
this._constraints.deviceId &&
|
|
79
|
-
this._mediaStreamTrack.getSettings().deviceId !==
|
|
80
|
-
unwrapConstraint(this._constraints.deviceId);
|
|
81
|
-
|
|
82
77
|
if (
|
|
83
78
|
this.source === Track.Source.Microphone &&
|
|
84
|
-
(this.stopOnMute ||
|
|
79
|
+
(this.stopOnMute ||
|
|
80
|
+
this._mediaStreamTrack.readyState === 'ended' ||
|
|
81
|
+
this.pendingDeviceChange) &&
|
|
85
82
|
!this.isUserProvided
|
|
86
83
|
) {
|
|
87
84
|
this.log.debug('reacquiring mic track', this.logContext);
|
|
88
|
-
await this.
|
|
85
|
+
await this.restart(undefined, true);
|
|
89
86
|
}
|
|
90
87
|
await super.unmute();
|
|
91
88
|
|
|
@@ -106,8 +103,11 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
106
103
|
await this.restart(constraints);
|
|
107
104
|
}
|
|
108
105
|
|
|
109
|
-
protected async restart(
|
|
110
|
-
|
|
106
|
+
protected async restart(
|
|
107
|
+
constraints?: MediaTrackConstraints,
|
|
108
|
+
isUnmuting?: boolean,
|
|
109
|
+
): Promise<typeof this> {
|
|
110
|
+
const track = await super.restart(constraints, isUnmuting);
|
|
111
111
|
this.checkForSilence();
|
|
112
112
|
return track;
|
|
113
113
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Mutex } from '@livekit/mutex';
|
|
2
|
-
import { debounce } from 'ts-debounce';
|
|
3
2
|
import { getBrowser } from '../../utils/browserParser';
|
|
4
3
|
import DeviceManager from '../DeviceManager';
|
|
4
|
+
import { debounce } from '../debounce';
|
|
5
5
|
import { DeviceUnsupportedError, TrackInvalidError } from '../errors';
|
|
6
6
|
import { TrackEvent } from '../events';
|
|
7
7
|
import type { LoggerOptions } from '../types';
|
|
@@ -65,6 +65,8 @@ export default abstract class LocalTrack<
|
|
|
65
65
|
|
|
66
66
|
protected trackChangeLock: Mutex;
|
|
67
67
|
|
|
68
|
+
protected pendingDeviceChange: boolean = false;
|
|
69
|
+
|
|
68
70
|
/**
|
|
69
71
|
*
|
|
70
72
|
* @param mediaTrack
|
|
@@ -145,7 +147,11 @@ export default abstract class LocalTrack<
|
|
|
145
147
|
return this._mediaStreamTrack.getSettings();
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
private async setMediaStreamTrack(
|
|
150
|
+
private async setMediaStreamTrack(
|
|
151
|
+
newTrack: MediaStreamTrack,
|
|
152
|
+
force?: boolean,
|
|
153
|
+
isUnmuting?: boolean,
|
|
154
|
+
) {
|
|
149
155
|
if (newTrack === this._mediaStreamTrack && !force) {
|
|
150
156
|
return;
|
|
151
157
|
}
|
|
@@ -202,7 +208,8 @@ export default abstract class LocalTrack<
|
|
|
202
208
|
this._mediaStreamTrack = newTrack;
|
|
203
209
|
if (newTrack) {
|
|
204
210
|
// sync muted state with the enabled state of the newly provided track
|
|
205
|
-
|
|
211
|
+
// if restarting as part of an unmute, set enabled to true directly to avoid mute cycling
|
|
212
|
+
this._mediaStreamTrack.enabled = isUnmuting ? true : !this.isMuted;
|
|
206
213
|
// when a valid track is replace, we'd want to start producing
|
|
207
214
|
await this.resumeUpstream();
|
|
208
215
|
this.attachedElements.forEach((el) => {
|
|
@@ -246,6 +253,7 @@ export default abstract class LocalTrack<
|
|
|
246
253
|
// when track is muted, underlying media stream track is stopped and
|
|
247
254
|
// will be restarted later
|
|
248
255
|
if (this.isMuted) {
|
|
256
|
+
this.pendingDeviceChange = true;
|
|
249
257
|
return true;
|
|
250
258
|
}
|
|
251
259
|
|
|
@@ -320,7 +328,7 @@ export default abstract class LocalTrack<
|
|
|
320
328
|
}
|
|
321
329
|
}
|
|
322
330
|
|
|
323
|
-
protected async restart(constraints?: MediaTrackConstraints) {
|
|
331
|
+
protected async restart(constraints?: MediaTrackConstraints, isUnmuting?: boolean) {
|
|
324
332
|
this.manuallyStopped = false;
|
|
325
333
|
const unlock = await this.trackChangeLock.lock();
|
|
326
334
|
|
|
@@ -363,8 +371,9 @@ export default abstract class LocalTrack<
|
|
|
363
371
|
newTrack.addEventListener('ended', this.handleEnded);
|
|
364
372
|
this.log.debug('re-acquired MediaStreamTrack', this.logContext);
|
|
365
373
|
|
|
366
|
-
await this.setMediaStreamTrack(newTrack);
|
|
374
|
+
await this.setMediaStreamTrack(newTrack, false, isUnmuting);
|
|
367
375
|
this._constraints = constraints;
|
|
376
|
+
this.pendingDeviceChange = false;
|
|
368
377
|
this.emit(TrackEvent.Restarted, this);
|
|
369
378
|
if (this.manuallyStopped) {
|
|
370
379
|
this.log.warn(
|
|
@@ -168,7 +168,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
|
168
168
|
|
|
169
169
|
if (this.source === Track.Source.Camera && !this.isUserProvided) {
|
|
170
170
|
this.log.debug('reacquiring camera track', this.logContext);
|
|
171
|
-
await this.
|
|
171
|
+
await this.restart(undefined, true);
|
|
172
172
|
}
|
|
173
173
|
await super.unmute();
|
|
174
174
|
return this;
|
package/src/room/types.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { DataStream_Chunk, Encryption_Type } from '@livekit/protocol';
|
|
2
|
-
import type { Future } from './utils';
|
|
3
2
|
|
|
4
3
|
export type SimulationOptions = {
|
|
5
4
|
publish?: {
|
|
@@ -125,7 +124,6 @@ export interface StreamController<T extends DataStream_Chunk> {
|
|
|
125
124
|
startTime: number;
|
|
126
125
|
endTime?: number;
|
|
127
126
|
sendingParticipantIdentity: string;
|
|
128
|
-
outOfBandFailureRejectingFuture: Future<never, Error>;
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
export interface BaseStreamInfo {
|
package/src/room/utils.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import TypedPromise from '../utils/TypedPromise';
|
|
9
9
|
import { getBrowser } from '../utils/browserParser';
|
|
10
10
|
import type { BrowserDetails } from '../utils/browserParser';
|
|
11
|
+
import { type Throws } from '../utils/throws';
|
|
11
12
|
import { protocolVersion, version } from '../version';
|
|
12
13
|
import { type ConnectionError, ConnectionErrorReason } from './errors';
|
|
13
14
|
import type LocalParticipant from './participant/LocalParticipant';
|
|
@@ -457,8 +458,12 @@ export function getStereoAudioStreamTrack() {
|
|
|
457
458
|
return stereoTrack;
|
|
458
459
|
}
|
|
459
460
|
|
|
461
|
+
/** An object that represents a serialized version of a `new Promise((resolve, reject) => {})`
|
|
462
|
+
* constructor. Wait for a promise resolution with `await future.promise` and explicitly resolve or
|
|
463
|
+
* reject the inner promise with `future.resolve(...)` or `future.reject(...)`.
|
|
464
|
+
*/
|
|
460
465
|
export class Future<T, E extends Error> {
|
|
461
|
-
promise: Promise<T
|
|
466
|
+
promise: Promise<Throws<T, E>>;
|
|
462
467
|
|
|
463
468
|
resolve?: (arg: T) => void;
|
|
464
469
|
|
|
@@ -486,7 +491,7 @@ export class Future<T, E extends Error> {
|
|
|
486
491
|
}).finally(() => {
|
|
487
492
|
this._isResolved = true;
|
|
488
493
|
this.onFinally?.();
|
|
489
|
-
})
|
|
494
|
+
}) as Promise<Throws<T, E>>;
|
|
490
495
|
}
|
|
491
496
|
}
|
|
492
497
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type EventMap } from 'typed-emitter';
|
|
2
|
+
import type TypedEventEmitter from 'typed-emitter';
|
|
3
|
+
import { Future } from '../room/utils';
|
|
4
|
+
|
|
5
|
+
/** A test helper to listen to events received by an event emitter and allow them to be imperatively
|
|
6
|
+
* queried after the fact. */
|
|
7
|
+
export function subscribeToEvents<
|
|
8
|
+
Callbacks extends EventMap,
|
|
9
|
+
EventNames extends keyof Callbacks = keyof Callbacks,
|
|
10
|
+
>(eventEmitter: TypedEventEmitter<Callbacks>, eventNames: Array<EventNames>) {
|
|
11
|
+
const nextEventListeners = new Map<EventNames, Array<Future<unknown, never>>>(
|
|
12
|
+
eventNames.map((eventName) => [eventName, []]),
|
|
13
|
+
);
|
|
14
|
+
const buffers = new Map<EventNames, Array<unknown>>(
|
|
15
|
+
eventNames.map((eventName) => [eventName, []]),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const eventHandlers = eventNames.map((eventName) => {
|
|
19
|
+
const onEvent = ((event: unknown) => {
|
|
20
|
+
const listeners = nextEventListeners.get(eventName)!;
|
|
21
|
+
if (listeners.length > 0) {
|
|
22
|
+
for (const listener of listeners) {
|
|
23
|
+
listener.resolve?.(event);
|
|
24
|
+
}
|
|
25
|
+
nextEventListeners.set(eventName, []);
|
|
26
|
+
} else {
|
|
27
|
+
buffers.get(eventName)!.push(event);
|
|
28
|
+
}
|
|
29
|
+
}) as Callbacks[keyof Callbacks];
|
|
30
|
+
return [eventName, onEvent] as [keyof Callbacks, Callbacks[keyof Callbacks]];
|
|
31
|
+
});
|
|
32
|
+
for (const [eventName, onEvent] of eventHandlers) {
|
|
33
|
+
eventEmitter.on(eventName, onEvent);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
/** Listen for the next occurrance of an event to be emitted, or return the last event that was
|
|
38
|
+
* buffered (but hasn't been processed yet). */
|
|
39
|
+
async waitFor<
|
|
40
|
+
EventPayload extends Parameters<Callbacks[EventName]>[0],
|
|
41
|
+
EventName extends EventNames = EventNames,
|
|
42
|
+
>(eventName: EventName): Promise<EventPayload> {
|
|
43
|
+
// If an event is already buffered which hasn't been processed yet, pull that off the buffer
|
|
44
|
+
// and use it.
|
|
45
|
+
const earliestBufferedEvent = buffers.get(eventName)!.shift();
|
|
46
|
+
if (earliestBufferedEvent) {
|
|
47
|
+
return earliestBufferedEvent as EventPayload;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Otherwise wait for the next event to come in.
|
|
51
|
+
const future = new Future<unknown, never>();
|
|
52
|
+
nextEventListeners.get(eventName)!.push(future);
|
|
53
|
+
const nextEvent = await future.promise;
|
|
54
|
+
return nextEvent as EventPayload;
|
|
55
|
+
},
|
|
56
|
+
/** Cleanup any lingering subscriptions. */
|
|
57
|
+
unsubscribe: () => {
|
|
58
|
+
for (const [eventName, onEvent] of eventHandlers) {
|
|
59
|
+
eventEmitter.off(eventName, onEvent);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
package/src/utils/throws.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
type Primitives = null | undefined | string | number | bigint | boolean | symbol;
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Branded type that encodes possible thrown errors in the return type.
|
|
3
5
|
*
|
|
@@ -11,7 +13,7 @@
|
|
|
11
13
|
*
|
|
12
14
|
* For more info about how this is checked, see ./throws-transformer at the root of this repo.
|
|
13
15
|
*/
|
|
14
|
-
export type Throws<T, E extends Error> = T & { readonly __throws?: E }
|
|
16
|
+
export type Throws<T, E extends Error> = (T & { readonly __throws?: E }) | Extract<T, Primitives>;
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Extract the error types from a Throws type.
|