@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.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
|
/**
|
|
@@ -8755,7 +8676,12 @@ class BasePeerConnection {
|
|
|
8755
8676
|
}
|
|
8756
8677
|
// we can't recover from a failed connection state (contrary to ICE)
|
|
8757
8678
|
if (state === 'failed') {
|
|
8758
|
-
|
|
8679
|
+
const reason = this.iceHasEverConnected
|
|
8680
|
+
? ReconnectReason.CONNECTION_FAILED
|
|
8681
|
+
: ReconnectReason.ICE_NEVER_CONNECTED;
|
|
8682
|
+
clearTimeout(this.preConnectStuckTimeout);
|
|
8683
|
+
this.preConnectStuckTimeout = undefined;
|
|
8684
|
+
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, reason, this.peerType);
|
|
8759
8685
|
return;
|
|
8760
8686
|
}
|
|
8761
8687
|
this.handleConnectionStateUpdate(state);
|
|
@@ -10754,15 +10680,16 @@ class StreamSfuClient {
|
|
|
10754
10680
|
/**
|
|
10755
10681
|
* Decides what a ringing call should do next, based on the current call state.
|
|
10756
10682
|
*
|
|
10757
|
-
* The `call.accepted
|
|
10758
|
-
*
|
|
10759
|
-
* handlers rely on `CallState.updateFromEvent`, which runs
|
|
10760
|
-
* 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.
|
|
10761
10688
|
*
|
|
10762
10689
|
* @param call the call to reconcile.
|
|
10763
|
-
* @param joinSource
|
|
10764
|
-
*
|
|
10765
|
-
*
|
|
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).
|
|
10766
10693
|
* @returns whether the ring reached a terminal state. A failed join is not
|
|
10767
10694
|
* terminal: the caller should keep trying while the ring is open.
|
|
10768
10695
|
*/
|
|
@@ -10817,10 +10744,16 @@ const reconcileAsCaller = async (call, joinSource) => {
|
|
|
10817
10744
|
return false;
|
|
10818
10745
|
};
|
|
10819
10746
|
// the current user's own accept or reject, on this or another device, is
|
|
10820
|
-
// 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.
|
|
10821
10749
|
const reconcileAsCallee = async (call) => {
|
|
10822
|
-
const
|
|
10823
|
-
|
|
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 ?? {};
|
|
10824
10757
|
if (createdById && rejectedBy[createdById]) {
|
|
10825
10758
|
call.logger.info('ring: the caller cancelled, leaving');
|
|
10826
10759
|
return leave(call, {
|
|
@@ -10869,7 +10802,7 @@ class RingStatePoller {
|
|
|
10869
10802
|
unsubscribe = [];
|
|
10870
10803
|
constructor(call, options = {}) {
|
|
10871
10804
|
this.call = call;
|
|
10872
|
-
this.startAfterMs = options.startAfterMs ??
|
|
10805
|
+
this.startAfterMs = options.startAfterMs ?? 9_000;
|
|
10873
10806
|
this.intervalMs = options.intervalMs ?? 5_000;
|
|
10874
10807
|
}
|
|
10875
10808
|
/**
|
|
@@ -11534,12 +11467,33 @@ const registerRingingCallEventHandlers = (call) => {
|
|
|
11534
11467
|
};
|
|
11535
11468
|
|
|
11536
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
|
+
};
|
|
11537
11488
|
/**
|
|
11538
11489
|
* Tracks audio element bindings and periodically warns about
|
|
11539
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.
|
|
11540
11493
|
*/
|
|
11541
11494
|
class AudioBindingsWatchdog {
|
|
11542
11495
|
bindings = new Map();
|
|
11496
|
+
tracedStates = new Map();
|
|
11543
11497
|
enabled = true;
|
|
11544
11498
|
watchdogInterval;
|
|
11545
11499
|
unsubscribeCallingState;
|
|
@@ -11550,8 +11504,6 @@ class AudioBindingsWatchdog {
|
|
|
11550
11504
|
this.tracer = tracer;
|
|
11551
11505
|
this.state = state;
|
|
11552
11506
|
this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
|
|
11553
|
-
if (!this.enabled)
|
|
11554
|
-
return;
|
|
11555
11507
|
if (callingState !== CallingState.JOINED) {
|
|
11556
11508
|
this.stop();
|
|
11557
11509
|
}
|
|
@@ -11580,15 +11532,15 @@ class AudioBindingsWatchdog {
|
|
|
11580
11532
|
this.bindings.delete(toBindingKey(sessionId, trackType));
|
|
11581
11533
|
};
|
|
11582
11534
|
/**
|
|
11583
|
-
* Enables or disables the
|
|
11584
|
-
*
|
|
11535
|
+
* Enables or disables the dangling binding warnings.
|
|
11536
|
+
* Bindings are still tracked and their playback state is still traced.
|
|
11585
11537
|
*/
|
|
11586
11538
|
setEnabled = (enabled) => {
|
|
11587
11539
|
this.enabled = enabled;
|
|
11588
11540
|
if (enabled) {
|
|
11589
11541
|
this.start();
|
|
11590
11542
|
}
|
|
11591
|
-
else {
|
|
11543
|
+
else if (this.state.callingState !== CallingState.JOINED) {
|
|
11592
11544
|
this.stop();
|
|
11593
11545
|
}
|
|
11594
11546
|
};
|
|
@@ -11598,33 +11550,61 @@ class AudioBindingsWatchdog {
|
|
|
11598
11550
|
dispose = () => {
|
|
11599
11551
|
this.stop();
|
|
11600
11552
|
this.bindings.clear();
|
|
11553
|
+
this.tracedStates.clear();
|
|
11601
11554
|
this.unsubscribeCallingState();
|
|
11602
11555
|
};
|
|
11603
11556
|
start = () => {
|
|
11604
11557
|
clearInterval(this.watchdogInterval);
|
|
11605
11558
|
this.watchdogInterval = setInterval(() => {
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
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);
|
|
11621
11574
|
}
|
|
11622
|
-
if (
|
|
11623
|
-
|
|
11624
|
-
this.
|
|
11625
|
-
|
|
11575
|
+
if (screenShareAudioStream &&
|
|
11576
|
+
publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
|
|
11577
|
+
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
|
|
11578
|
+
danglingUserIds.push(userId);
|
|
11626
11579
|
}
|
|
11627
|
-
}
|
|
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);
|
|
11628
11608
|
};
|
|
11629
11609
|
stop = () => {
|
|
11630
11610
|
clearInterval(this.watchdogInterval);
|
|
@@ -11903,7 +11883,9 @@ class TrackSubscriptionManager {
|
|
|
11903
11883
|
class MediaPlaybackWatchdog {
|
|
11904
11884
|
logger = videoLoggerSystem.getLogger('MediaPlaybackWatchdog');
|
|
11905
11885
|
kind;
|
|
11886
|
+
traceInfo;
|
|
11906
11887
|
isBlocked;
|
|
11888
|
+
onGiveUp;
|
|
11907
11889
|
element;
|
|
11908
11890
|
tracer;
|
|
11909
11891
|
controller = new AbortController();
|
|
@@ -11915,8 +11897,14 @@ class MediaPlaybackWatchdog {
|
|
|
11915
11897
|
constructor(opts) {
|
|
11916
11898
|
this.element = opts.element;
|
|
11917
11899
|
this.kind = opts.kind;
|
|
11900
|
+
this.traceInfo = {
|
|
11901
|
+
kind: opts.kind,
|
|
11902
|
+
sessionId: opts.sessionId,
|
|
11903
|
+
trackType: opts.trackType,
|
|
11904
|
+
};
|
|
11918
11905
|
this.tracer = opts.tracer;
|
|
11919
11906
|
this.isBlocked = opts.isBlocked ?? (() => false);
|
|
11907
|
+
this.onGiveUp = opts.onGiveUp ?? (() => { });
|
|
11920
11908
|
this.attach();
|
|
11921
11909
|
}
|
|
11922
11910
|
attach = () => {
|
|
@@ -11934,6 +11922,23 @@ class MediaPlaybackWatchdog {
|
|
|
11934
11922
|
this.controller.abort();
|
|
11935
11923
|
this.clearTimers();
|
|
11936
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
|
+
};
|
|
11937
11942
|
clearTimers = () => {
|
|
11938
11943
|
if (this.pendingTimer)
|
|
11939
11944
|
clearTimeout(this.pendingTimer);
|
|
@@ -11955,17 +11960,34 @@ class MediaPlaybackWatchdog {
|
|
|
11955
11960
|
if (this.element.paused)
|
|
11956
11961
|
return;
|
|
11957
11962
|
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11958
|
-
|
|
11963
|
+
...this.traceInfo,
|
|
11959
11964
|
attempts: this.attempt,
|
|
11960
11965
|
});
|
|
11961
11966
|
this.attempt = 0;
|
|
11962
11967
|
this.stopped = false;
|
|
11963
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
|
+
*/
|
|
11964
11975
|
onPauseOrSuspend = (event) => {
|
|
11965
|
-
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
|
+
}
|
|
11966
11987
|
return;
|
|
11988
|
+
}
|
|
11967
11989
|
this.tracer.trace('mediaPlayback.paused', {
|
|
11968
|
-
|
|
11990
|
+
...this.traceInfo,
|
|
11969
11991
|
reason: event.type,
|
|
11970
11992
|
});
|
|
11971
11993
|
if (this.settleTimer)
|
|
@@ -11979,7 +12001,7 @@ class MediaPlaybackWatchdog {
|
|
|
11979
12001
|
const skipReason = this.computeSkipReason();
|
|
11980
12002
|
if (skipReason) {
|
|
11981
12003
|
this.tracer.trace('mediaPlayback.recover.skipped', {
|
|
11982
|
-
|
|
12004
|
+
...this.traceInfo,
|
|
11983
12005
|
reason: skipReason,
|
|
11984
12006
|
});
|
|
11985
12007
|
return;
|
|
@@ -11988,9 +12010,10 @@ class MediaPlaybackWatchdog {
|
|
|
11988
12010
|
this.stopped = true;
|
|
11989
12011
|
this.clearTimers();
|
|
11990
12012
|
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
11991
|
-
|
|
12013
|
+
...this.traceInfo,
|
|
11992
12014
|
attempts: this.attempt,
|
|
11993
12015
|
});
|
|
12016
|
+
this.onGiveUp();
|
|
11994
12017
|
return;
|
|
11995
12018
|
}
|
|
11996
12019
|
const delay = this.attempt === 0 ? 0 : retryInterval(this.attempt);
|
|
@@ -12018,7 +12041,7 @@ class MediaPlaybackWatchdog {
|
|
|
12018
12041
|
return;
|
|
12019
12042
|
this.attempt += 1;
|
|
12020
12043
|
this.tracer.trace('mediaPlayback.recover.attempt', {
|
|
12021
|
-
|
|
12044
|
+
...this.traceInfo,
|
|
12022
12045
|
attempt: this.attempt,
|
|
12023
12046
|
});
|
|
12024
12047
|
try {
|
|
@@ -12033,6 +12056,41 @@ class MediaPlaybackWatchdog {
|
|
|
12033
12056
|
};
|
|
12034
12057
|
}
|
|
12035
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
|
+
|
|
12036
12094
|
/**
|
|
12037
12095
|
* A manager class that handles dynascale related tasks like:
|
|
12038
12096
|
*
|
|
@@ -12046,6 +12104,9 @@ class DynascaleManager {
|
|
|
12046
12104
|
tracer;
|
|
12047
12105
|
useWebAudio = false;
|
|
12048
12106
|
audioContext;
|
|
12107
|
+
playbackWatchdogs = new Set();
|
|
12108
|
+
stopWatchingInterruptions;
|
|
12109
|
+
stopTracingAudioSession;
|
|
12049
12110
|
trackSubscriptionManager;
|
|
12050
12111
|
blockedAudioTracker;
|
|
12051
12112
|
/**
|
|
@@ -12062,6 +12123,10 @@ class DynascaleManager {
|
|
|
12062
12123
|
* Closes the audio context if it was created.
|
|
12063
12124
|
*/
|
|
12064
12125
|
dispose = async () => {
|
|
12126
|
+
this.stopWatchingInterruptions?.();
|
|
12127
|
+
this.stopWatchingInterruptions = undefined;
|
|
12128
|
+
this.stopTracingAudioSession?.();
|
|
12129
|
+
this.stopTracingAudioSession = undefined;
|
|
12065
12130
|
const context = this.audioContext;
|
|
12066
12131
|
if (context && context.state !== 'closed') {
|
|
12067
12132
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
@@ -12201,10 +12266,12 @@ class DynascaleManager {
|
|
|
12201
12266
|
// without prior user interaction:
|
|
12202
12267
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
|
|
12203
12268
|
videoElement.muted = true;
|
|
12204
|
-
const playbackWatchdog =
|
|
12269
|
+
const playbackWatchdog = this.createPlaybackWatchdog({
|
|
12205
12270
|
element: videoElement,
|
|
12206
12271
|
kind: 'video',
|
|
12207
12272
|
tracer: this.tracer,
|
|
12273
|
+
sessionId,
|
|
12274
|
+
trackType,
|
|
12208
12275
|
});
|
|
12209
12276
|
const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
|
|
12210
12277
|
const streamSubscription = participant$
|
|
@@ -12232,7 +12299,7 @@ class DynascaleManager {
|
|
|
12232
12299
|
publishedTracksSubscription?.unsubscribe();
|
|
12233
12300
|
streamSubscription.unsubscribe();
|
|
12234
12301
|
resizeObserver?.disconnect();
|
|
12235
|
-
|
|
12302
|
+
this.disposePlaybackWatchdog(playbackWatchdog);
|
|
12236
12303
|
};
|
|
12237
12304
|
};
|
|
12238
12305
|
/**
|
|
@@ -12250,6 +12317,7 @@ class DynascaleManager {
|
|
|
12250
12317
|
const participant = this.callState.findParticipantBySessionId(sessionId);
|
|
12251
12318
|
if (!participant || participant.isLocalParticipant)
|
|
12252
12319
|
return;
|
|
12320
|
+
this.traceAudioSessionState();
|
|
12253
12321
|
const participant$ = this.callState.participants$.pipe(map((ps) => ps.find((p) => p.sessionId === sessionId)), takeWhile((p) => !!p), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
|
|
12254
12322
|
const updateSinkId = (deviceId, audioContext) => {
|
|
12255
12323
|
if (!deviceId)
|
|
@@ -12269,7 +12337,9 @@ class DynascaleManager {
|
|
|
12269
12337
|
let sourceNode = undefined;
|
|
12270
12338
|
let gainNode = undefined;
|
|
12271
12339
|
let audioWatchdog = undefined;
|
|
12340
|
+
let blockedByGiveUp = false;
|
|
12272
12341
|
const clearBlockedAudio = () => {
|
|
12342
|
+
blockedByGiveUp = false;
|
|
12273
12343
|
if (!this.blockedAudioTracker.isBlocked(audioElement))
|
|
12274
12344
|
return;
|
|
12275
12345
|
this.blockedAudioTracker.markBlocked(audioElement, false);
|
|
@@ -12277,15 +12347,18 @@ class DynascaleManager {
|
|
|
12277
12347
|
audioElement.addEventListener('playing', clearBlockedAudio);
|
|
12278
12348
|
const isAudioTrack = trackType === 'audioTrack';
|
|
12279
12349
|
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
12350
|
+
let sourceChangeTimer;
|
|
12280
12351
|
const updateMediaStreamSubscription = participant$
|
|
12281
12352
|
.pipe(distinctUntilKeyChanged(trackKey))
|
|
12282
12353
|
.subscribe((p) => {
|
|
12283
12354
|
const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
|
|
12284
12355
|
if (audioElement.srcObject === source)
|
|
12285
12356
|
return;
|
|
12286
|
-
|
|
12357
|
+
clearTimeout(sourceChangeTimer);
|
|
12358
|
+
sourceChangeTimer = setTimeout(() => {
|
|
12359
|
+
sourceChangeTimer = undefined;
|
|
12287
12360
|
audioElement.srcObject = source ?? null;
|
|
12288
|
-
audioWatchdog
|
|
12361
|
+
this.disposePlaybackWatchdog(audioWatchdog);
|
|
12289
12362
|
audioWatchdog = undefined;
|
|
12290
12363
|
if (!source) {
|
|
12291
12364
|
clearBlockedAudio();
|
|
@@ -12316,15 +12389,25 @@ class DynascaleManager {
|
|
|
12316
12389
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
12317
12390
|
if (e.name === 'NotAllowedError') {
|
|
12318
12391
|
this.tracer.trace('audioPlaybackBlocked', null);
|
|
12392
|
+
blockedByGiveUp = false;
|
|
12319
12393
|
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
12320
12394
|
}
|
|
12321
12395
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
12322
12396
|
});
|
|
12323
|
-
audioWatchdog =
|
|
12397
|
+
audioWatchdog = this.createPlaybackWatchdog({
|
|
12324
12398
|
element: audioElement,
|
|
12325
12399
|
kind: 'audio',
|
|
12326
12400
|
tracer: this.tracer,
|
|
12327
|
-
|
|
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
|
+
},
|
|
12328
12411
|
});
|
|
12329
12412
|
}
|
|
12330
12413
|
const { selectedDevice } = this.speaker.state;
|
|
@@ -12354,13 +12437,51 @@ class DynascaleManager {
|
|
|
12354
12437
|
sinkIdSubscription?.unsubscribe();
|
|
12355
12438
|
volumeSubscription.unsubscribe();
|
|
12356
12439
|
updateMediaStreamSubscription.unsubscribe();
|
|
12440
|
+
clearTimeout(sourceChangeTimer);
|
|
12357
12441
|
audioElement.srcObject = null;
|
|
12358
12442
|
sourceNode?.disconnect();
|
|
12359
12443
|
gainNode?.disconnect();
|
|
12360
|
-
audioWatchdog
|
|
12444
|
+
this.disposePlaybackWatchdog(audioWatchdog);
|
|
12361
12445
|
audioWatchdog = undefined;
|
|
12362
12446
|
};
|
|
12363
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
|
+
};
|
|
12364
12485
|
getOrCreateAudioContext = () => {
|
|
12365
12486
|
if (!this.useWebAudio)
|
|
12366
12487
|
return;
|
|
@@ -12383,7 +12504,6 @@ class DynascaleManager {
|
|
|
12383
12504
|
audioSession.type = 'play-and-record';
|
|
12384
12505
|
let isSessionInterrupted = false;
|
|
12385
12506
|
audioSession.addEventListener('statechange', () => {
|
|
12386
|
-
this.tracer.trace('audioSession.state', audioSession.state);
|
|
12387
12507
|
if (audioSession.state === 'interrupted') {
|
|
12388
12508
|
isSessionInterrupted = true;
|
|
12389
12509
|
}
|
|
@@ -13942,9 +14062,22 @@ class DeviceManager {
|
|
|
13942
14062
|
}
|
|
13943
14063
|
const devices = getCurrentValue(this.listDevices()) || [];
|
|
13944
14064
|
const currentDevice = this.findDevice(devices, selectedDevice) ??
|
|
13945
|
-
createSyntheticDevice(selectedDevice, deviceKind);
|
|
14065
|
+
createSyntheticDevice(selectedDevice, deviceKind, this.getActiveTrackLabel(selectedDevice));
|
|
13946
14066
|
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
13947
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
|
+
};
|
|
13948
14081
|
async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
|
|
13949
14082
|
const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
13950
14083
|
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
@@ -16780,9 +16913,6 @@ class Call {
|
|
|
16780
16913
|
options: statsOptions,
|
|
16781
16914
|
subscriber: this.subscriber,
|
|
16782
16915
|
publisher: this.publisher,
|
|
16783
|
-
microphone: this.microphone,
|
|
16784
|
-
camera: this.camera,
|
|
16785
|
-
state: this.state,
|
|
16786
16916
|
tracer: this.tracer,
|
|
16787
16917
|
unifiedSessionId,
|
|
16788
16918
|
});
|
|
@@ -19632,11 +19762,11 @@ class StreamClient {
|
|
|
19632
19762
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
19633
19763
|
};
|
|
19634
19764
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
19635
|
-
"1.61.
|
|
19765
|
+
"1.61.2";
|
|
19636
19766
|
getUserAgent = () => {
|
|
19637
19767
|
if (!this.cachedUserAgent) {
|
|
19638
19768
|
const { clientAppIdentifier = {} } = this.options;
|
|
19639
|
-
const { sdkName = 'js', sdkVersion = "1.61.
|
|
19769
|
+
const { sdkName = 'js', sdkVersion = "1.61.2", ...extras } = clientAppIdentifier;
|
|
19640
19770
|
this.cachedUserAgent = [
|
|
19641
19771
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
19642
19772
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -20171,7 +20301,11 @@ class ClientEventReporter {
|
|
|
20171
20301
|
return;
|
|
20172
20302
|
}
|
|
20173
20303
|
if (event.state === 'failed') {
|
|
20174
|
-
|
|
20304
|
+
const iceConnected = event.iceConnectionState === 'connected' ||
|
|
20305
|
+
event.iceConnectionState === 'completed';
|
|
20306
|
+
this.emitPeerConnectionFailure(cid, role, iceConnected ? 'DTLS_CONNECTIVITY_FAILED' : 'ICE_CONNECTIVITY_FAILED', iceConnected
|
|
20307
|
+
? 'DTLS connectivity checks failed'
|
|
20308
|
+
: 'ICE connectivity checks failed');
|
|
20175
20309
|
return;
|
|
20176
20310
|
}
|
|
20177
20311
|
switch (event.state) {
|
|
@@ -20572,6 +20706,10 @@ class StreamVideoClient {
|
|
|
20572
20706
|
const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
|
|
20573
20707
|
if (call) {
|
|
20574
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
|
+
});
|
|
20575
20713
|
}
|
|
20576
20714
|
}
|
|
20577
20715
|
return;
|