@stream-io/video-client 1.24.0 → 1.25.0
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/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +335 -127
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +334 -126
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +335 -127
- package/dist/index.es.js.map +1 -1
- package/dist/src/StreamSfuClient.d.ts +12 -4
- package/dist/src/StreamVideoClient.d.ts +3 -1
- package/dist/src/coordinator/connection/errors.d.ts +1 -0
- package/dist/src/gen/video/sfu/models/models.d.ts +4 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +23 -4
- package/dist/src/rtc/NegotiationError.d.ts +15 -0
- package/dist/src/rtc/Publisher.d.ts +2 -2
- package/dist/src/rtc/helpers/sdp.d.ts +7 -0
- package/dist/src/types.d.ts +11 -0
- package/package.json +1 -1
- package/src/Call.ts +66 -38
- package/src/StreamSfuClient.ts +17 -7
- package/src/StreamVideoClient.ts +17 -7
- package/src/coordinator/connection/connection.ts +2 -1
- package/src/coordinator/connection/errors.ts +31 -0
- package/src/devices/ScreenShareManager.ts +12 -2
- package/src/devices/devices.ts +23 -12
- package/src/events/__tests__/internal.test.ts +1 -0
- package/src/gen/google/protobuf/struct.ts +2 -2
- package/src/gen/google/protobuf/timestamp.ts +1 -1
- package/src/gen/video/sfu/event/events.ts +15 -15
- package/src/gen/video/sfu/models/models.ts +9 -5
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +1 -1
- package/src/gen/video/sfu/signal_rpc/signal.ts +6 -6
- package/src/rtc/BasePeerConnection.ts +132 -46
- package/src/rtc/NegotiationError.ts +21 -0
- package/src/rtc/Publisher.ts +12 -9
- package/src/rtc/Subscriber.ts +8 -2
- package/src/rtc/__tests__/Publisher.test.ts +160 -17
- package/src/rtc/__tests__/Subscriber.test.ts +31 -14
- package/src/rtc/helpers/__tests__/sdp.stereo.test.ts +120 -0
- package/src/rtc/helpers/sdp.ts +43 -1
- package/src/types.ts +12 -0
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import './mocks/webrtc.mocks';
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { anyString } from 'vitest-mock-extended';
|
|
5
|
+
import { NegotiationError } from '../NegotiationError';
|
|
4
6
|
import { Publisher } from '../Publisher';
|
|
5
7
|
import { CallState } from '../../store';
|
|
6
8
|
import { StreamSfuClient } from '../../StreamSfuClient';
|
|
7
9
|
import { DispatchableMessage, Dispatcher } from '../Dispatcher';
|
|
8
10
|
import {
|
|
11
|
+
ErrorCode,
|
|
9
12
|
PeerType,
|
|
10
13
|
PublishOption,
|
|
11
|
-
SdkType,
|
|
12
14
|
TrackInfo,
|
|
13
15
|
TrackType,
|
|
16
|
+
WebsocketReconnectStrategy,
|
|
14
17
|
} from '../../gen/video/sfu/models/models';
|
|
18
|
+
import { SetPublisherResponse } from '../../gen/video/sfu/signal_rpc/signal';
|
|
15
19
|
import { SfuEvent } from '../../gen/video/sfu/event/events';
|
|
16
20
|
import { IceTrickleBuffer } from '../IceTrickleBuffer';
|
|
17
21
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
18
22
|
import { TransceiverCache } from '../TransceiverCache';
|
|
23
|
+
import { promiseWithResolvers } from '../../helpers/promise';
|
|
19
24
|
|
|
20
25
|
vi.mock('../../StreamSfuClient', () => {
|
|
21
26
|
console.log('MOCKING StreamSfuClient');
|
|
@@ -47,7 +52,7 @@ describe('Publisher', () => {
|
|
|
47
52
|
ice_servers: [],
|
|
48
53
|
},
|
|
49
54
|
logTag: 'test',
|
|
50
|
-
enableTracing:
|
|
55
|
+
enableTracing: true,
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
// @ts-expect-error readonly field
|
|
@@ -63,18 +68,6 @@ describe('Publisher', () => {
|
|
|
63
68
|
state,
|
|
64
69
|
logTag: 'test',
|
|
65
70
|
enableTracing: false,
|
|
66
|
-
clientDetails: {
|
|
67
|
-
sdk: {
|
|
68
|
-
type: SdkType.PLAIN_JAVASCRIPT,
|
|
69
|
-
major: '1',
|
|
70
|
-
minor: '0',
|
|
71
|
-
patch: '0',
|
|
72
|
-
},
|
|
73
|
-
device: {
|
|
74
|
-
name: 'test-device',
|
|
75
|
-
version: '1.0.0',
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
71
|
publishOptions: [
|
|
79
72
|
{
|
|
80
73
|
id: 1,
|
|
@@ -242,20 +235,169 @@ describe('Publisher', () => {
|
|
|
242
235
|
expect(publisher['negotiate']).not.toHaveBeenCalled();
|
|
243
236
|
});
|
|
244
237
|
|
|
238
|
+
it('should initiate new negotiation when ICE restart is requested', async () => {
|
|
239
|
+
// @ts-expect-error private method
|
|
240
|
+
vi.spyOn(publisher, 'negotiate').mockResolvedValue();
|
|
241
|
+
|
|
242
|
+
await publisher.restartIce();
|
|
243
|
+
expect(publisher['negotiate']).toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
245
246
|
it(`should perform ICE restart when connection state changes to 'failed'`, () => {
|
|
246
|
-
publisher
|
|
247
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
247
248
|
// @ts-expect-error private api
|
|
248
249
|
publisher['pc'].iceConnectionState = 'failed';
|
|
249
250
|
publisher['onIceConnectionStateChange']();
|
|
250
|
-
expect(publisher
|
|
251
|
+
expect(publisher.restartIce).toHaveBeenCalled();
|
|
251
252
|
});
|
|
252
253
|
|
|
253
|
-
it(`should perform ICE restart
|
|
254
|
+
it(`should perform rejoin when ICE restart fails after connection state changes to 'failed'`, async () => {
|
|
255
|
+
const { promise: lock, resolve: unlock } = promiseWithResolvers<void>();
|
|
256
|
+
publisher['onReconnectionNeeded'] = vi
|
|
257
|
+
.fn()
|
|
258
|
+
.mockImplementation(() => unlock());
|
|
259
|
+
vi.spyOn(publisher, 'restartIce').mockRejectedValue('ICE restart failed');
|
|
260
|
+
// @ts-expect-error private api
|
|
261
|
+
publisher['pc'].iceConnectionState = 'failed';
|
|
262
|
+
publisher['onIceConnectionStateChange']();
|
|
263
|
+
|
|
264
|
+
await lock;
|
|
265
|
+
expect(publisher.restartIce).toHaveBeenCalled();
|
|
266
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalled();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it(`should perform fast reconnect when ICE restart fails with SIGNAL_LOST error`, async () => {
|
|
270
|
+
const { promise: lock, resolve: unlock } = promiseWithResolvers<void>();
|
|
271
|
+
publisher['onReconnectionNeeded'] = vi
|
|
272
|
+
.fn()
|
|
273
|
+
.mockImplementation(() => unlock());
|
|
274
|
+
publisher.getAnnouncedTracks = vi
|
|
275
|
+
.fn()
|
|
276
|
+
.mockReturnValue([
|
|
277
|
+
{ trackId: '123', trackType: TrackType.VIDEO, mid: '0' },
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
// @ts-expect-error private api
|
|
281
|
+
vi.spyOn(publisher, 'negotiate');
|
|
282
|
+
vi.spyOn(publisher, 'restartIce');
|
|
283
|
+
|
|
284
|
+
const pc = publisher['pc'];
|
|
285
|
+
|
|
286
|
+
sfuClient.setPublisher = vi.fn().mockImplementation(() => {
|
|
287
|
+
// @ts-expect-error private api
|
|
288
|
+
pc.signalingState = 'have-local-offer';
|
|
289
|
+
return {
|
|
290
|
+
response: {
|
|
291
|
+
error: {
|
|
292
|
+
code: ErrorCode.PARTICIPANT_SIGNAL_LOST,
|
|
293
|
+
message: 'Signal lost',
|
|
294
|
+
shouldRetry: true,
|
|
295
|
+
},
|
|
296
|
+
} as SetPublisherResponse,
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// @ts-expect-error private api
|
|
301
|
+
pc.iceConnectionState = 'failed';
|
|
302
|
+
publisher['onIceConnectionStateChange']();
|
|
303
|
+
|
|
304
|
+
await lock;
|
|
305
|
+
expect(publisher.restartIce).toHaveBeenCalled();
|
|
306
|
+
expect(publisher['negotiate']).toHaveBeenCalled();
|
|
307
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledWith(
|
|
308
|
+
WebsocketReconnectStrategy.FAST,
|
|
309
|
+
anyString(),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
expect(pc.setLocalDescription).toHaveBeenCalledTimes(2);
|
|
313
|
+
expect(pc.setLocalDescription).toHaveBeenLastCalledWith({
|
|
314
|
+
type: 'rollback',
|
|
315
|
+
});
|
|
316
|
+
expect(pc.setRemoteDescription).not.toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it(`should perform REJOIN reconnect when ICE restart fails with any other error code`, async () => {
|
|
320
|
+
const { promise: lock, resolve: unlock } = promiseWithResolvers<void>();
|
|
321
|
+
publisher['onReconnectionNeeded'] = vi
|
|
322
|
+
.fn()
|
|
323
|
+
.mockImplementation(() => unlock());
|
|
324
|
+
publisher.getAnnouncedTracks = vi
|
|
325
|
+
.fn()
|
|
326
|
+
.mockReturnValue([
|
|
327
|
+
{ trackId: '123', trackType: TrackType.VIDEO, mid: '0' },
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
// @ts-expect-error private api
|
|
331
|
+
vi.spyOn(publisher, 'negotiate');
|
|
332
|
+
vi.spyOn(publisher, 'restartIce');
|
|
333
|
+
|
|
334
|
+
sfuClient.setPublisher = vi.fn().mockResolvedValue({
|
|
335
|
+
response: {
|
|
336
|
+
error: {
|
|
337
|
+
code: ErrorCode.PARTICIPANT_NOT_FOUND,
|
|
338
|
+
message: 'participant not found',
|
|
339
|
+
shouldRetry: true,
|
|
340
|
+
},
|
|
341
|
+
} as SetPublisherResponse,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// @ts-expect-error private api
|
|
345
|
+
publisher['pc'].iceConnectionState = 'failed';
|
|
346
|
+
publisher['onIceConnectionStateChange']();
|
|
347
|
+
|
|
348
|
+
await lock;
|
|
349
|
+
expect(publisher.restartIce).toHaveBeenCalled();
|
|
350
|
+
expect(publisher['negotiate']).toHaveBeenCalled();
|
|
351
|
+
await expect(publisher.restartIce).rejects.toThrowError(NegotiationError);
|
|
352
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledWith(
|
|
353
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
354
|
+
anyString(),
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it(`should schedule ICE restart when connection state changes to 'disconnected'`, () => {
|
|
254
359
|
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
360
|
+
vi.useFakeTimers();
|
|
361
|
+
// @ts-expect-error private api
|
|
362
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
363
|
+
publisher['onIceConnectionStateChange']();
|
|
364
|
+
|
|
365
|
+
vi.runOnlyPendingTimers();
|
|
366
|
+
expect(publisher.restartIce).toHaveBeenCalled();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it(`should perform rejoin when scheduled ICE restart fails`, async () => {
|
|
370
|
+
vi.spyOn(publisher, 'restartIce').mockRejectedValue('ICE restart failed');
|
|
371
|
+
const { promise: lock, resolve } = promiseWithResolvers<void>();
|
|
372
|
+
publisher['onReconnectionNeeded'] = vi
|
|
373
|
+
.fn()
|
|
374
|
+
.mockImplementation(() => resolve());
|
|
375
|
+
vi.useFakeTimers();
|
|
255
376
|
// @ts-expect-error private api
|
|
256
377
|
publisher['pc'].iceConnectionState = 'disconnected';
|
|
257
378
|
publisher['onIceConnectionStateChange']();
|
|
379
|
+
|
|
380
|
+
vi.runOnlyPendingTimers();
|
|
381
|
+
|
|
382
|
+
await lock;
|
|
258
383
|
expect(publisher.restartIce).toHaveBeenCalled();
|
|
384
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalled();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it(`should schedule ICE restart but cancel it if connection recovers in the meantime`, () => {
|
|
388
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
389
|
+
vi.useFakeTimers();
|
|
390
|
+
// @ts-expect-error private api
|
|
391
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
392
|
+
publisher['onIceConnectionStateChange']();
|
|
393
|
+
|
|
394
|
+
// @ts-expect-error private api
|
|
395
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
396
|
+
publisher['onIceConnectionStateChange']();
|
|
397
|
+
|
|
398
|
+
vi.runOnlyPendingTimers();
|
|
399
|
+
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
400
|
+
expect(publisher['iceRestartTimeout']).toBeUndefined();
|
|
259
401
|
});
|
|
260
402
|
});
|
|
261
403
|
|
|
@@ -721,6 +863,7 @@ describe('Publisher', () => {
|
|
|
721
863
|
it('isPublishing should return true if there are active tracks', () => {
|
|
722
864
|
expect(publisher.isPublishing(TrackType.VIDEO)).toBe(true);
|
|
723
865
|
expect(publisher.isPublishing(TrackType.SCREEN_SHARE_AUDIO)).toBe(false);
|
|
866
|
+
expect(publisher.isPublishing()).toBe(true);
|
|
724
867
|
});
|
|
725
868
|
|
|
726
869
|
it('getTrackType should return the track type', () => {
|
|
@@ -6,11 +6,13 @@ import { StreamSfuClient } from '../../StreamSfuClient';
|
|
|
6
6
|
import { Subscriber } from '../Subscriber';
|
|
7
7
|
import { CallState } from '../../store';
|
|
8
8
|
import { SfuEvent, SubscriberOffer } from '../../gen/video/sfu/event/events';
|
|
9
|
+
import { ICERestartResponse } from '../../gen/video/sfu/signal_rpc/signal';
|
|
9
10
|
import {
|
|
11
|
+
ErrorCode,
|
|
10
12
|
PeerType,
|
|
11
|
-
SdkType,
|
|
12
13
|
TrackType,
|
|
13
14
|
} from '../../gen/video/sfu/models/models';
|
|
15
|
+
import { NegotiationError } from '../NegotiationError';
|
|
14
16
|
import { IceTrickleBuffer } from '../IceTrickleBuffer';
|
|
15
17
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
16
18
|
|
|
@@ -43,7 +45,7 @@ describe('Subscriber', () => {
|
|
|
43
45
|
token: 'token',
|
|
44
46
|
ice_servers: [],
|
|
45
47
|
},
|
|
46
|
-
enableTracing:
|
|
48
|
+
enableTracing: true,
|
|
47
49
|
});
|
|
48
50
|
// @ts-expect-error readonly field
|
|
49
51
|
sfuClient.iceTrickleBuffer = new IceTrickleBuffer();
|
|
@@ -54,15 +56,7 @@ describe('Subscriber', () => {
|
|
|
54
56
|
state,
|
|
55
57
|
connectionConfig: { iceServers: [] },
|
|
56
58
|
logTag: 'test',
|
|
57
|
-
enableTracing:
|
|
58
|
-
clientDetails: {
|
|
59
|
-
sdk: {
|
|
60
|
-
type: SdkType.PLAIN_JAVASCRIPT,
|
|
61
|
-
major: '1',
|
|
62
|
-
minor: '0',
|
|
63
|
-
patch: '1',
|
|
64
|
-
},
|
|
65
|
-
},
|
|
59
|
+
enableTracing: true,
|
|
66
60
|
});
|
|
67
61
|
});
|
|
68
62
|
|
|
@@ -92,7 +86,7 @@ describe('Subscriber', () => {
|
|
|
92
86
|
});
|
|
93
87
|
|
|
94
88
|
it('should ask the SFU for ICE restart', async () => {
|
|
95
|
-
sfuClient.iceRestart = vi.fn();
|
|
89
|
+
sfuClient.iceRestart = vi.fn().mockResolvedValue({ response: {} });
|
|
96
90
|
// @ts-expect-error - private field
|
|
97
91
|
subscriber['pc'].connectionState = 'connected';
|
|
98
92
|
|
|
@@ -103,20 +97,43 @@ describe('Subscriber', () => {
|
|
|
103
97
|
});
|
|
104
98
|
|
|
105
99
|
it(`should perform ICE restart when connection state changes to 'failed'`, () => {
|
|
106
|
-
subscriber
|
|
100
|
+
vi.spyOn(subscriber, 'restartIce').mockResolvedValue();
|
|
107
101
|
// @ts-expect-error - private field
|
|
108
102
|
subscriber['pc'].iceConnectionState = 'failed';
|
|
109
103
|
subscriber['onIceConnectionStateChange']();
|
|
110
|
-
expect(subscriber['
|
|
104
|
+
expect(subscriber['restartIce']).toHaveBeenCalled();
|
|
111
105
|
});
|
|
112
106
|
|
|
113
107
|
it(`should perform ICE restart when connection state changes to 'disconnected'`, () => {
|
|
114
108
|
vi.spyOn(subscriber, 'restartIce').mockResolvedValue();
|
|
109
|
+
vi.useFakeTimers();
|
|
115
110
|
// @ts-expect-error - private field
|
|
116
111
|
subscriber['pc'].iceConnectionState = 'disconnected';
|
|
117
112
|
subscriber['onIceConnectionStateChange']();
|
|
113
|
+
vi.runOnlyPendingTimers();
|
|
118
114
|
expect(subscriber.restartIce).toHaveBeenCalled();
|
|
119
115
|
});
|
|
116
|
+
|
|
117
|
+
it(`should throw NegotiationError when SFU returns an error`, async () => {
|
|
118
|
+
sfuClient.iceRestart = vi.fn().mockResolvedValue({
|
|
119
|
+
response: {
|
|
120
|
+
error: {
|
|
121
|
+
code: ErrorCode.PARTICIPANT_SIGNAL_LOST,
|
|
122
|
+
message: 'Signal lost',
|
|
123
|
+
},
|
|
124
|
+
} as ICERestartResponse,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// @ts-expect-error - private field
|
|
128
|
+
subscriber['pc'].connectionState = 'connected';
|
|
129
|
+
|
|
130
|
+
await expect(subscriber.restartIce()).rejects.toThrowError(
|
|
131
|
+
NegotiationError,
|
|
132
|
+
);
|
|
133
|
+
expect(sfuClient.iceRestart).toHaveBeenCalledWith({
|
|
134
|
+
peerType: PeerType.SUBSCRIBER,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
120
137
|
});
|
|
121
138
|
|
|
122
139
|
describe('OnTrack', () => {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { enableStereo } from '../sdp';
|
|
3
|
+
|
|
4
|
+
const offerSdp = `o=- 5087842825318906515 1750254551 IN IP4 127.0.0.1
|
|
5
|
+
s=-
|
|
6
|
+
t=0 0
|
|
7
|
+
a=group:BUNDLE 0 1
|
|
8
|
+
a=extmap-allow-mixed
|
|
9
|
+
a=msid-semantic: WMS a1e5f21f716affb7:TRACK_TYPE_AUDIO:eO a1e5f21f716affb7:TRACK_TYPE_VIDEO:Nk
|
|
10
|
+
a=ice-lite
|
|
11
|
+
m=audio 51808 UDP/TLS/RTP/SAVPF 111 63 (19 more lines) direction=sendrecv mid=0
|
|
12
|
+
c=IN IP4 18.119.157.125
|
|
13
|
+
a=rtcp:51808 IN IP4 18.119.157.125
|
|
14
|
+
a=candidate:965407073 1 udp 2130706431 18.119.157.125 51808 typ host generation 0
|
|
15
|
+
a=candidate:965407073 2 udp 2130706431 18.119.157.125 51808 typ host generation 0
|
|
16
|
+
a=ice-ufrag:AFJnYPvMfEaZeHdt
|
|
17
|
+
a=ice-pwd:mbwUwrcoSApXwOyGrQOAsipWsfFGHcww
|
|
18
|
+
a=fingerprint:sha-256 13:79:52:41:12:BB:A7:5D:39:F0:9B:1A:95:58:94:D6:F9:D3:1E:00:A4:9D:CA:12:26:AE:7C:2A:E1:FC:42:F4
|
|
19
|
+
a=setup:actpass
|
|
20
|
+
a=mid:0
|
|
21
|
+
a=sendrecv
|
|
22
|
+
a=msid:a1e5f21f716affb7:TRACK_TYPE_AUDIO:eO 5040549b-8458-4646-892d-ad08f4475568
|
|
23
|
+
a=rtcp-mux
|
|
24
|
+
a=rtcp-rsize
|
|
25
|
+
a=rtpmap:111 opus/48000/2
|
|
26
|
+
a=fmtp:111 maxaveragebitrate=510000;minptime=10;sprop-stereo=1;stereo=1;useinbandfec=1
|
|
27
|
+
a=rtpmap:63 red/48000/2
|
|
28
|
+
a=fmtp:63 111/111
|
|
29
|
+
a=ssrc:1826085709 cname:a1e5f21f716affb7:TRACK_TYPE_AUDIO:eO
|
|
30
|
+
a=ssrc:1826085709 msid:a1e5f21f716affb7:TRACK_TYPE_AUDIO:eO 5040549b-8458-4646-892d-ad08f4475568
|
|
31
|
+
m=video 9 UDP/TLS/RTP/SAVPF 96 97 direction=sendrecv mid=1
|
|
32
|
+
c=IN IP4 0.0.0.0
|
|
33
|
+
a=rtcp:9 IN IP4 0.0.0.0
|
|
34
|
+
a=ice-ufrag:AFJnYPvMfEaZeHdt
|
|
35
|
+
a=ice-pwd:mbwUwrcoSApXwOyGrQOAsipWsfFGHcww
|
|
36
|
+
a=fingerprint:sha-256 13:79:52:41:12:BB:A7:5D:39:F0:9B:1A:95:58:94:D6:F9:D3:1E:00:A4:9D:CA:12:26:AE:7C:2A:E1:FC:42:F4
|
|
37
|
+
a=setup:actpass
|
|
38
|
+
a=mid:1
|
|
39
|
+
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
|
40
|
+
a=extmap:1 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
|
|
41
|
+
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
|
42
|
+
a=sendrecv
|
|
43
|
+
a=msid:a1e5f21f716affb7:TRACK_TYPE_VIDEO:Nk 40c4a11d-fa67-49b6-a4fa-1625b4bde4a5
|
|
44
|
+
a=rtcp-mux
|
|
45
|
+
a=rtcp-rsize
|
|
46
|
+
a=rtpmap:96 VP8/90000
|
|
47
|
+
a=rtcp-fb:96 ccm fir
|
|
48
|
+
a=rtcp-fb:96 nack
|
|
49
|
+
a=rtcp-fb:96 nack pli
|
|
50
|
+
a=rtcp-fb:96 goog-remb
|
|
51
|
+
a=rtcp-fb:96 transport-cc
|
|
52
|
+
a=rtpmap:97 rtx/90000
|
|
53
|
+
a=fmtp:97 apt=96
|
|
54
|
+
a=ssrc-group:FID 3718708632 2750164767
|
|
55
|
+
a=ssrc:3718708632 cname:a1e5f21f716affb7:TRACK_TYPE_VIDEO:Nk
|
|
56
|
+
a=ssrc:3718708632 msid:a1e5f21f716affb7:TRACK_TYPE_VIDEO:Nk 40c4a11d-fa67-49b6-a4fa-1625b4bde4a5
|
|
57
|
+
a=ssrc:2750164767 cname:a1e5f21f716affb7:TRACK_TYPE_VIDEO:Nk
|
|
58
|
+
a=ssrc:2750164767 msid:a1e5f21f716affb7:TRACK_TYPE_VIDEO:Nk 40c4a11d-fa67-49b6-a4fa-1625b4bde4a5
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const answerSdp = `v=0
|
|
62
|
+
o=- 6793916097087762106 2 IN IP4 127.0.0.1
|
|
63
|
+
s=-
|
|
64
|
+
t=0 0
|
|
65
|
+
a=group:BUNDLE 0 1
|
|
66
|
+
a=extmap-allow-mixed
|
|
67
|
+
a=msid-semantic: WMS
|
|
68
|
+
m=audio 9 UDP/TLS/RTP/SAVPF 111 63
|
|
69
|
+
c=IN IP4 0.0.0.0
|
|
70
|
+
a=rtcp:9 IN IP4 0.0.0.0
|
|
71
|
+
a=ice-ufrag:zcgT
|
|
72
|
+
a=ice-pwd:v559SXDwx4y9yAv7oeCvjsDR
|
|
73
|
+
a=ice-options:trickle
|
|
74
|
+
a=fingerprint:sha-256 F7:C8:B3:87:4A:AD:5A:86:48:1B:51:04:BE:CE:3B:D6:D3:7C:25:63:3E:9C:2B:F6:5B:8C:65:1F:72:8A:11:61
|
|
75
|
+
a=setup:active
|
|
76
|
+
a=mid:0
|
|
77
|
+
a=recvonly
|
|
78
|
+
a=rtcp-mux
|
|
79
|
+
a=rtcp-rsize
|
|
80
|
+
a=rtpmap:111 opus/48000/2
|
|
81
|
+
a=fmtp:111 minptime=10;useinbandfec=1
|
|
82
|
+
a=rtpmap:63 red/48000/2
|
|
83
|
+
a=fmtp:63 111/111
|
|
84
|
+
m=video 9 UDP/TLS/RTP/SAVPF 96 97
|
|
85
|
+
c=IN IP4 0.0.0.0
|
|
86
|
+
a=rtcp:9 IN IP4 0.0.0.0
|
|
87
|
+
a=ice-ufrag:zcgT
|
|
88
|
+
a=ice-pwd:v559SXDwx4y9yAv7oeCvjsDR
|
|
89
|
+
a=ice-options:trickle
|
|
90
|
+
a=fingerprint:sha-256 F7:C8:B3:87:4A:AD:5A:86:48:1B:51:04:BE:CE:3B:D6:D3:7C:25:63:3E:9C:2B:F6:5B:8C:65:1F:72:8A:11:61
|
|
91
|
+
a=setup:active
|
|
92
|
+
a=mid:1
|
|
93
|
+
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
|
94
|
+
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
|
95
|
+
a=extmap:1 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
|
|
96
|
+
a=recvonly
|
|
97
|
+
a=rtcp-mux
|
|
98
|
+
a=rtcp-rsize
|
|
99
|
+
a=rtpmap:96 VP8/90000
|
|
100
|
+
a=rtcp-fb:96 goog-remb
|
|
101
|
+
a=rtcp-fb:96 transport-cc
|
|
102
|
+
a=rtcp-fb:96 ccm fir
|
|
103
|
+
a=rtcp-fb:96 nack
|
|
104
|
+
a=rtcp-fb:96 nack pli
|
|
105
|
+
a=rtpmap:97 rtx/90000
|
|
106
|
+
a=fmtp:97 apt=96
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
describe('sdp - enableStereo', () => {
|
|
110
|
+
it('should enable stereo in the answer SDP based on the offered stereo in the offer SDP', () => {
|
|
111
|
+
const result = enableStereo(offerSdp, answerSdp);
|
|
112
|
+
expect(result).toContain('a=fmtp:111 minptime=10;useinbandfec=1;stereo=1');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should not modify the answer SDP if no stereo is offered', () => {
|
|
116
|
+
const modifiedOfferSdp = offerSdp.replace(/stereo=1/g, 'stereo=0');
|
|
117
|
+
const result = enableStereo(modifiedOfferSdp, answerSdp);
|
|
118
|
+
expect(result).toBe(answerSdp);
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/rtc/helpers/sdp.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parse } from 'sdp-transform';
|
|
1
|
+
import { parse, write } from 'sdp-transform';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Extracts the mid from the transceiver or the SDP.
|
|
@@ -28,3 +28,45 @@ export const extractMid = (
|
|
|
28
28
|
if (transceiverInitIndex < 0) return '';
|
|
29
29
|
return String(transceiverInitIndex);
|
|
30
30
|
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Enables stereo in the answer SDP based on the offered stereo in the offer SDP.
|
|
34
|
+
*
|
|
35
|
+
* @param offerSdp the offer SDP containing the stereo configuration.
|
|
36
|
+
* @param answerSdp the answer SDP to be modified.
|
|
37
|
+
*/
|
|
38
|
+
export const enableStereo = (offerSdp: string, answerSdp: string): string => {
|
|
39
|
+
const offeredStereoMids = new Set<string>();
|
|
40
|
+
const parsedOfferSdp = parse(offerSdp);
|
|
41
|
+
for (const media of parsedOfferSdp.media) {
|
|
42
|
+
if (media.type !== 'audio') continue;
|
|
43
|
+
|
|
44
|
+
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
45
|
+
if (!opus) continue;
|
|
46
|
+
|
|
47
|
+
for (const fmtp of media.fmtp) {
|
|
48
|
+
if (fmtp.payload === opus.payload && fmtp.config.includes('stereo=1')) {
|
|
49
|
+
offeredStereoMids.add(media.mid!);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// No stereo offered, return the original answerSdp
|
|
55
|
+
if (offeredStereoMids.size === 0) return answerSdp;
|
|
56
|
+
|
|
57
|
+
const parsedAnswerSdp = parse(answerSdp);
|
|
58
|
+
for (const media of parsedAnswerSdp.media) {
|
|
59
|
+
if (media.type !== 'audio' || !offeredStereoMids.has(media.mid!)) continue;
|
|
60
|
+
|
|
61
|
+
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
62
|
+
if (!opus) continue;
|
|
63
|
+
|
|
64
|
+
for (const fmtp of media.fmtp) {
|
|
65
|
+
if (fmtp.payload === opus.payload && !fmtp.config.includes('stereo=1')) {
|
|
66
|
+
fmtp.config += ';stereo=1';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return write(parsedAnswerSdp);
|
|
72
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -215,6 +215,18 @@ export type ScreenShareSettings = {
|
|
|
215
215
|
* Defaults to 3000000 (3Mbps).
|
|
216
216
|
*/
|
|
217
217
|
maxBitrate?: number;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The content hint to use when publishing the screen share.
|
|
221
|
+
* This can be used to optimize the video quality for different types of content.
|
|
222
|
+
*
|
|
223
|
+
* Defaults to '' (no hint, browser's default behavior).
|
|
224
|
+
* Use 'motion' for video content, 'detail' for presentations or documents, and 'text' for text-heavy content.
|
|
225
|
+
*
|
|
226
|
+
* Please read the documentation for more information on content hints:
|
|
227
|
+
* - https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/contentHint
|
|
228
|
+
*/
|
|
229
|
+
contentHint?: '' | 'motion' | 'detail' | 'text';
|
|
218
230
|
};
|
|
219
231
|
|
|
220
232
|
export type CallLeaveOptions = {
|