@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.browser.es.js
CHANGED
|
@@ -2665,18 +2665,6 @@ class SendStatsRequest$Type extends MessageType {
|
|
|
2665
2665
|
constructor() {
|
|
2666
2666
|
super('stream.video.sfu.signal.SendStatsRequest', [
|
|
2667
2667
|
{ no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2668
|
-
{
|
|
2669
|
-
no: 2,
|
|
2670
|
-
name: 'subscriber_stats',
|
|
2671
|
-
kind: 'scalar',
|
|
2672
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2673
|
-
},
|
|
2674
|
-
{
|
|
2675
|
-
no: 3,
|
|
2676
|
-
name: 'publisher_stats',
|
|
2677
|
-
kind: 'scalar',
|
|
2678
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2679
|
-
},
|
|
2680
2668
|
{
|
|
2681
2669
|
no: 4,
|
|
2682
2670
|
name: 'webrtc_version',
|
|
@@ -2690,8 +2678,6 @@ class SendStatsRequest$Type extends MessageType {
|
|
|
2690
2678
|
kind: 'scalar',
|
|
2691
2679
|
T: 9 /*ScalarType.STRING*/,
|
|
2692
2680
|
},
|
|
2693
|
-
{ no: 7, name: 'audio_devices', kind: 'message', T: () => InputDevices },
|
|
2694
|
-
{ no: 8, name: 'video_devices', kind: 'message', T: () => InputDevices },
|
|
2695
2681
|
{
|
|
2696
2682
|
no: 9,
|
|
2697
2683
|
name: 'android',
|
|
@@ -2708,18 +2694,6 @@ class SendStatsRequest$Type extends MessageType {
|
|
|
2708
2694
|
},
|
|
2709
2695
|
{ no: 11, name: 'telemetry', kind: 'message', T: () => Telemetry },
|
|
2710
2696
|
{ no: 12, name: 'rtmp', kind: 'message', T: () => RTMPIngress },
|
|
2711
|
-
{
|
|
2712
|
-
no: 13,
|
|
2713
|
-
name: 'subscriber_rtc_stats',
|
|
2714
|
-
kind: 'scalar',
|
|
2715
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2716
|
-
},
|
|
2717
|
-
{
|
|
2718
|
-
no: 14,
|
|
2719
|
-
name: 'publisher_rtc_stats',
|
|
2720
|
-
kind: 'scalar',
|
|
2721
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2722
|
-
},
|
|
2723
2697
|
{ no: 15, name: 'rtc_stats', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2724
2698
|
{
|
|
2725
2699
|
no: 16,
|
|
@@ -5081,7 +5055,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5081
5055
|
}
|
|
5082
5056
|
};
|
|
5083
5057
|
|
|
5084
|
-
const version = "1.61.
|
|
5058
|
+
const version = "1.61.2";
|
|
5085
5059
|
const [major, minor, patch] = version.split('.');
|
|
5086
5060
|
let sdkInfo = {
|
|
5087
5061
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -7809,30 +7783,21 @@ class SfuStatsReporter {
|
|
|
7809
7783
|
sfuClient;
|
|
7810
7784
|
subscriber;
|
|
7811
7785
|
publisher;
|
|
7812
|
-
microphone;
|
|
7813
|
-
camera;
|
|
7814
|
-
state;
|
|
7815
7786
|
tracer;
|
|
7816
7787
|
unifiedSessionId;
|
|
7817
7788
|
intervalId;
|
|
7818
7789
|
timeoutId;
|
|
7819
7790
|
reportCount = 0;
|
|
7820
|
-
unsubscribeDevicePermissionsSubscription;
|
|
7821
|
-
unsubscribeListDevicesSubscription;
|
|
7822
7791
|
sdkName;
|
|
7823
7792
|
sdkVersion;
|
|
7824
7793
|
webRTCVersion;
|
|
7825
|
-
inputDevices = new Map();
|
|
7826
7794
|
statsConcurrencyTag = Symbol('sfuStatsReporter');
|
|
7827
7795
|
isStopped = false;
|
|
7828
|
-
constructor(sfuClient, { options, clientDetails, subscriber, publisher,
|
|
7796
|
+
constructor(sfuClient, { options, clientDetails, subscriber, publisher, tracer, unifiedSessionId, }) {
|
|
7829
7797
|
this.sfuClient = sfuClient;
|
|
7830
7798
|
this.options = options;
|
|
7831
7799
|
this.subscriber = subscriber;
|
|
7832
7800
|
this.publisher = publisher;
|
|
7833
|
-
this.microphone = microphone;
|
|
7834
|
-
this.camera = camera;
|
|
7835
|
-
this.state = state;
|
|
7836
7801
|
this.tracer = tracer;
|
|
7837
7802
|
this.unifiedSessionId = unifiedSessionId;
|
|
7838
7803
|
const { sdk, browser } = clientDetails;
|
|
@@ -7846,35 +7811,6 @@ class SfuStatsReporter {
|
|
|
7846
7811
|
`${browser?.name || ''}-${browser?.version || ''}` ||
|
|
7847
7812
|
'N/A';
|
|
7848
7813
|
}
|
|
7849
|
-
observeDevice = (device, kind) => {
|
|
7850
|
-
const { browserPermissionState$ } = device.state;
|
|
7851
|
-
this.unsubscribeDevicePermissionsSubscription?.();
|
|
7852
|
-
this.unsubscribeDevicePermissionsSubscription = createSubscription(combineLatest([browserPermissionState$, this.state.ownCapabilities$]), ([browserPermissionState, ownCapabilities]) => {
|
|
7853
|
-
// cleanup the previous listDevices() subscription in case
|
|
7854
|
-
// permissions or capabilities have changed.
|
|
7855
|
-
// we will subscribe again if everything is in order.
|
|
7856
|
-
this.unsubscribeListDevicesSubscription?.();
|
|
7857
|
-
const hasCapability = kind === 'mic'
|
|
7858
|
-
? ownCapabilities.includes(OwnCapability.SEND_AUDIO)
|
|
7859
|
-
: ownCapabilities.includes(OwnCapability.SEND_VIDEO);
|
|
7860
|
-
if (browserPermissionState !== 'granted' || !hasCapability) {
|
|
7861
|
-
this.inputDevices.set(kind, {
|
|
7862
|
-
currentDevice: '',
|
|
7863
|
-
availableDevices: [],
|
|
7864
|
-
isPermitted: false,
|
|
7865
|
-
});
|
|
7866
|
-
return;
|
|
7867
|
-
}
|
|
7868
|
-
this.unsubscribeListDevicesSubscription = createSubscription(combineLatest([device.listDevices(), device.state.selectedDevice$]), ([devices, deviceId]) => {
|
|
7869
|
-
const selected = devices.find((d) => d.deviceId === deviceId);
|
|
7870
|
-
this.inputDevices.set(kind, {
|
|
7871
|
-
currentDevice: selected?.label || deviceId || '',
|
|
7872
|
-
availableDevices: devices.map((d) => d.label),
|
|
7873
|
-
isPermitted: true,
|
|
7874
|
-
});
|
|
7875
|
-
});
|
|
7876
|
-
});
|
|
7877
|
-
};
|
|
7878
7814
|
sendConnectionTime = (connectionTimeSeconds) => {
|
|
7879
7815
|
this.sendTelemetryData({
|
|
7880
7816
|
data: {
|
|
@@ -7909,7 +7845,7 @@ class SfuStatsReporter {
|
|
|
7909
7845
|
this.subscriber.stats.takeSample(),
|
|
7910
7846
|
this.publisher?.stats.takeSample(),
|
|
7911
7847
|
]);
|
|
7912
|
-
send = (
|
|
7848
|
+
send = (subscriberPerfStats, publisherPerfStats, telemetry) => {
|
|
7913
7849
|
// serialize sends so overlapping ones can't race on the trace buffers or
|
|
7914
7850
|
// deliver an older delta after a newer one already succeeded. Not gated by
|
|
7915
7851
|
// `isStopped`: an explicit final flush must still deliver after stop().
|
|
@@ -7927,8 +7863,8 @@ class SfuStatsReporter {
|
|
|
7927
7863
|
this.subscriber.stats.clearPendingDeltas();
|
|
7928
7864
|
}
|
|
7929
7865
|
let pubPending = [];
|
|
7930
|
-
if (pubTracer &&
|
|
7931
|
-
pubPending = this.publisher
|
|
7866
|
+
if (pubTracer && this.publisher) {
|
|
7867
|
+
pubPending = this.publisher.stats.getPendingDeltas();
|
|
7932
7868
|
}
|
|
7933
7869
|
else {
|
|
7934
7870
|
this.publisher?.stats.clearPendingDeltas();
|
|
@@ -7950,17 +7886,9 @@ class SfuStatsReporter {
|
|
|
7950
7886
|
sdk: this.sdkName,
|
|
7951
7887
|
sdkVersion: this.sdkVersion,
|
|
7952
7888
|
webrtcVersion: this.webRTCVersion,
|
|
7953
|
-
subscriberStats: JSON.stringify(flatten(subscriberStats.stats)),
|
|
7954
|
-
publisherStats: publisherStats
|
|
7955
|
-
? JSON.stringify(flatten(publisherStats.stats))
|
|
7956
|
-
: '[]',
|
|
7957
|
-
subscriberRtcStats: '',
|
|
7958
|
-
publisherRtcStats: '',
|
|
7959
7889
|
rtcStats: JSON.stringify(traces),
|
|
7960
|
-
encodeStats:
|
|
7961
|
-
decodeStats:
|
|
7962
|
-
audioDevices: this.inputDevices.get('mic'),
|
|
7963
|
-
videoDevices: this.inputDevices.get('camera'),
|
|
7890
|
+
encodeStats: publisherPerfStats ?? [],
|
|
7891
|
+
decodeStats: subscriberPerfStats,
|
|
7964
7892
|
unifiedSessionId: this.unifiedSessionId,
|
|
7965
7893
|
deviceState: getDeviceState(),
|
|
7966
7894
|
telemetry,
|
|
@@ -7974,8 +7902,8 @@ class SfuStatsReporter {
|
|
|
7974
7902
|
// delivery confirmed: advance the delivery baseline for each chain.
|
|
7975
7903
|
if (subTracer)
|
|
7976
7904
|
this.subscriber.stats.commitDeltas(subPending);
|
|
7977
|
-
if (pubTracer &&
|
|
7978
|
-
this.publisher
|
|
7905
|
+
if (pubTracer && this.publisher) {
|
|
7906
|
+
this.publisher.stats.commitDeltas(pubPending);
|
|
7979
7907
|
}
|
|
7980
7908
|
}
|
|
7981
7909
|
catch (err) {
|
|
@@ -7997,8 +7925,8 @@ class SfuStatsReporter {
|
|
|
7997
7925
|
run = async (telemetry) => {
|
|
7998
7926
|
if (this.isStopped)
|
|
7999
7927
|
return;
|
|
8000
|
-
const [
|
|
8001
|
-
await this.send(
|
|
7928
|
+
const [subscriberPerfStats, publisherPerfStats] = await this.sample();
|
|
7929
|
+
await this.send(subscriberPerfStats, publisherPerfStats, telemetry);
|
|
8002
7930
|
};
|
|
8003
7931
|
scheduleNextReport = () => {
|
|
8004
7932
|
const intervals = [1500, 3000, 3000, 5000];
|
|
@@ -8019,8 +7947,6 @@ class SfuStatsReporter {
|
|
|
8019
7947
|
start = () => {
|
|
8020
7948
|
if (this.options.reporting_interval_ms <= 0)
|
|
8021
7949
|
return;
|
|
8022
|
-
this.observeDevice(this.microphone, 'mic');
|
|
8023
|
-
this.observeDevice(this.camera, 'camera');
|
|
8024
7950
|
this.isStopped = false;
|
|
8025
7951
|
this.reportCount = 0;
|
|
8026
7952
|
clearInterval(this.intervalId);
|
|
@@ -8029,11 +7955,6 @@ class SfuStatsReporter {
|
|
|
8029
7955
|
};
|
|
8030
7956
|
stop = () => {
|
|
8031
7957
|
this.isStopped = true;
|
|
8032
|
-
this.unsubscribeDevicePermissionsSubscription?.();
|
|
8033
|
-
this.unsubscribeDevicePermissionsSubscription = undefined;
|
|
8034
|
-
this.unsubscribeListDevicesSubscription?.();
|
|
8035
|
-
this.unsubscribeListDevicesSubscription = undefined;
|
|
8036
|
-
this.inputDevices.clear();
|
|
8037
7958
|
clearInterval(this.intervalId);
|
|
8038
7959
|
this.intervalId = undefined;
|
|
8039
7960
|
clearTimeout(this.timeoutId);
|
|
@@ -8054,8 +7975,8 @@ class SfuStatsReporter {
|
|
|
8054
7975
|
return;
|
|
8055
7976
|
try {
|
|
8056
7977
|
const [sample] = await timeboxed([this.sample()], 2000);
|
|
8057
|
-
const [
|
|
8058
|
-
this.send(
|
|
7978
|
+
const [subscriberPerfStats, publisherPerfStats] = sample;
|
|
7979
|
+
this.send(subscriberPerfStats, publisherPerfStats).catch((err) => {
|
|
8059
7980
|
this.logger.warn('Failed to flush report stats', err);
|
|
8060
7981
|
});
|
|
8061
7982
|
}
|
|
@@ -8182,9 +8103,9 @@ class StatsTracer {
|
|
|
8182
8103
|
this.maxPendingDeltas = maxPendingDeltas;
|
|
8183
8104
|
}
|
|
8184
8105
|
/**
|
|
8185
|
-
* Samples the RTCPeerConnection: returns the
|
|
8186
|
-
*
|
|
8187
|
-
*
|
|
8106
|
+
* Samples the RTCPeerConnection: returns the derived performance stats and
|
|
8107
|
+
* appends the delta-compressed sample to the un-acked delivery chain
|
|
8108
|
+
* (retrieved via `getPendingDeltas`).
|
|
8188
8109
|
*
|
|
8189
8110
|
* @internal
|
|
8190
8111
|
*/
|
|
@@ -8209,7 +8130,7 @@ class StatsTracer {
|
|
|
8209
8130
|
this.pendingDeltas.push({ delta, ts: now });
|
|
8210
8131
|
this.frameTimeHistory = this.frameTimeHistory.slice(-2);
|
|
8211
8132
|
this.fpsHistory = this.fpsHistory.slice(-2);
|
|
8212
|
-
return
|
|
8133
|
+
return performanceStats;
|
|
8213
8134
|
});
|
|
8214
8135
|
};
|
|
8215
8136
|
/**
|
|
@@ -10758,15 +10679,16 @@ class StreamSfuClient {
|
|
|
10758
10679
|
/**
|
|
10759
10680
|
* Decides what a ringing call should do next, based on the current call state.
|
|
10760
10681
|
*
|
|
10761
|
-
* The `call.accepted
|
|
10762
|
-
*
|
|
10763
|
-
* handlers rely on `CallState.updateFromEvent`, which runs
|
|
10764
|
-
* poller applies the polled ring state itself
|
|
10682
|
+
* The `call.accepted` and `call.rejected` handlers, the ring state poller and
|
|
10683
|
+
* the rewatch after a WS reconnect all run this. They differ only in how the
|
|
10684
|
+
* state got there: the handlers rely on `CallState.updateFromEvent`, which runs
|
|
10685
|
+
* before them, while the poller applies the polled ring state itself and the
|
|
10686
|
+
* rewatch applies the queried call state.
|
|
10765
10687
|
*
|
|
10766
10688
|
* @param call the call to reconcile.
|
|
10767
|
-
* @param joinSource
|
|
10768
|
-
*
|
|
10769
|
-
*
|
|
10689
|
+
* @param joinSource what triggered this run, reported on the caller's join:
|
|
10690
|
+
* `ring-ws` for the event handlers, `ring-poll-api` for the poller and the
|
|
10691
|
+
* rewatch (both come from an API response).
|
|
10770
10692
|
* @returns whether the ring reached a terminal state. A failed join is not
|
|
10771
10693
|
* terminal: the caller should keep trying while the ring is open.
|
|
10772
10694
|
*/
|
|
@@ -10821,10 +10743,16 @@ const reconcileAsCaller = async (call, joinSource) => {
|
|
|
10821
10743
|
return false;
|
|
10822
10744
|
};
|
|
10823
10745
|
// the current user's own accept or reject, on this or another device, is
|
|
10824
|
-
// handled by `resolveOwnRingOutcome`, and `call.ended` by
|
|
10746
|
+
// handled by `resolveOwnRingOutcome`, and a live `call.ended` by
|
|
10747
|
+
// `watchCallEnded`. An end missed while offline is handled here.
|
|
10825
10748
|
const reconcileAsCallee = async (call) => {
|
|
10826
|
-
const
|
|
10827
|
-
|
|
10749
|
+
const { session, endedAt, createdBy } = call.state;
|
|
10750
|
+
if (endedAt || session?.ended_at) {
|
|
10751
|
+
call.logger.info('ring: the call has ended, leaving');
|
|
10752
|
+
return leave(call, { reason: 'ended', message: 'ring: call ended' });
|
|
10753
|
+
}
|
|
10754
|
+
const createdById = createdBy?.id;
|
|
10755
|
+
const rejectedBy = session?.rejected_by ?? {};
|
|
10828
10756
|
if (createdById && rejectedBy[createdById]) {
|
|
10829
10757
|
call.logger.info('ring: the caller cancelled, leaving');
|
|
10830
10758
|
return leave(call, {
|
|
@@ -10873,7 +10801,7 @@ class RingStatePoller {
|
|
|
10873
10801
|
unsubscribe = [];
|
|
10874
10802
|
constructor(call, options = {}) {
|
|
10875
10803
|
this.call = call;
|
|
10876
|
-
this.startAfterMs = options.startAfterMs ??
|
|
10804
|
+
this.startAfterMs = options.startAfterMs ?? 9_000;
|
|
10877
10805
|
this.intervalMs = options.intervalMs ?? 5_000;
|
|
10878
10806
|
}
|
|
10879
10807
|
/**
|
|
@@ -11538,12 +11466,33 @@ const registerRingingCallEventHandlers = (call) => {
|
|
|
11538
11466
|
};
|
|
11539
11467
|
|
|
11540
11468
|
const toBindingKey = (sessionId, trackType = 'audioTrack') => `${sessionId}/${trackType}`;
|
|
11469
|
+
const toElementState = (element) => {
|
|
11470
|
+
const stream = element.srcObject;
|
|
11471
|
+
const [track] = stream?.getAudioTracks?.() ?? [];
|
|
11472
|
+
return {
|
|
11473
|
+
paused: element.paused,
|
|
11474
|
+
muted: element.muted,
|
|
11475
|
+
volume: Math.round(element.volume * 100) / 100,
|
|
11476
|
+
readyState: element.readyState,
|
|
11477
|
+
sinkId: element.sinkId,
|
|
11478
|
+
track: track
|
|
11479
|
+
? {
|
|
11480
|
+
enabled: track.enabled,
|
|
11481
|
+
muted: track.muted,
|
|
11482
|
+
readyState: track.readyState,
|
|
11483
|
+
}
|
|
11484
|
+
: null,
|
|
11485
|
+
};
|
|
11486
|
+
};
|
|
11541
11487
|
/**
|
|
11542
11488
|
* Tracks audio element bindings and periodically warns about
|
|
11543
11489
|
* remote participants whose audio streams have no bound element.
|
|
11490
|
+
* On the same tick, it traces the playback state of the bound elements,
|
|
11491
|
+
* limited to the bindings whose state changed since the previous trace.
|
|
11544
11492
|
*/
|
|
11545
11493
|
class AudioBindingsWatchdog {
|
|
11546
11494
|
bindings = new Map();
|
|
11495
|
+
tracedStates = new Map();
|
|
11547
11496
|
enabled = true;
|
|
11548
11497
|
watchdogInterval;
|
|
11549
11498
|
unsubscribeCallingState;
|
|
@@ -11554,8 +11503,6 @@ class AudioBindingsWatchdog {
|
|
|
11554
11503
|
this.tracer = tracer;
|
|
11555
11504
|
this.state = state;
|
|
11556
11505
|
this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
|
|
11557
|
-
if (!this.enabled)
|
|
11558
|
-
return;
|
|
11559
11506
|
if (callingState !== CallingState.JOINED) {
|
|
11560
11507
|
this.stop();
|
|
11561
11508
|
}
|
|
@@ -11584,15 +11531,15 @@ class AudioBindingsWatchdog {
|
|
|
11584
11531
|
this.bindings.delete(toBindingKey(sessionId, trackType));
|
|
11585
11532
|
};
|
|
11586
11533
|
/**
|
|
11587
|
-
* Enables or disables the
|
|
11588
|
-
*
|
|
11534
|
+
* Enables or disables the dangling binding warnings.
|
|
11535
|
+
* Bindings are still tracked and their playback state is still traced.
|
|
11589
11536
|
*/
|
|
11590
11537
|
setEnabled = (enabled) => {
|
|
11591
11538
|
this.enabled = enabled;
|
|
11592
11539
|
if (enabled) {
|
|
11593
11540
|
this.start();
|
|
11594
11541
|
}
|
|
11595
|
-
else {
|
|
11542
|
+
else if (this.state.callingState !== CallingState.JOINED) {
|
|
11596
11543
|
this.stop();
|
|
11597
11544
|
}
|
|
11598
11545
|
};
|
|
@@ -11602,33 +11549,61 @@ class AudioBindingsWatchdog {
|
|
|
11602
11549
|
dispose = () => {
|
|
11603
11550
|
this.stop();
|
|
11604
11551
|
this.bindings.clear();
|
|
11552
|
+
this.tracedStates.clear();
|
|
11605
11553
|
this.unsubscribeCallingState();
|
|
11606
11554
|
};
|
|
11607
11555
|
start = () => {
|
|
11608
11556
|
clearInterval(this.watchdogInterval);
|
|
11609
11557
|
this.watchdogInterval = setInterval(() => {
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11558
|
+
if (this.enabled)
|
|
11559
|
+
this.warnAboutDanglingBindings();
|
|
11560
|
+
this.traceBindingStates();
|
|
11561
|
+
}, 3000);
|
|
11562
|
+
};
|
|
11563
|
+
warnAboutDanglingBindings = () => {
|
|
11564
|
+
const danglingUserIds = [];
|
|
11565
|
+
for (const p of this.state.participants) {
|
|
11566
|
+
if (p.isLocalParticipant)
|
|
11567
|
+
continue;
|
|
11568
|
+
const { audioStream, screenShareAudioStream, sessionId, userId, publishedTracks, } = p;
|
|
11569
|
+
if (audioStream &&
|
|
11570
|
+
publishedTracks.includes(TrackType.AUDIO) &&
|
|
11571
|
+
!this.bindings.has(toBindingKey(sessionId))) {
|
|
11572
|
+
danglingUserIds.push(userId);
|
|
11625
11573
|
}
|
|
11626
|
-
if (
|
|
11627
|
-
|
|
11628
|
-
this.
|
|
11629
|
-
|
|
11574
|
+
if (screenShareAudioStream &&
|
|
11575
|
+
publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
|
|
11576
|
+
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
|
|
11577
|
+
danglingUserIds.push(userId);
|
|
11630
11578
|
}
|
|
11631
|
-
}
|
|
11579
|
+
}
|
|
11580
|
+
if (danglingUserIds.length > 0) {
|
|
11581
|
+
const key = 'audioBinding.danglingWarning';
|
|
11582
|
+
this.tracer.traceOnce(key, key, danglingUserIds);
|
|
11583
|
+
this.logger.warn(`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`);
|
|
11584
|
+
}
|
|
11585
|
+
};
|
|
11586
|
+
traceBindingStates = () => {
|
|
11587
|
+
const changes = {};
|
|
11588
|
+
let hasChanges = false;
|
|
11589
|
+
for (const [key, element] of this.bindings) {
|
|
11590
|
+
const state = toElementState(element);
|
|
11591
|
+
const serialized = JSON.stringify(state);
|
|
11592
|
+
if (this.tracedStates.get(key) === serialized)
|
|
11593
|
+
continue;
|
|
11594
|
+
this.tracedStates.set(key, serialized);
|
|
11595
|
+
changes[key] = state;
|
|
11596
|
+
hasChanges = true;
|
|
11597
|
+
}
|
|
11598
|
+
for (const key of this.tracedStates.keys()) {
|
|
11599
|
+
if (this.bindings.has(key))
|
|
11600
|
+
continue;
|
|
11601
|
+
this.tracedStates.delete(key);
|
|
11602
|
+
changes[key] = null;
|
|
11603
|
+
hasChanges = true;
|
|
11604
|
+
}
|
|
11605
|
+
if (hasChanges)
|
|
11606
|
+
this.tracer.trace('audioBinding.state', changes);
|
|
11632
11607
|
};
|
|
11633
11608
|
stop = () => {
|
|
11634
11609
|
clearInterval(this.watchdogInterval);
|
|
@@ -11907,7 +11882,9 @@ class TrackSubscriptionManager {
|
|
|
11907
11882
|
class MediaPlaybackWatchdog {
|
|
11908
11883
|
logger = videoLoggerSystem.getLogger('MediaPlaybackWatchdog');
|
|
11909
11884
|
kind;
|
|
11885
|
+
traceInfo;
|
|
11910
11886
|
isBlocked;
|
|
11887
|
+
onGiveUp;
|
|
11911
11888
|
element;
|
|
11912
11889
|
tracer;
|
|
11913
11890
|
controller = new AbortController();
|
|
@@ -11919,8 +11896,14 @@ class MediaPlaybackWatchdog {
|
|
|
11919
11896
|
constructor(opts) {
|
|
11920
11897
|
this.element = opts.element;
|
|
11921
11898
|
this.kind = opts.kind;
|
|
11899
|
+
this.traceInfo = {
|
|
11900
|
+
kind: opts.kind,
|
|
11901
|
+
sessionId: opts.sessionId,
|
|
11902
|
+
trackType: opts.trackType,
|
|
11903
|
+
};
|
|
11922
11904
|
this.tracer = opts.tracer;
|
|
11923
11905
|
this.isBlocked = opts.isBlocked ?? (() => false);
|
|
11906
|
+
this.onGiveUp = opts.onGiveUp ?? (() => { });
|
|
11924
11907
|
this.attach();
|
|
11925
11908
|
}
|
|
11926
11909
|
attach = () => {
|
|
@@ -11938,6 +11921,23 @@ class MediaPlaybackWatchdog {
|
|
|
11938
11921
|
this.controller.abort();
|
|
11939
11922
|
this.clearTimers();
|
|
11940
11923
|
};
|
|
11924
|
+
/**
|
|
11925
|
+
* Starts a fresh recovery cycle, e.g. after a media interruption ended.
|
|
11926
|
+
* Unlike a regular pause-triggered recovery, it also tries elements that
|
|
11927
|
+
* were skipped as blocked or not ready, since those conditions may have
|
|
11928
|
+
* cleared together with the interruption.
|
|
11929
|
+
*/
|
|
11930
|
+
rearm = () => {
|
|
11931
|
+
if (this.disposed)
|
|
11932
|
+
return;
|
|
11933
|
+
this.clearTimers();
|
|
11934
|
+
this.attempt = 0;
|
|
11935
|
+
this.stopped = false;
|
|
11936
|
+
const { paused, srcObject, ended } = this.element;
|
|
11937
|
+
if (!paused || !srcObject || ended)
|
|
11938
|
+
return;
|
|
11939
|
+
this.pendingTimer = setTimeout(this.attemptPlay, 0);
|
|
11940
|
+
};
|
|
11941
11941
|
clearTimers = () => {
|
|
11942
11942
|
if (this.pendingTimer)
|
|
11943
11943
|
clearTimeout(this.pendingTimer);
|
|
@@ -11959,17 +11959,34 @@ class MediaPlaybackWatchdog {
|
|
|
11959
11959
|
if (this.element.paused)
|
|
11960
11960
|
return;
|
|
11961
11961
|
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11962
|
-
|
|
11962
|
+
...this.traceInfo,
|
|
11963
11963
|
attempts: this.attempt,
|
|
11964
11964
|
});
|
|
11965
11965
|
this.attempt = 0;
|
|
11966
11966
|
this.stopped = false;
|
|
11967
11967
|
};
|
|
11968
|
+
/**
|
|
11969
|
+
* Media elements backed by a MediaStream fire `suspend` routinely (after
|
|
11970
|
+
* the initial load, on source swaps, on layer switches), so a `suspend`
|
|
11971
|
+
* that needs no recovery is ignored without a trace. An explicit `pause`
|
|
11972
|
+
* is rare and always traced, along with the reason recovery was skipped.
|
|
11973
|
+
*/
|
|
11968
11974
|
onPauseOrSuspend = (event) => {
|
|
11969
|
-
if (this.disposed || this.stopped)
|
|
11975
|
+
if (this.disposed || this.stopped || this.pendingTimer)
|
|
11976
|
+
return;
|
|
11977
|
+
const skipReason = this.computeSkipReason();
|
|
11978
|
+
if (skipReason) {
|
|
11979
|
+
if (event.type === 'pause') {
|
|
11980
|
+
this.tracer.trace('mediaPlayback.paused', {
|
|
11981
|
+
...this.traceInfo,
|
|
11982
|
+
reason: event.type,
|
|
11983
|
+
skipped: skipReason,
|
|
11984
|
+
});
|
|
11985
|
+
}
|
|
11970
11986
|
return;
|
|
11987
|
+
}
|
|
11971
11988
|
this.tracer.trace('mediaPlayback.paused', {
|
|
11972
|
-
|
|
11989
|
+
...this.traceInfo,
|
|
11973
11990
|
reason: event.type,
|
|
11974
11991
|
});
|
|
11975
11992
|
if (this.settleTimer)
|
|
@@ -11983,7 +12000,7 @@ class MediaPlaybackWatchdog {
|
|
|
11983
12000
|
const skipReason = this.computeSkipReason();
|
|
11984
12001
|
if (skipReason) {
|
|
11985
12002
|
this.tracer.trace('mediaPlayback.recover.skipped', {
|
|
11986
|
-
|
|
12003
|
+
...this.traceInfo,
|
|
11987
12004
|
reason: skipReason,
|
|
11988
12005
|
});
|
|
11989
12006
|
return;
|
|
@@ -11992,9 +12009,10 @@ class MediaPlaybackWatchdog {
|
|
|
11992
12009
|
this.stopped = true;
|
|
11993
12010
|
this.clearTimers();
|
|
11994
12011
|
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
11995
|
-
|
|
12012
|
+
...this.traceInfo,
|
|
11996
12013
|
attempts: this.attempt,
|
|
11997
12014
|
});
|
|
12015
|
+
this.onGiveUp();
|
|
11998
12016
|
return;
|
|
11999
12017
|
}
|
|
12000
12018
|
const delay = this.attempt === 0 ? 0 : retryInterval(this.attempt);
|
|
@@ -12022,7 +12040,7 @@ class MediaPlaybackWatchdog {
|
|
|
12022
12040
|
return;
|
|
12023
12041
|
this.attempt += 1;
|
|
12024
12042
|
this.tracer.trace('mediaPlayback.recover.attempt', {
|
|
12025
|
-
|
|
12043
|
+
...this.traceInfo,
|
|
12026
12044
|
attempt: this.attempt,
|
|
12027
12045
|
});
|
|
12028
12046
|
try {
|
|
@@ -12037,6 +12055,41 @@ class MediaPlaybackWatchdog {
|
|
|
12037
12055
|
};
|
|
12038
12056
|
}
|
|
12039
12057
|
|
|
12058
|
+
/**
|
|
12059
|
+
* Checks whether media playback and capture are likely interrupted: the page
|
|
12060
|
+
* is hidden, or WebKit reports the audio session as interrupted (a phone
|
|
12061
|
+
* call, Siri, or another app taking over audio).
|
|
12062
|
+
*/
|
|
12063
|
+
const isMediaInterrupted = () => {
|
|
12064
|
+
if (typeof document === 'undefined')
|
|
12065
|
+
return false;
|
|
12066
|
+
return (document.visibilityState === 'hidden' ||
|
|
12067
|
+
navigator.audioSession?.state === 'interrupted');
|
|
12068
|
+
};
|
|
12069
|
+
/**
|
|
12070
|
+
* Invokes `onResumed` every time the page leaves a media interruption,
|
|
12071
|
+
* as defined by {@link isMediaInterrupted}.
|
|
12072
|
+
*
|
|
12073
|
+
* @param onResumed the callback to invoke.
|
|
12074
|
+
* @returns a function that stops watching.
|
|
12075
|
+
*/
|
|
12076
|
+
const watchMediaInterruptions = (onResumed) => {
|
|
12077
|
+
if (typeof document === 'undefined')
|
|
12078
|
+
return () => { };
|
|
12079
|
+
let interrupted = isMediaInterrupted();
|
|
12080
|
+
const check = () => {
|
|
12081
|
+
const wasInterrupted = interrupted;
|
|
12082
|
+
interrupted = isMediaInterrupted();
|
|
12083
|
+
if (wasInterrupted && !interrupted)
|
|
12084
|
+
onResumed();
|
|
12085
|
+
};
|
|
12086
|
+
const controller = new AbortController();
|
|
12087
|
+
const { signal } = controller;
|
|
12088
|
+
document.addEventListener('visibilitychange', check, { signal });
|
|
12089
|
+
navigator.audioSession?.addEventListener('statechange', check, { signal });
|
|
12090
|
+
return () => controller.abort();
|
|
12091
|
+
};
|
|
12092
|
+
|
|
12040
12093
|
/**
|
|
12041
12094
|
* A manager class that handles dynascale related tasks like:
|
|
12042
12095
|
*
|
|
@@ -12050,6 +12103,9 @@ class DynascaleManager {
|
|
|
12050
12103
|
tracer;
|
|
12051
12104
|
useWebAudio = false;
|
|
12052
12105
|
audioContext;
|
|
12106
|
+
playbackWatchdogs = new Set();
|
|
12107
|
+
stopWatchingInterruptions;
|
|
12108
|
+
stopTracingAudioSession;
|
|
12053
12109
|
trackSubscriptionManager;
|
|
12054
12110
|
blockedAudioTracker;
|
|
12055
12111
|
/**
|
|
@@ -12066,6 +12122,10 @@ class DynascaleManager {
|
|
|
12066
12122
|
* Closes the audio context if it was created.
|
|
12067
12123
|
*/
|
|
12068
12124
|
dispose = async () => {
|
|
12125
|
+
this.stopWatchingInterruptions?.();
|
|
12126
|
+
this.stopWatchingInterruptions = undefined;
|
|
12127
|
+
this.stopTracingAudioSession?.();
|
|
12128
|
+
this.stopTracingAudioSession = undefined;
|
|
12069
12129
|
const context = this.audioContext;
|
|
12070
12130
|
if (context && context.state !== 'closed') {
|
|
12071
12131
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
@@ -12205,10 +12265,12 @@ class DynascaleManager {
|
|
|
12205
12265
|
// without prior user interaction:
|
|
12206
12266
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
|
|
12207
12267
|
videoElement.muted = true;
|
|
12208
|
-
const playbackWatchdog =
|
|
12268
|
+
const playbackWatchdog = this.createPlaybackWatchdog({
|
|
12209
12269
|
element: videoElement,
|
|
12210
12270
|
kind: 'video',
|
|
12211
12271
|
tracer: this.tracer,
|
|
12272
|
+
sessionId,
|
|
12273
|
+
trackType,
|
|
12212
12274
|
});
|
|
12213
12275
|
const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
|
|
12214
12276
|
const streamSubscription = participant$
|
|
@@ -12236,7 +12298,7 @@ class DynascaleManager {
|
|
|
12236
12298
|
publishedTracksSubscription?.unsubscribe();
|
|
12237
12299
|
streamSubscription.unsubscribe();
|
|
12238
12300
|
resizeObserver?.disconnect();
|
|
12239
|
-
|
|
12301
|
+
this.disposePlaybackWatchdog(playbackWatchdog);
|
|
12240
12302
|
};
|
|
12241
12303
|
};
|
|
12242
12304
|
/**
|
|
@@ -12254,6 +12316,7 @@ class DynascaleManager {
|
|
|
12254
12316
|
const participant = this.callState.findParticipantBySessionId(sessionId);
|
|
12255
12317
|
if (!participant || participant.isLocalParticipant)
|
|
12256
12318
|
return;
|
|
12319
|
+
this.traceAudioSessionState();
|
|
12257
12320
|
const participant$ = this.callState.participants$.pipe(map((ps) => ps.find((p) => p.sessionId === sessionId)), takeWhile((p) => !!p), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
|
|
12258
12321
|
const updateSinkId = (deviceId, audioContext) => {
|
|
12259
12322
|
if (!deviceId)
|
|
@@ -12273,7 +12336,9 @@ class DynascaleManager {
|
|
|
12273
12336
|
let sourceNode = undefined;
|
|
12274
12337
|
let gainNode = undefined;
|
|
12275
12338
|
let audioWatchdog = undefined;
|
|
12339
|
+
let blockedByGiveUp = false;
|
|
12276
12340
|
const clearBlockedAudio = () => {
|
|
12341
|
+
blockedByGiveUp = false;
|
|
12277
12342
|
if (!this.blockedAudioTracker.isBlocked(audioElement))
|
|
12278
12343
|
return;
|
|
12279
12344
|
this.blockedAudioTracker.markBlocked(audioElement, false);
|
|
@@ -12281,15 +12346,18 @@ class DynascaleManager {
|
|
|
12281
12346
|
audioElement.addEventListener('playing', clearBlockedAudio);
|
|
12282
12347
|
const isAudioTrack = trackType === 'audioTrack';
|
|
12283
12348
|
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
12349
|
+
let sourceChangeTimer;
|
|
12284
12350
|
const updateMediaStreamSubscription = participant$
|
|
12285
12351
|
.pipe(distinctUntilKeyChanged(trackKey))
|
|
12286
12352
|
.subscribe((p) => {
|
|
12287
12353
|
const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
|
|
12288
12354
|
if (audioElement.srcObject === source)
|
|
12289
12355
|
return;
|
|
12290
|
-
|
|
12356
|
+
clearTimeout(sourceChangeTimer);
|
|
12357
|
+
sourceChangeTimer = setTimeout(() => {
|
|
12358
|
+
sourceChangeTimer = undefined;
|
|
12291
12359
|
audioElement.srcObject = source ?? null;
|
|
12292
|
-
audioWatchdog
|
|
12360
|
+
this.disposePlaybackWatchdog(audioWatchdog);
|
|
12293
12361
|
audioWatchdog = undefined;
|
|
12294
12362
|
if (!source) {
|
|
12295
12363
|
clearBlockedAudio();
|
|
@@ -12320,15 +12388,25 @@ class DynascaleManager {
|
|
|
12320
12388
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
12321
12389
|
if (e.name === 'NotAllowedError') {
|
|
12322
12390
|
this.tracer.trace('audioPlaybackBlocked', null);
|
|
12391
|
+
blockedByGiveUp = false;
|
|
12323
12392
|
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
12324
12393
|
}
|
|
12325
12394
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
12326
12395
|
});
|
|
12327
|
-
audioWatchdog =
|
|
12396
|
+
audioWatchdog = this.createPlaybackWatchdog({
|
|
12328
12397
|
element: audioElement,
|
|
12329
12398
|
kind: 'audio',
|
|
12330
12399
|
tracer: this.tracer,
|
|
12331
|
-
|
|
12400
|
+
sessionId,
|
|
12401
|
+
trackType,
|
|
12402
|
+
// only an autoplay-policy block needs a user gesture; a give-up
|
|
12403
|
+
// block is for the UI and must not stop a re-armed cycle
|
|
12404
|
+
isBlocked: () => !blockedByGiveUp &&
|
|
12405
|
+
this.blockedAudioTracker.isBlocked(audioElement),
|
|
12406
|
+
onGiveUp: () => {
|
|
12407
|
+
blockedByGiveUp = true;
|
|
12408
|
+
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
12409
|
+
},
|
|
12332
12410
|
});
|
|
12333
12411
|
}
|
|
12334
12412
|
const { selectedDevice } = this.speaker.state;
|
|
@@ -12358,13 +12436,51 @@ class DynascaleManager {
|
|
|
12358
12436
|
sinkIdSubscription?.unsubscribe();
|
|
12359
12437
|
volumeSubscription.unsubscribe();
|
|
12360
12438
|
updateMediaStreamSubscription.unsubscribe();
|
|
12439
|
+
clearTimeout(sourceChangeTimer);
|
|
12361
12440
|
audioElement.srcObject = null;
|
|
12362
12441
|
sourceNode?.disconnect();
|
|
12363
12442
|
gainNode?.disconnect();
|
|
12364
|
-
audioWatchdog
|
|
12443
|
+
this.disposePlaybackWatchdog(audioWatchdog);
|
|
12365
12444
|
audioWatchdog = undefined;
|
|
12366
12445
|
};
|
|
12367
12446
|
};
|
|
12447
|
+
/**
|
|
12448
|
+
* Creates a playback watchdog that is re-armed whenever a media
|
|
12449
|
+
* interruption (backgrounding, phone call, Siri) ends.
|
|
12450
|
+
*/
|
|
12451
|
+
createPlaybackWatchdog = (opts) => {
|
|
12452
|
+
const watchdog = new MediaPlaybackWatchdog(opts);
|
|
12453
|
+
this.playbackWatchdogs.add(watchdog);
|
|
12454
|
+
this.stopWatchingInterruptions ??= watchMediaInterruptions(this.rearmPlayback);
|
|
12455
|
+
return watchdog;
|
|
12456
|
+
};
|
|
12457
|
+
disposePlaybackWatchdog = (watchdog) => {
|
|
12458
|
+
if (!watchdog)
|
|
12459
|
+
return;
|
|
12460
|
+
watchdog.dispose();
|
|
12461
|
+
this.playbackWatchdogs.delete(watchdog);
|
|
12462
|
+
};
|
|
12463
|
+
rearmPlayback = () => {
|
|
12464
|
+
this.tracer.trace('mediaPlayback.rearm', this.playbackWatchdogs.size);
|
|
12465
|
+
this.playbackWatchdogs.forEach((watchdog) => watchdog.rearm());
|
|
12466
|
+
};
|
|
12467
|
+
/**
|
|
12468
|
+
* Traces the WebKit audio session state (initial value and changes) in
|
|
12469
|
+
* both playback modes, as it explains most iOS playback interruptions.
|
|
12470
|
+
*/
|
|
12471
|
+
traceAudioSessionState = () => {
|
|
12472
|
+
const audioSession = navigator.audioSession;
|
|
12473
|
+
if (!audioSession || this.stopTracingAudioSession)
|
|
12474
|
+
return;
|
|
12475
|
+
const trace = () => {
|
|
12476
|
+
this.tracer.trace('audioSession.state', audioSession.state);
|
|
12477
|
+
};
|
|
12478
|
+
trace();
|
|
12479
|
+
audioSession.addEventListener('statechange', trace);
|
|
12480
|
+
this.stopTracingAudioSession = () => {
|
|
12481
|
+
audioSession.removeEventListener('statechange', trace);
|
|
12482
|
+
};
|
|
12483
|
+
};
|
|
12368
12484
|
getOrCreateAudioContext = () => {
|
|
12369
12485
|
if (!this.useWebAudio)
|
|
12370
12486
|
return;
|
|
@@ -12387,7 +12503,6 @@ class DynascaleManager {
|
|
|
12387
12503
|
audioSession.type = 'play-and-record';
|
|
12388
12504
|
let isSessionInterrupted = false;
|
|
12389
12505
|
audioSession.addEventListener('statechange', () => {
|
|
12390
|
-
this.tracer.trace('audioSession.state', audioSession.state);
|
|
12391
12506
|
if (audioSession.state === 'interrupted') {
|
|
12392
12507
|
isSessionInterrupted = true;
|
|
12393
12508
|
}
|
|
@@ -13946,9 +14061,22 @@ class DeviceManager {
|
|
|
13946
14061
|
}
|
|
13947
14062
|
const devices = getCurrentValue(this.listDevices()) || [];
|
|
13948
14063
|
const currentDevice = this.findDevice(devices, selectedDevice) ??
|
|
13949
|
-
createSyntheticDevice(selectedDevice, deviceKind);
|
|
14064
|
+
createSyntheticDevice(selectedDevice, deviceKind, this.getActiveTrackLabel(selectedDevice));
|
|
13950
14065
|
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
13951
14066
|
}
|
|
14067
|
+
/**
|
|
14068
|
+
* Returns the label of the active track if it was captured from the given
|
|
14069
|
+
* device. Used as a fallback when the device list hasn't been enumerated yet,
|
|
14070
|
+
* so the persisted preference still carries the device label.
|
|
14071
|
+
*/
|
|
14072
|
+
getActiveTrackLabel = (deviceId) => {
|
|
14073
|
+
const stream = this.state.rootMediaStream;
|
|
14074
|
+
const tracks = this.trackType === TrackType.AUDIO
|
|
14075
|
+
? stream?.getAudioTracks()
|
|
14076
|
+
: stream?.getVideoTracks();
|
|
14077
|
+
const track = tracks?.find((t) => t.getSettings().deviceId === deviceId);
|
|
14078
|
+
return track?.label;
|
|
14079
|
+
};
|
|
13952
14080
|
async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
|
|
13953
14081
|
const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
13954
14082
|
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
@@ -16784,9 +16912,6 @@ class Call {
|
|
|
16784
16912
|
options: statsOptions,
|
|
16785
16913
|
subscriber: this.subscriber,
|
|
16786
16914
|
publisher: this.publisher,
|
|
16787
|
-
microphone: this.microphone,
|
|
16788
|
-
camera: this.camera,
|
|
16789
|
-
state: this.state,
|
|
16790
16915
|
tracer: this.tracer,
|
|
16791
16916
|
unifiedSessionId,
|
|
16792
16917
|
});
|
|
@@ -19638,11 +19763,11 @@ class StreamClient {
|
|
|
19638
19763
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
19639
19764
|
};
|
|
19640
19765
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
19641
|
-
"1.61.
|
|
19766
|
+
"1.61.2";
|
|
19642
19767
|
getUserAgent = () => {
|
|
19643
19768
|
if (!this.cachedUserAgent) {
|
|
19644
19769
|
const { clientAppIdentifier = {} } = this.options;
|
|
19645
|
-
const { sdkName = 'js', sdkVersion = "1.61.
|
|
19770
|
+
const { sdkName = 'js', sdkVersion = "1.61.2", ...extras } = clientAppIdentifier;
|
|
19646
19771
|
this.cachedUserAgent = [
|
|
19647
19772
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
19648
19773
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -20582,6 +20707,10 @@ class StreamVideoClient {
|
|
|
20582
20707
|
const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
|
|
20583
20708
|
if (call) {
|
|
20584
20709
|
call.updateFromCallStateResponse(c);
|
|
20710
|
+
// ring events missed while the WS was down aren't replayed
|
|
20711
|
+
reconcileRingState(call, 'ring-poll-api').catch((err) => {
|
|
20712
|
+
call.logger.error('Failed to reconcile the ring state after rewatch', err);
|
|
20713
|
+
});
|
|
20585
20714
|
}
|
|
20586
20715
|
}
|
|
20587
20716
|
return;
|