@stream-io/video-client 1.58.0 → 1.59.0
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 +319 -71
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +329 -69
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +319 -71
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +15 -4
- package/dist/src/StreamVideoClient.d.ts +9 -1
- package/dist/src/devices/CameraManager.d.ts +2 -1
- package/dist/src/devices/DeviceManager.d.ts +1 -1
- package/dist/src/devices/MicrophoneManager.d.ts +2 -1
- package/dist/src/gen/coordinator/index.d.ts +26 -0
- package/dist/src/helpers/MediaStreamRecorder.d.ts +26 -0
- package/dist/src/helpers/concurrency.d.ts +8 -0
- package/dist/src/helpers/loopback.d.ts +55 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +0 -2
- package/dist/src/rtc/Subscriber.d.ts +5 -0
- package/index.ts +2 -0
- package/package.json +1 -1
- package/src/Call.ts +156 -134
- package/src/StreamVideoClient.ts +35 -8
- package/src/__tests__/Call.lifecycle.test.ts +33 -2
- package/src/__tests__/Call.publishing.test.ts +42 -1
- package/src/__tests__/StreamVideoClient.api.test.ts +33 -0
- package/src/__tests__/StreamVideoClient.rewatch.test.ts +213 -0
- package/src/__tests__/StreamVideoClient.ringing.test.ts +11 -4
- package/src/__tests__/StreamVideoClient.test.ts +14 -12
- package/src/devices/CameraManager.ts +16 -4
- package/src/devices/DeviceManager.ts +7 -4
- package/src/devices/MicrophoneManager.ts +11 -3
- package/src/devices/__tests__/CameraManager.test.ts +27 -1
- package/src/devices/__tests__/DeviceManager.test.ts +49 -1
- package/src/devices/__tests__/MicrophoneManager.test.ts +23 -1
- package/src/events/call.ts +6 -3
- package/src/gen/coordinator/index.ts +26 -0
- package/src/helpers/MediaStreamRecorder.ts +104 -0
- package/src/helpers/__tests__/MediaStreamRecorder.test.ts +324 -0
- package/src/helpers/__tests__/concurrency.test.ts +74 -0
- package/src/helpers/__tests__/loopback.test.ts +367 -0
- package/src/helpers/concurrency.ts +24 -0
- package/src/helpers/loopback.ts +200 -0
- package/src/reporting/ClientEventReporter.ts +15 -28
- package/src/reporting/__tests__/ClientEventReporter.test.ts +3 -7
- package/src/rtc/Subscriber.ts +8 -0
- package/src/rtc/__tests__/Subscriber.test.ts +101 -0
package/dist/index.cjs.js
CHANGED
|
@@ -5079,7 +5079,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5079
5079
|
}
|
|
5080
5080
|
};
|
|
5081
5081
|
|
|
5082
|
-
const version = "1.
|
|
5082
|
+
const version = "1.59.0";
|
|
5083
5083
|
const [major, minor, patch] = version.split('.');
|
|
5084
5084
|
let sdkInfo = {
|
|
5085
5085
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5336,6 +5336,27 @@ const withoutConcurrency = createRunner(wrapWithContinuationTracking);
|
|
|
5336
5336
|
*/
|
|
5337
5337
|
const withCancellation = createRunner(wrapWithCancellation);
|
|
5338
5338
|
const pendingPromises = new Map();
|
|
5339
|
+
/**
|
|
5340
|
+
* Wraps an async function so concurrent calls share the in-flight promise
|
|
5341
|
+
* instead of queueing another execution.
|
|
5342
|
+
*
|
|
5343
|
+
* @param cb Async function to wrap.
|
|
5344
|
+
* @returns Wrapped async function.
|
|
5345
|
+
*/
|
|
5346
|
+
function singleFlight(cb) {
|
|
5347
|
+
let pending;
|
|
5348
|
+
return (...args) => {
|
|
5349
|
+
if (pending)
|
|
5350
|
+
return pending;
|
|
5351
|
+
const promise = cb(...args).finally(() => {
|
|
5352
|
+
if (pending === promise) {
|
|
5353
|
+
pending = undefined;
|
|
5354
|
+
}
|
|
5355
|
+
});
|
|
5356
|
+
pending = promise;
|
|
5357
|
+
return promise;
|
|
5358
|
+
};
|
|
5359
|
+
}
|
|
5339
5360
|
function hasPending(tag) {
|
|
5340
5361
|
return pendingPromises.has(tag);
|
|
5341
5362
|
}
|
|
@@ -9824,6 +9845,11 @@ class Subscriber extends BasePeerConnection {
|
|
|
9824
9845
|
throw e;
|
|
9825
9846
|
}
|
|
9826
9847
|
};
|
|
9848
|
+
/**
|
|
9849
|
+
* Whether the given stream is one the SFU echoed back to us
|
|
9850
|
+
* (`allowOwnTracksLoopback`) rather than a local capture stream.
|
|
9851
|
+
*/
|
|
9852
|
+
isSelfSubscribedStream = (stream) => !!stream && !!this.trackedStreams?.has(stream);
|
|
9827
9853
|
handleOnTrack = (e) => {
|
|
9828
9854
|
const { streams, track } = e;
|
|
9829
9855
|
const [primaryStream] = streams;
|
|
@@ -9913,6 +9939,8 @@ class Subscriber extends BasePeerConnection {
|
|
|
9913
9939
|
const target = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9914
9940
|
if (!target)
|
|
9915
9941
|
return;
|
|
9942
|
+
if (target.isLocalParticipant)
|
|
9943
|
+
return;
|
|
9916
9944
|
this.state.updateParticipant(target.sessionId, (p) => {
|
|
9917
9945
|
const current = p.interruptedTracks ?? [];
|
|
9918
9946
|
const has = current.includes(trackType);
|
|
@@ -10655,7 +10683,6 @@ const watchCallRejected = (call) => {
|
|
|
10655
10683
|
*/
|
|
10656
10684
|
const watchCallEnded = (call) => {
|
|
10657
10685
|
return function onCallEnded() {
|
|
10658
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
10659
10686
|
const { callingState } = call.state;
|
|
10660
10687
|
if (callingState !== exports.CallingState.IDLE &&
|
|
10661
10688
|
callingState !== exports.CallingState.LEFT) {
|
|
@@ -10664,9 +10691,13 @@ const watchCallEnded = (call) => {
|
|
|
10664
10691
|
reason: 'call.ended event received',
|
|
10665
10692
|
});
|
|
10666
10693
|
call
|
|
10667
|
-
.leave({
|
|
10694
|
+
.leave({
|
|
10695
|
+
message: 'call.ended event received',
|
|
10696
|
+
reject: false,
|
|
10697
|
+
reason: 'ended',
|
|
10698
|
+
})
|
|
10668
10699
|
.catch((err) => {
|
|
10669
|
-
call.logger.error('Failed to leave call after call.ended
|
|
10700
|
+
call.logger.error('Failed to leave call after call.ended', err);
|
|
10670
10701
|
});
|
|
10671
10702
|
}
|
|
10672
10703
|
};
|
|
@@ -13472,7 +13503,7 @@ class DeviceManager {
|
|
|
13472
13503
|
createSyntheticDevice(selectedDevice, deviceKind);
|
|
13473
13504
|
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
13474
13505
|
}
|
|
13475
|
-
async applyPersistedPreferences(enabledInCallType) {
|
|
13506
|
+
async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
|
|
13476
13507
|
const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
13477
13508
|
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
13478
13509
|
const preferenceList = toPreferenceList(preferences[deviceKey]);
|
|
@@ -13499,13 +13530,13 @@ class DeviceManager {
|
|
|
13499
13530
|
}
|
|
13500
13531
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
13501
13532
|
if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
|
|
13502
|
-
await this.applyMutedState(muted);
|
|
13533
|
+
await this.applyMutedState(muted, forceDisabled);
|
|
13503
13534
|
appliedMute = true;
|
|
13504
13535
|
}
|
|
13505
13536
|
return appliedDevice || appliedMute;
|
|
13506
13537
|
}
|
|
13507
|
-
async applyMutedState(muted) {
|
|
13508
|
-
if (this.state.status !== undefined)
|
|
13538
|
+
async applyMutedState(muted, forceDisabled) {
|
|
13539
|
+
if (forceDisabled || this.state.status !== undefined)
|
|
13509
13540
|
return;
|
|
13510
13541
|
if (muted) {
|
|
13511
13542
|
await this.disable();
|
|
@@ -13857,8 +13888,9 @@ class CameraManager extends DeviceManager {
|
|
|
13857
13888
|
*
|
|
13858
13889
|
* @param settings the video settings to apply.
|
|
13859
13890
|
* @param publish whether to publish the stream after applying the settings.
|
|
13891
|
+
* @param forceDisabled whether to keep the camera disabled, regardless of server-side or local preferences.
|
|
13860
13892
|
*/
|
|
13861
|
-
async apply(settings, publish) {
|
|
13893
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
13862
13894
|
// Wait for any in progress camera operation
|
|
13863
13895
|
await this.statusChangeSettled();
|
|
13864
13896
|
await this.selectTargetResolution(settings.target_resolution);
|
|
@@ -13870,8 +13902,7 @@ class CameraManager extends DeviceManager {
|
|
|
13870
13902
|
if (shouldApplyDefaults &&
|
|
13871
13903
|
this.devicePersistence.enabled &&
|
|
13872
13904
|
permissionState === 'granted') {
|
|
13873
|
-
persistedPreferencesApplied =
|
|
13874
|
-
await this.applyPersistedPreferences(enabledInCallType);
|
|
13905
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(enabledInCallType, forceDisabled);
|
|
13875
13906
|
}
|
|
13876
13907
|
// apply a direction and enable the camera only if in "pristine" state,
|
|
13877
13908
|
// and there are no persisted preferences
|
|
@@ -13881,7 +13912,10 @@ class CameraManager extends DeviceManager {
|
|
|
13881
13912
|
const direction = settings.camera_facing === 'front' ? 'front' : 'back';
|
|
13882
13913
|
await this.selectDirection(direction, { enableCamera: false });
|
|
13883
13914
|
}
|
|
13884
|
-
if (
|
|
13915
|
+
if (!forceDisabled &&
|
|
13916
|
+
canPublish &&
|
|
13917
|
+
settings.camera_default_on &&
|
|
13918
|
+
enabledInCallType) {
|
|
13885
13919
|
await this.enable();
|
|
13886
13920
|
}
|
|
13887
13921
|
}
|
|
@@ -14476,8 +14510,9 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14476
14510
|
* Applies the audio settings to the microphone.
|
|
14477
14511
|
* @param settings the audio settings to apply.
|
|
14478
14512
|
* @param publish whether to publish the stream after applying the settings.
|
|
14513
|
+
* @param forceDisabled whether to keep the mic disabled, regardless of server-side or local preferences.
|
|
14479
14514
|
*/
|
|
14480
|
-
async apply(settings, publish) {
|
|
14515
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
14481
14516
|
// Wait for any in progress mic operation
|
|
14482
14517
|
await this.statusChangeSettled();
|
|
14483
14518
|
// apply server-side settings only when the device state is pristine
|
|
@@ -14489,11 +14524,11 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14489
14524
|
if (shouldApplyDefaults &&
|
|
14490
14525
|
this.devicePersistence.enabled &&
|
|
14491
14526
|
permissionState === 'granted') {
|
|
14492
|
-
persistedPreferencesApplied = await this.applyPersistedPreferences(true);
|
|
14527
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(true, forceDisabled);
|
|
14493
14528
|
}
|
|
14494
14529
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
14495
14530
|
if (shouldApplyDefaults && !persistedPreferencesApplied) {
|
|
14496
|
-
if (canPublish && settings.mic_default_on) {
|
|
14531
|
+
if (!forceDisabled && canPublish && settings.mic_default_on) {
|
|
14497
14532
|
await this.enable();
|
|
14498
14533
|
}
|
|
14499
14534
|
}
|
|
@@ -15462,7 +15497,7 @@ class Call {
|
|
|
15462
15497
|
}
|
|
15463
15498
|
}
|
|
15464
15499
|
}
|
|
15465
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
15500
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, reason === 'ended' ? 'remote' : undefined);
|
|
15466
15501
|
this.statsReporter?.stop();
|
|
15467
15502
|
this.statsReporter = undefined;
|
|
15468
15503
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
@@ -15625,8 +15660,18 @@ class Call {
|
|
|
15625
15660
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
15626
15661
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
15627
15662
|
this.clientStore.setCalls([this, ...calls]);
|
|
15628
|
-
|
|
15629
|
-
|
|
15663
|
+
await this.applyDeviceConfig(settings, { publish: false });
|
|
15664
|
+
};
|
|
15665
|
+
/**
|
|
15666
|
+
* Applies a backend-provided call state snapshot
|
|
15667
|
+
* (call, members, own capabilities) to this instance.
|
|
15668
|
+
*
|
|
15669
|
+
* @internal an internal method and should not be used outside the SDK.
|
|
15670
|
+
*/
|
|
15671
|
+
updateFromCallStateResponse = (response) => {
|
|
15672
|
+
this.state.updateFromCallResponse(response.call);
|
|
15673
|
+
this.state.setMembers(response.members);
|
|
15674
|
+
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15630
15675
|
};
|
|
15631
15676
|
/**
|
|
15632
15677
|
* Loads the information about the call.
|
|
@@ -15639,9 +15684,7 @@ class Call {
|
|
|
15639
15684
|
get = async (params) => {
|
|
15640
15685
|
await this.setup();
|
|
15641
15686
|
const response = await this.streamClient.get(this.streamClientBasePath, params);
|
|
15642
|
-
this.
|
|
15643
|
-
this.state.setMembers(response.members);
|
|
15644
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15687
|
+
this.updateFromCallStateResponse(response);
|
|
15645
15688
|
if (params?.ring) {
|
|
15646
15689
|
this.ringingSubject.next(true);
|
|
15647
15690
|
}
|
|
@@ -15649,9 +15692,7 @@ class Call {
|
|
|
15649
15692
|
this.watching = true;
|
|
15650
15693
|
this.clientStore.registerOrUpdateCall(this);
|
|
15651
15694
|
}
|
|
15652
|
-
|
|
15653
|
-
const skipSpeakerApply = isReactNative();
|
|
15654
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15695
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15655
15696
|
return response;
|
|
15656
15697
|
};
|
|
15657
15698
|
/**
|
|
@@ -15662,9 +15703,7 @@ class Call {
|
|
|
15662
15703
|
getOrCreate = async (data) => {
|
|
15663
15704
|
await this.setup();
|
|
15664
15705
|
const response = await this.streamClient.post(this.streamClientBasePath, data);
|
|
15665
|
-
this.
|
|
15666
|
-
this.state.setMembers(response.members);
|
|
15667
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15706
|
+
this.updateFromCallStateResponse(response);
|
|
15668
15707
|
if (data?.ring) {
|
|
15669
15708
|
this.ringingSubject.next(true);
|
|
15670
15709
|
}
|
|
@@ -15672,9 +15711,7 @@ class Call {
|
|
|
15672
15711
|
this.watching = true;
|
|
15673
15712
|
this.clientStore.registerOrUpdateCall(this);
|
|
15674
15713
|
}
|
|
15675
|
-
|
|
15676
|
-
const skipSpeakerApply = isReactNative();
|
|
15677
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15714
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15678
15715
|
return response;
|
|
15679
15716
|
};
|
|
15680
15717
|
/**
|
|
@@ -15738,7 +15775,7 @@ class Call {
|
|
|
15738
15775
|
*
|
|
15739
15776
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
15740
15777
|
*/
|
|
15741
|
-
join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15778
|
+
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15742
15779
|
const callingState = this.state.callingState;
|
|
15743
15780
|
if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
|
|
15744
15781
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -15760,7 +15797,6 @@ class Call {
|
|
|
15760
15797
|
callId: this.id,
|
|
15761
15798
|
getCallSessionId: () => this.state.session?.id ?? '',
|
|
15762
15799
|
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
15763
|
-
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
15764
15800
|
});
|
|
15765
15801
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
15766
15802
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
@@ -15812,7 +15848,7 @@ class Call {
|
|
|
15812
15848
|
callingX?.endCall(this, 'error');
|
|
15813
15849
|
throw error;
|
|
15814
15850
|
}
|
|
15815
|
-
};
|
|
15851
|
+
});
|
|
15816
15852
|
/**
|
|
15817
15853
|
* Will make a single attempt to watch for call related WebSocket events
|
|
15818
15854
|
* and initiate a call session with the server.
|
|
@@ -15984,7 +16020,10 @@ class Call {
|
|
|
15984
16020
|
if (!this.deviceSettingsAppliedOnce &&
|
|
15985
16021
|
this.state.settings &&
|
|
15986
16022
|
!supersededByLeave()) {
|
|
15987
|
-
await this.applyDeviceConfig(this.state.settings,
|
|
16023
|
+
await this.applyDeviceConfig(this.state.settings, {
|
|
16024
|
+
publish: true,
|
|
16025
|
+
skipSpeakerApply: false,
|
|
16026
|
+
});
|
|
15988
16027
|
this.deviceSettingsAppliedOnce = true;
|
|
15989
16028
|
}
|
|
15990
16029
|
if (!this.callManagerStarted && !supersededByLeave()) {
|
|
@@ -16774,12 +16813,16 @@ class Call {
|
|
|
16774
16813
|
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
16775
16814
|
if (!streamStateProp)
|
|
16776
16815
|
continue;
|
|
16777
|
-
this.state.updateParticipant(sessionId, (p) =>
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
|
|
16816
|
+
this.state.updateParticipant(sessionId, (p) => {
|
|
16817
|
+
const currentStream = p[streamStateProp];
|
|
16818
|
+
const isEchoedStream = this.subscriber?.isSelfSubscribedStream(currentStream);
|
|
16819
|
+
return {
|
|
16820
|
+
publishedTracks: mediaStream
|
|
16821
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
16822
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
16823
|
+
[streamStateProp]: isEchoedStream ? currentStream : mediaStream,
|
|
16824
|
+
};
|
|
16825
|
+
});
|
|
16783
16826
|
}
|
|
16784
16827
|
};
|
|
16785
16828
|
/**
|
|
@@ -17469,18 +17512,21 @@ class Call {
|
|
|
17469
17512
|
*
|
|
17470
17513
|
* @internal
|
|
17471
17514
|
*/
|
|
17472
|
-
applyDeviceConfig = async (settings,
|
|
17515
|
+
applyDeviceConfig = async (settings, opts) => {
|
|
17516
|
+
const { publish,
|
|
17517
|
+
// Skip speaker setup on RN until joining (explicitly passed as false during join())
|
|
17518
|
+
skipSpeakerApply = isReactNative(), withDisabledDevices, } = opts;
|
|
17473
17519
|
if (!skipSpeakerApply) {
|
|
17474
|
-
await this.speaker
|
|
17475
|
-
|
|
17476
|
-
|
|
17520
|
+
await this.speaker
|
|
17521
|
+
.apply(settings)
|
|
17522
|
+
.catch((err) => this.logger.warn('Speaker init failed', err));
|
|
17477
17523
|
}
|
|
17478
|
-
await this.camera
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
await this.microphone
|
|
17482
|
-
|
|
17483
|
-
|
|
17524
|
+
await this.camera
|
|
17525
|
+
.apply(settings.video, publish, withDisabledDevices)
|
|
17526
|
+
.catch((err) => this.logger.warn('Camera init failed', err));
|
|
17527
|
+
await this.microphone
|
|
17528
|
+
.apply(settings.audio, publish, withDisabledDevices)
|
|
17529
|
+
.catch((err) => this.logger.warn('Mic init failed', err));
|
|
17484
17530
|
};
|
|
17485
17531
|
/**
|
|
17486
17532
|
* Will begin tracking the given element for visibility changes within the
|
|
@@ -18988,11 +19034,11 @@ class StreamClient {
|
|
|
18988
19034
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18989
19035
|
};
|
|
18990
19036
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18991
|
-
"1.
|
|
19037
|
+
"1.59.0";
|
|
18992
19038
|
getUserAgent = () => {
|
|
18993
19039
|
if (!this.cachedUserAgent) {
|
|
18994
19040
|
const { clientAppIdentifier = {} } = this.options;
|
|
18995
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
19041
|
+
const { sdkName = 'js', sdkVersion = "1.59.0", ...extras } = clientAppIdentifier;
|
|
18996
19042
|
this.cachedUserAgent = [
|
|
18997
19043
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18998
19044
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19355,7 +19401,6 @@ class ClientEventReporter {
|
|
|
19355
19401
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
19356
19402
|
type: ctx?.callType,
|
|
19357
19403
|
id: ctx?.callId,
|
|
19358
|
-
call_cid: cid,
|
|
19359
19404
|
stage: 'JoinInitiated',
|
|
19360
19405
|
join_attempt_id: joinAttemptId,
|
|
19361
19406
|
...(coordinatorConnectId && {
|
|
@@ -19537,7 +19582,6 @@ class ClientEventReporter {
|
|
|
19537
19582
|
startedAt: Date.now(),
|
|
19538
19583
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
19539
19584
|
sfuId: this.getSfuId(cid),
|
|
19540
|
-
userSessionId: this.getUserSessionId(cid),
|
|
19541
19585
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
19542
19586
|
lastIceState: iceConnectionState,
|
|
19543
19587
|
};
|
|
@@ -19548,9 +19592,6 @@ class ClientEventReporter {
|
|
|
19548
19592
|
peer_connection: role,
|
|
19549
19593
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19550
19594
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19551
|
-
...(pair.userSessionId && {
|
|
19552
|
-
user_session_id: pair.userSessionId,
|
|
19553
|
-
}),
|
|
19554
19595
|
event_type: 'initiated',
|
|
19555
19596
|
});
|
|
19556
19597
|
};
|
|
@@ -19565,9 +19606,6 @@ class ClientEventReporter {
|
|
|
19565
19606
|
peer_connection: role,
|
|
19566
19607
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19567
19608
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19568
|
-
...(pair.userSessionId && {
|
|
19569
|
-
user_session_id: pair.userSessionId,
|
|
19570
|
-
}),
|
|
19571
19609
|
event_type: 'completed',
|
|
19572
19610
|
outcome: 'success',
|
|
19573
19611
|
retry_count_attempt: 0,
|
|
@@ -19585,9 +19623,6 @@ class ClientEventReporter {
|
|
|
19585
19623
|
...this.sessionIdField(cid),
|
|
19586
19624
|
peer_connection: role,
|
|
19587
19625
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19588
|
-
...(pair.userSessionId && {
|
|
19589
|
-
user_session_id: pair.userSessionId,
|
|
19590
|
-
}),
|
|
19591
19626
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19592
19627
|
event_type: 'completed',
|
|
19593
19628
|
outcome: 'failure',
|
|
@@ -19600,7 +19635,6 @@ class ClientEventReporter {
|
|
|
19600
19635
|
this.peerConnectionPairs.delete(key);
|
|
19601
19636
|
};
|
|
19602
19637
|
getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
19603
|
-
getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
19604
19638
|
sessionIdField = (cid) => {
|
|
19605
19639
|
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
19606
19640
|
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
@@ -19612,7 +19646,6 @@ class ClientEventReporter {
|
|
|
19612
19646
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
19613
19647
|
type: ctx?.callType ?? '',
|
|
19614
19648
|
id: ctx?.callId ?? '',
|
|
19615
|
-
call_cid: cid,
|
|
19616
19649
|
stage,
|
|
19617
19650
|
stage_id: pair.sid,
|
|
19618
19651
|
...(pair.joinAttemptIdSnapshot && {
|
|
@@ -19863,6 +19896,7 @@ class StreamVideoClient {
|
|
|
19863
19896
|
}
|
|
19864
19897
|
else {
|
|
19865
19898
|
await call.updateFromRingingEvent(e);
|
|
19899
|
+
await call.get();
|
|
19866
19900
|
}
|
|
19867
19901
|
}
|
|
19868
19902
|
else {
|
|
@@ -19901,6 +19935,13 @@ class StreamVideoClient {
|
|
|
19901
19935
|
this.logger.error(`Failed to init call from event ${e.type}`, err);
|
|
19902
19936
|
}
|
|
19903
19937
|
};
|
|
19938
|
+
/**
|
|
19939
|
+
* Queries the API for calls matching the given filters.
|
|
19940
|
+
* @param data the query data.
|
|
19941
|
+
*/
|
|
19942
|
+
doQueryCalls = (data) => {
|
|
19943
|
+
return this.streamClient.post('/calls', data);
|
|
19944
|
+
};
|
|
19904
19945
|
/**
|
|
19905
19946
|
* Rewatches the given calls with retry logic.
|
|
19906
19947
|
* @param callsToReWatch array of call IDs to rewatch
|
|
@@ -19911,10 +19952,16 @@ class StreamVideoClient {
|
|
|
19911
19952
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
19912
19953
|
try {
|
|
19913
19954
|
this.logger.info(`Rewatching calls ${callsToReWatch.join(', ')} attempt ${attempt + 1}`);
|
|
19914
|
-
await this.
|
|
19955
|
+
const response = await this.doQueryCalls({
|
|
19915
19956
|
watch: true,
|
|
19916
19957
|
filter_conditions: { cid: { $in: callsToReWatch } },
|
|
19917
19958
|
});
|
|
19959
|
+
for (const c of response.calls) {
|
|
19960
|
+
const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
|
|
19961
|
+
if (call) {
|
|
19962
|
+
call.updateFromCallStateResponse(c);
|
|
19963
|
+
}
|
|
19964
|
+
}
|
|
19918
19965
|
return;
|
|
19919
19966
|
}
|
|
19920
19967
|
catch (err) {
|
|
@@ -20057,9 +20104,11 @@ class StreamVideoClient {
|
|
|
20057
20104
|
* Will query the API for calls matching the given filters.
|
|
20058
20105
|
*
|
|
20059
20106
|
* @param data the query data.
|
|
20107
|
+
* @param opts additional options, for tweaking the API behavior.
|
|
20060
20108
|
*/
|
|
20061
|
-
queryCalls = async (data = {}) => {
|
|
20062
|
-
const
|
|
20109
|
+
queryCalls = async (data = {}, opts = {}) => {
|
|
20110
|
+
const { withDisabledDevices = true } = opts;
|
|
20111
|
+
const response = await this.doQueryCalls(data);
|
|
20063
20112
|
const calls = [];
|
|
20064
20113
|
for (const c of response.calls) {
|
|
20065
20114
|
const call = new Call({
|
|
@@ -20073,7 +20122,10 @@ class StreamVideoClient {
|
|
|
20073
20122
|
clientStore: this.writeableStateStore,
|
|
20074
20123
|
});
|
|
20075
20124
|
call.state.updateFromCallResponse(c.call);
|
|
20076
|
-
await call.applyDeviceConfig(c.call.settings,
|
|
20125
|
+
await call.applyDeviceConfig(c.call.settings, {
|
|
20126
|
+
publish: false,
|
|
20127
|
+
withDisabledDevices,
|
|
20128
|
+
});
|
|
20077
20129
|
if (data.watch) {
|
|
20078
20130
|
await call.setup();
|
|
20079
20131
|
this.writeableStateStore.registerCall(call);
|
|
@@ -20225,6 +20277,202 @@ const humanize = (n) => {
|
|
|
20225
20277
|
return String(n);
|
|
20226
20278
|
};
|
|
20227
20279
|
|
|
20280
|
+
/** Default loopback stream wait timeout. */
|
|
20281
|
+
const LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = 10 * 1000;
|
|
20282
|
+
/** Default loopback recording duration. */
|
|
20283
|
+
const DEFAULT_LOOPBACK_RECORDING_DURATION_MS = 10 * 1000;
|
|
20284
|
+
/** Shortest permitted loopback recording duration. */
|
|
20285
|
+
const MIN_LOOPBACK_RECORDING_DURATION_MS = 5 * 1000;
|
|
20286
|
+
/** Longest permitted loopback recording duration. */
|
|
20287
|
+
const MAX_LOOPBACK_RECORDING_DURATION_MS = 2 * 60 * 1000;
|
|
20288
|
+
/** Thrown when loopback tracks do not arrive in time. */
|
|
20289
|
+
class LoopbackStreamsTimeoutError extends Error {
|
|
20290
|
+
constructor() {
|
|
20291
|
+
super('Timed out waiting for loopback streams. Ensure the call was joined ' +
|
|
20292
|
+
'with `allowOwnTracksLoopback: true` and that the SFU is configured ' +
|
|
20293
|
+
'to echo self-sub tracks.');
|
|
20294
|
+
this.name = 'LoopbackStreamsTimeoutError';
|
|
20295
|
+
}
|
|
20296
|
+
}
|
|
20297
|
+
/**
|
|
20298
|
+
* Returns the streams on the given participant that the SFU echoed back to us,
|
|
20299
|
+
* ignoring the local capture streams the publishing path writes to the same
|
|
20300
|
+
* fields.
|
|
20301
|
+
*/
|
|
20302
|
+
const getLoopbackStreams = (call, participant) => {
|
|
20303
|
+
const { subscriber } = call ?? {};
|
|
20304
|
+
const { audioStream, videoStream } = participant ?? {};
|
|
20305
|
+
return {
|
|
20306
|
+
loopbackVideoStream: subscriber?.isSelfSubscribedStream(videoStream)
|
|
20307
|
+
? videoStream
|
|
20308
|
+
: undefined,
|
|
20309
|
+
loopbackAudioStream: subscriber?.isSelfSubscribedStream(audioStream)
|
|
20310
|
+
? audioStream
|
|
20311
|
+
: undefined,
|
|
20312
|
+
};
|
|
20313
|
+
};
|
|
20314
|
+
/**
|
|
20315
|
+
* Returns loopback tracks when all required tracks are present.
|
|
20316
|
+
*
|
|
20317
|
+
* Prefer {@link waitForLoopbackStreams}, which observes call state until the
|
|
20318
|
+
* tracks arrive. This is the synchronous snapshot it is built from.
|
|
20319
|
+
*
|
|
20320
|
+
* @internal
|
|
20321
|
+
*/
|
|
20322
|
+
const getLoopbackTracks = (call, participant, includeVideo) => {
|
|
20323
|
+
if (!participant)
|
|
20324
|
+
return undefined;
|
|
20325
|
+
const { loopbackAudioStream, loopbackVideoStream } = getLoopbackStreams(call, participant);
|
|
20326
|
+
const audioTrack = loopbackAudioStream?.getAudioTracks()[0];
|
|
20327
|
+
const videoTrack = includeVideo
|
|
20328
|
+
? loopbackVideoStream?.getVideoTracks()[0]
|
|
20329
|
+
: undefined;
|
|
20330
|
+
if (!audioTrack || (includeVideo && !videoTrack)) {
|
|
20331
|
+
return undefined;
|
|
20332
|
+
}
|
|
20333
|
+
return { audioTrack, videoTrack };
|
|
20334
|
+
};
|
|
20335
|
+
/** Waits for the requested loopback tracks. */
|
|
20336
|
+
const waitForLoopbackStreams = (call, opts) => {
|
|
20337
|
+
const callState = call.state;
|
|
20338
|
+
const { includeVideo, signal, timeoutMs = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, } = opts;
|
|
20339
|
+
return new Promise((resolve, reject) => {
|
|
20340
|
+
const initial = getLoopbackTracks(call, callState.localParticipant, includeVideo);
|
|
20341
|
+
if (initial) {
|
|
20342
|
+
resolve(initial);
|
|
20343
|
+
return;
|
|
20344
|
+
}
|
|
20345
|
+
if (signal.aborted) {
|
|
20346
|
+
resolve(null);
|
|
20347
|
+
return;
|
|
20348
|
+
}
|
|
20349
|
+
let settled = false;
|
|
20350
|
+
const subscriptions = new rxjs.Subscription();
|
|
20351
|
+
const cleanup = () => {
|
|
20352
|
+
if (settled)
|
|
20353
|
+
return;
|
|
20354
|
+
settled = true;
|
|
20355
|
+
clearTimeout(timeoutId);
|
|
20356
|
+
signal.removeEventListener('abort', onAbort);
|
|
20357
|
+
subscriptions.unsubscribe();
|
|
20358
|
+
};
|
|
20359
|
+
const onAbort = () => {
|
|
20360
|
+
cleanup();
|
|
20361
|
+
resolve(null);
|
|
20362
|
+
};
|
|
20363
|
+
const timeoutId = setTimeout(() => {
|
|
20364
|
+
cleanup();
|
|
20365
|
+
reject(new LoopbackStreamsTimeoutError());
|
|
20366
|
+
}, timeoutMs);
|
|
20367
|
+
signal.addEventListener('abort', onAbort);
|
|
20368
|
+
subscriptions.add(callState.localParticipant$.subscribe((participant) => {
|
|
20369
|
+
const ready = getLoopbackTracks(call, participant, includeVideo);
|
|
20370
|
+
if (ready) {
|
|
20371
|
+
cleanup();
|
|
20372
|
+
resolve(ready);
|
|
20373
|
+
}
|
|
20374
|
+
}));
|
|
20375
|
+
});
|
|
20376
|
+
};
|
|
20377
|
+
/** Enables loopback audio while `fn` runs, then restores it. */
|
|
20378
|
+
const withLoopbackAudioEnabled = async (tracks, fn) => {
|
|
20379
|
+
const restore = enableLoopbackAudio(tracks);
|
|
20380
|
+
try {
|
|
20381
|
+
return await fn();
|
|
20382
|
+
}
|
|
20383
|
+
finally {
|
|
20384
|
+
restore();
|
|
20385
|
+
}
|
|
20386
|
+
};
|
|
20387
|
+
/** Enables loopback audio and returns a restore function. */
|
|
20388
|
+
const enableLoopbackAudio = (tracks) => {
|
|
20389
|
+
const { audioTrack } = tracks;
|
|
20390
|
+
const previous = audioTrack?.enabled ?? false;
|
|
20391
|
+
if (audioTrack) {
|
|
20392
|
+
audioTrack.enabled = true;
|
|
20393
|
+
}
|
|
20394
|
+
return () => {
|
|
20395
|
+
if (audioTrack) {
|
|
20396
|
+
audioTrack.enabled = previous;
|
|
20397
|
+
}
|
|
20398
|
+
};
|
|
20399
|
+
};
|
|
20400
|
+
/** Clamps and rounds a loopback recording duration. */
|
|
20401
|
+
const clampLoopbackRecordingDuration = (durationMs) => Math.round(Math.min(MAX_LOOPBACK_RECORDING_DURATION_MS, Math.max(MIN_LOOPBACK_RECORDING_DURATION_MS, durationMs)));
|
|
20402
|
+
|
|
20403
|
+
/** Single-use `MediaRecorder` wrapper that returns an in-memory blob. */
|
|
20404
|
+
class MediaStreamRecorder {
|
|
20405
|
+
recording = promiseWithResolvers();
|
|
20406
|
+
logger = videoLoggerSystem.getLogger('MediaStreamRecorder');
|
|
20407
|
+
chunks = [];
|
|
20408
|
+
timeoutId;
|
|
20409
|
+
recorder;
|
|
20410
|
+
tracks;
|
|
20411
|
+
maxDurationMs;
|
|
20412
|
+
constructor(stream, options = {}) {
|
|
20413
|
+
if (typeof MediaRecorder === 'undefined') {
|
|
20414
|
+
throw new Error('MediaRecorder is not available in this environment');
|
|
20415
|
+
}
|
|
20416
|
+
this.tracks = stream.getTracks();
|
|
20417
|
+
if (this.tracks.length === 0) {
|
|
20418
|
+
throw new Error('cannot record a MediaStream that has no tracks');
|
|
20419
|
+
}
|
|
20420
|
+
this.maxDurationMs = options.maxDurationMs;
|
|
20421
|
+
this.recorder = new MediaRecorder(new MediaStream(this.tracks));
|
|
20422
|
+
this.recorder.ondataavailable = (event) => {
|
|
20423
|
+
if (event.data?.size)
|
|
20424
|
+
this.chunks.push(event.data);
|
|
20425
|
+
};
|
|
20426
|
+
this.recorder.onstop = this.handleStop;
|
|
20427
|
+
this.recorder.onerror = this.handleError;
|
|
20428
|
+
}
|
|
20429
|
+
/** Starts recording and resolves when recording stops. */
|
|
20430
|
+
start = () => {
|
|
20431
|
+
if (this.isSettled()) {
|
|
20432
|
+
throw new Error('MediaStreamRecorder is single-use: this instance has already ' +
|
|
20433
|
+
'recorded. Construct a new one.');
|
|
20434
|
+
}
|
|
20435
|
+
if (this.recorder.state === 'inactive') {
|
|
20436
|
+
this.tracks.forEach((track) => track.addEventListener('ended', this.stop));
|
|
20437
|
+
if (this.maxDurationMs !== undefined) {
|
|
20438
|
+
this.timeoutId = setTimeout(this.stop, this.maxDurationMs);
|
|
20439
|
+
}
|
|
20440
|
+
this.recorder.start();
|
|
20441
|
+
}
|
|
20442
|
+
return this.recording.promise;
|
|
20443
|
+
};
|
|
20444
|
+
/** Stops recording early. */
|
|
20445
|
+
stop = () => {
|
|
20446
|
+
if (this.recorder.state !== 'inactive')
|
|
20447
|
+
this.recorder.stop();
|
|
20448
|
+
};
|
|
20449
|
+
isSettled = () => this.recording.isResolved() || this.recording.isRejected();
|
|
20450
|
+
detach = () => {
|
|
20451
|
+
clearTimeout(this.timeoutId);
|
|
20452
|
+
this.tracks.forEach((track) => track.removeEventListener('ended', this.stop));
|
|
20453
|
+
};
|
|
20454
|
+
handleStop = () => {
|
|
20455
|
+
this.detach();
|
|
20456
|
+
if (this.chunks.length === 0) {
|
|
20457
|
+
this.logger.warn('MediaRecorder recording produced no data');
|
|
20458
|
+
this.recording.resolve(null);
|
|
20459
|
+
return;
|
|
20460
|
+
}
|
|
20461
|
+
const mimeType = this.chunks[0].type || this.recorder.mimeType;
|
|
20462
|
+
this.recording.resolve({
|
|
20463
|
+
blob: new Blob(this.chunks, { type: mimeType }),
|
|
20464
|
+
});
|
|
20465
|
+
};
|
|
20466
|
+
handleError = (event) => {
|
|
20467
|
+
if (this.isSettled())
|
|
20468
|
+
return;
|
|
20469
|
+
this.detach();
|
|
20470
|
+
const error = event.error;
|
|
20471
|
+
this.logger.error('MediaRecorder error', error);
|
|
20472
|
+
this.recording.reject(error ?? new Error('MediaRecorder failed'));
|
|
20473
|
+
};
|
|
20474
|
+
}
|
|
20475
|
+
|
|
20228
20476
|
/**
|
|
20229
20477
|
* Tiny, type-safe event emitter.
|
|
20230
20478
|
*
|
|
@@ -20598,6 +20846,7 @@ exports.CallTypes = CallTypes;
|
|
|
20598
20846
|
exports.CameraManager = CameraManager;
|
|
20599
20847
|
exports.CameraManagerState = CameraManagerState;
|
|
20600
20848
|
exports.CreateDeviceRequestPushProviderEnum = CreateDeviceRequestPushProviderEnum;
|
|
20849
|
+
exports.DEFAULT_LOOPBACK_RECORDING_DURATION_MS = DEFAULT_LOOPBACK_RECORDING_DURATION_MS;
|
|
20601
20850
|
exports.DeviceManager = DeviceManager;
|
|
20602
20851
|
exports.DeviceManagerState = DeviceManagerState;
|
|
20603
20852
|
exports.DynascaleManager = DynascaleManager;
|
|
@@ -20615,7 +20864,12 @@ exports.IndividualRecordingSettingsResponseModeEnum = IndividualRecordingSetting
|
|
|
20615
20864
|
exports.IngressAudioEncodingOptionsRequestChannelsEnum = IngressAudioEncodingOptionsRequestChannelsEnum;
|
|
20616
20865
|
exports.IngressSourceRequestFpsEnum = IngressSourceRequestFpsEnum;
|
|
20617
20866
|
exports.IngressVideoLayerRequestCodecEnum = IngressVideoLayerRequestCodecEnum;
|
|
20867
|
+
exports.LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS;
|
|
20618
20868
|
exports.LayoutSettingsRequestNameEnum = LayoutSettingsRequestNameEnum;
|
|
20869
|
+
exports.LoopbackStreamsTimeoutError = LoopbackStreamsTimeoutError;
|
|
20870
|
+
exports.MAX_LOOPBACK_RECORDING_DURATION_MS = MAX_LOOPBACK_RECORDING_DURATION_MS;
|
|
20871
|
+
exports.MIN_LOOPBACK_RECORDING_DURATION_MS = MIN_LOOPBACK_RECORDING_DURATION_MS;
|
|
20872
|
+
exports.MediaStreamRecorder = MediaStreamRecorder;
|
|
20619
20873
|
exports.MicrophoneManager = MicrophoneManager;
|
|
20620
20874
|
exports.MicrophoneManagerState = MicrophoneManagerState;
|
|
20621
20875
|
exports.NoiseCancellationSettingsModeEnum = NoiseCancellationSettingsModeEnum;
|
|
@@ -20656,6 +20910,7 @@ exports.VideoSettingsResponseCameraFacingEnum = VideoSettingsResponseCameraFacin
|
|
|
20656
20910
|
exports.ViewportTracker = ViewportTracker;
|
|
20657
20911
|
exports.WSAuthMessageProductsEnum = WSAuthMessageProductsEnum;
|
|
20658
20912
|
exports.checkIfAudioOutputChangeSupported = checkIfAudioOutputChangeSupported;
|
|
20913
|
+
exports.clampLoopbackRecordingDuration = clampLoopbackRecordingDuration;
|
|
20659
20914
|
exports.combineComparators = combineComparators;
|
|
20660
20915
|
exports.conditional = conditional;
|
|
20661
20916
|
exports.createSoundDetector = createSoundDetector;
|
|
@@ -20664,6 +20919,7 @@ exports.descending = descending;
|
|
|
20664
20919
|
exports.deviceIds$ = deviceIds$;
|
|
20665
20920
|
exports.disposeOfMediaStream = disposeOfMediaStream;
|
|
20666
20921
|
exports.dominantSpeaker = dominantSpeaker;
|
|
20922
|
+
exports.enableLoopbackAudio = enableLoopbackAudio;
|
|
20667
20923
|
exports.getAudioBrowserPermission = getAudioBrowserPermission;
|
|
20668
20924
|
exports.getAudioDevices = getAudioDevices;
|
|
20669
20925
|
exports.getAudioOutputDevices = getAudioOutputDevices;
|
|
@@ -20671,6 +20927,8 @@ exports.getAudioStream = getAudioStream;
|
|
|
20671
20927
|
exports.getCallMediaEngineProvider = getCallMediaEngineProvider;
|
|
20672
20928
|
exports.getClientDetails = getClientDetails;
|
|
20673
20929
|
exports.getDeviceState = getDeviceState;
|
|
20930
|
+
exports.getLoopbackStreams = getLoopbackStreams;
|
|
20931
|
+
exports.getLoopbackTracks = getLoopbackTracks;
|
|
20674
20932
|
exports.getScreenShareStream = getScreenShareStream;
|
|
20675
20933
|
exports.getSdkInfo = getSdkInfo;
|
|
20676
20934
|
exports.getVideoBrowserPermission = getVideoBrowserPermission;
|
|
@@ -20707,5 +20965,7 @@ exports.setWebRTCInfo = setWebRTCInfo;
|
|
|
20707
20965
|
exports.speakerLayoutSortPreset = speakerLayoutSortPreset;
|
|
20708
20966
|
exports.speaking = speaking;
|
|
20709
20967
|
exports.videoLoggerSystem = videoLoggerSystem;
|
|
20968
|
+
exports.waitForLoopbackStreams = waitForLoopbackStreams;
|
|
20969
|
+
exports.withLoopbackAudioEnabled = withLoopbackAudioEnabled;
|
|
20710
20970
|
exports.withParticipantSource = withParticipantSource;
|
|
20711
20971
|
//# sourceMappingURL=index.cjs.js.map
|