@stream-io/video-client 0.0.28 → 0.0.30
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 +2512 -1754
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +2532 -1752
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +2512 -1754
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +2 -3
- package/dist/src/StreamSfuClient.d.ts +23 -10
- package/dist/src/StreamVideoClient.d.ts +1 -4
- package/dist/src/client-details.d.ts +2 -1
- package/dist/src/coordinator/connection/types.d.ts +2 -2
- package/dist/src/coordinator/connection/utils.d.ts +1 -0
- package/dist/src/events/internal.d.ts +4 -0
- package/dist/src/gen/coordinator/index.d.ts +6 -0
- package/dist/src/gen/google/protobuf/struct.d.ts +8 -15
- package/dist/src/gen/google/protobuf/timestamp.d.ts +2 -9
- package/dist/src/gen/video/sfu/event/events.d.ts +121 -1
- package/dist/src/gen/video/sfu/models/models.d.ts +38 -1
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +3 -14
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +4 -12
- package/dist/src/logger.d.ts +4 -2
- package/dist/src/rtc/Dispatcher.d.ts +1 -2
- package/dist/src/rtc/{publisher.d.ts → Publisher.d.ts} +49 -15
- package/dist/src/rtc/Subscriber.d.ts +58 -0
- package/dist/src/rtc/__tests__/Subscriber.test.d.ts +1 -0
- package/dist/src/rtc/flows/join.d.ts +8 -1
- package/dist/src/rtc/index.d.ts +2 -2
- package/dist/src/rtc/signal.d.ts +1 -0
- package/dist/src/stats/state-store-stats-reporter.d.ts +3 -4
- package/dist/src/store/CallState.d.ts +10 -0
- package/package.json +3 -1
- package/src/Call.ts +215 -209
- package/src/StreamSfuClient.ts +48 -21
- package/src/StreamVideoClient.ts +7 -24
- package/src/client-details.ts +33 -1
- package/src/coordinator/connection/client.ts +6 -8
- package/src/coordinator/connection/types.ts +2 -3
- package/src/coordinator/connection/utils.ts +1 -0
- package/src/events/call.ts +0 -1
- package/src/events/callEventHandlers.ts +2 -0
- package/src/events/internal.ts +20 -0
- package/src/events/sessions.ts +0 -1
- package/src/gen/coordinator/index.ts +6 -0
- package/src/gen/google/protobuf/struct.ts +541 -333
- package/src/gen/google/protobuf/timestamp.ts +214 -148
- package/src/gen/video/sfu/event/events.ts +353 -3
- package/src/gen/video/sfu/models/models.ts +37 -0
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +160 -94
- package/src/gen/video/sfu/signal_rpc/signal.ts +1214 -731
- package/src/logger.ts +43 -30
- package/src/rtc/Dispatcher.ts +5 -9
- package/src/rtc/{publisher.ts → Publisher.ts} +245 -111
- package/src/rtc/Subscriber.ts +304 -0
- package/src/rtc/__tests__/{publisher.test.ts → Publisher.test.ts} +77 -9
- package/src/rtc/__tests__/Subscriber.test.ts +121 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +20 -0
- package/src/rtc/flows/join.ts +42 -1
- package/src/rtc/index.ts +2 -2
- package/src/rtc/signal.ts +6 -5
- package/src/rtc/videoLayers.ts +1 -4
- package/src/stats/state-store-stats-reporter.ts +3 -5
- package/src/store/CallState.ts +20 -0
- package/src/types.ts +0 -1
- package/dist/src/rtc/subscriber.d.ts +0 -9
- package/src/rtc/subscriber.ts +0 -107
- /package/dist/src/rtc/__tests__/{publisher.test.d.ts → Publisher.test.d.ts} +0 -0
package/src/logger.ts
CHANGED
|
@@ -1,45 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Logger, LogLevel } from './coordinator/connection/types';
|
|
2
|
+
|
|
3
|
+
// log levels, sorted by verbosity
|
|
4
|
+
export const logLevels: Record<LogLevel, number> = Object.freeze({
|
|
5
|
+
trace: 0,
|
|
6
|
+
debug: 1,
|
|
7
|
+
info: 2,
|
|
8
|
+
warn: 3,
|
|
9
|
+
error: 4,
|
|
10
|
+
});
|
|
2
11
|
|
|
3
12
|
let logger: Logger | undefined;
|
|
13
|
+
let level: LogLevel = 'info';
|
|
4
14
|
|
|
5
|
-
export const logToConsole: Logger = (
|
|
6
|
-
logLevel: LogLevel,
|
|
7
|
-
message: string,
|
|
8
|
-
extraData?: Record<string, unknown>,
|
|
9
|
-
tags?: string[],
|
|
10
|
-
) => {
|
|
15
|
+
export const logToConsole: Logger = (logLevel, message, ...args) => {
|
|
11
16
|
let logMethod;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
switch (logLevel) {
|
|
18
|
+
case 'error':
|
|
19
|
+
logMethod = console.error;
|
|
20
|
+
break;
|
|
21
|
+
case 'warn':
|
|
22
|
+
logMethod = console.warn;
|
|
23
|
+
break;
|
|
24
|
+
case 'info':
|
|
25
|
+
logMethod = console.info;
|
|
26
|
+
break;
|
|
27
|
+
case 'trace':
|
|
28
|
+
logMethod = console.trace;
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
logMethod = console.log;
|
|
32
|
+
break;
|
|
18
33
|
}
|
|
19
34
|
|
|
20
|
-
logMethod(
|
|
21
|
-
logLevel,
|
|
22
|
-
`${tags?.join(':')} - ${message}`,
|
|
23
|
-
extraData ? extraData : '',
|
|
24
|
-
);
|
|
35
|
+
logMethod(message, ...args);
|
|
25
36
|
};
|
|
26
37
|
|
|
27
|
-
export const setLogger = (l: Logger) => {
|
|
38
|
+
export const setLogger = (l: Logger, lvl?: LogLevel) => {
|
|
28
39
|
logger = l;
|
|
40
|
+
if (lvl) {
|
|
41
|
+
setLogLevel(lvl);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const setLogLevel = (l: LogLevel) => {
|
|
46
|
+
level = l;
|
|
29
47
|
};
|
|
30
48
|
|
|
31
49
|
export const getLogger = (withTags?: string[]) => {
|
|
32
|
-
const loggerMethod = logger ||
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
) => {
|
|
39
|
-
loggerMethod(logLevel, messeage, extraData, [
|
|
40
|
-
...(tags || []),
|
|
41
|
-
...(withTags || []),
|
|
42
|
-
]);
|
|
50
|
+
const loggerMethod = logger || logToConsole;
|
|
51
|
+
const tags = (withTags || []).join(':');
|
|
52
|
+
const result: Logger = (logLevel, message, ...args) => {
|
|
53
|
+
if (logLevels[logLevel] >= logLevels[level]) {
|
|
54
|
+
loggerMethod(logLevel, `[${tags}]: ${message}`, ...args);
|
|
55
|
+
}
|
|
43
56
|
};
|
|
44
57
|
return result;
|
|
45
58
|
};
|
package/src/rtc/Dispatcher.ts
CHANGED
|
@@ -20,6 +20,7 @@ const sfuEventKinds: { [key in SfuEventKinds]: undefined } = {
|
|
|
20
20
|
trackUnpublished: undefined,
|
|
21
21
|
error: undefined,
|
|
22
22
|
callGrantsUpdated: undefined,
|
|
23
|
+
goAway: undefined,
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export const isSfuEvent = (
|
|
@@ -34,19 +35,14 @@ export class Dispatcher {
|
|
|
34
35
|
private subscribers: {
|
|
35
36
|
[eventName: string]: SfuEventListener[] | undefined;
|
|
36
37
|
} = {};
|
|
37
|
-
private logger
|
|
38
|
-
|
|
39
|
-
constructor() {
|
|
40
|
-
this.logger = getLogger(['sfu-client']);
|
|
41
|
-
}
|
|
38
|
+
private readonly logger: Logger = getLogger(['sfu-client']);
|
|
42
39
|
|
|
43
40
|
dispatch = (message: SfuEvent) => {
|
|
44
41
|
const eventKind = message.eventPayload.oneofKind;
|
|
45
42
|
if (eventKind) {
|
|
46
|
-
this.logger
|
|
47
|
-
this.logger?.(
|
|
43
|
+
this.logger(
|
|
48
44
|
'debug',
|
|
49
|
-
`
|
|
45
|
+
`Dispatching ${eventKind}`,
|
|
50
46
|
(message.eventPayload as any)[eventKind],
|
|
51
47
|
);
|
|
52
48
|
const listeners = this.subscribers[eventKind];
|
|
@@ -54,7 +50,7 @@ export class Dispatcher {
|
|
|
54
50
|
try {
|
|
55
51
|
fn(message);
|
|
56
52
|
} catch (e) {
|
|
57
|
-
this.logger
|
|
53
|
+
this.logger('warn', 'Listener failed with error', e);
|
|
58
54
|
}
|
|
59
55
|
});
|
|
60
56
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as SDP from 'sdp-transform';
|
|
1
2
|
import { StreamSfuClient } from '../StreamSfuClient';
|
|
2
3
|
import {
|
|
3
4
|
PeerType,
|
|
@@ -10,6 +11,7 @@ import { getIceCandidate } from './helpers/iceCandidate';
|
|
|
10
11
|
import {
|
|
11
12
|
findOptimalScreenSharingLayers,
|
|
12
13
|
findOptimalVideoLayers,
|
|
14
|
+
OptimalVideoLayer,
|
|
13
15
|
} from './videoLayers';
|
|
14
16
|
import { getPreferredCodecs } from './codecs';
|
|
15
17
|
import {
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
toggleDtx,
|
|
26
28
|
} from '../helpers/sdp-munging';
|
|
27
29
|
import { Logger } from '../coordinator/connection/types';
|
|
30
|
+
import { getLogger } from '../logger';
|
|
28
31
|
|
|
29
32
|
export type PublisherOpts = {
|
|
30
33
|
sfuClient: StreamSfuClient;
|
|
@@ -40,9 +43,9 @@ export type PublisherOpts = {
|
|
|
40
43
|
* @internal
|
|
41
44
|
*/
|
|
42
45
|
export class Publisher {
|
|
43
|
-
private
|
|
44
|
-
private readonly sfuClient: StreamSfuClient;
|
|
46
|
+
private pc: RTCPeerConnection;
|
|
45
47
|
private readonly state: CallState;
|
|
48
|
+
|
|
46
49
|
private readonly transceiverRegistry: {
|
|
47
50
|
[key in TrackType]: RTCRtpTransceiver | undefined;
|
|
48
51
|
} = {
|
|
@@ -52,7 +55,8 @@ export class Publisher {
|
|
|
52
55
|
[TrackType.SCREEN_SHARE_AUDIO]: undefined,
|
|
53
56
|
[TrackType.UNSPECIFIED]: undefined,
|
|
54
57
|
};
|
|
55
|
-
|
|
58
|
+
|
|
59
|
+
private readonly trackKindMapping: {
|
|
56
60
|
[key in TrackType]: 'video' | 'audio' | undefined;
|
|
57
61
|
} = {
|
|
58
62
|
[TrackType.AUDIO]: 'audio',
|
|
@@ -61,11 +65,37 @@ export class Publisher {
|
|
|
61
65
|
[TrackType.SCREEN_SHARE_AUDIO]: undefined,
|
|
62
66
|
[TrackType.UNSPECIFIED]: undefined,
|
|
63
67
|
};
|
|
64
|
-
private isDtxEnabled: boolean;
|
|
65
|
-
private isRedEnabled: boolean;
|
|
66
|
-
private preferredVideoCodec?: string;
|
|
67
|
-
private logger?: Logger;
|
|
68
68
|
|
|
69
|
+
private readonly trackLayersCache: {
|
|
70
|
+
[key in TrackType]: OptimalVideoLayer[] | undefined;
|
|
71
|
+
} = {
|
|
72
|
+
[TrackType.AUDIO]: undefined,
|
|
73
|
+
[TrackType.VIDEO]: undefined,
|
|
74
|
+
[TrackType.SCREEN_SHARE]: undefined,
|
|
75
|
+
[TrackType.SCREEN_SHARE_AUDIO]: undefined,
|
|
76
|
+
[TrackType.UNSPECIFIED]: undefined,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
private readonly isDtxEnabled: boolean;
|
|
80
|
+
private readonly isRedEnabled: boolean;
|
|
81
|
+
private readonly preferredVideoCodec?: string;
|
|
82
|
+
private logger: Logger = getLogger(['Publisher']);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The SFU client instance to use for publishing and signaling.
|
|
86
|
+
*/
|
|
87
|
+
sfuClient: StreamSfuClient;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Constructs a new `Publisher` instance.
|
|
91
|
+
*
|
|
92
|
+
* @param connectionConfig the connection configuration to use.
|
|
93
|
+
* @param sfuClient the SFU client to use.
|
|
94
|
+
* @param state the call state to use.
|
|
95
|
+
* @param isDtxEnabled whether DTX is enabled.
|
|
96
|
+
* @param isRedEnabled whether RED is enabled.
|
|
97
|
+
* @param preferredVideoCodec the preferred video codec.
|
|
98
|
+
*/
|
|
69
99
|
constructor({
|
|
70
100
|
connectionConfig,
|
|
71
101
|
sfuClient,
|
|
@@ -74,6 +104,15 @@ export class Publisher {
|
|
|
74
104
|
isRedEnabled,
|
|
75
105
|
preferredVideoCodec,
|
|
76
106
|
}: PublisherOpts) {
|
|
107
|
+
this.pc = this.createPeerConnection(connectionConfig);
|
|
108
|
+
this.sfuClient = sfuClient;
|
|
109
|
+
this.state = state;
|
|
110
|
+
this.isDtxEnabled = isDtxEnabled;
|
|
111
|
+
this.isRedEnabled = isRedEnabled;
|
|
112
|
+
this.preferredVideoCodec = preferredVideoCodec;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private createPeerConnection = (connectionConfig?: RTCConfiguration) => {
|
|
77
116
|
const pc = new RTCPeerConnection(connectionConfig);
|
|
78
117
|
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
79
118
|
pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
@@ -87,14 +126,28 @@ export class Publisher {
|
|
|
87
126
|
'icegatheringstatechange',
|
|
88
127
|
this.onIceGatheringStateChange,
|
|
89
128
|
);
|
|
129
|
+
pc.addEventListener('signalingstatechange', this.onSignalingStateChange);
|
|
130
|
+
return pc;
|
|
131
|
+
};
|
|
90
132
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Closes the publisher PeerConnection and cleans up the resources.
|
|
135
|
+
*/
|
|
136
|
+
close = ({ stopTracks = true } = {}) => {
|
|
137
|
+
if (stopTracks) {
|
|
138
|
+
this.stopPublishing();
|
|
139
|
+
Object.keys(this.transceiverRegistry).forEach((trackType) => {
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
this.transceiverRegistry[trackType] = undefined;
|
|
142
|
+
});
|
|
143
|
+
Object.keys(this.trackLayersCache).forEach((trackType) => {
|
|
144
|
+
// @ts-ignore
|
|
145
|
+
this.trackLayersCache[trackType] = undefined;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.pc.close();
|
|
150
|
+
};
|
|
98
151
|
|
|
99
152
|
/**
|
|
100
153
|
* Starts publishing the given track of the given media stream.
|
|
@@ -112,13 +165,17 @@ export class Publisher {
|
|
|
112
165
|
trackType: TrackType,
|
|
113
166
|
opts: PublishOptions = {},
|
|
114
167
|
) => {
|
|
115
|
-
|
|
168
|
+
if (track.readyState === 'ended') {
|
|
169
|
+
throw new Error(`Can't publish a track that has ended already.`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let transceiver = this.pc
|
|
116
173
|
.getTransceivers()
|
|
117
174
|
.find(
|
|
118
175
|
(t) =>
|
|
119
176
|
t === this.transceiverRegistry[trackType] &&
|
|
120
177
|
t.sender.track &&
|
|
121
|
-
t.sender.track?.kind === this.
|
|
178
|
+
t.sender.track?.kind === this.trackKindMapping[trackType],
|
|
122
179
|
);
|
|
123
180
|
|
|
124
181
|
/**
|
|
@@ -126,7 +183,7 @@ export class Publisher {
|
|
|
126
183
|
* Once the track has ended, it will notify the SFU and update the state.
|
|
127
184
|
*/
|
|
128
185
|
const handleTrackEnded = async () => {
|
|
129
|
-
this.logger
|
|
186
|
+
this.logger(
|
|
130
187
|
'info',
|
|
131
188
|
`Track ${TrackType[trackType]} has ended, notifying the SFU`,
|
|
132
189
|
);
|
|
@@ -158,7 +215,7 @@ export class Publisher {
|
|
|
158
215
|
// keep in mind that `track.stop()` doesn't trigger this event.
|
|
159
216
|
track.addEventListener('ended', handleTrackEnded);
|
|
160
217
|
|
|
161
|
-
transceiver = this.
|
|
218
|
+
transceiver = this.pc.addTransceiver(track, {
|
|
162
219
|
direction: 'sendonly',
|
|
163
220
|
streams:
|
|
164
221
|
trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
|
|
@@ -170,7 +227,7 @@ export class Publisher {
|
|
|
170
227
|
this.transceiverRegistry[trackType] = transceiver;
|
|
171
228
|
|
|
172
229
|
if ('setCodecPreferences' in transceiver && codecPreferences) {
|
|
173
|
-
this.logger
|
|
230
|
+
this.logger(
|
|
174
231
|
'info',
|
|
175
232
|
`Setting ${TrackType[trackType]} codec preferences`,
|
|
176
233
|
codecPreferences,
|
|
@@ -202,7 +259,7 @@ export class Publisher {
|
|
|
202
259
|
* @param trackType the track type to unpublish.
|
|
203
260
|
*/
|
|
204
261
|
unpublishStream = async (trackType: TrackType) => {
|
|
205
|
-
const transceiver = this.
|
|
262
|
+
const transceiver = this.pc
|
|
206
263
|
.getTransceivers()
|
|
207
264
|
.find((t) => t === this.transceiverRegistry[trackType] && t.sender.track);
|
|
208
265
|
if (
|
|
@@ -266,39 +323,39 @@ export class Publisher {
|
|
|
266
323
|
|
|
267
324
|
/**
|
|
268
325
|
* Stops publishing all tracks and stop all tracks.
|
|
269
|
-
*
|
|
270
|
-
* @param options - Options
|
|
271
|
-
* @param options.stopTracks - If `true` (default), all tracks will be stopped.
|
|
272
326
|
*/
|
|
273
|
-
stopPublishing = (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
s.track?.stop();
|
|
282
|
-
|
|
283
|
-
if (this.publisher.signalingState !== 'closed') {
|
|
284
|
-
this.publisher.removeTrack(s);
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
this.publisher.close();
|
|
327
|
+
stopPublishing = () => {
|
|
328
|
+
this.logger('debug', 'Stopping publishing all tracks');
|
|
329
|
+
this.pc.getSenders().forEach((s) => {
|
|
330
|
+
s.track?.stop();
|
|
331
|
+
if (this.pc.signalingState !== 'closed') {
|
|
332
|
+
this.pc.removeTrack(s);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
289
335
|
};
|
|
290
336
|
|
|
291
337
|
updateVideoPublishQuality = async (enabledRids: string[]) => {
|
|
292
|
-
this.logger
|
|
338
|
+
this.logger(
|
|
293
339
|
'info',
|
|
294
340
|
'Update publish quality, requested rids by SFU:',
|
|
295
341
|
enabledRids,
|
|
296
342
|
);
|
|
297
343
|
|
|
298
344
|
const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
|
|
299
|
-
if (!videoSender)
|
|
345
|
+
if (!videoSender) {
|
|
346
|
+
this.logger('warn', 'Update publish quality, no video sender found.');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
300
349
|
|
|
301
350
|
const params = videoSender.getParameters();
|
|
351
|
+
if (params.encodings.length === 0) {
|
|
352
|
+
this.logger(
|
|
353
|
+
'warn',
|
|
354
|
+
'Update publish quality, No suitable video encoding quality found',
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
302
359
|
let changed = false;
|
|
303
360
|
params.encodings.forEach((enc) => {
|
|
304
361
|
// flip 'active' flag only when necessary
|
|
@@ -308,18 +365,19 @@ export class Publisher {
|
|
|
308
365
|
changed = true;
|
|
309
366
|
}
|
|
310
367
|
});
|
|
368
|
+
|
|
369
|
+
const activeRids = params.encodings
|
|
370
|
+
.filter((e) => e.active)
|
|
371
|
+
.map((e) => e.rid)
|
|
372
|
+
.join(', ');
|
|
311
373
|
if (changed) {
|
|
312
|
-
if (params.encodings.length === 0) {
|
|
313
|
-
this.logger?.('warn', 'No suitable video encoding quality found');
|
|
314
|
-
}
|
|
315
374
|
await videoSender.setParameters(params);
|
|
316
|
-
this.logger
|
|
375
|
+
this.logger(
|
|
317
376
|
'info',
|
|
318
|
-
`Update publish quality, enabled rids: ${
|
|
319
|
-
.filter((e) => e.active)
|
|
320
|
-
.map((e) => e.rid)
|
|
321
|
-
.join(', ')}`,
|
|
377
|
+
`Update publish quality, enabled rids: ${activeRids}`,
|
|
322
378
|
);
|
|
379
|
+
} else {
|
|
380
|
+
this.logger('info', `Update publish quality, no change: ${activeRids}`);
|
|
323
381
|
}
|
|
324
382
|
};
|
|
325
383
|
|
|
@@ -328,9 +386,9 @@ export class Publisher {
|
|
|
328
386
|
* @param selector
|
|
329
387
|
* @returns
|
|
330
388
|
*/
|
|
331
|
-
getStats(selector?: MediaStreamTrack | null | undefined) {
|
|
332
|
-
return this.
|
|
333
|
-
}
|
|
389
|
+
getStats = (selector?: MediaStreamTrack | null | undefined) => {
|
|
390
|
+
return this.pc.getStats(selector);
|
|
391
|
+
};
|
|
334
392
|
|
|
335
393
|
private getCodecPreferences = (
|
|
336
394
|
trackType: TrackType,
|
|
@@ -353,7 +411,7 @@ export class Publisher {
|
|
|
353
411
|
private onIceCandidate = async (e: RTCPeerConnectionIceEvent) => {
|
|
354
412
|
const { candidate } = e;
|
|
355
413
|
if (!candidate) {
|
|
356
|
-
this.logger
|
|
414
|
+
this.logger('warn', 'null ice candidate');
|
|
357
415
|
return;
|
|
358
416
|
}
|
|
359
417
|
await this.sfuClient.iceTrickle({
|
|
@@ -362,10 +420,83 @@ export class Publisher {
|
|
|
362
420
|
});
|
|
363
421
|
};
|
|
364
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Performs a migration of this publisher instance to a new SFU.
|
|
425
|
+
*
|
|
426
|
+
* Initiates a new `iceRestart` offer/answer exchange with the new SFU.
|
|
427
|
+
*
|
|
428
|
+
* @param sfuClient the new SFU client to migrate to.
|
|
429
|
+
* @param connectionConfig the new connection configuration to use.
|
|
430
|
+
*/
|
|
431
|
+
migrateTo = async (
|
|
432
|
+
sfuClient: StreamSfuClient,
|
|
433
|
+
connectionConfig?: RTCConfiguration,
|
|
434
|
+
) => {
|
|
435
|
+
this.sfuClient = sfuClient;
|
|
436
|
+
this.pc.setConfiguration(connectionConfig);
|
|
437
|
+
|
|
438
|
+
const shouldRestartIce = this.pc.iceConnectionState === 'connected';
|
|
439
|
+
if (shouldRestartIce) {
|
|
440
|
+
// negotiate only if there are tracks to publish
|
|
441
|
+
await this.negotiate({ iceRestart: true });
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
365
445
|
private onNegotiationNeeded = async () => {
|
|
366
|
-
this.
|
|
367
|
-
|
|
368
|
-
|
|
446
|
+
await this.negotiate();
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Initiates a new offer/answer exchange with the currently connected SFU.
|
|
451
|
+
*
|
|
452
|
+
* @param options the optional offer options to use.
|
|
453
|
+
*/
|
|
454
|
+
private negotiate = async (options?: RTCOfferOptions) => {
|
|
455
|
+
const offer = await this.pc.createOffer(options);
|
|
456
|
+
offer.sdp = this.mungeCodecs(offer.sdp);
|
|
457
|
+
|
|
458
|
+
const trackInfos = this.getCurrentTrackInfos(offer.sdp);
|
|
459
|
+
if (trackInfos.length === 0) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`Can't initiate negotiation without announcing any tracks`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
await this.pc.setLocalDescription(offer);
|
|
466
|
+
|
|
467
|
+
const { response } = await this.sfuClient.setPublisher({
|
|
468
|
+
sdp: offer.sdp || '',
|
|
469
|
+
tracks: trackInfos,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
await this.pc.setRemoteDescription({
|
|
474
|
+
type: 'answer',
|
|
475
|
+
sdp: response.sdp,
|
|
476
|
+
});
|
|
477
|
+
} catch (e) {
|
|
478
|
+
this.logger('error', `setRemoteDescription error`, {
|
|
479
|
+
sdp: response.sdp,
|
|
480
|
+
error: e,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(
|
|
485
|
+
async (candidate) => {
|
|
486
|
+
try {
|
|
487
|
+
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
488
|
+
await this.pc.addIceCandidate(iceCandidate);
|
|
489
|
+
} catch (e) {
|
|
490
|
+
this.logger('error', `ICE candidate error`, {
|
|
491
|
+
error: e,
|
|
492
|
+
candidate,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
private mungeCodecs = (sdp?: string) => {
|
|
369
500
|
if (sdp) {
|
|
370
501
|
sdp = toggleDtx(sdp, this.isDtxEnabled);
|
|
371
502
|
if (isReactNative()) {
|
|
@@ -382,28 +513,63 @@ export class Publisher {
|
|
|
382
513
|
}
|
|
383
514
|
}
|
|
384
515
|
}
|
|
385
|
-
|
|
386
|
-
|
|
516
|
+
return sdp;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
getCurrentTrackInfos = (sdp?: string) => {
|
|
520
|
+
sdp = sdp || this.pc.localDescription?.sdp;
|
|
521
|
+
const extractMid = (
|
|
522
|
+
defaultMid: string | null,
|
|
523
|
+
track: MediaStreamTrack,
|
|
524
|
+
): string => {
|
|
525
|
+
if (defaultMid) return defaultMid;
|
|
526
|
+
if (!sdp) {
|
|
527
|
+
this.logger('warn', 'No SDP found. Returning empty mid');
|
|
528
|
+
return '';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this.logger('warn', 'No mid found for track. Trying to find it from SDP');
|
|
532
|
+
|
|
533
|
+
const parsedSdp = SDP.parse(sdp);
|
|
534
|
+
const media = parsedSdp.media.find((m) => m.type === track.kind);
|
|
535
|
+
if (typeof media?.mid === 'undefined') {
|
|
536
|
+
this.logger('warn', `No mid found in SDP for track type ${track.kind}`);
|
|
537
|
+
return '';
|
|
538
|
+
}
|
|
539
|
+
return String(media.mid);
|
|
540
|
+
};
|
|
387
541
|
|
|
388
542
|
const metadata = this.state.metadata;
|
|
389
543
|
const targetResolution = metadata?.settings.video.target_resolution;
|
|
390
|
-
|
|
544
|
+
return this.pc
|
|
391
545
|
.getTransceivers()
|
|
392
|
-
.filter((t) => t.direction === 'sendonly' &&
|
|
546
|
+
.filter((t) => t.direction === 'sendonly' && t.sender.track)
|
|
393
547
|
.map<TrackInfo>((transceiver) => {
|
|
394
|
-
const trackType = Number(
|
|
548
|
+
const trackType: TrackType = Number(
|
|
395
549
|
Object.keys(this.transceiverRegistry).find(
|
|
396
550
|
(key) =>
|
|
397
551
|
this.transceiverRegistry[key as any as TrackType] === transceiver,
|
|
398
552
|
),
|
|
399
553
|
);
|
|
400
554
|
const track = transceiver.sender.track!;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
555
|
+
let optimalLayers: OptimalVideoLayer[];
|
|
556
|
+
if (track.readyState === 'live') {
|
|
557
|
+
optimalLayers =
|
|
558
|
+
trackType === TrackType.VIDEO
|
|
559
|
+
? findOptimalVideoLayers(track, targetResolution)
|
|
560
|
+
: trackType === TrackType.SCREEN_SHARE
|
|
561
|
+
? findOptimalScreenSharingLayers(track)
|
|
562
|
+
: [];
|
|
563
|
+
this.trackLayersCache[trackType] = optimalLayers;
|
|
564
|
+
} else {
|
|
565
|
+
// we report the last known optimal layers for ended tracks
|
|
566
|
+
optimalLayers = this.trackLayersCache[trackType] || [];
|
|
567
|
+
this.logger(
|
|
568
|
+
'debug',
|
|
569
|
+
`Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`,
|
|
570
|
+
optimalLayers,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
407
573
|
|
|
408
574
|
const layers = optimalLayers.map<VideoLayer>((optimalLayer) => ({
|
|
409
575
|
rid: optimalLayer.rid || '',
|
|
@@ -420,69 +586,37 @@ export class Publisher {
|
|
|
420
586
|
trackId: track.id,
|
|
421
587
|
layers: layers,
|
|
422
588
|
trackType,
|
|
423
|
-
mid: transceiver.mid
|
|
589
|
+
mid: extractMid(transceiver.mid, track),
|
|
424
590
|
|
|
425
591
|
// FIXME OL: adjust these values
|
|
426
592
|
stereo: false,
|
|
427
|
-
dtx: this.isDtxEnabled,
|
|
428
|
-
red: this.isRedEnabled,
|
|
593
|
+
dtx: TrackType.AUDIO === trackType && this.isDtxEnabled,
|
|
594
|
+
red: TrackType.AUDIO === trackType && this.isRedEnabled,
|
|
429
595
|
};
|
|
430
596
|
});
|
|
431
|
-
|
|
432
|
-
// TODO debounce for 250ms
|
|
433
|
-
const { response } = await this.sfuClient.setPublisher({
|
|
434
|
-
sdp: offer.sdp || '',
|
|
435
|
-
tracks: trackInfos,
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
await this.publisher.setRemoteDescription({
|
|
440
|
-
type: 'answer',
|
|
441
|
-
sdp: response.sdp,
|
|
442
|
-
});
|
|
443
|
-
} catch (e) {
|
|
444
|
-
this.logger?.('error', `Publisher: setRemoteDescription error`, {
|
|
445
|
-
sdp: response.sdp,
|
|
446
|
-
error: e,
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(
|
|
451
|
-
async (candidate) => {
|
|
452
|
-
try {
|
|
453
|
-
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
454
|
-
await this.publisher.addIceCandidate(iceCandidate);
|
|
455
|
-
} catch (e) {
|
|
456
|
-
this.logger?.('error', `Publisher: ICE candidate error`, {
|
|
457
|
-
error: e,
|
|
458
|
-
candidate,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
},
|
|
462
|
-
);
|
|
463
597
|
};
|
|
464
598
|
|
|
465
599
|
private onIceCandidateError = (e: Event) => {
|
|
466
600
|
const errorMessage =
|
|
467
601
|
e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
468
602
|
`${e.errorCode}: ${e.errorText}`;
|
|
469
|
-
this.logger
|
|
603
|
+
this.logger('error', `ICE Candidate error`, errorMessage);
|
|
470
604
|
};
|
|
471
605
|
|
|
472
606
|
private onIceConnectionStateChange = () => {
|
|
473
|
-
this.logger
|
|
607
|
+
this.logger(
|
|
474
608
|
'error',
|
|
475
|
-
`
|
|
476
|
-
this.
|
|
609
|
+
`ICE Connection state changed`,
|
|
610
|
+
this.pc.iceConnectionState,
|
|
477
611
|
);
|
|
478
612
|
};
|
|
479
613
|
|
|
480
614
|
private onIceGatheringStateChange = () => {
|
|
481
|
-
this.logger
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
);
|
|
615
|
+
this.logger('error', `ICE Gathering State`, this.pc.iceGatheringState);
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
private onSignalingStateChange = () => {
|
|
619
|
+
this.logger('debug', `Signaling state changed`, this.pc.signalingState);
|
|
486
620
|
};
|
|
487
621
|
|
|
488
622
|
private ridToVideoQuality = (rid: string): VideoQuality => {
|