livekit-client 2.22.0 → 2.22.2
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/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 +364 -40
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1847 -179
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +187 -1
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +25 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +7 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/constants.d.ts +5 -0
- package/dist/src/e2ee/constants.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +9 -2
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +42 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- 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/options.d.ts +22 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +12 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +7 -2
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +8 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts +17 -17
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/statsSummary.d.ts +13 -0
- package/dist/src/room/statsSummary.d.ts.map +1 -0
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +10 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/utils/machineInspector.d.ts +54 -0
- package/dist/src/utils/machineInspector.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +25 -1
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +7 -0
- package/dist/ts4.2/e2ee/constants.d.ts +5 -0
- package/dist/ts4.2/e2ee/types.d.ts +9 -2
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +42 -1
- package/dist/ts4.2/index.d.ts +3 -2
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/options.d.ts +22 -0
- package/dist/ts4.2/room/PCTransport.d.ts +2 -1
- package/dist/ts4.2/room/PCTransportManager.d.ts +12 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +7 -2
- package/dist/ts4.2/room/Room.d.ts +8 -1
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +17 -17
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/statsSummary.d.ts +13 -0
- package/dist/ts4.2/room/track/Track.d.ts +10 -0
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -0
- package/package.json +7 -1
- package/src/api/SignalClient.test.ts +320 -8
- package/src/api/SignalClient.ts +261 -82
- package/src/api/SignalClientStateMachine.test.ts +472 -0
- package/src/api/SignalClientStateMachine.ts +180 -0
- package/src/api/WebSocketStream.ts +19 -3
- package/src/api/utils.test.ts +20 -1
- package/src/api/utils.ts +5 -0
- package/src/e2ee/E2eeManager.ts +38 -14
- package/src/e2ee/constants.ts +6 -0
- package/src/e2ee/subscriberBlackScreen.test.ts +544 -0
- package/src/e2ee/types.ts +9 -2
- package/src/e2ee/worker/FrameCryptor.race.test.ts +9 -26
- package/src/e2ee/worker/FrameCryptor.test.ts +0 -1
- package/src/e2ee/worker/FrameCryptor.ts +185 -52
- package/src/e2ee/worker/e2ee.worker.ts +38 -6
- package/src/index.ts +5 -0
- package/src/logger.ts +1 -0
- package/src/options.ts +24 -0
- package/src/room/PCTransport.ts +2 -1
- package/src/room/PCTransportManager.test.ts +35 -0
- package/src/room/PCTransportManager.ts +12 -4
- package/src/room/RTCEngine.ts +16 -7
- package/src/room/Room.ts +70 -5
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +171 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +17 -18
- package/src/room/data-stream/incoming/StreamReader.ts +20 -50
- package/src/room/events.ts +1 -1
- package/src/room/statsSummary.ts +187 -0
- package/src/room/token-source/test-tokens.ts +20 -0
- package/src/room/token-source/utils.test.ts +27 -0
- package/src/room/token-source/utils.ts +12 -5
- package/src/room/track/LocalAudioTrack.ts +9 -3
- package/src/room/track/Track.ts +27 -0
- package/src/room/utils.test.ts +24 -1
- package/src/room/utils.ts +1 -1
- package/src/utils/machineInspector.ts +90 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** one summarised stats entry; keys without a value are dropped */
|
|
2
|
+
type Summary = Record<string, unknown>;
|
|
3
|
+
|
|
4
|
+
function compact<T extends Summary>(summary: T): Summary {
|
|
5
|
+
const compacted: Summary = {};
|
|
6
|
+
for (const [key, value] of Object.entries(summary)) {
|
|
7
|
+
if (value !== undefined) {
|
|
8
|
+
compacted[key] = value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return compacted;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolution(width?: number, height?: number): string | undefined {
|
|
15
|
+
return width && height ? `${width}x${height}` : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** keeps the derived durations readable; reported values are logged as they are */
|
|
19
|
+
function round(seconds: number): number {
|
|
20
|
+
return Math.round(seconds * 10_000) / 10_000;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Average time media spent in the jitter buffer, in s. Both counters are
|
|
25
|
+
* cumulative, so this is the average over the lifetime of the stream.
|
|
26
|
+
*/
|
|
27
|
+
function jitterBuffer(stat: Summary): number | undefined {
|
|
28
|
+
const delay = stat.jitterBufferDelay as number | undefined;
|
|
29
|
+
const emitted = stat.jitterBufferEmittedCount as number | undefined;
|
|
30
|
+
return delay !== undefined && emitted ? round(delay / emitted) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Playout delay in s. Video receive reports it directly, the audio path keeps it
|
|
35
|
+
* in the linked `media-playout` stats summed over the samples played out. Pairs
|
|
36
|
+
* with `RemoteTrack.setPlayoutDelay`.
|
|
37
|
+
*/
|
|
38
|
+
function playoutDelay(stat: Summary, playout?: Summary): number | undefined {
|
|
39
|
+
if (stat.playoutDelay !== undefined) {
|
|
40
|
+
return round(stat.playoutDelay as number);
|
|
41
|
+
}
|
|
42
|
+
const total = playout?.totalPlayoutDelay as number | undefined;
|
|
43
|
+
const samples = playout?.totalSamplesCount as number | undefined;
|
|
44
|
+
return total !== undefined && samples ? round(total / samples) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Picks the interesting fields out of a `getStats()` report and groups them by
|
|
49
|
+
* RTP stream, so a stats dump can be read without unfolding the raw report.
|
|
50
|
+
*/
|
|
51
|
+
export function summarizeStatsReport(report: RTCStatsReport) {
|
|
52
|
+
const byId = new Map<string, Summary>();
|
|
53
|
+
const candidatePairs: Summary[] = [];
|
|
54
|
+
const inbound: Summary[] = [];
|
|
55
|
+
const outbound: Summary[] = [];
|
|
56
|
+
let transport: Summary | undefined;
|
|
57
|
+
|
|
58
|
+
report.forEach((stat) => byId.set(stat.id, stat));
|
|
59
|
+
|
|
60
|
+
const codecOf = (stat: Summary) =>
|
|
61
|
+
stat.codecId ? byId.get(stat.codecId as string)?.mimeType : undefined;
|
|
62
|
+
const relatedOf = (stat: Summary, key: 'remoteId' | 'mediaSourceId' | 'playoutId') =>
|
|
63
|
+
stat[key] ? byId.get(stat[key] as string) : undefined;
|
|
64
|
+
|
|
65
|
+
report.forEach((stat) => {
|
|
66
|
+
switch (stat.type) {
|
|
67
|
+
case 'inbound-rtp': {
|
|
68
|
+
const playout = relatedOf(stat, 'playoutId');
|
|
69
|
+
inbound.push(
|
|
70
|
+
compact({
|
|
71
|
+
kind: stat.kind,
|
|
72
|
+
ssrc: stat.ssrc,
|
|
73
|
+
mid: stat.mid,
|
|
74
|
+
// matches `streamTrackID` in the track's own log context
|
|
75
|
+
trackId: stat.trackIdentifier,
|
|
76
|
+
codec: codecOf(stat),
|
|
77
|
+
decoder: stat.decoderImplementation,
|
|
78
|
+
resolution: resolution(stat.frameWidth, stat.frameHeight),
|
|
79
|
+
fps: stat.framesPerSecond,
|
|
80
|
+
bytesReceived: stat.bytesReceived,
|
|
81
|
+
packetsReceived: stat.packetsReceived,
|
|
82
|
+
packetsLost: stat.packetsLost,
|
|
83
|
+
packetsDiscarded: stat.packetsDiscarded,
|
|
84
|
+
// frames received without frames decoded is a decode failure
|
|
85
|
+
framesReceived: stat.framesReceived,
|
|
86
|
+
framesDecoded: stat.framesDecoded,
|
|
87
|
+
framesDropped: stat.framesDropped,
|
|
88
|
+
keyFramesDecoded: stat.keyFramesDecoded,
|
|
89
|
+
freezeCount: stat.freezeCount,
|
|
90
|
+
totalFreezesDuration: stat.totalFreezesDuration,
|
|
91
|
+
pauseCount: stat.pauseCount,
|
|
92
|
+
nackCount: stat.nackCount,
|
|
93
|
+
pliCount: stat.pliCount,
|
|
94
|
+
firCount: stat.firCount,
|
|
95
|
+
jitter: stat.jitter,
|
|
96
|
+
jitterBuffer: jitterBuffer(stat),
|
|
97
|
+
playoutDelay: playoutDelay(stat, playout),
|
|
98
|
+
audioLevel: stat.audioLevel,
|
|
99
|
+
totalSamplesReceived: stat.totalSamplesReceived,
|
|
100
|
+
concealedSamples: stat.concealedSamples,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case 'outbound-rtp': {
|
|
106
|
+
const remote = relatedOf(stat, 'remoteId');
|
|
107
|
+
const source = relatedOf(stat, 'mediaSourceId');
|
|
108
|
+
outbound.push(
|
|
109
|
+
compact({
|
|
110
|
+
kind: stat.kind,
|
|
111
|
+
ssrc: stat.ssrc,
|
|
112
|
+
mid: stat.mid,
|
|
113
|
+
rid: stat.rid,
|
|
114
|
+
trackId: source?.trackIdentifier,
|
|
115
|
+
active: stat.active,
|
|
116
|
+
codec: codecOf(stat),
|
|
117
|
+
encoder: stat.encoderImplementation,
|
|
118
|
+
resolution: resolution(stat.frameWidth, stat.frameHeight),
|
|
119
|
+
fps: stat.framesPerSecond,
|
|
120
|
+
// what the source produces, to tell a stalled capture from a stalled encoder
|
|
121
|
+
captureResolution: resolution(
|
|
122
|
+
source?.width as number | undefined,
|
|
123
|
+
source?.height as number | undefined,
|
|
124
|
+
),
|
|
125
|
+
captureFps: source?.framesPerSecond,
|
|
126
|
+
audioLevel: source?.audioLevel,
|
|
127
|
+
targetBitrate: stat.targetBitrate,
|
|
128
|
+
bytesSent: stat.bytesSent,
|
|
129
|
+
packetsSent: stat.packetsSent,
|
|
130
|
+
retransmittedPacketsSent: stat.retransmittedPacketsSent,
|
|
131
|
+
framesEncoded: stat.framesEncoded,
|
|
132
|
+
keyFramesEncoded: stat.keyFramesEncoded,
|
|
133
|
+
limitedBy:
|
|
134
|
+
stat.qualityLimitationReason === 'none' ? undefined : stat.qualityLimitationReason,
|
|
135
|
+
nackCount: stat.nackCount,
|
|
136
|
+
pliCount: stat.pliCount,
|
|
137
|
+
firCount: stat.firCount,
|
|
138
|
+
// loss, jitter and RTT are only known from what the remote reports
|
|
139
|
+
remotePacketsLost: remote?.packetsLost,
|
|
140
|
+
remoteFractionLost: remote?.fractionLost,
|
|
141
|
+
remoteJitter: remote?.jitter,
|
|
142
|
+
remoteRoundTripTime: remote?.roundTripTime,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'transport':
|
|
148
|
+
transport = stat;
|
|
149
|
+
break;
|
|
150
|
+
case 'candidate-pair':
|
|
151
|
+
candidatePairs.push(stat);
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const selectedPairId = transport?.selectedCandidatePairId as string | undefined;
|
|
158
|
+
const pair =
|
|
159
|
+
(selectedPairId ? byId.get(selectedPairId) : undefined) ??
|
|
160
|
+
candidatePairs.find((candidate) => candidate.selected) ??
|
|
161
|
+
candidatePairs.find((candidate) => candidate.nominated);
|
|
162
|
+
const local = pair?.localCandidateId ? byId.get(pair.localCandidateId as string) : undefined;
|
|
163
|
+
const remote = pair?.remoteCandidateId ? byId.get(pair.remoteCandidateId as string) : undefined;
|
|
164
|
+
|
|
165
|
+
const connection = compact({
|
|
166
|
+
ice: transport?.iceState,
|
|
167
|
+
dtls: transport?.dtlsState,
|
|
168
|
+
route:
|
|
169
|
+
local && remote
|
|
170
|
+
? `${local.candidateType}/${local.protocol} -> ${remote.candidateType}`
|
|
171
|
+
: undefined,
|
|
172
|
+
network: local?.networkType,
|
|
173
|
+
currentRoundTripTime: pair?.currentRoundTripTime,
|
|
174
|
+
// the send bandwidth estimate; no RTP stream reports it
|
|
175
|
+
availableOutgoingBitrate: pair?.availableOutgoingBitrate,
|
|
176
|
+
availableIncomingBitrate: pair?.availableIncomingBitrate,
|
|
177
|
+
bytesSent: pair?.bytesSent,
|
|
178
|
+
bytesReceived: pair?.bytesReceived,
|
|
179
|
+
candidatePairChanges: transport?.selectedCandidatePairChanges,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
connection: Object.keys(connection).length > 0 ? connection : undefined,
|
|
184
|
+
outbound: outbound.length > 0 ? outbound : undefined,
|
|
185
|
+
inbound: inbound.length > 0 ? inbound : undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// Builds an unsigned (`alg: none`) JWT. These aren't signed at all, so they can only be used in
|
|
2
|
+
// tests which don't care about the signature.
|
|
3
|
+
function unsignedToken(payload: Record<string, unknown>) {
|
|
4
|
+
const encode = (value: Record<string, unknown>) =>
|
|
5
|
+
btoa(JSON.stringify(value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
6
|
+
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.`;
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
// Test JWTs created for test purposes only.
|
|
2
10
|
// None of these actually auth against anything.
|
|
3
11
|
export const TOKENS = {
|
|
@@ -25,4 +33,16 @@ export const TOKENS = {
|
|
|
25
33
|
// A dummy roomConfig value is also set, with room_config.name = "test room name", room_config.extraField = "extra field value", and room_config.agents = [{"agentName": "test agent name","metadata":"test agent metadata","extraField":"extra field value"}]
|
|
26
34
|
EXTRA_FIELDS:
|
|
27
35
|
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo5ODc2NTQzMjEwLCJuYmYiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwicm9vbUNvbmZpZyI6eyJuYW1lIjoidGVzdCByb29tIG5hbWUiLCJlbXB0eVRpbWVvdXQiOjAsImRlcGFydHVyZVRpbWVvdXQiOjAsIm1heFBhcnRpY2lwYW50cyI6MCwibWluUGxheW91dERlbGF5IjowLCJtYXhQbGF5b3V0RGVsYXkiOjAsInN5bmNTdHJlYW1zIjpmYWxzZSwiYWdlbnRzIjpbeyJhZ2VudE5hbWUiOiJ0ZXN0IGFnZW50IG5hbWUiLCJtZXRhZGF0YSI6InRlc3QgYWdlbnQgbWV0YWRhdGEiLCJleHRyYUZpZWxkIjoiZXh0cmEgZmllbGQgdmFsdWUifV0sIm1ldGFkYXRhIjoiIiwiZXh0cmFGaWVsZCI6ImV4dHJhIGZpZWxkIHZhbHVlIn19Cg.EDetpHG8cSubaApzgWJaQrpCiSy9KDBlfCfVdIydbQ-_CHiNnXOK_f_mCJbTf9A-duT1jmvPOkLrkkWFT60XPQ',
|
|
36
|
+
|
|
37
|
+
// Nbf date set at 1234567890 seconds (Fri Feb 13 2009 23:31:30 GMT+0000)
|
|
38
|
+
// No exp date set at all
|
|
39
|
+
NO_EXP: unsignedToken({ sub: '1234567890', nbf: 1234567890, iat: 1234567890 }),
|
|
40
|
+
|
|
41
|
+
// No nbf date set at all
|
|
42
|
+
// Exp date set at 1234567891 seconds (Fri Feb 13 2009 23:31:31 GMT+0000)
|
|
43
|
+
EXP_IN_PAST_NO_NBF: unsignedToken({ sub: '1234567890', exp: 1234567891, iat: 1234567890 }),
|
|
44
|
+
|
|
45
|
+
// No nbf date set at all
|
|
46
|
+
// Exp date set at 9876543210 seconds (Fri Dec 22 2282 20:13:30 GMT+0000)
|
|
47
|
+
VALID_NO_NBF: unsignedToken({ sub: '1234567890', exp: 9876543210, iat: 1234567890 }),
|
|
28
48
|
};
|
|
@@ -31,6 +31,33 @@ describe('isResponseTokenValid', () => {
|
|
|
31
31
|
);
|
|
32
32
|
expect(isValid).toBe(false);
|
|
33
33
|
});
|
|
34
|
+
it('should treat a jwt without exp as expired', () => {
|
|
35
|
+
const isValid = isResponseTokenValid(
|
|
36
|
+
TokenSourceResponse.fromJson({
|
|
37
|
+
serverUrl: 'ws://localhost:7800',
|
|
38
|
+
participantToken: TOKENS.NO_EXP,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
expect(isValid).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it('should honor exp when nbf is absent', () => {
|
|
44
|
+
const isValid = isResponseTokenValid(
|
|
45
|
+
TokenSourceResponse.fromJson({
|
|
46
|
+
serverUrl: 'ws://localhost:7800',
|
|
47
|
+
participantToken: TOKENS.EXP_IN_PAST_NO_NBF,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
expect(isValid).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it('should accept a non-expired jwt that omits nbf', () => {
|
|
53
|
+
const isValid = isResponseTokenValid(
|
|
54
|
+
TokenSourceResponse.fromJson({
|
|
55
|
+
serverUrl: 'ws://localhost:7800',
|
|
56
|
+
participantToken: TOKENS.VALID_NO_NBF,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
expect(isValid).toBe(true);
|
|
60
|
+
});
|
|
34
61
|
});
|
|
35
62
|
|
|
36
63
|
describe('decodeTokenPayload', () => {
|
|
@@ -7,19 +7,26 @@ const ONE_MINUTE_IN_MILLISECONDS = 60 * ONE_SECOND_IN_MILLISECONDS;
|
|
|
7
7
|
|
|
8
8
|
export function isResponseTokenValid(response: TokenSourceResponse) {
|
|
9
9
|
const jwtPayload = decodeTokenPayload(response.participantToken);
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Missing exp: TokenSourceCached would otherwise return this response forever.
|
|
11
|
+
// nbf is optional (RFC 7519); do not skip the exp check when it is absent.
|
|
12
|
+
if (!jwtPayload?.exp) {
|
|
13
|
+
return false;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const now = new Date();
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
if (jwtPayload.nbf) {
|
|
19
|
+
const nbfInMilliseconds = jwtPayload.nbf * ONE_SECOND_IN_MILLISECONDS;
|
|
20
|
+
const nbfDate = new Date(nbfInMilliseconds);
|
|
21
|
+
if (nbfDate > now) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
18
25
|
|
|
19
26
|
const expInMilliseconds = jwtPayload.exp * ONE_SECOND_IN_MILLISECONDS;
|
|
20
27
|
const expDate = new Date(expInMilliseconds - ONE_MINUTE_IN_MILLISECONDS);
|
|
21
28
|
|
|
22
|
-
return
|
|
29
|
+
return expDate > now;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/** Given a LiveKit generated participant token, decodes and returns the associated {@link TokenPayload} data. */
|
|
@@ -245,12 +245,18 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
245
245
|
type: 'audio',
|
|
246
246
|
streamId: v.id,
|
|
247
247
|
packetsSent: v.packetsSent,
|
|
248
|
-
packetsLost: v.packetsLost,
|
|
249
248
|
bytesSent: v.bytesSent,
|
|
250
249
|
timestamp: v.timestamp,
|
|
251
|
-
roundTripTime: v.roundTripTime,
|
|
252
|
-
jitter: v.jitter,
|
|
253
250
|
};
|
|
251
|
+
|
|
252
|
+
// loss, jitter and RTT are only known from what the remote reports back,
|
|
253
|
+
// the same way the video sender picks them up
|
|
254
|
+
const remote = stats.get(v.remoteId);
|
|
255
|
+
if (remote) {
|
|
256
|
+
audioStats.packetsLost = remote.packetsLost;
|
|
257
|
+
audioStats.jitter = remote.jitter;
|
|
258
|
+
audioStats.roundTripTime = remote.roundTripTime;
|
|
259
|
+
}
|
|
254
260
|
}
|
|
255
261
|
});
|
|
256
262
|
|
package/src/room/track/Track.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { SignalClient } from '../../api/SignalClient';
|
|
|
11
11
|
import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
|
|
12
12
|
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
13
13
|
import { TrackEvent } from '../events';
|
|
14
|
+
import { summarizeStatsReport } from '../statsSummary';
|
|
14
15
|
import type { LoggerOptions } from '../types';
|
|
15
16
|
import { isFireFox, isSafari, isWeb } from '../utils';
|
|
16
17
|
import type { TrackProcessor } from './processor/types';
|
|
@@ -85,6 +86,8 @@ export abstract class Track<
|
|
|
85
86
|
|
|
86
87
|
protected monitorInterval?: ReturnType<typeof setInterval>;
|
|
87
88
|
|
|
89
|
+
private finalStatsLogged = false;
|
|
90
|
+
|
|
88
91
|
protected log: StructuredLogger = log;
|
|
89
92
|
|
|
90
93
|
protected constructor(
|
|
@@ -271,6 +274,11 @@ export abstract class Track<
|
|
|
271
274
|
/* @internal */
|
|
272
275
|
abstract startMonitor(signalClient?: SignalClient): void;
|
|
273
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Raw stats of the sender or receiver this track is attached to.
|
|
279
|
+
*/
|
|
280
|
+
abstract getRTCStatsReport(): Promise<RTCStatsReport | undefined>;
|
|
281
|
+
|
|
274
282
|
/* @internal */
|
|
275
283
|
stopMonitor() {
|
|
276
284
|
if (this.monitorInterval) {
|
|
@@ -280,6 +288,25 @@ export abstract class Track<
|
|
|
280
288
|
cancelAnimationFrame(this.timeSyncHandle);
|
|
281
289
|
this.timeSyncHandle = undefined;
|
|
282
290
|
}
|
|
291
|
+
this.logFinalStats();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Dumps the raw stats of the track as it ends, once: a track that stops
|
|
296
|
+
* between two of the room's stats dumps is gone by the time the next one runs.
|
|
297
|
+
*/
|
|
298
|
+
private logFinalStats() {
|
|
299
|
+
if (this.finalStatsLogged) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.finalStatsLogged = true;
|
|
303
|
+
this.getRTCStatsReport()
|
|
304
|
+
.then((report) => {
|
|
305
|
+
if (report) {
|
|
306
|
+
this.log.info('final track stats', summarizeStatsReport(report));
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
.catch((error) => this.log.debug('could not collect final track stats', { error }));
|
|
283
310
|
}
|
|
284
311
|
|
|
285
312
|
/** @internal */
|
package/src/room/utils.test.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ClientInfo_Capability } from '@livekit/protocol';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import {
|
|
4
4
|
ddExtensionURI,
|
|
5
5
|
extractMaxAgeFromRequestHeaders,
|
|
6
6
|
getClientInfo,
|
|
7
7
|
negotiateDependencyDescriptor,
|
|
8
8
|
splitUtf8,
|
|
9
|
+
supportsAdaptiveStream,
|
|
9
10
|
toWebsocketUrl,
|
|
10
11
|
} from './utils';
|
|
11
12
|
|
|
@@ -257,3 +258,25 @@ describe('negotiateDependencyDescriptor', () => {
|
|
|
257
258
|
expect(negotiateDependencyDescriptor(transceiver)).toBe(false);
|
|
258
259
|
});
|
|
259
260
|
});
|
|
261
|
+
|
|
262
|
+
describe('supportsAdaptiveStream', () => {
|
|
263
|
+
afterEach(() => {
|
|
264
|
+
vi.unstubAllGlobals();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('reports support where both observers exist', () => {
|
|
268
|
+
expect(supportsAdaptiveStream()).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('reports no support where ResizeObserver is missing', () => {
|
|
272
|
+
vi.stubGlobal('ResizeObserver', undefined);
|
|
273
|
+
|
|
274
|
+
expect(supportsAdaptiveStream()).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('reports no support where IntersectionObserver is missing', () => {
|
|
278
|
+
vi.stubGlobal('IntersectionObserver', undefined);
|
|
279
|
+
|
|
280
|
+
expect(supportsAdaptiveStream()).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
});
|
package/src/room/utils.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function supportsAddTrack() {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function supportsAdaptiveStream() {
|
|
61
|
-
return typeof ResizeObserver !== undefined && typeof IntersectionObserver !== undefined;
|
|
61
|
+
return typeof ResizeObserver !== 'undefined' && typeof IntersectionObserver !== 'undefined';
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function supportsDynacast() {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in registry that lets a development tool observe the state machines driving the connection
|
|
3
|
+
* layer.
|
|
4
|
+
*
|
|
5
|
+
* The machines are private to the objects that own them and, more importantly, do not outlive them:
|
|
6
|
+
* a full reconnect replaces the whole engine along with its `SignalClient`, so anything watching a
|
|
7
|
+
* single machine reference goes blind exactly when the interesting part starts. Machines therefore
|
|
8
|
+
* announce themselves here as they are constructed, and a subscriber sees the whole succession.
|
|
9
|
+
*
|
|
10
|
+
* Nothing is recorded until {@link enableMachineInspector} is called, which no shipping code does —
|
|
11
|
+
* with the registry off, announcing is a comparison and a return.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The slice of machina's `Fsm` surface an inspector needs. Structural rather than machina's own
|
|
16
|
+
* type so that any machine satisfies it regardless of its state and input unions.
|
|
17
|
+
*/
|
|
18
|
+
export interface InspectableMachine {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly initialState: string;
|
|
21
|
+
readonly states: Record<string, Record<string, unknown>>;
|
|
22
|
+
readonly context?: unknown;
|
|
23
|
+
currentState(): string;
|
|
24
|
+
canHandle(input: string): boolean;
|
|
25
|
+
on(eventName: string, callback: (data: any) => void): { off(): void };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MachineAnnouncement {
|
|
29
|
+
/** What the machine drives, e.g. `signal`. Not unique: each new `SignalClient` announces one. */
|
|
30
|
+
label: string;
|
|
31
|
+
/** How many machines have been announced before this one, so successive instances are tellable apart. */
|
|
32
|
+
seq: number;
|
|
33
|
+
/**
|
|
34
|
+
* When the machine was constructed. Carried on the announcement because subscribers receive the
|
|
35
|
+
* recorded ones on subscribe, and stamping those at replay time would date them all to whenever
|
|
36
|
+
* the tool happened to open.
|
|
37
|
+
*/
|
|
38
|
+
at: number;
|
|
39
|
+
machine: InspectableMachine;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Kept small: enough for a panel opened mid-session to see how the connection got where it is. */
|
|
43
|
+
const HISTORY_LIMIT = 16;
|
|
44
|
+
|
|
45
|
+
let enabled = false;
|
|
46
|
+
let announced: MachineAnnouncement[] = [];
|
|
47
|
+
let subscribers: Array<(announcement: MachineAnnouncement) => void> = [];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Starts recording machine announcements. Call before the machines of interest are constructed —
|
|
51
|
+
* for the signal machine that means before the `Room` is created.
|
|
52
|
+
*/
|
|
53
|
+
export function enableMachineInspector() {
|
|
54
|
+
enabled = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isMachineInspectorEnabled() {
|
|
58
|
+
return enabled;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Announces a machine to whatever is watching. A no-op unless the inspector was enabled. */
|
|
62
|
+
export function announceMachine(label: string, machine: InspectableMachine) {
|
|
63
|
+
if (!enabled) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const announcement: MachineAnnouncement = {
|
|
67
|
+
label,
|
|
68
|
+
seq: announced.length,
|
|
69
|
+
at: Date.now(),
|
|
70
|
+
machine,
|
|
71
|
+
};
|
|
72
|
+
announced = [...announced.slice(-(HISTORY_LIMIT - 1)), announcement];
|
|
73
|
+
for (const subscriber of subscribers) {
|
|
74
|
+
subscriber(announcement);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Subscribes to machine announcements, replaying the ones already recorded so that a panel opened
|
|
80
|
+
* after connecting still sees the machine currently in charge. Returns an unsubscribe function.
|
|
81
|
+
*/
|
|
82
|
+
export function onMachineAnnounced(callback: (announcement: MachineAnnouncement) => void) {
|
|
83
|
+
subscribers = [...subscribers, callback];
|
|
84
|
+
for (const announcement of announced) {
|
|
85
|
+
callback(announcement);
|
|
86
|
+
}
|
|
87
|
+
return () => {
|
|
88
|
+
subscribers = subscribers.filter((subscriber) => subscriber !== callback);
|
|
89
|
+
};
|
|
90
|
+
}
|