@stream-io/video-client 1.20.2 → 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 +19 -0
- package/dist/index.browser.es.js +521 -285
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +521 -285
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +521 -285
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +3 -0
- package/dist/src/StreamSfuClient.d.ts +1 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +3 -3
- package/dist/src/devices/InputMediaDeviceManagerState.d.ts +0 -20
- package/dist/src/devices/SpeakerState.d.ts +3 -21
- 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 +50 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +29 -3
- package/dist/src/rpc/createClient.d.ts +1 -1
- package/dist/src/rtc/BasePeerConnection.d.ts +13 -7
- package/dist/src/rtc/Publisher.d.ts +4 -4
- package/dist/src/stats/CallStateStatsReporter.d.ts +5 -3
- package/dist/src/stats/SfuStatsReporter.d.ts +8 -1
- package/dist/src/stats/rtc/StatsTracer.d.ts +54 -0
- package/dist/src/stats/rtc/Tracer.d.ts +1 -5
- package/dist/src/stats/rtc/index.d.ts +2 -0
- package/dist/src/stats/rtc/types.d.ts +19 -0
- package/dist/src/stats/types.d.ts +13 -0
- package/dist/src/stats/utils.d.ts +14 -0
- package/index.ts +0 -4
- package/package.json +10 -10
- package/src/Call.ts +15 -4
- package/src/StreamSfuClient.ts +30 -18
- package/src/devices/CameraManager.ts +27 -23
- package/src/devices/CameraManagerState.ts +3 -2
- package/src/devices/InputMediaDeviceManager.ts +8 -5
- package/src/devices/InputMediaDeviceManagerState.ts +10 -31
- package/src/devices/MicrophoneManager.ts +1 -1
- package/src/devices/MicrophoneManagerState.ts +3 -2
- package/src/devices/ScreenShareManager.ts +2 -2
- package/src/devices/ScreenShareState.ts +5 -4
- package/src/devices/SpeakerManager.ts +2 -1
- package/src/devices/SpeakerState.ts +11 -27
- 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 +84 -0
- package/src/gen/video/sfu/signal_rpc/signal.ts +50 -2
- package/src/helpers/DynascaleManager.ts +0 -9
- package/src/rpc/createClient.ts +1 -1
- package/src/rtc/BasePeerConnection.ts +43 -16
- package/src/rtc/Publisher.ts +18 -16
- package/src/rtc/Subscriber.ts +2 -0
- package/src/rtc/__tests__/Publisher.test.ts +19 -0
- package/src/rtc/__tests__/Subscriber.test.ts +15 -1
- package/src/stats/CallStateStatsReporter.ts +19 -31
- package/src/stats/SfuStatsReporter.ts +45 -16
- package/src/stats/rtc/StatsTracer.ts +289 -0
- package/src/stats/rtc/Tracer.ts +1 -6
- package/src/stats/rtc/__tests__/Tracer.test.ts +57 -0
- package/src/stats/rtc/index.ts +3 -0
- package/src/stats/rtc/pc.ts +6 -76
- package/src/stats/rtc/types.ts +22 -0
- package/src/stats/types.ts +15 -0
- package/src/stats/utils.ts +15 -0
- package/dist/src/stats/rtc/mediaDevices.d.ts +0 -2
- package/src/stats/rtc/mediaDevices.ts +0 -42
package/dist/index.cjs.js
CHANGED
|
@@ -10,74 +10,6 @@ var uaParserJs = require('ua-parser-js');
|
|
|
10
10
|
var sdpTransform = require('sdp-transform');
|
|
11
11
|
var https = require('https');
|
|
12
12
|
|
|
13
|
-
class Tracer {
|
|
14
|
-
constructor(id) {
|
|
15
|
-
this.buffer = [];
|
|
16
|
-
this.enabled = true;
|
|
17
|
-
this.setEnabled = (enabled) => {
|
|
18
|
-
if (this.enabled === enabled)
|
|
19
|
-
return;
|
|
20
|
-
this.enabled = enabled;
|
|
21
|
-
this.buffer = [];
|
|
22
|
-
};
|
|
23
|
-
this.trace = (tag, data) => {
|
|
24
|
-
if (!this.enabled)
|
|
25
|
-
return;
|
|
26
|
-
this.buffer.push([tag, this.id, data, Date.now()]);
|
|
27
|
-
};
|
|
28
|
-
this.take = () => {
|
|
29
|
-
const snapshot = this.buffer;
|
|
30
|
-
this.buffer = [];
|
|
31
|
-
return {
|
|
32
|
-
snapshot,
|
|
33
|
-
rollback: () => {
|
|
34
|
-
this.buffer.unshift(...snapshot);
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
this.dispose = () => {
|
|
39
|
-
this.buffer = [];
|
|
40
|
-
};
|
|
41
|
-
this.id = id;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const tracer = new Tracer(null);
|
|
46
|
-
if (typeof navigator !== 'undefined' &&
|
|
47
|
-
typeof navigator.mediaDevices !== 'undefined') {
|
|
48
|
-
const dumpStream = (stream) => ({
|
|
49
|
-
id: stream.id,
|
|
50
|
-
tracks: stream.getTracks().map((track) => ({
|
|
51
|
-
id: track.id,
|
|
52
|
-
kind: track.kind,
|
|
53
|
-
label: track.label,
|
|
54
|
-
enabled: track.enabled,
|
|
55
|
-
muted: track.muted,
|
|
56
|
-
readyState: track.readyState,
|
|
57
|
-
})),
|
|
58
|
-
});
|
|
59
|
-
const trace = tracer.trace;
|
|
60
|
-
const target = navigator.mediaDevices;
|
|
61
|
-
for (const method of ['getUserMedia', 'getDisplayMedia']) {
|
|
62
|
-
const original = target[method];
|
|
63
|
-
if (!original)
|
|
64
|
-
continue;
|
|
65
|
-
target[method] = async function tracedMethod(constraints) {
|
|
66
|
-
const tag = `navigator.mediaDevices.${method}`;
|
|
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
13
|
/* tslint:disable */
|
|
82
14
|
/**
|
|
83
15
|
* @export
|
|
@@ -1796,6 +1728,47 @@ class AppleState$Type extends runtime.MessageType {
|
|
|
1796
1728
|
* @generated MessageType for protobuf message stream.video.sfu.models.AppleState
|
|
1797
1729
|
*/
|
|
1798
1730
|
const AppleState = new AppleState$Type();
|
|
1731
|
+
// @generated message type with reflection information, may provide speed optimized methods
|
|
1732
|
+
class PerformanceStats$Type extends runtime.MessageType {
|
|
1733
|
+
constructor() {
|
|
1734
|
+
super('stream.video.sfu.models.PerformanceStats', [
|
|
1735
|
+
{
|
|
1736
|
+
no: 1,
|
|
1737
|
+
name: 'track_type',
|
|
1738
|
+
kind: 'enum',
|
|
1739
|
+
T: () => [
|
|
1740
|
+
'stream.video.sfu.models.TrackType',
|
|
1741
|
+
TrackType,
|
|
1742
|
+
'TRACK_TYPE_',
|
|
1743
|
+
],
|
|
1744
|
+
},
|
|
1745
|
+
{ no: 2, name: 'codec', kind: 'message', T: () => Codec },
|
|
1746
|
+
{
|
|
1747
|
+
no: 3,
|
|
1748
|
+
name: 'avg_frame_time_ms',
|
|
1749
|
+
kind: 'scalar',
|
|
1750
|
+
T: 2 /*ScalarType.FLOAT*/,
|
|
1751
|
+
},
|
|
1752
|
+
{ no: 4, name: 'avg_fps', kind: 'scalar', T: 2 /*ScalarType.FLOAT*/ },
|
|
1753
|
+
{
|
|
1754
|
+
no: 5,
|
|
1755
|
+
name: 'video_dimension',
|
|
1756
|
+
kind: 'message',
|
|
1757
|
+
T: () => VideoDimension,
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
no: 6,
|
|
1761
|
+
name: 'target_bitrate',
|
|
1762
|
+
kind: 'scalar',
|
|
1763
|
+
T: 5 /*ScalarType.INT32*/,
|
|
1764
|
+
},
|
|
1765
|
+
]);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* @generated MessageType for protobuf message stream.video.sfu.models.PerformanceStats
|
|
1770
|
+
*/
|
|
1771
|
+
const PerformanceStats = new PerformanceStats$Type();
|
|
1799
1772
|
|
|
1800
1773
|
var models = /*#__PURE__*/Object.freeze({
|
|
1801
1774
|
__proto__: null,
|
|
@@ -1821,6 +1794,7 @@ var models = /*#__PURE__*/Object.freeze({
|
|
|
1821
1794
|
Participant: Participant,
|
|
1822
1795
|
ParticipantCount: ParticipantCount,
|
|
1823
1796
|
get PeerType () { return PeerType; },
|
|
1797
|
+
PerformanceStats: PerformanceStats,
|
|
1824
1798
|
Pin: Pin,
|
|
1825
1799
|
PublishOption: PublishOption,
|
|
1826
1800
|
RTMPIngress: RTMPIngress,
|
|
@@ -2000,6 +1974,27 @@ class SendStatsRequest$Type extends runtime.MessageType {
|
|
|
2000
1974
|
kind: 'scalar',
|
|
2001
1975
|
T: 9 /*ScalarType.STRING*/,
|
|
2002
1976
|
},
|
|
1977
|
+
{ no: 15, name: 'rtc_stats', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
1978
|
+
{
|
|
1979
|
+
no: 16,
|
|
1980
|
+
name: 'encode_stats',
|
|
1981
|
+
kind: 'message',
|
|
1982
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
1983
|
+
T: () => PerformanceStats,
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
no: 17,
|
|
1987
|
+
name: 'decode_stats',
|
|
1988
|
+
kind: 'message',
|
|
1989
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
1990
|
+
T: () => PerformanceStats,
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
no: 18,
|
|
1994
|
+
name: 'unified_session_id',
|
|
1995
|
+
kind: 'scalar',
|
|
1996
|
+
T: 9 /*ScalarType.STRING*/,
|
|
1997
|
+
},
|
|
2003
1998
|
]);
|
|
2004
1999
|
}
|
|
2005
2000
|
}
|
|
@@ -5351,6 +5346,20 @@ const flatten = (report) => {
|
|
|
5351
5346
|
});
|
|
5352
5347
|
return stats;
|
|
5353
5348
|
};
|
|
5349
|
+
/**
|
|
5350
|
+
* Dump the provided MediaStream into a JSON object.
|
|
5351
|
+
*/
|
|
5352
|
+
const dumpStream = (stream) => ({
|
|
5353
|
+
id: stream.id,
|
|
5354
|
+
tracks: stream.getTracks().map((track) => ({
|
|
5355
|
+
id: track.id,
|
|
5356
|
+
kind: track.kind,
|
|
5357
|
+
label: track.label,
|
|
5358
|
+
enabled: track.enabled,
|
|
5359
|
+
muted: track.muted,
|
|
5360
|
+
readyState: track.readyState,
|
|
5361
|
+
})),
|
|
5362
|
+
});
|
|
5354
5363
|
const getSdkSignature = (clientDetails) => {
|
|
5355
5364
|
const { sdk, ...platform } = clientDetails;
|
|
5356
5365
|
const sdkName = getSdkName(sdk);
|
|
@@ -5471,30 +5480,17 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
5471
5480
|
}
|
|
5472
5481
|
}
|
|
5473
5482
|
}
|
|
5474
|
-
const [subscriberStats, publisherStats] = await Promise.all([
|
|
5475
|
-
subscriber
|
|
5476
|
-
.getStats()
|
|
5477
|
-
.then((report) => transform(report, {
|
|
5478
|
-
kind: 'subscriber',
|
|
5479
|
-
trackKind: 'video',
|
|
5480
|
-
publisher,
|
|
5481
|
-
}))
|
|
5482
|
-
.then(aggregate),
|
|
5483
|
-
publisher
|
|
5484
|
-
? publisher
|
|
5485
|
-
.getStats()
|
|
5486
|
-
.then((report) => transform(report, {
|
|
5487
|
-
kind: 'publisher',
|
|
5488
|
-
trackKind: 'video',
|
|
5489
|
-
publisher,
|
|
5490
|
-
}))
|
|
5491
|
-
.then(aggregate)
|
|
5492
|
-
: getEmptyStats(),
|
|
5493
|
-
]);
|
|
5494
5483
|
const [subscriberRawStats, publisherRawStats] = await Promise.all([
|
|
5495
5484
|
getRawStatsForTrack('subscriber'),
|
|
5496
5485
|
publisher ? getRawStatsForTrack('publisher') : undefined,
|
|
5497
5486
|
]);
|
|
5487
|
+
const process = (report, kind) => aggregate(transform(report, { kind, trackKind: 'video', publisher }));
|
|
5488
|
+
const subscriberStats = subscriberRawStats
|
|
5489
|
+
? process(subscriberRawStats, 'subscriber')
|
|
5490
|
+
: getEmptyStats();
|
|
5491
|
+
const publisherStats = publisherRawStats
|
|
5492
|
+
? process(publisherRawStats, 'publisher')
|
|
5493
|
+
: getEmptyStats();
|
|
5498
5494
|
state.setCallStatsReport({
|
|
5499
5495
|
datacenter,
|
|
5500
5496
|
publisherStats,
|
|
@@ -5652,7 +5648,7 @@ const aggregate = (stats) => {
|
|
|
5652
5648
|
return report;
|
|
5653
5649
|
};
|
|
5654
5650
|
|
|
5655
|
-
const version = "1.
|
|
5651
|
+
const version = "1.22.0";
|
|
5656
5652
|
const [major, minor, patch] = version.split('.');
|
|
5657
5653
|
let sdkInfo = {
|
|
5658
5654
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5789,7 +5785,7 @@ const getClientDetails = async () => {
|
|
|
5789
5785
|
};
|
|
5790
5786
|
|
|
5791
5787
|
class SfuStatsReporter {
|
|
5792
|
-
constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, }) {
|
|
5788
|
+
constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, tracer, unifiedSessionId, }) {
|
|
5793
5789
|
this.logger = getLogger(['SfuStatsReporter']);
|
|
5794
5790
|
this.inputDevices = new Map();
|
|
5795
5791
|
this.observeDevice = (device, kind) => {
|
|
@@ -5846,31 +5842,40 @@ class SfuStatsReporter {
|
|
|
5846
5842
|
};
|
|
5847
5843
|
this.run = async (telemetry) => {
|
|
5848
5844
|
const [subscriberStats, publisherStats] = await Promise.all([
|
|
5849
|
-
this.subscriber.
|
|
5850
|
-
this.publisher?.
|
|
5845
|
+
this.subscriber.stats.get(),
|
|
5846
|
+
this.publisher?.stats.get(),
|
|
5851
5847
|
]);
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5848
|
+
this.subscriber.tracer?.trace('getstats', subscriberStats.delta);
|
|
5849
|
+
if (publisherStats) {
|
|
5850
|
+
this.publisher?.tracer?.trace('getstats', publisherStats.delta);
|
|
5851
|
+
}
|
|
5852
|
+
const subscriberTrace = this.subscriber.tracer?.take();
|
|
5853
|
+
const publisherTrace = this.publisher?.tracer?.take();
|
|
5854
|
+
const tracer = this.tracer.take();
|
|
5855
5855
|
const sfuTrace = this.sfuClient.getTrace();
|
|
5856
|
-
const
|
|
5857
|
-
...
|
|
5856
|
+
const traces = [
|
|
5857
|
+
...tracer.snapshot,
|
|
5858
5858
|
...(sfuTrace?.snapshot ?? []),
|
|
5859
5859
|
...(publisherTrace?.snapshot ?? []),
|
|
5860
|
+
...(subscriberTrace?.snapshot ?? []),
|
|
5860
5861
|
];
|
|
5861
5862
|
try {
|
|
5862
5863
|
await this.sfuClient.sendStats({
|
|
5863
5864
|
sdk: this.sdkName,
|
|
5864
5865
|
sdkVersion: this.sdkVersion,
|
|
5865
5866
|
webrtcVersion: this.webRTCVersion,
|
|
5866
|
-
subscriberStats,
|
|
5867
|
-
|
|
5868
|
-
? JSON.stringify(
|
|
5869
|
-
: '',
|
|
5870
|
-
|
|
5871
|
-
publisherRtcStats:
|
|
5867
|
+
subscriberStats: JSON.stringify(flatten(subscriberStats.stats)),
|
|
5868
|
+
publisherStats: publisherStats
|
|
5869
|
+
? JSON.stringify(flatten(publisherStats.stats))
|
|
5870
|
+
: '[]',
|
|
5871
|
+
subscriberRtcStats: '',
|
|
5872
|
+
publisherRtcStats: '',
|
|
5873
|
+
rtcStats: JSON.stringify(traces),
|
|
5874
|
+
encodeStats: publisherStats?.performanceStats ?? [],
|
|
5875
|
+
decodeStats: subscriberStats.performanceStats,
|
|
5872
5876
|
audioDevices: this.inputDevices.get('mic'),
|
|
5873
5877
|
videoDevices: this.inputDevices.get('camera'),
|
|
5878
|
+
unifiedSessionId: this.unifiedSessionId,
|
|
5874
5879
|
deviceState: getDeviceState(),
|
|
5875
5880
|
telemetry,
|
|
5876
5881
|
});
|
|
@@ -5878,7 +5883,7 @@ class SfuStatsReporter {
|
|
|
5878
5883
|
catch (err) {
|
|
5879
5884
|
publisherTrace?.rollback();
|
|
5880
5885
|
subscriberTrace?.rollback();
|
|
5881
|
-
|
|
5886
|
+
tracer.rollback();
|
|
5882
5887
|
sfuTrace?.rollback();
|
|
5883
5888
|
throw err;
|
|
5884
5889
|
}
|
|
@@ -5903,6 +5908,16 @@ class SfuStatsReporter {
|
|
|
5903
5908
|
this.inputDevices.clear();
|
|
5904
5909
|
clearInterval(this.intervalId);
|
|
5905
5910
|
this.intervalId = undefined;
|
|
5911
|
+
clearTimeout(this.timeoutId);
|
|
5912
|
+
this.timeoutId = undefined;
|
|
5913
|
+
};
|
|
5914
|
+
this.scheduleOne = (timeout) => {
|
|
5915
|
+
clearTimeout(this.timeoutId);
|
|
5916
|
+
this.timeoutId = setTimeout(() => {
|
|
5917
|
+
this.run().catch((err) => {
|
|
5918
|
+
this.logger('warn', 'Failed to report stats', err);
|
|
5919
|
+
});
|
|
5920
|
+
}, timeout);
|
|
5906
5921
|
};
|
|
5907
5922
|
this.sfuClient = sfuClient;
|
|
5908
5923
|
this.options = options;
|
|
@@ -5911,6 +5926,8 @@ class SfuStatsReporter {
|
|
|
5911
5926
|
this.microphone = microphone;
|
|
5912
5927
|
this.camera = camera;
|
|
5913
5928
|
this.state = state;
|
|
5929
|
+
this.tracer = tracer;
|
|
5930
|
+
this.unifiedSessionId = unifiedSessionId;
|
|
5914
5931
|
const { sdk, browser } = clientDetails;
|
|
5915
5932
|
this.sdkName = getSdkName(sdk);
|
|
5916
5933
|
this.sdkVersion = getSdkVersion(sdk);
|
|
@@ -5933,47 +5950,25 @@ const traceRTCPeerConnection = (pc, trace) => {
|
|
|
5933
5950
|
trace('ontrack', `${e.track.kind}:${e.track.id} ${streams}`);
|
|
5934
5951
|
});
|
|
5935
5952
|
pc.addEventListener('signalingstatechange', () => {
|
|
5936
|
-
trace('
|
|
5953
|
+
trace('signalingstatechange', pc.signalingState);
|
|
5937
5954
|
});
|
|
5938
5955
|
pc.addEventListener('iceconnectionstatechange', () => {
|
|
5939
|
-
trace('
|
|
5956
|
+
trace('iceconnectionstatechange', pc.iceConnectionState);
|
|
5940
5957
|
});
|
|
5941
5958
|
pc.addEventListener('icegatheringstatechange', () => {
|
|
5942
|
-
trace('
|
|
5959
|
+
trace('icegatheringstatechange', pc.iceGatheringState);
|
|
5943
5960
|
});
|
|
5944
5961
|
pc.addEventListener('connectionstatechange', () => {
|
|
5945
|
-
trace('
|
|
5962
|
+
trace('connectionstatechange', pc.connectionState);
|
|
5946
5963
|
});
|
|
5947
5964
|
pc.addEventListener('negotiationneeded', () => {
|
|
5948
|
-
trace('
|
|
5965
|
+
trace('negotiationneeded', undefined);
|
|
5949
5966
|
});
|
|
5950
5967
|
pc.addEventListener('datachannel', ({ channel }) => {
|
|
5951
|
-
trace('
|
|
5952
|
-
});
|
|
5953
|
-
let prev = {};
|
|
5954
|
-
const getStats = () => {
|
|
5955
|
-
pc.getStats(null)
|
|
5956
|
-
.then((stats) => {
|
|
5957
|
-
const now = toObject(stats);
|
|
5958
|
-
trace('getstats', deltaCompression(prev, now));
|
|
5959
|
-
prev = now;
|
|
5960
|
-
})
|
|
5961
|
-
.catch((err) => {
|
|
5962
|
-
trace('getstatsOnFailure', err.toString());
|
|
5963
|
-
});
|
|
5964
|
-
};
|
|
5965
|
-
const interval = setInterval(() => {
|
|
5966
|
-
getStats();
|
|
5967
|
-
}, 8000);
|
|
5968
|
-
pc.addEventListener('connectionstatechange', () => {
|
|
5969
|
-
const state = pc.connectionState;
|
|
5970
|
-
if (state === 'connected' || state === 'failed') {
|
|
5971
|
-
getStats();
|
|
5972
|
-
}
|
|
5968
|
+
trace('datachannel', [channel.id, channel.label]);
|
|
5973
5969
|
});
|
|
5974
5970
|
const origClose = pc.close;
|
|
5975
5971
|
pc.close = function tracedClose() {
|
|
5976
|
-
clearInterval(interval);
|
|
5977
5972
|
trace('close', undefined);
|
|
5978
5973
|
return origClose.call(this);
|
|
5979
5974
|
};
|
|
@@ -6003,9 +5998,162 @@ const traceRTCPeerConnection = (pc, trace) => {
|
|
|
6003
5998
|
};
|
|
6004
5999
|
}
|
|
6005
6000
|
};
|
|
6006
|
-
|
|
6001
|
+
|
|
6002
|
+
/**
|
|
6003
|
+
* StatsTracer is a class that collects and processes WebRTC stats.
|
|
6004
|
+
* It is used to track the performance of the WebRTC connection
|
|
6005
|
+
* and to provide information about the media streams.
|
|
6006
|
+
* It is used by both the Publisher and Subscriber classes.
|
|
6007
|
+
*
|
|
6008
|
+
* @internal
|
|
6009
|
+
*/
|
|
6010
|
+
class StatsTracer {
|
|
6011
|
+
/**
|
|
6012
|
+
* Creates a new StatsTracer instance.
|
|
6013
|
+
*/
|
|
6014
|
+
constructor(pc, peerType, trackIdToTrackType) {
|
|
6015
|
+
this.previousStats = {};
|
|
6016
|
+
this.frameTimeHistory = [];
|
|
6017
|
+
this.fpsHistory = [];
|
|
6018
|
+
/**
|
|
6019
|
+
* Get the stats from the RTCPeerConnection.
|
|
6020
|
+
* When called, it will return the stats for the current connection.
|
|
6021
|
+
* It will also return the delta between the current stats and the previous stats.
|
|
6022
|
+
* This is used to track the performance of the connection.
|
|
6023
|
+
*
|
|
6024
|
+
* @internal
|
|
6025
|
+
*/
|
|
6026
|
+
this.get = async () => {
|
|
6027
|
+
const stats = await this.pc.getStats();
|
|
6028
|
+
const currentStats = toObject(stats);
|
|
6029
|
+
const performanceStats = this.withOverrides(this.peerType === PeerType.SUBSCRIBER
|
|
6030
|
+
? this.getDecodeStats(currentStats)
|
|
6031
|
+
: this.getEncodeStats(currentStats));
|
|
6032
|
+
const delta = deltaCompression(this.previousStats, currentStats);
|
|
6033
|
+
// store the current data for the next iteration
|
|
6034
|
+
this.previousStats = currentStats;
|
|
6035
|
+
this.frameTimeHistory = this.frameTimeHistory.slice(-2);
|
|
6036
|
+
this.fpsHistory = this.fpsHistory.slice(-2);
|
|
6037
|
+
return { performanceStats, delta, stats };
|
|
6038
|
+
};
|
|
6039
|
+
/**
|
|
6040
|
+
* Collects encode stats from the RTCPeerConnection.
|
|
6041
|
+
*/
|
|
6042
|
+
this.getEncodeStats = (currentStats) => {
|
|
6043
|
+
const encodeStats = [];
|
|
6044
|
+
for (const rtp of Object.values(currentStats)) {
|
|
6045
|
+
if (rtp.type !== 'outbound-rtp')
|
|
6046
|
+
continue;
|
|
6047
|
+
const { codecId, framesSent = 0, kind, id, totalEncodeTime = 0, framesPerSecond = 0, frameHeight = 0, frameWidth = 0, targetBitrate = 0, mediaSourceId, } = rtp;
|
|
6048
|
+
if (kind === 'audio' || !this.previousStats[id])
|
|
6049
|
+
continue;
|
|
6050
|
+
const prevRtp = this.previousStats[id];
|
|
6051
|
+
const deltaTotalEncodeTime = totalEncodeTime - (prevRtp.totalEncodeTime || 0);
|
|
6052
|
+
const deltaFramesSent = framesSent - (prevRtp.framesSent || 0);
|
|
6053
|
+
const framesEncodeTime = deltaFramesSent > 0
|
|
6054
|
+
? (deltaTotalEncodeTime / deltaFramesSent) * 1000
|
|
6055
|
+
: 0;
|
|
6056
|
+
this.frameTimeHistory.push(framesEncodeTime);
|
|
6057
|
+
this.fpsHistory.push(framesPerSecond);
|
|
6058
|
+
let trackType = TrackType.VIDEO;
|
|
6059
|
+
if (mediaSourceId && currentStats[mediaSourceId]) {
|
|
6060
|
+
const mediaSource = currentStats[mediaSourceId];
|
|
6061
|
+
trackType =
|
|
6062
|
+
this.trackIdToTrackType.get(mediaSource.trackIdentifier) || trackType;
|
|
6063
|
+
}
|
|
6064
|
+
encodeStats.push({
|
|
6065
|
+
trackType,
|
|
6066
|
+
codec: getCodecFromStats(currentStats, codecId),
|
|
6067
|
+
avgFrameTimeMs: average(this.frameTimeHistory),
|
|
6068
|
+
avgFps: average(this.fpsHistory),
|
|
6069
|
+
targetBitrate: Math.round(targetBitrate),
|
|
6070
|
+
videoDimension: { width: frameWidth, height: frameHeight },
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
6073
|
+
return encodeStats;
|
|
6074
|
+
};
|
|
6075
|
+
/**
|
|
6076
|
+
* Collects decode stats from the RTCPeerConnection.
|
|
6077
|
+
*/
|
|
6078
|
+
this.getDecodeStats = (currentStats) => {
|
|
6079
|
+
let rtp = undefined;
|
|
6080
|
+
let max = 0;
|
|
6081
|
+
for (const item of Object.values(currentStats)) {
|
|
6082
|
+
if (item.type !== 'inbound-rtp')
|
|
6083
|
+
continue;
|
|
6084
|
+
const rtpItem = item;
|
|
6085
|
+
const { kind, frameWidth = 0, frameHeight = 0 } = rtpItem;
|
|
6086
|
+
const area = frameWidth * frameHeight;
|
|
6087
|
+
if (kind === 'video' && area > max) {
|
|
6088
|
+
rtp = rtpItem;
|
|
6089
|
+
max = area;
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
if (!rtp || !this.previousStats[rtp.id])
|
|
6093
|
+
return [];
|
|
6094
|
+
const prevRtp = this.previousStats[rtp.id];
|
|
6095
|
+
const { framesDecoded = 0, framesPerSecond = 0, totalDecodeTime = 0, trackIdentifier, } = rtp;
|
|
6096
|
+
const deltaTotalDecodeTime = totalDecodeTime - (prevRtp.totalDecodeTime || 0);
|
|
6097
|
+
const deltaFramesDecoded = framesDecoded - (prevRtp.framesDecoded || 0);
|
|
6098
|
+
const framesDecodeTime = deltaFramesDecoded > 0
|
|
6099
|
+
? (deltaTotalDecodeTime / deltaFramesDecoded) * 1000
|
|
6100
|
+
: 0;
|
|
6101
|
+
this.frameTimeHistory.push(framesDecodeTime);
|
|
6102
|
+
this.fpsHistory.push(framesPerSecond);
|
|
6103
|
+
const trackType = this.trackIdToTrackType.get(trackIdentifier) || TrackType.VIDEO;
|
|
6104
|
+
return [
|
|
6105
|
+
PerformanceStats.create({
|
|
6106
|
+
trackType,
|
|
6107
|
+
codec: getCodecFromStats(currentStats, rtp.codecId),
|
|
6108
|
+
avgFrameTimeMs: average(this.frameTimeHistory),
|
|
6109
|
+
avgFps: average(this.fpsHistory),
|
|
6110
|
+
videoDimension: { width: rtp.frameWidth, height: rtp.frameHeight },
|
|
6111
|
+
}),
|
|
6112
|
+
];
|
|
6113
|
+
};
|
|
6114
|
+
/**
|
|
6115
|
+
* Applies cost overrides to the performance stats.
|
|
6116
|
+
* This is used to override the default encode/decode times with custom values.
|
|
6117
|
+
* This is useful for testing and debugging purposes, and it shouldn't be used in production.
|
|
6118
|
+
*/
|
|
6119
|
+
this.withOverrides = (performanceStats) => {
|
|
6120
|
+
if (this.costOverrides) {
|
|
6121
|
+
for (const s of performanceStats) {
|
|
6122
|
+
const override = this.costOverrides.get(s.trackType);
|
|
6123
|
+
if (override !== undefined) {
|
|
6124
|
+
// override the average encode/decode time with the provided cost.
|
|
6125
|
+
// format: [override].[original-frame-time]
|
|
6126
|
+
s.avgFrameTimeMs = override + (s.avgFrameTimeMs || 0) / 1000;
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
return performanceStats;
|
|
6131
|
+
};
|
|
6132
|
+
/**
|
|
6133
|
+
* Set the encode/decode cost for a specific track type.
|
|
6134
|
+
* This is used to override the default encode/decode times with custom values.
|
|
6135
|
+
* This is useful for testing and debugging purposes, and it shouldn't be used in production.
|
|
6136
|
+
*
|
|
6137
|
+
* @internal
|
|
6138
|
+
*/
|
|
6139
|
+
this.setCost = (cost, trackType = TrackType.VIDEO) => {
|
|
6140
|
+
if (!this.costOverrides)
|
|
6141
|
+
this.costOverrides = new Map();
|
|
6142
|
+
this.costOverrides.set(trackType, cost);
|
|
6143
|
+
};
|
|
6144
|
+
this.pc = pc;
|
|
6145
|
+
this.peerType = peerType;
|
|
6146
|
+
this.trackIdToTrackType = trackIdToTrackType;
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
/**
|
|
6150
|
+
* Convert the stat report to an object.
|
|
6151
|
+
*
|
|
6152
|
+
* @param report the stat report to convert.
|
|
6153
|
+
*/
|
|
6154
|
+
const toObject = (report) => {
|
|
6007
6155
|
const obj = {};
|
|
6008
|
-
|
|
6156
|
+
report.forEach((v, k) => {
|
|
6009
6157
|
obj[k] = v;
|
|
6010
6158
|
});
|
|
6011
6159
|
return obj;
|
|
@@ -6028,12 +6176,13 @@ const deltaCompression = (oldStats, newStats) => {
|
|
|
6028
6176
|
}
|
|
6029
6177
|
}
|
|
6030
6178
|
let timestamp = -Infinity;
|
|
6031
|
-
|
|
6179
|
+
const values = Object.values(newStats);
|
|
6180
|
+
for (const report of values) {
|
|
6032
6181
|
if (report.timestamp > timestamp) {
|
|
6033
6182
|
timestamp = report.timestamp;
|
|
6034
6183
|
}
|
|
6035
6184
|
}
|
|
6036
|
-
for (const report of
|
|
6185
|
+
for (const report of values) {
|
|
6037
6186
|
if (report.timestamp === timestamp) {
|
|
6038
6187
|
report.timestamp = 0;
|
|
6039
6188
|
}
|
|
@@ -6041,6 +6190,59 @@ const deltaCompression = (oldStats, newStats) => {
|
|
|
6041
6190
|
newStats.timestamp = timestamp;
|
|
6042
6191
|
return newStats;
|
|
6043
6192
|
};
|
|
6193
|
+
/**
|
|
6194
|
+
* Calculates an average value.
|
|
6195
|
+
*/
|
|
6196
|
+
const average = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
6197
|
+
/**
|
|
6198
|
+
* Create a Codec object from the codec stats.
|
|
6199
|
+
*
|
|
6200
|
+
* @param stats the stats report.
|
|
6201
|
+
* @param codecId the codec ID to look for.
|
|
6202
|
+
*/
|
|
6203
|
+
const getCodecFromStats = (stats, codecId) => {
|
|
6204
|
+
if (!codecId || !stats[codecId])
|
|
6205
|
+
return;
|
|
6206
|
+
const codecStats = stats[codecId];
|
|
6207
|
+
return Codec.create({
|
|
6208
|
+
name: codecStats.mimeType.split('/').pop(), // video/av1 -> av1
|
|
6209
|
+
clockRate: codecStats.clockRate,
|
|
6210
|
+
payloadType: codecStats.payloadType,
|
|
6211
|
+
fmtp: codecStats.sdpFmtpLine,
|
|
6212
|
+
});
|
|
6213
|
+
};
|
|
6214
|
+
|
|
6215
|
+
class Tracer {
|
|
6216
|
+
constructor(id) {
|
|
6217
|
+
this.buffer = [];
|
|
6218
|
+
this.enabled = true;
|
|
6219
|
+
this.setEnabled = (enabled) => {
|
|
6220
|
+
if (this.enabled === enabled)
|
|
6221
|
+
return;
|
|
6222
|
+
this.enabled = enabled;
|
|
6223
|
+
this.buffer = [];
|
|
6224
|
+
};
|
|
6225
|
+
this.trace = (tag, data) => {
|
|
6226
|
+
if (!this.enabled)
|
|
6227
|
+
return;
|
|
6228
|
+
this.buffer.push([tag, this.id, data, Date.now()]);
|
|
6229
|
+
};
|
|
6230
|
+
this.take = () => {
|
|
6231
|
+
const snapshot = this.buffer;
|
|
6232
|
+
this.buffer = [];
|
|
6233
|
+
return {
|
|
6234
|
+
snapshot,
|
|
6235
|
+
rollback: () => {
|
|
6236
|
+
this.buffer.unshift(...snapshot);
|
|
6237
|
+
},
|
|
6238
|
+
};
|
|
6239
|
+
};
|
|
6240
|
+
this.dispose = () => {
|
|
6241
|
+
this.buffer = [];
|
|
6242
|
+
};
|
|
6243
|
+
this.id = id;
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6044
6246
|
|
|
6045
6247
|
/**
|
|
6046
6248
|
* A base class for the `Publisher` and `Subscriber` classes.
|
|
@@ -6050,17 +6252,20 @@ class BasePeerConnection {
|
|
|
6050
6252
|
/**
|
|
6051
6253
|
* Constructs a new `BasePeerConnection` instance.
|
|
6052
6254
|
*/
|
|
6053
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag,
|
|
6255
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, enableTracing, }) {
|
|
6054
6256
|
this.isIceRestarting = false;
|
|
6055
6257
|
this.isDisposed = false;
|
|
6258
|
+
this.trackIdToTrackType = new Map();
|
|
6056
6259
|
this.subscriptions = [];
|
|
6260
|
+
this.lock = Math.random().toString(36).slice(2);
|
|
6057
6261
|
/**
|
|
6058
6262
|
* Handles events synchronously.
|
|
6059
6263
|
* Consecutive events are queued and executed one after the other.
|
|
6060
6264
|
*/
|
|
6061
6265
|
this.on = (event, fn) => {
|
|
6062
6266
|
this.subscriptions.push(this.dispatcher.on(event, (e) => {
|
|
6063
|
-
|
|
6267
|
+
const lockKey = `pc.${this.lock}.${event}`;
|
|
6268
|
+
withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
|
|
6064
6269
|
if (this.isDisposed)
|
|
6065
6270
|
return;
|
|
6066
6271
|
this.logger('warn', `Error handling ${event}`, err);
|
|
@@ -6100,10 +6305,10 @@ class BasePeerConnection {
|
|
|
6100
6305
|
return this.pc.getStats(selector);
|
|
6101
6306
|
};
|
|
6102
6307
|
/**
|
|
6103
|
-
*
|
|
6308
|
+
* Maps the given track ID to the corresponding track type.
|
|
6104
6309
|
*/
|
|
6105
|
-
this.
|
|
6106
|
-
return this.
|
|
6310
|
+
this.getTrackType = (trackId) => {
|
|
6311
|
+
return this.trackIdToTrackType.get(trackId);
|
|
6107
6312
|
};
|
|
6108
6313
|
/**
|
|
6109
6314
|
* Handles the ICECandidate event and
|
|
@@ -6137,6 +6342,24 @@ class BasePeerConnection {
|
|
|
6137
6342
|
}
|
|
6138
6343
|
return JSON.stringify(candidate.toJSON());
|
|
6139
6344
|
};
|
|
6345
|
+
/**
|
|
6346
|
+
* Handles the ConnectionStateChange event.
|
|
6347
|
+
*/
|
|
6348
|
+
this.onConnectionStateChange = async () => {
|
|
6349
|
+
const state = this.pc.connectionState;
|
|
6350
|
+
this.logger('debug', `Connection state changed`, state);
|
|
6351
|
+
if (!this.tracer)
|
|
6352
|
+
return;
|
|
6353
|
+
if (state === 'connected' || state === 'failed') {
|
|
6354
|
+
try {
|
|
6355
|
+
const stats = await this.stats.get();
|
|
6356
|
+
this.tracer.trace('getstats', stats.delta);
|
|
6357
|
+
}
|
|
6358
|
+
catch (err) {
|
|
6359
|
+
this.tracer.trace('getstatsOnFailure', err.toString());
|
|
6360
|
+
}
|
|
6361
|
+
}
|
|
6362
|
+
};
|
|
6140
6363
|
/**
|
|
6141
6364
|
* Handles the ICE connection state change event.
|
|
6142
6365
|
*/
|
|
@@ -6193,18 +6416,22 @@ class BasePeerConnection {
|
|
|
6193
6416
|
logTag,
|
|
6194
6417
|
]);
|
|
6195
6418
|
this.pc = new RTCPeerConnection(connectionConfig);
|
|
6196
|
-
if (enableTracing) {
|
|
6197
|
-
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
|
|
6198
|
-
this.tracer = new Tracer(tag);
|
|
6199
|
-
this.tracer.trace('clientDetails', clientDetails);
|
|
6200
|
-
this.tracer.trace('create', connectionConfig);
|
|
6201
|
-
traceRTCPeerConnection(this.pc, this.tracer.trace);
|
|
6202
|
-
}
|
|
6203
6419
|
this.pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
6204
6420
|
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6205
6421
|
this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6206
6422
|
this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
6207
6423
|
this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
6424
|
+
this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
6425
|
+
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
|
|
6426
|
+
if (enableTracing) {
|
|
6427
|
+
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
|
|
6428
|
+
this.tracer = new Tracer(tag);
|
|
6429
|
+
this.tracer.trace('create', {
|
|
6430
|
+
url: sfuClient.edgeName,
|
|
6431
|
+
...connectionConfig,
|
|
6432
|
+
});
|
|
6433
|
+
traceRTCPeerConnection(this.pc, this.tracer.trace);
|
|
6434
|
+
}
|
|
6208
6435
|
}
|
|
6209
6436
|
/**
|
|
6210
6437
|
* Disposes the `RTCPeerConnection` instance.
|
|
@@ -6570,7 +6797,7 @@ class Publisher extends BasePeerConnection {
|
|
|
6570
6797
|
}
|
|
6571
6798
|
else {
|
|
6572
6799
|
const previousTrack = transceiver.sender.track;
|
|
6573
|
-
await
|
|
6800
|
+
await this.updateTransceiver(transceiver, trackToPublish, trackType);
|
|
6574
6801
|
if (!isReactNative()) {
|
|
6575
6802
|
this.stopTrack(previousTrack);
|
|
6576
6803
|
}
|
|
@@ -6592,8 +6819,20 @@ class Publisher extends BasePeerConnection {
|
|
|
6592
6819
|
const trackType = publishOption.trackType;
|
|
6593
6820
|
this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
6594
6821
|
this.transceiverCache.add(publishOption, transceiver);
|
|
6822
|
+
this.trackIdToTrackType.set(track.id, trackType);
|
|
6595
6823
|
await this.negotiate();
|
|
6596
6824
|
};
|
|
6825
|
+
/**
|
|
6826
|
+
* Updates the transceiver with the given track and track type.
|
|
6827
|
+
*/
|
|
6828
|
+
this.updateTransceiver = async (transceiver, track, trackType) => {
|
|
6829
|
+
const sender = transceiver.sender;
|
|
6830
|
+
if (sender.track)
|
|
6831
|
+
this.trackIdToTrackType.delete(sender.track.id);
|
|
6832
|
+
await sender.replaceTrack(track);
|
|
6833
|
+
if (track)
|
|
6834
|
+
this.trackIdToTrackType.set(track.id, trackType);
|
|
6835
|
+
};
|
|
6597
6836
|
/**
|
|
6598
6837
|
* Synchronizes the current Publisher state with the provided publish options.
|
|
6599
6838
|
*/
|
|
@@ -6623,7 +6862,7 @@ class Publisher extends BasePeerConnection {
|
|
|
6623
6862
|
continue;
|
|
6624
6863
|
// it is safe to stop the track here, it is a clone
|
|
6625
6864
|
this.stopTrack(transceiver.sender.track);
|
|
6626
|
-
await
|
|
6865
|
+
await this.updateTransceiver(transceiver, null, publishOption.trackType);
|
|
6627
6866
|
}
|
|
6628
6867
|
};
|
|
6629
6868
|
/**
|
|
@@ -6643,18 +6882,6 @@ class Publisher extends BasePeerConnection {
|
|
|
6643
6882
|
}
|
|
6644
6883
|
return false;
|
|
6645
6884
|
};
|
|
6646
|
-
/**
|
|
6647
|
-
* Maps the given track ID to the corresponding track type.
|
|
6648
|
-
*/
|
|
6649
|
-
this.getTrackType = (trackId) => {
|
|
6650
|
-
for (const transceiverId of this.transceiverCache.items()) {
|
|
6651
|
-
const { publishOption, transceiver } = transceiverId;
|
|
6652
|
-
if (transceiver.sender.track?.id === trackId) {
|
|
6653
|
-
return publishOption.trackType;
|
|
6654
|
-
}
|
|
6655
|
-
}
|
|
6656
|
-
return undefined;
|
|
6657
|
-
};
|
|
6658
6885
|
/**
|
|
6659
6886
|
* Stops the cloned track that is being published to the SFU.
|
|
6660
6887
|
*/
|
|
@@ -6757,7 +6984,7 @@ class Publisher extends BasePeerConnection {
|
|
|
6757
6984
|
* @param options the optional offer options to use.
|
|
6758
6985
|
*/
|
|
6759
6986
|
this.negotiate = async (options) => {
|
|
6760
|
-
return withoutConcurrency(
|
|
6987
|
+
return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
|
|
6761
6988
|
const offer = await this.pc.createOffer(options);
|
|
6762
6989
|
const tracks = this.getAnnouncedTracks(offer.sdp);
|
|
6763
6990
|
if (!tracks.length)
|
|
@@ -6948,6 +7175,7 @@ class Subscriber extends BasePeerConnection {
|
|
|
6948
7175
|
if (!trackType) {
|
|
6949
7176
|
return this.logger('error', `Unknown track type: ${rawTrackType}`);
|
|
6950
7177
|
}
|
|
7178
|
+
this.trackIdToTrackType.set(e.track.id, trackType);
|
|
6951
7179
|
if (!participantToUpdate) {
|
|
6952
7180
|
this.logger('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
6953
7181
|
this.state.registerOrphanedTrack({
|
|
@@ -7291,12 +7519,22 @@ class StreamSfuClient {
|
|
|
7291
7519
|
*/
|
|
7292
7520
|
this.abortController = new AbortController();
|
|
7293
7521
|
this.createWebSocket = () => {
|
|
7522
|
+
const eventsToTrace = {
|
|
7523
|
+
callEnded: true,
|
|
7524
|
+
changePublishQuality: true,
|
|
7525
|
+
error: true,
|
|
7526
|
+
goAway: true,
|
|
7527
|
+
};
|
|
7294
7528
|
this.signalWs = createWebSocketSignalChannel({
|
|
7295
7529
|
logTag: this.logTag,
|
|
7296
7530
|
endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
|
|
7297
7531
|
onMessage: (message) => {
|
|
7298
7532
|
this.lastMessageTimestamp = new Date();
|
|
7299
7533
|
this.scheduleConnectionCheck();
|
|
7534
|
+
const eventKind = message.eventPayload.oneofKind;
|
|
7535
|
+
if (eventsToTrace[eventKind]) {
|
|
7536
|
+
this.tracer?.trace(eventKind, message);
|
|
7537
|
+
}
|
|
7300
7538
|
this.dispatcher.dispatch(message, this.logTag);
|
|
7301
7539
|
},
|
|
7302
7540
|
});
|
|
@@ -7389,7 +7627,8 @@ class StreamSfuClient {
|
|
|
7389
7627
|
};
|
|
7390
7628
|
this.sendStats = async (stats) => {
|
|
7391
7629
|
await this.joinTask;
|
|
7392
|
-
|
|
7630
|
+
// NOTE: we don't retry sending stats
|
|
7631
|
+
return this.rpc.sendStats({ ...stats, sessionId: this.sessionId });
|
|
7393
7632
|
};
|
|
7394
7633
|
this.startNoiseCancellation = async () => {
|
|
7395
7634
|
await this.joinTask;
|
|
@@ -7441,7 +7680,7 @@ class StreamSfuClient {
|
|
|
7441
7680
|
unsubscribe();
|
|
7442
7681
|
current.reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
7443
7682
|
}, this.joinResponseTimeout);
|
|
7444
|
-
|
|
7683
|
+
const joinRequest = SfuRequest.create({
|
|
7445
7684
|
requestPayload: {
|
|
7446
7685
|
oneofKind: 'joinRequest',
|
|
7447
7686
|
joinRequest: JoinRequest.create({
|
|
@@ -7450,7 +7689,9 @@ class StreamSfuClient {
|
|
|
7450
7689
|
token: this.credentials.token,
|
|
7451
7690
|
}),
|
|
7452
7691
|
},
|
|
7453
|
-
})
|
|
7692
|
+
});
|
|
7693
|
+
this.tracer?.trace('joinRequest', joinRequest);
|
|
7694
|
+
await this.send(joinRequest);
|
|
7454
7695
|
return current.promise;
|
|
7455
7696
|
};
|
|
7456
7697
|
this.ping = async () => {
|
|
@@ -7511,7 +7752,9 @@ class StreamSfuClient {
|
|
|
7511
7752
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
7512
7753
|
this.logTag = logTag;
|
|
7513
7754
|
this.logger = getLogger(['SfuClient', logTag]);
|
|
7514
|
-
this.tracer = enableTracing
|
|
7755
|
+
this.tracer = enableTracing
|
|
7756
|
+
? new Tracer(`${logTag}-${this.edgeName}`)
|
|
7757
|
+
: undefined;
|
|
7515
7758
|
this.rpc = createSignalClient({
|
|
7516
7759
|
baseUrl: server.url,
|
|
7517
7760
|
interceptors: [
|
|
@@ -7649,6 +7892,13 @@ const watchSfuCallEnded = (call) => {
|
|
|
7649
7892
|
if (call.state.callingState === exports.CallingState.LEFT)
|
|
7650
7893
|
return;
|
|
7651
7894
|
try {
|
|
7895
|
+
if (e.reason === CallEndedReason.LIVE_ENDED) {
|
|
7896
|
+
call.state.setBackstage(true);
|
|
7897
|
+
// don't leave the call if the user has permission to join backstage
|
|
7898
|
+
const { hasPermission } = call.permissionsContext;
|
|
7899
|
+
if (hasPermission(OwnCapability.JOIN_BACKSTAGE))
|
|
7900
|
+
return;
|
|
7901
|
+
}
|
|
7652
7902
|
// `call.ended` event arrived after the call is already left
|
|
7653
7903
|
// and all event handlers are detached. We need to manually
|
|
7654
7904
|
// update the call state to reflect the call has ended.
|
|
@@ -8411,7 +8661,6 @@ class DynascaleManager {
|
|
|
8411
8661
|
const { selectedDevice } = this.speaker.state;
|
|
8412
8662
|
if (selectedDevice && 'setSinkId' in audioElement) {
|
|
8413
8663
|
audioElement.setSinkId(selectedDevice);
|
|
8414
|
-
tracer.trace('navigator.mediaDevices.setSinkId', selectedDevice);
|
|
8415
8664
|
}
|
|
8416
8665
|
}
|
|
8417
8666
|
});
|
|
@@ -8421,7 +8670,6 @@ class DynascaleManager {
|
|
|
8421
8670
|
: this.speaker.state.selectedDevice$.subscribe((deviceId) => {
|
|
8422
8671
|
if (deviceId) {
|
|
8423
8672
|
audioElement.setSinkId(deviceId);
|
|
8424
|
-
tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
|
|
8425
8673
|
}
|
|
8426
8674
|
});
|
|
8427
8675
|
const volumeSubscription = rxjs.combineLatest([
|
|
@@ -8863,15 +9111,25 @@ const getVideoDevices = lazy(() => {
|
|
|
8863
9111
|
const getAudioOutputDevices = lazy(() => {
|
|
8864
9112
|
return rxjs.merge(getDeviceChangeObserver(), getAudioBrowserPermission().asObservable()).pipe(rxjs.startWith(undefined), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput')), rxjs.shareReplay(1));
|
|
8865
9113
|
});
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
9114
|
+
let getUserMediaExecId = 0;
|
|
9115
|
+
const getStream = async (constraints, tracer) => {
|
|
9116
|
+
const tag = `navigator.mediaDevices.getUserMedia.${getUserMediaExecId++}.`;
|
|
9117
|
+
try {
|
|
9118
|
+
tracer?.trace(tag, constraints);
|
|
9119
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
9120
|
+
tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
|
|
9121
|
+
if (isFirefox()) {
|
|
9122
|
+
// When enumerating devices, Firefox will hide device labels unless there's been
|
|
9123
|
+
// an active user media stream on the page. So we force device list updates after
|
|
9124
|
+
// every successful getUserMedia call.
|
|
9125
|
+
navigator.mediaDevices.dispatchEvent(new Event('devicechange'));
|
|
9126
|
+
}
|
|
9127
|
+
return stream;
|
|
9128
|
+
}
|
|
9129
|
+
catch (error) {
|
|
9130
|
+
tracer?.trace(`${tag}OnFailure`, error.name);
|
|
9131
|
+
throw error;
|
|
8873
9132
|
}
|
|
8874
|
-
return stream;
|
|
8875
9133
|
};
|
|
8876
9134
|
function isNotFoundOrOverconstrainedError(error) {
|
|
8877
9135
|
if (!error || typeof error !== 'object') {
|
|
@@ -8895,11 +9153,11 @@ function isNotFoundOrOverconstrainedError(error) {
|
|
|
8895
9153
|
* Returns an audio media stream that fulfills the given constraints.
|
|
8896
9154
|
* If no constraints are provided, it uses the browser's default ones.
|
|
8897
9155
|
*
|
|
8898
|
-
* @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.
|
|
8899
9156
|
* @param trackConstraints the constraints to use when requesting the stream.
|
|
8900
|
-
* @
|
|
9157
|
+
* @param tracer the tracer to use for tracing the stream creation.
|
|
9158
|
+
* @returns a new `MediaStream` fulfilling the given constraints.
|
|
8901
9159
|
*/
|
|
8902
|
-
const getAudioStream = async (trackConstraints) => {
|
|
9160
|
+
const getAudioStream = async (trackConstraints, tracer) => {
|
|
8903
9161
|
const constraints = {
|
|
8904
9162
|
audio: {
|
|
8905
9163
|
...audioDeviceConstraints.audio,
|
|
@@ -8911,7 +9169,7 @@ const getAudioStream = async (trackConstraints) => {
|
|
|
8911
9169
|
throwOnNotAllowed: true,
|
|
8912
9170
|
forcePrompt: true,
|
|
8913
9171
|
});
|
|
8914
|
-
return await getStream(constraints);
|
|
9172
|
+
return await getStream(constraints, tracer);
|
|
8915
9173
|
}
|
|
8916
9174
|
catch (error) {
|
|
8917
9175
|
if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
|
|
@@ -8931,11 +9189,11 @@ const getAudioStream = async (trackConstraints) => {
|
|
|
8931
9189
|
* Returns a video media stream that fulfills the given constraints.
|
|
8932
9190
|
* If no constraints are provided, it uses the browser's default ones.
|
|
8933
9191
|
*
|
|
8934
|
-
* @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.
|
|
8935
9192
|
* @param trackConstraints the constraints to use when requesting the stream.
|
|
9193
|
+
* @param tracer the tracer to use for tracing the stream creation.
|
|
8936
9194
|
* @returns a new `MediaStream` fulfilling the given constraints.
|
|
8937
9195
|
*/
|
|
8938
|
-
const getVideoStream = async (trackConstraints) => {
|
|
9196
|
+
const getVideoStream = async (trackConstraints, tracer) => {
|
|
8939
9197
|
const constraints = {
|
|
8940
9198
|
video: {
|
|
8941
9199
|
...videoDeviceConstraints.video,
|
|
@@ -8947,7 +9205,7 @@ const getVideoStream = async (trackConstraints) => {
|
|
|
8947
9205
|
throwOnNotAllowed: true,
|
|
8948
9206
|
forcePrompt: true,
|
|
8949
9207
|
});
|
|
8950
|
-
return await getStream(constraints);
|
|
9208
|
+
return await getStream(constraints, tracer);
|
|
8951
9209
|
}
|
|
8952
9210
|
catch (error) {
|
|
8953
9211
|
if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
|
|
@@ -8963,19 +9221,21 @@ const getVideoStream = async (trackConstraints) => {
|
|
|
8963
9221
|
throw error;
|
|
8964
9222
|
}
|
|
8965
9223
|
};
|
|
9224
|
+
let getDisplayMediaExecId = 0;
|
|
8966
9225
|
/**
|
|
8967
9226
|
* Prompts the user for a permission to share a screen.
|
|
8968
9227
|
* If the user grants the permission, a screen sharing stream is returned. Throws otherwise.
|
|
8969
9228
|
*
|
|
8970
9229
|
* The callers of this API are responsible to handle the possible errors.
|
|
8971
9230
|
*
|
|
8972
|
-
* @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.
|
|
8973
|
-
*
|
|
8974
9231
|
* @param options any additional options to pass to the [`getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) API.
|
|
9232
|
+
* @param tracer the tracer to use for tracing the stream creation.
|
|
8975
9233
|
*/
|
|
8976
|
-
const getScreenShareStream = async (options) => {
|
|
9234
|
+
const getScreenShareStream = async (options, tracer) => {
|
|
9235
|
+
const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
|
|
8977
9236
|
try {
|
|
8978
|
-
|
|
9237
|
+
tracer?.trace(tag, options);
|
|
9238
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
8979
9239
|
video: true,
|
|
8980
9240
|
audio: {
|
|
8981
9241
|
channelCount: {
|
|
@@ -8989,8 +9249,11 @@ const getScreenShareStream = async (options) => {
|
|
|
8989
9249
|
systemAudio: 'include',
|
|
8990
9250
|
...options,
|
|
8991
9251
|
});
|
|
9252
|
+
tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
|
|
9253
|
+
return stream;
|
|
8992
9254
|
}
|
|
8993
9255
|
catch (e) {
|
|
9256
|
+
tracer?.trace(`${tag}OnFailure`, e.name);
|
|
8994
9257
|
getLogger(['devices'])('error', 'Failed to get screen share stream', e);
|
|
8995
9258
|
throw e;
|
|
8996
9259
|
}
|
|
@@ -9028,9 +9291,6 @@ const isMobile = () => /Mobi/i.test(navigator.userAgent);
|
|
|
9028
9291
|
|
|
9029
9292
|
class InputMediaDeviceManager {
|
|
9030
9293
|
constructor(call, state, trackType) {
|
|
9031
|
-
this.call = call;
|
|
9032
|
-
this.state = state;
|
|
9033
|
-
this.trackType = trackType;
|
|
9034
9294
|
/**
|
|
9035
9295
|
* if true, stops the media stream when call is left
|
|
9036
9296
|
*/
|
|
@@ -9048,6 +9308,9 @@ class InputMediaDeviceManager {
|
|
|
9048
9308
|
this.dispose = () => {
|
|
9049
9309
|
this.subscriptions.forEach((s) => s());
|
|
9050
9310
|
};
|
|
9311
|
+
this.call = call;
|
|
9312
|
+
this.state = state;
|
|
9313
|
+
this.trackType = trackType;
|
|
9051
9314
|
this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
|
|
9052
9315
|
if (deviceIds$ &&
|
|
9053
9316
|
!isReactNative() &&
|
|
@@ -9493,25 +9756,6 @@ class InputMediaDeviceManagerState {
|
|
|
9493
9756
|
* The default constraints for the device.
|
|
9494
9757
|
*/
|
|
9495
9758
|
this.defaultConstraints$ = this.defaultConstraintsSubject.asObservable();
|
|
9496
|
-
/**
|
|
9497
|
-
* Gets the current value of an observable, or undefined if the observable has
|
|
9498
|
-
* not emitted a value yet.
|
|
9499
|
-
*
|
|
9500
|
-
* @param observable$ the observable to get the value from.
|
|
9501
|
-
*/
|
|
9502
|
-
this.getCurrentValue = getCurrentValue;
|
|
9503
|
-
/**
|
|
9504
|
-
* Updates the value of the provided Subject.
|
|
9505
|
-
* An `update` can either be a new value or a function which takes
|
|
9506
|
-
* the current value and returns a new value.
|
|
9507
|
-
*
|
|
9508
|
-
* @internal
|
|
9509
|
-
*
|
|
9510
|
-
* @param subject the subject to update.
|
|
9511
|
-
* @param update the update to apply to the subject.
|
|
9512
|
-
* @return the updated value.
|
|
9513
|
-
*/
|
|
9514
|
-
this.setCurrentValue = setCurrentValue;
|
|
9515
9759
|
this.hasBrowserPermission$ = permission
|
|
9516
9760
|
? permission.asObservable().pipe(rxjs.shareReplay(1))
|
|
9517
9761
|
: rxjs.of(true);
|
|
@@ -9526,39 +9770,39 @@ class InputMediaDeviceManagerState {
|
|
|
9526
9770
|
* The device status
|
|
9527
9771
|
*/
|
|
9528
9772
|
get status() {
|
|
9529
|
-
return
|
|
9773
|
+
return getCurrentValue(this.status$);
|
|
9530
9774
|
}
|
|
9531
9775
|
/**
|
|
9532
9776
|
* The requested device status. Useful for optimistic UIs
|
|
9533
9777
|
*/
|
|
9534
9778
|
get optimisticStatus() {
|
|
9535
|
-
return
|
|
9779
|
+
return getCurrentValue(this.optimisticStatus$);
|
|
9536
9780
|
}
|
|
9537
9781
|
/**
|
|
9538
9782
|
* The currently selected device
|
|
9539
9783
|
*/
|
|
9540
9784
|
get selectedDevice() {
|
|
9541
|
-
return
|
|
9785
|
+
return getCurrentValue(this.selectedDevice$);
|
|
9542
9786
|
}
|
|
9543
9787
|
/**
|
|
9544
9788
|
* The current media stream, or `undefined` if the device is currently disabled.
|
|
9545
9789
|
*/
|
|
9546
9790
|
get mediaStream() {
|
|
9547
|
-
return
|
|
9791
|
+
return getCurrentValue(this.mediaStream$);
|
|
9548
9792
|
}
|
|
9549
9793
|
/**
|
|
9550
9794
|
* @internal
|
|
9551
9795
|
* @param status
|
|
9552
9796
|
*/
|
|
9553
9797
|
setStatus(status) {
|
|
9554
|
-
|
|
9798
|
+
setCurrentValue(this.statusSubject, status);
|
|
9555
9799
|
}
|
|
9556
9800
|
/**
|
|
9557
9801
|
* @internal
|
|
9558
9802
|
* @param pendingStatus
|
|
9559
9803
|
*/
|
|
9560
9804
|
setPendingStatus(pendingStatus) {
|
|
9561
|
-
|
|
9805
|
+
setCurrentValue(this.optimisticStatusSubject, pendingStatus);
|
|
9562
9806
|
}
|
|
9563
9807
|
/**
|
|
9564
9808
|
* Updates the `mediaStream` state variable.
|
|
@@ -9569,7 +9813,7 @@ class InputMediaDeviceManagerState {
|
|
|
9569
9813
|
* as this is the stream that holds the actual deviceId information.
|
|
9570
9814
|
*/
|
|
9571
9815
|
setMediaStream(stream, rootStream) {
|
|
9572
|
-
|
|
9816
|
+
setCurrentValue(this.mediaStreamSubject, stream);
|
|
9573
9817
|
if (rootStream) {
|
|
9574
9818
|
this.setDevice(this.getDeviceIdFromStream(rootStream));
|
|
9575
9819
|
}
|
|
@@ -9579,13 +9823,13 @@ class InputMediaDeviceManagerState {
|
|
|
9579
9823
|
* @param deviceId the device id to set.
|
|
9580
9824
|
*/
|
|
9581
9825
|
setDevice(deviceId) {
|
|
9582
|
-
|
|
9826
|
+
setCurrentValue(this.selectedDeviceSubject, deviceId);
|
|
9583
9827
|
}
|
|
9584
9828
|
/**
|
|
9585
9829
|
* Gets the default constraints for the device.
|
|
9586
9830
|
*/
|
|
9587
9831
|
get defaultConstraints() {
|
|
9588
|
-
return
|
|
9832
|
+
return getCurrentValue(this.defaultConstraints$);
|
|
9589
9833
|
}
|
|
9590
9834
|
/**
|
|
9591
9835
|
* Sets the default constraints for the device.
|
|
@@ -9594,7 +9838,7 @@ class InputMediaDeviceManagerState {
|
|
|
9594
9838
|
* @param constraints the constraints to set.
|
|
9595
9839
|
*/
|
|
9596
9840
|
setDefaultConstraints(constraints) {
|
|
9597
|
-
|
|
9841
|
+
setCurrentValue(this.defaultConstraintsSubject, constraints);
|
|
9598
9842
|
}
|
|
9599
9843
|
}
|
|
9600
9844
|
|
|
@@ -9612,13 +9856,13 @@ class CameraManagerState extends InputMediaDeviceManagerState {
|
|
|
9612
9856
|
* back - means the camera facing the environment
|
|
9613
9857
|
*/
|
|
9614
9858
|
get direction() {
|
|
9615
|
-
return
|
|
9859
|
+
return getCurrentValue(this.direction$);
|
|
9616
9860
|
}
|
|
9617
9861
|
/**
|
|
9618
9862
|
* @internal
|
|
9619
9863
|
*/
|
|
9620
9864
|
setDirection(direction) {
|
|
9621
|
-
|
|
9865
|
+
setCurrentValue(this.directionSubject, direction);
|
|
9622
9866
|
}
|
|
9623
9867
|
/**
|
|
9624
9868
|
* @internal
|
|
@@ -9663,32 +9907,32 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
9663
9907
|
* @param direction the direction of the camera to select.
|
|
9664
9908
|
*/
|
|
9665
9909
|
async selectDirection(direction) {
|
|
9666
|
-
if (this.isDirectionSupportedByDevice()) {
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9910
|
+
if (!this.isDirectionSupportedByDevice()) {
|
|
9911
|
+
this.logger('warn', 'Setting direction is not supported on this device');
|
|
9912
|
+
return;
|
|
9913
|
+
}
|
|
9914
|
+
// providing both device id and direction doesn't work, so we deselect the device
|
|
9915
|
+
this.state.setDirection(direction);
|
|
9916
|
+
this.state.setDevice(undefined);
|
|
9917
|
+
if (isReactNative()) {
|
|
9918
|
+
const videoTrack = this.getTracks()[0];
|
|
9919
|
+
await videoTrack?.applyConstraints({
|
|
9920
|
+
facingMode: direction === 'front' ? 'user' : 'environment',
|
|
9921
|
+
});
|
|
9922
|
+
return;
|
|
9923
|
+
}
|
|
9924
|
+
this.getTracks().forEach((track) => track.stop());
|
|
9925
|
+
try {
|
|
9926
|
+
await this.unmuteStream();
|
|
9927
|
+
}
|
|
9928
|
+
catch (error) {
|
|
9929
|
+
if (error instanceof Error && error.name === 'NotReadableError') {
|
|
9930
|
+
// the camera is already in use, and the device can't use it unless it's released.
|
|
9931
|
+
// in that case, we need to stop the stream and start it again.
|
|
9932
|
+
await this.muteStream();
|
|
9687
9933
|
await this.unmuteStream();
|
|
9688
9934
|
}
|
|
9689
|
-
|
|
9690
|
-
else {
|
|
9691
|
-
this.logger('warn', 'Camera direction ignored for desktop devices');
|
|
9935
|
+
throw error;
|
|
9692
9936
|
}
|
|
9693
9937
|
}
|
|
9694
9938
|
/**
|
|
@@ -9773,7 +10017,7 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
9773
10017
|
constraints.facingMode =
|
|
9774
10018
|
this.state.direction === 'front' ? 'user' : 'environment';
|
|
9775
10019
|
}
|
|
9776
|
-
return getVideoStream(constraints);
|
|
10020
|
+
return getVideoStream(constraints, this.call.tracer);
|
|
9777
10021
|
}
|
|
9778
10022
|
}
|
|
9779
10023
|
|
|
@@ -9791,13 +10035,13 @@ class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
|
9791
10035
|
* This feature is not available in the React Native SDK.
|
|
9792
10036
|
*/
|
|
9793
10037
|
get speakingWhileMuted() {
|
|
9794
|
-
return
|
|
10038
|
+
return getCurrentValue(this.speakingWhileMuted$);
|
|
9795
10039
|
}
|
|
9796
10040
|
/**
|
|
9797
10041
|
* @internal
|
|
9798
10042
|
*/
|
|
9799
10043
|
setSpeakingWhileMuted(isSpeaking) {
|
|
9800
|
-
|
|
10044
|
+
setCurrentValue(this.speakingWhileMutedSubject, isSpeaking);
|
|
9801
10045
|
}
|
|
9802
10046
|
getDeviceIdFromStream(stream) {
|
|
9803
10047
|
const [track] = stream.getAudioTracks();
|
|
@@ -10184,7 +10428,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
10184
10428
|
return getAudioDevices();
|
|
10185
10429
|
}
|
|
10186
10430
|
getStream(constraints) {
|
|
10187
|
-
return getAudioStream(constraints);
|
|
10431
|
+
return getAudioStream(constraints, this.call.tracer);
|
|
10188
10432
|
}
|
|
10189
10433
|
async startSpeakingWhileMutedDetection(deviceId) {
|
|
10190
10434
|
await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
|
|
@@ -10249,19 +10493,19 @@ class ScreenShareState extends InputMediaDeviceManagerState {
|
|
|
10249
10493
|
* The current screen share audio status.
|
|
10250
10494
|
*/
|
|
10251
10495
|
get audioEnabled() {
|
|
10252
|
-
return
|
|
10496
|
+
return getCurrentValue(this.audioEnabled$);
|
|
10253
10497
|
}
|
|
10254
10498
|
/**
|
|
10255
10499
|
* Set the current screen share audio status.
|
|
10256
10500
|
*/
|
|
10257
10501
|
setAudioEnabled(isEnabled) {
|
|
10258
|
-
|
|
10502
|
+
setCurrentValue(this.audioEnabledSubject, isEnabled);
|
|
10259
10503
|
}
|
|
10260
10504
|
/**
|
|
10261
10505
|
* The current screen share settings.
|
|
10262
10506
|
*/
|
|
10263
10507
|
get settings() {
|
|
10264
|
-
return
|
|
10508
|
+
return getCurrentValue(this.settings$);
|
|
10265
10509
|
}
|
|
10266
10510
|
/**
|
|
10267
10511
|
* Set the current screen share settings.
|
|
@@ -10269,7 +10513,7 @@ class ScreenShareState extends InputMediaDeviceManagerState {
|
|
|
10269
10513
|
* @param settings the screen share settings to set.
|
|
10270
10514
|
*/
|
|
10271
10515
|
setSettings(settings) {
|
|
10272
|
-
|
|
10516
|
+
setCurrentValue(this.settingsSubject, settings);
|
|
10273
10517
|
}
|
|
10274
10518
|
}
|
|
10275
10519
|
|
|
@@ -10327,7 +10571,7 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10327
10571
|
if (!this.state.audioEnabled) {
|
|
10328
10572
|
constraints.audio = false;
|
|
10329
10573
|
}
|
|
10330
|
-
return getScreenShareStream(constraints);
|
|
10574
|
+
return getScreenShareStream(constraints, this.call.tracer);
|
|
10331
10575
|
}
|
|
10332
10576
|
async stopPublishStream() {
|
|
10333
10577
|
return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
|
|
@@ -10336,37 +10580,19 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10336
10580
|
* Overrides the default `select` method to throw an error.
|
|
10337
10581
|
*/
|
|
10338
10582
|
async select() {
|
|
10339
|
-
throw new Error('
|
|
10583
|
+
throw new Error('Not supported');
|
|
10340
10584
|
}
|
|
10341
10585
|
}
|
|
10342
10586
|
|
|
10343
10587
|
class SpeakerState {
|
|
10344
|
-
constructor() {
|
|
10588
|
+
constructor(tracer) {
|
|
10345
10589
|
this.selectedDeviceSubject = new rxjs.BehaviorSubject('');
|
|
10346
10590
|
this.volumeSubject = new rxjs.BehaviorSubject(1);
|
|
10347
10591
|
/**
|
|
10348
10592
|
* [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
|
|
10349
10593
|
*/
|
|
10350
10594
|
this.isDeviceSelectionSupported = checkIfAudioOutputChangeSupported();
|
|
10351
|
-
|
|
10352
|
-
* Gets the current value of an observable, or undefined if the observable has
|
|
10353
|
-
* not emitted a value yet.
|
|
10354
|
-
*
|
|
10355
|
-
* @param observable$ the observable to get the value from.
|
|
10356
|
-
*/
|
|
10357
|
-
this.getCurrentValue = getCurrentValue;
|
|
10358
|
-
/**
|
|
10359
|
-
* Updates the value of the provided Subject.
|
|
10360
|
-
* An `update` can either be a new value or a function which takes
|
|
10361
|
-
* the current value and returns a new value.
|
|
10362
|
-
*
|
|
10363
|
-
* @internal
|
|
10364
|
-
*
|
|
10365
|
-
* @param subject the subject to update.
|
|
10366
|
-
* @param update the update to apply to the subject.
|
|
10367
|
-
* @return the updated value.
|
|
10368
|
-
*/
|
|
10369
|
-
this.setCurrentValue = setCurrentValue;
|
|
10595
|
+
this.tracer = tracer;
|
|
10370
10596
|
this.selectedDevice$ = this.selectedDeviceSubject
|
|
10371
10597
|
.asObservable()
|
|
10372
10598
|
.pipe(rxjs.distinctUntilChanged());
|
|
@@ -10380,7 +10606,7 @@ class SpeakerState {
|
|
|
10380
10606
|
* Note: this feature is not supported in React Native
|
|
10381
10607
|
*/
|
|
10382
10608
|
get selectedDevice() {
|
|
10383
|
-
return
|
|
10609
|
+
return getCurrentValue(this.selectedDevice$);
|
|
10384
10610
|
}
|
|
10385
10611
|
/**
|
|
10386
10612
|
* The currently selected volume
|
|
@@ -10388,27 +10614,27 @@ class SpeakerState {
|
|
|
10388
10614
|
* Note: this feature is not supported in React Native
|
|
10389
10615
|
*/
|
|
10390
10616
|
get volume() {
|
|
10391
|
-
return
|
|
10617
|
+
return getCurrentValue(this.volume$);
|
|
10392
10618
|
}
|
|
10393
10619
|
/**
|
|
10394
10620
|
* @internal
|
|
10395
10621
|
* @param deviceId
|
|
10396
10622
|
*/
|
|
10397
10623
|
setDevice(deviceId) {
|
|
10398
|
-
|
|
10624
|
+
setCurrentValue(this.selectedDeviceSubject, deviceId);
|
|
10625
|
+
this.tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
|
|
10399
10626
|
}
|
|
10400
10627
|
/**
|
|
10401
10628
|
* @internal
|
|
10402
10629
|
* @param volume
|
|
10403
10630
|
*/
|
|
10404
10631
|
setVolume(volume) {
|
|
10405
|
-
|
|
10632
|
+
setCurrentValue(this.volumeSubject, volume);
|
|
10406
10633
|
}
|
|
10407
10634
|
}
|
|
10408
10635
|
|
|
10409
10636
|
class SpeakerManager {
|
|
10410
10637
|
constructor(call) {
|
|
10411
|
-
this.state = new SpeakerState();
|
|
10412
10638
|
this.subscriptions = [];
|
|
10413
10639
|
/**
|
|
10414
10640
|
* Disposes the manager.
|
|
@@ -10419,6 +10645,7 @@ class SpeakerManager {
|
|
|
10419
10645
|
this.subscriptions.forEach((s) => s.unsubscribe());
|
|
10420
10646
|
};
|
|
10421
10647
|
this.call = call;
|
|
10648
|
+
this.state = new SpeakerState(call.tracer);
|
|
10422
10649
|
if (deviceIds$ && !isReactNative()) {
|
|
10423
10650
|
this.subscriptions.push(rxjs.combineLatest([deviceIds$, this.state.selectedDevice$]).subscribe(([devices, deviceId]) => {
|
|
10424
10651
|
if (!deviceId) {
|
|
@@ -10512,6 +10739,7 @@ class Call {
|
|
|
10512
10739
|
* The permissions context of this call.
|
|
10513
10740
|
*/
|
|
10514
10741
|
this.permissionsContext = new PermissionsContext();
|
|
10742
|
+
this.tracer = new Tracer(null);
|
|
10515
10743
|
/**
|
|
10516
10744
|
* The event dispatcher instance dedicated to this Call instance.
|
|
10517
10745
|
* @private
|
|
@@ -10783,6 +11011,7 @@ class Call {
|
|
|
10783
11011
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
10784
11012
|
this.initialized = false;
|
|
10785
11013
|
this.hasJoinedOnce = false;
|
|
11014
|
+
this.unifiedSessionId = undefined;
|
|
10786
11015
|
this.ringingSubject.next(false);
|
|
10787
11016
|
this.cancelAutoDrop();
|
|
10788
11017
|
this.clientStore.unregisterCall(this);
|
|
@@ -11210,7 +11439,6 @@ class Call {
|
|
|
11210
11439
|
state: this.state,
|
|
11211
11440
|
connectionConfig,
|
|
11212
11441
|
logTag: String(this.sfuClientTag),
|
|
11213
|
-
clientDetails,
|
|
11214
11442
|
enableTracing,
|
|
11215
11443
|
onUnrecoverableError: (reason) => {
|
|
11216
11444
|
this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
|
|
@@ -11232,7 +11460,6 @@ class Call {
|
|
|
11232
11460
|
connectionConfig,
|
|
11233
11461
|
publishOptions,
|
|
11234
11462
|
logTag: String(this.sfuClientTag),
|
|
11235
|
-
clientDetails,
|
|
11236
11463
|
enableTracing,
|
|
11237
11464
|
onUnrecoverableError: (reason) => {
|
|
11238
11465
|
this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
|
|
@@ -11241,7 +11468,6 @@ class Call {
|
|
|
11241
11468
|
},
|
|
11242
11469
|
});
|
|
11243
11470
|
}
|
|
11244
|
-
tracer.setEnabled(enableTracing);
|
|
11245
11471
|
this.statsReporter?.stop();
|
|
11246
11472
|
this.statsReporter = createStatsReporter({
|
|
11247
11473
|
subscriber: this.subscriber,
|
|
@@ -11249,8 +11475,10 @@ class Call {
|
|
|
11249
11475
|
state: this.state,
|
|
11250
11476
|
datacenter: sfuClient.edgeName,
|
|
11251
11477
|
});
|
|
11478
|
+
this.tracer.setEnabled(enableTracing);
|
|
11252
11479
|
this.sfuStatsReporter?.stop();
|
|
11253
11480
|
if (statsOptions?.reporting_interval_ms > 0) {
|
|
11481
|
+
this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
|
|
11254
11482
|
this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
|
|
11255
11483
|
clientDetails,
|
|
11256
11484
|
options: statsOptions,
|
|
@@ -11259,6 +11487,8 @@ class Call {
|
|
|
11259
11487
|
microphone: this.microphone,
|
|
11260
11488
|
camera: this.camera,
|
|
11261
11489
|
state: this.state,
|
|
11490
|
+
tracer: this.tracer,
|
|
11491
|
+
unifiedSessionId: this.unifiedSessionId,
|
|
11262
11492
|
});
|
|
11263
11493
|
this.sfuStatsReporter.start();
|
|
11264
11494
|
}
|
|
@@ -11491,6 +11721,7 @@ class Call {
|
|
|
11491
11721
|
}
|
|
11492
11722
|
});
|
|
11493
11723
|
const unregisterNetworkChanged = this.streamClient.on('network.changed', (e) => {
|
|
11724
|
+
this.tracer.trace('network.changed', e);
|
|
11494
11725
|
if (!e.online) {
|
|
11495
11726
|
this.logger('debug', '[Reconnect] Going offline');
|
|
11496
11727
|
if (!this.hasJoinedOnce)
|
|
@@ -11628,6 +11859,11 @@ class Call {
|
|
|
11628
11859
|
trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
11629
11860
|
}
|
|
11630
11861
|
}
|
|
11862
|
+
if (track.kind === 'video') {
|
|
11863
|
+
// schedules calibration report - the SFU will use the performance stats
|
|
11864
|
+
// to adjust the quality thresholds as early as possible
|
|
11865
|
+
this.sfuStatsReporter?.scheduleOne(3000);
|
|
11866
|
+
}
|
|
11631
11867
|
await this.updateLocalStreamState(mediaStream, ...trackTypes);
|
|
11632
11868
|
};
|
|
11633
11869
|
/**
|
|
@@ -13487,7 +13723,7 @@ class StreamClient {
|
|
|
13487
13723
|
this.getUserAgent = () => {
|
|
13488
13724
|
if (!this.cachedUserAgent) {
|
|
13489
13725
|
const { clientAppIdentifier = {} } = this.options;
|
|
13490
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
13726
|
+
const { sdkName = 'js', sdkVersion = "1.22.0", ...extras } = clientAppIdentifier;
|
|
13491
13727
|
this.cachedUserAgent = [
|
|
13492
13728
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
13493
13729
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|