@stream-io/video-client 0.6.3 → 0.6.5
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 +57 -42
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +57 -42
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +57 -42
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +1 -1
- package/dist/src/StreamSfuClient.d.ts +1 -1
- package/dist/src/StreamVideoClient.d.ts +1 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +1 -1
- package/dist/src/devices/SpeakerManager.d.ts +4 -3
- package/dist/src/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/Call.ts +29 -23
- package/src/StreamSfuClient.ts +3 -6
- package/src/StreamVideoClient.ts +2 -2
- package/src/devices/InputMediaDeviceManager.ts +4 -2
- package/src/devices/InputMediaDeviceManagerState.ts +5 -6
- package/src/devices/SpeakerManager.ts +18 -6
- package/src/devices/__tests__/InputMediaDeviceManagerState.test.ts +1 -25
- package/src/events/call.ts +3 -3
- package/src/events/internal.ts +1 -1
- package/src/store/CallState.ts +16 -7
- package/src/store/stateStore.ts +5 -3
- package/src/types.ts +6 -0
package/dist/src/Call.d.ts
CHANGED
|
@@ -115,7 +115,7 @@ export declare class Call {
|
|
|
115
115
|
/**
|
|
116
116
|
* Leave the call and stop the media streams that were published by the call.
|
|
117
117
|
*/
|
|
118
|
-
leave: ({ reject }?: CallLeaveOptions) => Promise<void>;
|
|
118
|
+
leave: ({ reject, reason, }?: CallLeaveOptions) => Promise<void>;
|
|
119
119
|
/**
|
|
120
120
|
* A flag indicating whether the call is "ringing" type of call.
|
|
121
121
|
*/
|
|
@@ -100,7 +100,7 @@ export declare class StreamSfuClient {
|
|
|
100
100
|
* @param sessionId the `sessionId` of the currently connected participant.
|
|
101
101
|
*/
|
|
102
102
|
constructor({ dispatcher, sfuServer, token, sessionId, }: StreamSfuClientConstructor);
|
|
103
|
-
close: (code
|
|
103
|
+
close: (code: number, reason: string) => void;
|
|
104
104
|
updateSubscriptions: (subscriptions: TrackSubscriptionDetails[]) => Promise<FinishedUnaryCall<import("./gen/video/sfu/signal_rpc/signal").UpdateSubscriptionsRequest, import("./gen/video/sfu/signal_rpc/signal").UpdateSubscriptionsResponse>>;
|
|
105
105
|
setPublisher: (data: Omit<SetPublisherRequest, 'sessionId'>) => Promise<FinishedUnaryCall<SetPublisherRequest, import("./gen/video/sfu/signal_rpc/signal").SetPublisherResponse>>;
|
|
106
106
|
sendAnswer: (data: Omit<SendAnswerRequest, 'sessionId'>) => Promise<FinishedUnaryCall<SendAnswerRequest, import("./gen/video/sfu/signal_rpc/signal").SendAnswerResponse>>;
|
|
@@ -72,7 +72,7 @@ export declare class StreamVideoClient {
|
|
|
72
72
|
* Creates a new call.
|
|
73
73
|
*
|
|
74
74
|
* @param type the type of the call.
|
|
75
|
-
* @param id the id of the call
|
|
75
|
+
* @param id the id of the call.
|
|
76
76
|
*/
|
|
77
77
|
call: (type: string, id: string) => Call;
|
|
78
78
|
/**
|
|
@@ -58,7 +58,7 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
|
|
|
58
58
|
/**
|
|
59
59
|
* Selects a device.
|
|
60
60
|
*
|
|
61
|
-
* Note:
|
|
61
|
+
* Note: This method is not supported in React Native
|
|
62
62
|
* @param deviceId the device id to select.
|
|
63
63
|
*/
|
|
64
64
|
select(deviceId: string | undefined): Promise<void>;
|
|
@@ -9,6 +9,7 @@ export declare class SpeakerManager {
|
|
|
9
9
|
* Lists the available audio output devices
|
|
10
10
|
*
|
|
11
11
|
* Note: It prompts the user for a permission to use devices (if not already granted)
|
|
12
|
+
* Note: This method is not supported in React Native
|
|
12
13
|
*
|
|
13
14
|
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
14
15
|
*/
|
|
@@ -16,7 +17,7 @@ export declare class SpeakerManager {
|
|
|
16
17
|
/**
|
|
17
18
|
* Select a device.
|
|
18
19
|
*
|
|
19
|
-
* Note:
|
|
20
|
+
* Note: This method is not supported in React Native
|
|
20
21
|
*
|
|
21
22
|
* @param deviceId empty string means the system default
|
|
22
23
|
*/
|
|
@@ -26,13 +27,13 @@ export declare class SpeakerManager {
|
|
|
26
27
|
* Set the volume of the audio elements
|
|
27
28
|
* @param volume a number between 0 and 1.
|
|
28
29
|
*
|
|
29
|
-
* Note:
|
|
30
|
+
* Note: This method is not supported in React Native
|
|
30
31
|
*/
|
|
31
32
|
setVolume(volume: number): void;
|
|
32
33
|
/**
|
|
33
34
|
* Set the volume of a participant.
|
|
34
35
|
*
|
|
35
|
-
* Note:
|
|
36
|
+
* Note: This method is not supported in React Native.
|
|
36
37
|
*
|
|
37
38
|
* @param sessionId the participant's session id.
|
|
38
39
|
* @param volume a number between 0 and 1. Set it to `undefined` to use the default volume.
|
package/dist/src/types.d.ts
CHANGED
|
@@ -133,6 +133,11 @@ export type CallLeaveOptions = {
|
|
|
133
133
|
* @default `false`.
|
|
134
134
|
*/
|
|
135
135
|
reject?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* The reason for leaving the call.
|
|
138
|
+
* This will be sent to the backend and will be visible in the logs.
|
|
139
|
+
*/
|
|
140
|
+
reason?: string;
|
|
136
141
|
};
|
|
137
142
|
/**
|
|
138
143
|
* The options to pass to {@link Call} constructor.
|
package/package.json
CHANGED
package/src/Call.ts
CHANGED
|
@@ -372,7 +372,7 @@ export class Call {
|
|
|
372
372
|
const currentUserId = this.currentUserId;
|
|
373
373
|
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
374
374
|
this.logger('info', 'Leaving call because of being blocked');
|
|
375
|
-
await this.leave();
|
|
375
|
+
await this.leave({ reason: 'user blocked' });
|
|
376
376
|
}
|
|
377
377
|
}),
|
|
378
378
|
);
|
|
@@ -459,7 +459,10 @@ export class Call {
|
|
|
459
459
|
/**
|
|
460
460
|
* Leave the call and stop the media streams that were published by the call.
|
|
461
461
|
*/
|
|
462
|
-
leave = async ({
|
|
462
|
+
leave = async ({
|
|
463
|
+
reject = false,
|
|
464
|
+
reason = 'user is leaving the call',
|
|
465
|
+
}: CallLeaveOptions = {}) => {
|
|
463
466
|
const callingState = this.state.callingState;
|
|
464
467
|
if (callingState === CallingState.LEFT) {
|
|
465
468
|
throw new Error('Cannot leave call that has already been left.');
|
|
@@ -494,7 +497,7 @@ export class Call {
|
|
|
494
497
|
this.publisher?.close();
|
|
495
498
|
this.publisher = undefined;
|
|
496
499
|
|
|
497
|
-
this.sfuClient?.close();
|
|
500
|
+
this.sfuClient?.close(StreamSfuClient.NORMAL_CLOSURE, reason);
|
|
498
501
|
this.sfuClient = undefined;
|
|
499
502
|
|
|
500
503
|
this.dispatcher.offAll();
|
|
@@ -740,7 +743,8 @@ export class Call {
|
|
|
740
743
|
* A closure which hides away the re-connection logic.
|
|
741
744
|
*/
|
|
742
745
|
const reconnect = async (
|
|
743
|
-
strategy: 'full' | 'fast' | 'migrate'
|
|
746
|
+
strategy: 'full' | 'fast' | 'migrate',
|
|
747
|
+
reason: string,
|
|
744
748
|
): Promise<void> => {
|
|
745
749
|
const currentState = this.state.callingState;
|
|
746
750
|
if (
|
|
@@ -777,7 +781,7 @@ export class Call {
|
|
|
777
781
|
if (strategy === 'fast') {
|
|
778
782
|
sfuClient.close(
|
|
779
783
|
StreamSfuClient.ERROR_CONNECTION_BROKEN,
|
|
780
|
-
|
|
784
|
+
`attempting fast reconnect: ${reason}`,
|
|
781
785
|
);
|
|
782
786
|
} else if (strategy === 'full') {
|
|
783
787
|
// in migration or recovery scenarios, we don't want to
|
|
@@ -797,7 +801,7 @@ export class Call {
|
|
|
797
801
|
// clean up current connection
|
|
798
802
|
sfuClient.close(
|
|
799
803
|
StreamSfuClient.NORMAL_CLOSURE,
|
|
800
|
-
|
|
804
|
+
`attempting full reconnect: ${reason}`,
|
|
801
805
|
);
|
|
802
806
|
}
|
|
803
807
|
await this.join({
|
|
@@ -807,10 +811,7 @@ export class Call {
|
|
|
807
811
|
|
|
808
812
|
// clean up previous connection
|
|
809
813
|
if (strategy === 'migrate') {
|
|
810
|
-
sfuClient.close(
|
|
811
|
-
StreamSfuClient.NORMAL_CLOSURE,
|
|
812
|
-
'js-client: attempting migration',
|
|
813
|
-
);
|
|
814
|
+
sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'attempting migration');
|
|
814
815
|
}
|
|
815
816
|
|
|
816
817
|
this.logger(
|
|
@@ -866,7 +867,7 @@ export class Call {
|
|
|
866
867
|
'info',
|
|
867
868
|
`[Migration]: Going away from SFU... Reason: ${GoAwayReason[reason]}`,
|
|
868
869
|
);
|
|
869
|
-
reconnect('migrate').catch((err) => {
|
|
870
|
+
reconnect('migrate', GoAwayReason[reason]).catch((err) => {
|
|
870
871
|
this.logger(
|
|
871
872
|
'warn',
|
|
872
873
|
`[Migration]: Failed to migrate to another SFU.`,
|
|
@@ -901,14 +902,16 @@ export class Call {
|
|
|
901
902
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
902
903
|
sfuClient.isFastReconnecting = this.reconnectAttempts === 0;
|
|
903
904
|
const strategy = sfuClient.isFastReconnecting ? 'fast' : 'full';
|
|
904
|
-
reconnect(strategy).catch(
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
905
|
+
reconnect(strategy, `SFU closed the WS with code: ${e.code}`).catch(
|
|
906
|
+
(err) => {
|
|
907
|
+
this.logger(
|
|
908
|
+
'error',
|
|
909
|
+
`[Rejoin]: ${strategy} rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
910
|
+
err,
|
|
911
|
+
);
|
|
912
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
913
|
+
},
|
|
914
|
+
);
|
|
912
915
|
} else {
|
|
913
916
|
this.logger(
|
|
914
917
|
'error',
|
|
@@ -936,7 +939,10 @@ export class Call {
|
|
|
936
939
|
do {
|
|
937
940
|
try {
|
|
938
941
|
sfuClient.isFastReconnecting = isFirstReconnectAttempt;
|
|
939
|
-
await reconnect(
|
|
942
|
+
await reconnect(
|
|
943
|
+
isFirstReconnectAttempt ? 'fast' : 'full',
|
|
944
|
+
'Network: online',
|
|
945
|
+
);
|
|
940
946
|
return; // break the loop if rejoin is successful
|
|
941
947
|
} catch (err) {
|
|
942
948
|
this.logger(
|
|
@@ -1057,7 +1063,7 @@ export class Call {
|
|
|
1057
1063
|
await this.publisher.restartIce();
|
|
1058
1064
|
} else if (previousSfuClient?.isFastReconnecting) {
|
|
1059
1065
|
// reconnection wasn't possible, so we need to do a full rejoin
|
|
1060
|
-
return await reconnect('full').catch((err) => {
|
|
1066
|
+
return await reconnect('full', 're-attempting').catch((err) => {
|
|
1061
1067
|
this.logger(
|
|
1062
1068
|
'error',
|
|
1063
1069
|
`[Rejoin]: Rejoin failed forced full rejoin.`,
|
|
@@ -1125,7 +1131,7 @@ export class Call {
|
|
|
1125
1131
|
`[Rejoin]: Rejoin ${this.reconnectAttempts} failed.`,
|
|
1126
1132
|
err,
|
|
1127
1133
|
);
|
|
1128
|
-
await reconnect();
|
|
1134
|
+
await reconnect('full', 'previous attempt failed');
|
|
1129
1135
|
this.logger(
|
|
1130
1136
|
'info',
|
|
1131
1137
|
`[Rejoin]: Rejoin ${this.reconnectAttempts} successful!`,
|
|
@@ -1825,7 +1831,7 @@ export class Call {
|
|
|
1825
1831
|
|
|
1826
1832
|
clearTimeout(this.dropTimeout);
|
|
1827
1833
|
this.dropTimeout = setTimeout(() => {
|
|
1828
|
-
this.leave().catch((err) => {
|
|
1834
|
+
this.leave({ reason: 'ring: timeout' }).catch((err) => {
|
|
1829
1835
|
this.logger('error', 'Failed to drop call', err);
|
|
1830
1836
|
});
|
|
1831
1837
|
}, timeoutInMs);
|
package/src/StreamSfuClient.ts
CHANGED
|
@@ -209,13 +209,10 @@ export class StreamSfuClient {
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
close = (
|
|
213
|
-
|
|
214
|
-
reason: string = 'js-client: requested signal connection close',
|
|
215
|
-
) => {
|
|
216
|
-
this.logger('debug', 'Closing SFU WS connection', code, reason);
|
|
212
|
+
close = (code: number, reason: string) => {
|
|
213
|
+
this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
|
|
217
214
|
if (this.signalWs.readyState !== this.signalWs.CLOSED) {
|
|
218
|
-
this.signalWs.close(code, reason);
|
|
215
|
+
this.signalWs.close(code, `js-client: ${reason}`);
|
|
219
216
|
}
|
|
220
217
|
|
|
221
218
|
this.unsubscribeIceTrickle();
|
package/src/StreamVideoClient.ts
CHANGED
|
@@ -232,7 +232,7 @@ export class StreamVideoClient {
|
|
|
232
232
|
// if `call.created` was received before `call.ring`.
|
|
233
233
|
// In that case, we cleanup the already tracked call.
|
|
234
234
|
const prevCall = this.writeableStateStore.findCall(call.type, call.id);
|
|
235
|
-
await prevCall?.leave();
|
|
235
|
+
await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
|
|
236
236
|
// we create a new call
|
|
237
237
|
const theCall = new Call({
|
|
238
238
|
streamClient: this.streamClient,
|
|
@@ -310,7 +310,7 @@ export class StreamVideoClient {
|
|
|
310
310
|
* Creates a new call.
|
|
311
311
|
*
|
|
312
312
|
* @param type the type of the call.
|
|
313
|
-
* @param id the id of the call
|
|
313
|
+
* @param id the id of the call.
|
|
314
314
|
*/
|
|
315
315
|
call = (type: string, id: string) => {
|
|
316
316
|
return new Call({
|
|
@@ -125,12 +125,14 @@ export abstract class InputMediaDeviceManager<
|
|
|
125
125
|
/**
|
|
126
126
|
* Selects a device.
|
|
127
127
|
*
|
|
128
|
-
* Note:
|
|
128
|
+
* Note: This method is not supported in React Native
|
|
129
129
|
* @param deviceId the device id to select.
|
|
130
130
|
*/
|
|
131
131
|
async select(deviceId: string | undefined) {
|
|
132
132
|
if (isReactNative()) {
|
|
133
|
-
throw new Error(
|
|
133
|
+
throw new Error(
|
|
134
|
+
'This method is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for reference.',
|
|
135
|
+
);
|
|
134
136
|
}
|
|
135
137
|
if (deviceId === this.state.selectedDevice) {
|
|
136
138
|
return;
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
import { isReactNative } from '../helpers/platforms';
|
|
8
8
|
import { RxUtils } from '../store';
|
|
9
9
|
import { getLogger } from '../logger';
|
|
10
|
-
import { isSafari } from '../helpers/browsers';
|
|
11
10
|
|
|
12
11
|
export type InputDeviceStatus = 'enabled' | 'disabled' | undefined;
|
|
13
12
|
|
|
@@ -67,15 +66,15 @@ export abstract class InputMediaDeviceManagerState<C = MediaTrackConstraints> {
|
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
let permissionState: PermissionStatus;
|
|
70
|
-
const notify = () =>
|
|
69
|
+
const notify = () => {
|
|
71
70
|
subscriber.next(
|
|
72
|
-
// In
|
|
71
|
+
// In some browsers, the 'change' event doesn't reliably emit and hence,
|
|
73
72
|
// permissionState stays in 'prompt' state forever.
|
|
73
|
+
// Typically, this happens when a user grants one-time permission.
|
|
74
74
|
// Instead of checking if a permission is granted, we check if it isn't denied
|
|
75
|
-
|
|
76
|
-
? permissionState.state !== 'denied'
|
|
77
|
-
: permissionState.state === 'granted',
|
|
75
|
+
permissionState.state !== 'denied',
|
|
78
76
|
);
|
|
77
|
+
};
|
|
79
78
|
navigator.permissions
|
|
80
79
|
.query({ name: this.permissionName })
|
|
81
80
|
.then((permissionStatus) => {
|
|
@@ -34,23 +34,31 @@ export class SpeakerManager {
|
|
|
34
34
|
* Lists the available audio output devices
|
|
35
35
|
*
|
|
36
36
|
* Note: It prompts the user for a permission to use devices (if not already granted)
|
|
37
|
+
* Note: This method is not supported in React Native
|
|
37
38
|
*
|
|
38
39
|
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
39
40
|
*/
|
|
40
41
|
listDevices() {
|
|
42
|
+
if (isReactNative()) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
41
47
|
return getAudioOutputDevices();
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
51
|
* Select a device.
|
|
46
52
|
*
|
|
47
|
-
* Note:
|
|
53
|
+
* Note: This method is not supported in React Native
|
|
48
54
|
*
|
|
49
55
|
* @param deviceId empty string means the system default
|
|
50
56
|
*/
|
|
51
57
|
select(deviceId: string) {
|
|
52
58
|
if (isReactNative()) {
|
|
53
|
-
throw new Error(
|
|
59
|
+
throw new Error(
|
|
60
|
+
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
61
|
+
);
|
|
54
62
|
}
|
|
55
63
|
this.state.setDevice(deviceId);
|
|
56
64
|
}
|
|
@@ -63,11 +71,13 @@ export class SpeakerManager {
|
|
|
63
71
|
* Set the volume of the audio elements
|
|
64
72
|
* @param volume a number between 0 and 1.
|
|
65
73
|
*
|
|
66
|
-
* Note:
|
|
74
|
+
* Note: This method is not supported in React Native
|
|
67
75
|
*/
|
|
68
76
|
setVolume(volume: number) {
|
|
69
77
|
if (isReactNative()) {
|
|
70
|
-
throw new Error(
|
|
78
|
+
throw new Error(
|
|
79
|
+
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
80
|
+
);
|
|
71
81
|
}
|
|
72
82
|
if (volume && (volume < 0 || volume > 1)) {
|
|
73
83
|
throw new Error('Volume must be between 0 and 1');
|
|
@@ -78,14 +88,16 @@ export class SpeakerManager {
|
|
|
78
88
|
/**
|
|
79
89
|
* Set the volume of a participant.
|
|
80
90
|
*
|
|
81
|
-
* Note:
|
|
91
|
+
* Note: This method is not supported in React Native.
|
|
82
92
|
*
|
|
83
93
|
* @param sessionId the participant's session id.
|
|
84
94
|
* @param volume a number between 0 and 1. Set it to `undefined` to use the default volume.
|
|
85
95
|
*/
|
|
86
96
|
setParticipantVolume(sessionId: string, volume: number | undefined) {
|
|
87
97
|
if (isReactNative()) {
|
|
88
|
-
throw new Error(
|
|
98
|
+
throw new Error(
|
|
99
|
+
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
100
|
+
);
|
|
89
101
|
}
|
|
90
102
|
if (volume && (volume < 0 || volume > 1)) {
|
|
91
103
|
throw new Error('Volume must be between 0 and 1, or undefined');
|
|
@@ -54,7 +54,7 @@ describe('InputMediaDeviceManagerState', () => {
|
|
|
54
54
|
expect(permissionStatus.addEventListener).toHaveBeenCalled();
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it('should emit
|
|
57
|
+
it('should emit true when prompt is needed', async () => {
|
|
58
58
|
const permissionStatus: Partial<PermissionStatus> = {
|
|
59
59
|
state: 'prompt',
|
|
60
60
|
addEventListener: vi.fn(),
|
|
@@ -64,30 +64,6 @@ describe('InputMediaDeviceManagerState', () => {
|
|
|
64
64
|
// @ts-ignore - navigator is readonly, but we need to mock it
|
|
65
65
|
globalThis.navigator.permissions = { query };
|
|
66
66
|
|
|
67
|
-
const hasPermission = await new Promise((resolve) => {
|
|
68
|
-
state.hasBrowserPermission$.subscribe((v) => resolve(v));
|
|
69
|
-
});
|
|
70
|
-
expect(hasPermission).toBe(false);
|
|
71
|
-
expect(query).toHaveBeenCalledWith({ name: 'camera' });
|
|
72
|
-
expect(permissionStatus.addEventListener).toHaveBeenCalled();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should emit true when prompt is needed in Safari', async () => {
|
|
76
|
-
const permissionStatus: Partial<PermissionStatus> = {
|
|
77
|
-
state: 'prompt',
|
|
78
|
-
addEventListener: vi.fn(),
|
|
79
|
-
};
|
|
80
|
-
const query = vi.fn(() => Promise.resolve(permissionStatus));
|
|
81
|
-
globalThis.navigator ??= { userAgent: 'safari' } as Navigator;
|
|
82
|
-
// @ts-ignore - navigator is readonly, but we need to mock it
|
|
83
|
-
globalThis.navigator.permissions = { query };
|
|
84
|
-
|
|
85
|
-
Object.defineProperty(globalThis.navigator, 'userAgent', {
|
|
86
|
-
get() {
|
|
87
|
-
return 'safari';
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
|
|
91
67
|
const hasPermission = await new Promise((resolve) => {
|
|
92
68
|
state.hasBrowserPermission$.subscribe((v) => resolve(v));
|
|
93
69
|
});
|
package/src/events/call.ts
CHANGED
|
@@ -56,12 +56,12 @@ export const watchCallRejected = (call: Call) => {
|
|
|
56
56
|
.every((m) => rejectedBy[m.user_id]);
|
|
57
57
|
if (everyoneElseRejected) {
|
|
58
58
|
call.logger('info', 'everyone rejected, leaving the call');
|
|
59
|
-
await call.leave();
|
|
59
|
+
await call.leave({ reason: 'ring: everyone rejected' });
|
|
60
60
|
}
|
|
61
61
|
} else {
|
|
62
62
|
if (rejectedBy[eventCall.created_by.id]) {
|
|
63
63
|
call.logger('info', 'call creator rejected, leaving call');
|
|
64
|
-
await call.leave();
|
|
64
|
+
await call.leave({ reason: 'ring: creator rejected' });
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
};
|
|
@@ -78,7 +78,7 @@ export const watchCallEnded = (call: Call) => {
|
|
|
78
78
|
callingState === CallingState.JOINED ||
|
|
79
79
|
callingState === CallingState.JOINING
|
|
80
80
|
) {
|
|
81
|
-
await call.leave();
|
|
81
|
+
await call.leave({ reason: 'call.ended event received' });
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
};
|
package/src/events/internal.ts
CHANGED
|
@@ -69,7 +69,7 @@ export const watchLiveEnded = (dispatcher: Dispatcher, call: Call) => {
|
|
|
69
69
|
if (e.error && e.error.code !== ErrorCode.LIVE_ENDED) return;
|
|
70
70
|
|
|
71
71
|
if (!call.permissionsContext.hasPermission(OwnCapability.JOIN_BACKSTAGE)) {
|
|
72
|
-
call.leave().catch((err) => {
|
|
72
|
+
call.leave({ reason: 'live ended' }).catch((err) => {
|
|
73
73
|
logger('error', 'Failed to leave call after live ended', err);
|
|
74
74
|
});
|
|
75
75
|
}
|
package/src/store/CallState.ts
CHANGED
|
@@ -97,6 +97,15 @@ export enum CallingState {
|
|
|
97
97
|
OFFLINE = 'offline',
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Returns the default egress object - when no egress data is available.
|
|
102
|
+
*/
|
|
103
|
+
const defaultEgress: EgressResponse = {
|
|
104
|
+
broadcasting: false,
|
|
105
|
+
hls: { playlist_url: '' },
|
|
106
|
+
rtmps: [],
|
|
107
|
+
};
|
|
108
|
+
|
|
100
109
|
/**
|
|
101
110
|
* Holds the state of the current call.
|
|
102
111
|
* @react You don't have to use this class directly, as we are exposing the state through Hooks.
|
|
@@ -978,15 +987,15 @@ export class CallState {
|
|
|
978
987
|
};
|
|
979
988
|
|
|
980
989
|
private updateFromHLSBroadcastStopped = () => {
|
|
981
|
-
this.setCurrentValue(this.egressSubject, (egress) => ({
|
|
982
|
-
...egress
|
|
990
|
+
this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
|
|
991
|
+
...egress,
|
|
983
992
|
broadcasting: false,
|
|
984
993
|
}));
|
|
985
994
|
};
|
|
986
995
|
|
|
987
996
|
private updateFromHLSBroadcastingFailed = () => {
|
|
988
|
-
this.setCurrentValue(this.egressSubject, (egress) => ({
|
|
989
|
-
...egress
|
|
997
|
+
this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
|
|
998
|
+
...egress,
|
|
990
999
|
broadcasting: false,
|
|
991
1000
|
}));
|
|
992
1001
|
};
|
|
@@ -994,11 +1003,11 @@ export class CallState {
|
|
|
994
1003
|
private updateFromHLSBroadcastStarted = (
|
|
995
1004
|
event: CallHLSBroadcastingStartedEvent,
|
|
996
1005
|
) => {
|
|
997
|
-
this.setCurrentValue(this.egressSubject, (egress) => ({
|
|
998
|
-
...egress
|
|
1006
|
+
this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
|
|
1007
|
+
...egress,
|
|
999
1008
|
broadcasting: true,
|
|
1000
1009
|
hls: {
|
|
1001
|
-
...egress
|
|
1010
|
+
...egress.hls,
|
|
1002
1011
|
playlist_url: event.hls_playlist_url,
|
|
1003
1012
|
},
|
|
1004
1013
|
}));
|
package/src/store/stateStore.ts
CHANGED
|
@@ -28,9 +28,11 @@ export class StreamVideoWriteableStateStore {
|
|
|
28
28
|
if (call.state.callingState === CallingState.LEFT) continue;
|
|
29
29
|
|
|
30
30
|
logger('info', `User disconnected, leaving call: ${call.cid}`);
|
|
31
|
-
await call
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
await call
|
|
32
|
+
.leave({ reason: 'client.disconnectUser() called' })
|
|
33
|
+
.catch((err) => {
|
|
34
|
+
logger('error', `Error leaving call: ${call.cid}`, err);
|
|
35
|
+
});
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
});
|
package/src/types.ts
CHANGED
|
@@ -173,6 +173,12 @@ export type CallLeaveOptions = {
|
|
|
173
173
|
* @default `false`.
|
|
174
174
|
*/
|
|
175
175
|
reject?: boolean;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The reason for leaving the call.
|
|
179
|
+
* This will be sent to the backend and will be visible in the logs.
|
|
180
|
+
*/
|
|
181
|
+
reason?: string;
|
|
176
182
|
};
|
|
177
183
|
|
|
178
184
|
/**
|