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