@stream-io/video-client 1.26.1 → 1.27.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 +18 -0
- package/dist/index.browser.es.js +276 -73
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +275 -71
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +276 -73
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +14 -2
- package/dist/src/StreamSfuClient.d.ts +7 -3
- package/dist/src/devices/devices.d.ts +5 -5
- package/dist/src/events/internal.d.ts +7 -1
- package/dist/src/gen/video/sfu/event/events.d.ts +57 -1
- package/dist/src/gen/video/sfu/models/models.d.ts +21 -0
- package/dist/src/helpers/array.d.ts +7 -0
- package/dist/src/helpers/lazy.d.ts +1 -1
- package/dist/src/helpers/participantUtils.d.ts +8 -1
- package/dist/src/rtc/BasePeerConnection.d.ts +2 -2
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/signal.d.ts +1 -1
- package/dist/src/stats/rtc/Tracer.d.ts +4 -1
- package/dist/src/stats/rtc/types.d.ts +1 -0
- package/dist/src/store/CallState.d.ts +2 -1
- package/dist/src/timers/index.d.ts +1 -1
- package/dist/src/types.d.ts +10 -1
- package/package.json +2 -2
- package/src/Call.ts +55 -9
- package/src/StreamSfuClient.ts +33 -14
- package/src/coordinator/connection/connection.ts +0 -2
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +5 -3
- package/src/devices/MicrophoneManager.ts +2 -1
- package/src/devices/SpeakerManager.ts +1 -1
- package/src/devices/devices.ts +29 -11
- package/src/events/__tests__/internal.test.ts +78 -0
- package/src/events/__tests__/participant.test.ts +66 -0
- package/src/events/callEventHandlers.ts +2 -0
- package/src/events/internal.ts +28 -1
- package/src/events/participant.ts +4 -1
- package/src/gen/video/sfu/event/events.ts +104 -0
- package/src/gen/video/sfu/models/models.ts +21 -0
- package/src/helpers/__tests__/participantUtils.test.ts +167 -0
- package/src/helpers/array.ts +16 -0
- package/src/helpers/lazy.ts +3 -3
- package/src/helpers/participantUtils.ts +23 -1
- package/src/rtc/BasePeerConnection.ts +6 -5
- package/src/rtc/Dispatcher.ts +3 -2
- package/src/rtc/__tests__/Publisher.test.ts +3 -2
- package/src/rtc/__tests__/Subscriber.test.ts +3 -2
- package/src/rtc/__tests__/videoLayers.test.ts +4 -6
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +12 -6
- package/src/stats/rtc/Tracer.ts +19 -1
- package/src/stats/rtc/types.ts +1 -0
- package/src/store/CallState.ts +7 -4
- package/src/types.ts +11 -0
package/src/StreamSfuClient.ts
CHANGED
|
@@ -51,6 +51,11 @@ export type StreamSfuClientConstructor = {
|
|
|
51
51
|
*/
|
|
52
52
|
credentials: Credentials;
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The `cid` (call ID) to use for the connection.
|
|
56
|
+
*/
|
|
57
|
+
cid: string;
|
|
58
|
+
|
|
54
59
|
/**
|
|
55
60
|
* `sessionId` to use for the connection.
|
|
56
61
|
*/
|
|
@@ -59,7 +64,7 @@ export type StreamSfuClientConstructor = {
|
|
|
59
64
|
/**
|
|
60
65
|
* A log tag to use for logging. Useful for debugging multiple instances.
|
|
61
66
|
*/
|
|
62
|
-
|
|
67
|
+
tag: string;
|
|
63
68
|
|
|
64
69
|
/**
|
|
65
70
|
* The timeout in milliseconds for waiting for the `joinResponse`.
|
|
@@ -83,6 +88,14 @@ export type StreamSfuClientConstructor = {
|
|
|
83
88
|
enableTracing: boolean;
|
|
84
89
|
};
|
|
85
90
|
|
|
91
|
+
type SfuWebSocketParams = {
|
|
92
|
+
attempt: string; // the reconnect attempt, start with 0
|
|
93
|
+
user_id: string;
|
|
94
|
+
api_key: string;
|
|
95
|
+
user_session_id: string;
|
|
96
|
+
cid: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
86
99
|
/**
|
|
87
100
|
* The client used for exchanging information with the SFU.
|
|
88
101
|
*/
|
|
@@ -140,7 +153,7 @@ export class StreamSfuClient {
|
|
|
140
153
|
private readonly unsubscribeNetworkChanged: () => void;
|
|
141
154
|
private readonly onSignalClose: ((reason: string) => void) | undefined;
|
|
142
155
|
private readonly logger: Logger;
|
|
143
|
-
|
|
156
|
+
readonly tag: string;
|
|
144
157
|
private readonly credentials: Credentials;
|
|
145
158
|
private readonly dispatcher: Dispatcher;
|
|
146
159
|
private readonly joinResponseTimeout: number;
|
|
@@ -191,7 +204,8 @@ export class StreamSfuClient {
|
|
|
191
204
|
dispatcher,
|
|
192
205
|
credentials,
|
|
193
206
|
sessionId,
|
|
194
|
-
|
|
207
|
+
cid,
|
|
208
|
+
tag,
|
|
195
209
|
joinResponseTimeout = 5000,
|
|
196
210
|
onSignalClose,
|
|
197
211
|
streamClient,
|
|
@@ -204,10 +218,10 @@ export class StreamSfuClient {
|
|
|
204
218
|
const { server, token } = credentials;
|
|
205
219
|
this.edgeName = server.edge_name;
|
|
206
220
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
207
|
-
this.
|
|
208
|
-
this.logger = getLogger(['SfuClient',
|
|
221
|
+
this.tag = tag;
|
|
222
|
+
this.logger = getLogger(['SfuClient', tag]);
|
|
209
223
|
this.tracer = enableTracing
|
|
210
|
-
? new Tracer(`${
|
|
224
|
+
? new Tracer(`${tag}-${this.edgeName}`)
|
|
211
225
|
: undefined;
|
|
212
226
|
this.rpc = createSignalClient({
|
|
213
227
|
baseUrl: server.url,
|
|
@@ -238,10 +252,16 @@ export class StreamSfuClient {
|
|
|
238
252
|
}
|
|
239
253
|
});
|
|
240
254
|
|
|
241
|
-
this.createWebSocket(
|
|
255
|
+
this.createWebSocket({
|
|
256
|
+
attempt: tag,
|
|
257
|
+
user_id: streamClient.user?.id || '',
|
|
258
|
+
api_key: streamClient.key,
|
|
259
|
+
user_session_id: this.sessionId,
|
|
260
|
+
cid,
|
|
261
|
+
});
|
|
242
262
|
}
|
|
243
263
|
|
|
244
|
-
private createWebSocket = () => {
|
|
264
|
+
private createWebSocket = (params: SfuWebSocketParams) => {
|
|
245
265
|
const eventsToTrace: Partial<Record<SfuEventKinds, boolean>> = {
|
|
246
266
|
callEnded: true,
|
|
247
267
|
changePublishQuality: true,
|
|
@@ -249,10 +269,11 @@ export class StreamSfuClient {
|
|
|
249
269
|
connectionQualityChanged: true,
|
|
250
270
|
error: true,
|
|
251
271
|
goAway: true,
|
|
272
|
+
inboundStateNotification: true,
|
|
252
273
|
};
|
|
253
274
|
this.signalWs = createWebSocketSignalChannel({
|
|
254
|
-
|
|
255
|
-
endpoint: `${this.credentials.server.ws_endpoint}
|
|
275
|
+
tag: this.tag,
|
|
276
|
+
endpoint: `${this.credentials.server.ws_endpoint}?${new URLSearchParams(params).toString()}`,
|
|
256
277
|
onMessage: (message) => {
|
|
257
278
|
this.lastMessageTimestamp = new Date();
|
|
258
279
|
this.scheduleConnectionCheck();
|
|
@@ -260,7 +281,7 @@ export class StreamSfuClient {
|
|
|
260
281
|
if (eventsToTrace[eventKind]) {
|
|
261
282
|
this.tracer?.trace(eventKind, message);
|
|
262
283
|
}
|
|
263
|
-
this.dispatcher.dispatch(message, this.
|
|
284
|
+
this.dispatcher.dispatch(message, this.tag);
|
|
264
285
|
},
|
|
265
286
|
});
|
|
266
287
|
|
|
@@ -443,9 +464,7 @@ export class StreamSfuClient {
|
|
|
443
464
|
this.migrateAwayTimeout = setTimeout(() => {
|
|
444
465
|
unsubscribe();
|
|
445
466
|
task.reject(
|
|
446
|
-
new Error(
|
|
447
|
-
`Migration (${this.logTag}) failed to complete in ${timeout}ms`,
|
|
448
|
-
),
|
|
467
|
+
new Error(`Migration (${this.tag}) failed to complete in ${timeout}ms`),
|
|
449
468
|
);
|
|
450
469
|
}, timeout);
|
|
451
470
|
|
|
@@ -471,7 +471,6 @@ export class StableWSConnection {
|
|
|
471
471
|
onmessage = (wsID: number, event: MessageEvent) => {
|
|
472
472
|
if (this.wsID !== wsID) return;
|
|
473
473
|
|
|
474
|
-
this._log('onmessage() - onmessage callback', { event, wsID });
|
|
475
474
|
const data =
|
|
476
475
|
typeof event.data === 'string'
|
|
477
476
|
? (JSON.parse(event.data) as StreamVideoEvent)
|
|
@@ -581,7 +580,6 @@ export class StableWSConnection {
|
|
|
581
580
|
this.totalFailures += 1;
|
|
582
581
|
this._setHealth(false);
|
|
583
582
|
this.isConnecting = false;
|
|
584
|
-
this.rejectConnectionOpen?.(new Error(`WebSocket error: ${event}`));
|
|
585
583
|
this._log(`onerror() - WS connection resulted into error`, { event });
|
|
586
584
|
|
|
587
585
|
this._reconnect();
|
|
@@ -149,7 +149,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
152
|
-
return getVideoDevices();
|
|
152
|
+
return getVideoDevices(this.call.tracer);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
protected getStream(
|
|
@@ -198,6 +198,10 @@ export abstract class InputMediaDeviceManager<
|
|
|
198
198
|
entry.stop?.();
|
|
199
199
|
this.filters = this.filters.filter((f) => f !== entry);
|
|
200
200
|
await this.applySettingsToStream();
|
|
201
|
+
this.call.tracer.trace(
|
|
202
|
+
`unregisterFilter.${TrackType[this.trackType]}`,
|
|
203
|
+
null,
|
|
204
|
+
);
|
|
201
205
|
}),
|
|
202
206
|
};
|
|
203
207
|
}
|
|
@@ -219,9 +223,7 @@ export abstract class InputMediaDeviceManager<
|
|
|
219
223
|
*/
|
|
220
224
|
async select(deviceId: string | undefined) {
|
|
221
225
|
if (isReactNative()) {
|
|
222
|
-
throw new Error(
|
|
223
|
-
'This method is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for reference.',
|
|
224
|
-
);
|
|
226
|
+
throw new Error('This method is not supported in React Native.');
|
|
225
227
|
}
|
|
226
228
|
const prevDeviceId = this.state.selectedDevice;
|
|
227
229
|
if (deviceId === prevDeviceId) {
|
|
@@ -195,6 +195,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
|
|
|
195
195
|
this.logger('warn', 'Failed to unregister noise cancellation', err);
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
this.call.tracer.trace('noiseCancellation.disabled', true);
|
|
198
199
|
await this.call.notifyNoiseCancellationStopped();
|
|
199
200
|
}
|
|
200
201
|
|
|
@@ -245,7 +246,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
|
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
248
|
-
return getAudioDevices();
|
|
249
|
+
return getAudioDevices(this.call.tracer);
|
|
249
250
|
}
|
|
250
251
|
|
|
251
252
|
protected getStream(
|
|
@@ -56,7 +56,7 @@ export class SpeakerManager {
|
|
|
56
56
|
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
|
-
return getAudioOutputDevices();
|
|
59
|
+
return getAudioOutputDevices(this.call.tracer);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
package/src/devices/devices.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
merge,
|
|
8
8
|
shareReplay,
|
|
9
9
|
startWith,
|
|
10
|
+
tap,
|
|
10
11
|
} from 'rxjs';
|
|
11
12
|
import { getLogger } from '../logger';
|
|
12
13
|
import { BrowserPermission } from './BrowserPermission';
|
|
@@ -21,8 +22,13 @@ import { getCurrentValue } from '../store/rxUtils';
|
|
|
21
22
|
*
|
|
22
23
|
* @param permission a BrowserPermission instance.
|
|
23
24
|
* @param kind the kind of devices to enumerate.
|
|
25
|
+
* @param tracer the tracer to use for tracing the device enumeration.
|
|
24
26
|
*/
|
|
25
|
-
const getDevices = (
|
|
27
|
+
const getDevices = (
|
|
28
|
+
permission: BrowserPermission,
|
|
29
|
+
kind: MediaDeviceKind,
|
|
30
|
+
tracer: Tracer | undefined,
|
|
31
|
+
) => {
|
|
26
32
|
return from(
|
|
27
33
|
(async () => {
|
|
28
34
|
let devices = await navigator.mediaDevices.enumerateDevices();
|
|
@@ -34,6 +40,11 @@ const getDevices = (permission: BrowserPermission, kind: MediaDeviceKind) => {
|
|
|
34
40
|
if (shouldPromptForBrowserPermission && (await permission.prompt())) {
|
|
35
41
|
devices = await navigator.mediaDevices.enumerateDevices();
|
|
36
42
|
}
|
|
43
|
+
tracer?.traceOnce(
|
|
44
|
+
'device-enumeration',
|
|
45
|
+
'navigator.mediaDevices.enumerateDevices',
|
|
46
|
+
devices,
|
|
47
|
+
);
|
|
37
48
|
return devices.filter(
|
|
38
49
|
(device) =>
|
|
39
50
|
device.kind === kind &&
|
|
@@ -99,11 +110,12 @@ export const getVideoBrowserPermission = lazy(
|
|
|
99
110
|
}),
|
|
100
111
|
);
|
|
101
112
|
|
|
102
|
-
const getDeviceChangeObserver = lazy(() => {
|
|
113
|
+
const getDeviceChangeObserver = lazy((tracer: Tracer | undefined) => {
|
|
103
114
|
// 'addEventListener' is not available in React Native, returning
|
|
104
115
|
// an observable that will never fire
|
|
105
116
|
if (!navigator.mediaDevices.addEventListener) return from([]);
|
|
106
117
|
return fromEvent(navigator.mediaDevices, 'devicechange').pipe(
|
|
118
|
+
tap(() => tracer?.resetTrace('device-enumeration')),
|
|
107
119
|
map(() => undefined),
|
|
108
120
|
debounceTime(500),
|
|
109
121
|
);
|
|
@@ -115,13 +127,15 @@ const getDeviceChangeObserver = lazy(() => {
|
|
|
115
127
|
* if devices are added/removed the list is updated, and if the permission is revoked,
|
|
116
128
|
* the observable errors.
|
|
117
129
|
*/
|
|
118
|
-
export const getAudioDevices = lazy(() => {
|
|
130
|
+
export const getAudioDevices = lazy((tracer?: Tracer) => {
|
|
119
131
|
return merge(
|
|
120
|
-
getDeviceChangeObserver(),
|
|
132
|
+
getDeviceChangeObserver(tracer),
|
|
121
133
|
getAudioBrowserPermission().asObservable(),
|
|
122
134
|
).pipe(
|
|
123
135
|
startWith(undefined),
|
|
124
|
-
concatMap(() =>
|
|
136
|
+
concatMap(() =>
|
|
137
|
+
getDevices(getAudioBrowserPermission(), 'audioinput', tracer),
|
|
138
|
+
),
|
|
125
139
|
shareReplay(1),
|
|
126
140
|
);
|
|
127
141
|
});
|
|
@@ -132,13 +146,15 @@ export const getAudioDevices = lazy(() => {
|
|
|
132
146
|
* if devices are added/removed the list is updated, and if the permission is revoked,
|
|
133
147
|
* the observable errors.
|
|
134
148
|
*/
|
|
135
|
-
export const getVideoDevices = lazy(() => {
|
|
149
|
+
export const getVideoDevices = lazy((tracer?: Tracer) => {
|
|
136
150
|
return merge(
|
|
137
|
-
getDeviceChangeObserver(),
|
|
151
|
+
getDeviceChangeObserver(tracer),
|
|
138
152
|
getVideoBrowserPermission().asObservable(),
|
|
139
153
|
).pipe(
|
|
140
154
|
startWith(undefined),
|
|
141
|
-
concatMap(() =>
|
|
155
|
+
concatMap(() =>
|
|
156
|
+
getDevices(getVideoBrowserPermission(), 'videoinput', tracer),
|
|
157
|
+
),
|
|
142
158
|
shareReplay(1),
|
|
143
159
|
);
|
|
144
160
|
});
|
|
@@ -149,13 +165,15 @@ export const getVideoDevices = lazy(() => {
|
|
|
149
165
|
* if devices are added/removed the list is updated, and if the permission is revoked,
|
|
150
166
|
* the observable errors.
|
|
151
167
|
*/
|
|
152
|
-
export const getAudioOutputDevices = lazy(() => {
|
|
168
|
+
export const getAudioOutputDevices = lazy((tracer?: Tracer) => {
|
|
153
169
|
return merge(
|
|
154
|
-
getDeviceChangeObserver(),
|
|
170
|
+
getDeviceChangeObserver(tracer),
|
|
155
171
|
getAudioBrowserPermission().asObservable(),
|
|
156
172
|
).pipe(
|
|
157
173
|
startWith(undefined),
|
|
158
|
-
concatMap(() =>
|
|
174
|
+
concatMap(() =>
|
|
175
|
+
getDevices(getAudioBrowserPermission(), 'audiooutput', tracer),
|
|
176
|
+
),
|
|
159
177
|
shareReplay(1),
|
|
160
178
|
);
|
|
161
179
|
});
|
|
@@ -2,8 +2,10 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import { Call } from '../../Call';
|
|
3
3
|
import { Dispatcher } from '../../rtc';
|
|
4
4
|
import { CallState } from '../../store';
|
|
5
|
+
import { noopComparator } from '../../sorting';
|
|
5
6
|
import {
|
|
6
7
|
watchConnectionQualityChanged,
|
|
8
|
+
watchInboundStateNotification,
|
|
7
9
|
watchLiveEnded,
|
|
8
10
|
watchParticipantCountChanged,
|
|
9
11
|
watchPinsUpdated,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
import {
|
|
12
14
|
ConnectionQuality,
|
|
13
15
|
ErrorCode,
|
|
16
|
+
TrackType,
|
|
14
17
|
} from '../../gen/video/sfu/models/models';
|
|
15
18
|
|
|
16
19
|
describe('internal events', () => {
|
|
@@ -131,4 +134,79 @@ describe('internal events', () => {
|
|
|
131
134
|
{ userId: 'u2', sessionId: 'session-2', pin: undefined },
|
|
132
135
|
]);
|
|
133
136
|
});
|
|
137
|
+
|
|
138
|
+
it('handles InboundStateNotification', () => {
|
|
139
|
+
const state = new CallState();
|
|
140
|
+
state.setSortParticipantsBy(noopComparator());
|
|
141
|
+
state.setParticipants([
|
|
142
|
+
// @ts-expect-error incomplete data
|
|
143
|
+
{ sessionId: 'session-1' },
|
|
144
|
+
// @ts-expect-error incomplete data
|
|
145
|
+
{ sessionId: 'session-2' },
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
const update = watchInboundStateNotification(state);
|
|
149
|
+
update({
|
|
150
|
+
inboundVideoStates: [
|
|
151
|
+
{
|
|
152
|
+
userId: '1',
|
|
153
|
+
sessionId: 'session-1',
|
|
154
|
+
trackType: TrackType.VIDEO,
|
|
155
|
+
paused: true,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
userId: '2',
|
|
159
|
+
sessionId: 'session-2',
|
|
160
|
+
trackType: TrackType.VIDEO,
|
|
161
|
+
paused: false,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
expect(
|
|
166
|
+
state.findParticipantBySessionId('session-1')?.pausedTracks,
|
|
167
|
+
).toContain(TrackType.VIDEO);
|
|
168
|
+
expect(
|
|
169
|
+
state.findParticipantBySessionId('session-2')?.pausedTracks,
|
|
170
|
+
).not.toContain(TrackType.VIDEO);
|
|
171
|
+
|
|
172
|
+
update({
|
|
173
|
+
inboundVideoStates: [
|
|
174
|
+
{
|
|
175
|
+
userId: '2',
|
|
176
|
+
sessionId: 'session-2',
|
|
177
|
+
trackType: TrackType.VIDEO,
|
|
178
|
+
paused: true,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
expect(
|
|
183
|
+
state.findParticipantBySessionId('session-1')?.pausedTracks,
|
|
184
|
+
).toContain(TrackType.VIDEO);
|
|
185
|
+
expect(
|
|
186
|
+
state.findParticipantBySessionId('session-2')?.pausedTracks,
|
|
187
|
+
).toContain(TrackType.VIDEO);
|
|
188
|
+
|
|
189
|
+
update({
|
|
190
|
+
inboundVideoStates: [
|
|
191
|
+
{
|
|
192
|
+
userId: '1',
|
|
193
|
+
sessionId: 'session-1',
|
|
194
|
+
trackType: TrackType.VIDEO,
|
|
195
|
+
paused: false,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
userId: '2',
|
|
199
|
+
sessionId: 'session-2',
|
|
200
|
+
trackType: TrackType.VIDEO,
|
|
201
|
+
paused: false,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
expect(
|
|
206
|
+
state.findParticipantBySessionId('session-1')?.pausedTracks,
|
|
207
|
+
).not.toContain(TrackType.VIDEO);
|
|
208
|
+
expect(
|
|
209
|
+
state.findParticipantBySessionId('session-2')?.pausedTracks,
|
|
210
|
+
).not.toContain(TrackType.VIDEO);
|
|
211
|
+
});
|
|
134
212
|
});
|
|
@@ -247,6 +247,72 @@ describe('Participant events', () => {
|
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
it('resets the paused track list if the track is unpublished', () => {
|
|
251
|
+
const state = new CallState();
|
|
252
|
+
state.setParticipants([
|
|
253
|
+
// @ts-expect-error setup one participant
|
|
254
|
+
{
|
|
255
|
+
sessionId: 'session-id',
|
|
256
|
+
publishedTracks: [TrackType.VIDEO, TrackType.SCREEN_SHARE],
|
|
257
|
+
pausedTracks: [TrackType.VIDEO, TrackType.SCREEN_SHARE],
|
|
258
|
+
},
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
const trackUnpublish = watchTrackUnpublished(state);
|
|
262
|
+
// @ts-expect-error incomplete data
|
|
263
|
+
trackUnpublish({ sessionId: 'session-id', type: TrackType.VIDEO });
|
|
264
|
+
expect(state.findParticipantBySessionId('session-id')).toEqual({
|
|
265
|
+
sessionId: 'session-id',
|
|
266
|
+
publishedTracks: [TrackType.SCREEN_SHARE],
|
|
267
|
+
pausedTracks: [TrackType.SCREEN_SHARE],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// @ts-expect-error incomplete data
|
|
271
|
+
trackUnpublish({ sessionId: 'session-id', type: TrackType.SCREEN_SHARE });
|
|
272
|
+
expect(state.findParticipantBySessionId('session-id')).toEqual({
|
|
273
|
+
sessionId: 'session-id',
|
|
274
|
+
publishedTracks: [],
|
|
275
|
+
pausedTracks: [],
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('resets the paused track list if the track is unpublished on full participant update', () => {
|
|
280
|
+
const state = new CallState();
|
|
281
|
+
state.setParticipants([
|
|
282
|
+
// @ts-expect-error setup one participant
|
|
283
|
+
{
|
|
284
|
+
sessionId: 'session-id',
|
|
285
|
+
publishedTracks: [TrackType.VIDEO, TrackType.SCREEN_SHARE],
|
|
286
|
+
pausedTracks: [TrackType.VIDEO, TrackType.SCREEN_SHARE],
|
|
287
|
+
},
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
const trackUnpublished = watchTrackUnpublished(state);
|
|
291
|
+
trackUnpublished({
|
|
292
|
+
sessionId: 'session-id',
|
|
293
|
+
type: TrackType.VIDEO,
|
|
294
|
+
// @ts-expect-error incomplete data
|
|
295
|
+
participant: { publishedTracks: [TrackType.SCREEN_SHARE] },
|
|
296
|
+
});
|
|
297
|
+
expect(state.findParticipantBySessionId('session-id')).toEqual({
|
|
298
|
+
sessionId: 'session-id',
|
|
299
|
+
publishedTracks: [TrackType.SCREEN_SHARE],
|
|
300
|
+
pausedTracks: [TrackType.SCREEN_SHARE],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
trackUnpublished({
|
|
304
|
+
sessionId: 'session-id',
|
|
305
|
+
type: TrackType.SCREEN_SHARE,
|
|
306
|
+
// @ts-expect-error incomplete data
|
|
307
|
+
participant: { publishedTracks: [] },
|
|
308
|
+
});
|
|
309
|
+
expect(state.findParticipantBySessionId('session-id')).toEqual({
|
|
310
|
+
sessionId: 'session-id',
|
|
311
|
+
publishedTracks: [],
|
|
312
|
+
pausedTracks: [],
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
250
316
|
it('adds the participant to the list of participants if provided', () => {
|
|
251
317
|
const state = new CallState();
|
|
252
318
|
const handler = watchTrackUnpublished(state);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
watchCallRejected,
|
|
10
10
|
watchConnectionQualityChanged,
|
|
11
11
|
watchDominantSpeakerChanged,
|
|
12
|
+
watchInboundStateNotification,
|
|
12
13
|
watchLiveEnded,
|
|
13
14
|
watchParticipantCountChanged,
|
|
14
15
|
watchParticipantJoined,
|
|
@@ -60,6 +61,7 @@ export const registerEventHandlers = (call: Call, dispatcher: Dispatcher) => {
|
|
|
60
61
|
|
|
61
62
|
call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
|
|
62
63
|
call.on('pinsUpdated', watchPinsUpdated(state)),
|
|
64
|
+
call.on('inboundStateNotification', watchInboundStateNotification(state)),
|
|
63
65
|
|
|
64
66
|
handleRemoteSoftMute(call),
|
|
65
67
|
];
|
package/src/events/internal.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { Call } from '../Call';
|
|
|
3
3
|
import { CallState } from '../store';
|
|
4
4
|
import { StreamVideoParticipantPatches } from '../types';
|
|
5
5
|
import { getLogger } from '../logger';
|
|
6
|
-
import
|
|
6
|
+
import { pushToIfMissing, removeFromIfPresent } from '../helpers/array';
|
|
7
|
+
import type {
|
|
8
|
+
InboundStateNotification,
|
|
9
|
+
PinsChanged,
|
|
10
|
+
} from '../gen/video/sfu/event/events';
|
|
7
11
|
import {
|
|
8
12
|
ErrorCode,
|
|
9
13
|
WebsocketReconnectStrategy,
|
|
@@ -89,3 +93,26 @@ export const watchPinsUpdated = (state: CallState) => {
|
|
|
89
93
|
state.setServerSidePins(pins);
|
|
90
94
|
};
|
|
91
95
|
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Watches for inbound state notifications and updates the paused tracks
|
|
99
|
+
*
|
|
100
|
+
* @param state the call state to update.
|
|
101
|
+
*/
|
|
102
|
+
export const watchInboundStateNotification = (state: CallState) => {
|
|
103
|
+
return function onInboundStateNotification(e: InboundStateNotification) {
|
|
104
|
+
const { inboundVideoStates } = e;
|
|
105
|
+
const current = state.getParticipantLookupBySessionId();
|
|
106
|
+
const patches: StreamVideoParticipantPatches = {};
|
|
107
|
+
for (const { sessionId, trackType, paused } of inboundVideoStates) {
|
|
108
|
+
const pausedTracks = [...(current[sessionId]?.pausedTracks ?? [])];
|
|
109
|
+
if (paused) {
|
|
110
|
+
pushToIfMissing(pausedTracks, trackType);
|
|
111
|
+
} else {
|
|
112
|
+
removeFromIfPresent(pausedTracks, trackType);
|
|
113
|
+
}
|
|
114
|
+
patches[sessionId] = { pausedTracks };
|
|
115
|
+
}
|
|
116
|
+
state.updateParticipants(patches);
|
|
117
|
+
};
|
|
118
|
+
};
|
|
@@ -106,10 +106,13 @@ export const watchTrackUnpublished = (state: CallState) => {
|
|
|
106
106
|
if (e.participant) {
|
|
107
107
|
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
108
108
|
const participant = Object.assign(e.participant, orphanedTracks);
|
|
109
|
-
state.updateOrAddParticipant(sessionId, participant)
|
|
109
|
+
state.updateOrAddParticipant(sessionId, participant, (p) => ({
|
|
110
|
+
pausedTracks: p.pausedTracks?.filter((t) => t !== type),
|
|
111
|
+
}));
|
|
110
112
|
} else {
|
|
111
113
|
state.updateParticipant(sessionId, (p) => ({
|
|
112
114
|
publishedTracks: p.publishedTracks.filter((t) => t !== type),
|
|
115
|
+
pausedTracks: p.pausedTracks?.filter((t) => t !== type),
|
|
113
116
|
}));
|
|
114
117
|
}
|
|
115
118
|
};
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CallEndedReason,
|
|
7
7
|
CallGrants,
|
|
8
8
|
CallState,
|
|
9
|
+
ClientCapability,
|
|
9
10
|
ClientDetails,
|
|
10
11
|
Codec,
|
|
11
12
|
ConnectionQuality,
|
|
@@ -255,6 +256,15 @@ export interface SfuEvent {
|
|
|
255
256
|
*/
|
|
256
257
|
changePublishOptions: ChangePublishOptions;
|
|
257
258
|
}
|
|
259
|
+
| {
|
|
260
|
+
oneofKind: 'inboundStateNotification';
|
|
261
|
+
/**
|
|
262
|
+
* InboundStateNotification
|
|
263
|
+
*
|
|
264
|
+
* @generated from protobuf field: stream.video.sfu.event.InboundStateNotification inbound_state_notification = 28;
|
|
265
|
+
*/
|
|
266
|
+
inboundStateNotification: InboundStateNotification;
|
|
267
|
+
}
|
|
258
268
|
| {
|
|
259
269
|
oneofKind: undefined;
|
|
260
270
|
};
|
|
@@ -508,6 +518,10 @@ export interface JoinRequest {
|
|
|
508
518
|
* @generated from protobuf field: repeated stream.video.sfu.models.SubscribeOption preferred_subscribe_options = 10;
|
|
509
519
|
*/
|
|
510
520
|
preferredSubscribeOptions: SubscribeOption[];
|
|
521
|
+
/**
|
|
522
|
+
* @generated from protobuf field: repeated stream.video.sfu.models.ClientCapability capabilities = 11;
|
|
523
|
+
*/
|
|
524
|
+
capabilities: ClientCapability[];
|
|
511
525
|
}
|
|
512
526
|
/**
|
|
513
527
|
* @generated from protobuf message stream.video.sfu.event.ReconnectDetails
|
|
@@ -875,6 +889,36 @@ export interface CallEnded {
|
|
|
875
889
|
*/
|
|
876
890
|
reason: CallEndedReason;
|
|
877
891
|
}
|
|
892
|
+
/**
|
|
893
|
+
* @generated from protobuf message stream.video.sfu.event.InboundStateNotification
|
|
894
|
+
*/
|
|
895
|
+
export interface InboundStateNotification {
|
|
896
|
+
/**
|
|
897
|
+
* @generated from protobuf field: repeated stream.video.sfu.event.InboundVideoState inbound_video_states = 1;
|
|
898
|
+
*/
|
|
899
|
+
inboundVideoStates: InboundVideoState[];
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* @generated from protobuf message stream.video.sfu.event.InboundVideoState
|
|
903
|
+
*/
|
|
904
|
+
export interface InboundVideoState {
|
|
905
|
+
/**
|
|
906
|
+
* @generated from protobuf field: string user_id = 1;
|
|
907
|
+
*/
|
|
908
|
+
userId: string;
|
|
909
|
+
/**
|
|
910
|
+
* @generated from protobuf field: string session_id = 2;
|
|
911
|
+
*/
|
|
912
|
+
sessionId: string;
|
|
913
|
+
/**
|
|
914
|
+
* @generated from protobuf field: stream.video.sfu.models.TrackType track_type = 3;
|
|
915
|
+
*/
|
|
916
|
+
trackType: TrackType;
|
|
917
|
+
/**
|
|
918
|
+
* @generated from protobuf field: bool paused = 4;
|
|
919
|
+
*/
|
|
920
|
+
paused: boolean;
|
|
921
|
+
}
|
|
878
922
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
879
923
|
class SfuEvent$Type extends MessageType<SfuEvent> {
|
|
880
924
|
constructor() {
|
|
@@ -1033,6 +1077,13 @@ class SfuEvent$Type extends MessageType<SfuEvent> {
|
|
|
1033
1077
|
oneof: 'eventPayload',
|
|
1034
1078
|
T: () => ChangePublishOptions,
|
|
1035
1079
|
},
|
|
1080
|
+
{
|
|
1081
|
+
no: 28,
|
|
1082
|
+
name: 'inbound_state_notification',
|
|
1083
|
+
kind: 'message',
|
|
1084
|
+
oneof: 'eventPayload',
|
|
1085
|
+
T: () => InboundStateNotification,
|
|
1086
|
+
},
|
|
1036
1087
|
]);
|
|
1037
1088
|
}
|
|
1038
1089
|
}
|
|
@@ -1342,6 +1393,17 @@ class JoinRequest$Type extends MessageType<JoinRequest> {
|
|
|
1342
1393
|
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1343
1394
|
T: () => SubscribeOption,
|
|
1344
1395
|
},
|
|
1396
|
+
{
|
|
1397
|
+
no: 11,
|
|
1398
|
+
name: 'capabilities',
|
|
1399
|
+
kind: 'enum',
|
|
1400
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
1401
|
+
T: () => [
|
|
1402
|
+
'stream.video.sfu.models.ClientCapability',
|
|
1403
|
+
ClientCapability,
|
|
1404
|
+
'CLIENT_CAPABILITY_',
|
|
1405
|
+
],
|
|
1406
|
+
},
|
|
1345
1407
|
]);
|
|
1346
1408
|
}
|
|
1347
1409
|
}
|
|
@@ -1787,3 +1849,45 @@ class CallEnded$Type extends MessageType<CallEnded> {
|
|
|
1787
1849
|
* @generated MessageType for protobuf message stream.video.sfu.event.CallEnded
|
|
1788
1850
|
*/
|
|
1789
1851
|
export const CallEnded = new CallEnded$Type();
|
|
1852
|
+
// @generated message type with reflection information, may provide speed optimized methods
|
|
1853
|
+
class InboundStateNotification$Type extends MessageType<InboundStateNotification> {
|
|
1854
|
+
constructor() {
|
|
1855
|
+
super('stream.video.sfu.event.InboundStateNotification', [
|
|
1856
|
+
{
|
|
1857
|
+
no: 1,
|
|
1858
|
+
name: 'inbound_video_states',
|
|
1859
|
+
kind: 'message',
|
|
1860
|
+
repeat: 2 /*RepeatType.UNPACKED*/,
|
|
1861
|
+
T: () => InboundVideoState,
|
|
1862
|
+
},
|
|
1863
|
+
]);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* @generated MessageType for protobuf message stream.video.sfu.event.InboundStateNotification
|
|
1868
|
+
*/
|
|
1869
|
+
export const InboundStateNotification = new InboundStateNotification$Type();
|
|
1870
|
+
// @generated message type with reflection information, may provide speed optimized methods
|
|
1871
|
+
class InboundVideoState$Type extends MessageType<InboundVideoState> {
|
|
1872
|
+
constructor() {
|
|
1873
|
+
super('stream.video.sfu.event.InboundVideoState', [
|
|
1874
|
+
{ no: 1, name: 'user_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
1875
|
+
{ no: 2, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
1876
|
+
{
|
|
1877
|
+
no: 3,
|
|
1878
|
+
name: 'track_type',
|
|
1879
|
+
kind: 'enum',
|
|
1880
|
+
T: () => [
|
|
1881
|
+
'stream.video.sfu.models.TrackType',
|
|
1882
|
+
TrackType,
|
|
1883
|
+
'TRACK_TYPE_',
|
|
1884
|
+
],
|
|
1885
|
+
},
|
|
1886
|
+
{ no: 4, name: 'paused', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1887
|
+
]);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* @generated MessageType for protobuf message stream.video.sfu.event.InboundVideoState
|
|
1892
|
+
*/
|
|
1893
|
+
export const InboundVideoState = new InboundVideoState$Type();
|