@stream-io/video-client 1.8.4 → 1.9.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/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +409 -441
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +409 -441
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +409 -441
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +10 -12
- package/dist/src/devices/CameraManager.d.ts +2 -22
- package/dist/src/events/internal.d.ts +0 -4
- package/dist/src/gen/video/sfu/event/events.d.ts +2 -71
- package/dist/src/helpers/sdp-munging.d.ts +8 -0
- package/dist/src/rtc/Publisher.d.ts +18 -23
- package/dist/src/rtc/bitrateLookup.d.ts +2 -0
- package/dist/src/rtc/codecs.d.ts +9 -2
- package/dist/src/rtc/videoLayers.d.ts +31 -4
- package/dist/src/types.d.ts +30 -2
- package/package.json +1 -1
- package/src/Call.ts +21 -38
- package/src/devices/CameraManager.ts +8 -42
- package/src/devices/ScreenShareManager.ts +1 -3
- package/src/devices/__tests__/CameraManager.test.ts +0 -15
- package/src/devices/__tests__/ScreenShareManager.test.ts +0 -14
- package/src/events/callEventHandlers.ts +0 -2
- package/src/events/internal.ts +0 -16
- package/src/gen/video/sfu/event/events.ts +8 -120
- package/src/helpers/sdp-munging.ts +38 -15
- package/src/rtc/Publisher.ts +211 -317
- package/src/rtc/__tests__/Publisher.test.ts +196 -7
- package/src/rtc/__tests__/bitrateLookup.test.ts +12 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +2 -0
- package/src/rtc/__tests__/videoLayers.test.ts +51 -36
- package/src/rtc/bitrateLookup.ts +61 -0
- package/src/rtc/codecs.ts +56 -9
- package/src/rtc/videoLayers.ts +68 -19
- package/src/types.ts +30 -2
package/src/rtc/Publisher.ts
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
import * as SDP from 'sdp-transform';
|
|
2
1
|
import { StreamSfuClient } from '../StreamSfuClient';
|
|
3
2
|
import {
|
|
4
3
|
PeerType,
|
|
5
4
|
TrackInfo,
|
|
6
5
|
TrackType,
|
|
7
6
|
VideoLayer,
|
|
8
|
-
VideoQuality,
|
|
9
7
|
} from '../gen/video/sfu/models/models';
|
|
10
8
|
import { getIceCandidate } from './helpers/iceCandidate';
|
|
11
9
|
import {
|
|
12
10
|
findOptimalScreenSharingLayers,
|
|
13
11
|
findOptimalVideoLayers,
|
|
14
12
|
OptimalVideoLayer,
|
|
13
|
+
ridToVideoQuality,
|
|
14
|
+
toSvcEncodings,
|
|
15
15
|
} from './videoLayers';
|
|
16
|
-
import { getPreferredCodecs,
|
|
16
|
+
import { getOptimalVideoCodec, getPreferredCodecs, isSvcCodec } from './codecs';
|
|
17
17
|
import { trackTypeToParticipantStreamKey } from './helpers/tracks';
|
|
18
18
|
import { CallingState, CallState } from '../store';
|
|
19
19
|
import { PublishOptions } from '../types';
|
|
20
|
-
import {
|
|
21
|
-
|
|
20
|
+
import {
|
|
21
|
+
enableHighQualityAudio,
|
|
22
|
+
extractMid,
|
|
23
|
+
toggleDtx,
|
|
24
|
+
} from '../helpers/sdp-munging';
|
|
22
25
|
import { Logger } from '../coordinator/connection/types';
|
|
23
26
|
import { getLogger } from '../logger';
|
|
24
27
|
import { Dispatcher } from './Dispatcher';
|
|
25
28
|
import { VideoLayerSetting } from '../gen/video/sfu/event/events';
|
|
26
29
|
import { TargetResolutionResponse } from '../gen/shims';
|
|
30
|
+
import { withoutConcurrency } from '../helpers/concurrency';
|
|
27
31
|
|
|
28
32
|
export type PublisherConstructorOpts = {
|
|
29
33
|
sfuClient: StreamSfuClient;
|
|
@@ -45,21 +49,9 @@ export class Publisher {
|
|
|
45
49
|
private readonly logger: Logger;
|
|
46
50
|
private pc: RTCPeerConnection;
|
|
47
51
|
private readonly state: CallState;
|
|
48
|
-
|
|
49
|
-
private readonly
|
|
50
|
-
|
|
51
|
-
} = {
|
|
52
|
-
[TrackType.AUDIO]: undefined,
|
|
53
|
-
[TrackType.VIDEO]: undefined,
|
|
54
|
-
[TrackType.SCREEN_SHARE]: undefined,
|
|
55
|
-
[TrackType.SCREEN_SHARE_AUDIO]: undefined,
|
|
56
|
-
[TrackType.UNSPECIFIED]: undefined,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
private readonly publishOptionsPerTrackType = new Map<
|
|
60
|
-
TrackType,
|
|
61
|
-
PublishOptions
|
|
62
|
-
>();
|
|
52
|
+
private readonly transceiverCache = new Map<TrackType, RTCRtpTransceiver>();
|
|
53
|
+
private readonly trackLayersCache = new Map<TrackType, OptimalVideoLayer[]>();
|
|
54
|
+
private readonly publishOptsForTrack = new Map<TrackType, PublishOptions>();
|
|
63
55
|
|
|
64
56
|
/**
|
|
65
57
|
* An array maintaining the order how transceivers were added to the peer connection.
|
|
@@ -69,52 +61,18 @@ export class Publisher {
|
|
|
69
61
|
* @internal
|
|
70
62
|
*/
|
|
71
63
|
private readonly transceiverInitOrder: TrackType[] = [];
|
|
72
|
-
|
|
73
|
-
private readonly trackKindMapping: {
|
|
74
|
-
[key in TrackType]: 'video' | 'audio' | undefined;
|
|
75
|
-
} = {
|
|
76
|
-
[TrackType.AUDIO]: 'audio',
|
|
77
|
-
[TrackType.VIDEO]: 'video',
|
|
78
|
-
[TrackType.SCREEN_SHARE]: 'video',
|
|
79
|
-
[TrackType.SCREEN_SHARE_AUDIO]: 'audio',
|
|
80
|
-
[TrackType.UNSPECIFIED]: undefined,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
private readonly trackLayersCache: {
|
|
84
|
-
[key in TrackType]: OptimalVideoLayer[] | undefined;
|
|
85
|
-
} = {
|
|
86
|
-
[TrackType.AUDIO]: undefined,
|
|
87
|
-
[TrackType.VIDEO]: undefined,
|
|
88
|
-
[TrackType.SCREEN_SHARE]: undefined,
|
|
89
|
-
[TrackType.SCREEN_SHARE_AUDIO]: undefined,
|
|
90
|
-
[TrackType.UNSPECIFIED]: undefined,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
64
|
private readonly isDtxEnabled: boolean;
|
|
94
65
|
private readonly isRedEnabled: boolean;
|
|
95
66
|
|
|
96
67
|
private readonly unsubscribeOnIceRestart: () => void;
|
|
68
|
+
private readonly unsubscribeChangePublishQuality: () => void;
|
|
97
69
|
private readonly onUnrecoverableError?: () => void;
|
|
98
70
|
|
|
99
71
|
private isIceRestarting = false;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* The SFU client instance to use for publishing and signaling.
|
|
103
|
-
*/
|
|
104
|
-
sfuClient: StreamSfuClient;
|
|
72
|
+
private sfuClient: StreamSfuClient;
|
|
105
73
|
|
|
106
74
|
/**
|
|
107
75
|
* Constructs a new `Publisher` instance.
|
|
108
|
-
*
|
|
109
|
-
* @param connectionConfig the connection configuration to use.
|
|
110
|
-
* @param sfuClient the SFU client to use.
|
|
111
|
-
* @param state the call state to use.
|
|
112
|
-
* @param dispatcher the dispatcher to use.
|
|
113
|
-
* @param isDtxEnabled whether DTX is enabled.
|
|
114
|
-
* @param isRedEnabled whether RED is enabled.
|
|
115
|
-
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
|
|
116
|
-
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
117
|
-
* @param logTag the log tag to use.
|
|
118
76
|
*/
|
|
119
77
|
constructor({
|
|
120
78
|
connectionConfig,
|
|
@@ -141,6 +99,21 @@ export class Publisher {
|
|
|
141
99
|
this.onUnrecoverableError?.();
|
|
142
100
|
});
|
|
143
101
|
});
|
|
102
|
+
|
|
103
|
+
this.unsubscribeChangePublishQuality = dispatcher.on(
|
|
104
|
+
'changePublishQuality',
|
|
105
|
+
({ videoSenders }) => {
|
|
106
|
+
withoutConcurrency('publisher.changePublishQuality', async () => {
|
|
107
|
+
for (const videoSender of videoSenders) {
|
|
108
|
+
const { layers } = videoSender;
|
|
109
|
+
const enabledLayers = layers.filter((l) => l.active);
|
|
110
|
+
await this.changePublishQuality(enabledLayers);
|
|
111
|
+
}
|
|
112
|
+
}).catch((err) => {
|
|
113
|
+
this.logger('warn', 'Failed to change publish quality', err);
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
);
|
|
144
117
|
}
|
|
145
118
|
|
|
146
119
|
private createPeerConnection = (connectionConfig?: RTCConfiguration) => {
|
|
@@ -167,14 +140,8 @@ export class Publisher {
|
|
|
167
140
|
close = ({ stopTracks }: { stopTracks: boolean }) => {
|
|
168
141
|
if (stopTracks) {
|
|
169
142
|
this.stopPublishing();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this.transceiverRegistry[trackType] = undefined;
|
|
173
|
-
});
|
|
174
|
-
Object.keys(this.trackLayersCache).forEach((trackType) => {
|
|
175
|
-
// @ts-ignore
|
|
176
|
-
this.trackLayersCache[trackType] = undefined;
|
|
177
|
-
});
|
|
143
|
+
this.transceiverCache.clear();
|
|
144
|
+
this.trackLayersCache.clear();
|
|
178
145
|
}
|
|
179
146
|
|
|
180
147
|
this.detachEventHandlers();
|
|
@@ -188,6 +155,7 @@ export class Publisher {
|
|
|
188
155
|
*/
|
|
189
156
|
detachEventHandlers = () => {
|
|
190
157
|
this.unsubscribeOnIceRestart();
|
|
158
|
+
this.unsubscribeChangePublishQuality();
|
|
191
159
|
|
|
192
160
|
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
193
161
|
this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
@@ -227,106 +195,97 @@ export class Publisher {
|
|
|
227
195
|
throw new Error(`Can't publish a track that has ended already.`);
|
|
228
196
|
}
|
|
229
197
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.find(
|
|
233
|
-
(t) =>
|
|
234
|
-
t === this.transceiverRegistry[trackType] &&
|
|
235
|
-
t.sender.track &&
|
|
236
|
-
t.sender.track?.kind === this.trackKindMapping[trackType],
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* An event handler which listens for the 'ended' event on the track.
|
|
241
|
-
* Once the track has ended, it will notify the SFU and update the state.
|
|
242
|
-
*/
|
|
243
|
-
const handleTrackEnded = () => {
|
|
244
|
-
this.logger(
|
|
245
|
-
'info',
|
|
246
|
-
`Track ${TrackType[trackType]} has ended abruptly, notifying the SFU`,
|
|
247
|
-
);
|
|
248
|
-
// cleanup, this event listener needs to run only once.
|
|
249
|
-
track.removeEventListener('ended', handleTrackEnded);
|
|
250
|
-
this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch(
|
|
251
|
-
(err) => this.logger('warn', `Couldn't notify track mute state`, err),
|
|
252
|
-
);
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
if (!transceiver) {
|
|
256
|
-
const { settings } = this.state;
|
|
257
|
-
const targetResolution = settings?.video
|
|
258
|
-
.target_resolution as TargetResolutionResponse;
|
|
259
|
-
const screenShareBitrate =
|
|
260
|
-
settings?.screensharing.target_resolution?.bitrate;
|
|
261
|
-
|
|
262
|
-
const videoEncodings =
|
|
263
|
-
trackType === TrackType.VIDEO
|
|
264
|
-
? findOptimalVideoLayers(track, targetResolution, opts)
|
|
265
|
-
: trackType === TrackType.SCREEN_SHARE
|
|
266
|
-
? findOptimalScreenSharingLayers(track, opts, screenShareBitrate)
|
|
267
|
-
: undefined;
|
|
198
|
+
// enable the track if it is disabled
|
|
199
|
+
if (!track.enabled) track.enabled = true;
|
|
268
200
|
|
|
201
|
+
const transceiver = this.transceiverCache.get(trackType);
|
|
202
|
+
if (!transceiver || !transceiver.sender.track) {
|
|
269
203
|
// listen for 'ended' event on the track as it might be ended abruptly
|
|
270
|
-
// by an external
|
|
204
|
+
// by an external factors such as permission revokes, a disconnected device, etc.
|
|
271
205
|
// keep in mind that `track.stop()` doesn't trigger this event.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
track.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
transceiver = this.pc.addTransceiver(track, {
|
|
278
|
-
direction: 'sendonly',
|
|
279
|
-
streams:
|
|
280
|
-
trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
|
|
281
|
-
? [mediaStream]
|
|
282
|
-
: undefined,
|
|
283
|
-
sendEncodings: videoEncodings,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
287
|
-
this.transceiverInitOrder.push(trackType);
|
|
288
|
-
this.transceiverRegistry[trackType] = transceiver;
|
|
289
|
-
this.publishOptionsPerTrackType.set(trackType, opts);
|
|
290
|
-
|
|
291
|
-
const { preferredCodec } = opts;
|
|
292
|
-
const codec =
|
|
293
|
-
isReactNative() && trackType === TrackType.VIDEO && !preferredCodec
|
|
294
|
-
? getRNOptimalCodec()
|
|
295
|
-
: preferredCodec;
|
|
296
|
-
|
|
297
|
-
const codecPreferences =
|
|
298
|
-
'setCodecPreferences' in transceiver
|
|
299
|
-
? this.getCodecPreferences(trackType, codec)
|
|
300
|
-
: undefined;
|
|
301
|
-
if (codecPreferences) {
|
|
302
|
-
this.logger(
|
|
303
|
-
'info',
|
|
304
|
-
`Setting ${TrackType[trackType]} codec preferences`,
|
|
305
|
-
codecPreferences,
|
|
206
|
+
const handleTrackEnded = () => {
|
|
207
|
+
this.logger('info', `Track ${TrackType[trackType]} has ended abruptly`);
|
|
208
|
+
track.removeEventListener('ended', handleTrackEnded);
|
|
209
|
+
this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch(
|
|
210
|
+
(err) => this.logger('warn', `Couldn't notify track mute state`, err),
|
|
306
211
|
);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
this.logger('warn', `Couldn't set codec preferences`, err);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
212
|
+
};
|
|
213
|
+
track.addEventListener('ended', handleTrackEnded);
|
|
214
|
+
this.addTransceiver(trackType, track, opts, mediaStream);
|
|
313
215
|
} else {
|
|
314
|
-
|
|
315
|
-
// don't stop the track if we are re-publishing the same track
|
|
316
|
-
if (previousTrack && previousTrack !== track) {
|
|
317
|
-
previousTrack.stop();
|
|
318
|
-
previousTrack.removeEventListener('ended', handleTrackEnded);
|
|
319
|
-
track.addEventListener('ended', handleTrackEnded);
|
|
320
|
-
}
|
|
321
|
-
if (!track.enabled) {
|
|
322
|
-
track.enabled = true;
|
|
323
|
-
}
|
|
324
|
-
await transceiver.sender.replaceTrack(track);
|
|
216
|
+
await this.updateTransceiver(transceiver, track);
|
|
325
217
|
}
|
|
326
218
|
|
|
327
219
|
await this.notifyTrackMuteStateChanged(mediaStream, trackType, false);
|
|
328
220
|
};
|
|
329
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Adds a new transceiver to the peer connection.
|
|
224
|
+
* This needs to be called when a new track kind is added to the peer connection.
|
|
225
|
+
* In other cases, use `updateTransceiver` method.
|
|
226
|
+
*/
|
|
227
|
+
private addTransceiver = (
|
|
228
|
+
trackType: TrackType,
|
|
229
|
+
track: MediaStreamTrack,
|
|
230
|
+
opts: PublishOptions,
|
|
231
|
+
mediaStream: MediaStream,
|
|
232
|
+
) => {
|
|
233
|
+
const { forceCodec, preferredCodec } = opts;
|
|
234
|
+
const codecInUse = forceCodec || getOptimalVideoCodec(preferredCodec);
|
|
235
|
+
const videoEncodings = this.computeLayers(trackType, track, opts);
|
|
236
|
+
const transceiver = this.pc.addTransceiver(track, {
|
|
237
|
+
direction: 'sendonly',
|
|
238
|
+
streams:
|
|
239
|
+
trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
|
|
240
|
+
? [mediaStream]
|
|
241
|
+
: undefined,
|
|
242
|
+
sendEncodings: isSvcCodec(codecInUse)
|
|
243
|
+
? toSvcEncodings(videoEncodings)
|
|
244
|
+
: videoEncodings,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
248
|
+
this.transceiverInitOrder.push(trackType);
|
|
249
|
+
this.transceiverCache.set(trackType, transceiver);
|
|
250
|
+
this.publishOptsForTrack.set(trackType, opts);
|
|
251
|
+
|
|
252
|
+
// handle codec preferences
|
|
253
|
+
if (!('setCodecPreferences' in transceiver)) return;
|
|
254
|
+
|
|
255
|
+
const codecPreferences = this.getCodecPreferences(
|
|
256
|
+
trackType,
|
|
257
|
+
trackType === TrackType.VIDEO ? codecInUse : undefined,
|
|
258
|
+
);
|
|
259
|
+
if (!codecPreferences) return;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
this.logger(
|
|
263
|
+
'info',
|
|
264
|
+
`Setting ${TrackType[trackType]} codec preferences`,
|
|
265
|
+
codecPreferences,
|
|
266
|
+
);
|
|
267
|
+
transceiver.setCodecPreferences(codecPreferences);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
this.logger('warn', `Couldn't set codec preferences`, err);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Updates the given transceiver with the new track.
|
|
275
|
+
* Stops the previous track and replaces it with the new one.
|
|
276
|
+
*/
|
|
277
|
+
private updateTransceiver = async (
|
|
278
|
+
transceiver: RTCRtpTransceiver,
|
|
279
|
+
track: MediaStreamTrack,
|
|
280
|
+
) => {
|
|
281
|
+
const previousTrack = transceiver.sender.track;
|
|
282
|
+
// don't stop the track if we are re-publishing the same track
|
|
283
|
+
if (previousTrack && previousTrack !== track) {
|
|
284
|
+
previousTrack.stop();
|
|
285
|
+
}
|
|
286
|
+
await transceiver.sender.replaceTrack(track);
|
|
287
|
+
};
|
|
288
|
+
|
|
330
289
|
/**
|
|
331
290
|
* Stops publishing the given track type to the SFU, if it is currently being published.
|
|
332
291
|
* Underlying track will be stopped and removed from the publisher.
|
|
@@ -334,9 +293,7 @@ export class Publisher {
|
|
|
334
293
|
* @param stopTrack specifies whether track should be stopped or just disabled
|
|
335
294
|
*/
|
|
336
295
|
unpublishStream = async (trackType: TrackType, stopTrack: boolean) => {
|
|
337
|
-
const transceiver = this.
|
|
338
|
-
.getTransceivers()
|
|
339
|
-
.find((t) => t === this.transceiverRegistry[trackType] && t.sender.track);
|
|
296
|
+
const transceiver = this.transceiverCache.get(trackType);
|
|
340
297
|
if (
|
|
341
298
|
transceiver &&
|
|
342
299
|
transceiver.sender.track &&
|
|
@@ -360,7 +317,7 @@ export class Publisher {
|
|
|
360
317
|
* @param trackType the track type to check.
|
|
361
318
|
*/
|
|
362
319
|
isPublishing = (trackType: TrackType): boolean => {
|
|
363
|
-
const transceiver = this.
|
|
320
|
+
const transceiver = this.transceiverCache.get(trackType);
|
|
364
321
|
if (!transceiver || !transceiver.sender) return false;
|
|
365
322
|
const track = transceiver.sender.track;
|
|
366
323
|
return !!track && track.readyState === 'live' && track.enabled;
|
|
@@ -406,14 +363,14 @@ export class Publisher {
|
|
|
406
363
|
});
|
|
407
364
|
};
|
|
408
365
|
|
|
409
|
-
|
|
366
|
+
private changePublishQuality = async (enabledLayers: VideoLayerSetting[]) => {
|
|
410
367
|
this.logger(
|
|
411
368
|
'info',
|
|
412
369
|
'Update publish quality, requested layers by SFU:',
|
|
413
370
|
enabledLayers,
|
|
414
371
|
);
|
|
415
372
|
|
|
416
|
-
const videoSender = this.
|
|
373
|
+
const videoSender = this.transceiverCache.get(TrackType.VIDEO)?.sender;
|
|
417
374
|
if (!videoSender) {
|
|
418
375
|
this.logger('warn', 'Update publish quality, no video sender found.');
|
|
419
376
|
return;
|
|
@@ -428,79 +385,64 @@ export class Publisher {
|
|
|
428
385
|
return;
|
|
429
386
|
}
|
|
430
387
|
|
|
388
|
+
const [codecInUse] = params.codecs;
|
|
389
|
+
const usesSvcCodec = codecInUse && isSvcCodec(codecInUse.mimeType);
|
|
390
|
+
|
|
431
391
|
let changed = false;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
392
|
+
for (const encoder of params.encodings) {
|
|
393
|
+
const layer = usesSvcCodec
|
|
394
|
+
? // for SVC, we only have one layer (q) and often rid is omitted
|
|
395
|
+
enabledLayers[0]
|
|
396
|
+
: // for non-SVC, we need to find the layer by rid (simulcast)
|
|
397
|
+
enabledLayers.find((l) => l.name === encoder.rid);
|
|
398
|
+
|
|
436
399
|
// flip 'active' flag only when necessary
|
|
437
|
-
const
|
|
438
|
-
if (
|
|
439
|
-
|
|
400
|
+
const shouldActivate = !!layer?.active;
|
|
401
|
+
if (shouldActivate !== encoder.active) {
|
|
402
|
+
encoder.active = shouldActivate;
|
|
440
403
|
changed = true;
|
|
441
404
|
}
|
|
442
|
-
if (shouldEnable) {
|
|
443
|
-
let layer = enabledLayers.find((vls) => vls.name === enc.rid);
|
|
444
|
-
if (layer !== undefined) {
|
|
445
|
-
if (
|
|
446
|
-
layer.scaleResolutionDownBy >= 1 &&
|
|
447
|
-
layer.scaleResolutionDownBy !== enc.scaleResolutionDownBy
|
|
448
|
-
) {
|
|
449
|
-
this.logger(
|
|
450
|
-
'debug',
|
|
451
|
-
'[dynascale]: setting scaleResolutionDownBy from server',
|
|
452
|
-
'layer',
|
|
453
|
-
layer.name,
|
|
454
|
-
'scale-resolution-down-by',
|
|
455
|
-
layer.scaleResolutionDownBy,
|
|
456
|
-
);
|
|
457
|
-
enc.scaleResolutionDownBy = layer.scaleResolutionDownBy;
|
|
458
|
-
changed = true;
|
|
459
|
-
}
|
|
460
405
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
layer.maxFramerate !== enc.maxFramerate
|
|
477
|
-
) {
|
|
478
|
-
this.logger(
|
|
479
|
-
'debug',
|
|
480
|
-
'[dynascale]: setting maxFramerate from server',
|
|
481
|
-
'layer',
|
|
482
|
-
layer.name,
|
|
483
|
-
'max-framerate',
|
|
484
|
-
layer.maxFramerate,
|
|
485
|
-
);
|
|
486
|
-
enc.maxFramerate = layer.maxFramerate;
|
|
487
|
-
changed = true;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
406
|
+
// skip the rest of the settings if the layer is disabled or not found
|
|
407
|
+
if (!layer) continue;
|
|
408
|
+
|
|
409
|
+
const {
|
|
410
|
+
maxFramerate,
|
|
411
|
+
scaleResolutionDownBy,
|
|
412
|
+
maxBitrate,
|
|
413
|
+
scalabilityMode,
|
|
414
|
+
} = layer;
|
|
415
|
+
if (
|
|
416
|
+
scaleResolutionDownBy >= 1 &&
|
|
417
|
+
scaleResolutionDownBy !== encoder.scaleResolutionDownBy
|
|
418
|
+
) {
|
|
419
|
+
encoder.scaleResolutionDownBy = scaleResolutionDownBy;
|
|
420
|
+
changed = true;
|
|
490
421
|
}
|
|
491
|
-
|
|
422
|
+
if (maxBitrate > 0 && maxBitrate !== encoder.maxBitrate) {
|
|
423
|
+
encoder.maxBitrate = maxBitrate;
|
|
424
|
+
changed = true;
|
|
425
|
+
}
|
|
426
|
+
if (maxFramerate > 0 && maxFramerate !== encoder.maxFramerate) {
|
|
427
|
+
encoder.maxFramerate = maxFramerate;
|
|
428
|
+
changed = true;
|
|
429
|
+
}
|
|
430
|
+
// @ts-expect-error scalabilityMode is not in the typedefs yet
|
|
431
|
+
if (scalabilityMode && scalabilityMode !== encoder.scalabilityMode) {
|
|
432
|
+
// @ts-expect-error scalabilityMode is not in the typedefs yet
|
|
433
|
+
encoder.scalabilityMode = scalabilityMode;
|
|
434
|
+
changed = true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
492
437
|
|
|
493
438
|
const activeLayers = params.encodings.filter((e) => e.active);
|
|
494
|
-
if (changed) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
'info',
|
|
498
|
-
`Update publish quality, enabled rids: `,
|
|
499
|
-
activeLayers,
|
|
500
|
-
);
|
|
501
|
-
} else {
|
|
502
|
-
this.logger('info', `Update publish quality, no change: `, activeLayers);
|
|
439
|
+
if (!changed) {
|
|
440
|
+
this.logger('info', `Update publish quality, no change:`, activeLayers);
|
|
441
|
+
return;
|
|
503
442
|
}
|
|
443
|
+
|
|
444
|
+
await videoSender.setParameters(params);
|
|
445
|
+
this.logger('info', `Update publish quality, enabled rids:`, activeLayers);
|
|
504
446
|
};
|
|
505
447
|
|
|
506
448
|
/**
|
|
@@ -514,7 +456,7 @@ export class Publisher {
|
|
|
514
456
|
|
|
515
457
|
private getCodecPreferences = (
|
|
516
458
|
trackType: TrackType,
|
|
517
|
-
preferredCodec?: string
|
|
459
|
+
preferredCodec?: string,
|
|
518
460
|
) => {
|
|
519
461
|
if (trackType === TrackType.VIDEO) {
|
|
520
462
|
return getPreferredCodecs('video', preferredCodec || 'vp8');
|
|
@@ -582,23 +524,22 @@ export class Publisher {
|
|
|
582
524
|
*/
|
|
583
525
|
private negotiate = async (options?: RTCOfferOptions) => {
|
|
584
526
|
const offer = await this.pc.createOffer(options);
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
527
|
+
if (offer.sdp) {
|
|
528
|
+
offer.sdp = toggleDtx(offer.sdp, this.isDtxEnabled);
|
|
529
|
+
if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
530
|
+
offer.sdp = this.enableHighQualityAudio(offer.sdp);
|
|
531
|
+
}
|
|
588
532
|
}
|
|
589
533
|
|
|
590
|
-
// set the munged SDP back to the offer
|
|
591
|
-
offer.sdp = sdp;
|
|
592
|
-
|
|
593
534
|
const trackInfos = this.getAnnouncedTracks(offer.sdp);
|
|
594
535
|
if (trackInfos.length === 0) {
|
|
595
536
|
throw new Error(`Can't negotiate without announcing any tracks`);
|
|
596
537
|
}
|
|
597
538
|
|
|
598
|
-
this.isIceRestarting = options?.iceRestart ?? false;
|
|
599
|
-
await this.pc.setLocalDescription(offer);
|
|
600
|
-
|
|
601
539
|
try {
|
|
540
|
+
this.isIceRestarting = options?.iceRestart ?? false;
|
|
541
|
+
await this.pc.setLocalDescription(offer);
|
|
542
|
+
|
|
602
543
|
const { response } = await this.sfuClient.setPublisher({
|
|
603
544
|
sdp: offer.sdp || '',
|
|
604
545
|
tracks: trackInfos,
|
|
@@ -623,61 +564,14 @@ export class Publisher {
|
|
|
623
564
|
};
|
|
624
565
|
|
|
625
566
|
private enableHighQualityAudio = (sdp: string) => {
|
|
626
|
-
const transceiver = this.
|
|
567
|
+
const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
|
|
627
568
|
if (!transceiver) return sdp;
|
|
628
569
|
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
private mungeCodecs = (sdp?: string) => {
|
|
634
|
-
if (sdp) {
|
|
635
|
-
sdp = toggleDtx(sdp, this.isDtxEnabled);
|
|
636
|
-
}
|
|
637
|
-
return sdp;
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
private extractMid = (
|
|
641
|
-
transceiver: RTCRtpTransceiver,
|
|
642
|
-
sdp: string | undefined,
|
|
643
|
-
trackType: TrackType,
|
|
644
|
-
): string => {
|
|
645
|
-
if (transceiver.mid) return transceiver.mid;
|
|
646
|
-
|
|
647
|
-
if (!sdp) {
|
|
648
|
-
this.logger('warn', 'No SDP found. Returning empty mid');
|
|
649
|
-
return '';
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
this.logger(
|
|
653
|
-
'debug',
|
|
654
|
-
`No 'mid' found for track. Trying to find it from the Offer SDP`,
|
|
570
|
+
const transceiverInitIndex = this.transceiverInitOrder.indexOf(
|
|
571
|
+
TrackType.SCREEN_SHARE_AUDIO,
|
|
655
572
|
);
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
const parsedSdp = SDP.parse(sdp);
|
|
659
|
-
const media = parsedSdp.media.find((m) => {
|
|
660
|
-
return (
|
|
661
|
-
m.type === track.kind &&
|
|
662
|
-
// if `msid` is not present, we assume that the track is the first one
|
|
663
|
-
(m.msid?.includes(track.id) ?? true)
|
|
664
|
-
);
|
|
665
|
-
});
|
|
666
|
-
if (typeof media?.mid === 'undefined') {
|
|
667
|
-
this.logger(
|
|
668
|
-
'debug',
|
|
669
|
-
`No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find it heuristically`,
|
|
670
|
-
);
|
|
671
|
-
|
|
672
|
-
const heuristicMid = this.transceiverInitOrder.indexOf(trackType);
|
|
673
|
-
if (heuristicMid !== -1) {
|
|
674
|
-
return String(heuristicMid);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
this.logger('debug', 'No heuristic mid found. Returning empty mid');
|
|
678
|
-
return '';
|
|
679
|
-
}
|
|
680
|
-
return String(media.mid);
|
|
573
|
+
const mid = extractMid(transceiver, transceiverInitIndex, sdp);
|
|
574
|
+
return enableHighQualityAudio(sdp, mid);
|
|
681
575
|
};
|
|
682
576
|
|
|
683
577
|
/**
|
|
@@ -688,35 +582,23 @@ export class Publisher {
|
|
|
688
582
|
*/
|
|
689
583
|
getAnnouncedTracks = (sdp?: string): TrackInfo[] => {
|
|
690
584
|
sdp = sdp || this.pc.localDescription?.sdp;
|
|
691
|
-
|
|
692
|
-
const { settings } = this.state;
|
|
693
|
-
const targetResolution = settings?.video
|
|
694
|
-
.target_resolution as TargetResolutionResponse;
|
|
695
585
|
return this.pc
|
|
696
586
|
.getTransceivers()
|
|
697
587
|
.filter((t) => t.direction === 'sendonly' && t.sender.track)
|
|
698
588
|
.map<TrackInfo>((transceiver) => {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
),
|
|
704
|
-
);
|
|
589
|
+
let trackType!: TrackType;
|
|
590
|
+
this.transceiverCache.forEach((value, key) => {
|
|
591
|
+
if (value === transceiver) trackType = key;
|
|
592
|
+
});
|
|
705
593
|
const track = transceiver.sender.track!;
|
|
706
594
|
let optimalLayers: OptimalVideoLayer[];
|
|
707
595
|
const isTrackLive = track.readyState === 'live';
|
|
708
596
|
if (isTrackLive) {
|
|
709
|
-
|
|
710
|
-
optimalLayers
|
|
711
|
-
trackType === TrackType.VIDEO
|
|
712
|
-
? findOptimalVideoLayers(track, targetResolution, publishOpts)
|
|
713
|
-
: trackType === TrackType.SCREEN_SHARE
|
|
714
|
-
? findOptimalScreenSharingLayers(track, publishOpts)
|
|
715
|
-
: [];
|
|
716
|
-
this.trackLayersCache[trackType] = optimalLayers;
|
|
597
|
+
optimalLayers = this.computeLayers(trackType, track) || [];
|
|
598
|
+
this.trackLayersCache.set(trackType, optimalLayers);
|
|
717
599
|
} else {
|
|
718
600
|
// we report the last known optimal layers for ended tracks
|
|
719
|
-
optimalLayers = this.trackLayersCache
|
|
601
|
+
optimalLayers = this.trackLayersCache.get(trackType) || [];
|
|
720
602
|
this.logger(
|
|
721
603
|
'debug',
|
|
722
604
|
`Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`,
|
|
@@ -728,7 +610,7 @@ export class Publisher {
|
|
|
728
610
|
rid: optimalLayer.rid || '',
|
|
729
611
|
bitrate: optimalLayer.maxBitrate || 0,
|
|
730
612
|
fps: optimalLayer.maxFramerate || 0,
|
|
731
|
-
quality:
|
|
613
|
+
quality: ridToVideoQuality(optimalLayer.rid || ''),
|
|
732
614
|
videoDimension: {
|
|
733
615
|
width: optimalLayer.width,
|
|
734
616
|
height: optimalLayer.height,
|
|
@@ -742,13 +624,13 @@ export class Publisher {
|
|
|
742
624
|
|
|
743
625
|
const trackSettings = track.getSettings();
|
|
744
626
|
const isStereo = isAudioTrack && trackSettings.channelCount === 2;
|
|
745
|
-
|
|
627
|
+
const transceiverInitIndex =
|
|
628
|
+
this.transceiverInitOrder.indexOf(trackType);
|
|
746
629
|
return {
|
|
747
630
|
trackId: track.id,
|
|
748
631
|
layers: layers,
|
|
749
632
|
trackType,
|
|
750
|
-
mid:
|
|
751
|
-
|
|
633
|
+
mid: extractMid(transceiver, transceiverInitIndex, sdp),
|
|
752
634
|
stereo: isStereo,
|
|
753
635
|
dtx: isAudioTrack && this.isDtxEnabled,
|
|
754
636
|
red: isAudioTrack && this.isRedEnabled,
|
|
@@ -757,6 +639,26 @@ export class Publisher {
|
|
|
757
639
|
});
|
|
758
640
|
};
|
|
759
641
|
|
|
642
|
+
private computeLayers = (
|
|
643
|
+
trackType: TrackType,
|
|
644
|
+
track: MediaStreamTrack,
|
|
645
|
+
opts?: PublishOptions,
|
|
646
|
+
): OptimalVideoLayer[] | undefined => {
|
|
647
|
+
const { settings } = this.state;
|
|
648
|
+
const targetResolution = settings?.video
|
|
649
|
+
.target_resolution as TargetResolutionResponse;
|
|
650
|
+
const screenShareBitrate =
|
|
651
|
+
settings?.screensharing.target_resolution?.bitrate;
|
|
652
|
+
|
|
653
|
+
const publishOpts = opts || this.publishOptsForTrack.get(trackType);
|
|
654
|
+
const codecInUse = getOptimalVideoCodec(publishOpts?.preferredCodec);
|
|
655
|
+
return trackType === TrackType.VIDEO
|
|
656
|
+
? findOptimalVideoLayers(track, targetResolution, codecInUse, publishOpts)
|
|
657
|
+
: trackType === TrackType.SCREEN_SHARE
|
|
658
|
+
? findOptimalScreenSharingLayers(track, publishOpts, screenShareBitrate)
|
|
659
|
+
: undefined;
|
|
660
|
+
};
|
|
661
|
+
|
|
760
662
|
private onIceCandidateError = (e: Event) => {
|
|
761
663
|
const errorMessage =
|
|
762
664
|
e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
@@ -789,12 +691,4 @@ export class Publisher {
|
|
|
789
691
|
private onSignalingStateChange = () => {
|
|
790
692
|
this.logger('debug', `Signaling state changed`, this.pc.signalingState);
|
|
791
693
|
};
|
|
792
|
-
|
|
793
|
-
private ridToVideoQuality = (rid: string): VideoQuality => {
|
|
794
|
-
return rid === 'q'
|
|
795
|
-
? VideoQuality.LOW_UNSPECIFIED
|
|
796
|
-
: rid === 'h'
|
|
797
|
-
? VideoQuality.MID
|
|
798
|
-
: VideoQuality.HIGH; // default to HIGH
|
|
799
|
-
};
|
|
800
694
|
}
|