@stream-io/video-client 1.14.0 → 1.15.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 +1533 -1783
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1514 -1783
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1533 -1783
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +43 -28
- package/dist/src/StreamSfuClient.d.ts +4 -5
- package/dist/src/devices/CameraManager.d.ts +5 -8
- package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
- package/dist/src/devices/MicrophoneManager.d.ts +7 -2
- package/dist/src/devices/ScreenShareManager.d.ts +1 -2
- package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
- package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
- package/dist/src/helpers/array.d.ts +7 -0
- package/dist/src/permissions/PermissionsContext.d.ts +6 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
- package/dist/src/rtc/Dispatcher.d.ts +0 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
- package/dist/src/rtc/Publisher.d.ts +32 -86
- package/dist/src/rtc/Subscriber.d.ts +4 -56
- package/dist/src/rtc/TransceiverCache.d.ts +55 -0
- package/dist/src/rtc/codecs.d.ts +1 -15
- package/dist/src/rtc/helpers/sdp.d.ts +8 -0
- package/dist/src/rtc/helpers/tracks.d.ts +1 -0
- package/dist/src/rtc/index.d.ts +3 -0
- package/dist/src/rtc/videoLayers.d.ts +11 -25
- package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
- package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
- package/dist/src/stats/index.d.ts +1 -1
- package/dist/src/stats/types.d.ts +8 -0
- package/dist/src/types.d.ts +12 -22
- package/package.json +1 -1
- package/src/Call.ts +254 -268
- package/src/StreamSfuClient.ts +9 -14
- package/src/StreamVideoClient.ts +1 -1
- package/src/__tests__/Call.publishing.test.ts +306 -0
- package/src/devices/CameraManager.ts +33 -16
- package/src/devices/InputMediaDeviceManager.ts +38 -27
- package/src/devices/MicrophoneManager.ts +29 -8
- package/src/devices/ScreenShareManager.ts +6 -8
- package/src/devices/__tests__/CameraManager.test.ts +111 -14
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
- package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
- package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
- package/src/devices/__tests__/mocks.ts +1 -0
- package/src/events/__tests__/internal.test.ts +132 -0
- package/src/events/__tests__/mutes.test.ts +0 -3
- package/src/events/__tests__/speaker.test.ts +92 -0
- package/src/events/participant.ts +3 -4
- package/src/gen/video/sfu/event/events.ts +91 -30
- package/src/gen/video/sfu/models/models.ts +105 -13
- package/src/helpers/array.ts +14 -0
- package/src/permissions/PermissionsContext.ts +22 -0
- package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
- package/src/rpc/__tests__/createClient.test.ts +38 -0
- package/src/rpc/createClient.ts +11 -5
- package/src/rtc/BasePeerConnection.ts +240 -0
- package/src/rtc/Dispatcher.ts +0 -9
- package/src/rtc/IceTrickleBuffer.ts +24 -4
- package/src/rtc/Publisher.ts +210 -528
- package/src/rtc/Subscriber.ts +26 -200
- package/src/rtc/TransceiverCache.ts +120 -0
- package/src/rtc/__tests__/Publisher.test.ts +407 -210
- package/src/rtc/__tests__/Subscriber.test.ts +88 -36
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
- package/src/rtc/__tests__/videoLayers.test.ts +161 -54
- package/src/rtc/codecs.ts +1 -131
- package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
- package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
- package/src/rtc/helpers/sdp.ts +30 -0
- package/src/rtc/helpers/tracks.ts +3 -0
- package/src/rtc/index.ts +4 -0
- package/src/rtc/videoLayers.ts +68 -76
- package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
- package/src/stats/SfuStatsReporter.ts +31 -3
- package/src/stats/index.ts +1 -1
- package/src/stats/types.ts +12 -0
- package/src/types.ts +12 -22
- package/dist/src/helpers/sdp-munging.d.ts +0 -24
- package/dist/src/rtc/bitrateLookup.d.ts +0 -2
- package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
- package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
- package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
- package/src/helpers/sdp-munging.ts +0 -265
- package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
- package/src/rtc/__tests__/codecs.test.ts +0 -145
- package/src/rtc/bitrateLookup.ts +0 -61
- package/src/rtc/helpers/iceCandidate.ts +0 -16
- /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
- /package/src/{compatibility.ts → helpers/compatibility.ts} +0 -0
package/src/rtc/Publisher.ts
CHANGED
|
@@ -1,46 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BasePeerConnection,
|
|
3
|
+
BasePeerConnectionOpts,
|
|
4
|
+
} from './BasePeerConnection';
|
|
5
|
+
import { TransceiverCache } from './TransceiverCache';
|
|
2
6
|
import {
|
|
3
7
|
PeerType,
|
|
8
|
+
PublishOption,
|
|
4
9
|
TrackInfo,
|
|
5
10
|
TrackType,
|
|
6
|
-
VideoLayer,
|
|
7
11
|
} from '../gen/video/sfu/models/models';
|
|
8
|
-
import {
|
|
12
|
+
import { VideoSender } from '../gen/video/sfu/event/events';
|
|
9
13
|
import {
|
|
10
|
-
|
|
11
|
-
findOptimalVideoLayers,
|
|
12
|
-
OptimalVideoLayer,
|
|
13
|
-
ridToVideoQuality,
|
|
14
|
+
computeVideoLayers,
|
|
14
15
|
toSvcEncodings,
|
|
16
|
+
toVideoLayers,
|
|
15
17
|
} from './videoLayers';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { PublishOptions } from '../types';
|
|
20
|
-
import {
|
|
21
|
-
enableHighQualityAudio,
|
|
22
|
-
extractMid,
|
|
23
|
-
preserveCodec,
|
|
24
|
-
toggleDtx,
|
|
25
|
-
} from '../helpers/sdp-munging';
|
|
26
|
-
import { Logger } from '../coordinator/connection/types';
|
|
27
|
-
import { getLogger } from '../logger';
|
|
28
|
-
import { Dispatcher } from './Dispatcher';
|
|
29
|
-
import { VideoLayerSetting } from '../gen/video/sfu/event/events';
|
|
30
|
-
import { TargetResolutionResponse } from '../gen/shims';
|
|
18
|
+
import { isSvcCodec } from './codecs';
|
|
19
|
+
import { isAudioTrackType } from './helpers/tracks';
|
|
20
|
+
import { extractMid } from './helpers/sdp';
|
|
31
21
|
import { withoutConcurrency } from '../helpers/concurrency';
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
export type PublisherConstructorOpts = {
|
|
36
|
-
sfuClient: StreamSfuClient;
|
|
37
|
-
state: CallState;
|
|
38
|
-
dispatcher: Dispatcher;
|
|
39
|
-
connectionConfig?: RTCConfiguration;
|
|
40
|
-
isDtxEnabled: boolean;
|
|
41
|
-
isRedEnabled: boolean;
|
|
42
|
-
onUnrecoverableError?: () => void;
|
|
43
|
-
logTag: string;
|
|
22
|
+
|
|
23
|
+
export type PublisherConstructorOpts = BasePeerConnectionOpts & {
|
|
24
|
+
publishOptions: PublishOption[];
|
|
44
25
|
};
|
|
45
26
|
|
|
46
27
|
/**
|
|
@@ -48,54 +29,19 @@ export type PublisherConstructorOpts = {
|
|
|
48
29
|
*
|
|
49
30
|
* @internal
|
|
50
31
|
*/
|
|
51
|
-
export class Publisher {
|
|
52
|
-
private readonly
|
|
53
|
-
private
|
|
54
|
-
private readonly state: CallState;
|
|
55
|
-
private readonly transceiverCache = new Map<TrackType, RTCRtpTransceiver>();
|
|
56
|
-
private readonly trackLayersCache = new Map<TrackType, OptimalVideoLayer[]>();
|
|
57
|
-
private readonly publishOptsForTrack = new Map<TrackType, PublishOptions>();
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* An array maintaining the order how transceivers were added to the peer connection.
|
|
61
|
-
* This is needed because some browsers (Firefox) don't reliably report
|
|
62
|
-
* trackId and `mid` parameters.
|
|
63
|
-
*
|
|
64
|
-
* @internal
|
|
65
|
-
*/
|
|
66
|
-
private readonly transceiverInitOrder: TrackType[] = [];
|
|
67
|
-
private readonly isDtxEnabled: boolean;
|
|
68
|
-
private readonly isRedEnabled: boolean;
|
|
69
|
-
|
|
70
|
-
private readonly unsubscribeOnIceRestart: () => void;
|
|
71
|
-
private readonly unsubscribeChangePublishQuality: () => void;
|
|
72
|
-
private readonly onUnrecoverableError?: () => void;
|
|
73
|
-
|
|
74
|
-
private isIceRestarting = false;
|
|
75
|
-
private sfuClient: StreamSfuClient;
|
|
32
|
+
export class Publisher extends BasePeerConnection {
|
|
33
|
+
private readonly transceiverCache = new TransceiverCache();
|
|
34
|
+
private publishOptions: PublishOption[];
|
|
76
35
|
|
|
77
36
|
/**
|
|
78
37
|
* Constructs a new `Publisher` instance.
|
|
79
38
|
*/
|
|
80
|
-
constructor({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
isRedEnabled,
|
|
87
|
-
onUnrecoverableError,
|
|
88
|
-
logTag,
|
|
89
|
-
}: PublisherConstructorOpts) {
|
|
90
|
-
this.logger = getLogger(['Publisher', logTag]);
|
|
91
|
-
this.pc = this.createPeerConnection(connectionConfig);
|
|
92
|
-
this.sfuClient = sfuClient;
|
|
93
|
-
this.state = state;
|
|
94
|
-
this.isDtxEnabled = isDtxEnabled;
|
|
95
|
-
this.isRedEnabled = isRedEnabled;
|
|
96
|
-
this.onUnrecoverableError = onUnrecoverableError;
|
|
97
|
-
|
|
98
|
-
this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
39
|
+
constructor({ publishOptions, ...baseOptions }: PublisherConstructorOpts) {
|
|
40
|
+
super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
|
|
41
|
+
this.publishOptions = publishOptions;
|
|
42
|
+
this.pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
43
|
+
|
|
44
|
+
this.on('iceRestart', (iceRestart) => {
|
|
99
45
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED) return;
|
|
100
46
|
this.restartIce().catch((err) => {
|
|
101
47
|
this.logger('warn', `ICERestart failed`, err);
|
|
@@ -103,79 +49,27 @@ export class Publisher {
|
|
|
103
49
|
});
|
|
104
50
|
});
|
|
105
51
|
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const { layers } = videoSender;
|
|
112
|
-
const enabledLayers = layers.filter((l) => l.active);
|
|
113
|
-
await this.changePublishQuality(enabledLayers);
|
|
114
|
-
}
|
|
115
|
-
}).catch((err) => {
|
|
116
|
-
this.logger('warn', 'Failed to change publish quality', err);
|
|
117
|
-
});
|
|
118
|
-
},
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private createPeerConnection = (connectionConfig?: RTCConfiguration) => {
|
|
123
|
-
const pc = new RTCPeerConnection(connectionConfig);
|
|
124
|
-
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
125
|
-
pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
126
|
-
|
|
127
|
-
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
128
|
-
pc.addEventListener(
|
|
129
|
-
'iceconnectionstatechange',
|
|
130
|
-
this.onIceConnectionStateChange,
|
|
131
|
-
);
|
|
132
|
-
pc.addEventListener(
|
|
133
|
-
'icegatheringstatechange',
|
|
134
|
-
this.onIceGatheringStateChange,
|
|
135
|
-
);
|
|
136
|
-
pc.addEventListener('signalingstatechange', this.onSignalingStateChange);
|
|
137
|
-
return pc;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Closes the publisher PeerConnection and cleans up the resources.
|
|
142
|
-
*/
|
|
143
|
-
close = ({ stopTracks }: { stopTracks: boolean }) => {
|
|
144
|
-
if (stopTracks) {
|
|
145
|
-
this.stopPublishing();
|
|
146
|
-
this.transceiverCache.clear();
|
|
147
|
-
this.trackLayersCache.clear();
|
|
148
|
-
}
|
|
52
|
+
this.on('changePublishQuality', async (event) => {
|
|
53
|
+
for (const videoSender of event.videoSenders) {
|
|
54
|
+
await this.changePublishQuality(videoSender);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
149
57
|
|
|
150
|
-
this.
|
|
151
|
-
|
|
152
|
-
|
|
58
|
+
this.on('changePublishOptions', (event) => {
|
|
59
|
+
this.publishOptions = event.publishOptions;
|
|
60
|
+
return this.syncPublishOptions();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
153
63
|
|
|
154
64
|
/**
|
|
155
65
|
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
156
66
|
* This is useful when we want to replace the `RTCPeerConnection`
|
|
157
67
|
* instance with a new one (in case of migration).
|
|
158
68
|
*/
|
|
159
|
-
detachEventHandlers
|
|
160
|
-
|
|
161
|
-
this.unsubscribeChangePublishQuality();
|
|
162
|
-
|
|
163
|
-
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
69
|
+
detachEventHandlers() {
|
|
70
|
+
super.detachEventHandlers();
|
|
164
71
|
this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
165
|
-
|
|
166
|
-
this.pc.removeEventListener(
|
|
167
|
-
'iceconnectionstatechange',
|
|
168
|
-
this.onIceConnectionStateChange,
|
|
169
|
-
);
|
|
170
|
-
this.pc.removeEventListener(
|
|
171
|
-
'icegatheringstatechange',
|
|
172
|
-
this.onIceGatheringStateChange,
|
|
173
|
-
);
|
|
174
|
-
this.pc.removeEventListener(
|
|
175
|
-
'signalingstatechange',
|
|
176
|
-
this.onSignalingStateChange,
|
|
177
|
-
);
|
|
178
|
-
};
|
|
72
|
+
}
|
|
179
73
|
|
|
180
74
|
/**
|
|
181
75
|
* Starts publishing the given track of the given media stream.
|
|
@@ -183,135 +77,86 @@ export class Publisher {
|
|
|
183
77
|
* Consecutive calls to this method will replace the stream.
|
|
184
78
|
* The previous stream will be stopped.
|
|
185
79
|
*
|
|
186
|
-
* @param mediaStream the media stream to publish.
|
|
187
80
|
* @param track the track to publish.
|
|
188
81
|
* @param trackType the track type to publish.
|
|
189
|
-
* @param opts the optional publish options to use.
|
|
190
82
|
*/
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
trackType: TrackType,
|
|
195
|
-
opts: PublishOptions = {},
|
|
196
|
-
) => {
|
|
197
|
-
if (track.readyState === 'ended') {
|
|
198
|
-
throw new Error(`Can't publish a track that has ended already.`);
|
|
83
|
+
publish = async (track: MediaStreamTrack, trackType: TrackType) => {
|
|
84
|
+
if (!this.publishOptions.some((o) => o.trackType === trackType)) {
|
|
85
|
+
throw new Error(`No publish options found for ${TrackType[trackType]}`);
|
|
199
86
|
}
|
|
200
87
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// by an external factors such as permission revokes, a disconnected device, etc.
|
|
208
|
-
// keep in mind that `track.stop()` doesn't trigger this event.
|
|
209
|
-
const handleTrackEnded = () => {
|
|
210
|
-
this.logger('info', `Track ${TrackType[trackType]} has ended abruptly`);
|
|
211
|
-
track.removeEventListener('ended', handleTrackEnded);
|
|
212
|
-
this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch(
|
|
213
|
-
(err) => this.logger('warn', `Couldn't notify track mute state`, err),
|
|
214
|
-
);
|
|
215
|
-
};
|
|
216
|
-
track.addEventListener('ended', handleTrackEnded);
|
|
217
|
-
this.addTransceiver(trackType, track, opts, mediaStream);
|
|
218
|
-
} else {
|
|
219
|
-
await this.updateTransceiver(transceiver, track);
|
|
220
|
-
}
|
|
88
|
+
for (const publishOption of this.publishOptions) {
|
|
89
|
+
if (publishOption.trackType !== trackType) continue;
|
|
90
|
+
|
|
91
|
+
// create a clone of the track as otherwise the same trackId will
|
|
92
|
+
// appear in the SDP in multiple transceivers
|
|
93
|
+
const trackToPublish = track.clone();
|
|
221
94
|
|
|
222
|
-
|
|
95
|
+
const transceiver = this.transceiverCache.get(publishOption);
|
|
96
|
+
if (!transceiver) {
|
|
97
|
+
this.addTransceiver(trackToPublish, publishOption);
|
|
98
|
+
} else {
|
|
99
|
+
await transceiver.sender.replaceTrack(trackToPublish);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
223
102
|
};
|
|
224
103
|
|
|
225
104
|
/**
|
|
226
|
-
* Adds a new transceiver to the peer connection.
|
|
227
|
-
* This needs to be called when a new track kind is added to the peer connection.
|
|
228
|
-
* In other cases, use `updateTransceiver` method.
|
|
105
|
+
* Adds a new transceiver carrying the given track to the peer connection.
|
|
229
106
|
*/
|
|
230
107
|
private addTransceiver = (
|
|
231
|
-
trackType: TrackType,
|
|
232
108
|
track: MediaStreamTrack,
|
|
233
|
-
|
|
234
|
-
mediaStream: MediaStream,
|
|
109
|
+
publishOption: PublishOption,
|
|
235
110
|
) => {
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
111
|
+
const videoEncodings = computeVideoLayers(track, publishOption);
|
|
112
|
+
const sendEncodings = isSvcCodec(publishOption.codec?.name)
|
|
113
|
+
? toSvcEncodings(videoEncodings)
|
|
114
|
+
: videoEncodings;
|
|
239
115
|
const transceiver = this.pc.addTransceiver(track, {
|
|
240
116
|
direction: 'sendonly',
|
|
241
|
-
|
|
242
|
-
trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
|
|
243
|
-
? [mediaStream]
|
|
244
|
-
: undefined,
|
|
245
|
-
sendEncodings: isSvcCodec(codecInUse)
|
|
246
|
-
? toSvcEncodings(videoEncodings)
|
|
247
|
-
: videoEncodings,
|
|
117
|
+
sendEncodings,
|
|
248
118
|
});
|
|
249
119
|
|
|
120
|
+
const trackType = publishOption.trackType;
|
|
250
121
|
this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
251
|
-
this.
|
|
252
|
-
this.transceiverCache.set(trackType, transceiver);
|
|
253
|
-
this.publishOptsForTrack.set(trackType, opts);
|
|
254
|
-
|
|
255
|
-
// handle codec preferences
|
|
256
|
-
if (!('setCodecPreferences' in transceiver)) return;
|
|
257
|
-
|
|
258
|
-
const codecPreferences = this.getCodecPreferences(
|
|
259
|
-
trackType,
|
|
260
|
-
trackType === TrackType.VIDEO ? codecInUse : undefined,
|
|
261
|
-
'receiver',
|
|
262
|
-
);
|
|
263
|
-
if (!codecPreferences) return;
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
this.logger(
|
|
267
|
-
'info',
|
|
268
|
-
`Setting ${TrackType[trackType]} codec preferences`,
|
|
269
|
-
codecPreferences,
|
|
270
|
-
);
|
|
271
|
-
transceiver.setCodecPreferences(codecPreferences);
|
|
272
|
-
} catch (err) {
|
|
273
|
-
this.logger('warn', `Couldn't set codec preferences`, err);
|
|
274
|
-
}
|
|
122
|
+
this.transceiverCache.add(publishOption, transceiver);
|
|
275
123
|
};
|
|
276
124
|
|
|
277
125
|
/**
|
|
278
|
-
*
|
|
279
|
-
* Stops the previous track and replaces it with the new one.
|
|
126
|
+
* Synchronizes the current Publisher state with the provided publish options.
|
|
280
127
|
*/
|
|
281
|
-
private
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
128
|
+
private syncPublishOptions = async () => {
|
|
129
|
+
// enable publishing with new options -> [av1, vp9]
|
|
130
|
+
for (const publishOption of this.publishOptions) {
|
|
131
|
+
const { trackType } = publishOption;
|
|
132
|
+
if (!this.isPublishing(trackType)) continue;
|
|
133
|
+
if (this.transceiverCache.has(publishOption)) continue;
|
|
134
|
+
|
|
135
|
+
const item = this.transceiverCache.find(
|
|
136
|
+
(i) =>
|
|
137
|
+
!!i.transceiver.sender.track &&
|
|
138
|
+
i.publishOption.trackType === trackType,
|
|
139
|
+
);
|
|
140
|
+
if (!item || !item.transceiver) continue;
|
|
141
|
+
|
|
142
|
+
// take the track from the existing transceiver for the same track type,
|
|
143
|
+
// clone it and publish it with the new publish options
|
|
144
|
+
const track = item.transceiver.sender.track!.clone();
|
|
145
|
+
this.addTransceiver(track, publishOption);
|
|
289
146
|
}
|
|
290
|
-
await transceiver.sender.replaceTrack(track);
|
|
291
|
-
};
|
|
292
147
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
transceiver.sender.track
|
|
304
|
-
(
|
|
305
|
-
? transceiver.sender.track.readyState === 'live'
|
|
306
|
-
: transceiver.sender.track.enabled)
|
|
307
|
-
) {
|
|
308
|
-
stopTrack
|
|
309
|
-
? transceiver.sender.track.stop()
|
|
310
|
-
: (transceiver.sender.track.enabled = false);
|
|
311
|
-
// We don't need to notify SFU if unpublishing in response to remote soft mute
|
|
312
|
-
if (this.state.localParticipant?.publishedTracks.includes(trackType)) {
|
|
313
|
-
await this.notifyTrackMuteStateChanged(undefined, trackType, true);
|
|
314
|
-
}
|
|
148
|
+
// stop publishing with options not required anymore -> [vp9]
|
|
149
|
+
for (const item of this.transceiverCache.items()) {
|
|
150
|
+
const { publishOption, transceiver } = item;
|
|
151
|
+
const hasPublishOption = this.publishOptions.some(
|
|
152
|
+
(option) =>
|
|
153
|
+
option.id === publishOption.id &&
|
|
154
|
+
option.trackType === publishOption.trackType,
|
|
155
|
+
);
|
|
156
|
+
if (hasPublishOption) continue;
|
|
157
|
+
// it is safe to stop the track here, it is a clone
|
|
158
|
+
transceiver.sender.track?.stop();
|
|
159
|
+
await transceiver.sender.replaceTrack(null);
|
|
315
160
|
}
|
|
316
161
|
};
|
|
317
162
|
|
|
@@ -321,72 +166,59 @@ export class Publisher {
|
|
|
321
166
|
* @param trackType the track type to check.
|
|
322
167
|
*/
|
|
323
168
|
isPublishing = (trackType: TrackType): boolean => {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
const track = transceiver.sender.track;
|
|
327
|
-
return !!track && track.readyState === 'live' && track.enabled;
|
|
328
|
-
};
|
|
169
|
+
for (const item of this.transceiverCache.items()) {
|
|
170
|
+
if (item.publishOption.trackType !== trackType) continue;
|
|
329
171
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
) => {
|
|
335
|
-
await this.sfuClient.updateMuteState(trackType, isMuted);
|
|
336
|
-
|
|
337
|
-
const audioOrVideoOrScreenShareStream =
|
|
338
|
-
trackTypeToParticipantStreamKey(trackType);
|
|
339
|
-
if (!audioOrVideoOrScreenShareStream) return;
|
|
340
|
-
if (isMuted) {
|
|
341
|
-
this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
|
|
342
|
-
publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
|
|
343
|
-
[audioOrVideoOrScreenShareStream]: undefined,
|
|
344
|
-
}));
|
|
345
|
-
} else {
|
|
346
|
-
this.state.updateParticipant(this.sfuClient.sessionId, (p) => {
|
|
347
|
-
return {
|
|
348
|
-
publishedTracks: p.publishedTracks.includes(trackType)
|
|
349
|
-
? p.publishedTracks
|
|
350
|
-
: [...p.publishedTracks, trackType],
|
|
351
|
-
[audioOrVideoOrScreenShareStream]: mediaStream,
|
|
352
|
-
};
|
|
353
|
-
});
|
|
172
|
+
const track = item.transceiver.sender.track;
|
|
173
|
+
if (!track) continue;
|
|
174
|
+
|
|
175
|
+
if (track.readyState === 'live' && track.enabled) return true;
|
|
354
176
|
}
|
|
177
|
+
return false;
|
|
355
178
|
};
|
|
356
179
|
|
|
357
180
|
/**
|
|
358
|
-
*
|
|
181
|
+
* Maps the given track ID to the corresponding track type.
|
|
359
182
|
*/
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.pc.removeTrack(s);
|
|
183
|
+
getTrackType = (trackId: string): TrackType | undefined => {
|
|
184
|
+
for (const transceiverId of this.transceiverCache.items()) {
|
|
185
|
+
const { publishOption, transceiver } = transceiverId;
|
|
186
|
+
if (transceiver.sender.track?.id === trackId) {
|
|
187
|
+
return publishOption.trackType;
|
|
366
188
|
}
|
|
367
|
-
}
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
368
191
|
};
|
|
369
192
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Stops the cloned track that is being published to the SFU.
|
|
195
|
+
*/
|
|
196
|
+
stopTracks = (...trackTypes: TrackType[]) => {
|
|
197
|
+
for (const item of this.transceiverCache.items()) {
|
|
198
|
+
const { publishOption, transceiver } = item;
|
|
199
|
+
if (!trackTypes.includes(publishOption.trackType)) continue;
|
|
200
|
+
transceiver.sender.track?.stop();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
376
203
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
204
|
+
private changePublishQuality = async (videoSender: VideoSender) => {
|
|
205
|
+
const { trackType, layers, publishOptionId } = videoSender;
|
|
206
|
+
const enabledLayers = layers.filter((l) => l.active);
|
|
207
|
+
|
|
208
|
+
const tag = 'Update publish quality:';
|
|
209
|
+
this.logger('info', `${tag} requested layers by SFU:`, enabledLayers);
|
|
210
|
+
|
|
211
|
+
const sender = this.transceiverCache.getWith(
|
|
212
|
+
trackType,
|
|
213
|
+
publishOptionId,
|
|
214
|
+
)?.sender;
|
|
215
|
+
if (!sender) {
|
|
216
|
+
return this.logger('warn', `${tag} no video sender found.`);
|
|
381
217
|
}
|
|
382
218
|
|
|
383
|
-
const params =
|
|
219
|
+
const params = sender.getParameters();
|
|
384
220
|
if (params.encodings.length === 0) {
|
|
385
|
-
this.logger(
|
|
386
|
-
'warn',
|
|
387
|
-
'Update publish quality, No suitable video encoding quality found',
|
|
388
|
-
);
|
|
389
|
-
return;
|
|
221
|
+
return this.logger('warn', `${tag} there are no encodings set.`);
|
|
390
222
|
}
|
|
391
223
|
|
|
392
224
|
const [codecInUse] = params.codecs;
|
|
@@ -440,73 +272,13 @@ export class Publisher {
|
|
|
440
272
|
}
|
|
441
273
|
}
|
|
442
274
|
|
|
443
|
-
const
|
|
275
|
+
const activeEncoders = params.encodings.filter((e) => e.active);
|
|
444
276
|
if (!changed) {
|
|
445
|
-
this.logger('info',
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
await videoSender.setParameters(params);
|
|
450
|
-
this.logger('info', `Update publish quality, enabled rids:`, activeLayers);
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Returns the result of the `RTCPeerConnection.getStats()` method
|
|
455
|
-
* @param selector
|
|
456
|
-
* @returns
|
|
457
|
-
*/
|
|
458
|
-
getStats = (selector?: MediaStreamTrack | null | undefined) => {
|
|
459
|
-
return this.pc.getStats(selector);
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
private getCodecPreferences = (
|
|
463
|
-
trackType: TrackType,
|
|
464
|
-
preferredCodec: string | undefined,
|
|
465
|
-
codecPreferencesSource: 'sender' | 'receiver',
|
|
466
|
-
) => {
|
|
467
|
-
if (trackType === TrackType.VIDEO) {
|
|
468
|
-
return getPreferredCodecs(
|
|
469
|
-
'video',
|
|
470
|
-
preferredCodec || 'vp8',
|
|
471
|
-
undefined,
|
|
472
|
-
codecPreferencesSource,
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
if (trackType === TrackType.AUDIO) {
|
|
476
|
-
const defaultAudioCodec = this.isRedEnabled ? 'red' : 'opus';
|
|
477
|
-
const codecToRemove = !this.isRedEnabled ? 'red' : undefined;
|
|
478
|
-
return getPreferredCodecs(
|
|
479
|
-
'audio',
|
|
480
|
-
preferredCodec ?? defaultAudioCodec,
|
|
481
|
-
codecToRemove,
|
|
482
|
-
codecPreferencesSource,
|
|
483
|
-
);
|
|
277
|
+
return this.logger('info', `${tag} no change:`, activeEncoders);
|
|
484
278
|
}
|
|
485
|
-
};
|
|
486
279
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (!candidate) {
|
|
490
|
-
this.logger('debug', 'null ice candidate');
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
this.sfuClient
|
|
494
|
-
.iceTrickle({
|
|
495
|
-
iceCandidate: getIceCandidate(candidate),
|
|
496
|
-
peerType: PeerType.PUBLISHER_UNSPECIFIED,
|
|
497
|
-
})
|
|
498
|
-
.catch((err) => {
|
|
499
|
-
this.logger('warn', `ICETrickle failed`, err);
|
|
500
|
-
});
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Sets the SFU client to use.
|
|
505
|
-
*
|
|
506
|
-
* @param sfuClient the SFU client to use.
|
|
507
|
-
*/
|
|
508
|
-
setSfuClient = (sfuClient: StreamSfuClient) => {
|
|
509
|
-
this.sfuClient = sfuClient;
|
|
280
|
+
await sender.setParameters(params);
|
|
281
|
+
this.logger('info', `${tag} enabled rids:`, activeEncoders);
|
|
510
282
|
};
|
|
511
283
|
|
|
512
284
|
/**
|
|
@@ -523,10 +295,12 @@ export class Publisher {
|
|
|
523
295
|
};
|
|
524
296
|
|
|
525
297
|
private onNegotiationNeeded = () => {
|
|
526
|
-
this.negotiate().catch(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
298
|
+
withoutConcurrency('publisher.negotiate', () => this.negotiate()).catch(
|
|
299
|
+
(err) => {
|
|
300
|
+
this.logger('error', `Negotiation failed.`, err);
|
|
301
|
+
this.onUnrecoverableError?.();
|
|
302
|
+
},
|
|
303
|
+
);
|
|
530
304
|
};
|
|
531
305
|
|
|
532
306
|
/**
|
|
@@ -536,19 +310,6 @@ export class Publisher {
|
|
|
536
310
|
*/
|
|
537
311
|
private negotiate = async (options?: RTCOfferOptions) => {
|
|
538
312
|
const offer = await this.pc.createOffer(options);
|
|
539
|
-
if (offer.sdp) {
|
|
540
|
-
offer.sdp = toggleDtx(offer.sdp, this.isDtxEnabled);
|
|
541
|
-
if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
542
|
-
offer.sdp = this.enableHighQualityAudio(offer.sdp);
|
|
543
|
-
}
|
|
544
|
-
if (this.isPublishing(TrackType.VIDEO)) {
|
|
545
|
-
// Hotfix for platforms that don't respect the ordered codec list
|
|
546
|
-
// (Firefox, Android, Linux, etc...).
|
|
547
|
-
// We remove all the codecs from the SDP except the one we want to use.
|
|
548
|
-
offer.sdp = this.removeUnpreferredCodecs(offer.sdp, TrackType.VIDEO);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
313
|
const trackInfos = this.getAnnouncedTracks(offer.sdp);
|
|
553
314
|
if (trackInfos.length === 0) {
|
|
554
315
|
throw new Error(`Can't negotiate without announcing any tracks`);
|
|
@@ -569,164 +330,85 @@ export class Publisher {
|
|
|
569
330
|
this.isIceRestarting = false;
|
|
570
331
|
}
|
|
571
332
|
|
|
572
|
-
this.
|
|
573
|
-
async (candidate) => {
|
|
574
|
-
try {
|
|
575
|
-
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
576
|
-
await this.pc.addIceCandidate(iceCandidate);
|
|
577
|
-
} catch (e) {
|
|
578
|
-
this.logger('warn', `ICE candidate error`, e, candidate);
|
|
579
|
-
}
|
|
580
|
-
},
|
|
581
|
-
);
|
|
333
|
+
this.addTrickledIceCandidates();
|
|
582
334
|
};
|
|
583
335
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const transceiver = this.transceiverCache.get(trackType);
|
|
595
|
-
if (!transceiver) return sdp;
|
|
596
|
-
|
|
597
|
-
const index = this.transceiverInitOrder.indexOf(trackType);
|
|
598
|
-
const mid = extractMid(transceiver, index, sdp);
|
|
599
|
-
const [codecToPreserve] = orderedCodecs;
|
|
600
|
-
return preserveCodec(sdp, mid, codecToPreserve);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
private enableHighQualityAudio = (sdp: string) => {
|
|
604
|
-
const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
|
|
605
|
-
if (!transceiver) return sdp;
|
|
606
|
-
|
|
607
|
-
const transceiverInitIndex = this.transceiverInitOrder.indexOf(
|
|
608
|
-
TrackType.SCREEN_SHARE_AUDIO,
|
|
609
|
-
);
|
|
610
|
-
const mid = extractMid(transceiver, transceiverInitIndex, sdp);
|
|
611
|
-
return enableHighQualityAudio(sdp, mid);
|
|
336
|
+
/**
|
|
337
|
+
* Returns a list of tracks that are currently being published.
|
|
338
|
+
*/
|
|
339
|
+
getPublishedTracks = (): MediaStreamTrack[] => {
|
|
340
|
+
const tracks: MediaStreamTrack[] = [];
|
|
341
|
+
for (const { transceiver } of this.transceiverCache.items()) {
|
|
342
|
+
const track = transceiver.sender.track;
|
|
343
|
+
if (track && track.readyState === 'live') tracks.push(track);
|
|
344
|
+
}
|
|
345
|
+
return tracks;
|
|
612
346
|
};
|
|
613
347
|
|
|
614
348
|
/**
|
|
615
349
|
* Returns a list of tracks that are currently being published.
|
|
616
|
-
*
|
|
617
|
-
* @internal
|
|
618
350
|
* @param sdp an optional SDP to extract the `mid` from.
|
|
619
351
|
*/
|
|
620
|
-
getAnnouncedTracks = (sdp
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (value === transceiver) trackType = key;
|
|
629
|
-
});
|
|
630
|
-
const track = transceiver.sender.track!;
|
|
631
|
-
let optimalLayers: OptimalVideoLayer[];
|
|
632
|
-
const isTrackLive = track.readyState === 'live';
|
|
633
|
-
if (isTrackLive) {
|
|
634
|
-
optimalLayers = this.computeLayers(trackType, track) || [];
|
|
635
|
-
this.trackLayersCache.set(trackType, optimalLayers);
|
|
636
|
-
} else {
|
|
637
|
-
// we report the last known optimal layers for ended tracks
|
|
638
|
-
optimalLayers = this.trackLayersCache.get(trackType) || [];
|
|
639
|
-
this.logger(
|
|
640
|
-
'debug',
|
|
641
|
-
`Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`,
|
|
642
|
-
optimalLayers,
|
|
643
|
-
);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const layers = optimalLayers.map<VideoLayer>((optimalLayer) => ({
|
|
647
|
-
rid: optimalLayer.rid || '',
|
|
648
|
-
bitrate: optimalLayer.maxBitrate || 0,
|
|
649
|
-
fps: optimalLayer.maxFramerate || 0,
|
|
650
|
-
quality: ridToVideoQuality(optimalLayer.rid || ''),
|
|
651
|
-
videoDimension: {
|
|
652
|
-
width: optimalLayer.width,
|
|
653
|
-
height: optimalLayer.height,
|
|
654
|
-
},
|
|
655
|
-
}));
|
|
656
|
-
|
|
657
|
-
const isAudioTrack = [
|
|
658
|
-
TrackType.AUDIO,
|
|
659
|
-
TrackType.SCREEN_SHARE_AUDIO,
|
|
660
|
-
].includes(trackType);
|
|
661
|
-
|
|
662
|
-
const trackSettings = track.getSettings();
|
|
663
|
-
const isStereo = isAudioTrack && trackSettings.channelCount === 2;
|
|
664
|
-
const transceiverInitIndex =
|
|
665
|
-
this.transceiverInitOrder.indexOf(trackType);
|
|
666
|
-
return {
|
|
667
|
-
trackId: track.id,
|
|
668
|
-
layers: layers,
|
|
669
|
-
trackType,
|
|
670
|
-
mid: extractMid(transceiver, transceiverInitIndex, sdp),
|
|
671
|
-
stereo: isStereo,
|
|
672
|
-
dtx: isAudioTrack && this.isDtxEnabled,
|
|
673
|
-
red: isAudioTrack && this.isRedEnabled,
|
|
674
|
-
muted: !isTrackLive,
|
|
675
|
-
};
|
|
676
|
-
});
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
private computeLayers = (
|
|
680
|
-
trackType: TrackType,
|
|
681
|
-
track: MediaStreamTrack,
|
|
682
|
-
opts?: PublishOptions,
|
|
683
|
-
): OptimalVideoLayer[] | undefined => {
|
|
684
|
-
const { settings } = this.state;
|
|
685
|
-
const targetResolution = settings?.video
|
|
686
|
-
.target_resolution as TargetResolutionResponse;
|
|
687
|
-
const screenShareBitrate =
|
|
688
|
-
settings?.screensharing.target_resolution?.bitrate;
|
|
689
|
-
|
|
690
|
-
const publishOpts = opts || this.publishOptsForTrack.get(trackType);
|
|
691
|
-
const codecInUse =
|
|
692
|
-
opts?.forceCodec || getOptimalVideoCodec(opts?.preferredCodec);
|
|
693
|
-
return trackType === TrackType.VIDEO
|
|
694
|
-
? findOptimalVideoLayers(track, targetResolution, codecInUse, publishOpts)
|
|
695
|
-
: trackType === TrackType.SCREEN_SHARE
|
|
696
|
-
? findOptimalScreenSharingLayers(track, publishOpts, screenShareBitrate)
|
|
697
|
-
: undefined;
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
private onIceCandidateError = (e: Event) => {
|
|
701
|
-
const errorMessage =
|
|
702
|
-
e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
703
|
-
`${e.errorCode}: ${e.errorText}`;
|
|
704
|
-
const iceState = this.pc.iceConnectionState;
|
|
705
|
-
const logLevel =
|
|
706
|
-
iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
707
|
-
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
private onIceConnectionStateChange = () => {
|
|
711
|
-
const state = this.pc.iceConnectionState;
|
|
712
|
-
this.logger('debug', `ICE Connection state changed to`, state);
|
|
713
|
-
|
|
714
|
-
if (this.state.callingState === CallingState.RECONNECTING) return;
|
|
715
|
-
|
|
716
|
-
if (state === 'failed' || state === 'disconnected') {
|
|
717
|
-
this.logger('debug', `Attempting to restart ICE`);
|
|
718
|
-
this.restartIce().catch((e) => {
|
|
719
|
-
this.logger('error', `ICE restart error`, e);
|
|
720
|
-
this.onUnrecoverableError?.();
|
|
721
|
-
});
|
|
352
|
+
getAnnouncedTracks = (sdp: string | undefined): TrackInfo[] => {
|
|
353
|
+
const trackInfos: TrackInfo[] = [];
|
|
354
|
+
for (const bundle of this.transceiverCache.items()) {
|
|
355
|
+
const { transceiver, publishOption } = bundle;
|
|
356
|
+
const track = transceiver.sender.track;
|
|
357
|
+
if (!track) continue;
|
|
358
|
+
|
|
359
|
+
trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
|
|
722
360
|
}
|
|
361
|
+
return trackInfos;
|
|
723
362
|
};
|
|
724
363
|
|
|
725
|
-
|
|
726
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Returns a list of tracks that are currently being published.
|
|
366
|
+
* This method shall be used for the reconnection flow.
|
|
367
|
+
* There we shouldn't announce the tracks that have been stopped due to a codec switch.
|
|
368
|
+
*/
|
|
369
|
+
getAnnouncedTracksForReconnect = (): TrackInfo[] => {
|
|
370
|
+
const sdp = this.pc.localDescription?.sdp;
|
|
371
|
+
const trackInfos: TrackInfo[] = [];
|
|
372
|
+
for (const publishOption of this.publishOptions) {
|
|
373
|
+
const transceiver = this.transceiverCache.get(publishOption);
|
|
374
|
+
if (!transceiver || !transceiver.sender.track) continue;
|
|
375
|
+
|
|
376
|
+
trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
|
|
377
|
+
}
|
|
378
|
+
return trackInfos;
|
|
727
379
|
};
|
|
728
380
|
|
|
729
|
-
|
|
730
|
-
|
|
381
|
+
/**
|
|
382
|
+
* Converts the given transceiver to a `TrackInfo` object.
|
|
383
|
+
*/
|
|
384
|
+
private toTrackInfo = (
|
|
385
|
+
transceiver: RTCRtpTransceiver,
|
|
386
|
+
publishOption: PublishOption,
|
|
387
|
+
sdp: string | undefined,
|
|
388
|
+
): TrackInfo => {
|
|
389
|
+
const track = transceiver.sender.track!;
|
|
390
|
+
const isTrackLive = track.readyState === 'live';
|
|
391
|
+
const layers = isTrackLive
|
|
392
|
+
? computeVideoLayers(track, publishOption)
|
|
393
|
+
: this.transceiverCache.getLayers(publishOption);
|
|
394
|
+
this.transceiverCache.setLayers(publishOption, layers);
|
|
395
|
+
|
|
396
|
+
const isAudioTrack = isAudioTrackType(publishOption.trackType);
|
|
397
|
+
const isStereo = isAudioTrack && track.getSettings().channelCount === 2;
|
|
398
|
+
const transceiverIndex = this.transceiverCache.indexOf(transceiver);
|
|
399
|
+
const audioSettings = this.state.settings?.audio;
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
trackId: track.id,
|
|
403
|
+
layers: toVideoLayers(layers),
|
|
404
|
+
trackType: publishOption.trackType,
|
|
405
|
+
mid: extractMid(transceiver, transceiverIndex, sdp),
|
|
406
|
+
stereo: isStereo,
|
|
407
|
+
dtx: isAudioTrack && !!audioSettings?.opus_dtx_enabled,
|
|
408
|
+
red: isAudioTrack && !!audioSettings?.redundant_coding_enabled,
|
|
409
|
+
muted: !isTrackLive,
|
|
410
|
+
codec: publishOption.codec,
|
|
411
|
+
publishOptionId: publishOption.id,
|
|
412
|
+
};
|
|
731
413
|
};
|
|
732
414
|
}
|