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