@stream-io/video-client 1.20.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/index.browser.es.js +416 -174
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +416 -174
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +416 -174
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +2 -1
- package/dist/src/StreamSfuClient.d.ts +1 -1
- package/dist/src/devices/InputMediaDeviceManagerState.d.ts +0 -20
- package/dist/src/devices/SpeakerState.d.ts +0 -20
- package/dist/src/gen/coordinator/index.d.ts +7 -0
- 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 +12 -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 +5 -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/types.d.ts +7 -2
- package/package.json +1 -1
- package/src/Call.ts +22 -12
- package/src/StreamSfuClient.ts +30 -18
- package/src/__tests__/Call.autodrop.test.ts +101 -0
- package/src/devices/CameraManagerState.ts +3 -2
- package/src/devices/InputMediaDeviceManagerState.ts +10 -31
- package/src/devices/MicrophoneManagerState.ts +3 -2
- package/src/devices/ScreenShareState.ts +5 -4
- package/src/devices/SpeakerState.ts +7 -26
- package/src/events/__tests__/call.test.ts +5 -1
- package/src/events/call.ts +8 -4
- package/src/events/internal.ts +1 -1
- package/src/gen/coordinator/index.ts +7 -0
- 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 +37 -15
- package/src/rtc/Publisher.ts +17 -15
- 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 +37 -12
- 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/mediaDevices.ts +4 -3
- 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/store/stateStore.ts +1 -1
- package/src/types.ts +8 -2
package/dist/index.cjs.js
CHANGED
|
@@ -62,16 +62,17 @@ if (typeof navigator !== 'undefined' &&
|
|
|
62
62
|
const original = target[method];
|
|
63
63
|
if (!original)
|
|
64
64
|
continue;
|
|
65
|
+
let mark = 0;
|
|
65
66
|
target[method] = async function tracedMethod(constraints) {
|
|
66
|
-
const tag = `navigator.mediaDevices.${method}`;
|
|
67
|
+
const tag = `navigator.mediaDevices.${method}.${mark++}`;
|
|
67
68
|
trace(tag, constraints);
|
|
68
69
|
try {
|
|
69
70
|
const stream = await original.call(target, constraints);
|
|
70
|
-
trace(`${tag}OnSuccess`, dumpStream(stream));
|
|
71
|
+
trace(`${tag}.OnSuccess`, dumpStream(stream));
|
|
71
72
|
return stream;
|
|
72
73
|
}
|
|
73
74
|
catch (err) {
|
|
74
|
-
trace(`${tag}OnFailure`, err.name);
|
|
75
|
+
trace(`${tag}.OnFailure`, err.name);
|
|
75
76
|
throw err;
|
|
76
77
|
}
|
|
77
78
|
};
|
|
@@ -119,6 +120,7 @@ const FrameRecordingSettingsRequestQualityEnum = {
|
|
|
119
120
|
_720P: '720p',
|
|
120
121
|
_1080P: '1080p',
|
|
121
122
|
_1440P: '1440p',
|
|
123
|
+
_2160P: '2160p',
|
|
122
124
|
};
|
|
123
125
|
/**
|
|
124
126
|
* @export
|
|
@@ -191,11 +193,13 @@ const RTMPBroadcastRequestQualityEnum = {
|
|
|
191
193
|
_720P: '720p',
|
|
192
194
|
_1080P: '1080p',
|
|
193
195
|
_1440P: '1440p',
|
|
196
|
+
_2160P: '2160p',
|
|
194
197
|
PORTRAIT_360X640: 'portrait-360x640',
|
|
195
198
|
PORTRAIT_480X854: 'portrait-480x854',
|
|
196
199
|
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
197
200
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
198
201
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
202
|
+
PORTRAIT_2160X3840: 'portrait-2160x3840',
|
|
199
203
|
};
|
|
200
204
|
/**
|
|
201
205
|
* @export
|
|
@@ -206,11 +210,13 @@ const RTMPSettingsRequestQualityEnum = {
|
|
|
206
210
|
_720P: '720p',
|
|
207
211
|
_1080P: '1080p',
|
|
208
212
|
_1440P: '1440p',
|
|
213
|
+
_2160P: '2160p',
|
|
209
214
|
PORTRAIT_360X640: 'portrait-360x640',
|
|
210
215
|
PORTRAIT_480X854: 'portrait-480x854',
|
|
211
216
|
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
212
217
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
213
218
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
219
|
+
PORTRAIT_2160X3840: 'portrait-2160x3840',
|
|
214
220
|
};
|
|
215
221
|
/**
|
|
216
222
|
* @export
|
|
@@ -229,11 +235,13 @@ const RecordSettingsRequestQualityEnum = {
|
|
|
229
235
|
_720P: '720p',
|
|
230
236
|
_1080P: '1080p',
|
|
231
237
|
_1440P: '1440p',
|
|
238
|
+
_2160P: '2160p',
|
|
232
239
|
PORTRAIT_360X640: 'portrait-360x640',
|
|
233
240
|
PORTRAIT_480X854: 'portrait-480x854',
|
|
234
241
|
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
235
242
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
236
243
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
244
|
+
PORTRAIT_2160X3840: 'portrait-2160x3840',
|
|
237
245
|
};
|
|
238
246
|
/**
|
|
239
247
|
* @export
|
|
@@ -1789,6 +1797,47 @@ class AppleState$Type extends runtime.MessageType {
|
|
|
1789
1797
|
* @generated MessageType for protobuf message stream.video.sfu.models.AppleState
|
|
1790
1798
|
*/
|
|
1791
1799
|
const AppleState = new AppleState$Type();
|
|
1800
|
+
// @generated message type with reflection information, may provide speed optimized methods
|
|
1801
|
+
class PerformanceStats$Type extends runtime.MessageType {
|
|
1802
|
+
constructor() {
|
|
1803
|
+
super('stream.video.sfu.models.PerformanceStats', [
|
|
1804
|
+
{
|
|
1805
|
+
no: 1,
|
|
1806
|
+
name: 'track_type',
|
|
1807
|
+
kind: 'enum',
|
|
1808
|
+
T: () => [
|
|
1809
|
+
'stream.video.sfu.models.TrackType',
|
|
1810
|
+
TrackType,
|
|
1811
|
+
'TRACK_TYPE_',
|
|
1812
|
+
],
|
|
1813
|
+
},
|
|
1814
|
+
{ no: 2, name: 'codec', kind: 'message', T: () => Codec },
|
|
1815
|
+
{
|
|
1816
|
+
no: 3,
|
|
1817
|
+
name: 'avg_frame_time_ms',
|
|
1818
|
+
kind: 'scalar',
|
|
1819
|
+
T: 2 /*ScalarType.FLOAT*/,
|
|
1820
|
+
},
|
|
1821
|
+
{ no: 4, name: 'avg_fps', kind: 'scalar', T: 2 /*ScalarType.FLOAT*/ },
|
|
1822
|
+
{
|
|
1823
|
+
no: 5,
|
|
1824
|
+
name: 'video_dimension',
|
|
1825
|
+
kind: 'message',
|
|
1826
|
+
T: () => VideoDimension,
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
no: 6,
|
|
1830
|
+
name: 'target_bitrate',
|
|
1831
|
+
kind: 'scalar',
|
|
1832
|
+
T: 5 /*ScalarType.INT32*/,
|
|
1833
|
+
},
|
|
1834
|
+
]);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* @generated MessageType for protobuf message stream.video.sfu.models.PerformanceStats
|
|
1839
|
+
*/
|
|
1840
|
+
const PerformanceStats = new PerformanceStats$Type();
|
|
1792
1841
|
|
|
1793
1842
|
var models = /*#__PURE__*/Object.freeze({
|
|
1794
1843
|
__proto__: null,
|
|
@@ -1814,6 +1863,7 @@ var models = /*#__PURE__*/Object.freeze({
|
|
|
1814
1863
|
Participant: Participant,
|
|
1815
1864
|
ParticipantCount: ParticipantCount,
|
|
1816
1865
|
get PeerType () { return PeerType; },
|
|
1866
|
+
PerformanceStats: PerformanceStats,
|
|
1817
1867
|
Pin: Pin,
|
|
1818
1868
|
PublishOption: PublishOption,
|
|
1819
1869
|
RTMPIngress: RTMPIngress,
|
|
@@ -1993,6 +2043,27 @@ class SendStatsRequest$Type extends runtime.MessageType {
|
|
|
1993
2043
|
kind: 'scalar',
|
|
1994
2044
|
T: 9 /*ScalarType.STRING*/,
|
|
1995
2045
|
},
|
|
2046
|
+
{ no: 15, name: 'rtc_stats', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2047
|
+
{
|
|
2048
|
+
no: 16,
|
|
2049
|
+
name: 'encode_stats',
|
|
2050
|
+
kind: 'message',
|
|
2051
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2052
|
+
T: () => PerformanceStats,
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
no: 17,
|
|
2056
|
+
name: 'decode_stats',
|
|
2057
|
+
kind: 'message',
|
|
2058
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2059
|
+
T: () => PerformanceStats,
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
no: 18,
|
|
2063
|
+
name: 'unified_session_id',
|
|
2064
|
+
kind: 'scalar',
|
|
2065
|
+
T: 9 /*ScalarType.STRING*/,
|
|
2066
|
+
},
|
|
1996
2067
|
]);
|
|
1997
2068
|
}
|
|
1998
2069
|
}
|
|
@@ -4060,7 +4131,7 @@ class StreamVideoWriteableStateStore {
|
|
|
4060
4131
|
continue;
|
|
4061
4132
|
logger('info', `User disconnected, leaving call: ${call.cid}`);
|
|
4062
4133
|
await call
|
|
4063
|
-
.leave({
|
|
4134
|
+
.leave({ message: 'client.disconnectUser() called' })
|
|
4064
4135
|
.catch((err) => {
|
|
4065
4136
|
logger('error', `Error leaving call: ${call.cid}`, err);
|
|
4066
4137
|
});
|
|
@@ -5464,30 +5535,17 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
5464
5535
|
}
|
|
5465
5536
|
}
|
|
5466
5537
|
}
|
|
5467
|
-
const [subscriberStats, publisherStats] = await Promise.all([
|
|
5468
|
-
subscriber
|
|
5469
|
-
.getStats()
|
|
5470
|
-
.then((report) => transform(report, {
|
|
5471
|
-
kind: 'subscriber',
|
|
5472
|
-
trackKind: 'video',
|
|
5473
|
-
publisher,
|
|
5474
|
-
}))
|
|
5475
|
-
.then(aggregate),
|
|
5476
|
-
publisher
|
|
5477
|
-
? publisher
|
|
5478
|
-
.getStats()
|
|
5479
|
-
.then((report) => transform(report, {
|
|
5480
|
-
kind: 'publisher',
|
|
5481
|
-
trackKind: 'video',
|
|
5482
|
-
publisher,
|
|
5483
|
-
}))
|
|
5484
|
-
.then(aggregate)
|
|
5485
|
-
: getEmptyStats(),
|
|
5486
|
-
]);
|
|
5487
5538
|
const [subscriberRawStats, publisherRawStats] = await Promise.all([
|
|
5488
5539
|
getRawStatsForTrack('subscriber'),
|
|
5489
5540
|
publisher ? getRawStatsForTrack('publisher') : undefined,
|
|
5490
5541
|
]);
|
|
5542
|
+
const process = (report, kind) => aggregate(transform(report, { kind, trackKind: 'video', publisher }));
|
|
5543
|
+
const subscriberStats = subscriberRawStats
|
|
5544
|
+
? process(subscriberRawStats, 'subscriber')
|
|
5545
|
+
: getEmptyStats();
|
|
5546
|
+
const publisherStats = publisherRawStats
|
|
5547
|
+
? process(publisherRawStats, 'publisher')
|
|
5548
|
+
: getEmptyStats();
|
|
5491
5549
|
state.setCallStatsReport({
|
|
5492
5550
|
datacenter,
|
|
5493
5551
|
publisherStats,
|
|
@@ -5645,7 +5703,7 @@ const aggregate = (stats) => {
|
|
|
5645
5703
|
return report;
|
|
5646
5704
|
};
|
|
5647
5705
|
|
|
5648
|
-
const version = "1.
|
|
5706
|
+
const version = "1.21.0";
|
|
5649
5707
|
const [major, minor, patch] = version.split('.');
|
|
5650
5708
|
let sdkInfo = {
|
|
5651
5709
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5782,7 +5840,7 @@ const getClientDetails = async () => {
|
|
|
5782
5840
|
};
|
|
5783
5841
|
|
|
5784
5842
|
class SfuStatsReporter {
|
|
5785
|
-
constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, }) {
|
|
5843
|
+
constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, unifiedSessionId, }) {
|
|
5786
5844
|
this.logger = getLogger(['SfuStatsReporter']);
|
|
5787
5845
|
this.inputDevices = new Map();
|
|
5788
5846
|
this.observeDevice = (device, kind) => {
|
|
@@ -5839,31 +5897,40 @@ class SfuStatsReporter {
|
|
|
5839
5897
|
};
|
|
5840
5898
|
this.run = async (telemetry) => {
|
|
5841
5899
|
const [subscriberStats, publisherStats] = await Promise.all([
|
|
5842
|
-
this.subscriber.
|
|
5843
|
-
this.publisher?.
|
|
5900
|
+
this.subscriber.stats.get(),
|
|
5901
|
+
this.publisher?.stats.get(),
|
|
5844
5902
|
]);
|
|
5845
|
-
|
|
5846
|
-
|
|
5903
|
+
this.subscriber.tracer?.trace('getstats', subscriberStats.delta);
|
|
5904
|
+
if (publisherStats) {
|
|
5905
|
+
this.publisher?.tracer?.trace('getstats', publisherStats.delta);
|
|
5906
|
+
}
|
|
5907
|
+
const subscriberTrace = this.subscriber.tracer?.take();
|
|
5908
|
+
const publisherTrace = this.publisher?.tracer?.take();
|
|
5847
5909
|
const mediaTrace = tracer.take();
|
|
5848
5910
|
const sfuTrace = this.sfuClient.getTrace();
|
|
5849
|
-
const
|
|
5911
|
+
const traces = [
|
|
5850
5912
|
...mediaTrace.snapshot,
|
|
5851
5913
|
...(sfuTrace?.snapshot ?? []),
|
|
5852
5914
|
...(publisherTrace?.snapshot ?? []),
|
|
5915
|
+
...(subscriberTrace?.snapshot ?? []),
|
|
5853
5916
|
];
|
|
5854
5917
|
try {
|
|
5855
5918
|
await this.sfuClient.sendStats({
|
|
5856
5919
|
sdk: this.sdkName,
|
|
5857
5920
|
sdkVersion: this.sdkVersion,
|
|
5858
5921
|
webrtcVersion: this.webRTCVersion,
|
|
5859
|
-
subscriberStats,
|
|
5860
|
-
|
|
5861
|
-
? JSON.stringify(
|
|
5862
|
-
: '',
|
|
5863
|
-
|
|
5864
|
-
publisherRtcStats:
|
|
5922
|
+
subscriberStats: JSON.stringify(flatten(subscriberStats.stats)),
|
|
5923
|
+
publisherStats: publisherStats
|
|
5924
|
+
? JSON.stringify(flatten(publisherStats.stats))
|
|
5925
|
+
: '[]',
|
|
5926
|
+
subscriberRtcStats: '',
|
|
5927
|
+
publisherRtcStats: '',
|
|
5928
|
+
rtcStats: JSON.stringify(traces),
|
|
5929
|
+
encodeStats: publisherStats?.performanceStats ?? [],
|
|
5930
|
+
decodeStats: subscriberStats.performanceStats,
|
|
5865
5931
|
audioDevices: this.inputDevices.get('mic'),
|
|
5866
5932
|
videoDevices: this.inputDevices.get('camera'),
|
|
5933
|
+
unifiedSessionId: this.unifiedSessionId,
|
|
5867
5934
|
deviceState: getDeviceState(),
|
|
5868
5935
|
telemetry,
|
|
5869
5936
|
});
|
|
@@ -5896,6 +5963,16 @@ class SfuStatsReporter {
|
|
|
5896
5963
|
this.inputDevices.clear();
|
|
5897
5964
|
clearInterval(this.intervalId);
|
|
5898
5965
|
this.intervalId = undefined;
|
|
5966
|
+
clearTimeout(this.timeoutId);
|
|
5967
|
+
this.timeoutId = undefined;
|
|
5968
|
+
};
|
|
5969
|
+
this.scheduleOne = (timeout) => {
|
|
5970
|
+
clearTimeout(this.timeoutId);
|
|
5971
|
+
this.timeoutId = setTimeout(() => {
|
|
5972
|
+
this.run().catch((err) => {
|
|
5973
|
+
this.logger('warn', 'Failed to report stats', err);
|
|
5974
|
+
});
|
|
5975
|
+
}, timeout);
|
|
5899
5976
|
};
|
|
5900
5977
|
this.sfuClient = sfuClient;
|
|
5901
5978
|
this.options = options;
|
|
@@ -5904,6 +5981,7 @@ class SfuStatsReporter {
|
|
|
5904
5981
|
this.microphone = microphone;
|
|
5905
5982
|
this.camera = camera;
|
|
5906
5983
|
this.state = state;
|
|
5984
|
+
this.unifiedSessionId = unifiedSessionId;
|
|
5907
5985
|
const { sdk, browser } = clientDetails;
|
|
5908
5986
|
this.sdkName = getSdkName(sdk);
|
|
5909
5987
|
this.sdkVersion = getSdkVersion(sdk);
|
|
@@ -5926,47 +6004,25 @@ const traceRTCPeerConnection = (pc, trace) => {
|
|
|
5926
6004
|
trace('ontrack', `${e.track.kind}:${e.track.id} ${streams}`);
|
|
5927
6005
|
});
|
|
5928
6006
|
pc.addEventListener('signalingstatechange', () => {
|
|
5929
|
-
trace('
|
|
6007
|
+
trace('signalingstatechange', pc.signalingState);
|
|
5930
6008
|
});
|
|
5931
6009
|
pc.addEventListener('iceconnectionstatechange', () => {
|
|
5932
|
-
trace('
|
|
6010
|
+
trace('iceconnectionstatechange', pc.iceConnectionState);
|
|
5933
6011
|
});
|
|
5934
6012
|
pc.addEventListener('icegatheringstatechange', () => {
|
|
5935
|
-
trace('
|
|
6013
|
+
trace('icegatheringstatechange', pc.iceGatheringState);
|
|
5936
6014
|
});
|
|
5937
6015
|
pc.addEventListener('connectionstatechange', () => {
|
|
5938
|
-
trace('
|
|
6016
|
+
trace('connectionstatechange', pc.connectionState);
|
|
5939
6017
|
});
|
|
5940
6018
|
pc.addEventListener('negotiationneeded', () => {
|
|
5941
|
-
trace('
|
|
6019
|
+
trace('negotiationneeded', undefined);
|
|
5942
6020
|
});
|
|
5943
6021
|
pc.addEventListener('datachannel', ({ channel }) => {
|
|
5944
|
-
trace('
|
|
5945
|
-
});
|
|
5946
|
-
let prev = {};
|
|
5947
|
-
const getStats = () => {
|
|
5948
|
-
pc.getStats(null)
|
|
5949
|
-
.then((stats) => {
|
|
5950
|
-
const now = toObject(stats);
|
|
5951
|
-
trace('getstats', deltaCompression(prev, now));
|
|
5952
|
-
prev = now;
|
|
5953
|
-
})
|
|
5954
|
-
.catch((err) => {
|
|
5955
|
-
trace('getstatsOnFailure', err.toString());
|
|
5956
|
-
});
|
|
5957
|
-
};
|
|
5958
|
-
const interval = setInterval(() => {
|
|
5959
|
-
getStats();
|
|
5960
|
-
}, 8000);
|
|
5961
|
-
pc.addEventListener('connectionstatechange', () => {
|
|
5962
|
-
const state = pc.connectionState;
|
|
5963
|
-
if (state === 'connected' || state === 'failed') {
|
|
5964
|
-
getStats();
|
|
5965
|
-
}
|
|
6022
|
+
trace('datachannel', [channel.id, channel.label]);
|
|
5966
6023
|
});
|
|
5967
6024
|
const origClose = pc.close;
|
|
5968
6025
|
pc.close = function tracedClose() {
|
|
5969
|
-
clearInterval(interval);
|
|
5970
6026
|
trace('close', undefined);
|
|
5971
6027
|
return origClose.call(this);
|
|
5972
6028
|
};
|
|
@@ -5996,9 +6052,162 @@ const traceRTCPeerConnection = (pc, trace) => {
|
|
|
5996
6052
|
};
|
|
5997
6053
|
}
|
|
5998
6054
|
};
|
|
5999
|
-
|
|
6055
|
+
|
|
6056
|
+
/**
|
|
6057
|
+
* StatsTracer is a class that collects and processes WebRTC stats.
|
|
6058
|
+
* It is used to track the performance of the WebRTC connection
|
|
6059
|
+
* and to provide information about the media streams.
|
|
6060
|
+
* It is used by both the Publisher and Subscriber classes.
|
|
6061
|
+
*
|
|
6062
|
+
* @internal
|
|
6063
|
+
*/
|
|
6064
|
+
class StatsTracer {
|
|
6065
|
+
/**
|
|
6066
|
+
* Creates a new StatsTracer instance.
|
|
6067
|
+
*/
|
|
6068
|
+
constructor(pc, peerType, trackIdToTrackType) {
|
|
6069
|
+
this.previousStats = {};
|
|
6070
|
+
this.frameTimeHistory = [];
|
|
6071
|
+
this.fpsHistory = [];
|
|
6072
|
+
/**
|
|
6073
|
+
* Get the stats from the RTCPeerConnection.
|
|
6074
|
+
* When called, it will return the stats for the current connection.
|
|
6075
|
+
* It will also return the delta between the current stats and the previous stats.
|
|
6076
|
+
* This is used to track the performance of the connection.
|
|
6077
|
+
*
|
|
6078
|
+
* @internal
|
|
6079
|
+
*/
|
|
6080
|
+
this.get = async () => {
|
|
6081
|
+
const stats = await this.pc.getStats();
|
|
6082
|
+
const currentStats = toObject(stats);
|
|
6083
|
+
const performanceStats = this.withOverrides(this.peerType === PeerType.SUBSCRIBER
|
|
6084
|
+
? this.getDecodeStats(currentStats)
|
|
6085
|
+
: this.getEncodeStats(currentStats));
|
|
6086
|
+
const delta = deltaCompression(this.previousStats, currentStats);
|
|
6087
|
+
// store the current data for the next iteration
|
|
6088
|
+
this.previousStats = currentStats;
|
|
6089
|
+
this.frameTimeHistory = this.frameTimeHistory.slice(-2);
|
|
6090
|
+
this.fpsHistory = this.fpsHistory.slice(-2);
|
|
6091
|
+
return { performanceStats, delta, stats };
|
|
6092
|
+
};
|
|
6093
|
+
/**
|
|
6094
|
+
* Collects encode stats from the RTCPeerConnection.
|
|
6095
|
+
*/
|
|
6096
|
+
this.getEncodeStats = (currentStats) => {
|
|
6097
|
+
const encodeStats = [];
|
|
6098
|
+
for (const rtp of Object.values(currentStats)) {
|
|
6099
|
+
if (rtp.type !== 'outbound-rtp')
|
|
6100
|
+
continue;
|
|
6101
|
+
const { codecId, framesSent = 0, kind, id, totalEncodeTime = 0, framesPerSecond = 0, frameHeight = 0, frameWidth = 0, targetBitrate = 0, mediaSourceId, } = rtp;
|
|
6102
|
+
if (kind === 'audio' || !this.previousStats[id])
|
|
6103
|
+
continue;
|
|
6104
|
+
const prevRtp = this.previousStats[id];
|
|
6105
|
+
const deltaTotalEncodeTime = totalEncodeTime - (prevRtp.totalEncodeTime || 0);
|
|
6106
|
+
const deltaFramesSent = framesSent - (prevRtp.framesSent || 0);
|
|
6107
|
+
const framesEncodeTime = deltaFramesSent > 0
|
|
6108
|
+
? (deltaTotalEncodeTime / deltaFramesSent) * 1000
|
|
6109
|
+
: 0;
|
|
6110
|
+
this.frameTimeHistory.push(framesEncodeTime);
|
|
6111
|
+
this.fpsHistory.push(framesPerSecond);
|
|
6112
|
+
let trackType = TrackType.VIDEO;
|
|
6113
|
+
if (mediaSourceId && currentStats[mediaSourceId]) {
|
|
6114
|
+
const mediaSource = currentStats[mediaSourceId];
|
|
6115
|
+
trackType =
|
|
6116
|
+
this.trackIdToTrackType.get(mediaSource.trackIdentifier) || trackType;
|
|
6117
|
+
}
|
|
6118
|
+
encodeStats.push({
|
|
6119
|
+
trackType,
|
|
6120
|
+
codec: getCodecFromStats(currentStats, codecId),
|
|
6121
|
+
avgFrameTimeMs: average(this.frameTimeHistory),
|
|
6122
|
+
avgFps: average(this.fpsHistory),
|
|
6123
|
+
targetBitrate: Math.round(targetBitrate),
|
|
6124
|
+
videoDimension: { width: frameWidth, height: frameHeight },
|
|
6125
|
+
});
|
|
6126
|
+
}
|
|
6127
|
+
return encodeStats;
|
|
6128
|
+
};
|
|
6129
|
+
/**
|
|
6130
|
+
* Collects decode stats from the RTCPeerConnection.
|
|
6131
|
+
*/
|
|
6132
|
+
this.getDecodeStats = (currentStats) => {
|
|
6133
|
+
let rtp = undefined;
|
|
6134
|
+
let max = 0;
|
|
6135
|
+
for (const item of Object.values(currentStats)) {
|
|
6136
|
+
if (item.type !== 'inbound-rtp')
|
|
6137
|
+
continue;
|
|
6138
|
+
const rtpItem = item;
|
|
6139
|
+
const { kind, frameWidth = 0, frameHeight = 0 } = rtpItem;
|
|
6140
|
+
const area = frameWidth * frameHeight;
|
|
6141
|
+
if (kind === 'video' && area > max) {
|
|
6142
|
+
rtp = rtpItem;
|
|
6143
|
+
max = area;
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
if (!rtp || !this.previousStats[rtp.id])
|
|
6147
|
+
return [];
|
|
6148
|
+
const prevRtp = this.previousStats[rtp.id];
|
|
6149
|
+
const { framesDecoded = 0, framesPerSecond = 0, totalDecodeTime = 0, trackIdentifier, } = rtp;
|
|
6150
|
+
const deltaTotalDecodeTime = totalDecodeTime - (prevRtp.totalDecodeTime || 0);
|
|
6151
|
+
const deltaFramesDecoded = framesDecoded - (prevRtp.framesDecoded || 0);
|
|
6152
|
+
const framesDecodeTime = deltaFramesDecoded > 0
|
|
6153
|
+
? (deltaTotalDecodeTime / deltaFramesDecoded) * 1000
|
|
6154
|
+
: 0;
|
|
6155
|
+
this.frameTimeHistory.push(framesDecodeTime);
|
|
6156
|
+
this.fpsHistory.push(framesPerSecond);
|
|
6157
|
+
const trackType = this.trackIdToTrackType.get(trackIdentifier) || TrackType.VIDEO;
|
|
6158
|
+
return [
|
|
6159
|
+
PerformanceStats.create({
|
|
6160
|
+
trackType,
|
|
6161
|
+
codec: getCodecFromStats(currentStats, rtp.codecId),
|
|
6162
|
+
avgFrameTimeMs: average(this.frameTimeHistory),
|
|
6163
|
+
avgFps: average(this.fpsHistory),
|
|
6164
|
+
videoDimension: { width: rtp.frameWidth, height: rtp.frameHeight },
|
|
6165
|
+
}),
|
|
6166
|
+
];
|
|
6167
|
+
};
|
|
6168
|
+
/**
|
|
6169
|
+
* Applies cost overrides to the performance stats.
|
|
6170
|
+
* This is used to override the default encode/decode times with custom values.
|
|
6171
|
+
* This is useful for testing and debugging purposes, and it shouldn't be used in production.
|
|
6172
|
+
*/
|
|
6173
|
+
this.withOverrides = (performanceStats) => {
|
|
6174
|
+
if (this.costOverrides) {
|
|
6175
|
+
for (const s of performanceStats) {
|
|
6176
|
+
const override = this.costOverrides.get(s.trackType);
|
|
6177
|
+
if (override !== undefined) {
|
|
6178
|
+
// override the average encode/decode time with the provided cost.
|
|
6179
|
+
// format: [override].[original-frame-time]
|
|
6180
|
+
s.avgFrameTimeMs = override + (s.avgFrameTimeMs || 0) / 1000;
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
return performanceStats;
|
|
6185
|
+
};
|
|
6186
|
+
/**
|
|
6187
|
+
* Set the encode/decode cost for a specific track type.
|
|
6188
|
+
* This is used to override the default encode/decode times with custom values.
|
|
6189
|
+
* This is useful for testing and debugging purposes, and it shouldn't be used in production.
|
|
6190
|
+
*
|
|
6191
|
+
* @internal
|
|
6192
|
+
*/
|
|
6193
|
+
this.setCost = (cost, trackType = TrackType.VIDEO) => {
|
|
6194
|
+
if (!this.costOverrides)
|
|
6195
|
+
this.costOverrides = new Map();
|
|
6196
|
+
this.costOverrides.set(trackType, cost);
|
|
6197
|
+
};
|
|
6198
|
+
this.pc = pc;
|
|
6199
|
+
this.peerType = peerType;
|
|
6200
|
+
this.trackIdToTrackType = trackIdToTrackType;
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
/**
|
|
6204
|
+
* Convert the stat report to an object.
|
|
6205
|
+
*
|
|
6206
|
+
* @param report the stat report to convert.
|
|
6207
|
+
*/
|
|
6208
|
+
const toObject = (report) => {
|
|
6000
6209
|
const obj = {};
|
|
6001
|
-
|
|
6210
|
+
report.forEach((v, k) => {
|
|
6002
6211
|
obj[k] = v;
|
|
6003
6212
|
});
|
|
6004
6213
|
return obj;
|
|
@@ -6021,12 +6230,13 @@ const deltaCompression = (oldStats, newStats) => {
|
|
|
6021
6230
|
}
|
|
6022
6231
|
}
|
|
6023
6232
|
let timestamp = -Infinity;
|
|
6024
|
-
|
|
6233
|
+
const values = Object.values(newStats);
|
|
6234
|
+
for (const report of values) {
|
|
6025
6235
|
if (report.timestamp > timestamp) {
|
|
6026
6236
|
timestamp = report.timestamp;
|
|
6027
6237
|
}
|
|
6028
6238
|
}
|
|
6029
|
-
for (const report of
|
|
6239
|
+
for (const report of values) {
|
|
6030
6240
|
if (report.timestamp === timestamp) {
|
|
6031
6241
|
report.timestamp = 0;
|
|
6032
6242
|
}
|
|
@@ -6034,6 +6244,27 @@ const deltaCompression = (oldStats, newStats) => {
|
|
|
6034
6244
|
newStats.timestamp = timestamp;
|
|
6035
6245
|
return newStats;
|
|
6036
6246
|
};
|
|
6247
|
+
/**
|
|
6248
|
+
* Calculates an average value.
|
|
6249
|
+
*/
|
|
6250
|
+
const average = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
6251
|
+
/**
|
|
6252
|
+
* Create a Codec object from the codec stats.
|
|
6253
|
+
*
|
|
6254
|
+
* @param stats the stats report.
|
|
6255
|
+
* @param codecId the codec ID to look for.
|
|
6256
|
+
*/
|
|
6257
|
+
const getCodecFromStats = (stats, codecId) => {
|
|
6258
|
+
if (!codecId || !stats[codecId])
|
|
6259
|
+
return;
|
|
6260
|
+
const codecStats = stats[codecId];
|
|
6261
|
+
return Codec.create({
|
|
6262
|
+
name: codecStats.mimeType.split('/').pop(), // video/av1 -> av1
|
|
6263
|
+
clockRate: codecStats.clockRate,
|
|
6264
|
+
payloadType: codecStats.payloadType,
|
|
6265
|
+
fmtp: codecStats.sdpFmtpLine,
|
|
6266
|
+
});
|
|
6267
|
+
};
|
|
6037
6268
|
|
|
6038
6269
|
/**
|
|
6039
6270
|
* A base class for the `Publisher` and `Subscriber` classes.
|
|
@@ -6043,9 +6274,10 @@ class BasePeerConnection {
|
|
|
6043
6274
|
/**
|
|
6044
6275
|
* Constructs a new `BasePeerConnection` instance.
|
|
6045
6276
|
*/
|
|
6046
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag,
|
|
6277
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, enableTracing, }) {
|
|
6047
6278
|
this.isIceRestarting = false;
|
|
6048
6279
|
this.isDisposed = false;
|
|
6280
|
+
this.trackIdToTrackType = new Map();
|
|
6049
6281
|
this.subscriptions = [];
|
|
6050
6282
|
/**
|
|
6051
6283
|
* Handles events synchronously.
|
|
@@ -6093,10 +6325,10 @@ class BasePeerConnection {
|
|
|
6093
6325
|
return this.pc.getStats(selector);
|
|
6094
6326
|
};
|
|
6095
6327
|
/**
|
|
6096
|
-
*
|
|
6328
|
+
* Maps the given track ID to the corresponding track type.
|
|
6097
6329
|
*/
|
|
6098
|
-
this.
|
|
6099
|
-
return this.
|
|
6330
|
+
this.getTrackType = (trackId) => {
|
|
6331
|
+
return this.trackIdToTrackType.get(trackId);
|
|
6100
6332
|
};
|
|
6101
6333
|
/**
|
|
6102
6334
|
* Handles the ICECandidate event and
|
|
@@ -6130,6 +6362,24 @@ class BasePeerConnection {
|
|
|
6130
6362
|
}
|
|
6131
6363
|
return JSON.stringify(candidate.toJSON());
|
|
6132
6364
|
};
|
|
6365
|
+
/**
|
|
6366
|
+
* Handles the ConnectionStateChange event.
|
|
6367
|
+
*/
|
|
6368
|
+
this.onConnectionStateChange = async () => {
|
|
6369
|
+
const state = this.pc.connectionState;
|
|
6370
|
+
this.logger('debug', `Connection state changed`, state);
|
|
6371
|
+
if (!this.tracer)
|
|
6372
|
+
return;
|
|
6373
|
+
if (state === 'connected' || state === 'failed') {
|
|
6374
|
+
try {
|
|
6375
|
+
const stats = await this.stats.get();
|
|
6376
|
+
this.tracer.trace('getstats', stats.delta);
|
|
6377
|
+
}
|
|
6378
|
+
catch (err) {
|
|
6379
|
+
this.tracer.trace('getstatsOnFailure', err.toString());
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
};
|
|
6133
6383
|
/**
|
|
6134
6384
|
* Handles the ICE connection state change event.
|
|
6135
6385
|
*/
|
|
@@ -6186,18 +6436,19 @@ class BasePeerConnection {
|
|
|
6186
6436
|
logTag,
|
|
6187
6437
|
]);
|
|
6188
6438
|
this.pc = new RTCPeerConnection(connectionConfig);
|
|
6189
|
-
if (enableTracing) {
|
|
6190
|
-
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
|
|
6191
|
-
this.tracer = new Tracer(tag);
|
|
6192
|
-
this.tracer.trace('clientDetails', clientDetails);
|
|
6193
|
-
this.tracer.trace('create', connectionConfig);
|
|
6194
|
-
traceRTCPeerConnection(this.pc, this.tracer.trace);
|
|
6195
|
-
}
|
|
6196
6439
|
this.pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
6197
6440
|
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6198
6441
|
this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6199
6442
|
this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
6200
6443
|
this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
6444
|
+
this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
6445
|
+
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
|
|
6446
|
+
if (enableTracing) {
|
|
6447
|
+
const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}-${sfuClient.edgeName}`;
|
|
6448
|
+
this.tracer = new Tracer(tag);
|
|
6449
|
+
this.tracer.trace('create', connectionConfig);
|
|
6450
|
+
traceRTCPeerConnection(this.pc, this.tracer.trace);
|
|
6451
|
+
}
|
|
6201
6452
|
}
|
|
6202
6453
|
/**
|
|
6203
6454
|
* Disposes the `RTCPeerConnection` instance.
|
|
@@ -6563,7 +6814,7 @@ class Publisher extends BasePeerConnection {
|
|
|
6563
6814
|
}
|
|
6564
6815
|
else {
|
|
6565
6816
|
const previousTrack = transceiver.sender.track;
|
|
6566
|
-
await
|
|
6817
|
+
await this.updateTransceiver(transceiver, trackToPublish, trackType);
|
|
6567
6818
|
if (!isReactNative()) {
|
|
6568
6819
|
this.stopTrack(previousTrack);
|
|
6569
6820
|
}
|
|
@@ -6585,8 +6836,20 @@ class Publisher extends BasePeerConnection {
|
|
|
6585
6836
|
const trackType = publishOption.trackType;
|
|
6586
6837
|
this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
6587
6838
|
this.transceiverCache.add(publishOption, transceiver);
|
|
6839
|
+
this.trackIdToTrackType.set(track.id, trackType);
|
|
6588
6840
|
await this.negotiate();
|
|
6589
6841
|
};
|
|
6842
|
+
/**
|
|
6843
|
+
* Updates the transceiver with the given track and track type.
|
|
6844
|
+
*/
|
|
6845
|
+
this.updateTransceiver = async (transceiver, track, trackType) => {
|
|
6846
|
+
const sender = transceiver.sender;
|
|
6847
|
+
if (sender.track)
|
|
6848
|
+
this.trackIdToTrackType.delete(sender.track.id);
|
|
6849
|
+
await sender.replaceTrack(track);
|
|
6850
|
+
if (track)
|
|
6851
|
+
this.trackIdToTrackType.set(track.id, trackType);
|
|
6852
|
+
};
|
|
6590
6853
|
/**
|
|
6591
6854
|
* Synchronizes the current Publisher state with the provided publish options.
|
|
6592
6855
|
*/
|
|
@@ -6616,7 +6879,7 @@ class Publisher extends BasePeerConnection {
|
|
|
6616
6879
|
continue;
|
|
6617
6880
|
// it is safe to stop the track here, it is a clone
|
|
6618
6881
|
this.stopTrack(transceiver.sender.track);
|
|
6619
|
-
await
|
|
6882
|
+
await this.updateTransceiver(transceiver, null, publishOption.trackType);
|
|
6620
6883
|
}
|
|
6621
6884
|
};
|
|
6622
6885
|
/**
|
|
@@ -6636,18 +6899,6 @@ class Publisher extends BasePeerConnection {
|
|
|
6636
6899
|
}
|
|
6637
6900
|
return false;
|
|
6638
6901
|
};
|
|
6639
|
-
/**
|
|
6640
|
-
* Maps the given track ID to the corresponding track type.
|
|
6641
|
-
*/
|
|
6642
|
-
this.getTrackType = (trackId) => {
|
|
6643
|
-
for (const transceiverId of this.transceiverCache.items()) {
|
|
6644
|
-
const { publishOption, transceiver } = transceiverId;
|
|
6645
|
-
if (transceiver.sender.track?.id === trackId) {
|
|
6646
|
-
return publishOption.trackType;
|
|
6647
|
-
}
|
|
6648
|
-
}
|
|
6649
|
-
return undefined;
|
|
6650
|
-
};
|
|
6651
6902
|
/**
|
|
6652
6903
|
* Stops the cloned track that is being published to the SFU.
|
|
6653
6904
|
*/
|
|
@@ -6941,6 +7192,7 @@ class Subscriber extends BasePeerConnection {
|
|
|
6941
7192
|
if (!trackType) {
|
|
6942
7193
|
return this.logger('error', `Unknown track type: ${rawTrackType}`);
|
|
6943
7194
|
}
|
|
7195
|
+
this.trackIdToTrackType.set(e.track.id, trackType);
|
|
6944
7196
|
if (!participantToUpdate) {
|
|
6945
7197
|
this.logger('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
6946
7198
|
this.state.registerOrphanedTrack({
|
|
@@ -7284,12 +7536,22 @@ class StreamSfuClient {
|
|
|
7284
7536
|
*/
|
|
7285
7537
|
this.abortController = new AbortController();
|
|
7286
7538
|
this.createWebSocket = () => {
|
|
7539
|
+
const eventsToTrace = {
|
|
7540
|
+
callEnded: true,
|
|
7541
|
+
changePublishQuality: true,
|
|
7542
|
+
error: true,
|
|
7543
|
+
goAway: true,
|
|
7544
|
+
};
|
|
7287
7545
|
this.signalWs = createWebSocketSignalChannel({
|
|
7288
7546
|
logTag: this.logTag,
|
|
7289
7547
|
endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
|
|
7290
7548
|
onMessage: (message) => {
|
|
7291
7549
|
this.lastMessageTimestamp = new Date();
|
|
7292
7550
|
this.scheduleConnectionCheck();
|
|
7551
|
+
const eventKind = message.eventPayload.oneofKind;
|
|
7552
|
+
if (eventsToTrace[eventKind]) {
|
|
7553
|
+
this.tracer?.trace(eventKind, message);
|
|
7554
|
+
}
|
|
7293
7555
|
this.dispatcher.dispatch(message, this.logTag);
|
|
7294
7556
|
},
|
|
7295
7557
|
});
|
|
@@ -7382,7 +7644,8 @@ class StreamSfuClient {
|
|
|
7382
7644
|
};
|
|
7383
7645
|
this.sendStats = async (stats) => {
|
|
7384
7646
|
await this.joinTask;
|
|
7385
|
-
|
|
7647
|
+
// NOTE: we don't retry sending stats
|
|
7648
|
+
return this.rpc.sendStats({ ...stats, sessionId: this.sessionId });
|
|
7386
7649
|
};
|
|
7387
7650
|
this.startNoiseCancellation = async () => {
|
|
7388
7651
|
await this.joinTask;
|
|
@@ -7434,7 +7697,7 @@ class StreamSfuClient {
|
|
|
7434
7697
|
unsubscribe();
|
|
7435
7698
|
current.reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
7436
7699
|
}, this.joinResponseTimeout);
|
|
7437
|
-
|
|
7700
|
+
const joinRequest = SfuRequest.create({
|
|
7438
7701
|
requestPayload: {
|
|
7439
7702
|
oneofKind: 'joinRequest',
|
|
7440
7703
|
joinRequest: JoinRequest.create({
|
|
@@ -7443,7 +7706,9 @@ class StreamSfuClient {
|
|
|
7443
7706
|
token: this.credentials.token,
|
|
7444
7707
|
}),
|
|
7445
7708
|
},
|
|
7446
|
-
})
|
|
7709
|
+
});
|
|
7710
|
+
this.tracer?.trace('joinRequest', joinRequest);
|
|
7711
|
+
await this.send(joinRequest);
|
|
7447
7712
|
return current.promise;
|
|
7448
7713
|
};
|
|
7449
7714
|
this.ping = async () => {
|
|
@@ -7504,7 +7769,9 @@ class StreamSfuClient {
|
|
|
7504
7769
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
7505
7770
|
this.logTag = logTag;
|
|
7506
7771
|
this.logger = getLogger(['SfuClient', logTag]);
|
|
7507
|
-
this.tracer = enableTracing
|
|
7772
|
+
this.tracer = enableTracing
|
|
7773
|
+
? new Tracer(`${logTag}-${this.edgeName}`)
|
|
7774
|
+
: undefined;
|
|
7508
7775
|
this.rpc = createSignalClient({
|
|
7509
7776
|
baseUrl: server.url,
|
|
7510
7777
|
interceptors: [
|
|
@@ -7603,13 +7870,17 @@ const watchCallRejected = (call) => {
|
|
|
7603
7870
|
.every((m) => rejectedBy[m.user_id]);
|
|
7604
7871
|
if (everyoneElseRejected) {
|
|
7605
7872
|
call.logger('info', 'everyone rejected, leaving the call');
|
|
7606
|
-
await call.leave({
|
|
7873
|
+
await call.leave({
|
|
7874
|
+
reject: true,
|
|
7875
|
+
reason: 'cancel',
|
|
7876
|
+
message: 'ring: everyone rejected',
|
|
7877
|
+
});
|
|
7607
7878
|
}
|
|
7608
7879
|
}
|
|
7609
7880
|
else {
|
|
7610
7881
|
if (rejectedBy[eventCall.created_by.id]) {
|
|
7611
7882
|
call.logger('info', 'call creator rejected, leaving call');
|
|
7612
|
-
await call.leave({
|
|
7883
|
+
await call.leave({ message: 'ring: creator rejected' });
|
|
7613
7884
|
}
|
|
7614
7885
|
}
|
|
7615
7886
|
};
|
|
@@ -7623,7 +7894,7 @@ const watchCallEnded = (call) => {
|
|
|
7623
7894
|
if (callingState !== exports.CallingState.IDLE &&
|
|
7624
7895
|
callingState !== exports.CallingState.LEFT) {
|
|
7625
7896
|
call
|
|
7626
|
-
.leave({
|
|
7897
|
+
.leave({ message: 'call.ended event received', reject: false })
|
|
7627
7898
|
.catch((err) => {
|
|
7628
7899
|
call.logger('error', 'Failed to leave call after call.ended ', err);
|
|
7629
7900
|
});
|
|
@@ -7643,7 +7914,7 @@ const watchSfuCallEnded = (call) => {
|
|
|
7643
7914
|
// update the call state to reflect the call has ended.
|
|
7644
7915
|
call.state.setEndedAt(new Date());
|
|
7645
7916
|
const reason = CallEndedReason[e.reason];
|
|
7646
|
-
await call.leave({
|
|
7917
|
+
await call.leave({ message: `callEnded received: ${reason}` });
|
|
7647
7918
|
}
|
|
7648
7919
|
catch (err) {
|
|
7649
7920
|
call.logger('error', 'Failed to leave call after being ended by the SFU', err);
|
|
@@ -7710,7 +7981,7 @@ const watchLiveEnded = (dispatcher, call) => {
|
|
|
7710
7981
|
return;
|
|
7711
7982
|
call.state.setBackstage(true);
|
|
7712
7983
|
if (!call.permissionsContext.hasPermission(OwnCapability.JOIN_BACKSTAGE)) {
|
|
7713
|
-
call.leave({
|
|
7984
|
+
call.leave({ message: 'live ended' }).catch((err) => {
|
|
7714
7985
|
call.logger('error', 'Failed to leave call after live ended', err);
|
|
7715
7986
|
});
|
|
7716
7987
|
}
|
|
@@ -8400,7 +8671,6 @@ class DynascaleManager {
|
|
|
8400
8671
|
const { selectedDevice } = this.speaker.state;
|
|
8401
8672
|
if (selectedDevice && 'setSinkId' in audioElement) {
|
|
8402
8673
|
audioElement.setSinkId(selectedDevice);
|
|
8403
|
-
tracer.trace('navigator.mediaDevices.setSinkId', selectedDevice);
|
|
8404
8674
|
}
|
|
8405
8675
|
}
|
|
8406
8676
|
});
|
|
@@ -8410,7 +8680,6 @@ class DynascaleManager {
|
|
|
8410
8680
|
: this.speaker.state.selectedDevice$.subscribe((deviceId) => {
|
|
8411
8681
|
if (deviceId) {
|
|
8412
8682
|
audioElement.setSinkId(deviceId);
|
|
8413
|
-
tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
|
|
8414
8683
|
}
|
|
8415
8684
|
});
|
|
8416
8685
|
const volumeSubscription = rxjs.combineLatest([
|
|
@@ -9482,25 +9751,6 @@ class InputMediaDeviceManagerState {
|
|
|
9482
9751
|
* The default constraints for the device.
|
|
9483
9752
|
*/
|
|
9484
9753
|
this.defaultConstraints$ = this.defaultConstraintsSubject.asObservable();
|
|
9485
|
-
/**
|
|
9486
|
-
* Gets the current value of an observable, or undefined if the observable has
|
|
9487
|
-
* not emitted a value yet.
|
|
9488
|
-
*
|
|
9489
|
-
* @param observable$ the observable to get the value from.
|
|
9490
|
-
*/
|
|
9491
|
-
this.getCurrentValue = getCurrentValue;
|
|
9492
|
-
/**
|
|
9493
|
-
* Updates the value of the provided Subject.
|
|
9494
|
-
* An `update` can either be a new value or a function which takes
|
|
9495
|
-
* the current value and returns a new value.
|
|
9496
|
-
*
|
|
9497
|
-
* @internal
|
|
9498
|
-
*
|
|
9499
|
-
* @param subject the subject to update.
|
|
9500
|
-
* @param update the update to apply to the subject.
|
|
9501
|
-
* @return the updated value.
|
|
9502
|
-
*/
|
|
9503
|
-
this.setCurrentValue = setCurrentValue;
|
|
9504
9754
|
this.hasBrowserPermission$ = permission
|
|
9505
9755
|
? permission.asObservable().pipe(rxjs.shareReplay(1))
|
|
9506
9756
|
: rxjs.of(true);
|
|
@@ -9515,39 +9765,39 @@ class InputMediaDeviceManagerState {
|
|
|
9515
9765
|
* The device status
|
|
9516
9766
|
*/
|
|
9517
9767
|
get status() {
|
|
9518
|
-
return
|
|
9768
|
+
return getCurrentValue(this.status$);
|
|
9519
9769
|
}
|
|
9520
9770
|
/**
|
|
9521
9771
|
* The requested device status. Useful for optimistic UIs
|
|
9522
9772
|
*/
|
|
9523
9773
|
get optimisticStatus() {
|
|
9524
|
-
return
|
|
9774
|
+
return getCurrentValue(this.optimisticStatus$);
|
|
9525
9775
|
}
|
|
9526
9776
|
/**
|
|
9527
9777
|
* The currently selected device
|
|
9528
9778
|
*/
|
|
9529
9779
|
get selectedDevice() {
|
|
9530
|
-
return
|
|
9780
|
+
return getCurrentValue(this.selectedDevice$);
|
|
9531
9781
|
}
|
|
9532
9782
|
/**
|
|
9533
9783
|
* The current media stream, or `undefined` if the device is currently disabled.
|
|
9534
9784
|
*/
|
|
9535
9785
|
get mediaStream() {
|
|
9536
|
-
return
|
|
9786
|
+
return getCurrentValue(this.mediaStream$);
|
|
9537
9787
|
}
|
|
9538
9788
|
/**
|
|
9539
9789
|
* @internal
|
|
9540
9790
|
* @param status
|
|
9541
9791
|
*/
|
|
9542
9792
|
setStatus(status) {
|
|
9543
|
-
|
|
9793
|
+
setCurrentValue(this.statusSubject, status);
|
|
9544
9794
|
}
|
|
9545
9795
|
/**
|
|
9546
9796
|
* @internal
|
|
9547
9797
|
* @param pendingStatus
|
|
9548
9798
|
*/
|
|
9549
9799
|
setPendingStatus(pendingStatus) {
|
|
9550
|
-
|
|
9800
|
+
setCurrentValue(this.optimisticStatusSubject, pendingStatus);
|
|
9551
9801
|
}
|
|
9552
9802
|
/**
|
|
9553
9803
|
* Updates the `mediaStream` state variable.
|
|
@@ -9558,7 +9808,7 @@ class InputMediaDeviceManagerState {
|
|
|
9558
9808
|
* as this is the stream that holds the actual deviceId information.
|
|
9559
9809
|
*/
|
|
9560
9810
|
setMediaStream(stream, rootStream) {
|
|
9561
|
-
|
|
9811
|
+
setCurrentValue(this.mediaStreamSubject, stream);
|
|
9562
9812
|
if (rootStream) {
|
|
9563
9813
|
this.setDevice(this.getDeviceIdFromStream(rootStream));
|
|
9564
9814
|
}
|
|
@@ -9568,13 +9818,13 @@ class InputMediaDeviceManagerState {
|
|
|
9568
9818
|
* @param deviceId the device id to set.
|
|
9569
9819
|
*/
|
|
9570
9820
|
setDevice(deviceId) {
|
|
9571
|
-
|
|
9821
|
+
setCurrentValue(this.selectedDeviceSubject, deviceId);
|
|
9572
9822
|
}
|
|
9573
9823
|
/**
|
|
9574
9824
|
* Gets the default constraints for the device.
|
|
9575
9825
|
*/
|
|
9576
9826
|
get defaultConstraints() {
|
|
9577
|
-
return
|
|
9827
|
+
return getCurrentValue(this.defaultConstraints$);
|
|
9578
9828
|
}
|
|
9579
9829
|
/**
|
|
9580
9830
|
* Sets the default constraints for the device.
|
|
@@ -9583,7 +9833,7 @@ class InputMediaDeviceManagerState {
|
|
|
9583
9833
|
* @param constraints the constraints to set.
|
|
9584
9834
|
*/
|
|
9585
9835
|
setDefaultConstraints(constraints) {
|
|
9586
|
-
|
|
9836
|
+
setCurrentValue(this.defaultConstraintsSubject, constraints);
|
|
9587
9837
|
}
|
|
9588
9838
|
}
|
|
9589
9839
|
|
|
@@ -9601,13 +9851,13 @@ class CameraManagerState extends InputMediaDeviceManagerState {
|
|
|
9601
9851
|
* back - means the camera facing the environment
|
|
9602
9852
|
*/
|
|
9603
9853
|
get direction() {
|
|
9604
|
-
return
|
|
9854
|
+
return getCurrentValue(this.direction$);
|
|
9605
9855
|
}
|
|
9606
9856
|
/**
|
|
9607
9857
|
* @internal
|
|
9608
9858
|
*/
|
|
9609
9859
|
setDirection(direction) {
|
|
9610
|
-
|
|
9860
|
+
setCurrentValue(this.directionSubject, direction);
|
|
9611
9861
|
}
|
|
9612
9862
|
/**
|
|
9613
9863
|
* @internal
|
|
@@ -9780,13 +10030,13 @@ class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
|
9780
10030
|
* This feature is not available in the React Native SDK.
|
|
9781
10031
|
*/
|
|
9782
10032
|
get speakingWhileMuted() {
|
|
9783
|
-
return
|
|
10033
|
+
return getCurrentValue(this.speakingWhileMuted$);
|
|
9784
10034
|
}
|
|
9785
10035
|
/**
|
|
9786
10036
|
* @internal
|
|
9787
10037
|
*/
|
|
9788
10038
|
setSpeakingWhileMuted(isSpeaking) {
|
|
9789
|
-
|
|
10039
|
+
setCurrentValue(this.speakingWhileMutedSubject, isSpeaking);
|
|
9790
10040
|
}
|
|
9791
10041
|
getDeviceIdFromStream(stream) {
|
|
9792
10042
|
const [track] = stream.getAudioTracks();
|
|
@@ -10238,19 +10488,19 @@ class ScreenShareState extends InputMediaDeviceManagerState {
|
|
|
10238
10488
|
* The current screen share audio status.
|
|
10239
10489
|
*/
|
|
10240
10490
|
get audioEnabled() {
|
|
10241
|
-
return
|
|
10491
|
+
return getCurrentValue(this.audioEnabled$);
|
|
10242
10492
|
}
|
|
10243
10493
|
/**
|
|
10244
10494
|
* Set the current screen share audio status.
|
|
10245
10495
|
*/
|
|
10246
10496
|
setAudioEnabled(isEnabled) {
|
|
10247
|
-
|
|
10497
|
+
setCurrentValue(this.audioEnabledSubject, isEnabled);
|
|
10248
10498
|
}
|
|
10249
10499
|
/**
|
|
10250
10500
|
* The current screen share settings.
|
|
10251
10501
|
*/
|
|
10252
10502
|
get settings() {
|
|
10253
|
-
return
|
|
10503
|
+
return getCurrentValue(this.settings$);
|
|
10254
10504
|
}
|
|
10255
10505
|
/**
|
|
10256
10506
|
* Set the current screen share settings.
|
|
@@ -10258,7 +10508,7 @@ class ScreenShareState extends InputMediaDeviceManagerState {
|
|
|
10258
10508
|
* @param settings the screen share settings to set.
|
|
10259
10509
|
*/
|
|
10260
10510
|
setSettings(settings) {
|
|
10261
|
-
|
|
10511
|
+
setCurrentValue(this.settingsSubject, settings);
|
|
10262
10512
|
}
|
|
10263
10513
|
}
|
|
10264
10514
|
|
|
@@ -10337,25 +10587,6 @@ class SpeakerState {
|
|
|
10337
10587
|
* [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
|
|
10338
10588
|
*/
|
|
10339
10589
|
this.isDeviceSelectionSupported = checkIfAudioOutputChangeSupported();
|
|
10340
|
-
/**
|
|
10341
|
-
* Gets the current value of an observable, or undefined if the observable has
|
|
10342
|
-
* not emitted a value yet.
|
|
10343
|
-
*
|
|
10344
|
-
* @param observable$ the observable to get the value from.
|
|
10345
|
-
*/
|
|
10346
|
-
this.getCurrentValue = getCurrentValue;
|
|
10347
|
-
/**
|
|
10348
|
-
* Updates the value of the provided Subject.
|
|
10349
|
-
* An `update` can either be a new value or a function which takes
|
|
10350
|
-
* the current value and returns a new value.
|
|
10351
|
-
*
|
|
10352
|
-
* @internal
|
|
10353
|
-
*
|
|
10354
|
-
* @param subject the subject to update.
|
|
10355
|
-
* @param update the update to apply to the subject.
|
|
10356
|
-
* @return the updated value.
|
|
10357
|
-
*/
|
|
10358
|
-
this.setCurrentValue = setCurrentValue;
|
|
10359
10590
|
this.selectedDevice$ = this.selectedDeviceSubject
|
|
10360
10591
|
.asObservable()
|
|
10361
10592
|
.pipe(rxjs.distinctUntilChanged());
|
|
@@ -10369,7 +10600,7 @@ class SpeakerState {
|
|
|
10369
10600
|
* Note: this feature is not supported in React Native
|
|
10370
10601
|
*/
|
|
10371
10602
|
get selectedDevice() {
|
|
10372
|
-
return
|
|
10603
|
+
return getCurrentValue(this.selectedDevice$);
|
|
10373
10604
|
}
|
|
10374
10605
|
/**
|
|
10375
10606
|
* The currently selected volume
|
|
@@ -10377,21 +10608,22 @@ class SpeakerState {
|
|
|
10377
10608
|
* Note: this feature is not supported in React Native
|
|
10378
10609
|
*/
|
|
10379
10610
|
get volume() {
|
|
10380
|
-
return
|
|
10611
|
+
return getCurrentValue(this.volume$);
|
|
10381
10612
|
}
|
|
10382
10613
|
/**
|
|
10383
10614
|
* @internal
|
|
10384
10615
|
* @param deviceId
|
|
10385
10616
|
*/
|
|
10386
10617
|
setDevice(deviceId) {
|
|
10387
|
-
|
|
10618
|
+
setCurrentValue(this.selectedDeviceSubject, deviceId);
|
|
10619
|
+
tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
|
|
10388
10620
|
}
|
|
10389
10621
|
/**
|
|
10390
10622
|
* @internal
|
|
10391
10623
|
* @param volume
|
|
10392
10624
|
*/
|
|
10393
10625
|
setVolume(volume) {
|
|
10394
|
-
|
|
10626
|
+
setCurrentValue(this.volumeSubject, volume);
|
|
10395
10627
|
}
|
|
10396
10628
|
}
|
|
10397
10629
|
|
|
@@ -10572,7 +10804,7 @@ class Call {
|
|
|
10572
10804
|
const currentUserId = this.currentUserId;
|
|
10573
10805
|
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
10574
10806
|
this.logger('info', 'Leaving call because of being blocked');
|
|
10575
|
-
await this.leave({
|
|
10807
|
+
await this.leave({ message: 'user blocked' }).catch((err) => {
|
|
10576
10808
|
this.logger('error', 'Error leaving call after being blocked', err);
|
|
10577
10809
|
});
|
|
10578
10810
|
}
|
|
@@ -10725,7 +10957,7 @@ class Call {
|
|
|
10725
10957
|
/**
|
|
10726
10958
|
* Leave the call and stop the media streams that were published by the call.
|
|
10727
10959
|
*/
|
|
10728
|
-
this.leave = async ({ reject, reason
|
|
10960
|
+
this.leave = async ({ reject, reason, message } = {}) => {
|
|
10729
10961
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
10730
10962
|
const callingState = this.state.callingState;
|
|
10731
10963
|
if (callingState === exports.CallingState.LEFT) {
|
|
@@ -10743,7 +10975,7 @@ class Call {
|
|
|
10743
10975
|
}
|
|
10744
10976
|
if (callingState === exports.CallingState.RINGING && reject !== false) {
|
|
10745
10977
|
if (reject) {
|
|
10746
|
-
await this.reject('decline');
|
|
10978
|
+
await this.reject(reason ?? 'decline');
|
|
10747
10979
|
}
|
|
10748
10980
|
else {
|
|
10749
10981
|
// if reject was undefined, we still have to cancel the call automatically
|
|
@@ -10762,7 +10994,7 @@ class Call {
|
|
|
10762
10994
|
this.subscriber = undefined;
|
|
10763
10995
|
this.publisher?.dispose();
|
|
10764
10996
|
this.publisher = undefined;
|
|
10765
|
-
await this.sfuClient?.leaveAndClose(reason);
|
|
10997
|
+
await this.sfuClient?.leaveAndClose(message ?? reason ?? 'user is leaving the call');
|
|
10766
10998
|
this.sfuClient = undefined;
|
|
10767
10999
|
this.dynascaleManager.setSfuClient(undefined);
|
|
10768
11000
|
this.state.setCallingState(exports.CallingState.LEFT);
|
|
@@ -10772,6 +11004,7 @@ class Call {
|
|
|
10772
11004
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
10773
11005
|
this.initialized = false;
|
|
10774
11006
|
this.hasJoinedOnce = false;
|
|
11007
|
+
this.unifiedSessionId = undefined;
|
|
10775
11008
|
this.ringingSubject.next(false);
|
|
10776
11009
|
this.cancelAutoDrop();
|
|
10777
11010
|
this.clientStore.unregisterCall(this);
|
|
@@ -11199,7 +11432,6 @@ class Call {
|
|
|
11199
11432
|
state: this.state,
|
|
11200
11433
|
connectionConfig,
|
|
11201
11434
|
logTag: String(this.sfuClientTag),
|
|
11202
|
-
clientDetails,
|
|
11203
11435
|
enableTracing,
|
|
11204
11436
|
onUnrecoverableError: (reason) => {
|
|
11205
11437
|
this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
|
|
@@ -11221,7 +11453,6 @@ class Call {
|
|
|
11221
11453
|
connectionConfig,
|
|
11222
11454
|
publishOptions,
|
|
11223
11455
|
logTag: String(this.sfuClientTag),
|
|
11224
|
-
clientDetails,
|
|
11225
11456
|
enableTracing,
|
|
11226
11457
|
onUnrecoverableError: (reason) => {
|
|
11227
11458
|
this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
|
|
@@ -11240,6 +11471,7 @@ class Call {
|
|
|
11240
11471
|
});
|
|
11241
11472
|
this.sfuStatsReporter?.stop();
|
|
11242
11473
|
if (statsOptions?.reporting_interval_ms > 0) {
|
|
11474
|
+
this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
|
|
11243
11475
|
this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
|
|
11244
11476
|
clientDetails,
|
|
11245
11477
|
options: statsOptions,
|
|
@@ -11248,6 +11480,7 @@ class Call {
|
|
|
11248
11480
|
microphone: this.microphone,
|
|
11249
11481
|
camera: this.camera,
|
|
11250
11482
|
state: this.state,
|
|
11483
|
+
unifiedSessionId: this.unifiedSessionId,
|
|
11251
11484
|
});
|
|
11252
11485
|
this.sfuStatsReporter.start();
|
|
11253
11486
|
}
|
|
@@ -11469,7 +11702,7 @@ class Call {
|
|
|
11469
11702
|
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
|
|
11470
11703
|
return;
|
|
11471
11704
|
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
11472
|
-
this.leave({
|
|
11705
|
+
this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
|
|
11473
11706
|
this.logger('warn', `Can't leave call after disconnect request`, err);
|
|
11474
11707
|
});
|
|
11475
11708
|
}
|
|
@@ -11617,6 +11850,11 @@ class Call {
|
|
|
11617
11850
|
trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
11618
11851
|
}
|
|
11619
11852
|
}
|
|
11853
|
+
if (track.kind === 'video') {
|
|
11854
|
+
// schedules calibration report - the SFU will use the performance stats
|
|
11855
|
+
// to adjust the quality thresholds as early as possible
|
|
11856
|
+
this.sfuStatsReporter?.scheduleOne(3000);
|
|
11857
|
+
}
|
|
11620
11858
|
await this.updateLocalStreamState(mediaStream, ...trackTypes);
|
|
11621
11859
|
};
|
|
11622
11860
|
/**
|
|
@@ -12099,7 +12337,11 @@ class Call {
|
|
|
12099
12337
|
// e.g. it was already accepted and joined
|
|
12100
12338
|
if (this.state.callingState !== exports.CallingState.RINGING)
|
|
12101
12339
|
return;
|
|
12102
|
-
this.leave({
|
|
12340
|
+
this.leave({
|
|
12341
|
+
reject: true,
|
|
12342
|
+
reason: 'timeout',
|
|
12343
|
+
message: `ringing timeout - ${this.isCreatedByMe ? 'no one accepted' : `user didn't interact with incoming call screen`}`,
|
|
12344
|
+
}).catch((err) => {
|
|
12103
12345
|
this.logger('error', 'Failed to drop call', err);
|
|
12104
12346
|
});
|
|
12105
12347
|
}, timeoutInMs);
|
|
@@ -13472,7 +13714,7 @@ class StreamClient {
|
|
|
13472
13714
|
this.getUserAgent = () => {
|
|
13473
13715
|
if (!this.cachedUserAgent) {
|
|
13474
13716
|
const { clientAppIdentifier = {} } = this.options;
|
|
13475
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
13717
|
+
const { sdkName = 'js', sdkVersion = "1.21.0", ...extras } = clientAppIdentifier;
|
|
13476
13718
|
this.cachedUserAgent = [
|
|
13477
13719
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
13478
13720
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|