@stream-io/video-client 1.21.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/index.browser.es.js +142 -133
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +142 -133
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +142 -133
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +2 -0
- package/dist/src/devices/InputMediaDeviceManager.d.ts +3 -3
- package/dist/src/devices/SpeakerState.d.ts +3 -1
- package/dist/src/devices/devices.d.ts +8 -8
- package/dist/src/events/call.d.ts +1 -1
- package/dist/src/rtc/BasePeerConnection.d.ts +1 -0
- package/dist/src/stats/SfuStatsReporter.d.ts +4 -1
- package/dist/src/stats/utils.d.ts +14 -0
- package/index.ts +0 -4
- package/package.json +10 -10
- package/src/Call.ts +5 -2
- package/src/devices/CameraManager.ts +27 -23
- package/src/devices/InputMediaDeviceManager.ts +8 -5
- package/src/devices/MicrophoneManager.ts +1 -1
- package/src/devices/ScreenShareManager.ts +2 -2
- package/src/devices/SpeakerManager.ts +2 -1
- package/src/devices/SpeakerState.ts +6 -3
- package/src/devices/__tests__/CameraManager.test.ts +43 -27
- package/src/devices/__tests__/MicrophoneManager.test.ts +5 -3
- package/src/devices/__tests__/ScreenShareManager.test.ts +5 -1
- package/src/devices/__tests__/mocks.ts +2 -3
- package/src/devices/devices.ts +38 -16
- package/src/events/__tests__/call.test.ts +23 -0
- package/src/events/call.ts +12 -1
- package/src/rtc/BasePeerConnection.ts +8 -3
- package/src/rtc/Publisher.ts +1 -1
- package/src/stats/SfuStatsReporter.ts +9 -5
- package/src/stats/utils.ts +15 -0
- package/dist/src/stats/rtc/mediaDevices.d.ts +0 -2
- package/src/stats/rtc/mediaDevices.ts +0 -43
package/src/devices/devices.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { getLogger } from '../logger';
|
|
|
12
12
|
import { BrowserPermission } from './BrowserPermission';
|
|
13
13
|
import { lazy } from '../helpers/lazy';
|
|
14
14
|
import { isFirefox } from '../helpers/browsers';
|
|
15
|
+
import { dumpStream, Tracer } from '../stats';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Returns an Observable that emits the list of available devices
|
|
@@ -158,15 +159,27 @@ export const getAudioOutputDevices = lazy(() => {
|
|
|
158
159
|
);
|
|
159
160
|
});
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
let getUserMediaExecId = 0;
|
|
163
|
+
const getStream = async (
|
|
164
|
+
constraints: MediaStreamConstraints,
|
|
165
|
+
tracer: Tracer | undefined,
|
|
166
|
+
) => {
|
|
167
|
+
const tag = `navigator.mediaDevices.getUserMedia.${getUserMediaExecId++}.`;
|
|
168
|
+
try {
|
|
169
|
+
tracer?.trace(tag, constraints);
|
|
170
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
171
|
+
tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
|
|
172
|
+
if (isFirefox()) {
|
|
173
|
+
// When enumerating devices, Firefox will hide device labels unless there's been
|
|
174
|
+
// an active user media stream on the page. So we force device list updates after
|
|
175
|
+
// every successful getUserMedia call.
|
|
176
|
+
navigator.mediaDevices.dispatchEvent(new Event('devicechange'));
|
|
177
|
+
}
|
|
178
|
+
return stream;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
tracer?.trace(`${tag}OnFailure`, (error as Error).name);
|
|
181
|
+
throw error;
|
|
168
182
|
}
|
|
169
|
-
return stream;
|
|
170
183
|
};
|
|
171
184
|
|
|
172
185
|
function isNotFoundOrOverconstrainedError(error: unknown) {
|
|
@@ -195,12 +208,13 @@ function isNotFoundOrOverconstrainedError(error: unknown) {
|
|
|
195
208
|
* Returns an audio media stream that fulfills the given constraints.
|
|
196
209
|
* If no constraints are provided, it uses the browser's default ones.
|
|
197
210
|
*
|
|
198
|
-
* @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
|
|
199
211
|
* @param trackConstraints the constraints to use when requesting the stream.
|
|
200
|
-
* @
|
|
212
|
+
* @param tracer the tracer to use for tracing the stream creation.
|
|
213
|
+
* @returns a new `MediaStream` fulfilling the given constraints.
|
|
201
214
|
*/
|
|
202
215
|
export const getAudioStream = async (
|
|
203
216
|
trackConstraints?: MediaTrackConstraints,
|
|
217
|
+
tracer?: Tracer,
|
|
204
218
|
): Promise<MediaStream> => {
|
|
205
219
|
const constraints: MediaStreamConstraints = {
|
|
206
220
|
audio: {
|
|
@@ -214,7 +228,7 @@ export const getAudioStream = async (
|
|
|
214
228
|
throwOnNotAllowed: true,
|
|
215
229
|
forcePrompt: true,
|
|
216
230
|
});
|
|
217
|
-
return await getStream(constraints);
|
|
231
|
+
return await getStream(constraints, tracer);
|
|
218
232
|
} catch (error) {
|
|
219
233
|
if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
|
|
220
234
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -239,12 +253,13 @@ export const getAudioStream = async (
|
|
|
239
253
|
* Returns a video media stream that fulfills the given constraints.
|
|
240
254
|
* If no constraints are provided, it uses the browser's default ones.
|
|
241
255
|
*
|
|
242
|
-
* @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
|
|
243
256
|
* @param trackConstraints the constraints to use when requesting the stream.
|
|
257
|
+
* @param tracer the tracer to use for tracing the stream creation.
|
|
244
258
|
* @returns a new `MediaStream` fulfilling the given constraints.
|
|
245
259
|
*/
|
|
246
260
|
export const getVideoStream = async (
|
|
247
261
|
trackConstraints?: MediaTrackConstraints,
|
|
262
|
+
tracer?: Tracer,
|
|
248
263
|
): Promise<MediaStream> => {
|
|
249
264
|
const constraints: MediaStreamConstraints = {
|
|
250
265
|
video: {
|
|
@@ -257,7 +272,7 @@ export const getVideoStream = async (
|
|
|
257
272
|
throwOnNotAllowed: true,
|
|
258
273
|
forcePrompt: true,
|
|
259
274
|
});
|
|
260
|
-
return await getStream(constraints);
|
|
275
|
+
return await getStream(constraints, tracer);
|
|
261
276
|
} catch (error) {
|
|
262
277
|
if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
|
|
263
278
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -278,21 +293,25 @@ export const getVideoStream = async (
|
|
|
278
293
|
}
|
|
279
294
|
};
|
|
280
295
|
|
|
296
|
+
let getDisplayMediaExecId = 0;
|
|
297
|
+
|
|
281
298
|
/**
|
|
282
299
|
* Prompts the user for a permission to share a screen.
|
|
283
300
|
* If the user grants the permission, a screen sharing stream is returned. Throws otherwise.
|
|
284
301
|
*
|
|
285
302
|
* The callers of this API are responsible to handle the possible errors.
|
|
286
303
|
*
|
|
287
|
-
* @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
|
|
288
|
-
*
|
|
289
304
|
* @param options any additional options to pass to the [`getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) API.
|
|
305
|
+
* @param tracer the tracer to use for tracing the stream creation.
|
|
290
306
|
*/
|
|
291
307
|
export const getScreenShareStream = async (
|
|
292
308
|
options?: DisplayMediaStreamOptions,
|
|
309
|
+
tracer?: Tracer | undefined,
|
|
293
310
|
) => {
|
|
311
|
+
const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
|
|
294
312
|
try {
|
|
295
|
-
|
|
313
|
+
tracer?.trace(tag, options);
|
|
314
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
296
315
|
video: true,
|
|
297
316
|
audio: {
|
|
298
317
|
channelCount: {
|
|
@@ -306,7 +325,10 @@ export const getScreenShareStream = async (
|
|
|
306
325
|
systemAudio: 'include',
|
|
307
326
|
...options,
|
|
308
327
|
});
|
|
328
|
+
tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
|
|
329
|
+
return stream;
|
|
309
330
|
} catch (e) {
|
|
331
|
+
tracer?.trace(`${tag}OnFailure`, (e as Error).name);
|
|
310
332
|
getLogger(['devices'])('error', 'Failed to get screen share stream', e);
|
|
311
333
|
throw e;
|
|
312
334
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
CallAcceptedEvent,
|
|
11
11
|
CallEndedEvent,
|
|
12
12
|
CallResponse,
|
|
13
|
+
OwnCapability,
|
|
13
14
|
RejectCallResponse,
|
|
14
15
|
} from '../../gen/coordinator';
|
|
15
16
|
import { Call } from '../../Call';
|
|
@@ -295,6 +296,28 @@ describe('Call ringing events', () => {
|
|
|
295
296
|
|
|
296
297
|
expect(call.leave).not.toHaveBeenCalled();
|
|
297
298
|
});
|
|
299
|
+
|
|
300
|
+
it('will stay in backstage if live ended and has permission', async () => {
|
|
301
|
+
const call = fakeCall();
|
|
302
|
+
call.state.setBackstage(false);
|
|
303
|
+
call.permissionsContext.setPermissions([OwnCapability.JOIN_BACKSTAGE]);
|
|
304
|
+
vi.spyOn(call, 'leave').mockImplementation(async () => {
|
|
305
|
+
console.log(`TEST: leave() called`);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
watchSfuCallEnded(call);
|
|
309
|
+
const event: SfuEvent = {
|
|
310
|
+
eventPayload: {
|
|
311
|
+
oneofKind: 'callEnded',
|
|
312
|
+
callEnded: { reason: CallEndedReason.LIVE_ENDED },
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
// @ts-expect-error type issue
|
|
316
|
+
call['dispatcher'].dispatch(event);
|
|
317
|
+
|
|
318
|
+
expect(call.leave).not.toHaveBeenCalled();
|
|
319
|
+
expect(call.state.backstage).toBe(true);
|
|
320
|
+
});
|
|
298
321
|
});
|
|
299
322
|
|
|
300
323
|
describe('call.leave', () => {
|
package/src/events/call.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { CallingState } from '../store';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
CallAcceptedEvent,
|
|
5
|
+
CallRejectedEvent,
|
|
6
|
+
OwnCapability,
|
|
7
|
+
} from '../gen/coordinator';
|
|
4
8
|
import { CallEnded } from '../gen/video/sfu/event/events';
|
|
5
9
|
import { CallEndedReason } from '../gen/video/sfu/models/models';
|
|
6
10
|
|
|
@@ -99,6 +103,13 @@ export const watchSfuCallEnded = (call: Call) => {
|
|
|
99
103
|
return call.on('callEnded', async (e: CallEnded) => {
|
|
100
104
|
if (call.state.callingState === CallingState.LEFT) return;
|
|
101
105
|
try {
|
|
106
|
+
if (e.reason === CallEndedReason.LIVE_ENDED) {
|
|
107
|
+
call.state.setBackstage(true);
|
|
108
|
+
|
|
109
|
+
// don't leave the call if the user has permission to join backstage
|
|
110
|
+
const { hasPermission } = call.permissionsContext;
|
|
111
|
+
if (hasPermission(OwnCapability.JOIN_BACKSTAGE)) return;
|
|
112
|
+
}
|
|
102
113
|
// `call.ended` event arrived after the call is already left
|
|
103
114
|
// and all event handlers are detached. We need to manually
|
|
104
115
|
// update the call state to reflect the call has ended.
|
|
@@ -43,6 +43,7 @@ export abstract class BasePeerConnection {
|
|
|
43
43
|
readonly stats: StatsTracer;
|
|
44
44
|
private readonly subscriptions: (() => void)[] = [];
|
|
45
45
|
private unsubscribeIceTrickle?: () => void;
|
|
46
|
+
protected readonly lock = Math.random().toString(36).slice(2);
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Constructs a new `BasePeerConnection` instance.
|
|
@@ -83,9 +84,12 @@ export abstract class BasePeerConnection {
|
|
|
83
84
|
);
|
|
84
85
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
|
|
85
86
|
if (enableTracing) {
|
|
86
|
-
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}
|
|
87
|
+
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
|
|
87
88
|
this.tracer = new Tracer(tag);
|
|
88
|
-
this.tracer.trace('create',
|
|
89
|
+
this.tracer.trace('create', {
|
|
90
|
+
url: sfuClient.edgeName,
|
|
91
|
+
...connectionConfig,
|
|
92
|
+
});
|
|
89
93
|
traceRTCPeerConnection(this.pc, this.tracer.trace);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
@@ -135,7 +139,8 @@ export abstract class BasePeerConnection {
|
|
|
135
139
|
): void => {
|
|
136
140
|
this.subscriptions.push(
|
|
137
141
|
this.dispatcher.on(event, (e) => {
|
|
138
|
-
|
|
142
|
+
const lockKey = `pc.${this.lock}.${event}`;
|
|
143
|
+
withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
|
|
139
144
|
if (this.isDisposed) return;
|
|
140
145
|
this.logger('warn', `Error handling ${event}`, err);
|
|
141
146
|
});
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -323,7 +323,7 @@ export class Publisher extends BasePeerConnection {
|
|
|
323
323
|
* @param options the optional offer options to use.
|
|
324
324
|
*/
|
|
325
325
|
private negotiate = async (options?: RTCOfferOptions): Promise<void> => {
|
|
326
|
-
return withoutConcurrency(
|
|
326
|
+
return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
|
|
327
327
|
const offer = await this.pc.createOffer(options);
|
|
328
328
|
const tracks = this.getAnnouncedTracks(offer.sdp);
|
|
329
329
|
if (!tracks.length) throw new Error(`Can't negotiate without any tracks`);
|
|
@@ -3,7 +3,7 @@ import { StreamSfuClient } from '../StreamSfuClient';
|
|
|
3
3
|
import { OwnCapability, StatsOptions } from '../gen/coordinator';
|
|
4
4
|
import { getLogger } from '../logger';
|
|
5
5
|
import { Publisher, Subscriber } from '../rtc';
|
|
6
|
-
import {
|
|
6
|
+
import { Tracer, TraceRecord } from './rtc';
|
|
7
7
|
import { flatten, getSdkName, getSdkVersion } from './utils';
|
|
8
8
|
import { getDeviceState, getWebRTCInfo } from '../helpers/client-details';
|
|
9
9
|
import {
|
|
@@ -24,6 +24,7 @@ export type SfuStatsReporterOptions = {
|
|
|
24
24
|
microphone: MicrophoneManager;
|
|
25
25
|
camera: CameraManager;
|
|
26
26
|
state: CallState;
|
|
27
|
+
tracer: Tracer;
|
|
27
28
|
unifiedSessionId: string;
|
|
28
29
|
};
|
|
29
30
|
|
|
@@ -38,6 +39,7 @@ export class SfuStatsReporter {
|
|
|
38
39
|
private readonly microphone: MicrophoneManager;
|
|
39
40
|
private readonly camera: CameraManager;
|
|
40
41
|
private readonly state: CallState;
|
|
42
|
+
private readonly tracer: Tracer;
|
|
41
43
|
private readonly unifiedSessionId: string;
|
|
42
44
|
|
|
43
45
|
private intervalId: NodeJS.Timeout | undefined;
|
|
@@ -59,6 +61,7 @@ export class SfuStatsReporter {
|
|
|
59
61
|
microphone,
|
|
60
62
|
camera,
|
|
61
63
|
state,
|
|
64
|
+
tracer,
|
|
62
65
|
unifiedSessionId,
|
|
63
66
|
}: SfuStatsReporterOptions,
|
|
64
67
|
) {
|
|
@@ -69,6 +72,7 @@ export class SfuStatsReporter {
|
|
|
69
72
|
this.microphone = microphone;
|
|
70
73
|
this.camera = camera;
|
|
71
74
|
this.state = state;
|
|
75
|
+
this.tracer = tracer;
|
|
72
76
|
this.unifiedSessionId = unifiedSessionId;
|
|
73
77
|
|
|
74
78
|
const { sdk, browser } = clientDetails;
|
|
@@ -166,10 +170,10 @@ export class SfuStatsReporter {
|
|
|
166
170
|
|
|
167
171
|
const subscriberTrace = this.subscriber.tracer?.take();
|
|
168
172
|
const publisherTrace = this.publisher?.tracer?.take();
|
|
169
|
-
const
|
|
173
|
+
const tracer = this.tracer.take();
|
|
170
174
|
const sfuTrace = this.sfuClient.getTrace();
|
|
171
|
-
const traces = [
|
|
172
|
-
...
|
|
175
|
+
const traces: TraceRecord[] = [
|
|
176
|
+
...tracer.snapshot,
|
|
173
177
|
...(sfuTrace?.snapshot ?? []),
|
|
174
178
|
...(publisherTrace?.snapshot ?? []),
|
|
175
179
|
...(subscriberTrace?.snapshot ?? []),
|
|
@@ -198,7 +202,7 @@ export class SfuStatsReporter {
|
|
|
198
202
|
} catch (err) {
|
|
199
203
|
publisherTrace?.rollback();
|
|
200
204
|
subscriberTrace?.rollback();
|
|
201
|
-
|
|
205
|
+
tracer.rollback();
|
|
202
206
|
sfuTrace?.rollback();
|
|
203
207
|
throw err;
|
|
204
208
|
}
|
package/src/stats/utils.ts
CHANGED
|
@@ -13,6 +13,21 @@ export const flatten = (report: RTCStatsReport) => {
|
|
|
13
13
|
return stats;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Dump the provided MediaStream into a JSON object.
|
|
18
|
+
*/
|
|
19
|
+
export const dumpStream = (stream: MediaStream) => ({
|
|
20
|
+
id: stream.id,
|
|
21
|
+
tracks: stream.getTracks().map((track) => ({
|
|
22
|
+
id: track.id,
|
|
23
|
+
kind: track.kind,
|
|
24
|
+
label: track.label,
|
|
25
|
+
enabled: track.enabled,
|
|
26
|
+
muted: track.muted,
|
|
27
|
+
readyState: track.readyState,
|
|
28
|
+
})),
|
|
29
|
+
});
|
|
30
|
+
|
|
16
31
|
export const getSdkSignature = (clientDetails: ClientDetails) => {
|
|
17
32
|
const { sdk, ...platform } = clientDetails;
|
|
18
33
|
const sdkName = getSdkName(sdk);
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Tracer } from './Tracer';
|
|
2
|
-
|
|
3
|
-
export const tracer = new Tracer(null);
|
|
4
|
-
|
|
5
|
-
if (
|
|
6
|
-
typeof navigator !== 'undefined' &&
|
|
7
|
-
typeof navigator.mediaDevices !== 'undefined'
|
|
8
|
-
) {
|
|
9
|
-
const dumpStream = (stream: MediaStream) => ({
|
|
10
|
-
id: stream.id,
|
|
11
|
-
tracks: stream.getTracks().map((track) => ({
|
|
12
|
-
id: track.id,
|
|
13
|
-
kind: track.kind,
|
|
14
|
-
label: track.label,
|
|
15
|
-
enabled: track.enabled,
|
|
16
|
-
muted: track.muted,
|
|
17
|
-
readyState: track.readyState,
|
|
18
|
-
})),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const trace = tracer.trace;
|
|
22
|
-
const target = navigator.mediaDevices;
|
|
23
|
-
for (const method of ['getUserMedia', 'getDisplayMedia'] as const) {
|
|
24
|
-
const original = target[method];
|
|
25
|
-
if (!original) continue;
|
|
26
|
-
|
|
27
|
-
let mark = 0;
|
|
28
|
-
target[method] = async function tracedMethod(
|
|
29
|
-
constraints: MediaStreamConstraints,
|
|
30
|
-
) {
|
|
31
|
-
const tag = `navigator.mediaDevices.${method}.${mark++}`;
|
|
32
|
-
trace(tag, constraints);
|
|
33
|
-
try {
|
|
34
|
-
const stream = await original.call(target, constraints);
|
|
35
|
-
trace(`${tag}.OnSuccess`, dumpStream(stream));
|
|
36
|
-
return stream;
|
|
37
|
-
} catch (err) {
|
|
38
|
-
trace(`${tag}.OnFailure`, (err as Error).name);
|
|
39
|
-
throw err;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
}
|