livekit-client 2.21.0 → 2.22.1
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 +10 -10
- 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 +189 -2
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2243 -270
- 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 +189 -2
- 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/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 +51 -3
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +27 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +10 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/participant/LocalParticipant.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/TokenSource.d.ts +19 -10
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- 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/LocalVideoTrack.d.ts +12 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +3 -0
- package/dist/src/room/track/RemoteTrack.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/room/utils.d.ts +15 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/promiseState.d.ts +12 -0
- package/dist/src/test/promiseState.d.ts.map +1 -0
- package/dist/src/test/signalToken.d.ts.map +1 -1
- package/dist/src/utils/AsyncQueue.d.ts +3 -3
- package/dist/src/utils/AsyncQueue.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/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 +51 -3
- package/dist/ts4.2/room/RTCEngine.d.ts +27 -3
- package/dist/ts4.2/room/Room.d.ts +10 -1
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/ts4.2/room/statsSummary.d.ts +13 -0
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
- package/dist/ts4.2/room/track/Track.d.ts +10 -0
- package/dist/ts4.2/room/utils.d.ts +15 -0
- package/dist/ts4.2/test/promiseState.d.ts +12 -0
- package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -0
- package/package.json +19 -12
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/api/SignalClient.test.ts +320 -8
- package/src/api/SignalClient.ts +260 -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/utils.ts +1 -1
- package/src/index.ts +5 -0
- package/src/logger.ts +1 -0
- package/src/options.ts +24 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +183 -81
- package/src/room/RTCEngine.test.ts +339 -2
- package/src/room/RTCEngine.ts +168 -18
- package/src/room/Room.test.ts +134 -3
- package/src/room/Room.ts +122 -22
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +1 -1
- package/src/room/data-stream/incoming/StreamReader.ts +1 -1
- package/src/room/errors.ts +1 -2
- package/src/room/events.ts +1 -1
- package/src/room/statsSummary.ts +187 -0
- package/src/room/token-source/TokenSource.ts +25 -12
- 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/LocalVideoTrack.test.ts +105 -2
- package/src/room/track/LocalVideoTrack.ts +36 -10
- package/src/room/track/RemoteTrack.test.ts +144 -0
- package/src/room/track/RemoteTrack.ts +39 -12
- package/src/room/track/Track.ts +29 -1
- package/src/room/utils.test.ts +94 -2
- package/src/room/utils.ts +53 -1
- package/src/test/promiseState.ts +23 -0
- package/src/test/signalServerSetup.ts +2 -1
- package/src/test/signalToken.ts +17 -13
- package/src/type-polyfills/header-extensions.d.ts +13 -0
- package/src/utils/AsyncQueue.ts +3 -3
- package/src/utils/machineInspector.ts +90 -0
package/src/api/utils.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { createRtcUrl, createValidateUrl } from './utils';
|
|
2
|
+
import { createRtcUrl, createValidateUrl, getAbortReasonAsString } from './utils';
|
|
3
3
|
|
|
4
4
|
describe('createRtcUrl', () => {
|
|
5
5
|
it('should create a basic RTC URL', () => {
|
|
@@ -125,3 +125,22 @@ describe('createValidateUrl', () => {
|
|
|
125
125
|
expect(parsedResult.pathname).toBe('/sub/path/rtc/validate');
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
describe('getAbortReasonAsString', () => {
|
|
130
|
+
it('uses an error message, which is how the connect timeout describes itself', () => {
|
|
131
|
+
// the timeout hands the abort handler an Error, not a signal
|
|
132
|
+
expect(
|
|
133
|
+
getAbortReasonAsString(new Error('room connection has timed out (signal)'), 'fallback'),
|
|
134
|
+
).toBe('room connection has timed out (signal)');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("reads an abort signal's reason", () => {
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
controller.abort('user navigated away');
|
|
140
|
+
expect(getAbortReasonAsString(controller.signal, 'fallback')).toBe('user navigated away');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('falls back when there is nothing to describe', () => {
|
|
144
|
+
expect(getAbortReasonAsString(undefined, 'fallback')).toBe('fallback');
|
|
145
|
+
});
|
|
146
|
+
});
|
package/src/api/utils.ts
CHANGED
|
@@ -45,6 +45,11 @@ export function getAbortReasonAsString(
|
|
|
45
45
|
signal: AbortSignal | Error | unknown,
|
|
46
46
|
defaultMessage = 'Unknown reason',
|
|
47
47
|
) {
|
|
48
|
+
// the connect timeout hands this an Error rather than a signal, and its message is the only
|
|
49
|
+
// description of what went wrong — without this a timeout reports itself as a generic abort
|
|
50
|
+
if (signal instanceof Error) {
|
|
51
|
+
return signal.message;
|
|
52
|
+
}
|
|
48
53
|
if (!(signal instanceof AbortSignal)) {
|
|
49
54
|
return defaultMessage;
|
|
50
55
|
}
|
package/src/e2ee/utils.ts
CHANGED
|
@@ -139,7 +139,7 @@ export function needsRbspUnescaping(frameData: NonSharedUint8Array) {
|
|
|
139
139
|
export function parseRbsp(stream: NonSharedUint8Array): NonSharedUint8Array {
|
|
140
140
|
const dataOut: number[] = [];
|
|
141
141
|
var length = stream.length;
|
|
142
|
-
for (var i = 0; i < stream.length;
|
|
142
|
+
for (var i = 0; i < stream.length;) {
|
|
143
143
|
// Be careful about over/underflow here. byte_length_ - 3 can underflow, and
|
|
144
144
|
// i + 3 can overflow, but byte_length_ - i can't, because i < byte_length_
|
|
145
145
|
// above, and that expression will produce the number of bytes left in
|
package/src/index.ts
CHANGED
|
@@ -53,12 +53,14 @@ import {
|
|
|
53
53
|
isLocalTrack,
|
|
54
54
|
isRemoteParticipant,
|
|
55
55
|
isRemoteTrack,
|
|
56
|
+
isSVCCodec,
|
|
56
57
|
isVideoCodec,
|
|
57
58
|
isVideoTrack,
|
|
58
59
|
supportsAV1,
|
|
59
60
|
supportsAdaptiveStream,
|
|
60
61
|
supportsAudioOutputSelection,
|
|
61
62
|
supportsDynacast,
|
|
63
|
+
supportsH265,
|
|
62
64
|
supportsVP9,
|
|
63
65
|
} from './room/utils';
|
|
64
66
|
import { getBrowser } from './utils/browserParser';
|
|
@@ -149,12 +151,14 @@ export {
|
|
|
149
151
|
supportsAdaptiveStream,
|
|
150
152
|
supportsAudioOutputSelection,
|
|
151
153
|
supportsDynacast,
|
|
154
|
+
supportsH265,
|
|
152
155
|
supportsVP9,
|
|
153
156
|
Mutex,
|
|
154
157
|
isAudioCodec,
|
|
155
158
|
isAudioTrack,
|
|
156
159
|
isLocalTrack,
|
|
157
160
|
isRemoteTrack,
|
|
161
|
+
isSVCCodec,
|
|
158
162
|
isVideoCodec,
|
|
159
163
|
isVideoTrack,
|
|
160
164
|
isLocalParticipant,
|
|
@@ -180,6 +184,7 @@ export type {
|
|
|
180
184
|
DataTrackSubscribeOptions,
|
|
181
185
|
RemoteDataTrackPipelineOptions,
|
|
182
186
|
};
|
|
187
|
+
export { type DataTrackFrame } from './room/data-track/frame';
|
|
183
188
|
export { DataTrackPacket, type DataTrackPacketHeader } from './room/data-track/packet';
|
|
184
189
|
export {
|
|
185
190
|
type DataTrackExtensions,
|
package/src/logger.ts
CHANGED
package/src/options.ts
CHANGED
|
@@ -120,6 +120,30 @@ export interface InternalRoomOptions {
|
|
|
120
120
|
* @default true
|
|
121
121
|
*/
|
|
122
122
|
singlePeerConnection: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Options controlling data stream behavior for this room.
|
|
126
|
+
*/
|
|
127
|
+
dataStream?: RoomDataStreamOptions;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Options controlling data stream behavior for a room.
|
|
132
|
+
*/
|
|
133
|
+
export interface RoomDataStreamOptions {
|
|
134
|
+
/**
|
|
135
|
+
* Maximum size, in bytes, of the payload this client accepts from a single incoming data stream.
|
|
136
|
+
*
|
|
137
|
+
* A compressed stream can inflate to an arbitrarily large payload, so the decompressed output is
|
|
138
|
+
* bounded rather than trusting the size declared on the wire. An incoming stream that exceeds the
|
|
139
|
+
* cap fails with a `DataStreamErrorReason.PayloadTooLarge` error on the next read instead of
|
|
140
|
+
* buffering without bound.
|
|
141
|
+
*
|
|
142
|
+
* This is enforced on the receiving side only: raising it on a sender has no effect.
|
|
143
|
+
*
|
|
144
|
+
* @default 5_000_000_000 (5 GB)
|
|
145
|
+
*/
|
|
146
|
+
maxPayloadByteLength?: number;
|
|
123
147
|
}
|
|
124
148
|
|
|
125
149
|
/**
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { type MediaDescription, parse } from 'sdp-transform';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
applyVideoStartBitrate,
|
|
5
|
+
conformBundledCodecFmtp,
|
|
6
|
+
ensureAudioNackAndStereo,
|
|
7
|
+
ensureVideoDDExtension,
|
|
8
|
+
extractStereoAndNackAudioFromOffer,
|
|
9
|
+
fmtpConfigHasParam,
|
|
10
|
+
placeholderMidsFromTransceivers,
|
|
11
|
+
} from './PCTransport';
|
|
12
|
+
import { ddExtensionURI } from './utils';
|
|
4
13
|
|
|
5
14
|
/** Parse the `key[=value]` pairs of an fmtp config into a comparable set. */
|
|
6
15
|
const paramSet = (config: string) => new Set(config.split(';').filter(Boolean));
|
|
@@ -52,6 +61,35 @@ a=recvonly
|
|
|
52
61
|
a=rtpmap:49 H265/90000
|
|
53
62
|
a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST`;
|
|
54
63
|
|
|
64
|
+
describe('video start bitrate', () => {
|
|
65
|
+
it('applies the bitrate only to the section whose msid track ID matches the cid', () => {
|
|
66
|
+
const { media } = parse(`v=0
|
|
67
|
+
o=- 0 0 IN IP4 127.0.0.1
|
|
68
|
+
s=-
|
|
69
|
+
t=0 0
|
|
70
|
+
a=group:BUNDLE 0 1
|
|
71
|
+
m=video 9 UDP/TLS/RTP/SAVPF 96
|
|
72
|
+
c=IN IP4 0.0.0.0
|
|
73
|
+
a=mid:0
|
|
74
|
+
a=sendonly
|
|
75
|
+
a=msid:PA_remote|camera other-track
|
|
76
|
+
a=rtpmap:96 VP8/90000
|
|
77
|
+
m=video 9 UDP/TLS/RTP/SAVPF 96
|
|
78
|
+
c=IN IP4 0.0.0.0
|
|
79
|
+
a=mid:1
|
|
80
|
+
a=sendonly
|
|
81
|
+
a=msid:PA_remote|camera camera-cid
|
|
82
|
+
a=rtpmap:96 VP8/90000`);
|
|
83
|
+
|
|
84
|
+
for (const section of media) {
|
|
85
|
+
applyVideoStartBitrate(section, 'camera-cid', 'VP8', 1_000);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expect(fmtpOf(media, '0', 96)).toBeUndefined();
|
|
89
|
+
expect(paramSet(fmtpOf(media, '1', 96)!)).toContain('x-google-start-bitrate=900');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
55
93
|
describe('placeholderMidsFromTransceivers', () => {
|
|
56
94
|
const tr = (mid: string | null, track: unknown) =>
|
|
57
95
|
({ mid, sender: { track } }) as unknown as RTCRtpTransceiver;
|
|
@@ -148,3 +186,207 @@ a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST`);
|
|
|
148
186
|
expect(fmtpOf(media, '5', 49)).toBe(fmtpOf(media, '3', 49)); // placeholder conformed
|
|
149
187
|
});
|
|
150
188
|
});
|
|
189
|
+
|
|
190
|
+
describe('fmtpConfigHasParam', () => {
|
|
191
|
+
it('matches a whole `;`-delimited parameter, not a substring', () => {
|
|
192
|
+
// `stereo=1` is a substring of `sprop-stereo=1` — an exact-token match must
|
|
193
|
+
// not treat the latter as declaring the former.
|
|
194
|
+
expect(fmtpConfigHasParam('minptime=10;stereo=1', 'stereo=1')).toBe(true);
|
|
195
|
+
expect(fmtpConfigHasParam('minptime=10;sprop-stereo=1', 'stereo=1')).toBe(false);
|
|
196
|
+
expect(fmtpConfigHasParam('minptime=10;sprop-stereo=1', 'sprop-stereo=1')).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('tolerates surrounding whitespace between parameters', () => {
|
|
200
|
+
expect(fmtpConfigHasParam('minptime=10; stereo=1', 'stereo=1')).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/** Build a parsed audio media section from its `a=` lines for the munge helpers. */
|
|
205
|
+
const audioSection = (rtpmap: string, fmtp: string, mid = '0') =>
|
|
206
|
+
parse(`v=0
|
|
207
|
+
o=- 0 0 IN IP4 127.0.0.1
|
|
208
|
+
s=-
|
|
209
|
+
t=0 0
|
|
210
|
+
m=audio 9 UDP/TLS/RTP/SAVPF 111
|
|
211
|
+
a=mid:${mid}
|
|
212
|
+
${rtpmap}
|
|
213
|
+
${fmtp}`).media[0];
|
|
214
|
+
|
|
215
|
+
describe('ensureAudioNackAndStereo', () => {
|
|
216
|
+
it('adds stereo=1 even when sprop-stereo=1 is already present', () => {
|
|
217
|
+
const media = audioSection(
|
|
218
|
+
'a=rtpmap:111 opus/48000/2',
|
|
219
|
+
'a=fmtp:111 minptime=10;sprop-stereo=1',
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
ensureAudioNackAndStereo(media as any, ['all'], []);
|
|
223
|
+
|
|
224
|
+
const config = media.fmtp.find((f) => f.payload === 111)!.config;
|
|
225
|
+
expect(paramSet(config)).toContain('stereo=1');
|
|
226
|
+
expect(paramSet(config)).toContain('sprop-stereo=1');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('does not add a duplicate stereo=1 when it is already present', () => {
|
|
230
|
+
const media = audioSection('a=rtpmap:111 opus/48000/2', 'a=fmtp:111 minptime=10;stereo=1');
|
|
231
|
+
|
|
232
|
+
ensureAudioNackAndStereo(media as any, ['all'], []);
|
|
233
|
+
|
|
234
|
+
const config = media.fmtp.find((f) => f.payload === 111)!.config;
|
|
235
|
+
expect(config.match(/(?:^|;)stereo=1(?:;|$)/g)).toHaveLength(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('matches the opus codec case-insensitively (RFC 4855)', () => {
|
|
239
|
+
const media = audioSection('a=rtpmap:111 OPUS/48000/2', 'a=fmtp:111 minptime=10');
|
|
240
|
+
|
|
241
|
+
ensureAudioNackAndStereo(media as any, ['all'], []);
|
|
242
|
+
|
|
243
|
+
expect(paramSet(media.fmtp.find((f) => f.payload === 111)!.config)).toContain('stereo=1');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('extractStereoAndNackAudioFromOffer', () => {
|
|
248
|
+
const offerWith = (rtpmap: string, fmtp: string) => ({
|
|
249
|
+
type: 'offer' as const,
|
|
250
|
+
sdp: `v=0
|
|
251
|
+
o=- 0 0 IN IP4 127.0.0.1
|
|
252
|
+
s=-
|
|
253
|
+
t=0 0
|
|
254
|
+
m=audio 9 UDP/TLS/RTP/SAVPF 111
|
|
255
|
+
a=mid:0
|
|
256
|
+
${rtpmap}
|
|
257
|
+
${fmtp}`,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('detects sprop-stereo=1 as a whole parameter', () => {
|
|
261
|
+
const { stereoMids } = extractStereoAndNackAudioFromOffer(
|
|
262
|
+
offerWith('a=rtpmap:111 opus/48000/2', 'a=fmtp:111 minptime=10;sprop-stereo=1'),
|
|
263
|
+
);
|
|
264
|
+
expect(stereoMids).toEqual(['0']);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('does not treat plain stereo=1 as sprop-stereo=1', () => {
|
|
268
|
+
const { stereoMids } = extractStereoAndNackAudioFromOffer(
|
|
269
|
+
offerWith('a=rtpmap:111 opus/48000/2', 'a=fmtp:111 minptime=10;stereo=1'),
|
|
270
|
+
);
|
|
271
|
+
expect(stereoMids).toEqual([]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('matches the opus codec case-insensitively (RFC 4855)', () => {
|
|
275
|
+
const { stereoMids } = extractStereoAndNackAudioFromOffer(
|
|
276
|
+
offerWith('a=rtpmap:111 OPUS/48000/2', 'a=fmtp:111 minptime=10;sprop-stereo=1'),
|
|
277
|
+
);
|
|
278
|
+
expect(stereoMids).toEqual(['0']);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// A single peer connection bundle as Chrome offers it: the section our camera sends on, the
|
|
283
|
+
// recvonly sections subscribed tracks arrive on, and one that lost AV1 in negotiation.
|
|
284
|
+
const SINGLE_PC_OFFER = `v=0
|
|
285
|
+
o=- 0 0 IN IP4 127.0.0.1
|
|
286
|
+
s=-
|
|
287
|
+
t=0 0
|
|
288
|
+
a=group:BUNDLE 0 1 2
|
|
289
|
+
m=video 9 UDP/TLS/RTP/SAVPF 96 45
|
|
290
|
+
c=IN IP4 0.0.0.0
|
|
291
|
+
a=mid:0
|
|
292
|
+
a=sendonly
|
|
293
|
+
a=msid:s cam
|
|
294
|
+
a=rtpmap:96 VP8/90000
|
|
295
|
+
a=rtpmap:45 AV1/90000
|
|
296
|
+
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
|
297
|
+
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
|
|
298
|
+
m=video 9 UDP/TLS/RTP/SAVPF 96 45
|
|
299
|
+
c=IN IP4 0.0.0.0
|
|
300
|
+
a=mid:1
|
|
301
|
+
a=recvonly
|
|
302
|
+
a=rtpmap:96 VP8/90000
|
|
303
|
+
a=rtpmap:45 AV1/90000
|
|
304
|
+
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
|
305
|
+
m=video 9 UDP/TLS/RTP/SAVPF 96
|
|
306
|
+
c=IN IP4 0.0.0.0
|
|
307
|
+
a=mid:2
|
|
308
|
+
a=recvonly
|
|
309
|
+
a=rtpmap:96 VP8/90000
|
|
310
|
+
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time`;
|
|
311
|
+
|
|
312
|
+
const sectionOf = (media: MediaDescription[], mid: string) =>
|
|
313
|
+
media.find((section) => `${section.mid}` === mid)!;
|
|
314
|
+
|
|
315
|
+
const ddOf = (media: MediaDescription[], mid: string) =>
|
|
316
|
+
sectionOf(media, mid).ext?.find((ext) => ext.uri === ddExtensionURI)?.value;
|
|
317
|
+
|
|
318
|
+
describe('ensureVideoDDExtension', () => {
|
|
319
|
+
it('assigns an id above every extension in the bundle', () => {
|
|
320
|
+
const sdp = parse(SINGLE_PC_OFFER);
|
|
321
|
+
// 11 is the highest in use, across all sections rather than just this one
|
|
322
|
+
expect(ensureVideoDDExtension(sectionOf(sdp.media, '1'), sdp, 0)).toBe(12);
|
|
323
|
+
expect(ddOf(sdp.media, '1')).toBe(12);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('reuses the id already chosen for the connection', () => {
|
|
327
|
+
const sdp = parse(SINGLE_PC_OFFER);
|
|
328
|
+
expect(ensureVideoDDExtension(sectionOf(sdp.media, '1'), sdp, 7)).toBe(7);
|
|
329
|
+
expect(ensureVideoDDExtension(sectionOf(sdp.media, '2'), sdp, 7)).toBe(7);
|
|
330
|
+
expect(ddOf(sdp.media, '1')).toBe(7);
|
|
331
|
+
expect(ddOf(sdp.media, '2')).toBe(7);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('leaves a section that already carries the extension alone, and reports its id', () => {
|
|
335
|
+
const sdp = parse(`${SINGLE_PC_OFFER}
|
|
336
|
+
a=extmap:3 ${ddExtensionURI}`);
|
|
337
|
+
expect(ensureVideoDDExtension(sectionOf(sdp.media, '2'), sdp, 0)).toBe(3);
|
|
338
|
+
expect(sectionOf(sdp.media, '2').ext).toHaveLength(2);
|
|
339
|
+
expect(ddOf(sdp.media, '2')).toBe(3);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('adopts the id the browser already advertises rather than inventing a second one', () => {
|
|
343
|
+
// Chrome maps the extension itself on the section it sends AV1 on, but not on recvonly
|
|
344
|
+
// ones. A bundle has to map the URI to one id, so the send section's id has to win over
|
|
345
|
+
// both a fresh id and the cached one.
|
|
346
|
+
const sdp = parse(SINGLE_PC_OFFER);
|
|
347
|
+
sectionOf(sdp.media, '0').ext!.push({ value: 13, uri: ddExtensionURI });
|
|
348
|
+
|
|
349
|
+
expect(ensureVideoDDExtension(sectionOf(sdp.media, '1'), sdp, 0)).toBe(13);
|
|
350
|
+
expect(ensureVideoDDExtension(sectionOf(sdp.media, '2'), sdp, 7)).toBe(13);
|
|
351
|
+
expect(ddOf(sdp.media, '1')).toBe(13);
|
|
352
|
+
expect(ddOf(sdp.media, '2')).toBe(13);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('steps over the id RFC 8285 reserves', () => {
|
|
356
|
+
const sdp = parse(SINGLE_PC_OFFER);
|
|
357
|
+
sectionOf(sdp.media, '0').ext!.push({ value: 14, uri: 'urn:3gpp:video-orientation' });
|
|
358
|
+
|
|
359
|
+
expect(ensureVideoDDExtension(sectionOf(sdp.media, '1'), sdp, 0)).toBe(16);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('abandons the cached id once something else stands for it', () => {
|
|
363
|
+
// The id was free when it was picked, then the first section we send on brought the fuller
|
|
364
|
+
// extension set along and claimed it. Reusing it anyway is what makes the browser reject
|
|
365
|
+
// the offer with "a BUNDLE group contains a codec collision for header extension id".
|
|
366
|
+
const sdp = parse(SINGLE_PC_OFFER);
|
|
367
|
+
sectionOf(sdp.media, '0').ext!.push({ value: 12, uri: 'urn:3gpp:video-orientation' });
|
|
368
|
+
|
|
369
|
+
expect(ensureVideoDDExtension(sectionOf(sdp.media, '1'), sdp, 12)).toBe(13);
|
|
370
|
+
expect(ddOf(sdp.media, '1')).toBe(13);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('leaves the offer alone when the mapped id is contested', () => {
|
|
374
|
+
// Nothing consistent for the whole bundle is available: the extension is mapped to 12 on one
|
|
375
|
+
// section and 12 means something else on another, and the browser's half of the map is not
|
|
376
|
+
// ours to renumber. Losing AV1 beats an offer that cannot be applied at all.
|
|
377
|
+
const sdp = parse(SINGLE_PC_OFFER);
|
|
378
|
+
sectionOf(sdp.media, '0').ext!.push({ value: 12, uri: 'urn:3gpp:video-orientation' });
|
|
379
|
+
sectionOf(sdp.media, '2').ext!.push({ value: 12, uri: ddExtensionURI });
|
|
380
|
+
|
|
381
|
+
expect(ensureVideoDDExtension(sectionOf(sdp.media, '1'), sdp, 0)).toBe(0);
|
|
382
|
+
expect(ddOf(sdp.media, '1')).toBeUndefined();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('adds the extension to a section that has none', () => {
|
|
386
|
+
const sdp = parse(SINGLE_PC_OFFER);
|
|
387
|
+
const section = sectionOf(sdp.media, '1');
|
|
388
|
+
delete section.ext;
|
|
389
|
+
expect(ensureVideoDDExtension(section, sdp, 0)).toBe(12);
|
|
390
|
+
expect(ddOf(sdp.media, '1')).toBe(12);
|
|
391
|
+
});
|
|
392
|
+
});
|