@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.es.js
CHANGED
|
@@ -7,7 +7,7 @@ import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transpor
|
|
|
7
7
|
import * as scopedLogger from '@stream-io/logger';
|
|
8
8
|
export { LogLevelEnum } from '@stream-io/logger';
|
|
9
9
|
import { parse, write } from 'sdp-transform';
|
|
10
|
-
import { Subject, Observable, combineLatest, BehaviorSubject, ReplaySubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, firstValueFrom, of } from 'rxjs';
|
|
10
|
+
import { Subject, Observable, combineLatest, BehaviorSubject, ReplaySubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, firstValueFrom, of, Subscription } from 'rxjs';
|
|
11
11
|
import { UAParser } from 'ua-parser-js';
|
|
12
12
|
import { WorkerTimer } from '@stream-io/worker-timer';
|
|
13
13
|
import https from 'https';
|
|
@@ -4412,6 +4412,28 @@ function isErrorResponse(res) {
|
|
|
4412
4412
|
function isCloseEvent(res) {
|
|
4413
4413
|
return res.code !== undefined;
|
|
4414
4414
|
}
|
|
4415
|
+
/**
|
|
4416
|
+
* Invokes a single event listener in isolation, so that a faulty listener
|
|
4417
|
+
* cannot abort the dispatch loop nor the bookkeeping that follows it.
|
|
4418
|
+
*
|
|
4419
|
+
* @param listener the listener to invoke.
|
|
4420
|
+
* @param event the event to pass to the listener.
|
|
4421
|
+
* @param logger the logger to report listener failures with.
|
|
4422
|
+
*/
|
|
4423
|
+
const invokeEventListener = (listener, event, logger) => {
|
|
4424
|
+
try {
|
|
4425
|
+
const result = listener(event);
|
|
4426
|
+
if (result && typeof result.then === 'function') {
|
|
4427
|
+
Promise.resolve(result).catch((error) => report(logger, error, event));
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
catch (error) {
|
|
4431
|
+
report(logger, error, event);
|
|
4432
|
+
}
|
|
4433
|
+
};
|
|
4434
|
+
const report = (logger, error, event) => {
|
|
4435
|
+
logger.error('Unhandled error in event listener', error, event);
|
|
4436
|
+
};
|
|
4415
4437
|
|
|
4416
4438
|
/**
|
|
4417
4439
|
* Checks whether we are using React Native
|
|
@@ -5060,7 +5082,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
5060
5082
|
}
|
|
5061
5083
|
};
|
|
5062
5084
|
|
|
5063
|
-
const version = "
|
|
5085
|
+
const version = "2.0.0-beta.0";
|
|
5064
5086
|
const [major, minor, patch] = version.split('.');
|
|
5065
5087
|
let sdkInfo = {
|
|
5066
5088
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5317,6 +5339,27 @@ const withoutConcurrency = createRunner(wrapWithContinuationTracking);
|
|
|
5317
5339
|
*/
|
|
5318
5340
|
const withCancellation = createRunner(wrapWithCancellation);
|
|
5319
5341
|
const pendingPromises = new Map();
|
|
5342
|
+
/**
|
|
5343
|
+
* Wraps an async function so concurrent calls share the in-flight promise
|
|
5344
|
+
* instead of queueing another execution.
|
|
5345
|
+
*
|
|
5346
|
+
* @param cb Async function to wrap.
|
|
5347
|
+
* @returns Wrapped async function.
|
|
5348
|
+
*/
|
|
5349
|
+
function singleFlight(cb) {
|
|
5350
|
+
let pending;
|
|
5351
|
+
return (...args) => {
|
|
5352
|
+
if (pending)
|
|
5353
|
+
return pending;
|
|
5354
|
+
const promise = cb(...args).finally(() => {
|
|
5355
|
+
if (pending === promise) {
|
|
5356
|
+
pending = undefined;
|
|
5357
|
+
}
|
|
5358
|
+
});
|
|
5359
|
+
pending = promise;
|
|
5360
|
+
return promise;
|
|
5361
|
+
};
|
|
5362
|
+
}
|
|
5320
5363
|
function hasPending(tag) {
|
|
5321
5364
|
return pendingPromises.has(tag);
|
|
5322
5365
|
}
|
|
@@ -5888,6 +5931,27 @@ const publishingAudio = (a, b) => {
|
|
|
5888
5931
|
return 1;
|
|
5889
5932
|
return 0;
|
|
5890
5933
|
};
|
|
5934
|
+
/**
|
|
5935
|
+
* A comparator which prioritizes participants who are publishing any track
|
|
5936
|
+
* over participants who are not publishing at all.
|
|
5937
|
+
*
|
|
5938
|
+
* Unlike `publishingVideo` and `publishingAudio`, this comparator does not
|
|
5939
|
+
* distinguish between track types. Muting the microphone or turning off the
|
|
5940
|
+
* camera does not change a participant's rank as long as they still publish
|
|
5941
|
+
* something, which keeps the ordering stable across ordinary device toggles.
|
|
5942
|
+
*
|
|
5943
|
+
* @param a the first participant.
|
|
5944
|
+
* @param b the second participant.
|
|
5945
|
+
*/
|
|
5946
|
+
const publishing = (a, b) => {
|
|
5947
|
+
const hasA = a.publishedTracks.length > 0;
|
|
5948
|
+
const hasB = b.publishedTracks.length > 0;
|
|
5949
|
+
if (hasA && !hasB)
|
|
5950
|
+
return -1;
|
|
5951
|
+
if (!hasA && hasB)
|
|
5952
|
+
return 1;
|
|
5953
|
+
return 0;
|
|
5954
|
+
};
|
|
5891
5955
|
/**
|
|
5892
5956
|
* A comparator which prioritizes participants who are pinned.
|
|
5893
5957
|
*
|
|
@@ -5998,11 +6062,11 @@ const withVideoIngressSource = withParticipantSource(ParticipantSource.RTMP, Par
|
|
|
5998
6062
|
/**
|
|
5999
6063
|
* The default sorting preset.
|
|
6000
6064
|
*/
|
|
6001
|
-
const defaultSortPreset = combineComparators(screenSharing, pinned, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
|
|
6065
|
+
const defaultSortPreset = combineComparators(screenSharing, pinned, publishing, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
|
|
6002
6066
|
/**
|
|
6003
6067
|
* The sorting preset for speaker layout.
|
|
6004
6068
|
*/
|
|
6005
|
-
const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
|
|
6069
|
+
const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, publishing, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
|
|
6006
6070
|
/**
|
|
6007
6071
|
* The sorting preset for layouts that don't render all participants but
|
|
6008
6072
|
* instead, render them in pages.
|
|
@@ -6011,7 +6075,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
|
|
|
6011
6075
|
/**
|
|
6012
6076
|
* The sorting preset for livestreams and audio rooms.
|
|
6013
6077
|
*/
|
|
6014
|
-
const livestreamOrAudioRoomSortPreset = combineComparators(ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
|
|
6078
|
+
const livestreamOrAudioRoomSortPreset = combineComparators(publishing, ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
|
|
6015
6079
|
|
|
6016
6080
|
/**
|
|
6017
6081
|
* Returns the default egress object - when no egress data is available.
|
|
@@ -9805,6 +9869,11 @@ class Subscriber extends BasePeerConnection {
|
|
|
9805
9869
|
throw e;
|
|
9806
9870
|
}
|
|
9807
9871
|
};
|
|
9872
|
+
/**
|
|
9873
|
+
* Whether the given stream is one the SFU echoed back to us
|
|
9874
|
+
* (`allowOwnTracksLoopback`) rather than a local capture stream.
|
|
9875
|
+
*/
|
|
9876
|
+
isSelfSubscribedStream = (stream) => !!stream && !!this.trackedStreams?.has(stream);
|
|
9808
9877
|
handleOnTrack = (e) => {
|
|
9809
9878
|
const { streams, track } = e;
|
|
9810
9879
|
const [primaryStream] = streams;
|
|
@@ -9894,6 +9963,8 @@ class Subscriber extends BasePeerConnection {
|
|
|
9894
9963
|
const target = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9895
9964
|
if (!target)
|
|
9896
9965
|
return;
|
|
9966
|
+
if (target.isLocalParticipant)
|
|
9967
|
+
return;
|
|
9897
9968
|
this.state.updateParticipant(target.sessionId, (p) => {
|
|
9898
9969
|
const current = p.interruptedTracks ?? [];
|
|
9899
9970
|
const has = current.includes(trackType);
|
|
@@ -10636,7 +10707,6 @@ const watchCallRejected = (call) => {
|
|
|
10636
10707
|
*/
|
|
10637
10708
|
const watchCallEnded = (call) => {
|
|
10638
10709
|
return function onCallEnded() {
|
|
10639
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
10640
10710
|
const { callingState } = call.state;
|
|
10641
10711
|
if (callingState !== CallingState.IDLE &&
|
|
10642
10712
|
callingState !== CallingState.LEFT) {
|
|
@@ -10645,9 +10715,13 @@ const watchCallEnded = (call) => {
|
|
|
10645
10715
|
reason: 'call.ended event received',
|
|
10646
10716
|
});
|
|
10647
10717
|
call
|
|
10648
|
-
.leave({
|
|
10718
|
+
.leave({
|
|
10719
|
+
message: 'call.ended event received',
|
|
10720
|
+
reject: false,
|
|
10721
|
+
reason: 'ended',
|
|
10722
|
+
})
|
|
10649
10723
|
.catch((err) => {
|
|
10650
|
-
call.logger.error('Failed to leave call after call.ended
|
|
10724
|
+
call.logger.error('Failed to leave call after call.ended', err);
|
|
10651
10725
|
});
|
|
10652
10726
|
}
|
|
10653
10727
|
};
|
|
@@ -13453,7 +13527,7 @@ class DeviceManager {
|
|
|
13453
13527
|
createSyntheticDevice(selectedDevice, deviceKind);
|
|
13454
13528
|
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
13455
13529
|
}
|
|
13456
|
-
async applyPersistedPreferences(enabledInCallType) {
|
|
13530
|
+
async applyPersistedPreferences(enabledInCallType, forceDisabled = false) {
|
|
13457
13531
|
const deviceKey = this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
13458
13532
|
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
13459
13533
|
const preferenceList = toPreferenceList(preferences[deviceKey]);
|
|
@@ -13480,13 +13554,13 @@ class DeviceManager {
|
|
|
13480
13554
|
}
|
|
13481
13555
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
13482
13556
|
if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
|
|
13483
|
-
await this.applyMutedState(muted);
|
|
13557
|
+
await this.applyMutedState(muted, forceDisabled);
|
|
13484
13558
|
appliedMute = true;
|
|
13485
13559
|
}
|
|
13486
13560
|
return appliedDevice || appliedMute;
|
|
13487
13561
|
}
|
|
13488
|
-
async applyMutedState(muted) {
|
|
13489
|
-
if (this.state.status !== undefined)
|
|
13562
|
+
async applyMutedState(muted, forceDisabled) {
|
|
13563
|
+
if (forceDisabled || this.state.status !== undefined)
|
|
13490
13564
|
return;
|
|
13491
13565
|
if (muted) {
|
|
13492
13566
|
await this.disable();
|
|
@@ -13838,8 +13912,9 @@ class CameraManager extends DeviceManager {
|
|
|
13838
13912
|
*
|
|
13839
13913
|
* @param settings the video settings to apply.
|
|
13840
13914
|
* @param publish whether to publish the stream after applying the settings.
|
|
13915
|
+
* @param forceDisabled whether to keep the camera disabled, regardless of server-side or local preferences.
|
|
13841
13916
|
*/
|
|
13842
|
-
async apply(settings, publish) {
|
|
13917
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
13843
13918
|
// Wait for any in progress camera operation
|
|
13844
13919
|
await this.statusChangeSettled();
|
|
13845
13920
|
await this.selectTargetResolution(settings.target_resolution);
|
|
@@ -13851,8 +13926,7 @@ class CameraManager extends DeviceManager {
|
|
|
13851
13926
|
if (shouldApplyDefaults &&
|
|
13852
13927
|
this.devicePersistence.enabled &&
|
|
13853
13928
|
permissionState === 'granted') {
|
|
13854
|
-
persistedPreferencesApplied =
|
|
13855
|
-
await this.applyPersistedPreferences(enabledInCallType);
|
|
13929
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(enabledInCallType, forceDisabled);
|
|
13856
13930
|
}
|
|
13857
13931
|
// apply a direction and enable the camera only if in "pristine" state,
|
|
13858
13932
|
// and there are no persisted preferences
|
|
@@ -13862,7 +13936,10 @@ class CameraManager extends DeviceManager {
|
|
|
13862
13936
|
const direction = settings.camera_facing === 'front' ? 'front' : 'back';
|
|
13863
13937
|
await this.selectDirection(direction, { enableCamera: false });
|
|
13864
13938
|
}
|
|
13865
|
-
if (
|
|
13939
|
+
if (!forceDisabled &&
|
|
13940
|
+
canPublish &&
|
|
13941
|
+
settings.camera_default_on &&
|
|
13942
|
+
enabledInCallType) {
|
|
13866
13943
|
await this.enable();
|
|
13867
13944
|
}
|
|
13868
13945
|
}
|
|
@@ -14457,8 +14534,9 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14457
14534
|
* Applies the audio settings to the microphone.
|
|
14458
14535
|
* @param settings the audio settings to apply.
|
|
14459
14536
|
* @param publish whether to publish the stream after applying the settings.
|
|
14537
|
+
* @param forceDisabled whether to keep the mic disabled, regardless of server-side or local preferences.
|
|
14460
14538
|
*/
|
|
14461
|
-
async apply(settings, publish) {
|
|
14539
|
+
async apply(settings, publish, forceDisabled = false) {
|
|
14462
14540
|
// Wait for any in progress mic operation
|
|
14463
14541
|
await this.statusChangeSettled();
|
|
14464
14542
|
// apply server-side settings only when the device state is pristine
|
|
@@ -14470,11 +14548,11 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
14470
14548
|
if (shouldApplyDefaults &&
|
|
14471
14549
|
this.devicePersistence.enabled &&
|
|
14472
14550
|
permissionState === 'granted') {
|
|
14473
|
-
persistedPreferencesApplied = await this.applyPersistedPreferences(true);
|
|
14551
|
+
persistedPreferencesApplied = await this.applyPersistedPreferences(true, forceDisabled);
|
|
14474
14552
|
}
|
|
14475
14553
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
14476
14554
|
if (shouldApplyDefaults && !persistedPreferencesApplied) {
|
|
14477
|
-
if (canPublish && settings.mic_default_on) {
|
|
14555
|
+
if (!forceDisabled && canPublish && settings.mic_default_on) {
|
|
14478
14556
|
await this.enable();
|
|
14479
14557
|
}
|
|
14480
14558
|
}
|
|
@@ -15443,7 +15521,7 @@ class Call {
|
|
|
15443
15521
|
}
|
|
15444
15522
|
}
|
|
15445
15523
|
}
|
|
15446
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this);
|
|
15524
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, reason === 'ended' ? 'remote' : undefined);
|
|
15447
15525
|
this.statsReporter?.stop();
|
|
15448
15526
|
this.statsReporter = undefined;
|
|
15449
15527
|
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
@@ -15606,8 +15684,18 @@ class Call {
|
|
|
15606
15684
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
15607
15685
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
15608
15686
|
this.clientStore.setCalls([this, ...calls]);
|
|
15609
|
-
|
|
15610
|
-
|
|
15687
|
+
await this.applyDeviceConfig(settings, { publish: false });
|
|
15688
|
+
};
|
|
15689
|
+
/**
|
|
15690
|
+
* Applies a backend-provided call state snapshot
|
|
15691
|
+
* (call, members, own capabilities) to this instance.
|
|
15692
|
+
*
|
|
15693
|
+
* @internal an internal method and should not be used outside the SDK.
|
|
15694
|
+
*/
|
|
15695
|
+
updateFromCallStateResponse = (response) => {
|
|
15696
|
+
this.state.updateFromCallResponse(response.call);
|
|
15697
|
+
this.state.setMembers(response.members);
|
|
15698
|
+
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15611
15699
|
};
|
|
15612
15700
|
/**
|
|
15613
15701
|
* Loads the information about the call.
|
|
@@ -15620,9 +15708,7 @@ class Call {
|
|
|
15620
15708
|
get = async (params) => {
|
|
15621
15709
|
await this.setup();
|
|
15622
15710
|
const response = await this.streamClient.get(this.streamClientBasePath, params);
|
|
15623
|
-
this.
|
|
15624
|
-
this.state.setMembers(response.members);
|
|
15625
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15711
|
+
this.updateFromCallStateResponse(response);
|
|
15626
15712
|
if (params?.ring) {
|
|
15627
15713
|
this.ringingSubject.next(true);
|
|
15628
15714
|
}
|
|
@@ -15630,9 +15716,7 @@ class Call {
|
|
|
15630
15716
|
this.watching = true;
|
|
15631
15717
|
this.clientStore.registerOrUpdateCall(this);
|
|
15632
15718
|
}
|
|
15633
|
-
|
|
15634
|
-
const skipSpeakerApply = isReactNative();
|
|
15635
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15719
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15636
15720
|
return response;
|
|
15637
15721
|
};
|
|
15638
15722
|
/**
|
|
@@ -15643,9 +15727,7 @@ class Call {
|
|
|
15643
15727
|
getOrCreate = async (data) => {
|
|
15644
15728
|
await this.setup();
|
|
15645
15729
|
const response = await this.streamClient.post(this.streamClientBasePath, data);
|
|
15646
|
-
this.
|
|
15647
|
-
this.state.setMembers(response.members);
|
|
15648
|
-
this.state.setOwnCapabilities(response.own_capabilities);
|
|
15730
|
+
this.updateFromCallStateResponse(response);
|
|
15649
15731
|
if (data?.ring) {
|
|
15650
15732
|
this.ringingSubject.next(true);
|
|
15651
15733
|
}
|
|
@@ -15653,9 +15735,7 @@ class Call {
|
|
|
15653
15735
|
this.watching = true;
|
|
15654
15736
|
this.clientStore.registerOrUpdateCall(this);
|
|
15655
15737
|
}
|
|
15656
|
-
|
|
15657
|
-
const skipSpeakerApply = isReactNative();
|
|
15658
|
-
await this.applyDeviceConfig(response.call.settings, false, skipSpeakerApply);
|
|
15738
|
+
await this.applyDeviceConfig(response.call.settings, { publish: false });
|
|
15659
15739
|
return response;
|
|
15660
15740
|
};
|
|
15661
15741
|
/**
|
|
@@ -15719,7 +15799,7 @@ class Call {
|
|
|
15719
15799
|
*
|
|
15720
15800
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
15721
15801
|
*/
|
|
15722
|
-
join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15802
|
+
join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
|
|
15723
15803
|
const callingState = this.state.callingState;
|
|
15724
15804
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
15725
15805
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -15741,7 +15821,6 @@ class Call {
|
|
|
15741
15821
|
callId: this.id,
|
|
15742
15822
|
getCallSessionId: () => this.state.session?.id ?? '',
|
|
15743
15823
|
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
15744
|
-
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
15745
15824
|
});
|
|
15746
15825
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
15747
15826
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
@@ -15793,7 +15872,7 @@ class Call {
|
|
|
15793
15872
|
callingX?.endCall(this, 'error');
|
|
15794
15873
|
throw error;
|
|
15795
15874
|
}
|
|
15796
|
-
};
|
|
15875
|
+
});
|
|
15797
15876
|
/**
|
|
15798
15877
|
* Will make a single attempt to watch for call related WebSocket events
|
|
15799
15878
|
* and initiate a call session with the server.
|
|
@@ -15965,7 +16044,10 @@ class Call {
|
|
|
15965
16044
|
if (!this.deviceSettingsAppliedOnce &&
|
|
15966
16045
|
this.state.settings &&
|
|
15967
16046
|
!supersededByLeave()) {
|
|
15968
|
-
await this.applyDeviceConfig(this.state.settings,
|
|
16047
|
+
await this.applyDeviceConfig(this.state.settings, {
|
|
16048
|
+
publish: true,
|
|
16049
|
+
skipSpeakerApply: false,
|
|
16050
|
+
});
|
|
15969
16051
|
this.deviceSettingsAppliedOnce = true;
|
|
15970
16052
|
}
|
|
15971
16053
|
if (!this.callManagerStarted && !supersededByLeave()) {
|
|
@@ -16755,12 +16837,16 @@ class Call {
|
|
|
16755
16837
|
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
16756
16838
|
if (!streamStateProp)
|
|
16757
16839
|
continue;
|
|
16758
|
-
this.state.updateParticipant(sessionId, (p) =>
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16840
|
+
this.state.updateParticipant(sessionId, (p) => {
|
|
16841
|
+
const currentStream = p[streamStateProp];
|
|
16842
|
+
const isEchoedStream = this.subscriber?.isSelfSubscribedStream(currentStream);
|
|
16843
|
+
return {
|
|
16844
|
+
publishedTracks: mediaStream
|
|
16845
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
16846
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
16847
|
+
[streamStateProp]: isEchoedStream ? currentStream : mediaStream,
|
|
16848
|
+
};
|
|
16849
|
+
});
|
|
16764
16850
|
}
|
|
16765
16851
|
};
|
|
16766
16852
|
/**
|
|
@@ -17450,18 +17536,21 @@ class Call {
|
|
|
17450
17536
|
*
|
|
17451
17537
|
* @internal
|
|
17452
17538
|
*/
|
|
17453
|
-
applyDeviceConfig = async (settings,
|
|
17539
|
+
applyDeviceConfig = async (settings, opts) => {
|
|
17540
|
+
const { publish,
|
|
17541
|
+
// Skip speaker setup on RN until joining (explicitly passed as false during join())
|
|
17542
|
+
skipSpeakerApply = isReactNative(), withDisabledDevices, } = opts;
|
|
17454
17543
|
if (!skipSpeakerApply) {
|
|
17455
|
-
await this.speaker
|
|
17456
|
-
|
|
17457
|
-
|
|
17544
|
+
await this.speaker
|
|
17545
|
+
.apply(settings)
|
|
17546
|
+
.catch((err) => this.logger.warn('Speaker init failed', err));
|
|
17458
17547
|
}
|
|
17459
|
-
await this.camera
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
await this.microphone
|
|
17463
|
-
|
|
17464
|
-
|
|
17548
|
+
await this.camera
|
|
17549
|
+
.apply(settings.video, publish, withDisabledDevices)
|
|
17550
|
+
.catch((err) => this.logger.warn('Camera init failed', err));
|
|
17551
|
+
await this.microphone
|
|
17552
|
+
.apply(settings.audio, publish, withDisabledDevices)
|
|
17553
|
+
.catch((err) => this.logger.warn('Mic init failed', err));
|
|
17465
17554
|
};
|
|
17466
17555
|
/**
|
|
17467
17556
|
* Will begin tracking the given element for visibility changes within the
|
|
@@ -18945,11 +19034,11 @@ class StreamClient {
|
|
|
18945
19034
|
return;
|
|
18946
19035
|
// call generic listeners
|
|
18947
19036
|
for (const listener of this.listeners.all || []) {
|
|
18948
|
-
listener
|
|
19037
|
+
invokeEventListener(listener, event, this.logger);
|
|
18949
19038
|
}
|
|
18950
19039
|
// call type specific listeners
|
|
18951
19040
|
for (const listener of this.listeners[event.type] || []) {
|
|
18952
|
-
listener
|
|
19041
|
+
invokeEventListener(listener, event, this.logger);
|
|
18953
19042
|
}
|
|
18954
19043
|
};
|
|
18955
19044
|
/**
|
|
@@ -18969,11 +19058,11 @@ class StreamClient {
|
|
|
18969
19058
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18970
19059
|
};
|
|
18971
19060
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18972
|
-
"
|
|
19061
|
+
"2.0.0-beta.0";
|
|
18973
19062
|
getUserAgent = () => {
|
|
18974
19063
|
if (!this.cachedUserAgent) {
|
|
18975
19064
|
const { clientAppIdentifier = {} } = this.options;
|
|
18976
|
-
const { sdkName = 'js', sdkVersion = "
|
|
19065
|
+
const { sdkName = 'js', sdkVersion = "2.0.0-beta.0", ...extras } = clientAppIdentifier;
|
|
18977
19066
|
this.cachedUserAgent = [
|
|
18978
19067
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18979
19068
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -19336,7 +19425,6 @@ class ClientEventReporter {
|
|
|
19336
19425
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
19337
19426
|
type: ctx?.callType,
|
|
19338
19427
|
id: ctx?.callId,
|
|
19339
|
-
call_cid: cid,
|
|
19340
19428
|
stage: 'JoinInitiated',
|
|
19341
19429
|
join_attempt_id: joinAttemptId,
|
|
19342
19430
|
...(coordinatorConnectId && {
|
|
@@ -19518,7 +19606,6 @@ class ClientEventReporter {
|
|
|
19518
19606
|
startedAt: Date.now(),
|
|
19519
19607
|
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
19520
19608
|
sfuId: this.getSfuId(cid),
|
|
19521
|
-
userSessionId: this.getUserSessionId(cid),
|
|
19522
19609
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
19523
19610
|
lastIceState: iceConnectionState,
|
|
19524
19611
|
};
|
|
@@ -19529,9 +19616,6 @@ class ClientEventReporter {
|
|
|
19529
19616
|
peer_connection: role,
|
|
19530
19617
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19531
19618
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19532
|
-
...(pair.userSessionId && {
|
|
19533
|
-
user_session_id: pair.userSessionId,
|
|
19534
|
-
}),
|
|
19535
19619
|
event_type: 'initiated',
|
|
19536
19620
|
});
|
|
19537
19621
|
};
|
|
@@ -19546,9 +19630,6 @@ class ClientEventReporter {
|
|
|
19546
19630
|
peer_connection: role,
|
|
19547
19631
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19548
19632
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19549
|
-
...(pair.userSessionId && {
|
|
19550
|
-
user_session_id: pair.userSessionId,
|
|
19551
|
-
}),
|
|
19552
19633
|
event_type: 'completed',
|
|
19553
19634
|
outcome: 'success',
|
|
19554
19635
|
retry_count_attempt: 0,
|
|
@@ -19566,9 +19647,6 @@ class ClientEventReporter {
|
|
|
19566
19647
|
...this.sessionIdField(cid),
|
|
19567
19648
|
peer_connection: role,
|
|
19568
19649
|
was_previously_connected: pair.wasPreviouslyConnected,
|
|
19569
|
-
...(pair.userSessionId && {
|
|
19570
|
-
user_session_id: pair.userSessionId,
|
|
19571
|
-
}),
|
|
19572
19650
|
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
19573
19651
|
event_type: 'completed',
|
|
19574
19652
|
outcome: 'failure',
|
|
@@ -19581,7 +19659,6 @@ class ClientEventReporter {
|
|
|
19581
19659
|
this.peerConnectionPairs.delete(key);
|
|
19582
19660
|
};
|
|
19583
19661
|
getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
19584
|
-
getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
19585
19662
|
sessionIdField = (cid) => {
|
|
19586
19663
|
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
19587
19664
|
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
@@ -19593,7 +19670,6 @@ class ClientEventReporter {
|
|
|
19593
19670
|
user_id: this.streamClient.userID || this.coordinatorConnectUserId,
|
|
19594
19671
|
type: ctx?.callType ?? '',
|
|
19595
19672
|
id: ctx?.callId ?? '',
|
|
19596
|
-
call_cid: cid,
|
|
19597
19673
|
stage,
|
|
19598
19674
|
stage_id: pair.sid,
|
|
19599
19675
|
...(pair.joinAttemptIdSnapshot && {
|
|
@@ -19844,6 +19920,7 @@ class StreamVideoClient {
|
|
|
19844
19920
|
}
|
|
19845
19921
|
else {
|
|
19846
19922
|
await call.updateFromRingingEvent(e);
|
|
19923
|
+
await call.get();
|
|
19847
19924
|
}
|
|
19848
19925
|
}
|
|
19849
19926
|
else {
|
|
@@ -19882,6 +19959,13 @@ class StreamVideoClient {
|
|
|
19882
19959
|
this.logger.error(`Failed to init call from event ${e.type}`, err);
|
|
19883
19960
|
}
|
|
19884
19961
|
};
|
|
19962
|
+
/**
|
|
19963
|
+
* Queries the API for calls matching the given filters.
|
|
19964
|
+
* @param data the query data.
|
|
19965
|
+
*/
|
|
19966
|
+
doQueryCalls = (data) => {
|
|
19967
|
+
return this.streamClient.post('/calls', data);
|
|
19968
|
+
};
|
|
19885
19969
|
/**
|
|
19886
19970
|
* Rewatches the given calls with retry logic.
|
|
19887
19971
|
* @param callsToReWatch array of call IDs to rewatch
|
|
@@ -19892,10 +19976,16 @@ class StreamVideoClient {
|
|
|
19892
19976
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
19893
19977
|
try {
|
|
19894
19978
|
this.logger.info(`Rewatching calls ${callsToReWatch.join(', ')} attempt ${attempt + 1}`);
|
|
19895
|
-
await this.
|
|
19979
|
+
const response = await this.doQueryCalls({
|
|
19896
19980
|
watch: true,
|
|
19897
19981
|
filter_conditions: { cid: { $in: callsToReWatch } },
|
|
19898
19982
|
});
|
|
19983
|
+
for (const c of response.calls) {
|
|
19984
|
+
const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
|
|
19985
|
+
if (call) {
|
|
19986
|
+
call.updateFromCallStateResponse(c);
|
|
19987
|
+
}
|
|
19988
|
+
}
|
|
19899
19989
|
return;
|
|
19900
19990
|
}
|
|
19901
19991
|
catch (err) {
|
|
@@ -20038,9 +20128,11 @@ class StreamVideoClient {
|
|
|
20038
20128
|
* Will query the API for calls matching the given filters.
|
|
20039
20129
|
*
|
|
20040
20130
|
* @param data the query data.
|
|
20131
|
+
* @param opts additional options, for tweaking the API behavior.
|
|
20041
20132
|
*/
|
|
20042
|
-
queryCalls = async (data = {}) => {
|
|
20043
|
-
const
|
|
20133
|
+
queryCalls = async (data = {}, opts = {}) => {
|
|
20134
|
+
const { withDisabledDevices = true } = opts;
|
|
20135
|
+
const response = await this.doQueryCalls(data);
|
|
20044
20136
|
const calls = [];
|
|
20045
20137
|
for (const c of response.calls) {
|
|
20046
20138
|
const call = new Call({
|
|
@@ -20054,7 +20146,10 @@ class StreamVideoClient {
|
|
|
20054
20146
|
clientStore: this.writeableStateStore,
|
|
20055
20147
|
});
|
|
20056
20148
|
call.state.updateFromCallResponse(c.call);
|
|
20057
|
-
await call.applyDeviceConfig(c.call.settings,
|
|
20149
|
+
await call.applyDeviceConfig(c.call.settings, {
|
|
20150
|
+
publish: false,
|
|
20151
|
+
withDisabledDevices,
|
|
20152
|
+
});
|
|
20058
20153
|
if (data.watch) {
|
|
20059
20154
|
await call.setup();
|
|
20060
20155
|
this.writeableStateStore.registerCall(call);
|
|
@@ -20206,6 +20301,202 @@ const humanize = (n) => {
|
|
|
20206
20301
|
return String(n);
|
|
20207
20302
|
};
|
|
20208
20303
|
|
|
20304
|
+
/** Default loopback stream wait timeout. */
|
|
20305
|
+
const LOOPBACK_STREAMS_WAIT_TIMEOUT_MS = 10 * 1000;
|
|
20306
|
+
/** Default loopback recording duration. */
|
|
20307
|
+
const DEFAULT_LOOPBACK_RECORDING_DURATION_MS = 10 * 1000;
|
|
20308
|
+
/** Shortest permitted loopback recording duration. */
|
|
20309
|
+
const MIN_LOOPBACK_RECORDING_DURATION_MS = 5 * 1000;
|
|
20310
|
+
/** Longest permitted loopback recording duration. */
|
|
20311
|
+
const MAX_LOOPBACK_RECORDING_DURATION_MS = 2 * 60 * 1000;
|
|
20312
|
+
/** Thrown when loopback tracks do not arrive in time. */
|
|
20313
|
+
class LoopbackStreamsTimeoutError extends Error {
|
|
20314
|
+
constructor() {
|
|
20315
|
+
super('Timed out waiting for loopback streams. Ensure the call was joined ' +
|
|
20316
|
+
'with `allowOwnTracksLoopback: true` and that the SFU is configured ' +
|
|
20317
|
+
'to echo self-sub tracks.');
|
|
20318
|
+
this.name = 'LoopbackStreamsTimeoutError';
|
|
20319
|
+
}
|
|
20320
|
+
}
|
|
20321
|
+
/**
|
|
20322
|
+
* Returns the streams on the given participant that the SFU echoed back to us,
|
|
20323
|
+
* ignoring the local capture streams the publishing path writes to the same
|
|
20324
|
+
* fields.
|
|
20325
|
+
*/
|
|
20326
|
+
const getLoopbackStreams = (call, participant) => {
|
|
20327
|
+
const { subscriber } = call ?? {};
|
|
20328
|
+
const { audioStream, videoStream } = participant ?? {};
|
|
20329
|
+
return {
|
|
20330
|
+
loopbackVideoStream: subscriber?.isSelfSubscribedStream(videoStream)
|
|
20331
|
+
? videoStream
|
|
20332
|
+
: undefined,
|
|
20333
|
+
loopbackAudioStream: subscriber?.isSelfSubscribedStream(audioStream)
|
|
20334
|
+
? audioStream
|
|
20335
|
+
: undefined,
|
|
20336
|
+
};
|
|
20337
|
+
};
|
|
20338
|
+
/**
|
|
20339
|
+
* Returns loopback tracks when all required tracks are present.
|
|
20340
|
+
*
|
|
20341
|
+
* Prefer {@link waitForLoopbackStreams}, which observes call state until the
|
|
20342
|
+
* tracks arrive. This is the synchronous snapshot it is built from.
|
|
20343
|
+
*
|
|
20344
|
+
* @internal
|
|
20345
|
+
*/
|
|
20346
|
+
const getLoopbackTracks = (call, participant, includeVideo) => {
|
|
20347
|
+
if (!participant)
|
|
20348
|
+
return undefined;
|
|
20349
|
+
const { loopbackAudioStream, loopbackVideoStream } = getLoopbackStreams(call, participant);
|
|
20350
|
+
const audioTrack = loopbackAudioStream?.getAudioTracks()[0];
|
|
20351
|
+
const videoTrack = includeVideo
|
|
20352
|
+
? loopbackVideoStream?.getVideoTracks()[0]
|
|
20353
|
+
: undefined;
|
|
20354
|
+
if (!audioTrack || (includeVideo && !videoTrack)) {
|
|
20355
|
+
return undefined;
|
|
20356
|
+
}
|
|
20357
|
+
return { audioTrack, videoTrack };
|
|
20358
|
+
};
|
|
20359
|
+
/** Waits for the requested loopback tracks. */
|
|
20360
|
+
const waitForLoopbackStreams = (call, opts) => {
|
|
20361
|
+
const callState = call.state;
|
|
20362
|
+
const { includeVideo, signal, timeoutMs = LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, } = opts;
|
|
20363
|
+
return new Promise((resolve, reject) => {
|
|
20364
|
+
const initial = getLoopbackTracks(call, callState.localParticipant, includeVideo);
|
|
20365
|
+
if (initial) {
|
|
20366
|
+
resolve(initial);
|
|
20367
|
+
return;
|
|
20368
|
+
}
|
|
20369
|
+
if (signal.aborted) {
|
|
20370
|
+
resolve(null);
|
|
20371
|
+
return;
|
|
20372
|
+
}
|
|
20373
|
+
let settled = false;
|
|
20374
|
+
const subscriptions = new Subscription();
|
|
20375
|
+
const cleanup = () => {
|
|
20376
|
+
if (settled)
|
|
20377
|
+
return;
|
|
20378
|
+
settled = true;
|
|
20379
|
+
clearTimeout(timeoutId);
|
|
20380
|
+
signal.removeEventListener('abort', onAbort);
|
|
20381
|
+
subscriptions.unsubscribe();
|
|
20382
|
+
};
|
|
20383
|
+
const onAbort = () => {
|
|
20384
|
+
cleanup();
|
|
20385
|
+
resolve(null);
|
|
20386
|
+
};
|
|
20387
|
+
const timeoutId = setTimeout(() => {
|
|
20388
|
+
cleanup();
|
|
20389
|
+
reject(new LoopbackStreamsTimeoutError());
|
|
20390
|
+
}, timeoutMs);
|
|
20391
|
+
signal.addEventListener('abort', onAbort);
|
|
20392
|
+
subscriptions.add(callState.localParticipant$.subscribe((participant) => {
|
|
20393
|
+
const ready = getLoopbackTracks(call, participant, includeVideo);
|
|
20394
|
+
if (ready) {
|
|
20395
|
+
cleanup();
|
|
20396
|
+
resolve(ready);
|
|
20397
|
+
}
|
|
20398
|
+
}));
|
|
20399
|
+
});
|
|
20400
|
+
};
|
|
20401
|
+
/** Enables loopback audio while `fn` runs, then restores it. */
|
|
20402
|
+
const withLoopbackAudioEnabled = async (tracks, fn) => {
|
|
20403
|
+
const restore = enableLoopbackAudio(tracks);
|
|
20404
|
+
try {
|
|
20405
|
+
return await fn();
|
|
20406
|
+
}
|
|
20407
|
+
finally {
|
|
20408
|
+
restore();
|
|
20409
|
+
}
|
|
20410
|
+
};
|
|
20411
|
+
/** Enables loopback audio and returns a restore function. */
|
|
20412
|
+
const enableLoopbackAudio = (tracks) => {
|
|
20413
|
+
const { audioTrack } = tracks;
|
|
20414
|
+
const previous = audioTrack?.enabled ?? false;
|
|
20415
|
+
if (audioTrack) {
|
|
20416
|
+
audioTrack.enabled = true;
|
|
20417
|
+
}
|
|
20418
|
+
return () => {
|
|
20419
|
+
if (audioTrack) {
|
|
20420
|
+
audioTrack.enabled = previous;
|
|
20421
|
+
}
|
|
20422
|
+
};
|
|
20423
|
+
};
|
|
20424
|
+
/** Clamps and rounds a loopback recording duration. */
|
|
20425
|
+
const clampLoopbackRecordingDuration = (durationMs) => Math.round(Math.min(MAX_LOOPBACK_RECORDING_DURATION_MS, Math.max(MIN_LOOPBACK_RECORDING_DURATION_MS, durationMs)));
|
|
20426
|
+
|
|
20427
|
+
/** Single-use `MediaRecorder` wrapper that returns an in-memory blob. */
|
|
20428
|
+
class MediaStreamRecorder {
|
|
20429
|
+
recording = promiseWithResolvers();
|
|
20430
|
+
logger = videoLoggerSystem.getLogger('MediaStreamRecorder');
|
|
20431
|
+
chunks = [];
|
|
20432
|
+
timeoutId;
|
|
20433
|
+
recorder;
|
|
20434
|
+
tracks;
|
|
20435
|
+
maxDurationMs;
|
|
20436
|
+
constructor(stream, options = {}) {
|
|
20437
|
+
if (typeof MediaRecorder === 'undefined') {
|
|
20438
|
+
throw new Error('MediaRecorder is not available in this environment');
|
|
20439
|
+
}
|
|
20440
|
+
this.tracks = stream.getTracks();
|
|
20441
|
+
if (this.tracks.length === 0) {
|
|
20442
|
+
throw new Error('cannot record a MediaStream that has no tracks');
|
|
20443
|
+
}
|
|
20444
|
+
this.maxDurationMs = options.maxDurationMs;
|
|
20445
|
+
this.recorder = new MediaRecorder(new MediaStream(this.tracks));
|
|
20446
|
+
this.recorder.ondataavailable = (event) => {
|
|
20447
|
+
if (event.data?.size)
|
|
20448
|
+
this.chunks.push(event.data);
|
|
20449
|
+
};
|
|
20450
|
+
this.recorder.onstop = this.handleStop;
|
|
20451
|
+
this.recorder.onerror = this.handleError;
|
|
20452
|
+
}
|
|
20453
|
+
/** Starts recording and resolves when recording stops. */
|
|
20454
|
+
start = () => {
|
|
20455
|
+
if (this.isSettled()) {
|
|
20456
|
+
throw new Error('MediaStreamRecorder is single-use: this instance has already ' +
|
|
20457
|
+
'recorded. Construct a new one.');
|
|
20458
|
+
}
|
|
20459
|
+
if (this.recorder.state === 'inactive') {
|
|
20460
|
+
this.tracks.forEach((track) => track.addEventListener('ended', this.stop));
|
|
20461
|
+
if (this.maxDurationMs !== undefined) {
|
|
20462
|
+
this.timeoutId = setTimeout(this.stop, this.maxDurationMs);
|
|
20463
|
+
}
|
|
20464
|
+
this.recorder.start();
|
|
20465
|
+
}
|
|
20466
|
+
return this.recording.promise;
|
|
20467
|
+
};
|
|
20468
|
+
/** Stops recording early. */
|
|
20469
|
+
stop = () => {
|
|
20470
|
+
if (this.recorder.state !== 'inactive')
|
|
20471
|
+
this.recorder.stop();
|
|
20472
|
+
};
|
|
20473
|
+
isSettled = () => this.recording.isResolved() || this.recording.isRejected();
|
|
20474
|
+
detach = () => {
|
|
20475
|
+
clearTimeout(this.timeoutId);
|
|
20476
|
+
this.tracks.forEach((track) => track.removeEventListener('ended', this.stop));
|
|
20477
|
+
};
|
|
20478
|
+
handleStop = () => {
|
|
20479
|
+
this.detach();
|
|
20480
|
+
if (this.chunks.length === 0) {
|
|
20481
|
+
this.logger.warn('MediaRecorder recording produced no data');
|
|
20482
|
+
this.recording.resolve(null);
|
|
20483
|
+
return;
|
|
20484
|
+
}
|
|
20485
|
+
const mimeType = this.chunks[0].type || this.recorder.mimeType;
|
|
20486
|
+
this.recording.resolve({
|
|
20487
|
+
blob: new Blob(this.chunks, { type: mimeType }),
|
|
20488
|
+
});
|
|
20489
|
+
};
|
|
20490
|
+
handleError = (event) => {
|
|
20491
|
+
if (this.isSettled())
|
|
20492
|
+
return;
|
|
20493
|
+
this.detach();
|
|
20494
|
+
const error = event.error;
|
|
20495
|
+
this.logger.error('MediaRecorder error', error);
|
|
20496
|
+
this.recording.reject(error ?? new Error('MediaRecorder failed'));
|
|
20497
|
+
};
|
|
20498
|
+
}
|
|
20499
|
+
|
|
20209
20500
|
/**
|
|
20210
20501
|
* Tiny, type-safe event emitter.
|
|
20211
20502
|
*
|
|
@@ -20557,5 +20848,5 @@ var e2eeWorker$1 = /*#__PURE__*/Object.freeze({
|
|
|
20557
20848
|
e2eeWorker: e2eeWorker
|
|
20558
20849
|
});
|
|
20559
20850
|
|
|
20560
|
-
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
|
|
20851
|
+
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DEFAULT_LOOPBACK_RECORDING_DURATION_MS, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, LayoutSettingsRequestNameEnum, LoopbackStreamsTimeoutError, MAX_LOOPBACK_RECORDING_DURATION_MS, MIN_LOOPBACK_RECORDING_DURATION_MS, MediaStreamRecorder, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, clampLoopbackRecordingDuration, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, enableLoopbackAudio, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getLoopbackStreams, getLoopbackTracks, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishing, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, waitForLoopbackStreams, withLoopbackAudioEnabled, withParticipantSource };
|
|
20561
20852
|
//# sourceMappingURL=index.es.js.map
|