@stream-io/video-client 1.21.0 → 1.22.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 +19 -0
- package/dist/index.browser.es.js +171 -143
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +171 -143
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +171 -143
- 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/gen/video/sfu/models/models.d.ts +8 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +1 -0
- package/dist/src/stats/SfuStatsReporter.d.ts +5 -1
- package/dist/src/stats/utils.d.ts +14 -0
- package/index.ts +0 -4
- package/package.json +10 -10
- package/src/Call.ts +15 -8
- 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/gen/video/sfu/models/models.ts +14 -0
- package/src/rtc/BasePeerConnection.ts +11 -5
- package/src/rtc/Publisher.ts +5 -1
- package/src/rtc/__tests__/Publisher.test.ts +2 -2
- package/src/rtc/__tests__/Subscriber.test.ts +2 -2
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -1
- package/src/rtc/__tests__/videoLayers.test.ts +44 -0
- package/src/rtc/videoLayers.ts +6 -3
- package/src/stats/SfuStatsReporter.ts +15 -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/dist/index.es.js
CHANGED
|
@@ -9,75 +9,6 @@ import { UAParser } from 'ua-parser-js';
|
|
|
9
9
|
import { parse } from 'sdp-transform';
|
|
10
10
|
import https from 'https';
|
|
11
11
|
|
|
12
|
-
class Tracer {
|
|
13
|
-
constructor(id) {
|
|
14
|
-
this.buffer = [];
|
|
15
|
-
this.enabled = true;
|
|
16
|
-
this.setEnabled = (enabled) => {
|
|
17
|
-
if (this.enabled === enabled)
|
|
18
|
-
return;
|
|
19
|
-
this.enabled = enabled;
|
|
20
|
-
this.buffer = [];
|
|
21
|
-
};
|
|
22
|
-
this.trace = (tag, data) => {
|
|
23
|
-
if (!this.enabled)
|
|
24
|
-
return;
|
|
25
|
-
this.buffer.push([tag, this.id, data, Date.now()]);
|
|
26
|
-
};
|
|
27
|
-
this.take = () => {
|
|
28
|
-
const snapshot = this.buffer;
|
|
29
|
-
this.buffer = [];
|
|
30
|
-
return {
|
|
31
|
-
snapshot,
|
|
32
|
-
rollback: () => {
|
|
33
|
-
this.buffer.unshift(...snapshot);
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
this.dispose = () => {
|
|
38
|
-
this.buffer = [];
|
|
39
|
-
};
|
|
40
|
-
this.id = id;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const tracer = new Tracer(null);
|
|
45
|
-
if (typeof navigator !== 'undefined' &&
|
|
46
|
-
typeof navigator.mediaDevices !== 'undefined') {
|
|
47
|
-
const dumpStream = (stream) => ({
|
|
48
|
-
id: stream.id,
|
|
49
|
-
tracks: stream.getTracks().map((track) => ({
|
|
50
|
-
id: track.id,
|
|
51
|
-
kind: track.kind,
|
|
52
|
-
label: track.label,
|
|
53
|
-
enabled: track.enabled,
|
|
54
|
-
muted: track.muted,
|
|
55
|
-
readyState: track.readyState,
|
|
56
|
-
})),
|
|
57
|
-
});
|
|
58
|
-
const trace = tracer.trace;
|
|
59
|
-
const target = navigator.mediaDevices;
|
|
60
|
-
for (const method of ['getUserMedia', 'getDisplayMedia']) {
|
|
61
|
-
const original = target[method];
|
|
62
|
-
if (!original)
|
|
63
|
-
continue;
|
|
64
|
-
let mark = 0;
|
|
65
|
-
target[method] = async function tracedMethod(constraints) {
|
|
66
|
-
const tag = `navigator.mediaDevices.${method}.${mark++}`;
|
|
67
|
-
trace(tag, constraints);
|
|
68
|
-
try {
|
|
69
|
-
const stream = await original.call(target, constraints);
|
|
70
|
-
trace(`${tag}.OnSuccess`, dumpStream(stream));
|
|
71
|
-
return stream;
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
trace(`${tag}.OnFailure`, err.name);
|
|
75
|
-
throw err;
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
12
|
/* tslint:disable */
|
|
82
13
|
/**
|
|
83
14
|
* @export
|
|
@@ -1431,6 +1362,12 @@ class PublishOption$Type extends MessageType {
|
|
|
1431
1362
|
T: () => VideoDimension,
|
|
1432
1363
|
},
|
|
1433
1364
|
{ no: 8, name: 'id', kind: 'scalar', T: 5 /*ScalarType.INT32*/ },
|
|
1365
|
+
{
|
|
1366
|
+
no: 9,
|
|
1367
|
+
name: 'use_single_layer',
|
|
1368
|
+
kind: 'scalar',
|
|
1369
|
+
T: 8 /*ScalarType.BOOL*/,
|
|
1370
|
+
},
|
|
1434
1371
|
]);
|
|
1435
1372
|
}
|
|
1436
1373
|
}
|
|
@@ -5414,6 +5351,20 @@ const flatten = (report) => {
|
|
|
5414
5351
|
});
|
|
5415
5352
|
return stats;
|
|
5416
5353
|
};
|
|
5354
|
+
/**
|
|
5355
|
+
* Dump the provided MediaStream into a JSON object.
|
|
5356
|
+
*/
|
|
5357
|
+
const dumpStream = (stream) => ({
|
|
5358
|
+
id: stream.id,
|
|
5359
|
+
tracks: stream.getTracks().map((track) => ({
|
|
5360
|
+
id: track.id,
|
|
5361
|
+
kind: track.kind,
|
|
5362
|
+
label: track.label,
|
|
5363
|
+
enabled: track.enabled,
|
|
5364
|
+
muted: track.muted,
|
|
5365
|
+
readyState: track.readyState,
|
|
5366
|
+
})),
|
|
5367
|
+
});
|
|
5417
5368
|
const getSdkSignature = (clientDetails) => {
|
|
5418
5369
|
const { sdk, ...platform } = clientDetails;
|
|
5419
5370
|
const sdkName = getSdkName(sdk);
|
|
@@ -5702,7 +5653,7 @@ const aggregate = (stats) => {
|
|
|
5702
5653
|
return report;
|
|
5703
5654
|
};
|
|
5704
5655
|
|
|
5705
|
-
const version = "1.
|
|
5656
|
+
const version = "1.22.1";
|
|
5706
5657
|
const [major, minor, patch] = version.split('.');
|
|
5707
5658
|
let sdkInfo = {
|
|
5708
5659
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5839,7 +5790,7 @@ const getClientDetails = async () => {
|
|
|
5839
5790
|
};
|
|
5840
5791
|
|
|
5841
5792
|
class SfuStatsReporter {
|
|
5842
|
-
constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, unifiedSessionId, }) {
|
|
5793
|
+
constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, tracer, unifiedSessionId, }) {
|
|
5843
5794
|
this.logger = getLogger(['SfuStatsReporter']);
|
|
5844
5795
|
this.inputDevices = new Map();
|
|
5845
5796
|
this.observeDevice = (device, kind) => {
|
|
@@ -5905,10 +5856,10 @@ class SfuStatsReporter {
|
|
|
5905
5856
|
}
|
|
5906
5857
|
const subscriberTrace = this.subscriber.tracer?.take();
|
|
5907
5858
|
const publisherTrace = this.publisher?.tracer?.take();
|
|
5908
|
-
const
|
|
5859
|
+
const tracer = this.tracer.take();
|
|
5909
5860
|
const sfuTrace = this.sfuClient.getTrace();
|
|
5910
5861
|
const traces = [
|
|
5911
|
-
...
|
|
5862
|
+
...tracer.snapshot,
|
|
5912
5863
|
...(sfuTrace?.snapshot ?? []),
|
|
5913
5864
|
...(publisherTrace?.snapshot ?? []),
|
|
5914
5865
|
...(subscriberTrace?.snapshot ?? []),
|
|
@@ -5937,7 +5888,7 @@ class SfuStatsReporter {
|
|
|
5937
5888
|
catch (err) {
|
|
5938
5889
|
publisherTrace?.rollback();
|
|
5939
5890
|
subscriberTrace?.rollback();
|
|
5940
|
-
|
|
5891
|
+
tracer.rollback();
|
|
5941
5892
|
sfuTrace?.rollback();
|
|
5942
5893
|
throw err;
|
|
5943
5894
|
}
|
|
@@ -5965,6 +5916,11 @@ class SfuStatsReporter {
|
|
|
5965
5916
|
clearTimeout(this.timeoutId);
|
|
5966
5917
|
this.timeoutId = undefined;
|
|
5967
5918
|
};
|
|
5919
|
+
this.flush = () => {
|
|
5920
|
+
this.run().catch((err) => {
|
|
5921
|
+
this.logger('warn', 'Failed to flush report stats', err);
|
|
5922
|
+
});
|
|
5923
|
+
};
|
|
5968
5924
|
this.scheduleOne = (timeout) => {
|
|
5969
5925
|
clearTimeout(this.timeoutId);
|
|
5970
5926
|
this.timeoutId = setTimeout(() => {
|
|
@@ -5980,6 +5936,7 @@ class SfuStatsReporter {
|
|
|
5980
5936
|
this.microphone = microphone;
|
|
5981
5937
|
this.camera = camera;
|
|
5982
5938
|
this.state = state;
|
|
5939
|
+
this.tracer = tracer;
|
|
5983
5940
|
this.unifiedSessionId = unifiedSessionId;
|
|
5984
5941
|
const { sdk, browser } = clientDetails;
|
|
5985
5942
|
this.sdkName = getSdkName(sdk);
|
|
@@ -6265,6 +6222,38 @@ const getCodecFromStats = (stats, codecId) => {
|
|
|
6265
6222
|
});
|
|
6266
6223
|
};
|
|
6267
6224
|
|
|
6225
|
+
class Tracer {
|
|
6226
|
+
constructor(id) {
|
|
6227
|
+
this.buffer = [];
|
|
6228
|
+
this.enabled = true;
|
|
6229
|
+
this.setEnabled = (enabled) => {
|
|
6230
|
+
if (this.enabled === enabled)
|
|
6231
|
+
return;
|
|
6232
|
+
this.enabled = enabled;
|
|
6233
|
+
this.buffer = [];
|
|
6234
|
+
};
|
|
6235
|
+
this.trace = (tag, data) => {
|
|
6236
|
+
if (!this.enabled)
|
|
6237
|
+
return;
|
|
6238
|
+
this.buffer.push([tag, this.id, data, Date.now()]);
|
|
6239
|
+
};
|
|
6240
|
+
this.take = () => {
|
|
6241
|
+
const snapshot = this.buffer;
|
|
6242
|
+
this.buffer = [];
|
|
6243
|
+
return {
|
|
6244
|
+
snapshot,
|
|
6245
|
+
rollback: () => {
|
|
6246
|
+
this.buffer.unshift(...snapshot);
|
|
6247
|
+
},
|
|
6248
|
+
};
|
|
6249
|
+
};
|
|
6250
|
+
this.dispose = () => {
|
|
6251
|
+
this.buffer = [];
|
|
6252
|
+
};
|
|
6253
|
+
this.id = id;
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6268
6257
|
/**
|
|
6269
6258
|
* A base class for the `Publisher` and `Subscriber` classes.
|
|
6270
6259
|
* @internal
|
|
@@ -6278,13 +6267,15 @@ class BasePeerConnection {
|
|
|
6278
6267
|
this.isDisposed = false;
|
|
6279
6268
|
this.trackIdToTrackType = new Map();
|
|
6280
6269
|
this.subscriptions = [];
|
|
6270
|
+
this.lock = Math.random().toString(36).slice(2);
|
|
6281
6271
|
/**
|
|
6282
6272
|
* Handles events synchronously.
|
|
6283
6273
|
* Consecutive events are queued and executed one after the other.
|
|
6284
6274
|
*/
|
|
6285
6275
|
this.on = (event, fn) => {
|
|
6286
6276
|
this.subscriptions.push(this.dispatcher.on(event, (e) => {
|
|
6287
|
-
|
|
6277
|
+
const lockKey = `pc.${this.lock}.${event}`;
|
|
6278
|
+
withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
|
|
6288
6279
|
if (this.isDisposed)
|
|
6289
6280
|
return;
|
|
6290
6281
|
this.logger('warn', `Error handling ${event}`, err);
|
|
@@ -6392,11 +6383,12 @@ class BasePeerConnection {
|
|
|
6392
6383
|
// do nothing when ICE is restarting
|
|
6393
6384
|
if (this.isIceRestarting)
|
|
6394
6385
|
return;
|
|
6395
|
-
if (state === 'failed'
|
|
6386
|
+
if (state === 'failed') {
|
|
6387
|
+
this.onUnrecoverableError?.('ICE connection failed');
|
|
6388
|
+
}
|
|
6389
|
+
else if (state === 'disconnected') {
|
|
6396
6390
|
this.logger('debug', `Attempting to restart ICE`);
|
|
6397
6391
|
this.restartIce().catch((e) => {
|
|
6398
|
-
if (this.isDisposed)
|
|
6399
|
-
return;
|
|
6400
6392
|
const reason = `ICE restart failed`;
|
|
6401
6393
|
this.logger('error', reason, e);
|
|
6402
6394
|
this.onUnrecoverableError?.(`${reason}: ${e}`);
|
|
@@ -6443,9 +6435,12 @@ class BasePeerConnection {
|
|
|
6443
6435
|
this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
6444
6436
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
|
|
6445
6437
|
if (enableTracing) {
|
|
6446
|
-
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}
|
|
6438
|
+
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
|
|
6447
6439
|
this.tracer = new Tracer(tag);
|
|
6448
|
-
this.tracer.trace('create',
|
|
6440
|
+
this.tracer.trace('create', {
|
|
6441
|
+
url: sfuClient.edgeName,
|
|
6442
|
+
...connectionConfig,
|
|
6443
|
+
});
|
|
6449
6444
|
traceRTCPeerConnection(this.pc, this.tracer.trace);
|
|
6450
6445
|
}
|
|
6451
6446
|
}
|
|
@@ -6661,7 +6656,7 @@ const computeVideoLayers = (videoTrack, publishOption) => {
|
|
|
6661
6656
|
const optimalVideoLayers = [];
|
|
6662
6657
|
const settings = videoTrack.getSettings();
|
|
6663
6658
|
const { width = 0, height = 0 } = settings;
|
|
6664
|
-
const { bitrate, codec, fps, maxSpatialLayers = 3, maxTemporalLayers = 3, videoDimension = { width: 1280, height: 720 }, } = publishOption;
|
|
6659
|
+
const { bitrate, codec, fps, maxSpatialLayers = 3, maxTemporalLayers = 3, videoDimension = { width: 1280, height: 720 }, useSingleLayer, } = publishOption;
|
|
6665
6660
|
const maxBitrate = getComputedMaxBitrate(videoDimension, width, height, bitrate);
|
|
6666
6661
|
let downscaleFactor = 1;
|
|
6667
6662
|
let bitrateFactor = 1;
|
|
@@ -6678,7 +6673,7 @@ const computeVideoLayers = (videoTrack, publishOption) => {
|
|
|
6678
6673
|
if (svcCodec) {
|
|
6679
6674
|
// for SVC codecs, we need to set the scalability mode, and the
|
|
6680
6675
|
// codec will handle the rest (layers, temporal layers, etc.)
|
|
6681
|
-
layer.scalabilityMode = toScalabilityMode(maxSpatialLayers, maxTemporalLayers);
|
|
6676
|
+
layer.scalabilityMode = toScalabilityMode(useSingleLayer ? 1 : maxSpatialLayers, maxTemporalLayers);
|
|
6682
6677
|
}
|
|
6683
6678
|
else {
|
|
6684
6679
|
// for non-SVC codecs, we need to downscale proportionally (simulcast)
|
|
@@ -6693,7 +6688,7 @@ const computeVideoLayers = (videoTrack, publishOption) => {
|
|
|
6693
6688
|
}
|
|
6694
6689
|
// for simplicity, we start with all layers enabled, then this function
|
|
6695
6690
|
// will clear/reassign the layers that are not needed
|
|
6696
|
-
return withSimulcastConstraints(settings, optimalVideoLayers);
|
|
6691
|
+
return withSimulcastConstraints(settings, optimalVideoLayers, useSingleLayer);
|
|
6697
6692
|
};
|
|
6698
6693
|
/**
|
|
6699
6694
|
* Computes the maximum bitrate for a given resolution.
|
|
@@ -6727,7 +6722,7 @@ const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight, bi
|
|
|
6727
6722
|
*
|
|
6728
6723
|
* https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/media/engine/simulcast.cc#90
|
|
6729
6724
|
*/
|
|
6730
|
-
const withSimulcastConstraints = (settings, optimalVideoLayers) => {
|
|
6725
|
+
const withSimulcastConstraints = (settings, optimalVideoLayers, useSingleLayer) => {
|
|
6731
6726
|
let layers;
|
|
6732
6727
|
const size = Math.max(settings.width || 0, settings.height || 0);
|
|
6733
6728
|
if (size <= 320) {
|
|
@@ -6743,9 +6738,10 @@ const withSimulcastConstraints = (settings, optimalVideoLayers) => {
|
|
|
6743
6738
|
layers = optimalVideoLayers;
|
|
6744
6739
|
}
|
|
6745
6740
|
const ridMapping = ['q', 'h', 'f'];
|
|
6746
|
-
return layers.map((layer, index) => ({
|
|
6741
|
+
return layers.map((layer, index, arr) => ({
|
|
6747
6742
|
...layer,
|
|
6748
6743
|
rid: ridMapping[index], // reassign rid
|
|
6744
|
+
active: useSingleLayer && index < arr.length - 1 ? false : layer.active,
|
|
6749
6745
|
}));
|
|
6750
6746
|
};
|
|
6751
6747
|
|
|
@@ -6832,6 +6828,9 @@ class Publisher extends BasePeerConnection {
|
|
|
6832
6828
|
direction: 'sendonly',
|
|
6833
6829
|
sendEncodings,
|
|
6834
6830
|
});
|
|
6831
|
+
const params = transceiver.sender.getParameters();
|
|
6832
|
+
params.degradationPreference = 'maintain-framerate';
|
|
6833
|
+
await transceiver.sender.setParameters(params);
|
|
6835
6834
|
const trackType = publishOption.trackType;
|
|
6836
6835
|
this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
6837
6836
|
this.transceiverCache.add(publishOption, transceiver);
|
|
@@ -7000,7 +6999,7 @@ class Publisher extends BasePeerConnection {
|
|
|
7000
6999
|
* @param options the optional offer options to use.
|
|
7001
7000
|
*/
|
|
7002
7001
|
this.negotiate = async (options) => {
|
|
7003
|
-
return withoutConcurrency(
|
|
7002
|
+
return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
|
|
7004
7003
|
const offer = await this.pc.createOffer(options);
|
|
7005
7004
|
const tracks = this.getAnnouncedTracks(offer.sdp);
|
|
7006
7005
|
if (!tracks.length)
|
|
@@ -7908,6 +7907,13 @@ const watchSfuCallEnded = (call) => {
|
|
|
7908
7907
|
if (call.state.callingState === CallingState.LEFT)
|
|
7909
7908
|
return;
|
|
7910
7909
|
try {
|
|
7910
|
+
if (e.reason === CallEndedReason.LIVE_ENDED) {
|
|
7911
|
+
call.state.setBackstage(true);
|
|
7912
|
+
// don't leave the call if the user has permission to join backstage
|
|
7913
|
+
const { hasPermission } = call.permissionsContext;
|
|
7914
|
+
if (hasPermission(OwnCapability.JOIN_BACKSTAGE))
|
|
7915
|
+
return;
|
|
7916
|
+
}
|
|
7911
7917
|
// `call.ended` event arrived after the call is already left
|
|
7912
7918
|
// and all event handlers are detached. We need to manually
|
|
7913
7919
|
// update the call state to reflect the call has ended.
|
|
@@ -9120,15 +9126,25 @@ const getVideoDevices = lazy(() => {
|
|
|
9120
9126
|
const getAudioOutputDevices = lazy(() => {
|
|
9121
9127
|
return merge(getDeviceChangeObserver(), getAudioBrowserPermission().asObservable()).pipe(startWith(undefined), concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput')), shareReplay(1));
|
|
9122
9128
|
});
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9129
|
+
let getUserMediaExecId = 0;
|
|
9130
|
+
const getStream = async (constraints, tracer) => {
|
|
9131
|
+
const tag = `navigator.mediaDevices.getUserMedia.${getUserMediaExecId++}.`;
|
|
9132
|
+
try {
|
|
9133
|
+
tracer?.trace(tag, constraints);
|
|
9134
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
9135
|
+
tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
|
|
9136
|
+
if (isFirefox()) {
|
|
9137
|
+
// When enumerating devices, Firefox will hide device labels unless there's been
|
|
9138
|
+
// an active user media stream on the page. So we force device list updates after
|
|
9139
|
+
// every successful getUserMedia call.
|
|
9140
|
+
navigator.mediaDevices.dispatchEvent(new Event('devicechange'));
|
|
9141
|
+
}
|
|
9142
|
+
return stream;
|
|
9143
|
+
}
|
|
9144
|
+
catch (error) {
|
|
9145
|
+
tracer?.trace(`${tag}OnFailure`, error.name);
|
|
9146
|
+
throw error;
|
|
9130
9147
|
}
|
|
9131
|
-
return stream;
|
|
9132
9148
|
};
|
|
9133
9149
|
function isNotFoundOrOverconstrainedError(error) {
|
|
9134
9150
|
if (!error || typeof error !== 'object') {
|
|
@@ -9152,11 +9168,11 @@ function isNotFoundOrOverconstrainedError(error) {
|
|
|
9152
9168
|
* Returns an audio media stream that fulfills the given constraints.
|
|
9153
9169
|
* If no constraints are provided, it uses the browser's default ones.
|
|
9154
9170
|
*
|
|
9155
|
-
* @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.
|
|
9156
9171
|
* @param trackConstraints the constraints to use when requesting the stream.
|
|
9157
|
-
* @
|
|
9172
|
+
* @param tracer the tracer to use for tracing the stream creation.
|
|
9173
|
+
* @returns a new `MediaStream` fulfilling the given constraints.
|
|
9158
9174
|
*/
|
|
9159
|
-
const getAudioStream = async (trackConstraints) => {
|
|
9175
|
+
const getAudioStream = async (trackConstraints, tracer) => {
|
|
9160
9176
|
const constraints = {
|
|
9161
9177
|
audio: {
|
|
9162
9178
|
...audioDeviceConstraints.audio,
|
|
@@ -9168,7 +9184,7 @@ const getAudioStream = async (trackConstraints) => {
|
|
|
9168
9184
|
throwOnNotAllowed: true,
|
|
9169
9185
|
forcePrompt: true,
|
|
9170
9186
|
});
|
|
9171
|
-
return await getStream(constraints);
|
|
9187
|
+
return await getStream(constraints, tracer);
|
|
9172
9188
|
}
|
|
9173
9189
|
catch (error) {
|
|
9174
9190
|
if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
|
|
@@ -9188,11 +9204,11 @@ const getAudioStream = async (trackConstraints) => {
|
|
|
9188
9204
|
* Returns a video media stream that fulfills the given constraints.
|
|
9189
9205
|
* If no constraints are provided, it uses the browser's default ones.
|
|
9190
9206
|
*
|
|
9191
|
-
* @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.
|
|
9192
9207
|
* @param trackConstraints the constraints to use when requesting the stream.
|
|
9208
|
+
* @param tracer the tracer to use for tracing the stream creation.
|
|
9193
9209
|
* @returns a new `MediaStream` fulfilling the given constraints.
|
|
9194
9210
|
*/
|
|
9195
|
-
const getVideoStream = async (trackConstraints) => {
|
|
9211
|
+
const getVideoStream = async (trackConstraints, tracer) => {
|
|
9196
9212
|
const constraints = {
|
|
9197
9213
|
video: {
|
|
9198
9214
|
...videoDeviceConstraints.video,
|
|
@@ -9204,7 +9220,7 @@ const getVideoStream = async (trackConstraints) => {
|
|
|
9204
9220
|
throwOnNotAllowed: true,
|
|
9205
9221
|
forcePrompt: true,
|
|
9206
9222
|
});
|
|
9207
|
-
return await getStream(constraints);
|
|
9223
|
+
return await getStream(constraints, tracer);
|
|
9208
9224
|
}
|
|
9209
9225
|
catch (error) {
|
|
9210
9226
|
if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
|
|
@@ -9220,19 +9236,21 @@ const getVideoStream = async (trackConstraints) => {
|
|
|
9220
9236
|
throw error;
|
|
9221
9237
|
}
|
|
9222
9238
|
};
|
|
9239
|
+
let getDisplayMediaExecId = 0;
|
|
9223
9240
|
/**
|
|
9224
9241
|
* Prompts the user for a permission to share a screen.
|
|
9225
9242
|
* If the user grants the permission, a screen sharing stream is returned. Throws otherwise.
|
|
9226
9243
|
*
|
|
9227
9244
|
* The callers of this API are responsible to handle the possible errors.
|
|
9228
9245
|
*
|
|
9229
|
-
* @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.
|
|
9230
|
-
*
|
|
9231
9246
|
* @param options any additional options to pass to the [`getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) API.
|
|
9247
|
+
* @param tracer the tracer to use for tracing the stream creation.
|
|
9232
9248
|
*/
|
|
9233
|
-
const getScreenShareStream = async (options) => {
|
|
9249
|
+
const getScreenShareStream = async (options, tracer) => {
|
|
9250
|
+
const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
|
|
9234
9251
|
try {
|
|
9235
|
-
|
|
9252
|
+
tracer?.trace(tag, options);
|
|
9253
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
9236
9254
|
video: true,
|
|
9237
9255
|
audio: {
|
|
9238
9256
|
channelCount: {
|
|
@@ -9246,8 +9264,11 @@ const getScreenShareStream = async (options) => {
|
|
|
9246
9264
|
systemAudio: 'include',
|
|
9247
9265
|
...options,
|
|
9248
9266
|
});
|
|
9267
|
+
tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
|
|
9268
|
+
return stream;
|
|
9249
9269
|
}
|
|
9250
9270
|
catch (e) {
|
|
9271
|
+
tracer?.trace(`${tag}OnFailure`, e.name);
|
|
9251
9272
|
getLogger(['devices'])('error', 'Failed to get screen share stream', e);
|
|
9252
9273
|
throw e;
|
|
9253
9274
|
}
|
|
@@ -9285,9 +9306,6 @@ const isMobile = () => /Mobi/i.test(navigator.userAgent);
|
|
|
9285
9306
|
|
|
9286
9307
|
class InputMediaDeviceManager {
|
|
9287
9308
|
constructor(call, state, trackType) {
|
|
9288
|
-
this.call = call;
|
|
9289
|
-
this.state = state;
|
|
9290
|
-
this.trackType = trackType;
|
|
9291
9309
|
/**
|
|
9292
9310
|
* if true, stops the media stream when call is left
|
|
9293
9311
|
*/
|
|
@@ -9305,6 +9323,9 @@ class InputMediaDeviceManager {
|
|
|
9305
9323
|
this.dispose = () => {
|
|
9306
9324
|
this.subscriptions.forEach((s) => s());
|
|
9307
9325
|
};
|
|
9326
|
+
this.call = call;
|
|
9327
|
+
this.state = state;
|
|
9328
|
+
this.trackType = trackType;
|
|
9308
9329
|
this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
|
|
9309
9330
|
if (deviceIds$ &&
|
|
9310
9331
|
!isReactNative() &&
|
|
@@ -9901,32 +9922,32 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
9901
9922
|
* @param direction the direction of the camera to select.
|
|
9902
9923
|
*/
|
|
9903
9924
|
async selectDirection(direction) {
|
|
9904
|
-
if (this.isDirectionSupportedByDevice()) {
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
+
if (!this.isDirectionSupportedByDevice()) {
|
|
9926
|
+
this.logger('warn', 'Setting direction is not supported on this device');
|
|
9927
|
+
return;
|
|
9928
|
+
}
|
|
9929
|
+
// providing both device id and direction doesn't work, so we deselect the device
|
|
9930
|
+
this.state.setDirection(direction);
|
|
9931
|
+
this.state.setDevice(undefined);
|
|
9932
|
+
if (isReactNative()) {
|
|
9933
|
+
const videoTrack = this.getTracks()[0];
|
|
9934
|
+
await videoTrack?.applyConstraints({
|
|
9935
|
+
facingMode: direction === 'front' ? 'user' : 'environment',
|
|
9936
|
+
});
|
|
9937
|
+
return;
|
|
9938
|
+
}
|
|
9939
|
+
this.getTracks().forEach((track) => track.stop());
|
|
9940
|
+
try {
|
|
9941
|
+
await this.unmuteStream();
|
|
9942
|
+
}
|
|
9943
|
+
catch (error) {
|
|
9944
|
+
if (error instanceof Error && error.name === 'NotReadableError') {
|
|
9945
|
+
// the camera is already in use, and the device can't use it unless it's released.
|
|
9946
|
+
// in that case, we need to stop the stream and start it again.
|
|
9947
|
+
await this.muteStream();
|
|
9925
9948
|
await this.unmuteStream();
|
|
9926
9949
|
}
|
|
9927
|
-
|
|
9928
|
-
else {
|
|
9929
|
-
this.logger('warn', 'Camera direction ignored for desktop devices');
|
|
9950
|
+
throw error;
|
|
9930
9951
|
}
|
|
9931
9952
|
}
|
|
9932
9953
|
/**
|
|
@@ -10011,7 +10032,7 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
10011
10032
|
constraints.facingMode =
|
|
10012
10033
|
this.state.direction === 'front' ? 'user' : 'environment';
|
|
10013
10034
|
}
|
|
10014
|
-
return getVideoStream(constraints);
|
|
10035
|
+
return getVideoStream(constraints, this.call.tracer);
|
|
10015
10036
|
}
|
|
10016
10037
|
}
|
|
10017
10038
|
|
|
@@ -10422,7 +10443,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
10422
10443
|
return getAudioDevices();
|
|
10423
10444
|
}
|
|
10424
10445
|
getStream(constraints) {
|
|
10425
|
-
return getAudioStream(constraints);
|
|
10446
|
+
return getAudioStream(constraints, this.call.tracer);
|
|
10426
10447
|
}
|
|
10427
10448
|
async startSpeakingWhileMutedDetection(deviceId) {
|
|
10428
10449
|
await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
|
|
@@ -10565,7 +10586,7 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10565
10586
|
if (!this.state.audioEnabled) {
|
|
10566
10587
|
constraints.audio = false;
|
|
10567
10588
|
}
|
|
10568
|
-
return getScreenShareStream(constraints);
|
|
10589
|
+
return getScreenShareStream(constraints, this.call.tracer);
|
|
10569
10590
|
}
|
|
10570
10591
|
async stopPublishStream() {
|
|
10571
10592
|
return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
|
|
@@ -10574,18 +10595,19 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10574
10595
|
* Overrides the default `select` method to throw an error.
|
|
10575
10596
|
*/
|
|
10576
10597
|
async select() {
|
|
10577
|
-
throw new Error('
|
|
10598
|
+
throw new Error('Not supported');
|
|
10578
10599
|
}
|
|
10579
10600
|
}
|
|
10580
10601
|
|
|
10581
10602
|
class SpeakerState {
|
|
10582
|
-
constructor() {
|
|
10603
|
+
constructor(tracer) {
|
|
10583
10604
|
this.selectedDeviceSubject = new BehaviorSubject('');
|
|
10584
10605
|
this.volumeSubject = new BehaviorSubject(1);
|
|
10585
10606
|
/**
|
|
10586
10607
|
* [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
|
|
10587
10608
|
*/
|
|
10588
10609
|
this.isDeviceSelectionSupported = checkIfAudioOutputChangeSupported();
|
|
10610
|
+
this.tracer = tracer;
|
|
10589
10611
|
this.selectedDevice$ = this.selectedDeviceSubject
|
|
10590
10612
|
.asObservable()
|
|
10591
10613
|
.pipe(distinctUntilChanged());
|
|
@@ -10615,7 +10637,7 @@ class SpeakerState {
|
|
|
10615
10637
|
*/
|
|
10616
10638
|
setDevice(deviceId) {
|
|
10617
10639
|
setCurrentValue(this.selectedDeviceSubject, deviceId);
|
|
10618
|
-
tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
|
|
10640
|
+
this.tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
|
|
10619
10641
|
}
|
|
10620
10642
|
/**
|
|
10621
10643
|
* @internal
|
|
@@ -10628,7 +10650,6 @@ class SpeakerState {
|
|
|
10628
10650
|
|
|
10629
10651
|
class SpeakerManager {
|
|
10630
10652
|
constructor(call) {
|
|
10631
|
-
this.state = new SpeakerState();
|
|
10632
10653
|
this.subscriptions = [];
|
|
10633
10654
|
/**
|
|
10634
10655
|
* Disposes the manager.
|
|
@@ -10639,6 +10660,7 @@ class SpeakerManager {
|
|
|
10639
10660
|
this.subscriptions.forEach((s) => s.unsubscribe());
|
|
10640
10661
|
};
|
|
10641
10662
|
this.call = call;
|
|
10663
|
+
this.state = new SpeakerState(call.tracer);
|
|
10642
10664
|
if (deviceIds$ && !isReactNative()) {
|
|
10643
10665
|
this.subscriptions.push(combineLatest([deviceIds$, this.state.selectedDevice$]).subscribe(([devices, deviceId]) => {
|
|
10644
10666
|
if (!deviceId) {
|
|
@@ -10732,6 +10754,7 @@ class Call {
|
|
|
10732
10754
|
* The permissions context of this call.
|
|
10733
10755
|
*/
|
|
10734
10756
|
this.permissionsContext = new PermissionsContext();
|
|
10757
|
+
this.tracer = new Tracer(null);
|
|
10735
10758
|
/**
|
|
10736
10759
|
* The event dispatcher instance dedicated to this Call instance.
|
|
10737
10760
|
* @private
|
|
@@ -10987,6 +11010,7 @@ class Call {
|
|
|
10987
11010
|
}
|
|
10988
11011
|
this.statsReporter?.stop();
|
|
10989
11012
|
this.statsReporter = undefined;
|
|
11013
|
+
this.sfuStatsReporter?.flush();
|
|
10990
11014
|
this.sfuStatsReporter?.stop();
|
|
10991
11015
|
this.sfuStatsReporter = undefined;
|
|
10992
11016
|
this.subscriber?.dispose();
|
|
@@ -11300,7 +11324,7 @@ class Call {
|
|
|
11300
11324
|
if (!performingRejoin && !performingFastReconnect && !performingMigration) {
|
|
11301
11325
|
this.sfuStatsReporter?.sendConnectionTime((Date.now() - connectStartTime) / 1000);
|
|
11302
11326
|
}
|
|
11303
|
-
if (performingRejoin) {
|
|
11327
|
+
if (performingRejoin && isWsHealthy) {
|
|
11304
11328
|
const strategy = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
11305
11329
|
await previousSfuClient?.leaveAndClose(`Closing previous WS after reconnect with strategy: ${strategy}`);
|
|
11306
11330
|
}
|
|
@@ -11460,7 +11484,6 @@ class Call {
|
|
|
11460
11484
|
},
|
|
11461
11485
|
});
|
|
11462
11486
|
}
|
|
11463
|
-
tracer.setEnabled(enableTracing);
|
|
11464
11487
|
this.statsReporter?.stop();
|
|
11465
11488
|
this.statsReporter = createStatsReporter({
|
|
11466
11489
|
subscriber: this.subscriber,
|
|
@@ -11468,6 +11491,7 @@ class Call {
|
|
|
11468
11491
|
state: this.state,
|
|
11469
11492
|
datacenter: sfuClient.edgeName,
|
|
11470
11493
|
});
|
|
11494
|
+
this.tracer.setEnabled(enableTracing);
|
|
11471
11495
|
this.sfuStatsReporter?.stop();
|
|
11472
11496
|
if (statsOptions?.reporting_interval_ms > 0) {
|
|
11473
11497
|
this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
|
|
@@ -11479,6 +11503,7 @@ class Call {
|
|
|
11479
11503
|
microphone: this.microphone,
|
|
11480
11504
|
camera: this.camera,
|
|
11481
11505
|
state: this.state,
|
|
11506
|
+
tracer: this.tracer,
|
|
11482
11507
|
unifiedSessionId: this.unifiedSessionId,
|
|
11483
11508
|
});
|
|
11484
11509
|
this.sfuStatsReporter.start();
|
|
@@ -11551,10 +11576,10 @@ class Call {
|
|
|
11551
11576
|
*/
|
|
11552
11577
|
this.reconnect = async (strategy, reason) => {
|
|
11553
11578
|
if (this.state.callingState === CallingState.RECONNECTING ||
|
|
11579
|
+
this.state.callingState === CallingState.MIGRATING ||
|
|
11554
11580
|
this.state.callingState === CallingState.RECONNECTING_FAILED)
|
|
11555
11581
|
return;
|
|
11556
11582
|
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
11557
|
-
this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
|
|
11558
11583
|
const reconnectStartTime = Date.now();
|
|
11559
11584
|
this.reconnectStrategy = strategy;
|
|
11560
11585
|
this.reconnectReason = reason;
|
|
@@ -11574,6 +11599,7 @@ class Call {
|
|
|
11574
11599
|
try {
|
|
11575
11600
|
// wait until the network is available
|
|
11576
11601
|
await this.networkAvailableTask?.promise;
|
|
11602
|
+
this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[this.reconnectStrategy]}`);
|
|
11577
11603
|
switch (this.reconnectStrategy) {
|
|
11578
11604
|
case WebsocketReconnectStrategy.UNSPECIFIED:
|
|
11579
11605
|
case WebsocketReconnectStrategy.DISCONNECT:
|
|
@@ -11613,6 +11639,7 @@ class Call {
|
|
|
11613
11639
|
} while (this.state.callingState !== CallingState.JOINED &&
|
|
11614
11640
|
this.state.callingState !== CallingState.RECONNECTING_FAILED &&
|
|
11615
11641
|
this.state.callingState !== CallingState.LEFT);
|
|
11642
|
+
this.logger('info', '[Reconnect] Reconnection flow finished');
|
|
11616
11643
|
});
|
|
11617
11644
|
};
|
|
11618
11645
|
/**
|
|
@@ -11712,6 +11739,7 @@ class Call {
|
|
|
11712
11739
|
}
|
|
11713
11740
|
});
|
|
11714
11741
|
const unregisterNetworkChanged = this.streamClient.on('network.changed', (e) => {
|
|
11742
|
+
this.tracer.trace('network.changed', e);
|
|
11715
11743
|
if (!e.online) {
|
|
11716
11744
|
this.logger('debug', '[Reconnect] Going offline');
|
|
11717
11745
|
if (!this.hasJoinedOnce)
|
|
@@ -13713,7 +13741,7 @@ class StreamClient {
|
|
|
13713
13741
|
this.getUserAgent = () => {
|
|
13714
13742
|
if (!this.cachedUserAgent) {
|
|
13715
13743
|
const { clientAppIdentifier = {} } = this.options;
|
|
13716
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
13744
|
+
const { sdkName = 'js', sdkVersion = "1.22.1", ...extras } = clientAppIdentifier;
|
|
13717
13745
|
this.cachedUserAgent = [
|
|
13718
13746
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
13719
13747
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|