@stream-io/video-client 1.61.0 → 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 +15 -0
- package/dist/index.browser.es.js +292 -154
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +292 -154
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +292 -154
- 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 +8 -3
- package/src/reporting/__tests__/ClientEventReporter.test.ts +2 -2
- 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/rtc/BasePeerConnection.ts +6 -1
- package/src/rtc/__tests__/Publisher.test.ts +50 -0
- 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
|
/**
|
|
@@ -8774,7 +8695,12 @@ class BasePeerConnection {
|
|
|
8774
8695
|
}
|
|
8775
8696
|
// we can't recover from a failed connection state (contrary to ICE)
|
|
8776
8697
|
if (state === 'failed') {
|
|
8777
|
-
|
|
8698
|
+
const reason = this.iceHasEverConnected
|
|
8699
|
+
? ReconnectReason.CONNECTION_FAILED
|
|
8700
|
+
: ReconnectReason.ICE_NEVER_CONNECTED;
|
|
8701
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
8702
|
+
this.preConnectStuckTimeout = undefined;
|
|
8703
|
+
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, reason, this.peerType);
|
|
8778
8704
|
return;
|
|
8779
8705
|
}
|
|
8780
8706
|
this.handleConnectionStateUpdate(state);
|
|
@@ -10773,15 +10699,16 @@ class StreamSfuClient {
|
|
|
10773
10699
|
/**
|
|
10774
10700
|
* Decides what a ringing call should do next, based on the current call state.
|
|
10775
10701
|
*
|
|
10776
|
-
* The `call.accepted
|
|
10777
|
-
*
|
|
10778
|
-
* handlers rely on `CallState.updateFromEvent`, which runs
|
|
10779
|
-
* 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.
|
|
10780
10707
|
*
|
|
10781
10708
|
* @param call the call to reconcile.
|
|
10782
|
-
* @param joinSource
|
|
10783
|
-
*
|
|
10784
|
-
*
|
|
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).
|
|
10785
10712
|
* @returns whether the ring reached a terminal state. A failed join is not
|
|
10786
10713
|
* terminal: the caller should keep trying while the ring is open.
|
|
10787
10714
|
*/
|
|
@@ -10836,10 +10763,16 @@ const reconcileAsCaller = async (call, joinSource) => {
|
|
|
10836
10763
|
return false;
|
|
10837
10764
|
};
|
|
10838
10765
|
// the current user's own accept or reject, on this or another device, is
|
|
10839
|
-
// 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.
|
|
10840
10768
|
const reconcileAsCallee = async (call) => {
|
|
10841
|
-
const
|
|
10842
|
-
|
|
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 ?? {};
|
|
10843
10776
|
if (createdById && rejectedBy[createdById]) {
|
|
10844
10777
|
call.logger.info('ring: the caller cancelled, leaving');
|
|
10845
10778
|
return leave(call, {
|
|
@@ -10888,7 +10821,7 @@ class RingStatePoller {
|
|
|
10888
10821
|
unsubscribe = [];
|
|
10889
10822
|
constructor(call, options = {}) {
|
|
10890
10823
|
this.call = call;
|
|
10891
|
-
this.startAfterMs = options.startAfterMs ??
|
|
10824
|
+
this.startAfterMs = options.startAfterMs ?? 9_000;
|
|
10892
10825
|
this.intervalMs = options.intervalMs ?? 5_000;
|
|
10893
10826
|
}
|
|
10894
10827
|
/**
|
|
@@ -11553,12 +11486,33 @@ const registerRingingCallEventHandlers = (call) => {
|
|
|
11553
11486
|
};
|
|
11554
11487
|
|
|
11555
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
|
+
};
|
|
11556
11507
|
/**
|
|
11557
11508
|
* Tracks audio element bindings and periodically warns about
|
|
11558
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.
|
|
11559
11512
|
*/
|
|
11560
11513
|
class AudioBindingsWatchdog {
|
|
11561
11514
|
bindings = new Map();
|
|
11515
|
+
tracedStates = new Map();
|
|
11562
11516
|
enabled = true;
|
|
11563
11517
|
watchdogInterval;
|
|
11564
11518
|
unsubscribeCallingState;
|
|
@@ -11569,8 +11523,6 @@ class AudioBindingsWatchdog {
|
|
|
11569
11523
|
this.tracer = tracer;
|
|
11570
11524
|
this.state = state;
|
|
11571
11525
|
this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
|
|
11572
|
-
if (!this.enabled)
|
|
11573
|
-
return;
|
|
11574
11526
|
if (callingState !== exports.CallingState.JOINED) {
|
|
11575
11527
|
this.stop();
|
|
11576
11528
|
}
|
|
@@ -11599,15 +11551,15 @@ class AudioBindingsWatchdog {
|
|
|
11599
11551
|
this.bindings.delete(toBindingKey(sessionId, trackType));
|
|
11600
11552
|
};
|
|
11601
11553
|
/**
|
|
11602
|
-
* Enables or disables the
|
|
11603
|
-
*
|
|
11554
|
+
* Enables or disables the dangling binding warnings.
|
|
11555
|
+
* Bindings are still tracked and their playback state is still traced.
|
|
11604
11556
|
*/
|
|
11605
11557
|
setEnabled = (enabled) => {
|
|
11606
11558
|
this.enabled = enabled;
|
|
11607
11559
|
if (enabled) {
|
|
11608
11560
|
this.start();
|
|
11609
11561
|
}
|
|
11610
|
-
else {
|
|
11562
|
+
else if (this.state.callingState !== exports.CallingState.JOINED) {
|
|
11611
11563
|
this.stop();
|
|
11612
11564
|
}
|
|
11613
11565
|
};
|
|
@@ -11617,33 +11569,61 @@ class AudioBindingsWatchdog {
|
|
|
11617
11569
|
dispose = () => {
|
|
11618
11570
|
this.stop();
|
|
11619
11571
|
this.bindings.clear();
|
|
11572
|
+
this.tracedStates.clear();
|
|
11620
11573
|
this.unsubscribeCallingState();
|
|
11621
11574
|
};
|
|
11622
11575
|
start = () => {
|
|
11623
11576
|
clearInterval(this.watchdogInterval);
|
|
11624
11577
|
this.watchdogInterval = setInterval(() => {
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
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);
|
|
11640
11593
|
}
|
|
11641
|
-
if (
|
|
11642
|
-
|
|
11643
|
-
this.
|
|
11644
|
-
|
|
11594
|
+
if (screenShareAudioStream &&
|
|
11595
|
+
publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
|
|
11596
|
+
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
|
|
11597
|
+
danglingUserIds.push(userId);
|
|
11645
11598
|
}
|
|
11646
|
-
}
|
|
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);
|
|
11647
11627
|
};
|
|
11648
11628
|
stop = () => {
|
|
11649
11629
|
clearInterval(this.watchdogInterval);
|
|
@@ -11922,7 +11902,9 @@ class TrackSubscriptionManager {
|
|
|
11922
11902
|
class MediaPlaybackWatchdog {
|
|
11923
11903
|
logger = videoLoggerSystem.getLogger('MediaPlaybackWatchdog');
|
|
11924
11904
|
kind;
|
|
11905
|
+
traceInfo;
|
|
11925
11906
|
isBlocked;
|
|
11907
|
+
onGiveUp;
|
|
11926
11908
|
element;
|
|
11927
11909
|
tracer;
|
|
11928
11910
|
controller = new AbortController();
|
|
@@ -11934,8 +11916,14 @@ class MediaPlaybackWatchdog {
|
|
|
11934
11916
|
constructor(opts) {
|
|
11935
11917
|
this.element = opts.element;
|
|
11936
11918
|
this.kind = opts.kind;
|
|
11919
|
+
this.traceInfo = {
|
|
11920
|
+
kind: opts.kind,
|
|
11921
|
+
sessionId: opts.sessionId,
|
|
11922
|
+
trackType: opts.trackType,
|
|
11923
|
+
};
|
|
11937
11924
|
this.tracer = opts.tracer;
|
|
11938
11925
|
this.isBlocked = opts.isBlocked ?? (() => false);
|
|
11926
|
+
this.onGiveUp = opts.onGiveUp ?? (() => { });
|
|
11939
11927
|
this.attach();
|
|
11940
11928
|
}
|
|
11941
11929
|
attach = () => {
|
|
@@ -11953,6 +11941,23 @@ class MediaPlaybackWatchdog {
|
|
|
11953
11941
|
this.controller.abort();
|
|
11954
11942
|
this.clearTimers();
|
|
11955
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
|
+
};
|
|
11956
11961
|
clearTimers = () => {
|
|
11957
11962
|
if (this.pendingTimer)
|
|
11958
11963
|
clearTimeout(this.pendingTimer);
|
|
@@ -11974,17 +11979,34 @@ class MediaPlaybackWatchdog {
|
|
|
11974
11979
|
if (this.element.paused)
|
|
11975
11980
|
return;
|
|
11976
11981
|
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11977
|
-
|
|
11982
|
+
...this.traceInfo,
|
|
11978
11983
|
attempts: this.attempt,
|
|
11979
11984
|
});
|
|
11980
11985
|
this.attempt = 0;
|
|
11981
11986
|
this.stopped = false;
|
|
11982
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
|
+
*/
|
|
11983
11994
|
onPauseOrSuspend = (event) => {
|
|
11984
|
-
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
|
+
}
|
|
11985
12006
|
return;
|
|
12007
|
+
}
|
|
11986
12008
|
this.tracer.trace('mediaPlayback.paused', {
|
|
11987
|
-
|
|
12009
|
+
...this.traceInfo,
|
|
11988
12010
|
reason: event.type,
|
|
11989
12011
|
});
|
|
11990
12012
|
if (this.settleTimer)
|
|
@@ -11998,7 +12020,7 @@ class MediaPlaybackWatchdog {
|
|
|
11998
12020
|
const skipReason = this.computeSkipReason();
|
|
11999
12021
|
if (skipReason) {
|
|
12000
12022
|
this.tracer.trace('mediaPlayback.recover.skipped', {
|
|
12001
|
-
|
|
12023
|
+
...this.traceInfo,
|
|
12002
12024
|
reason: skipReason,
|
|
12003
12025
|
});
|
|
12004
12026
|
return;
|
|
@@ -12007,9 +12029,10 @@ class MediaPlaybackWatchdog {
|
|
|
12007
12029
|
this.stopped = true;
|
|
12008
12030
|
this.clearTimers();
|
|
12009
12031
|
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
12010
|
-
|
|
12032
|
+
...this.traceInfo,
|
|
12011
12033
|
attempts: this.attempt,
|
|
12012
12034
|
});
|
|
12035
|
+
this.onGiveUp();
|
|
12013
12036
|
return;
|
|
12014
12037
|
}
|
|
12015
12038
|
const delay = this.attempt === 0 ? 0 : retryInterval(this.attempt);
|
|
@@ -12037,7 +12060,7 @@ class MediaPlaybackWatchdog {
|
|
|
12037
12060
|
return;
|
|
12038
12061
|
this.attempt += 1;
|
|
12039
12062
|
this.tracer.trace('mediaPlayback.recover.attempt', {
|
|
12040
|
-
|
|
12063
|
+
...this.traceInfo,
|
|
12041
12064
|
attempt: this.attempt,
|
|
12042
12065
|
});
|
|
12043
12066
|
try {
|
|
@@ -12052,6 +12075,41 @@ class MediaPlaybackWatchdog {
|
|
|
12052
12075
|
};
|
|
12053
12076
|
}
|
|
12054
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
|
+
|
|
12055
12113
|
/**
|
|
12056
12114
|
* A manager class that handles dynascale related tasks like:
|
|
12057
12115
|
*
|
|
@@ -12065,6 +12123,9 @@ class DynascaleManager {
|
|
|
12065
12123
|
tracer;
|
|
12066
12124
|
useWebAudio = false;
|
|
12067
12125
|
audioContext;
|
|
12126
|
+
playbackWatchdogs = new Set();
|
|
12127
|
+
stopWatchingInterruptions;
|
|
12128
|
+
stopTracingAudioSession;
|
|
12068
12129
|
trackSubscriptionManager;
|
|
12069
12130
|
blockedAudioTracker;
|
|
12070
12131
|
/**
|
|
@@ -12081,6 +12142,10 @@ class DynascaleManager {
|
|
|
12081
12142
|
* Closes the audio context if it was created.
|
|
12082
12143
|
*/
|
|
12083
12144
|
dispose = async () => {
|
|
12145
|
+
this.stopWatchingInterruptions?.();
|
|
12146
|
+
this.stopWatchingInterruptions = undefined;
|
|
12147
|
+
this.stopTracingAudioSession?.();
|
|
12148
|
+
this.stopTracingAudioSession = undefined;
|
|
12084
12149
|
const context = this.audioContext;
|
|
12085
12150
|
if (context && context.state !== 'closed') {
|
|
12086
12151
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
@@ -12220,10 +12285,12 @@ class DynascaleManager {
|
|
|
12220
12285
|
// without prior user interaction:
|
|
12221
12286
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
|
|
12222
12287
|
videoElement.muted = true;
|
|
12223
|
-
const playbackWatchdog =
|
|
12288
|
+
const playbackWatchdog = this.createPlaybackWatchdog({
|
|
12224
12289
|
element: videoElement,
|
|
12225
12290
|
kind: 'video',
|
|
12226
12291
|
tracer: this.tracer,
|
|
12292
|
+
sessionId,
|
|
12293
|
+
trackType,
|
|
12227
12294
|
});
|
|
12228
12295
|
const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
|
|
12229
12296
|
const streamSubscription = participant$
|
|
@@ -12251,7 +12318,7 @@ class DynascaleManager {
|
|
|
12251
12318
|
publishedTracksSubscription?.unsubscribe();
|
|
12252
12319
|
streamSubscription.unsubscribe();
|
|
12253
12320
|
resizeObserver?.disconnect();
|
|
12254
|
-
|
|
12321
|
+
this.disposePlaybackWatchdog(playbackWatchdog);
|
|
12255
12322
|
};
|
|
12256
12323
|
};
|
|
12257
12324
|
/**
|
|
@@ -12269,6 +12336,7 @@ class DynascaleManager {
|
|
|
12269
12336
|
const participant = this.callState.findParticipantBySessionId(sessionId);
|
|
12270
12337
|
if (!participant || participant.isLocalParticipant)
|
|
12271
12338
|
return;
|
|
12339
|
+
this.traceAudioSessionState();
|
|
12272
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 }));
|
|
12273
12341
|
const updateSinkId = (deviceId, audioContext) => {
|
|
12274
12342
|
if (!deviceId)
|
|
@@ -12288,7 +12356,9 @@ class DynascaleManager {
|
|
|
12288
12356
|
let sourceNode = undefined;
|
|
12289
12357
|
let gainNode = undefined;
|
|
12290
12358
|
let audioWatchdog = undefined;
|
|
12359
|
+
let blockedByGiveUp = false;
|
|
12291
12360
|
const clearBlockedAudio = () => {
|
|
12361
|
+
blockedByGiveUp = false;
|
|
12292
12362
|
if (!this.blockedAudioTracker.isBlocked(audioElement))
|
|
12293
12363
|
return;
|
|
12294
12364
|
this.blockedAudioTracker.markBlocked(audioElement, false);
|
|
@@ -12296,15 +12366,18 @@ class DynascaleManager {
|
|
|
12296
12366
|
audioElement.addEventListener('playing', clearBlockedAudio);
|
|
12297
12367
|
const isAudioTrack = trackType === 'audioTrack';
|
|
12298
12368
|
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
12369
|
+
let sourceChangeTimer;
|
|
12299
12370
|
const updateMediaStreamSubscription = participant$
|
|
12300
12371
|
.pipe(rxjs.distinctUntilKeyChanged(trackKey))
|
|
12301
12372
|
.subscribe((p) => {
|
|
12302
12373
|
const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
|
|
12303
12374
|
if (audioElement.srcObject === source)
|
|
12304
12375
|
return;
|
|
12305
|
-
|
|
12376
|
+
clearTimeout(sourceChangeTimer);
|
|
12377
|
+
sourceChangeTimer = setTimeout(() => {
|
|
12378
|
+
sourceChangeTimer = undefined;
|
|
12306
12379
|
audioElement.srcObject = source ?? null;
|
|
12307
|
-
audioWatchdog
|
|
12380
|
+
this.disposePlaybackWatchdog(audioWatchdog);
|
|
12308
12381
|
audioWatchdog = undefined;
|
|
12309
12382
|
if (!source) {
|
|
12310
12383
|
clearBlockedAudio();
|
|
@@ -12335,15 +12408,25 @@ class DynascaleManager {
|
|
|
12335
12408
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
12336
12409
|
if (e.name === 'NotAllowedError') {
|
|
12337
12410
|
this.tracer.trace('audioPlaybackBlocked', null);
|
|
12411
|
+
blockedByGiveUp = false;
|
|
12338
12412
|
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
12339
12413
|
}
|
|
12340
12414
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
12341
12415
|
});
|
|
12342
|
-
audioWatchdog =
|
|
12416
|
+
audioWatchdog = this.createPlaybackWatchdog({
|
|
12343
12417
|
element: audioElement,
|
|
12344
12418
|
kind: 'audio',
|
|
12345
12419
|
tracer: this.tracer,
|
|
12346
|
-
|
|
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
|
+
},
|
|
12347
12430
|
});
|
|
12348
12431
|
}
|
|
12349
12432
|
const { selectedDevice } = this.speaker.state;
|
|
@@ -12373,13 +12456,51 @@ class DynascaleManager {
|
|
|
12373
12456
|
sinkIdSubscription?.unsubscribe();
|
|
12374
12457
|
volumeSubscription.unsubscribe();
|
|
12375
12458
|
updateMediaStreamSubscription.unsubscribe();
|
|
12459
|
+
clearTimeout(sourceChangeTimer);
|
|
12376
12460
|
audioElement.srcObject = null;
|
|
12377
12461
|
sourceNode?.disconnect();
|
|
12378
12462
|
gainNode?.disconnect();
|
|
12379
|
-
audioWatchdog
|
|
12463
|
+
this.disposePlaybackWatchdog(audioWatchdog);
|
|
12380
12464
|
audioWatchdog = undefined;
|
|
12381
12465
|
};
|
|
12382
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
|
+
};
|
|
12383
12504
|
getOrCreateAudioContext = () => {
|
|
12384
12505
|
if (!this.useWebAudio)
|
|
12385
12506
|
return;
|
|
@@ -12402,7 +12523,6 @@ class DynascaleManager {
|
|
|
12402
12523
|
audioSession.type = 'play-and-record';
|
|
12403
12524
|
let isSessionInterrupted = false;
|
|
12404
12525
|
audioSession.addEventListener('statechange', () => {
|
|
12405
|
-
this.tracer.trace('audioSession.state', audioSession.state);
|
|
12406
12526
|
if (audioSession.state === 'interrupted') {
|
|
12407
12527
|
isSessionInterrupted = true;
|
|
12408
12528
|
}
|
|
@@ -13961,9 +14081,22 @@ class DeviceManager {
|
|
|
13961
14081
|
}
|
|
13962
14082
|
const devices = getCurrentValue(this.listDevices()) || [];
|
|
13963
14083
|
const currentDevice = this.findDevice(devices, selectedDevice) ??
|
|
13964
|
-
createSyntheticDevice(selectedDevice, deviceKind);
|
|
14084
|
+
createSyntheticDevice(selectedDevice, deviceKind, this.getActiveTrackLabel(selectedDevice));
|
|
13965
14085
|
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
13966
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
|
+
};
|
|
13967
14100
|
async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
|
|
13968
14101
|
const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
13969
14102
|
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
@@ -16799,9 +16932,6 @@ class Call {
|
|
|
16799
16932
|
options: statsOptions,
|
|
16800
16933
|
subscriber: this.subscriber,
|
|
16801
16934
|
publisher: this.publisher,
|
|
16802
|
-
microphone: this.microphone,
|
|
16803
|
-
camera: this.camera,
|
|
16804
|
-
state: this.state,
|
|
16805
16935
|
tracer: this.tracer,
|
|
16806
16936
|
unifiedSessionId,
|
|
16807
16937
|
});
|
|
@@ -19651,11 +19781,11 @@ class StreamClient {
|
|
|
19651
19781
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
19652
19782
|
};
|
|
19653
19783
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
19654
|
-
"1.61.
|
|
19784
|
+
"1.61.2";
|
|
19655
19785
|
getUserAgent = () => {
|
|
19656
19786
|
if (!this.cachedUserAgent) {
|
|
19657
19787
|
const { clientAppIdentifier = {} } = this.options;
|
|
19658
|
-
const { sdkName = 'js', sdkVersion = "1.61.
|
|
19788
|
+
const { sdkName = 'js', sdkVersion = "1.61.2", ...extras } = clientAppIdentifier;
|
|
19659
19789
|
this.cachedUserAgent = [
|
|
19660
19790
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
19661
19791
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -20190,7 +20320,11 @@ class ClientEventReporter {
|
|
|
20190
20320
|
return;
|
|
20191
20321
|
}
|
|
20192
20322
|
if (event.state === 'failed') {
|
|
20193
|
-
|
|
20323
|
+
const iceConnected = event.iceConnectionState === 'connected' ||
|
|
20324
|
+
event.iceConnectionState === 'completed';
|
|
20325
|
+
this.emitPeerConnectionFailure(cid, role, iceConnected ? 'DTLS_CONNECTIVITY_FAILED' : 'ICE_CONNECTIVITY_FAILED', iceConnected
|
|
20326
|
+
? 'DTLS connectivity checks failed'
|
|
20327
|
+
: 'ICE connectivity checks failed');
|
|
20194
20328
|
return;
|
|
20195
20329
|
}
|
|
20196
20330
|
switch (event.state) {
|
|
@@ -20591,6 +20725,10 @@ class StreamVideoClient {
|
|
|
20591
20725
|
const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
|
|
20592
20726
|
if (call) {
|
|
20593
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
|
+
});
|
|
20594
20732
|
}
|
|
20595
20733
|
}
|
|
20596
20734
|
return;
|