@stream-io/video-client 1.58.0 → 2.0.0-beta.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 +22 -0
- package/dist/index.browser.es.js +367 -76
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +378 -74
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +367 -76
- 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/coordinator/connection/utils.d.ts +10 -0
- 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/dist/src/sorting/participants.d.ts +13 -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 +43 -24
- package/src/__tests__/StreamVideoClient.test.ts +14 -12
- package/src/coordinator/connection/__tests__/eventListenerIsolation.test.ts +115 -0
- package/src/coordinator/connection/client.ts +3 -2
- package/src/coordinator/connection/utils.ts +28 -0
- 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/src/sorting/__tests__/presets.test.ts +188 -3
- package/src/sorting/__tests__/sorting.test.ts +22 -1
- package/src/sorting/participants.ts +20 -0
- package/src/sorting/presets.ts +4 -0
- package/src/store/__tests__/CallState.test.ts +2 -2
package/dist/index.cjs.js
CHANGED
|
@@ -4431,6 +4431,28 @@ function isErrorResponse(res) {
|
|
|
4431
4431
|
function isCloseEvent(res) {
|
|
4432
4432
|
return res.code !== undefined;
|
|
4433
4433
|
}
|
|
4434
|
+
/**
|
|
4435
|
+
* Invokes a single event listener in isolation, so that a faulty listener
|
|
4436
|
+
* cannot abort the dispatch loop nor the bookkeeping that follows it.
|
|
4437
|
+
*
|
|
4438
|
+
* @param listener the listener to invoke.
|
|
4439
|
+
* @param event the event to pass to the listener.
|
|
4440
|
+
* @param logger the logger to report listener failures with.
|
|
4441
|
+
*/
|
|
4442
|
+
const invokeEventListener = (listener, event, logger) => {
|
|
4443
|
+
try {
|
|
4444
|
+
const result = listener(event);
|
|
4445
|
+
if (result && typeof result.then === 'function') {
|
|
4446
|
+
Promise.resolve(result).catch((error) => report(logger, error, event));
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
catch (error) {
|
|
4450
|
+
report(logger, error, event);
|
|
4451
|
+
}
|
|
4452
|
+
};
|
|
4453
|
+
const report = (logger, error, event) => {
|
|
4454
|
+
logger.error('Unhandled error in event listener', error, event);
|
|
4455
|
+
};
|
|
4434
4456
|
|
|
4435
4457
|
/**
|
|
4436
4458
|
* Checks whether we are using React Native
|
|
@@ -5079,7 +5101,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5079
5101
|
}
|
|
5080
5102
|
};
|
|
5081
5103
|
|
|
5082
|
-
const version = "
|
|
5104
|
+
const version = "2.0.0-beta.0";
|
|
5083
5105
|
const [major, minor, patch] = version.split('.');
|
|
5084
5106
|
let sdkInfo = {
|
|
5085
5107
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5336,6 +5358,27 @@ const withoutConcurrency = createRunner(wrapWithContinuationTracking);
|
|
|
5336
5358
|
*/
|
|
5337
5359
|
const withCancellation = createRunner(wrapWithCancellation);
|
|
5338
5360
|
const pendingPromises = new Map();
|
|
5361
|
+
/**
|
|
5362
|
+
* Wraps an async function so concurrent calls share the in-flight promise
|
|
5363
|
+
* instead of queueing another execution.
|
|
5364
|
+
*
|
|
5365
|
+
* @param cb Async function to wrap.
|
|
5366
|
+
* @returns Wrapped async function.
|
|
5367
|
+
*/
|
|
5368
|
+
function singleFlight(cb) {
|
|
5369
|
+
let pending;
|
|
5370
|
+
return (...args) => {
|
|
5371
|
+
if (pending)
|
|
5372
|
+
return pending;
|
|
5373
|
+
const promise = cb(...args).finally(() => {
|
|
5374
|
+
if (pending === promise) {
|
|
5375
|
+
pending = undefined;
|
|
5376
|
+
}
|
|
5377
|
+
});
|
|
5378
|
+
pending = promise;
|
|
5379
|
+
return promise;
|
|
5380
|
+
};
|
|
5381
|
+
}
|
|
5339
5382
|
function hasPending(tag) {
|
|
5340
5383
|
return pendingPromises.has(tag);
|
|
5341
5384
|
}
|
|
@@ -5907,6 +5950,27 @@ const publishingAudio = (a, b) => {
|
|
|
5907
5950
|
return 1;
|
|
5908
5951
|
return 0;
|
|
5909
5952
|
};
|
|
5953
|
+
/**
|
|
5954
|
+
* A comparator which prioritizes participants who are publishing any track
|
|
5955
|
+
* over participants who are not publishing at all.
|
|
5956
|
+
*
|
|
5957
|
+
* Unlike `publishingVideo` and `publishingAudio`, this comparator does not
|
|
5958
|
+
* distinguish between track types. Muting the microphone or turning off the
|
|
5959
|
+
* camera does not change a participant's rank as long as they still publish
|
|
5960
|
+
* something, which keeps the ordering stable across ordinary device toggles.
|
|
5961
|
+
*
|
|
5962
|
+
* @param a the first participant.
|
|
5963
|
+
* @param b the second participant.
|
|
5964
|
+
*/
|
|
5965
|
+
const publishing = (a, b) => {
|
|
5966
|
+
const hasA = a.publishedTracks.length > 0;
|
|
5967
|
+
const hasB = b.publishedTracks.length > 0;
|
|
5968
|
+
if (hasA && !hasB)
|
|
5969
|
+
return -1;
|
|
5970
|
+
if (!hasA && hasB)
|
|
5971
|
+
return 1;
|
|
5972
|
+
return 0;
|
|
5973
|
+
};
|
|
5910
5974
|
/**
|
|
5911
5975
|
* A comparator which prioritizes participants who are pinned.
|
|
5912
5976
|
*
|
|
@@ -6017,11 +6081,11 @@ const withVideoIngressSource = withParticipantSource(ParticipantSource.RTMP, Par
|
|
|
6017
6081
|
/**
|
|
6018
6082
|
* The default sorting preset.
|
|
6019
6083
|
*/
|
|
6020
|
-
const defaultSortPreset = combineComparators(screenSharing, pinned, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
|
|
6084
|
+
const defaultSortPreset = combineComparators(screenSharing, pinned, publishing, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
|
|
6021
6085
|
/**
|
|
6022
6086
|
* The sorting preset for speaker layout.
|
|
6023
6087
|
*/
|
|
6024
|
-
const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
|
|
6088
|
+
const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, publishing, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
|
|
6025
6089
|
/**
|
|
6026
6090
|
* The sorting preset for layouts that don't render all participants but
|
|
6027
6091
|
* instead, render them in pages.
|
|
@@ -6030,7 +6094,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
|
|
|
6030
6094
|
/**
|
|
6031
6095
|
* The sorting preset for livestreams and audio rooms.
|
|
6032
6096
|
*/
|
|
6033
|
-
const livestreamOrAudioRoomSortPreset = combineComparators(ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
|
|
6097
|
+
const livestreamOrAudioRoomSortPreset = combineComparators(publishing, ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
|
|
6034
6098
|
|
|
6035
6099
|
/**
|
|
6036
6100
|
* Returns the default egress object - when no egress data is available.
|
|
@@ -9824,6 +9888,11 @@ class Subscriber extends BasePeerConnection {
|
|
|
9824
9888
|
throw e;
|
|
9825
9889
|
}
|
|
9826
9890
|
};
|
|
9891
|
+
/**
|
|
9892
|
+
* Whether the given stream is one the SFU echoed back to us
|
|
9893
|
+
* (`allowOwnTracksLoopback`) rather than a local capture stream.
|
|
9894
|
+
*/
|
|
9895
|
+
isSelfSubscribedStream = (stream) => !!stream && !!this.trackedStreams?.has(stream);
|
|
9827
9896
|
handleOnTrack = (e) => {
|
|
9828
9897
|
const { streams, track } = e;
|
|
9829
9898
|
const [primaryStream] = streams;
|
|
@@ -9913,6 +9982,8 @@ class Subscriber extends BasePeerConnection {
|
|
|
9913
9982
|
const target = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9914
9983
|
if (!target)
|
|
9915
9984
|
return;
|
|
9985
|
+
if (target.isLocalParticipant)
|
|
9986
|
+
return;
|
|
9916
9987
|
this.state.updateParticipant(target.sessionId, (p) => {
|
|
9917
9988
|
const current = p.interruptedTracks ?? [];
|
|
9918
9989
|
const has = current.includes(trackType);
|
|
@@ -10655,7 +10726,6 @@ const watchCallRejected = (call) => {
|
|
|
10655
10726
|
*/
|
|
10656
10727
|
const watchCallEnded = (call) => {
|
|
10657
10728
|
return function onCallEnded() {
|
|
10658
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
10659
10729
|
const { callingState } = call.state;
|
|
10660
10730
|
if (callingState !== exports.CallingState.IDLE &&
|
|
10661
10731
|
callingState !== exports.CallingState.LEFT) {
|
|
@@ -10664,9 +10734,13 @@ const watchCallEnded = (call) => {
|
|
|
10664
10734
|
reason: 'call.ended event received',
|
|
10665
10735
|
});
|
|
10666
10736
|
call
|
|
10667
|
-
.leave({
|
|
10737
|
+
.leave({
|
|
10738
|
+
message: 'call.ended event received',
|
|
10739
|
+
reject: false,
|
|
10740
|
+
reason: 'ended',
|
|
10741
|
+
})
|
|
10668
10742
|
.catch((err) => {
|
|
10669
|
-
call.logger.error('Failed to leave call after call.ended
|
|
10743
|
+
call.logger.error('Failed to leave call after call.ended', err);
|
|
10670
10744
|
});
|
|
10671
10745
|
}
|
|
10672
10746
|
};
|
|
@@ -13472,7 +13546,7 @@ class DeviceManager {
|
|
|
13472
13546
|
createSyntheticDevice(selectedDevice, deviceKind);
|
|
13473
13547
|
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
13474
13548
|
}
|
|
13475
|
-
async applyPersistedPreferences(enabledInCallType) {
|
|
13549
|
+
async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
|
|
13476
13550
|
const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
13477
13551
|
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
13478
13552
|
const preferenceList = toPreferenceList(preferences[deviceKey]);
|
|
@@ -13499,13 +13573,13 @@ class DeviceManager {
|
|
|
13499
13573
|
}
|
|
13500
13574
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
13501
13575
|
if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
|
|
13502
|
-
await this.applyMutedState(muted);
|
|
13576
|
+
await this.applyMutedState(muted, forceDisabled);
|
|
13503
13577
|
appliedMute = true;
|
|
13504
13578
|
}
|
|
13505
13579
|
return appliedDevice || appliedMute;
|
|
13506
13580
|
}
|
|
13507
|
-
async applyMutedState(muted) {
|
|
13508
|
-
if (this.state.status !== undefined)
|
|
13581
|
+
async applyMutedState(muted, forceDisabled) {
|
|
13582
|
+
if (forceDisabled || this.state.status !== undefined)
|
|
13509
13583
|
return;
|
|
13510
13584
|
if (muted) {
|
|
13511
13585
|
await this.disable();
|
|
@@ -13857,8 +13931,9 @@ class CameraManager extends DeviceManager {
|
|
|
13857
13931
|
*
|
|
13858
13932
|
* @param settings the video settings to apply.
|
|
13859
13933
|
* @param publish whether to publish the stream after applying the settings.
|
|
13934
|
+
* @param forceDisabled whether to keep the camera disabled, regardless of server-side or local preferences.
|
|
13860
13935
|
*/
|
|
13861
|
-
async apply(settings, publish) {
|
|
13936
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
13862
13937
|
// Wait for any in progress camera operation
|
|
13863
13938
|
await this.statusChangeSettled();
|
|
13864
13939
|
await this.selectTargetResolution(settings.target_resolution);
|
|
@@ -13870,8 +13945,7 @@ class CameraManager extends DeviceManager {
|
|
|
13870
13945
|
if (shouldApplyDefaults &&
|
|
13871
13946
|
this.devicePersistence.enabled &&
|
|
13872
13947
|
permissionState === 'granted') {
|
|
13873
|
-
persistedPreferencesApplied =
|
|
13874
|
-
await this.applyPersistedPreferences(enabledInCallType);
|
|
13948
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(enabledInCallType, forceDisabled);
|
|
13875
13949
|
}
|
|
13876
13950
|
// apply a direction and enable the camera only if in "pristine" state,
|
|
13877
13951
|
// and there are no persisted preferences
|
|
@@ -13881,7 +13955,10 @@ class CameraManager extends DeviceManager {
|
|
|
13881
13955
|
const direction = settings.camera_facing === 'front' ? 'front' : 'back';
|
|
13882
13956
|
await this.selectDirection(direction, { enableCamera: false });
|
|
13883
13957
|
}
|
|
13884
|
-
if (
|
|
13958
|
+
if (!forceDisabled &&
|
|
13959
|
+
canPublish &&
|
|
13960
|
+
settings.camera_default_on &&
|
|
13961
|
+
enabledInCallType) {
|
|
13885
13962
|
await this.enable();
|
|
13886
13963
|
}
|
|
13887
13964
|
}
|
|
@@ -14476,8 +14553,9 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14476
14553
|
* Applies the audio settings to the microphone.
|
|
14477
14554
|
* @param settings the audio settings to apply.
|
|
14478
14555
|
* @param publish whether to publish the stream after applying the settings.
|
|
14556
|
+
* @param forceDisabled whether to keep the mic disabled, regardless of server-side or local preferences.
|
|
14479
14557
|
*/
|
|
14480
|
-
async apply(settings, publish) {
|
|
14558
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
14481
14559
|
// Wait for any in progress mic operation
|
|
14482
14560
|
await this.statusChangeSettled();
|
|
14483
14561
|
// apply server-side settings only when the device state is pristine
|
|
@@ -14489,11 +14567,11 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14489
14567
|
if (shouldApplyDefaults &&
|
|
14490
14568
|
this.devicePersistence.enabled &&
|
|
14491
14569
|
permissionState === 'granted') {
|
|
14492
|
-
persistedPreferencesApplied = await this.applyPersistedPreferences(true);
|
|
14570
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(true, forceDisabled);
|
|
14493
14571
|
}
|
|
14494
14572
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
14495
14573
|
if (shouldApplyDefaults && !persistedPreferencesApplied) {
|
|
14496
|
-
if (canPublish && settings.mic_default_on) {
|
|
14574
|
+
if (!forceDisabled && canPublish && settings.mic_default_on) {
|
|
14497
14575
|
await this.enable();
|
|
14498
14576
|
}
|
|
14499
14577
|
}
|
|
@@ -15462,7 +15540,7 @@ class Call {
|
|
|
15462
15540
|
}
|
|
15463
15541
|
}
|
|
15464
15542
|
}
|
|
15465
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
15543
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, reason === 'ended' ? 'remote' : undefined);
|
|
15466
15544
|
this.statsReporter?.stop();
|
|
15467
15545
|
this.statsReporter = undefined;
|
|
15468
15546
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
@@ -15625,8 +15703,18 @@ class Call {
|
|
|
15625
15703
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
15626
15704
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
15627
15705
|
this.clientStore.setCalls([this, ...calls]);
|
|
15628
|
-
|
|
15629
|
-
|
|
15706
|
+
await this.applyDeviceConfig(settings, { publish: false });
|
|
15707
|
+
};
|
|
15708
|
+
/**
|
|
15709
|
+
* Applies a backend-provided call state snapshot
|
|
15710
|
+
* (call, members, own capabilities) to this instance.
|
|
15711
|
+
*
|
|
15712
|
+
* @internal an internal method and should not be used outside the SDK.
|
|
15713
|
+
*/
|
|
15714
|
+
updateFromCallStateResponse = (response) => {
|
|
15715
|
+
this.state.updateFromCallResponse(response.call);
|
|
15716
|
+
this.state.setMembers(response.members);
|
|
15717
|
+
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15630
15718
|
};
|
|
15631
15719
|
/**
|
|
15632
15720
|
* Loads the information about the call.
|
|
@@ -15639,9 +15727,7 @@ class Call {
|
|
|
15639
15727
|
get = async (params) => {
|
|
15640
15728
|
await this.setup();
|
|
15641
15729
|
const response = await this.streamClient.get(this.streamClientBasePath, params);
|
|
15642
|
-
this.
|
|
15643
|
-
this.state.setMembers(response.members);
|
|
15644
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15730
|
+
this.updateFromCallStateResponse(response);
|
|
15645
15731
|
if (params?.ring) {
|
|
15646
15732
|
this.ringingSubject.next(true);
|
|
15647
15733
|
}
|
|
@@ -15649,9 +15735,7 @@ class Call {
|
|
|
15649
15735
|
this.watching = true;
|
|
15650
15736
|
this.clientStore.registerOrUpdateCall(this);
|
|
15651
15737
|
}
|
|
15652
|
-
|
|
15653
|
-
const skipSpeakerApply = isReactNative();
|
|
15654
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15738
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15655
15739
|
return response;
|
|
15656
15740
|
};
|
|
15657
15741
|
/**
|
|
@@ -15662,9 +15746,7 @@ class Call {
|
|
|
15662
15746
|
getOrCreate = async (data) => {
|
|
15663
15747
|
await this.setup();
|
|
15664
15748
|
const response = await this.streamClient.post(this.streamClientBasePath, data);
|
|
15665
|
-
this.
|
|
15666
|
-
this.state.setMembers(response.members);
|
|
15667
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15749
|
+
this.updateFromCallStateResponse(response);
|
|
15668
15750
|
if (data?.ring) {
|
|
15669
15751
|
this.ringingSubject.next(true);
|
|
15670
15752
|
}
|
|
@@ -15672,9 +15754,7 @@ class Call {
|
|
|
15672
15754
|
this.watching = true;
|
|
15673
15755
|
this.clientStore.registerOrUpdateCall(this);
|
|
15674
15756
|
}
|
|
15675
|
-
|
|
15676
|
-
const skipSpeakerApply = isReactNative();
|
|
15677
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15757
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15678
15758
|
return response;
|
|
15679
15759
|
};
|
|
15680
15760
|
/**
|
|
@@ -15738,7 +15818,7 @@ class Call {
|
|
|
15738
15818
|
*
|
|
15739
15819
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
15740
15820
|
*/
|
|
15741
|
-
join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15821
|
+
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15742
15822
|
const callingState = this.state.callingState;
|
|
15743
15823
|
if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
|
|
15744
15824
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -15760,7 +15840,6 @@ class Call {
|
|
|
15760
15840
|
callId: this.id,
|
|
15761
15841
|
getCallSessionId: () => this.state.session?.id ?? '',
|
|
15762
15842
|
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
15763
|
-
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
15764
15843
|
});
|
|
15765
15844
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
15766
15845
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
@@ -15812,7 +15891,7 @@ class Call {
|
|
|
15812
15891
|
callingX?.endCall(this, 'error');
|
|
15813
15892
|
throw error;
|
|
15814
15893
|
}
|
|
15815
|
-
};
|
|
15894
|
+
});
|
|
15816
15895
|
/**
|
|
15817
15896
|
* Will make a single attempt to watch for call related WebSocket events
|
|
15818
15897
|
* and initiate a call session with the server.
|
|
@@ -15984,7 +16063,10 @@ class Call {
|
|
|
15984
16063
|
if (!this.deviceSettingsAppliedOnce &&
|
|
15985
16064
|
this.state.settings &&
|
|
15986
16065
|
!supersededByLeave()) {
|
|
15987
|
-
await this.applyDeviceConfig(this.state.settings,
|
|
16066
|
+
await this.applyDeviceConfig(this.state.settings, {
|
|
16067
|
+
publish: true,
|
|
16068
|
+
skipSpeakerApply: false,
|
|
16069
|
+
});
|
|
15988
16070
|
this.deviceSettingsAppliedOnce = true;
|
|
15989
16071
|
}
|
|
15990
16072
|
if (!this.callManagerStarted && !supersededByLeave()) {
|
|
@@ -16774,12 +16856,16 @@ class Call {
|
|
|
16774
16856
|
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
16775
16857
|
if (!streamStateProp)
|
|
16776
16858
|
continue;
|
|
16777
|
-
this.state.updateParticipant(sessionId, (p) =>
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
|
|
16859
|
+
this.state.updateParticipant(sessionId, (p) => {
|
|
16860
|
+
const currentStream = p[streamStateProp];
|
|
16861
|
+
const isEchoedStream = this.subscriber?.isSelfSubscribedStream(currentStream);
|
|
16862
|
+
return {
|
|
16863
|
+
publishedTracks: mediaStream
|
|
16864
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
16865
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
16866
|
+
[streamStateProp]: isEchoedStream ? currentStream : mediaStream,
|
|
16867
|
+
};
|
|
16868
|
+
});
|
|
16783
16869
|
}
|
|
16784
16870
|
};
|
|
16785
16871
|
/**
|
|
@@ -17469,18 +17555,21 @@ class Call {
|
|
|
17469
17555
|
*
|
|
17470
17556
|
* @internal
|
|
17471
17557
|
*/
|
|
17472
|
-
applyDeviceConfig = async (settings,
|
|
17558
|
+
applyDeviceConfig = async (settings, opts) => {
|
|
17559
|
+
const { publish,
|
|
17560
|
+
// Skip speaker setup on RN until joining (explicitly passed as false during join())
|
|
17561
|
+
skipSpeakerApply = isReactNative(), withDisabledDevices, } = opts;
|
|
17473
17562
|
if (!skipSpeakerApply) {
|
|
17474
|
-
await this.speaker
|
|
17475
|
-
|
|
17476
|
-
|
|
17563
|
+
await this.speaker
|
|
17564
|
+
.apply(settings)
|
|
17565
|
+
.catch((err) => this.logger.warn('Speaker init failed', err));
|
|
17477
17566
|
}
|
|
17478
|
-
await this.camera
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
await this.microphone
|
|
17482
|
-
|
|
17483
|
-
|
|
17567
|
+
await this.camera
|
|
17568
|
+
.apply(settings.video, publish, withDisabledDevices)
|
|
17569
|
+
.catch((err) => this.logger.warn('Camera init failed', err));
|
|
17570
|
+
await this.microphone
|
|
17571
|
+
.apply(settings.audio, publish, withDisabledDevices)
|
|
17572
|
+
.catch((err) => this.logger.warn('Mic init failed', err));
|
|
17484
17573
|
};
|
|
17485
17574
|
/**
|
|
17486
17575
|
* Will begin tracking the given element for visibility changes within the
|
|
@@ -18964,11 +19053,11 @@ class StreamClient {
|
|
|
18964
19053
|
return;
|
|
18965
19054
|
// call generic listeners
|
|
18966
19055
|
for (const listener of this.listeners.all || []) {
|
|
18967
|
-
listener
|
|
19056
|
+
invokeEventListener(listener, event, this.logger);
|
|
18968
19057
|
}
|
|
18969
19058
|
// call type specific listeners
|
|
18970
19059
|
for (const listener of this.listeners[event.type] || []) {
|
|
18971
|
-
listener
|
|
19060
|
+
invokeEventListener(listener, event, this.logger);
|
|
18972
19061
|
}
|
|
18973
19062
|
};
|
|
18974
19063
|
/**
|
|
@@ -18988,11 +19077,11 @@ class StreamClient {
|
|
|
18988
19077
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18989
19078
|
};
|
|
18990
19079
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18991
|
-
"
|
|
19080
|
+
"2.0.0-beta.0";
|
|
18992
19081
|
getUserAgent = () => {
|
|
18993
19082
|
if (!this.cachedUserAgent) {
|
|
18994
19083
|
const { clientAppIdentifier = {} } = this.options;
|
|
18995
|
-
const { sdkName = 'js', sdkVersion = "
|
|
19084
|
+
const { sdkName = 'js', sdkVersion = "2.0.0-beta.0", ...extras } = clientAppIdentifier;
|
|
18996
19085
|
this.cachedUserAgent = [
|
|
18997
19086
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18998
19087
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19355,7 +19444,6 @@ class ClientEventReporter {
|
|
|
19355
19444
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
19356
19445
|
type: ctx?.callType,
|
|
19357
19446
|
id: ctx?.callId,
|
|
19358
|
-
call_cid: cid,
|
|
19359
19447
|
stage: 'JoinInitiated',
|
|
19360
19448
|
join_attempt_id: joinAttemptId,
|
|
19361
19449
|
...(coordinatorConnectId && {
|
|
@@ -19537,7 +19625,6 @@ class ClientEventReporter {
|
|
|
19537
19625
|
startedAt: Date.now(),
|
|
19538
19626
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
19539
19627
|
sfuId: this.getSfuId(cid),
|
|
19540
|
-
userSessionId: this.getUserSessionId(cid),
|
|
19541
19628
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
19542
19629
|
lastIceState: iceConnectionState,
|
|
19543
19630
|
};
|
|
@@ -19548,9 +19635,6 @@ class ClientEventReporter {
|
|
|
19548
19635
|
peer_connection: role,
|
|
19549
19636
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19550
19637
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19551
|
-
...(pair.userSessionId && {
|
|
19552
|
-
user_session_id: pair.userSessionId,
|
|
19553
|
-
}),
|
|
19554
19638
|
event_type: 'initiated',
|
|
19555
19639
|
});
|
|
19556
19640
|
};
|
|
@@ -19565,9 +19649,6 @@ class ClientEventReporter {
|
|
|
19565
19649
|
peer_connection: role,
|
|
19566
19650
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19567
19651
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19568
|
-
...(pair.userSessionId && {
|
|
19569
|
-
user_session_id: pair.userSessionId,
|
|
19570
|
-
}),
|
|
19571
19652
|
event_type: 'completed',
|
|
19572
19653
|
outcome: 'success',
|
|
19573
19654
|
retry_count_attempt: 0,
|
|
@@ -19585,9 +19666,6 @@ class ClientEventReporter {
|
|
|
19585
19666
|
...this.sessionIdField(cid),
|
|
19586
19667
|
peer_connection: role,
|
|
19587
19668
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19588
|
-
...(pair.userSessionId && {
|
|
19589
|
-
user_session_id: pair.userSessionId,
|
|
19590
|
-
}),
|
|
19591
19669
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19592
19670
|
event_type: 'completed',
|
|
19593
19671
|
outcome: 'failure',
|
|
@@ -19600,7 +19678,6 @@ class ClientEventReporter {
|
|
|
19600
19678
|
this.peerConnectionPairs.delete(key);
|
|
19601
19679
|
};
|
|
19602
19680
|
getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
19603
|
-
getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
19604
19681
|
sessionIdField = (cid) => {
|
|
19605
19682
|
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
19606
19683
|
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
@@ -19612,7 +19689,6 @@ class ClientEventReporter {
|
|
|
19612
19689
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
19613
19690
|
type: ctx?.callType ?? '',
|
|
19614
19691
|
id: ctx?.callId ?? '',
|
|
19615
|
-
call_cid: cid,
|
|
19616
19692
|
stage,
|
|
19617
19693
|
stage_id: pair.sid,
|
|
19618
19694
|
...(pair.joinAttemptIdSnapshot && {
|
|
@@ -19863,6 +19939,7 @@ class StreamVideoClient {
|
|
|
19863
19939
|
}
|
|
19864
19940
|
else {
|
|
19865
19941
|
await call.updateFromRingingEvent(e);
|
|
19942
|
+
await call.get();
|
|
19866
19943
|
}
|
|
19867
19944
|
}
|
|
19868
19945
|
else {
|
|
@@ -19901,6 +19978,13 @@ class StreamVideoClient {
|
|
|
19901
19978
|
this.logger.error(`Failed to init call from event ${e.type}`, err);
|
|
19902
19979
|
}
|
|
19903
19980
|
};
|
|
19981
|
+
/**
|
|
19982
|
+
* Queries the API for calls matching the given filters.
|
|
19983
|
+
* @param data the query data.
|
|
19984
|
+
*/
|
|
19985
|
+
doQueryCalls = (data) => {
|
|
19986
|
+
return this.streamClient.post('/calls', data);
|
|
19987
|
+
};
|
|
19904
19988
|
/**
|
|
19905
19989
|
* Rewatches the given calls with retry logic.
|
|
19906
19990
|
* @param callsToReWatch array of call IDs to rewatch
|
|
@@ -19911,10 +19995,16 @@ class StreamVideoClient {
|
|
|
19911
19995
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
19912
19996
|
try {
|
|
19913
19997
|
this.logger.info(`Rewatching calls ${callsToReWatch.join(', ')} attempt ${attempt + 1}`);
|
|
19914
|
-
await this.
|
|
19998
|
+
const response = await this.doQueryCalls({
|
|
19915
19999
|
watch: true,
|
|
19916
20000
|
filter_conditions: { cid: { $in: callsToReWatch } },
|
|
19917
20001
|
});
|
|
20002
|
+
for (const c of response.calls) {
|
|
20003
|
+
const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
|
|
20004
|
+
if (call) {
|
|
20005
|
+
call.updateFromCallStateResponse(c);
|
|
20006
|
+
}
|
|
20007
|
+
}
|
|
19918
20008
|
return;
|
|
19919
20009
|
}
|
|
19920
20010
|
catch (err) {
|
|
@@ -20057,9 +20147,11 @@ class StreamVideoClient {
|
|
|
20057
20147
|
* Will query the API for calls matching the given filters.
|
|
20058
20148
|
*
|
|
20059
20149
|
* @param data the query data.
|
|
20150
|
+
* @param opts additional options, for tweaking the API behavior.
|
|
20060
20151
|
*/
|
|
20061
|
-
queryCalls = async (data = {}) => {
|
|
20062
|
-
const
|
|
20152
|
+
queryCalls = async (data = {}, opts = {}) => {
|
|
20153
|
+
const { withDisabledDevices = true } = opts;
|
|
20154
|
+
const response = await this.doQueryCalls(data);
|
|
20063
20155
|
const calls = [];
|
|
20064
20156
|
for (const c of response.calls) {
|
|
20065
20157
|
const call = new Call({
|
|
@@ -20073,7 +20165,10 @@ class StreamVideoClient {
|
|
|
20073
20165
|
clientStore: this.writeableStateStore,
|
|
20074
20166
|
});
|
|
20075
20167
|
call.state.updateFromCallResponse(c.call);
|
|
20076
|
-
await call.applyDeviceConfig(c.call.settings,
|
|
20168
|
+
await call.applyDeviceConfig(c.call.settings, {
|
|
20169
|
+
publish: false,
|
|
20170
|
+
withDisabledDevices,
|
|
20171
|
+
});
|
|
20077
20172
|
if (data.watch) {
|
|
20078
20173
|
await call.setup();
|
|
20079
20174
|
this.writeableStateStore.registerCall(call);
|
|
@@ -20225,6 +20320,202 @@ const humanize = (n) => {
|
|
|
20225
20320
|
return String(n);
|
|
20226
20321
|
};
|
|
20227
20322
|
|
|
20323
|
+
/** Default loopback stream wait timeout. */
|
|
20324
|
+
const LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = 10 * 1000;
|
|
20325
|
+
/** Default loopback recording duration. */
|
|
20326
|
+
const DEFAULT_LOOPBACK_RECORDING_DURATION_MS = 10 * 1000;
|
|
20327
|
+
/** Shortest permitted loopback recording duration. */
|
|
20328
|
+
const MIN_LOOPBACK_RECORDING_DURATION_MS = 5 * 1000;
|
|
20329
|
+
/** Longest permitted loopback recording duration. */
|
|
20330
|
+
const MAX_LOOPBACK_RECORDING_DURATION_MS = 2 * 60 * 1000;
|
|
20331
|
+
/** Thrown when loopback tracks do not arrive in time. */
|
|
20332
|
+
class LoopbackStreamsTimeoutError extends Error {
|
|
20333
|
+
constructor() {
|
|
20334
|
+
super('Timed out waiting for loopback streams. Ensure the call was joined ' +
|
|
20335
|
+
'with `allowOwnTracksLoopback: true` and that the SFU is configured ' +
|
|
20336
|
+
'to echo self-sub tracks.');
|
|
20337
|
+
this.name = 'LoopbackStreamsTimeoutError';
|
|
20338
|
+
}
|
|
20339
|
+
}
|
|
20340
|
+
/**
|
|
20341
|
+
* Returns the streams on the given participant that the SFU echoed back to us,
|
|
20342
|
+
* ignoring the local capture streams the publishing path writes to the same
|
|
20343
|
+
* fields.
|
|
20344
|
+
*/
|
|
20345
|
+
const getLoopbackStreams = (call, participant) => {
|
|
20346
|
+
const { subscriber } = call ?? {};
|
|
20347
|
+
const { audioStream, videoStream } = participant ?? {};
|
|
20348
|
+
return {
|
|
20349
|
+
loopbackVideoStream: subscriber?.isSelfSubscribedStream(videoStream)
|
|
20350
|
+
? videoStream
|
|
20351
|
+
: undefined,
|
|
20352
|
+
loopbackAudioStream: subscriber?.isSelfSubscribedStream(audioStream)
|
|
20353
|
+
? audioStream
|
|
20354
|
+
: undefined,
|
|
20355
|
+
};
|
|
20356
|
+
};
|
|
20357
|
+
/**
|
|
20358
|
+
* Returns loopback tracks when all required tracks are present.
|
|
20359
|
+
*
|
|
20360
|
+
* Prefer {@link waitForLoopbackStreams}, which observes call state until the
|
|
20361
|
+
* tracks arrive. This is the synchronous snapshot it is built from.
|
|
20362
|
+
*
|
|
20363
|
+
* @internal
|
|
20364
|
+
*/
|
|
20365
|
+
const getLoopbackTracks = (call, participant, includeVideo) => {
|
|
20366
|
+
if (!participant)
|
|
20367
|
+
return undefined;
|
|
20368
|
+
const { loopbackAudioStream, loopbackVideoStream } = getLoopbackStreams(call, participant);
|
|
20369
|
+
const audioTrack = loopbackAudioStream?.getAudioTracks()[0];
|
|
20370
|
+
const videoTrack = includeVideo
|
|
20371
|
+
? loopbackVideoStream?.getVideoTracks()[0]
|
|
20372
|
+
: undefined;
|
|
20373
|
+
if (!audioTrack || (includeVideo && !videoTrack)) {
|
|
20374
|
+
return undefined;
|
|
20375
|
+
}
|
|
20376
|
+
return { audioTrack, videoTrack };
|
|
20377
|
+
};
|
|
20378
|
+
/** Waits for the requested loopback tracks. */
|
|
20379
|
+
const waitForLoopbackStreams = (call, opts) => {
|
|
20380
|
+
const callState = call.state;
|
|
20381
|
+
const { includeVideo, signal, timeoutMs = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, } = opts;
|
|
20382
|
+
return new Promise((resolve, reject) => {
|
|
20383
|
+
const initial = getLoopbackTracks(call, callState.localParticipant, includeVideo);
|
|
20384
|
+
if (initial) {
|
|
20385
|
+
resolve(initial);
|
|
20386
|
+
return;
|
|
20387
|
+
}
|
|
20388
|
+
if (signal.aborted) {
|
|
20389
|
+
resolve(null);
|
|
20390
|
+
return;
|
|
20391
|
+
}
|
|
20392
|
+
let settled = false;
|
|
20393
|
+
const subscriptions = new rxjs.Subscription();
|
|
20394
|
+
const cleanup = () => {
|
|
20395
|
+
if (settled)
|
|
20396
|
+
return;
|
|
20397
|
+
settled = true;
|
|
20398
|
+
clearTimeout(timeoutId);
|
|
20399
|
+
signal.removeEventListener('abort', onAbort);
|
|
20400
|
+
subscriptions.unsubscribe();
|
|
20401
|
+
};
|
|
20402
|
+
const onAbort = () => {
|
|
20403
|
+
cleanup();
|
|
20404
|
+
resolve(null);
|
|
20405
|
+
};
|
|
20406
|
+
const timeoutId = setTimeout(() => {
|
|
20407
|
+
cleanup();
|
|
20408
|
+
reject(new LoopbackStreamsTimeoutError());
|
|
20409
|
+
}, timeoutMs);
|
|
20410
|
+
signal.addEventListener('abort', onAbort);
|
|
20411
|
+
subscriptions.add(callState.localParticipant$.subscribe((participant) => {
|
|
20412
|
+
const ready = getLoopbackTracks(call, participant, includeVideo);
|
|
20413
|
+
if (ready) {
|
|
20414
|
+
cleanup();
|
|
20415
|
+
resolve(ready);
|
|
20416
|
+
}
|
|
20417
|
+
}));
|
|
20418
|
+
});
|
|
20419
|
+
};
|
|
20420
|
+
/** Enables loopback audio while `fn` runs, then restores it. */
|
|
20421
|
+
const withLoopbackAudioEnabled = async (tracks, fn) => {
|
|
20422
|
+
const restore = enableLoopbackAudio(tracks);
|
|
20423
|
+
try {
|
|
20424
|
+
return await fn();
|
|
20425
|
+
}
|
|
20426
|
+
finally {
|
|
20427
|
+
restore();
|
|
20428
|
+
}
|
|
20429
|
+
};
|
|
20430
|
+
/** Enables loopback audio and returns a restore function. */
|
|
20431
|
+
const enableLoopbackAudio = (tracks) => {
|
|
20432
|
+
const { audioTrack } = tracks;
|
|
20433
|
+
const previous = audioTrack?.enabled ?? false;
|
|
20434
|
+
if (audioTrack) {
|
|
20435
|
+
audioTrack.enabled = true;
|
|
20436
|
+
}
|
|
20437
|
+
return () => {
|
|
20438
|
+
if (audioTrack) {
|
|
20439
|
+
audioTrack.enabled = previous;
|
|
20440
|
+
}
|
|
20441
|
+
};
|
|
20442
|
+
};
|
|
20443
|
+
/** Clamps and rounds a loopback recording duration. */
|
|
20444
|
+
const clampLoopbackRecordingDuration = (durationMs) => Math.round(Math.min(MAX_LOOPBACK_RECORDING_DURATION_MS, Math.max(MIN_LOOPBACK_RECORDING_DURATION_MS, durationMs)));
|
|
20445
|
+
|
|
20446
|
+
/** Single-use `MediaRecorder` wrapper that returns an in-memory blob. */
|
|
20447
|
+
class MediaStreamRecorder {
|
|
20448
|
+
recording = promiseWithResolvers();
|
|
20449
|
+
logger = videoLoggerSystem.getLogger('MediaStreamRecorder');
|
|
20450
|
+
chunks = [];
|
|
20451
|
+
timeoutId;
|
|
20452
|
+
recorder;
|
|
20453
|
+
tracks;
|
|
20454
|
+
maxDurationMs;
|
|
20455
|
+
constructor(stream, options = {}) {
|
|
20456
|
+
if (typeof MediaRecorder === 'undefined') {
|
|
20457
|
+
throw new Error('MediaRecorder is not available in this environment');
|
|
20458
|
+
}
|
|
20459
|
+
this.tracks = stream.getTracks();
|
|
20460
|
+
if (this.tracks.length === 0) {
|
|
20461
|
+
throw new Error('cannot record a MediaStream that has no tracks');
|
|
20462
|
+
}
|
|
20463
|
+
this.maxDurationMs = options.maxDurationMs;
|
|
20464
|
+
this.recorder = new MediaRecorder(new MediaStream(this.tracks));
|
|
20465
|
+
this.recorder.ondataavailable = (event) => {
|
|
20466
|
+
if (event.data?.size)
|
|
20467
|
+
this.chunks.push(event.data);
|
|
20468
|
+
};
|
|
20469
|
+
this.recorder.onstop = this.handleStop;
|
|
20470
|
+
this.recorder.onerror = this.handleError;
|
|
20471
|
+
}
|
|
20472
|
+
/** Starts recording and resolves when recording stops. */
|
|
20473
|
+
start = () => {
|
|
20474
|
+
if (this.isSettled()) {
|
|
20475
|
+
throw new Error('MediaStreamRecorder is single-use: this instance has already ' +
|
|
20476
|
+
'recorded. Construct a new one.');
|
|
20477
|
+
}
|
|
20478
|
+
if (this.recorder.state === 'inactive') {
|
|
20479
|
+
this.tracks.forEach((track) => track.addEventListener('ended', this.stop));
|
|
20480
|
+
if (this.maxDurationMs !== undefined) {
|
|
20481
|
+
this.timeoutId = setTimeout(this.stop, this.maxDurationMs);
|
|
20482
|
+
}
|
|
20483
|
+
this.recorder.start();
|
|
20484
|
+
}
|
|
20485
|
+
return this.recording.promise;
|
|
20486
|
+
};
|
|
20487
|
+
/** Stops recording early. */
|
|
20488
|
+
stop = () => {
|
|
20489
|
+
if (this.recorder.state !== 'inactive')
|
|
20490
|
+
this.recorder.stop();
|
|
20491
|
+
};
|
|
20492
|
+
isSettled = () => this.recording.isResolved() || this.recording.isRejected();
|
|
20493
|
+
detach = () => {
|
|
20494
|
+
clearTimeout(this.timeoutId);
|
|
20495
|
+
this.tracks.forEach((track) => track.removeEventListener('ended', this.stop));
|
|
20496
|
+
};
|
|
20497
|
+
handleStop = () => {
|
|
20498
|
+
this.detach();
|
|
20499
|
+
if (this.chunks.length === 0) {
|
|
20500
|
+
this.logger.warn('MediaRecorder recording produced no data');
|
|
20501
|
+
this.recording.resolve(null);
|
|
20502
|
+
return;
|
|
20503
|
+
}
|
|
20504
|
+
const mimeType = this.chunks[0].type || this.recorder.mimeType;
|
|
20505
|
+
this.recording.resolve({
|
|
20506
|
+
blob: new Blob(this.chunks, { type: mimeType }),
|
|
20507
|
+
});
|
|
20508
|
+
};
|
|
20509
|
+
handleError = (event) => {
|
|
20510
|
+
if (this.isSettled())
|
|
20511
|
+
return;
|
|
20512
|
+
this.detach();
|
|
20513
|
+
const error = event.error;
|
|
20514
|
+
this.logger.error('MediaRecorder error', error);
|
|
20515
|
+
this.recording.reject(error ?? new Error('MediaRecorder failed'));
|
|
20516
|
+
};
|
|
20517
|
+
}
|
|
20518
|
+
|
|
20228
20519
|
/**
|
|
20229
20520
|
* Tiny, type-safe event emitter.
|
|
20230
20521
|
*
|
|
@@ -20598,6 +20889,7 @@ exports.CallTypes = CallTypes;
|
|
|
20598
20889
|
exports.CameraManager = CameraManager;
|
|
20599
20890
|
exports.CameraManagerState = CameraManagerState;
|
|
20600
20891
|
exports.CreateDeviceRequestPushProviderEnum = CreateDeviceRequestPushProviderEnum;
|
|
20892
|
+
exports.DEFAULT_LOOPBACK_RECORDING_DURATION_MS = DEFAULT_LOOPBACK_RECORDING_DURATION_MS;
|
|
20601
20893
|
exports.DeviceManager = DeviceManager;
|
|
20602
20894
|
exports.DeviceManagerState = DeviceManagerState;
|
|
20603
20895
|
exports.DynascaleManager = DynascaleManager;
|
|
@@ -20615,7 +20907,12 @@ exports.IndividualRecordingSettingsResponseModeEnum = IndividualRecordingSetting
|
|
|
20615
20907
|
exports.IngressAudioEncodingOptionsRequestChannelsEnum = IngressAudioEncodingOptionsRequestChannelsEnum;
|
|
20616
20908
|
exports.IngressSourceRequestFpsEnum = IngressSourceRequestFpsEnum;
|
|
20617
20909
|
exports.IngressVideoLayerRequestCodecEnum = IngressVideoLayerRequestCodecEnum;
|
|
20910
|
+
exports.LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS;
|
|
20618
20911
|
exports.LayoutSettingsRequestNameEnum = LayoutSettingsRequestNameEnum;
|
|
20912
|
+
exports.LoopbackStreamsTimeoutError = LoopbackStreamsTimeoutError;
|
|
20913
|
+
exports.MAX_LOOPBACK_RECORDING_DURATION_MS = MAX_LOOPBACK_RECORDING_DURATION_MS;
|
|
20914
|
+
exports.MIN_LOOPBACK_RECORDING_DURATION_MS = MIN_LOOPBACK_RECORDING_DURATION_MS;
|
|
20915
|
+
exports.MediaStreamRecorder = MediaStreamRecorder;
|
|
20619
20916
|
exports.MicrophoneManager = MicrophoneManager;
|
|
20620
20917
|
exports.MicrophoneManagerState = MicrophoneManagerState;
|
|
20621
20918
|
exports.NoiseCancellationSettingsModeEnum = NoiseCancellationSettingsModeEnum;
|
|
@@ -20656,6 +20953,7 @@ exports.VideoSettingsResponseCameraFacingEnum = VideoSettingsResponseCameraFacin
|
|
|
20656
20953
|
exports.ViewportTracker = ViewportTracker;
|
|
20657
20954
|
exports.WSAuthMessageProductsEnum = WSAuthMessageProductsEnum;
|
|
20658
20955
|
exports.checkIfAudioOutputChangeSupported = checkIfAudioOutputChangeSupported;
|
|
20956
|
+
exports.clampLoopbackRecordingDuration = clampLoopbackRecordingDuration;
|
|
20659
20957
|
exports.combineComparators = combineComparators;
|
|
20660
20958
|
exports.conditional = conditional;
|
|
20661
20959
|
exports.createSoundDetector = createSoundDetector;
|
|
@@ -20664,6 +20962,7 @@ exports.descending = descending;
|
|
|
20664
20962
|
exports.deviceIds$ = deviceIds$;
|
|
20665
20963
|
exports.disposeOfMediaStream = disposeOfMediaStream;
|
|
20666
20964
|
exports.dominantSpeaker = dominantSpeaker;
|
|
20965
|
+
exports.enableLoopbackAudio = enableLoopbackAudio;
|
|
20667
20966
|
exports.getAudioBrowserPermission = getAudioBrowserPermission;
|
|
20668
20967
|
exports.getAudioDevices = getAudioDevices;
|
|
20669
20968
|
exports.getAudioOutputDevices = getAudioOutputDevices;
|
|
@@ -20671,6 +20970,8 @@ exports.getAudioStream = getAudioStream;
|
|
|
20671
20970
|
exports.getCallMediaEngineProvider = getCallMediaEngineProvider;
|
|
20672
20971
|
exports.getClientDetails = getClientDetails;
|
|
20673
20972
|
exports.getDeviceState = getDeviceState;
|
|
20973
|
+
exports.getLoopbackStreams = getLoopbackStreams;
|
|
20974
|
+
exports.getLoopbackTracks = getLoopbackTracks;
|
|
20674
20975
|
exports.getScreenShareStream = getScreenShareStream;
|
|
20675
20976
|
exports.getSdkInfo = getSdkInfo;
|
|
20676
20977
|
exports.getVideoBrowserPermission = getVideoBrowserPermission;
|
|
@@ -20691,6 +20992,7 @@ exports.name = name;
|
|
|
20691
20992
|
exports.noopComparator = noopComparator;
|
|
20692
20993
|
exports.paginatedLayoutSortPreset = paginatedLayoutSortPreset;
|
|
20693
20994
|
exports.pinned = pinned;
|
|
20995
|
+
exports.publishing = publishing;
|
|
20694
20996
|
exports.publishingAudio = publishingAudio;
|
|
20695
20997
|
exports.publishingVideo = publishingVideo;
|
|
20696
20998
|
exports.reactionType = reactionType;
|
|
@@ -20707,5 +21009,7 @@ exports.setWebRTCInfo = setWebRTCInfo;
|
|
|
20707
21009
|
exports.speakerLayoutSortPreset = speakerLayoutSortPreset;
|
|
20708
21010
|
exports.speaking = speaking;
|
|
20709
21011
|
exports.videoLoggerSystem = videoLoggerSystem;
|
|
21012
|
+
exports.waitForLoopbackStreams = waitForLoopbackStreams;
|
|
21013
|
+
exports.withLoopbackAudioEnabled = withLoopbackAudioEnabled;
|
|
20710
21014
|
exports.withParticipantSource = withParticipantSource;
|
|
20711
21015
|
//# sourceMappingURL=index.cjs.js.map
|