@stream-io/video-client 1.61.1 → 1.61.2
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 +9 -0
- package/dist/index.browser.es.js +281 -152
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +281 -152
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +281 -152
- package/dist/index.es.js.map +1 -1
- package/dist/src/coordinator/connection/types.d.ts +1 -1
- package/dist/src/devices/DeviceManager.d.ts +6 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +0 -27
- package/dist/src/helpers/AudioBindingsWatchdog.d.ts +7 -2
- package/dist/src/helpers/DynascaleManager.d.ts +15 -0
- package/dist/src/helpers/MediaPlaybackWatchdog.d.ts +18 -0
- package/dist/src/helpers/mediaInterruptions.d.ts +14 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +1 -1
- package/dist/src/ringing/reconcileRingState.d.ts +8 -7
- package/dist/src/stats/SfuStatsReporter.d.ts +1 -13
- package/dist/src/stats/rtc/StatsTracer.d.ts +6 -6
- package/dist/src/stats/rtc/types.d.ts +0 -11
- package/package.json +1 -1
- package/src/Call.ts +0 -3
- package/src/StreamVideoClient.ts +8 -0
- package/src/__tests__/StreamVideoClient.rewatch.test.ts +136 -0
- package/src/coordinator/connection/types.ts +1 -1
- package/src/devices/DeviceManager.ts +20 -1
- package/src/devices/__tests__/CameraManager.test.ts +50 -1
- package/src/devices/__tests__/mediaStreamTestHelpers.ts +10 -6
- package/src/gen/video/sfu/signal_rpc/signal.ts +0 -53
- package/src/helpers/AudioBindingsWatchdog.ts +91 -35
- package/src/helpers/DynascaleManager.ts +84 -9
- package/src/helpers/MediaPlaybackWatchdog.ts +55 -6
- package/src/helpers/__tests__/DynascaleManager.test.ts +247 -9
- package/src/helpers/__tests__/MediaPlaybackWatchdog.test.ts +140 -0
- package/src/helpers/__tests__/mediaInterruptions.test.ts +131 -0
- package/src/helpers/mediaInterruptions.ts +37 -0
- package/src/reporting/ClientEventReporter.ts +1 -1
- package/src/ringing/RingStatePoller.ts +1 -1
- package/src/ringing/__tests__/RingStatePoller.test.ts +1 -1
- package/src/ringing/__tests__/reconcileRingState.test.ts +33 -0
- package/src/ringing/reconcileRingState.ts +18 -10
- package/src/stats/SfuStatsReporter.ts +19 -93
- package/src/stats/__tests__/SfuStatsReporter.test.ts +2 -18
- package/src/stats/rtc/StatsTracer.ts +6 -6
- package/src/stats/rtc/types.ts +0 -13
package/dist/index.cjs.js
CHANGED
|
@@ -2685,18 +2685,6 @@ class SendStatsRequest$Type extends runtime.MessageType {
|
|
|
2685
2685
|
constructor() {
|
|
2686
2686
|
super('stream.video.sfu.signal.SendStatsRequest', [
|
|
2687
2687
|
{ no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2688
|
-
{
|
|
2689
|
-
no: 2,
|
|
2690
|
-
name: 'subscriber_stats',
|
|
2691
|
-
kind: 'scalar',
|
|
2692
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2693
|
-
},
|
|
2694
|
-
{
|
|
2695
|
-
no: 3,
|
|
2696
|
-
name: 'publisher_stats',
|
|
2697
|
-
kind: 'scalar',
|
|
2698
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2699
|
-
},
|
|
2700
2688
|
{
|
|
2701
2689
|
no: 4,
|
|
2702
2690
|
name: 'webrtc_version',
|
|
@@ -2710,8 +2698,6 @@ class SendStatsRequest$Type extends runtime.MessageType {
|
|
|
2710
2698
|
kind: 'scalar',
|
|
2711
2699
|
T: 9 /*ScalarType.STRING*/,
|
|
2712
2700
|
},
|
|
2713
|
-
{ no: 7, name: 'audio_devices', kind: 'message', T: () => InputDevices },
|
|
2714
|
-
{ no: 8, name: 'video_devices', kind: 'message', T: () => InputDevices },
|
|
2715
2701
|
{
|
|
2716
2702
|
no: 9,
|
|
2717
2703
|
name: 'android',
|
|
@@ -2728,18 +2714,6 @@ class SendStatsRequest$Type extends runtime.MessageType {
|
|
|
2728
2714
|
},
|
|
2729
2715
|
{ no: 11, name: 'telemetry', kind: 'message', T: () => Telemetry },
|
|
2730
2716
|
{ no: 12, name: 'rtmp', kind: 'message', T: () => RTMPIngress },
|
|
2731
|
-
{
|
|
2732
|
-
no: 13,
|
|
2733
|
-
name: 'subscriber_rtc_stats',
|
|
2734
|
-
kind: 'scalar',
|
|
2735
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2736
|
-
},
|
|
2737
|
-
{
|
|
2738
|
-
no: 14,
|
|
2739
|
-
name: 'publisher_rtc_stats',
|
|
2740
|
-
kind: 'scalar',
|
|
2741
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2742
|
-
},
|
|
2743
2717
|
{ no: 15, name: 'rtc_stats', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2744
2718
|
{
|
|
2745
2719
|
no: 16,
|
|
@@ -5101,7 +5075,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5101
5075
|
}
|
|
5102
5076
|
};
|
|
5103
5077
|
|
|
5104
|
-
const version = "1.61.
|
|
5078
|
+
const version = "1.61.2";
|
|
5105
5079
|
const [major, minor, patch] = version.split('.');
|
|
5106
5080
|
let sdkInfo = {
|
|
5107
5081
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -7829,30 +7803,21 @@ class SfuStatsReporter {
|
|
|
7829
7803
|
sfuClient;
|
|
7830
7804
|
subscriber;
|
|
7831
7805
|
publisher;
|
|
7832
|
-
microphone;
|
|
7833
|
-
camera;
|
|
7834
|
-
state;
|
|
7835
7806
|
tracer;
|
|
7836
7807
|
unifiedSessionId;
|
|
7837
7808
|
intervalId;
|
|
7838
7809
|
timeoutId;
|
|
7839
7810
|
reportCount = 0;
|
|
7840
|
-
unsubscribeDevicePermissionsSubscription;
|
|
7841
|
-
unsubscribeListDevicesSubscription;
|
|
7842
7811
|
sdkName;
|
|
7843
7812
|
sdkVersion;
|
|
7844
7813
|
webRTCVersion;
|
|
7845
|
-
inputDevices = new Map();
|
|
7846
7814
|
statsConcurrencyTag = Symbol('sfuStatsReporter');
|
|
7847
7815
|
isStopped = false;
|
|
7848
|
-
constructor(sfuClient, { options, clientDetails, subscriber, publisher,
|
|
7816
|
+
constructor(sfuClient, { options, clientDetails, subscriber, publisher, tracer, unifiedSessionId, }) {
|
|
7849
7817
|
this.sfuClient = sfuClient;
|
|
7850
7818
|
this.options = options;
|
|
7851
7819
|
this.subscriber = subscriber;
|
|
7852
7820
|
this.publisher = publisher;
|
|
7853
|
-
this.microphone = microphone;
|
|
7854
|
-
this.camera = camera;
|
|
7855
|
-
this.state = state;
|
|
7856
7821
|
this.tracer = tracer;
|
|
7857
7822
|
this.unifiedSessionId = unifiedSessionId;
|
|
7858
7823
|
const { sdk, browser } = clientDetails;
|
|
@@ -7866,35 +7831,6 @@ class SfuStatsReporter {
|
|
|
7866
7831
|
`${browser?.name || ''}-${browser?.version || ''}` ||
|
|
7867
7832
|
'N/A';
|
|
7868
7833
|
}
|
|
7869
|
-
observeDevice = (device, kind) => {
|
|
7870
|
-
const { browserPermissionState$ } = device.state;
|
|
7871
|
-
this.unsubscribeDevicePermissionsSubscription?.();
|
|
7872
|
-
this.unsubscribeDevicePermissionsSubscription = createSubscription(rxjs.combineLatest([browserPermissionState$, this.state.ownCapabilities$]), ([browserPermissionState, ownCapabilities]) => {
|
|
7873
|
-
// cleanup the previous listDevices() subscription in case
|
|
7874
|
-
// permissions or capabilities have changed.
|
|
7875
|
-
// we will subscribe again if everything is in order.
|
|
7876
|
-
this.unsubscribeListDevicesSubscription?.();
|
|
7877
|
-
const hasCapability = kind === 'mic'
|
|
7878
|
-
? ownCapabilities.includes(OwnCapability.SEND_AUDIO)
|
|
7879
|
-
: ownCapabilities.includes(OwnCapability.SEND_VIDEO);
|
|
7880
|
-
if (browserPermissionState !== 'granted' || !hasCapability) {
|
|
7881
|
-
this.inputDevices.set(kind, {
|
|
7882
|
-
currentDevice: '',
|
|
7883
|
-
availableDevices: [],
|
|
7884
|
-
isPermitted: false,
|
|
7885
|
-
});
|
|
7886
|
-
return;
|
|
7887
|
-
}
|
|
7888
|
-
this.unsubscribeListDevicesSubscription = createSubscription(rxjs.combineLatest([device.listDevices(), device.state.selectedDevice$]), ([devices, deviceId]) => {
|
|
7889
|
-
const selected = devices.find((d) => d.deviceId === deviceId);
|
|
7890
|
-
this.inputDevices.set(kind, {
|
|
7891
|
-
currentDevice: selected?.label || deviceId || '',
|
|
7892
|
-
availableDevices: devices.map((d) => d.label),
|
|
7893
|
-
isPermitted: true,
|
|
7894
|
-
});
|
|
7895
|
-
});
|
|
7896
|
-
});
|
|
7897
|
-
};
|
|
7898
7834
|
sendConnectionTime = (connectionTimeSeconds) => {
|
|
7899
7835
|
this.sendTelemetryData({
|
|
7900
7836
|
data: {
|
|
@@ -7929,7 +7865,7 @@ class SfuStatsReporter {
|
|
|
7929
7865
|
this.subscriber.stats.takeSample(),
|
|
7930
7866
|
this.publisher?.stats.takeSample(),
|
|
7931
7867
|
]);
|
|
7932
|
-
send = (
|
|
7868
|
+
send = (subscriberPerfStats, publisherPerfStats, telemetry) => {
|
|
7933
7869
|
// serialize sends so overlapping ones can't race on the trace buffers or
|
|
7934
7870
|
// deliver an older delta after a newer one already succeeded. Not gated by
|
|
7935
7871
|
// `isStopped`: an explicit final flush must still deliver after stop().
|
|
@@ -7947,8 +7883,8 @@ class SfuStatsReporter {
|
|
|
7947
7883
|
this.subscriber.stats.clearPendingDeltas();
|
|
7948
7884
|
}
|
|
7949
7885
|
let pubPending = [];
|
|
7950
|
-
if (pubTracer &&
|
|
7951
|
-
pubPending = this.publisher
|
|
7886
|
+
if (pubTracer && this.publisher) {
|
|
7887
|
+
pubPending = this.publisher.stats.getPendingDeltas();
|
|
7952
7888
|
}
|
|
7953
7889
|
else {
|
|
7954
7890
|
this.publisher?.stats.clearPendingDeltas();
|
|
@@ -7970,17 +7906,9 @@ class SfuStatsReporter {
|
|
|
7970
7906
|
sdk: this.sdkName,
|
|
7971
7907
|
sdkVersion: this.sdkVersion,
|
|
7972
7908
|
webrtcVersion: this.webRTCVersion,
|
|
7973
|
-
subscriberStats: JSON.stringify(flatten(subscriberStats.stats)),
|
|
7974
|
-
publisherStats: publisherStats
|
|
7975
|
-
? JSON.stringify(flatten(publisherStats.stats))
|
|
7976
|
-
: '[]',
|
|
7977
|
-
subscriberRtcStats: '',
|
|
7978
|
-
publisherRtcStats: '',
|
|
7979
7909
|
rtcStats: JSON.stringify(traces),
|
|
7980
|
-
encodeStats:
|
|
7981
|
-
decodeStats:
|
|
7982
|
-
audioDevices: this.inputDevices.get('mic'),
|
|
7983
|
-
videoDevices: this.inputDevices.get('camera'),
|
|
7910
|
+
encodeStats: publisherPerfStats ?? [],
|
|
7911
|
+
decodeStats: subscriberPerfStats,
|
|
7984
7912
|
unifiedSessionId: this.unifiedSessionId,
|
|
7985
7913
|
deviceState: getDeviceState(),
|
|
7986
7914
|
telemetry,
|
|
@@ -7994,8 +7922,8 @@ class SfuStatsReporter {
|
|
|
7994
7922
|
// delivery confirmed: advance the delivery baseline for each chain.
|
|
7995
7923
|
if (subTracer)
|
|
7996
7924
|
this.subscriber.stats.commitDeltas(subPending);
|
|
7997
|
-
if (pubTracer &&
|
|
7998
|
-
this.publisher
|
|
7925
|
+
if (pubTracer && this.publisher) {
|
|
7926
|
+
this.publisher.stats.commitDeltas(pubPending);
|
|
7999
7927
|
}
|
|
8000
7928
|
}
|
|
8001
7929
|
catch (err) {
|
|
@@ -8017,8 +7945,8 @@ class SfuStatsReporter {
|
|
|
8017
7945
|
run = async (telemetry) => {
|
|
8018
7946
|
if (this.isStopped)
|
|
8019
7947
|
return;
|
|
8020
|
-
const [
|
|
8021
|
-
await this.send(
|
|
7948
|
+
const [subscriberPerfStats, publisherPerfStats] = await this.sample();
|
|
7949
|
+
await this.send(subscriberPerfStats, publisherPerfStats, telemetry);
|
|
8022
7950
|
};
|
|
8023
7951
|
scheduleNextReport = () => {
|
|
8024
7952
|
const intervals = [1500, 3000, 3000, 5000];
|
|
@@ -8039,8 +7967,6 @@ class SfuStatsReporter {
|
|
|
8039
7967
|
start = () => {
|
|
8040
7968
|
if (this.options.reporting_interval_ms <= 0)
|
|
8041
7969
|
return;
|
|
8042
|
-
this.observeDevice(this.microphone, 'mic');
|
|
8043
|
-
this.observeDevice(this.camera, 'camera');
|
|
8044
7970
|
this.isStopped = false;
|
|
8045
7971
|
this.reportCount = 0;
|
|
8046
7972
|
clearInterval(this.intervalId);
|
|
@@ -8049,11 +7975,6 @@ class SfuStatsReporter {
|
|
|
8049
7975
|
};
|
|
8050
7976
|
stop = () => {
|
|
8051
7977
|
this.isStopped = true;
|
|
8052
|
-
this.unsubscribeDevicePermissionsSubscription?.();
|
|
8053
|
-
this.unsubscribeDevicePermissionsSubscription = undefined;
|
|
8054
|
-
this.unsubscribeListDevicesSubscription?.();
|
|
8055
|
-
this.unsubscribeListDevicesSubscription = undefined;
|
|
8056
|
-
this.inputDevices.clear();
|
|
8057
7978
|
clearInterval(this.intervalId);
|
|
8058
7979
|
this.intervalId = undefined;
|
|
8059
7980
|
clearTimeout(this.timeoutId);
|
|
@@ -8074,8 +7995,8 @@ class SfuStatsReporter {
|
|
|
8074
7995
|
return;
|
|
8075
7996
|
try {
|
|
8076
7997
|
const [sample] = await timeboxed([this.sample()], 2000);
|
|
8077
|
-
const [
|
|
8078
|
-
this.send(
|
|
7998
|
+
const [subscriberPerfStats, publisherPerfStats] = sample;
|
|
7999
|
+
this.send(subscriberPerfStats, publisherPerfStats).catch((err) => {
|
|
8079
8000
|
this.logger.warn('Failed to flush report stats', err);
|
|
8080
8001
|
});
|
|
8081
8002
|
}
|
|
@@ -8202,9 +8123,9 @@ class StatsTracer {
|
|
|
8202
8123
|
this.maxPendingDeltas = maxPendingDeltas;
|
|
8203
8124
|
}
|
|
8204
8125
|
/**
|
|
8205
|
-
* Samples the RTCPeerConnection: returns the
|
|
8206
|
-
*
|
|
8207
|
-
*
|
|
8126
|
+
* Samples the RTCPeerConnection: returns the derived performance stats and
|
|
8127
|
+
* appends the delta-compressed sample to the un-acked delivery chain
|
|
8128
|
+
* (retrieved via `getPendingDeltas`).
|
|
8208
8129
|
*
|
|
8209
8130
|
* @internal
|
|
8210
8131
|
*/
|
|
@@ -8229,7 +8150,7 @@ class StatsTracer {
|
|
|
8229
8150
|
this.pendingDeltas.push({ delta, ts: now });
|
|
8230
8151
|
this.frameTimeHistory = this.frameTimeHistory.slice(-2);
|
|
8231
8152
|
this.fpsHistory = this.fpsHistory.slice(-2);
|
|
8232
|
-
return
|
|
8153
|
+
return performanceStats;
|
|
8233
8154
|
});
|
|
8234
8155
|
};
|
|
8235
8156
|
/**
|
|
@@ -10778,15 +10699,16 @@ class StreamSfuClient {
|
|
|
10778
10699
|
/**
|
|
10779
10700
|
* Decides what a ringing call should do next, based on the current call state.
|
|
10780
10701
|
*
|
|
10781
|
-
* The `call.accepted
|
|
10782
|
-
*
|
|
10783
|
-
* handlers rely on `CallState.updateFromEvent`, which runs
|
|
10784
|
-
* poller applies the polled ring state itself
|
|
10702
|
+
* The `call.accepted` and `call.rejected` handlers, the ring state poller and
|
|
10703
|
+
* the rewatch after a WS reconnect all run this. They differ only in how the
|
|
10704
|
+
* state got there: the handlers rely on `CallState.updateFromEvent`, which runs
|
|
10705
|
+
* before them, while the poller applies the polled ring state itself and the
|
|
10706
|
+
* rewatch applies the queried call state.
|
|
10785
10707
|
*
|
|
10786
10708
|
* @param call the call to reconcile.
|
|
10787
|
-
* @param joinSource
|
|
10788
|
-
*
|
|
10789
|
-
*
|
|
10709
|
+
* @param joinSource what triggered this run, reported on the caller's join:
|
|
10710
|
+
* `ring-ws` for the event handlers, `ring-poll-api` for the poller and the
|
|
10711
|
+
* rewatch (both come from an API response).
|
|
10790
10712
|
* @returns whether the ring reached a terminal state. A failed join is not
|
|
10791
10713
|
* terminal: the caller should keep trying while the ring is open.
|
|
10792
10714
|
*/
|
|
@@ -10841,10 +10763,16 @@ const reconcileAsCaller = async (call, joinSource) => {
|
|
|
10841
10763
|
return false;
|
|
10842
10764
|
};
|
|
10843
10765
|
// the current user's own accept or reject, on this or another device, is
|
|
10844
|
-
// handled by `resolveOwnRingOutcome`, and `call.ended` by
|
|
10766
|
+
// handled by `resolveOwnRingOutcome`, and a live `call.ended` by
|
|
10767
|
+
// `watchCallEnded`. An end missed while offline is handled here.
|
|
10845
10768
|
const reconcileAsCallee = async (call) => {
|
|
10846
|
-
const
|
|
10847
|
-
|
|
10769
|
+
const { session, endedAt, createdBy } = call.state;
|
|
10770
|
+
if (endedAt || session?.ended_at) {
|
|
10771
|
+
call.logger.info('ring: the call has ended, leaving');
|
|
10772
|
+
return leave(call, { reason: 'ended', message: 'ring: call ended' });
|
|
10773
|
+
}
|
|
10774
|
+
const createdById = createdBy?.id;
|
|
10775
|
+
const rejectedBy = session?.rejected_by ?? {};
|
|
10848
10776
|
if (createdById && rejectedBy[createdById]) {
|
|
10849
10777
|
call.logger.info('ring: the caller cancelled, leaving');
|
|
10850
10778
|
return leave(call, {
|
|
@@ -10893,7 +10821,7 @@ class RingStatePoller {
|
|
|
10893
10821
|
unsubscribe = [];
|
|
10894
10822
|
constructor(call, options = {}) {
|
|
10895
10823
|
this.call = call;
|
|
10896
|
-
this.startAfterMs = options.startAfterMs ??
|
|
10824
|
+
this.startAfterMs = options.startAfterMs ?? 9_000;
|
|
10897
10825
|
this.intervalMs = options.intervalMs ?? 5_000;
|
|
10898
10826
|
}
|
|
10899
10827
|
/**
|
|
@@ -11558,12 +11486,33 @@ const registerRingingCallEventHandlers = (call) => {
|
|
|
11558
11486
|
};
|
|
11559
11487
|
|
|
11560
11488
|
const toBindingKey = (sessionId, trackType = 'audioTrack') => `${sessionId}/${trackType}`;
|
|
11489
|
+
const toElementState = (element) => {
|
|
11490
|
+
const stream = element.srcObject;
|
|
11491
|
+
const [track] = stream?.getAudioTracks?.() ?? [];
|
|
11492
|
+
return {
|
|
11493
|
+
paused: element.paused,
|
|
11494
|
+
muted: element.muted,
|
|
11495
|
+
volume: Math.round(element.volume * 100) / 100,
|
|
11496
|
+
readyState: element.readyState,
|
|
11497
|
+
sinkId: element.sinkId,
|
|
11498
|
+
track: track
|
|
11499
|
+
? {
|
|
11500
|
+
enabled: track.enabled,
|
|
11501
|
+
muted: track.muted,
|
|
11502
|
+
readyState: track.readyState,
|
|
11503
|
+
}
|
|
11504
|
+
: null,
|
|
11505
|
+
};
|
|
11506
|
+
};
|
|
11561
11507
|
/**
|
|
11562
11508
|
* Tracks audio element bindings and periodically warns about
|
|
11563
11509
|
* remote participants whose audio streams have no bound element.
|
|
11510
|
+
* On the same tick, it traces the playback state of the bound elements,
|
|
11511
|
+
* limited to the bindings whose state changed since the previous trace.
|
|
11564
11512
|
*/
|
|
11565
11513
|
class AudioBindingsWatchdog {
|
|
11566
11514
|
bindings = new Map();
|
|
11515
|
+
tracedStates = new Map();
|
|
11567
11516
|
enabled = true;
|
|
11568
11517
|
watchdogInterval;
|
|
11569
11518
|
unsubscribeCallingState;
|
|
@@ -11574,8 +11523,6 @@ class AudioBindingsWatchdog {
|
|
|
11574
11523
|
this.tracer = tracer;
|
|
11575
11524
|
this.state = state;
|
|
11576
11525
|
this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
|
|
11577
|
-
if (!this.enabled)
|
|
11578
|
-
return;
|
|
11579
11526
|
if (callingState !== exports.CallingState.JOINED) {
|
|
11580
11527
|
this.stop();
|
|
11581
11528
|
}
|
|
@@ -11604,15 +11551,15 @@ class AudioBindingsWatchdog {
|
|
|
11604
11551
|
this.bindings.delete(toBindingKey(sessionId, trackType));
|
|
11605
11552
|
};
|
|
11606
11553
|
/**
|
|
11607
|
-
* Enables or disables the
|
|
11608
|
-
*
|
|
11554
|
+
* Enables or disables the dangling binding warnings.
|
|
11555
|
+
* Bindings are still tracked and their playback state is still traced.
|
|
11609
11556
|
*/
|
|
11610
11557
|
setEnabled = (enabled) => {
|
|
11611
11558
|
this.enabled = enabled;
|
|
11612
11559
|
if (enabled) {
|
|
11613
11560
|
this.start();
|
|
11614
11561
|
}
|
|
11615
|
-
else {
|
|
11562
|
+
else if (this.state.callingState !== exports.CallingState.JOINED) {
|
|
11616
11563
|
this.stop();
|
|
11617
11564
|
}
|
|
11618
11565
|
};
|
|
@@ -11622,33 +11569,61 @@ class AudioBindingsWatchdog {
|
|
|
11622
11569
|
dispose = () => {
|
|
11623
11570
|
this.stop();
|
|
11624
11571
|
this.bindings.clear();
|
|
11572
|
+
this.tracedStates.clear();
|
|
11625
11573
|
this.unsubscribeCallingState();
|
|
11626
11574
|
};
|
|
11627
11575
|
start = () => {
|
|
11628
11576
|
clearInterval(this.watchdogInterval);
|
|
11629
11577
|
this.watchdogInterval = setInterval(() => {
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11578
|
+
if (this.enabled)
|
|
11579
|
+
this.warnAboutDanglingBindings();
|
|
11580
|
+
this.traceBindingStates();
|
|
11581
|
+
}, 3000);
|
|
11582
|
+
};
|
|
11583
|
+
warnAboutDanglingBindings = () => {
|
|
11584
|
+
const danglingUserIds = [];
|
|
11585
|
+
for (const p of this.state.participants) {
|
|
11586
|
+
if (p.isLocalParticipant)
|
|
11587
|
+
continue;
|
|
11588
|
+
const { audioStream, screenShareAudioStream, sessionId, userId, publishedTracks, } = p;
|
|
11589
|
+
if (audioStream &&
|
|
11590
|
+
publishedTracks.includes(TrackType.AUDIO) &&
|
|
11591
|
+
!this.bindings.has(toBindingKey(sessionId))) {
|
|
11592
|
+
danglingUserIds.push(userId);
|
|
11645
11593
|
}
|
|
11646
|
-
if (
|
|
11647
|
-
|
|
11648
|
-
this.
|
|
11649
|
-
|
|
11594
|
+
if (screenShareAudioStream &&
|
|
11595
|
+
publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
|
|
11596
|
+
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
|
|
11597
|
+
danglingUserIds.push(userId);
|
|
11650
11598
|
}
|
|
11651
|
-
}
|
|
11599
|
+
}
|
|
11600
|
+
if (danglingUserIds.length > 0) {
|
|
11601
|
+
const key = 'audioBinding.danglingWarning';
|
|
11602
|
+
this.tracer.traceOnce(key, key, danglingUserIds);
|
|
11603
|
+
this.logger.warn(`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`);
|
|
11604
|
+
}
|
|
11605
|
+
};
|
|
11606
|
+
traceBindingStates = () => {
|
|
11607
|
+
const changes = {};
|
|
11608
|
+
let hasChanges = false;
|
|
11609
|
+
for (const [key, element] of this.bindings) {
|
|
11610
|
+
const state = toElementState(element);
|
|
11611
|
+
const serialized = JSON.stringify(state);
|
|
11612
|
+
if (this.tracedStates.get(key) === serialized)
|
|
11613
|
+
continue;
|
|
11614
|
+
this.tracedStates.set(key, serialized);
|
|
11615
|
+
changes[key] = state;
|
|
11616
|
+
hasChanges = true;
|
|
11617
|
+
}
|
|
11618
|
+
for (const key of this.tracedStates.keys()) {
|
|
11619
|
+
if (this.bindings.has(key))
|
|
11620
|
+
continue;
|
|
11621
|
+
this.tracedStates.delete(key);
|
|
11622
|
+
changes[key] = null;
|
|
11623
|
+
hasChanges = true;
|
|
11624
|
+
}
|
|
11625
|
+
if (hasChanges)
|
|
11626
|
+
this.tracer.trace('audioBinding.state', changes);
|
|
11652
11627
|
};
|
|
11653
11628
|
stop = () => {
|
|
11654
11629
|
clearInterval(this.watchdogInterval);
|
|
@@ -11927,7 +11902,9 @@ class TrackSubscriptionManager {
|
|
|
11927
11902
|
class MediaPlaybackWatchdog {
|
|
11928
11903
|
logger = videoLoggerSystem.getLogger('MediaPlaybackWatchdog');
|
|
11929
11904
|
kind;
|
|
11905
|
+
traceInfo;
|
|
11930
11906
|
isBlocked;
|
|
11907
|
+
onGiveUp;
|
|
11931
11908
|
element;
|
|
11932
11909
|
tracer;
|
|
11933
11910
|
controller = new AbortController();
|
|
@@ -11939,8 +11916,14 @@ class MediaPlaybackWatchdog {
|
|
|
11939
11916
|
constructor(opts) {
|
|
11940
11917
|
this.element = opts.element;
|
|
11941
11918
|
this.kind = opts.kind;
|
|
11919
|
+
this.traceInfo = {
|
|
11920
|
+
kind: opts.kind,
|
|
11921
|
+
sessionId: opts.sessionId,
|
|
11922
|
+
trackType: opts.trackType,
|
|
11923
|
+
};
|
|
11942
11924
|
this.tracer = opts.tracer;
|
|
11943
11925
|
this.isBlocked = opts.isBlocked ?? (() => false);
|
|
11926
|
+
this.onGiveUp = opts.onGiveUp ?? (() => { });
|
|
11944
11927
|
this.attach();
|
|
11945
11928
|
}
|
|
11946
11929
|
attach = () => {
|
|
@@ -11958,6 +11941,23 @@ class MediaPlaybackWatchdog {
|
|
|
11958
11941
|
this.controller.abort();
|
|
11959
11942
|
this.clearTimers();
|
|
11960
11943
|
};
|
|
11944
|
+
/**
|
|
11945
|
+
* Starts a fresh recovery cycle, e.g. after a media interruption ended.
|
|
11946
|
+
* Unlike a regular pause-triggered recovery, it also tries elements that
|
|
11947
|
+
* were skipped as blocked or not ready, since those conditions may have
|
|
11948
|
+
* cleared together with the interruption.
|
|
11949
|
+
*/
|
|
11950
|
+
rearm = () => {
|
|
11951
|
+
if (this.disposed)
|
|
11952
|
+
return;
|
|
11953
|
+
this.clearTimers();
|
|
11954
|
+
this.attempt = 0;
|
|
11955
|
+
this.stopped = false;
|
|
11956
|
+
const { paused, srcObject, ended } = this.element;
|
|
11957
|
+
if (!paused || !srcObject || ended)
|
|
11958
|
+
return;
|
|
11959
|
+
this.pendingTimer = setTimeout(this.attemptPlay, 0);
|
|
11960
|
+
};
|
|
11961
11961
|
clearTimers = () => {
|
|
11962
11962
|
if (this.pendingTimer)
|
|
11963
11963
|
clearTimeout(this.pendingTimer);
|
|
@@ -11979,17 +11979,34 @@ class MediaPlaybackWatchdog {
|
|
|
11979
11979
|
if (this.element.paused)
|
|
11980
11980
|
return;
|
|
11981
11981
|
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11982
|
-
|
|
11982
|
+
...this.traceInfo,
|
|
11983
11983
|
attempts: this.attempt,
|
|
11984
11984
|
});
|
|
11985
11985
|
this.attempt = 0;
|
|
11986
11986
|
this.stopped = false;
|
|
11987
11987
|
};
|
|
11988
|
+
/**
|
|
11989
|
+
* Media elements backed by a MediaStream fire `suspend` routinely (after
|
|
11990
|
+
* the initial load, on source swaps, on layer switches), so a `suspend`
|
|
11991
|
+
* that needs no recovery is ignored without a trace. An explicit `pause`
|
|
11992
|
+
* is rare and always traced, along with the reason recovery was skipped.
|
|
11993
|
+
*/
|
|
11988
11994
|
onPauseOrSuspend = (event) => {
|
|
11989
|
-
if (this.disposed || this.stopped)
|
|
11995
|
+
if (this.disposed || this.stopped || this.pendingTimer)
|
|
11996
|
+
return;
|
|
11997
|
+
const skipReason = this.computeSkipReason();
|
|
11998
|
+
if (skipReason) {
|
|
11999
|
+
if (event.type === 'pause') {
|
|
12000
|
+
this.tracer.trace('mediaPlayback.paused', {
|
|
12001
|
+
...this.traceInfo,
|
|
12002
|
+
reason: event.type,
|
|
12003
|
+
skipped: skipReason,
|
|
12004
|
+
});
|
|
12005
|
+
}
|
|
11990
12006
|
return;
|
|
12007
|
+
}
|
|
11991
12008
|
this.tracer.trace('mediaPlayback.paused', {
|
|
11992
|
-
|
|
12009
|
+
...this.traceInfo,
|
|
11993
12010
|
reason: event.type,
|
|
11994
12011
|
});
|
|
11995
12012
|
if (this.settleTimer)
|
|
@@ -12003,7 +12020,7 @@ class MediaPlaybackWatchdog {
|
|
|
12003
12020
|
const skipReason = this.computeSkipReason();
|
|
12004
12021
|
if (skipReason) {
|
|
12005
12022
|
this.tracer.trace('mediaPlayback.recover.skipped', {
|
|
12006
|
-
|
|
12023
|
+
...this.traceInfo,
|
|
12007
12024
|
reason: skipReason,
|
|
12008
12025
|
});
|
|
12009
12026
|
return;
|
|
@@ -12012,9 +12029,10 @@ class MediaPlaybackWatchdog {
|
|
|
12012
12029
|
this.stopped = true;
|
|
12013
12030
|
this.clearTimers();
|
|
12014
12031
|
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
12015
|
-
|
|
12032
|
+
...this.traceInfo,
|
|
12016
12033
|
attempts: this.attempt,
|
|
12017
12034
|
});
|
|
12035
|
+
this.onGiveUp();
|
|
12018
12036
|
return;
|
|
12019
12037
|
}
|
|
12020
12038
|
const delay = this.attempt === 0 ? 0 : retryInterval(this.attempt);
|
|
@@ -12042,7 +12060,7 @@ class MediaPlaybackWatchdog {
|
|
|
12042
12060
|
return;
|
|
12043
12061
|
this.attempt += 1;
|
|
12044
12062
|
this.tracer.trace('mediaPlayback.recover.attempt', {
|
|
12045
|
-
|
|
12063
|
+
...this.traceInfo,
|
|
12046
12064
|
attempt: this.attempt,
|
|
12047
12065
|
});
|
|
12048
12066
|
try {
|
|
@@ -12057,6 +12075,41 @@ class MediaPlaybackWatchdog {
|
|
|
12057
12075
|
};
|
|
12058
12076
|
}
|
|
12059
12077
|
|
|
12078
|
+
/**
|
|
12079
|
+
* Checks whether media playback and capture are likely interrupted: the page
|
|
12080
|
+
* is hidden, or WebKit reports the audio session as interrupted (a phone
|
|
12081
|
+
* call, Siri, or another app taking over audio).
|
|
12082
|
+
*/
|
|
12083
|
+
const isMediaInterrupted = () => {
|
|
12084
|
+
if (typeof document === 'undefined')
|
|
12085
|
+
return false;
|
|
12086
|
+
return (document.visibilityState === 'hidden' ||
|
|
12087
|
+
navigator.audioSession?.state === 'interrupted');
|
|
12088
|
+
};
|
|
12089
|
+
/**
|
|
12090
|
+
* Invokes `onResumed` every time the page leaves a media interruption,
|
|
12091
|
+
* as defined by {@link isMediaInterrupted}.
|
|
12092
|
+
*
|
|
12093
|
+
* @param onResumed the callback to invoke.
|
|
12094
|
+
* @returns a function that stops watching.
|
|
12095
|
+
*/
|
|
12096
|
+
const watchMediaInterruptions = (onResumed) => {
|
|
12097
|
+
if (typeof document === 'undefined')
|
|
12098
|
+
return () => { };
|
|
12099
|
+
let interrupted = isMediaInterrupted();
|
|
12100
|
+
const check = () => {
|
|
12101
|
+
const wasInterrupted = interrupted;
|
|
12102
|
+
interrupted = isMediaInterrupted();
|
|
12103
|
+
if (wasInterrupted && !interrupted)
|
|
12104
|
+
onResumed();
|
|
12105
|
+
};
|
|
12106
|
+
const controller = new AbortController();
|
|
12107
|
+
const { signal } = controller;
|
|
12108
|
+
document.addEventListener('visibilitychange', check, { signal });
|
|
12109
|
+
navigator.audioSession?.addEventListener('statechange', check, { signal });
|
|
12110
|
+
return () => controller.abort();
|
|
12111
|
+
};
|
|
12112
|
+
|
|
12060
12113
|
/**
|
|
12061
12114
|
* A manager class that handles dynascale related tasks like:
|
|
12062
12115
|
*
|
|
@@ -12070,6 +12123,9 @@ class DynascaleManager {
|
|
|
12070
12123
|
tracer;
|
|
12071
12124
|
useWebAudio = false;
|
|
12072
12125
|
audioContext;
|
|
12126
|
+
playbackWatchdogs = new Set();
|
|
12127
|
+
stopWatchingInterruptions;
|
|
12128
|
+
stopTracingAudioSession;
|
|
12073
12129
|
trackSubscriptionManager;
|
|
12074
12130
|
blockedAudioTracker;
|
|
12075
12131
|
/**
|
|
@@ -12086,6 +12142,10 @@ class DynascaleManager {
|
|
|
12086
12142
|
* Closes the audio context if it was created.
|
|
12087
12143
|
*/
|
|
12088
12144
|
dispose = async () => {
|
|
12145
|
+
this.stopWatchingInterruptions?.();
|
|
12146
|
+
this.stopWatchingInterruptions = undefined;
|
|
12147
|
+
this.stopTracingAudioSession?.();
|
|
12148
|
+
this.stopTracingAudioSession = undefined;
|
|
12089
12149
|
const context = this.audioContext;
|
|
12090
12150
|
if (context && context.state !== 'closed') {
|
|
12091
12151
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
@@ -12225,10 +12285,12 @@ class DynascaleManager {
|
|
|
12225
12285
|
// without prior user interaction:
|
|
12226
12286
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
|
|
12227
12287
|
videoElement.muted = true;
|
|
12228
|
-
const playbackWatchdog =
|
|
12288
|
+
const playbackWatchdog = this.createPlaybackWatchdog({
|
|
12229
12289
|
element: videoElement,
|
|
12230
12290
|
kind: 'video',
|
|
12231
12291
|
tracer: this.tracer,
|
|
12292
|
+
sessionId,
|
|
12293
|
+
trackType,
|
|
12232
12294
|
});
|
|
12233
12295
|
const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
|
|
12234
12296
|
const streamSubscription = participant$
|
|
@@ -12256,7 +12318,7 @@ class DynascaleManager {
|
|
|
12256
12318
|
publishedTracksSubscription?.unsubscribe();
|
|
12257
12319
|
streamSubscription.unsubscribe();
|
|
12258
12320
|
resizeObserver?.disconnect();
|
|
12259
|
-
|
|
12321
|
+
this.disposePlaybackWatchdog(playbackWatchdog);
|
|
12260
12322
|
};
|
|
12261
12323
|
};
|
|
12262
12324
|
/**
|
|
@@ -12274,6 +12336,7 @@ class DynascaleManager {
|
|
|
12274
12336
|
const participant = this.callState.findParticipantBySessionId(sessionId);
|
|
12275
12337
|
if (!participant || participant.isLocalParticipant)
|
|
12276
12338
|
return;
|
|
12339
|
+
this.traceAudioSessionState();
|
|
12277
12340
|
const participant$ = this.callState.participants$.pipe(rxjs.map((ps) => ps.find((p) => p.sessionId === sessionId)), rxjs.takeWhile((p) => !!p), rxjs.distinctUntilChanged(), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
12278
12341
|
const updateSinkId = (deviceId, audioContext) => {
|
|
12279
12342
|
if (!deviceId)
|
|
@@ -12293,7 +12356,9 @@ class DynascaleManager {
|
|
|
12293
12356
|
let sourceNode = undefined;
|
|
12294
12357
|
let gainNode = undefined;
|
|
12295
12358
|
let audioWatchdog = undefined;
|
|
12359
|
+
let blockedByGiveUp = false;
|
|
12296
12360
|
const clearBlockedAudio = () => {
|
|
12361
|
+
blockedByGiveUp = false;
|
|
12297
12362
|
if (!this.blockedAudioTracker.isBlocked(audioElement))
|
|
12298
12363
|
return;
|
|
12299
12364
|
this.blockedAudioTracker.markBlocked(audioElement, false);
|
|
@@ -12301,15 +12366,18 @@ class DynascaleManager {
|
|
|
12301
12366
|
audioElement.addEventListener('playing', clearBlockedAudio);
|
|
12302
12367
|
const isAudioTrack = trackType === 'audioTrack';
|
|
12303
12368
|
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
12369
|
+
let sourceChangeTimer;
|
|
12304
12370
|
const updateMediaStreamSubscription = participant$
|
|
12305
12371
|
.pipe(rxjs.distinctUntilKeyChanged(trackKey))
|
|
12306
12372
|
.subscribe((p) => {
|
|
12307
12373
|
const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
|
|
12308
12374
|
if (audioElement.srcObject === source)
|
|
12309
12375
|
return;
|
|
12310
|
-
|
|
12376
|
+
clearTimeout(sourceChangeTimer);
|
|
12377
|
+
sourceChangeTimer = setTimeout(() => {
|
|
12378
|
+
sourceChangeTimer = undefined;
|
|
12311
12379
|
audioElement.srcObject = source ?? null;
|
|
12312
|
-
audioWatchdog
|
|
12380
|
+
this.disposePlaybackWatchdog(audioWatchdog);
|
|
12313
12381
|
audioWatchdog = undefined;
|
|
12314
12382
|
if (!source) {
|
|
12315
12383
|
clearBlockedAudio();
|
|
@@ -12340,15 +12408,25 @@ class DynascaleManager {
|
|
|
12340
12408
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
12341
12409
|
if (e.name === 'NotAllowedError') {
|
|
12342
12410
|
this.tracer.trace('audioPlaybackBlocked', null);
|
|
12411
|
+
blockedByGiveUp = false;
|
|
12343
12412
|
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
12344
12413
|
}
|
|
12345
12414
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
12346
12415
|
});
|
|
12347
|
-
audioWatchdog =
|
|
12416
|
+
audioWatchdog = this.createPlaybackWatchdog({
|
|
12348
12417
|
element: audioElement,
|
|
12349
12418
|
kind: 'audio',
|
|
12350
12419
|
tracer: this.tracer,
|
|
12351
|
-
|
|
12420
|
+
sessionId,
|
|
12421
|
+
trackType,
|
|
12422
|
+
// only an autoplay-policy block needs a user gesture; a give-up
|
|
12423
|
+
// block is for the UI and must not stop a re-armed cycle
|
|
12424
|
+
isBlocked: () => !blockedByGiveUp &&
|
|
12425
|
+
this.blockedAudioTracker.isBlocked(audioElement),
|
|
12426
|
+
onGiveUp: () => {
|
|
12427
|
+
blockedByGiveUp = true;
|
|
12428
|
+
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
12429
|
+
},
|
|
12352
12430
|
});
|
|
12353
12431
|
}
|
|
12354
12432
|
const { selectedDevice } = this.speaker.state;
|
|
@@ -12378,13 +12456,51 @@ class DynascaleManager {
|
|
|
12378
12456
|
sinkIdSubscription?.unsubscribe();
|
|
12379
12457
|
volumeSubscription.unsubscribe();
|
|
12380
12458
|
updateMediaStreamSubscription.unsubscribe();
|
|
12459
|
+
clearTimeout(sourceChangeTimer);
|
|
12381
12460
|
audioElement.srcObject = null;
|
|
12382
12461
|
sourceNode?.disconnect();
|
|
12383
12462
|
gainNode?.disconnect();
|
|
12384
|
-
audioWatchdog
|
|
12463
|
+
this.disposePlaybackWatchdog(audioWatchdog);
|
|
12385
12464
|
audioWatchdog = undefined;
|
|
12386
12465
|
};
|
|
12387
12466
|
};
|
|
12467
|
+
/**
|
|
12468
|
+
* Creates a playback watchdog that is re-armed whenever a media
|
|
12469
|
+
* interruption (backgrounding, phone call, Siri) ends.
|
|
12470
|
+
*/
|
|
12471
|
+
createPlaybackWatchdog = (opts) => {
|
|
12472
|
+
const watchdog = new MediaPlaybackWatchdog(opts);
|
|
12473
|
+
this.playbackWatchdogs.add(watchdog);
|
|
12474
|
+
this.stopWatchingInterruptions ??= watchMediaInterruptions(this.rearmPlayback);
|
|
12475
|
+
return watchdog;
|
|
12476
|
+
};
|
|
12477
|
+
disposePlaybackWatchdog = (watchdog) => {
|
|
12478
|
+
if (!watchdog)
|
|
12479
|
+
return;
|
|
12480
|
+
watchdog.dispose();
|
|
12481
|
+
this.playbackWatchdogs.delete(watchdog);
|
|
12482
|
+
};
|
|
12483
|
+
rearmPlayback = () => {
|
|
12484
|
+
this.tracer.trace('mediaPlayback.rearm', this.playbackWatchdogs.size);
|
|
12485
|
+
this.playbackWatchdogs.forEach((watchdog) => watchdog.rearm());
|
|
12486
|
+
};
|
|
12487
|
+
/**
|
|
12488
|
+
* Traces the WebKit audio session state (initial value and changes) in
|
|
12489
|
+
* both playback modes, as it explains most iOS playback interruptions.
|
|
12490
|
+
*/
|
|
12491
|
+
traceAudioSessionState = () => {
|
|
12492
|
+
const audioSession = navigator.audioSession;
|
|
12493
|
+
if (!audioSession || this.stopTracingAudioSession)
|
|
12494
|
+
return;
|
|
12495
|
+
const trace = () => {
|
|
12496
|
+
this.tracer.trace('audioSession.state', audioSession.state);
|
|
12497
|
+
};
|
|
12498
|
+
trace();
|
|
12499
|
+
audioSession.addEventListener('statechange', trace);
|
|
12500
|
+
this.stopTracingAudioSession = () => {
|
|
12501
|
+
audioSession.removeEventListener('statechange', trace);
|
|
12502
|
+
};
|
|
12503
|
+
};
|
|
12388
12504
|
getOrCreateAudioContext = () => {
|
|
12389
12505
|
if (!this.useWebAudio)
|
|
12390
12506
|
return;
|
|
@@ -12407,7 +12523,6 @@ class DynascaleManager {
|
|
|
12407
12523
|
audioSession.type = 'play-and-record';
|
|
12408
12524
|
let isSessionInterrupted = false;
|
|
12409
12525
|
audioSession.addEventListener('statechange', () => {
|
|
12410
|
-
this.tracer.trace('audioSession.state', audioSession.state);
|
|
12411
12526
|
if (audioSession.state === 'interrupted') {
|
|
12412
12527
|
isSessionInterrupted = true;
|
|
12413
12528
|
}
|
|
@@ -13966,9 +14081,22 @@ class DeviceManager {
|
|
|
13966
14081
|
}
|
|
13967
14082
|
const devices = getCurrentValue(this.listDevices()) || [];
|
|
13968
14083
|
const currentDevice = this.findDevice(devices, selectedDevice) ??
|
|
13969
|
-
createSyntheticDevice(selectedDevice, deviceKind);
|
|
14084
|
+
createSyntheticDevice(selectedDevice, deviceKind, this.getActiveTrackLabel(selectedDevice));
|
|
13970
14085
|
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
13971
14086
|
}
|
|
14087
|
+
/**
|
|
14088
|
+
* Returns the label of the active track if it was captured from the given
|
|
14089
|
+
* device. Used as a fallback when the device list hasn't been enumerated yet,
|
|
14090
|
+
* so the persisted preference still carries the device label.
|
|
14091
|
+
*/
|
|
14092
|
+
getActiveTrackLabel = (deviceId) => {
|
|
14093
|
+
const stream = this.state.rootMediaStream;
|
|
14094
|
+
const tracks = this.trackType === TrackType.AUDIO
|
|
14095
|
+
? stream?.getAudioTracks()
|
|
14096
|
+
: stream?.getVideoTracks();
|
|
14097
|
+
const track = tracks?.find((t) => t.getSettings().deviceId === deviceId);
|
|
14098
|
+
return track?.label;
|
|
14099
|
+
};
|
|
13972
14100
|
async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
|
|
13973
14101
|
const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
13974
14102
|
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
@@ -16804,9 +16932,6 @@ class Call {
|
|
|
16804
16932
|
options: statsOptions,
|
|
16805
16933
|
subscriber: this.subscriber,
|
|
16806
16934
|
publisher: this.publisher,
|
|
16807
|
-
microphone: this.microphone,
|
|
16808
|
-
camera: this.camera,
|
|
16809
|
-
state: this.state,
|
|
16810
16935
|
tracer: this.tracer,
|
|
16811
16936
|
unifiedSessionId,
|
|
16812
16937
|
});
|
|
@@ -19656,11 +19781,11 @@ class StreamClient {
|
|
|
19656
19781
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
19657
19782
|
};
|
|
19658
19783
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
19659
|
-
"1.61.
|
|
19784
|
+
"1.61.2";
|
|
19660
19785
|
getUserAgent = () => {
|
|
19661
19786
|
if (!this.cachedUserAgent) {
|
|
19662
19787
|
const { clientAppIdentifier = {} } = this.options;
|
|
19663
|
-
const { sdkName = 'js', sdkVersion = "1.61.
|
|
19788
|
+
const { sdkName = 'js', sdkVersion = "1.61.2", ...extras } = clientAppIdentifier;
|
|
19664
19789
|
this.cachedUserAgent = [
|
|
19665
19790
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
19666
19791
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -20600,6 +20725,10 @@ class StreamVideoClient {
|
|
|
20600
20725
|
const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
|
|
20601
20726
|
if (call) {
|
|
20602
20727
|
call.updateFromCallStateResponse(c);
|
|
20728
|
+
// ring events missed while the WS was down aren't replayed
|
|
20729
|
+
reconcileRingState(call, 'ring-poll-api').catch((err) => {
|
|
20730
|
+
call.logger.error('Failed to reconcile the ring state after rewatch', err);
|
|
20731
|
+
});
|
|
20603
20732
|
}
|
|
20604
20733
|
}
|
|
20605
20734
|
return;
|