@stream-io/video-client 1.13.1 → 1.15.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 +14 -0
- package/dist/index.browser.es.js +1704 -1762
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1706 -1780
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1704 -1762
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +61 -30
- package/dist/src/StreamSfuClient.d.ts +4 -5
- package/dist/src/devices/CameraManager.d.ts +5 -8
- package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
- package/dist/src/devices/MicrophoneManager.d.ts +7 -2
- package/dist/src/devices/ScreenShareManager.d.ts +1 -2
- package/dist/src/gen/coordinator/index.d.ts +904 -515
- package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
- package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
- package/dist/src/helpers/array.d.ts +7 -0
- package/dist/src/permissions/PermissionsContext.d.ts +6 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
- package/dist/src/rtc/Dispatcher.d.ts +0 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
- package/dist/src/rtc/Publisher.d.ts +32 -86
- package/dist/src/rtc/Subscriber.d.ts +4 -56
- package/dist/src/rtc/TransceiverCache.d.ts +55 -0
- package/dist/src/rtc/codecs.d.ts +1 -15
- package/dist/src/rtc/helpers/sdp.d.ts +8 -0
- package/dist/src/rtc/helpers/tracks.d.ts +1 -0
- package/dist/src/rtc/index.d.ts +3 -0
- package/dist/src/rtc/videoLayers.d.ts +11 -25
- package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
- package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
- package/dist/src/stats/index.d.ts +1 -1
- package/dist/src/stats/types.d.ts +8 -0
- package/dist/src/store/CallState.d.ts +47 -5
- package/dist/src/store/rxUtils.d.ts +15 -1
- package/dist/src/types.d.ts +26 -22
- package/package.json +1 -1
- package/src/Call.ts +310 -271
- package/src/StreamSfuClient.ts +9 -14
- package/src/StreamVideoClient.ts +1 -1
- package/src/__tests__/Call.publishing.test.ts +306 -0
- package/src/devices/CameraManager.ts +33 -16
- package/src/devices/InputMediaDeviceManager.ts +36 -27
- package/src/devices/MicrophoneManager.ts +29 -8
- package/src/devices/ScreenShareManager.ts +6 -8
- package/src/devices/__tests__/CameraManager.test.ts +111 -14
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
- package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
- package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
- package/src/devices/__tests__/mocks.ts +1 -0
- package/src/events/__tests__/internal.test.ts +132 -0
- package/src/events/__tests__/mutes.test.ts +0 -3
- package/src/events/__tests__/speaker.test.ts +92 -0
- package/src/events/participant.ts +3 -4
- package/src/gen/coordinator/index.ts +902 -514
- package/src/gen/video/sfu/event/events.ts +91 -30
- package/src/gen/video/sfu/models/models.ts +105 -13
- package/src/helpers/array.ts +14 -0
- package/src/permissions/PermissionsContext.ts +22 -0
- package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
- package/src/rpc/__tests__/createClient.test.ts +38 -0
- package/src/rpc/createClient.ts +11 -5
- package/src/rtc/BasePeerConnection.ts +240 -0
- package/src/rtc/Dispatcher.ts +0 -9
- package/src/rtc/IceTrickleBuffer.ts +24 -4
- package/src/rtc/Publisher.ts +210 -528
- package/src/rtc/Subscriber.ts +26 -200
- package/src/rtc/TransceiverCache.ts +120 -0
- package/src/rtc/__tests__/Publisher.test.ts +407 -210
- package/src/rtc/__tests__/Subscriber.test.ts +88 -36
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
- package/src/rtc/__tests__/videoLayers.test.ts +161 -54
- package/src/rtc/codecs.ts +1 -131
- package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
- package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
- package/src/rtc/helpers/sdp.ts +30 -0
- package/src/rtc/helpers/tracks.ts +3 -0
- package/src/rtc/index.ts +4 -0
- package/src/rtc/videoLayers.ts +68 -76
- package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
- package/src/stats/SfuStatsReporter.ts +31 -3
- package/src/stats/index.ts +1 -1
- package/src/stats/types.ts +12 -0
- package/src/store/CallState.ts +115 -5
- package/src/store/__tests__/CallState.test.ts +101 -0
- package/src/store/rxUtils.ts +23 -1
- package/src/types.ts +27 -22
- package/dist/src/helpers/sdp-munging.d.ts +0 -24
- package/dist/src/rtc/bitrateLookup.d.ts +0 -2
- package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
- package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
- package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
- package/src/helpers/sdp-munging.ts +0 -265
- package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
- package/src/rtc/__tests__/codecs.test.ts +0 -145
- package/src/rtc/bitrateLookup.ts +0 -61
- package/src/rtc/helpers/iceCandidate.ts +0 -16
- /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
- /package/src/{compatibility.ts → helpers/compatibility.ts} +0 -0
package/src/Call.ts
CHANGED
|
@@ -2,12 +2,14 @@ import { StreamSfuClient } from './StreamSfuClient';
|
|
|
2
2
|
import {
|
|
3
3
|
Dispatcher,
|
|
4
4
|
getGenericSdp,
|
|
5
|
+
isAudioTrackType,
|
|
5
6
|
isSfuEvent,
|
|
7
|
+
muteTypeToTrackType,
|
|
6
8
|
Publisher,
|
|
7
9
|
Subscriber,
|
|
10
|
+
toRtcConfiguration,
|
|
11
|
+
trackTypeToParticipantStreamKey,
|
|
8
12
|
} from './rtc';
|
|
9
|
-
import { muteTypeToTrackType } from './rtc/helpers/tracks';
|
|
10
|
-
import { toRtcConfiguration } from './rtc/helpers/rtcConfiguration';
|
|
11
13
|
import {
|
|
12
14
|
registerEventHandlers,
|
|
13
15
|
registerRingingCallEventHandlers,
|
|
@@ -27,6 +29,7 @@ import type {
|
|
|
27
29
|
BlockUserRequest,
|
|
28
30
|
BlockUserResponse,
|
|
29
31
|
CallRingEvent,
|
|
32
|
+
CallSettingsResponse,
|
|
30
33
|
CollectUserFeedbackRequest,
|
|
31
34
|
CollectUserFeedbackResponse,
|
|
32
35
|
Credentials,
|
|
@@ -55,12 +58,16 @@ import type {
|
|
|
55
58
|
SendCallEventResponse,
|
|
56
59
|
SendReactionRequest,
|
|
57
60
|
SendReactionResponse,
|
|
61
|
+
StartClosedCaptionsRequest,
|
|
62
|
+
StartClosedCaptionsResponse,
|
|
58
63
|
StartHLSBroadcastingResponse,
|
|
59
64
|
StartRecordingRequest,
|
|
60
65
|
StartRecordingResponse,
|
|
61
66
|
StartTranscriptionRequest,
|
|
62
67
|
StartTranscriptionResponse,
|
|
63
68
|
StatsOptions,
|
|
69
|
+
StopClosedCaptionsRequest,
|
|
70
|
+
StopClosedCaptionsResponse,
|
|
64
71
|
StopHLSBroadcastingResponse,
|
|
65
72
|
StopLiveResponse,
|
|
66
73
|
StopRecordingResponse,
|
|
@@ -75,15 +82,16 @@ import type {
|
|
|
75
82
|
UpdateCallResponse,
|
|
76
83
|
UpdateUserPermissionsRequest,
|
|
77
84
|
UpdateUserPermissionsResponse,
|
|
78
|
-
|
|
85
|
+
VideoDimension,
|
|
79
86
|
} from './gen/coordinator';
|
|
80
87
|
import { OwnCapability } from './gen/coordinator';
|
|
81
88
|
import {
|
|
82
89
|
AudioTrackType,
|
|
83
90
|
CallConstructor,
|
|
84
91
|
CallLeaveOptions,
|
|
92
|
+
ClientPublishOptions,
|
|
93
|
+
ClosedCaptionsSettings,
|
|
85
94
|
JoinCallData,
|
|
86
|
-
PublishOptions,
|
|
87
95
|
TrackMuteType,
|
|
88
96
|
VideoTrackType,
|
|
89
97
|
} from './types';
|
|
@@ -91,6 +99,9 @@ import { BehaviorSubject, Subject, takeWhile } from 'rxjs';
|
|
|
91
99
|
import { ReconnectDetails } from './gen/video/sfu/event/events';
|
|
92
100
|
import {
|
|
93
101
|
ClientDetails,
|
|
102
|
+
Codec,
|
|
103
|
+
PublishOption,
|
|
104
|
+
SubscribeOption,
|
|
94
105
|
TrackType,
|
|
95
106
|
WebsocketReconnectStrategy,
|
|
96
107
|
} from './gen/video/sfu/models/models';
|
|
@@ -111,7 +122,6 @@ import {
|
|
|
111
122
|
import { getClientDetails } from './client-details';
|
|
112
123
|
import { getLogger } from './logger';
|
|
113
124
|
import {
|
|
114
|
-
CameraDirection,
|
|
115
125
|
CameraManager,
|
|
116
126
|
MicrophoneManager,
|
|
117
127
|
ScreenShareManager,
|
|
@@ -120,6 +130,7 @@ import {
|
|
|
120
130
|
import { getSdkSignature } from './stats/utils';
|
|
121
131
|
import { withoutConcurrency } from './helpers/concurrency';
|
|
122
132
|
import { ensureExhausted } from './helpers/ensureExhausted';
|
|
133
|
+
import { pushToIfMissing } from './helpers/array';
|
|
123
134
|
import {
|
|
124
135
|
makeSafePromise,
|
|
125
136
|
PromiseWithResolvers,
|
|
@@ -201,7 +212,8 @@ export class Call {
|
|
|
201
212
|
*/
|
|
202
213
|
private readonly dispatcher = new Dispatcher();
|
|
203
214
|
|
|
204
|
-
private
|
|
215
|
+
private clientPublishOptions?: ClientPublishOptions;
|
|
216
|
+
private currentPublishOptions?: PublishOption[];
|
|
205
217
|
private statsReporter?: StatsReporter;
|
|
206
218
|
private sfuStatsReporter?: SfuStatsReporter;
|
|
207
219
|
private dropTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -287,7 +299,7 @@ export class Call {
|
|
|
287
299
|
this.dynascaleManager = new DynascaleManager(this.state, this.speaker);
|
|
288
300
|
}
|
|
289
301
|
|
|
290
|
-
private async
|
|
302
|
+
private setup = async () => {
|
|
291
303
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
292
304
|
if (this.initialized) return;
|
|
293
305
|
|
|
@@ -298,6 +310,12 @@ export class Call {
|
|
|
298
310
|
}),
|
|
299
311
|
);
|
|
300
312
|
|
|
313
|
+
this.leaveCallHooks.add(
|
|
314
|
+
this.on('changePublishOptions', (event) => {
|
|
315
|
+
this.currentPublishOptions = event.publishOptions;
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
|
|
301
319
|
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
302
320
|
this.registerEffects();
|
|
303
321
|
this.registerReconnectHandlers();
|
|
@@ -308,9 +326,9 @@ export class Call {
|
|
|
308
326
|
|
|
309
327
|
this.initialized = true;
|
|
310
328
|
});
|
|
311
|
-
}
|
|
329
|
+
};
|
|
312
330
|
|
|
313
|
-
private registerEffects() {
|
|
331
|
+
private registerEffects = () => {
|
|
314
332
|
this.leaveCallHooks.add(
|
|
315
333
|
// handles updating the permissions context when the settings change.
|
|
316
334
|
createSubscription(this.state.settings$, (settings) => {
|
|
@@ -401,7 +419,7 @@ export class Call {
|
|
|
401
419
|
}
|
|
402
420
|
}),
|
|
403
421
|
);
|
|
404
|
-
}
|
|
422
|
+
};
|
|
405
423
|
|
|
406
424
|
private handleOwnCapabilitiesUpdated = async (
|
|
407
425
|
ownCapabilities: OwnCapability[],
|
|
@@ -540,10 +558,10 @@ export class Call {
|
|
|
540
558
|
this.sfuStatsReporter?.stop();
|
|
541
559
|
this.sfuStatsReporter = undefined;
|
|
542
560
|
|
|
543
|
-
this.subscriber?.
|
|
561
|
+
this.subscriber?.dispose();
|
|
544
562
|
this.subscriber = undefined;
|
|
545
563
|
|
|
546
|
-
this.publisher?.
|
|
564
|
+
this.publisher?.dispose();
|
|
547
565
|
this.publisher = undefined;
|
|
548
566
|
|
|
549
567
|
await this.sfuClient?.leaveAndClose(reason);
|
|
@@ -551,6 +569,7 @@ export class Call {
|
|
|
551
569
|
this.dynascaleManager.setSfuClient(undefined);
|
|
552
570
|
|
|
553
571
|
this.state.setCallingState(CallingState.LEFT);
|
|
572
|
+
this.state.dispose();
|
|
554
573
|
|
|
555
574
|
// Call all leave call hooks, e.g. to clean up global event handlers
|
|
556
575
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
@@ -607,9 +626,8 @@ export class Call {
|
|
|
607
626
|
// call.ring event excludes the call creator in the members list
|
|
608
627
|
// as the creator does not get the ring event
|
|
609
628
|
// so update the member list accordingly
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
);
|
|
629
|
+
const { created_by, settings } = event.call;
|
|
630
|
+
const creator = this.state.members.find((m) => m.user.id === created_by.id);
|
|
613
631
|
if (!creator) {
|
|
614
632
|
this.state.setMembers(event.members);
|
|
615
633
|
} else {
|
|
@@ -623,7 +641,7 @@ export class Call {
|
|
|
623
641
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
624
642
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
625
643
|
this.clientStore.setCalls([this, ...calls]);
|
|
626
|
-
await this.applyDeviceConfig(false);
|
|
644
|
+
await this.applyDeviceConfig(settings, false);
|
|
627
645
|
};
|
|
628
646
|
|
|
629
647
|
/**
|
|
@@ -658,7 +676,7 @@ export class Call {
|
|
|
658
676
|
this.clientStore.registerCall(this);
|
|
659
677
|
}
|
|
660
678
|
|
|
661
|
-
await this.applyDeviceConfig(false);
|
|
679
|
+
await this.applyDeviceConfig(response.call.settings, false);
|
|
662
680
|
|
|
663
681
|
return response;
|
|
664
682
|
};
|
|
@@ -689,7 +707,7 @@ export class Call {
|
|
|
689
707
|
this.clientStore.registerCall(this);
|
|
690
708
|
}
|
|
691
709
|
|
|
692
|
-
await this.applyDeviceConfig(false);
|
|
710
|
+
await this.applyDeviceConfig(response.call.settings, false);
|
|
693
711
|
|
|
694
712
|
return response;
|
|
695
713
|
};
|
|
@@ -814,20 +832,35 @@ export class Call {
|
|
|
814
832
|
// we don't need to send JoinRequest if we are re-using an existing healthy SFU client
|
|
815
833
|
if (previousSfuClient !== sfuClient) {
|
|
816
834
|
// prepare a generic SDP and send it to the SFU.
|
|
817
|
-
//
|
|
835
|
+
// these are throw-away SDPs that the SFU will use to determine
|
|
818
836
|
// the capabilities of the client (codec support, etc.)
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
837
|
+
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
838
|
+
getGenericSdp('recvonly'),
|
|
839
|
+
getGenericSdp('sendonly'),
|
|
840
|
+
]);
|
|
841
|
+
const isReconnecting =
|
|
842
|
+
this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
|
|
843
|
+
const reconnectDetails = isReconnecting
|
|
844
|
+
? this.getReconnectDetails(data?.migrating_from, previousSessionId)
|
|
845
|
+
: undefined;
|
|
846
|
+
const preferredPublishOptions = !isReconnecting
|
|
847
|
+
? this.getPreferredPublishOptions()
|
|
848
|
+
: this.currentPublishOptions || [];
|
|
849
|
+
const preferredSubscribeOptions = !isReconnecting
|
|
850
|
+
? this.getPreferredSubscribeOptions()
|
|
851
|
+
: [];
|
|
852
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } =
|
|
853
|
+
await sfuClient.join({
|
|
854
|
+
subscriberSdp,
|
|
855
|
+
publisherSdp,
|
|
856
|
+
clientDetails,
|
|
857
|
+
fastReconnect: performingFastReconnect,
|
|
858
|
+
reconnectDetails,
|
|
859
|
+
preferredPublishOptions,
|
|
860
|
+
preferredSubscribeOptions,
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
this.currentPublishOptions = publishOptions;
|
|
831
864
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
832
865
|
if (callState) {
|
|
833
866
|
this.state.updateFromSfuCallState(
|
|
@@ -857,18 +890,16 @@ export class Call {
|
|
|
857
890
|
connectionConfig,
|
|
858
891
|
clientDetails,
|
|
859
892
|
statsOptions,
|
|
893
|
+
publishOptions: this.currentPublishOptions || [],
|
|
860
894
|
closePreviousInstances: !performingMigration,
|
|
861
895
|
});
|
|
862
896
|
}
|
|
863
897
|
|
|
864
898
|
// make sure we only track connection timing if we are not calling this method as part of a reconnection flow
|
|
865
899
|
if (!performingRejoin && !performingFastReconnect && !performingMigration) {
|
|
866
|
-
this.sfuStatsReporter?.
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
connectionTimeSeconds: (Date.now() - connectStartTime) / 1000,
|
|
870
|
-
},
|
|
871
|
-
});
|
|
900
|
+
this.sfuStatsReporter?.sendConnectionTime(
|
|
901
|
+
(Date.now() - connectStartTime) / 1000,
|
|
902
|
+
);
|
|
872
903
|
}
|
|
873
904
|
|
|
874
905
|
if (performingRejoin) {
|
|
@@ -885,8 +916,8 @@ export class Call {
|
|
|
885
916
|
|
|
886
917
|
// device settings should be applied only once, we don't have to
|
|
887
918
|
// re-apply them on later reconnections or server-side data fetches
|
|
888
|
-
if (!this.deviceSettingsAppliedOnce) {
|
|
889
|
-
await this.applyDeviceConfig(true);
|
|
919
|
+
if (!this.deviceSettingsAppliedOnce && this.state.settings) {
|
|
920
|
+
await this.applyDeviceConfig(this.state.settings, true);
|
|
890
921
|
this.deviceSettingsAppliedOnce = true;
|
|
891
922
|
}
|
|
892
923
|
|
|
@@ -896,6 +927,8 @@ export class Call {
|
|
|
896
927
|
// we will spam the other participants with push notifications and `call.ring` events.
|
|
897
928
|
delete this.joinCallData?.ring;
|
|
898
929
|
delete this.joinCallData?.notify;
|
|
930
|
+
// reset the reconnect strategy to unspecified after a successful reconnection
|
|
931
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
|
|
899
932
|
|
|
900
933
|
this.logger('info', `Joined call ${this.cid}`);
|
|
901
934
|
};
|
|
@@ -910,7 +943,8 @@ export class Call {
|
|
|
910
943
|
): ReconnectDetails => {
|
|
911
944
|
const strategy = this.reconnectStrategy;
|
|
912
945
|
const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
|
|
913
|
-
const announcedTracks =
|
|
946
|
+
const announcedTracks =
|
|
947
|
+
this.publisher?.getAnnouncedTracksForReconnect() || [];
|
|
914
948
|
return {
|
|
915
949
|
strategy,
|
|
916
950
|
announcedTracks,
|
|
@@ -921,6 +955,62 @@ export class Call {
|
|
|
921
955
|
};
|
|
922
956
|
};
|
|
923
957
|
|
|
958
|
+
/**
|
|
959
|
+
* Prepares the preferred codec for the call.
|
|
960
|
+
* This is an experimental client feature and subject to change.
|
|
961
|
+
* @internal
|
|
962
|
+
*/
|
|
963
|
+
private getPreferredPublishOptions = (): PublishOption[] => {
|
|
964
|
+
const { preferredCodec, fmtpLine, preferredBitrate, maxSimulcastLayers } =
|
|
965
|
+
this.clientPublishOptions || {};
|
|
966
|
+
if (!preferredCodec && !preferredBitrate && !maxSimulcastLayers) return [];
|
|
967
|
+
|
|
968
|
+
const codec = preferredCodec
|
|
969
|
+
? Codec.create({ name: preferredCodec.split('/').pop(), fmtp: fmtpLine })
|
|
970
|
+
: undefined;
|
|
971
|
+
|
|
972
|
+
const preferredPublishOptions = [
|
|
973
|
+
PublishOption.create({
|
|
974
|
+
trackType: TrackType.VIDEO,
|
|
975
|
+
codec,
|
|
976
|
+
bitrate: preferredBitrate,
|
|
977
|
+
maxSpatialLayers: maxSimulcastLayers,
|
|
978
|
+
}),
|
|
979
|
+
];
|
|
980
|
+
|
|
981
|
+
const screenShareSettings = this.screenShare.getSettings();
|
|
982
|
+
if (screenShareSettings) {
|
|
983
|
+
preferredPublishOptions.push(
|
|
984
|
+
PublishOption.create({
|
|
985
|
+
trackType: TrackType.SCREEN_SHARE,
|
|
986
|
+
fps: screenShareSettings.maxFramerate,
|
|
987
|
+
bitrate: screenShareSettings.maxBitrate,
|
|
988
|
+
}),
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return preferredPublishOptions;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Prepares the preferred options for subscribing to tracks.
|
|
997
|
+
* This is an experimental client feature and subject to change.
|
|
998
|
+
* @internal
|
|
999
|
+
*/
|
|
1000
|
+
private getPreferredSubscribeOptions = (): SubscribeOption[] => {
|
|
1001
|
+
const { subscriberCodec, subscriberFmtpLine } =
|
|
1002
|
+
this.clientPublishOptions || {};
|
|
1003
|
+
if (!subscriberCodec || !subscriberFmtpLine) return [];
|
|
1004
|
+
return [
|
|
1005
|
+
SubscribeOption.create({
|
|
1006
|
+
trackType: TrackType.VIDEO,
|
|
1007
|
+
codecs: [
|
|
1008
|
+
{ name: subscriberCodec.split('/').pop(), fmtp: subscriberFmtpLine },
|
|
1009
|
+
],
|
|
1010
|
+
}),
|
|
1011
|
+
];
|
|
1012
|
+
};
|
|
1013
|
+
|
|
924
1014
|
/**
|
|
925
1015
|
* Performs an ICE restart on both the Publisher and Subscriber Peer Connections.
|
|
926
1016
|
* Uses the provided SFU client to restore the ICE connection.
|
|
@@ -959,6 +1049,7 @@ export class Call {
|
|
|
959
1049
|
connectionConfig: RTCConfiguration;
|
|
960
1050
|
statsOptions: StatsOptions;
|
|
961
1051
|
clientDetails: ClientDetails;
|
|
1052
|
+
publishOptions: PublishOption[];
|
|
962
1053
|
closePreviousInstances: boolean;
|
|
963
1054
|
}) => {
|
|
964
1055
|
const {
|
|
@@ -966,10 +1057,11 @@ export class Call {
|
|
|
966
1057
|
connectionConfig,
|
|
967
1058
|
clientDetails,
|
|
968
1059
|
statsOptions,
|
|
1060
|
+
publishOptions,
|
|
969
1061
|
closePreviousInstances,
|
|
970
1062
|
} = opts;
|
|
971
1063
|
if (closePreviousInstances && this.subscriber) {
|
|
972
|
-
this.subscriber.
|
|
1064
|
+
this.subscriber.dispose();
|
|
973
1065
|
}
|
|
974
1066
|
this.subscriber = new Subscriber({
|
|
975
1067
|
sfuClient,
|
|
@@ -993,18 +1085,14 @@ export class Call {
|
|
|
993
1085
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
994
1086
|
if (!isAnonymous) {
|
|
995
1087
|
if (closePreviousInstances && this.publisher) {
|
|
996
|
-
this.publisher.
|
|
1088
|
+
this.publisher.dispose();
|
|
997
1089
|
}
|
|
998
|
-
const audioSettings = this.state.settings?.audio;
|
|
999
|
-
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
1000
|
-
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
1001
1090
|
this.publisher = new Publisher({
|
|
1002
1091
|
sfuClient,
|
|
1003
1092
|
dispatcher: this.dispatcher,
|
|
1004
1093
|
state: this.state,
|
|
1005
1094
|
connectionConfig,
|
|
1006
|
-
|
|
1007
|
-
isRedEnabled,
|
|
1095
|
+
publishOptions,
|
|
1008
1096
|
logTag: String(this.sfuClientTag),
|
|
1009
1097
|
onUnrecoverableError: () => {
|
|
1010
1098
|
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
@@ -1194,19 +1282,14 @@ export class Call {
|
|
|
1194
1282
|
* @internal
|
|
1195
1283
|
*/
|
|
1196
1284
|
private reconnectFast = async () => {
|
|
1197
|
-
|
|
1285
|
+
const reconnectStartTime = Date.now();
|
|
1198
1286
|
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
1199
1287
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
1200
1288
|
await this.join(this.joinCallData);
|
|
1201
|
-
this.sfuStatsReporter?.
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
1206
|
-
strategy: WebsocketReconnectStrategy.FAST,
|
|
1207
|
-
},
|
|
1208
|
-
},
|
|
1209
|
-
});
|
|
1289
|
+
this.sfuStatsReporter?.sendReconnectionTime(
|
|
1290
|
+
WebsocketReconnectStrategy.FAST,
|
|
1291
|
+
(Date.now() - reconnectStartTime) / 1000,
|
|
1292
|
+
);
|
|
1210
1293
|
};
|
|
1211
1294
|
|
|
1212
1295
|
/**
|
|
@@ -1214,21 +1297,16 @@ export class Call {
|
|
|
1214
1297
|
* @internal
|
|
1215
1298
|
*/
|
|
1216
1299
|
private reconnectRejoin = async () => {
|
|
1217
|
-
|
|
1300
|
+
const reconnectStartTime = Date.now();
|
|
1218
1301
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
1219
1302
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
1220
1303
|
await this.join(this.joinCallData);
|
|
1221
1304
|
await this.restorePublishedTracks();
|
|
1222
1305
|
this.restoreSubscribedTracks();
|
|
1223
|
-
this.sfuStatsReporter?.
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
1228
|
-
strategy: WebsocketReconnectStrategy.REJOIN,
|
|
1229
|
-
},
|
|
1230
|
-
},
|
|
1231
|
-
});
|
|
1306
|
+
this.sfuStatsReporter?.sendReconnectionTime(
|
|
1307
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
1308
|
+
(Date.now() - reconnectStartTime) / 1000,
|
|
1309
|
+
);
|
|
1232
1310
|
};
|
|
1233
1311
|
|
|
1234
1312
|
/**
|
|
@@ -1236,7 +1314,7 @@ export class Call {
|
|
|
1236
1314
|
* @internal
|
|
1237
1315
|
*/
|
|
1238
1316
|
private reconnectMigrate = async () => {
|
|
1239
|
-
|
|
1317
|
+
const reconnectStartTime = Date.now();
|
|
1240
1318
|
const currentSfuClient = this.sfuClient;
|
|
1241
1319
|
if (!currentSfuClient) {
|
|
1242
1320
|
throw new Error('Cannot migrate without an active SFU client');
|
|
@@ -1275,21 +1353,16 @@ export class Call {
|
|
|
1275
1353
|
// the `migrationTask`
|
|
1276
1354
|
this.state.setCallingState(CallingState.JOINED);
|
|
1277
1355
|
} finally {
|
|
1278
|
-
currentSubscriber?.
|
|
1279
|
-
currentPublisher?.
|
|
1356
|
+
currentSubscriber?.dispose();
|
|
1357
|
+
currentPublisher?.dispose();
|
|
1280
1358
|
|
|
1281
1359
|
// and close the previous SFU client, without specifying close code
|
|
1282
1360
|
currentSfuClient.close();
|
|
1283
1361
|
}
|
|
1284
|
-
this.sfuStatsReporter?.
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
1289
|
-
strategy: WebsocketReconnectStrategy.MIGRATE,
|
|
1290
|
-
},
|
|
1291
|
-
},
|
|
1292
|
-
});
|
|
1362
|
+
this.sfuStatsReporter?.sendReconnectionTime(
|
|
1363
|
+
WebsocketReconnectStrategy.MIGRATE,
|
|
1364
|
+
(Date.now() - reconnectStartTime) / 1000,
|
|
1365
|
+
);
|
|
1293
1366
|
};
|
|
1294
1367
|
|
|
1295
1368
|
/**
|
|
@@ -1378,22 +1451,16 @@ export class Call {
|
|
|
1378
1451
|
// the tracks need to be restored in their original order of publishing
|
|
1379
1452
|
// otherwise, we might get `m-lines order mismatch` errors
|
|
1380
1453
|
for (const trackType of this.trackPublishOrder) {
|
|
1454
|
+
let mediaStream: MediaStream | undefined;
|
|
1381
1455
|
switch (trackType) {
|
|
1382
1456
|
case TrackType.AUDIO:
|
|
1383
|
-
|
|
1384
|
-
if (audioStream) {
|
|
1385
|
-
await this.publishAudioStream(audioStream);
|
|
1386
|
-
}
|
|
1457
|
+
mediaStream = this.microphone.state.mediaStream;
|
|
1387
1458
|
break;
|
|
1388
1459
|
case TrackType.VIDEO:
|
|
1389
|
-
|
|
1390
|
-
if (videoStream) await this.publishVideoStream(videoStream);
|
|
1460
|
+
mediaStream = this.camera.state.mediaStream;
|
|
1391
1461
|
break;
|
|
1392
1462
|
case TrackType.SCREEN_SHARE:
|
|
1393
|
-
|
|
1394
|
-
if (screenShareStream) {
|
|
1395
|
-
await this.publishScreenShareStream(screenShareStream);
|
|
1396
|
-
}
|
|
1463
|
+
mediaStream = this.screenShare.state.mediaStream;
|
|
1397
1464
|
break;
|
|
1398
1465
|
// screen share audio can't exist without a screen share, so we handle it there
|
|
1399
1466
|
case TrackType.SCREEN_SHARE_AUDIO:
|
|
@@ -1403,6 +1470,8 @@ export class Call {
|
|
|
1403
1470
|
ensureExhausted(trackType, 'Unknown track type');
|
|
1404
1471
|
break;
|
|
1405
1472
|
}
|
|
1473
|
+
|
|
1474
|
+
if (mediaStream) await this.publish(mediaStream, trackType);
|
|
1406
1475
|
}
|
|
1407
1476
|
};
|
|
1408
1477
|
|
|
@@ -1418,136 +1487,112 @@ export class Call {
|
|
|
1418
1487
|
|
|
1419
1488
|
/**
|
|
1420
1489
|
* Starts publishing the given video stream to the call.
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
* Consecutive calls to this method will replace the previously published stream.
|
|
1424
|
-
* The previous video stream will be stopped.
|
|
1425
|
-
*
|
|
1426
|
-
* @param videoStream the video stream to publish.
|
|
1490
|
+
* @deprecated use `call.publish()`.
|
|
1427
1491
|
*/
|
|
1428
1492
|
publishVideoStream = async (videoStream: MediaStream) => {
|
|
1429
|
-
|
|
1430
|
-
// joining is in progress, and we should wait until the client is ready
|
|
1431
|
-
await this.sfuClient.joinTask;
|
|
1432
|
-
|
|
1433
|
-
if (!this.permissionsContext.hasPermission(OwnCapability.SEND_VIDEO)) {
|
|
1434
|
-
throw new Error('No permission to publish video');
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
if (!this.publisher) throw new Error('Publisher is not initialized');
|
|
1438
|
-
|
|
1439
|
-
const [videoTrack] = videoStream.getVideoTracks();
|
|
1440
|
-
if (!videoTrack) throw new Error('There is no video track in the stream');
|
|
1441
|
-
|
|
1442
|
-
if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
|
|
1443
|
-
this.trackPublishOrder.push(TrackType.VIDEO);
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
await this.publisher.publishStream(
|
|
1447
|
-
videoStream,
|
|
1448
|
-
videoTrack,
|
|
1449
|
-
TrackType.VIDEO,
|
|
1450
|
-
this.publishOptions,
|
|
1451
|
-
);
|
|
1493
|
+
await this.publish(videoStream, TrackType.VIDEO);
|
|
1452
1494
|
};
|
|
1453
1495
|
|
|
1454
1496
|
/**
|
|
1455
1497
|
* Starts publishing the given audio stream to the call.
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
* Consecutive calls to this method will replace the audio stream that is currently being published.
|
|
1459
|
-
* The previous audio stream will be stopped.
|
|
1460
|
-
*
|
|
1461
|
-
* @param audioStream the audio stream to publish.
|
|
1498
|
+
* @deprecated use `call.publish()`
|
|
1462
1499
|
*/
|
|
1463
1500
|
publishAudioStream = async (audioStream: MediaStream) => {
|
|
1464
|
-
|
|
1465
|
-
// joining is in progress, and we should wait until the client is ready
|
|
1466
|
-
await this.sfuClient.joinTask;
|
|
1467
|
-
|
|
1468
|
-
if (!this.permissionsContext.hasPermission(OwnCapability.SEND_AUDIO)) {
|
|
1469
|
-
throw new Error('No permission to publish audio');
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
if (!this.publisher) throw new Error('Publisher is not initialized');
|
|
1473
|
-
|
|
1474
|
-
const [audioTrack] = audioStream.getAudioTracks();
|
|
1475
|
-
if (!audioTrack) throw new Error('There is no audio track in the stream');
|
|
1476
|
-
|
|
1477
|
-
if (!this.trackPublishOrder.includes(TrackType.AUDIO)) {
|
|
1478
|
-
this.trackPublishOrder.push(TrackType.AUDIO);
|
|
1479
|
-
}
|
|
1480
|
-
await this.publisher.publishStream(
|
|
1481
|
-
audioStream,
|
|
1482
|
-
audioTrack,
|
|
1483
|
-
TrackType.AUDIO,
|
|
1484
|
-
);
|
|
1501
|
+
await this.publish(audioStream, TrackType.AUDIO);
|
|
1485
1502
|
};
|
|
1486
1503
|
|
|
1487
1504
|
/**
|
|
1488
1505
|
* Starts publishing the given screen-share stream to the call.
|
|
1489
|
-
*
|
|
1490
|
-
* Consecutive calls to this method will replace the previous screen-share stream.
|
|
1491
|
-
* The previous screen-share stream will be stopped.
|
|
1492
|
-
*
|
|
1493
|
-
* @param screenShareStream the screen-share stream to publish.
|
|
1506
|
+
* @deprecated use `call.publish()`
|
|
1494
1507
|
*/
|
|
1495
1508
|
publishScreenShareStream = async (screenShareStream: MediaStream) => {
|
|
1509
|
+
await this.publish(screenShareStream, TrackType.SCREEN_SHARE);
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* Publishes the given media stream.
|
|
1514
|
+
*
|
|
1515
|
+
* @param mediaStream the media stream to publish.
|
|
1516
|
+
* @param trackType the type of the track to announce.
|
|
1517
|
+
*/
|
|
1518
|
+
publish = async (mediaStream: MediaStream, trackType: TrackType) => {
|
|
1496
1519
|
if (!this.sfuClient) throw new Error(`Call not joined yet.`);
|
|
1497
1520
|
// joining is in progress, and we should wait until the client is ready
|
|
1498
1521
|
await this.sfuClient.joinTask;
|
|
1499
1522
|
|
|
1500
|
-
if (!this.permissionsContext.
|
|
1501
|
-
throw new Error(
|
|
1523
|
+
if (!this.permissionsContext.canPublish(trackType)) {
|
|
1524
|
+
throw new Error(`No permission to publish ${TrackType[trackType]}`);
|
|
1502
1525
|
}
|
|
1503
1526
|
|
|
1504
1527
|
if (!this.publisher) throw new Error('Publisher is not initialized');
|
|
1505
1528
|
|
|
1506
|
-
const [
|
|
1507
|
-
|
|
1508
|
-
|
|
1529
|
+
const [track] = isAudioTrackType(trackType)
|
|
1530
|
+
? mediaStream.getAudioTracks()
|
|
1531
|
+
: mediaStream.getVideoTracks();
|
|
1532
|
+
|
|
1533
|
+
if (!track) {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
`There is no ${TrackType[trackType]} track in the stream`,
|
|
1536
|
+
);
|
|
1509
1537
|
}
|
|
1510
1538
|
|
|
1511
|
-
if (
|
|
1512
|
-
|
|
1539
|
+
if (track.readyState === 'ended') {
|
|
1540
|
+
throw new Error(`Can't publish ended tracks.`);
|
|
1513
1541
|
}
|
|
1514
|
-
const opts: PublishOptions = {
|
|
1515
|
-
screenShareSettings: this.screenShare.getSettings(),
|
|
1516
|
-
};
|
|
1517
|
-
await this.publisher.publishStream(
|
|
1518
|
-
screenShareStream,
|
|
1519
|
-
screenShareTrack,
|
|
1520
|
-
TrackType.SCREEN_SHARE,
|
|
1521
|
-
opts,
|
|
1522
|
-
);
|
|
1523
1542
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1543
|
+
pushToIfMissing(this.trackPublishOrder, trackType);
|
|
1544
|
+
await this.publisher.publish(track, trackType);
|
|
1545
|
+
|
|
1546
|
+
const trackTypes = [trackType];
|
|
1547
|
+
if (trackType === TrackType.SCREEN_SHARE) {
|
|
1548
|
+
const [audioTrack] = mediaStream.getAudioTracks();
|
|
1549
|
+
if (audioTrack) {
|
|
1550
|
+
pushToIfMissing(this.trackPublishOrder, TrackType.SCREEN_SHARE_AUDIO);
|
|
1551
|
+
await this.publisher.publish(audioTrack, TrackType.SCREEN_SHARE_AUDIO);
|
|
1552
|
+
trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
1528
1553
|
}
|
|
1529
|
-
await this.publisher.publishStream(
|
|
1530
|
-
screenShareStream,
|
|
1531
|
-
screenShareAudioTrack,
|
|
1532
|
-
TrackType.SCREEN_SHARE_AUDIO,
|
|
1533
|
-
opts,
|
|
1534
|
-
);
|
|
1535
1554
|
}
|
|
1555
|
+
|
|
1556
|
+
await this.updateLocalStreamState(mediaStream, ...trackTypes);
|
|
1536
1557
|
};
|
|
1537
1558
|
|
|
1538
1559
|
/**
|
|
1539
1560
|
* Stops publishing the given track type to the call, if it is currently being published.
|
|
1540
|
-
* Underlying track will be stopped and removed from the publisher.
|
|
1541
1561
|
*
|
|
1542
|
-
* @param
|
|
1543
|
-
* @param stopTrack if `true` the track will be stopped, else it will be just disabled
|
|
1562
|
+
* @param trackTypes the track types to stop publishing.
|
|
1544
1563
|
*/
|
|
1545
|
-
stopPublish = async (
|
|
1546
|
-
this.
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1564
|
+
stopPublish = async (...trackTypes: TrackType[]) => {
|
|
1565
|
+
if (!this.sfuClient || !this.publisher) return;
|
|
1566
|
+
this.publisher.stopTracks(...trackTypes);
|
|
1567
|
+
await this.updateLocalStreamState(undefined, ...trackTypes);
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Updates the call state with the new stream.
|
|
1572
|
+
*
|
|
1573
|
+
* @param mediaStream the new stream to update the call state with.
|
|
1574
|
+
* If undefined, the stream will be removed from the call state.
|
|
1575
|
+
* @param trackTypes the track types to update the call state with.
|
|
1576
|
+
*/
|
|
1577
|
+
private updateLocalStreamState = async (
|
|
1578
|
+
mediaStream: MediaStream | undefined,
|
|
1579
|
+
...trackTypes: TrackType[]
|
|
1580
|
+
) => {
|
|
1581
|
+
if (!this.sfuClient || !this.sfuClient.sessionId) return;
|
|
1582
|
+
await this.notifyTrackMuteState(!mediaStream, ...trackTypes);
|
|
1583
|
+
|
|
1584
|
+
const { sessionId } = this.sfuClient;
|
|
1585
|
+
for (const trackType of trackTypes) {
|
|
1586
|
+
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
1587
|
+
if (!streamStateProp) continue;
|
|
1588
|
+
|
|
1589
|
+
this.state.updateParticipant(sessionId, (p) => ({
|
|
1590
|
+
publishedTracks: mediaStream
|
|
1591
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
1592
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
1593
|
+
[streamStateProp]: mediaStream,
|
|
1594
|
+
}));
|
|
1595
|
+
}
|
|
1551
1596
|
};
|
|
1552
1597
|
|
|
1553
1598
|
/**
|
|
@@ -1556,9 +1601,20 @@ export class Call {
|
|
|
1556
1601
|
* @internal
|
|
1557
1602
|
* @param options the options to use.
|
|
1558
1603
|
*/
|
|
1559
|
-
updatePublishOptions(options:
|
|
1560
|
-
this.
|
|
1561
|
-
|
|
1604
|
+
updatePublishOptions = (options: ClientPublishOptions) => {
|
|
1605
|
+
this.logger(
|
|
1606
|
+
'warn',
|
|
1607
|
+
'[call.updatePublishOptions]: You are manually overriding the publish options for this call. ' +
|
|
1608
|
+
'This is not recommended, and it can cause call stability/compatibility issues. Use with caution.',
|
|
1609
|
+
);
|
|
1610
|
+
if (this.state.callingState === CallingState.JOINED) {
|
|
1611
|
+
this.logger(
|
|
1612
|
+
'warn',
|
|
1613
|
+
'Updating publish options after joining the call does not have an effect',
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
1617
|
+
};
|
|
1562
1618
|
|
|
1563
1619
|
/**
|
|
1564
1620
|
* Notifies the SFU that a noise cancellation process has started.
|
|
@@ -1582,6 +1638,17 @@ export class Call {
|
|
|
1582
1638
|
});
|
|
1583
1639
|
};
|
|
1584
1640
|
|
|
1641
|
+
/**
|
|
1642
|
+
* Notifies the SFU about the mute state of the given track types.
|
|
1643
|
+
* @internal
|
|
1644
|
+
*/
|
|
1645
|
+
notifyTrackMuteState = async (muted: boolean, ...trackTypes: TrackType[]) => {
|
|
1646
|
+
if (!this.sfuClient) return;
|
|
1647
|
+
await this.sfuClient.updateMuteStates(
|
|
1648
|
+
trackTypes.map((trackType) => ({ trackType, muted })),
|
|
1649
|
+
);
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1585
1652
|
/**
|
|
1586
1653
|
* Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore)).
|
|
1587
1654
|
* This is usually helpful when detailed stats for a specific participant are needed.
|
|
@@ -1771,7 +1838,54 @@ export class Call {
|
|
|
1771
1838
|
};
|
|
1772
1839
|
|
|
1773
1840
|
/**
|
|
1774
|
-
*
|
|
1841
|
+
* Starts the closed captions of the call.
|
|
1842
|
+
*/
|
|
1843
|
+
startClosedCaptions = async (
|
|
1844
|
+
options?: StartClosedCaptionsRequest,
|
|
1845
|
+
): Promise<StartClosedCaptionsResponse> => {
|
|
1846
|
+
const trx = this.state.setCaptioning(true); // optimistic update
|
|
1847
|
+
try {
|
|
1848
|
+
return await this.streamClient.post<
|
|
1849
|
+
StartClosedCaptionsResponse,
|
|
1850
|
+
StartClosedCaptionsRequest
|
|
1851
|
+
>(`${this.streamClientBasePath}/start_closed_captions`, options);
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
trx.rollback(); // revert the optimistic update
|
|
1854
|
+
throw err;
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* Stops the closed captions of the call.
|
|
1860
|
+
*/
|
|
1861
|
+
stopClosedCaptions = async (
|
|
1862
|
+
options?: StopClosedCaptionsRequest,
|
|
1863
|
+
): Promise<StopClosedCaptionsResponse> => {
|
|
1864
|
+
const trx = this.state.setCaptioning(false); // optimistic update
|
|
1865
|
+
try {
|
|
1866
|
+
return await this.streamClient.post<
|
|
1867
|
+
StopClosedCaptionsResponse,
|
|
1868
|
+
StopClosedCaptionsRequest
|
|
1869
|
+
>(`${this.streamClientBasePath}/stop_closed_captions`, options);
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
trx.rollback(); // revert the optimistic update
|
|
1872
|
+
throw err;
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Updates the closed caption settings.
|
|
1878
|
+
*
|
|
1879
|
+
* @param config the closed caption settings to apply
|
|
1880
|
+
*/
|
|
1881
|
+
updateClosedCaptionSettings = (config: Partial<ClosedCaptionsSettings>) => {
|
|
1882
|
+
this.state.updateClosedCaptionSettings(config);
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* Sends a `call.permission_request` event to all users connected to the call.
|
|
1887
|
+
* The call settings object contains information about which permissions can be requested during a call
|
|
1888
|
+
* (for example, a user might be allowed to request permission to publish audio, but not video).
|
|
1775
1889
|
*/
|
|
1776
1890
|
requestPermissions = async (
|
|
1777
1891
|
data: RequestPermissionRequest,
|
|
@@ -2149,93 +2263,18 @@ export class Call {
|
|
|
2149
2263
|
*
|
|
2150
2264
|
* @internal
|
|
2151
2265
|
*/
|
|
2152
|
-
applyDeviceConfig = async (
|
|
2153
|
-
|
|
2266
|
+
applyDeviceConfig = async (
|
|
2267
|
+
settings: CallSettingsResponse,
|
|
2268
|
+
publish: boolean,
|
|
2269
|
+
) => {
|
|
2270
|
+
await this.camera.apply(settings.video, publish).catch((err) => {
|
|
2154
2271
|
this.logger('warn', 'Camera init failed', err);
|
|
2155
2272
|
});
|
|
2156
|
-
await this.
|
|
2273
|
+
await this.microphone.apply(settings.audio, publish).catch((err) => {
|
|
2157
2274
|
this.logger('warn', 'Mic init failed', err);
|
|
2158
2275
|
});
|
|
2159
2276
|
};
|
|
2160
2277
|
|
|
2161
|
-
private initCamera = async (options: { setStatus: boolean }) => {
|
|
2162
|
-
// Wait for any in progress camera operation
|
|
2163
|
-
await this.camera.statusChangeSettled();
|
|
2164
|
-
|
|
2165
|
-
if (
|
|
2166
|
-
this.state.localParticipant?.videoStream ||
|
|
2167
|
-
!this.permissionsContext.hasPermission('send-video')
|
|
2168
|
-
) {
|
|
2169
|
-
return;
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
// Set camera direction if it's not yet set
|
|
2173
|
-
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
2174
|
-
let defaultDirection: CameraDirection = 'front';
|
|
2175
|
-
const backendSetting = this.state.settings?.video.camera_facing;
|
|
2176
|
-
if (backendSetting) {
|
|
2177
|
-
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
2178
|
-
}
|
|
2179
|
-
this.camera.state.setDirection(defaultDirection);
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
// Set target resolution
|
|
2183
|
-
const targetResolution = this.state.settings?.video.target_resolution;
|
|
2184
|
-
if (targetResolution) {
|
|
2185
|
-
await this.camera.selectTargetResolution(targetResolution);
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
if (options.setStatus) {
|
|
2189
|
-
// Publish already that was set before we joined
|
|
2190
|
-
if (
|
|
2191
|
-
this.camera.enabled &&
|
|
2192
|
-
this.camera.state.mediaStream &&
|
|
2193
|
-
!this.publisher?.isPublishing(TrackType.VIDEO)
|
|
2194
|
-
) {
|
|
2195
|
-
await this.publishVideoStream(this.camera.state.mediaStream);
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
// Start camera if backend config specifies, and there is no local setting
|
|
2199
|
-
if (
|
|
2200
|
-
this.camera.state.status === undefined &&
|
|
2201
|
-
this.state.settings?.video.camera_default_on
|
|
2202
|
-
) {
|
|
2203
|
-
await this.camera.enable();
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
};
|
|
2207
|
-
|
|
2208
|
-
private initMic = async (options: { setStatus: boolean }) => {
|
|
2209
|
-
// Wait for any in progress mic operation
|
|
2210
|
-
await this.microphone.statusChangeSettled();
|
|
2211
|
-
|
|
2212
|
-
if (
|
|
2213
|
-
this.state.localParticipant?.audioStream ||
|
|
2214
|
-
!this.permissionsContext.hasPermission('send-audio')
|
|
2215
|
-
) {
|
|
2216
|
-
return;
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
if (options.setStatus) {
|
|
2220
|
-
// Publish media stream that was set before we joined
|
|
2221
|
-
if (
|
|
2222
|
-
this.microphone.enabled &&
|
|
2223
|
-
this.microphone.state.mediaStream &&
|
|
2224
|
-
!this.publisher?.isPublishing(TrackType.AUDIO)
|
|
2225
|
-
) {
|
|
2226
|
-
await this.publishAudioStream(this.microphone.state.mediaStream);
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
// Start mic if backend config specifies, and there is no local setting
|
|
2230
|
-
if (
|
|
2231
|
-
this.microphone.state.status === undefined &&
|
|
2232
|
-
this.state.settings?.audio.mic_default_on
|
|
2233
|
-
) {
|
|
2234
|
-
await this.microphone.enable();
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
};
|
|
2238
|
-
|
|
2239
2278
|
/**
|
|
2240
2279
|
* Will begin tracking the given element for visibility changes within the
|
|
2241
2280
|
* configured viewport element (`call.setViewport`).
|
|
@@ -2377,7 +2416,7 @@ export class Call {
|
|
|
2377
2416
|
* preference has effect on. Affects all participants by default.
|
|
2378
2417
|
*/
|
|
2379
2418
|
setPreferredIncomingVideoResolution = (
|
|
2380
|
-
resolution:
|
|
2419
|
+
resolution: VideoDimension | undefined,
|
|
2381
2420
|
sessionIds?: string[],
|
|
2382
2421
|
) => {
|
|
2383
2422
|
this.dynascaleManager.setVideoTrackSubscriptionOverrides(
|