mediasfu-shared 1.0.1 → 1.0.2
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/LICENSE +21 -21
- package/README.md +103 -222
- package/dist/index.cjs +7500 -2163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4203 -273
- package/dist/index.js +7521 -2184
- package/dist/index.js.map +1 -1
- package/package.json +85 -78
- package/src/ProducerClient/producerClientEmits/joinRoomClient.ts +57 -0
- package/src/ProducerClient/producerClientEmits/updateRoomParametersClient.ts +401 -0
- package/src/consumers/addVideosGrid.ts +3 -2
- package/src/consumers/changeVids.ts +111 -41
- package/src/consumers/checkPermission.ts +35 -1
- package/src/consumers/connectRecvTransport.ts +42 -1
- package/src/consumers/consumerResume.ts +2 -2
- package/src/consumers/dispStreams.ts +83 -37
- package/src/consumers/frameworkConsumerContract.ts +6 -0
- package/src/consumers/generatePageContent.ts +24 -10
- package/src/consumers/getPipedProducersAlt.ts +112 -16
- package/src/consumers/gridLayout/addVideosGrid.engine.ts +42 -0
- package/src/consumers/gridLayout/prepopulateUserMedia.engine.ts +444 -0
- package/src/consumers/mixStreams.ts +45 -14
- package/src/consumers/onScreenChanges.ts +25 -10
- package/src/consumers/prepopulateUserMedia.ts +3 -2
- package/src/consumers/processConsumerTransports.ts +68 -23
- package/src/consumers/processConsumerTransportsAudio.ts +53 -16
- package/src/consumers/reUpdateInter.ts +61 -21
- package/src/consumers/readjust.ts +30 -14
- package/src/consumers/reorderStreams.ts +76 -42
- package/src/consumers/resumePauseAudioStreams.ts +66 -17
- package/src/consumers/resumePauseStreams.ts +53 -10
- package/src/consumers/socketReceiveMethods/joinConsumeRoom.ts +8 -0
- package/src/consumers/socketReceiveMethods/newPipeProducer.ts +114 -0
- package/src/consumers/socketReceiveMethods/producerClosed.ts +13 -0
- package/src/consumers/streamSuccessScreen.ts +2 -2
- package/src/consumers/streamSuccessVideo.ts +5 -0
- package/src/consumers/translationConsumerSwitch.ts +299 -0
- package/src/index.ts +85 -1
- package/src/methods/coHostMethods/modifyCoHostSettings.ts +9 -9
- package/src/methods/displaySettings/modifyDisplaySettings.ts +5 -0
- package/src/methods/index.ts +66 -0
- package/src/methods/message/sendMessage.ts +12 -29
- package/src/methods/panelists/focusPanelists.ts +83 -0
- package/src/methods/panelists/index.ts +3 -0
- package/src/methods/panelists/launchPanelists.ts +13 -0
- package/src/methods/panelists/updatePanelists.ts +135 -0
- package/src/methods/permissions/index.ts +3 -0
- package/src/methods/permissions/launchPermissions.ts +13 -0
- package/src/methods/permissions/updateParticipantPermission.ts +127 -0
- package/src/methods/permissions/updatePermissionConfig.ts +52 -0
- package/src/methods/polls/pollUpdated.ts +88 -0
- package/src/methods/recording/confirmRecording.ts +15 -12
- package/src/methods/recording/recordResumeTimer.ts +2 -2
- package/src/methods/recording/recordStartTimer.ts +2 -2
- package/src/methods/recording/timeLeftRecording.ts +25 -0
- package/src/methods/requests/hostRequestResponse.ts +153 -0
- package/src/methods/settings/modifySettings.ts +17 -17
- package/src/methods/socketReceive/allMembers.ts +450 -0
- package/src/methods/socketReceive/allMembersRest.ts +480 -0
- package/src/methods/socketReceive/allWaitingRoomMembers.ts +35 -0
- package/src/methods/socketReceive/banParticipant.ts +73 -0
- package/src/methods/socketReceive/controlMediaHost.ts +280 -0
- package/src/methods/socketReceive/disconnect.ts +40 -0
- package/src/methods/socketReceive/disconnectUserSelf.ts +56 -0
- package/src/methods/socketReceive/getDomains.ts +112 -0
- package/src/methods/socketReceive/meetingEnded.ts +49 -0
- package/src/methods/socketReceive/meetingStillThere.ts +26 -0
- package/src/methods/socketReceive/panelistReceiveMethods.ts +195 -0
- package/src/methods/socketReceive/participantRequested.ts +48 -0
- package/src/methods/socketReceive/permissionReceiveMethods.ts +59 -0
- package/src/methods/socketReceive/personJoined.ts +35 -0
- package/src/methods/socketReceive/producerMediaClosed.ts +223 -0
- package/src/methods/socketReceive/producerMediaPaused.ts +267 -0
- package/src/methods/socketReceive/producerMediaResumed.ts +157 -0
- package/src/methods/socketReceive/reInitiateRecording.ts +53 -0
- package/src/methods/socketReceive/receiveMessage.ts +117 -0
- package/src/methods/socketReceive/recordingNotice.ts +286 -0
- package/src/methods/socketReceive/roomRecordParams.ts +122 -0
- package/src/methods/socketReceive/screenProducerId.ts +61 -0
- package/src/methods/socketReceive/startRecords.ts +46 -0
- package/src/methods/socketReceive/stoppedRecording.ts +44 -0
- package/src/methods/socketReceive/translationReceiveMethods.ts +581 -0
- package/src/methods/socketReceive/updateConsumingDomains.ts +128 -0
- package/src/methods/socketReceive/updateMediaSettings.ts +45 -0
- package/src/methods/socketReceive/updatedCoHost.ts +75 -0
- package/src/methods/socketReceive/userWaiting.ts +45 -0
- package/src/methods/stream/clickAudio.ts +380 -0
- package/src/methods/stream/clickChat.ts +36 -0
- package/src/methods/stream/clickScreenShare.ts +173 -0
- package/src/methods/stream/clickVideo.ts +22 -5
- package/src/methods/stream/index.ts +1 -0
- package/src/methods/utils/SoundPlayer.ts +31 -0
- package/src/methods/utils/checkLimitsAndMakeRequest.ts +156 -2
- package/src/methods/utils/createResponseJoinRoom.ts +47 -0
- package/src/methods/utils/createRoomOnMediaSFU.ts +160 -0
- package/src/methods/utils/formatNumber.ts +42 -0
- package/src/methods/utils/generateRandomMessages.ts +70 -0
- package/src/methods/utils/generateRandomParticipants.ts +100 -0
- package/src/methods/utils/generateRandomPolls.ts +43 -0
- package/src/methods/utils/generateRandomRequestList.ts +51 -0
- package/src/methods/utils/generateRandomWaitingRoomList.ts +17 -0
- package/src/methods/utils/getModalPosition.ts +23 -0
- package/src/methods/utils/getOverlayPosition.ts +37 -0
- package/src/methods/utils/initialValuesState.ts +405 -0
- package/src/methods/utils/joinRoomOnMediaSFU.ts +124 -0
- package/src/methods/utils/liveSubtitle.ts +107 -0
- package/src/methods/utils/meetingTimeRemaining.ts +33 -0
- package/src/methods/utils/meetingTimer/startMeetingProgressTimer.ts +72 -0
- package/src/methods/utils/producer/aParams.ts +10 -0
- package/src/methods/utils/producer/hParams.ts +26 -0
- package/src/methods/utils/producer/screenParams.ts +13 -0
- package/src/methods/utils/producer/vParams.ts +26 -0
- package/src/methods/utils/producer/videoCaptureConstraints.ts +65 -0
- package/src/methods/utils/resolveMediaSFURoomApi.ts +16 -0
- package/src/methods/utils/sleep.ts +24 -0
- package/src/methods/utils/translationLanguages.ts +308 -0
- package/src/methods/utils/webrtc.ts +44 -0
- package/src/methods/welcome/handleWelcomeRequest.ts +11 -2
- package/src/methods/welcome/index.ts +5 -1
- package/src/methods/whiteboard/captureCanvasStream.ts +128 -0
- package/src/producers/producerEmits/joinConRoom.ts +2 -2
- package/src/producers/producerEmits/joinLocalRoom.ts +240 -0
- package/src/producers/producerEmits/joinRoom.ts +129 -0
- package/src/types/shared-base-types.ts +14 -3
- package/src/types/types.ts +255 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Socket } from 'socket.io-client';
|
|
2
|
+
import type {
|
|
3
|
+
CheckPermissionType,
|
|
4
|
+
CheckScreenShareParameters,
|
|
5
|
+
CheckScreenShareType,
|
|
6
|
+
ShowAlert,
|
|
7
|
+
StopShareScreenParameters,
|
|
8
|
+
StopShareScreenType,
|
|
9
|
+
} from '../../types/types';
|
|
10
|
+
import type { PermissionConfig } from '../permissions/updatePermissionConfig';
|
|
11
|
+
|
|
12
|
+
export interface ClickScreenShareParameters
|
|
13
|
+
extends CheckScreenShareParameters,
|
|
14
|
+
StopShareScreenParameters {
|
|
15
|
+
showAlert?: ShowAlert;
|
|
16
|
+
roomName: string;
|
|
17
|
+
member: string;
|
|
18
|
+
socket: Socket;
|
|
19
|
+
islevel: string;
|
|
20
|
+
youAreCoHost: boolean;
|
|
21
|
+
adminRestrictSetting: boolean;
|
|
22
|
+
audioSetting: string;
|
|
23
|
+
videoSetting: string;
|
|
24
|
+
screenshareSetting: string;
|
|
25
|
+
chatSetting: string;
|
|
26
|
+
permissionConfig?: PermissionConfig | null;
|
|
27
|
+
screenAction: boolean;
|
|
28
|
+
screenAlreadyOn: boolean;
|
|
29
|
+
screenRequestState: string | null;
|
|
30
|
+
screenRequestTime: number;
|
|
31
|
+
audioOnlyRoom: boolean;
|
|
32
|
+
updateRequestIntervalSeconds: number;
|
|
33
|
+
updateScreenRequestState: (state: string | null) => void;
|
|
34
|
+
updateScreenAlreadyOn: (status: boolean) => void;
|
|
35
|
+
|
|
36
|
+
checkPermission: CheckPermissionType;
|
|
37
|
+
checkScreenShare: CheckScreenShareType;
|
|
38
|
+
stopShareScreen: StopShareScreenType;
|
|
39
|
+
|
|
40
|
+
getUpdatedAllParams: () => ClickScreenShareParameters;
|
|
41
|
+
[key: string]: any;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ClickScreenShareOptions {
|
|
45
|
+
parameters: ClickScreenShareParameters;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ClickScreenShareType = (options: ClickScreenShareOptions) => Promise<void>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handles screen-share toggle flow for a participant.
|
|
52
|
+
*
|
|
53
|
+
* This helper checks room restrictions, host permission policies, request
|
|
54
|
+
* cooldowns, and then delegates to the supplied start/stop share helpers.
|
|
55
|
+
*
|
|
56
|
+
* @param options Function options containing the full runtime parameter bag.
|
|
57
|
+
* @returns A promise that resolves after the screen-share action has been processed.
|
|
58
|
+
*/
|
|
59
|
+
export const clickScreenShare = async ({ parameters }: ClickScreenShareOptions): Promise<void> => {
|
|
60
|
+
let {
|
|
61
|
+
showAlert,
|
|
62
|
+
roomName,
|
|
63
|
+
member,
|
|
64
|
+
socket,
|
|
65
|
+
islevel,
|
|
66
|
+
youAreCoHost,
|
|
67
|
+
adminRestrictSetting,
|
|
68
|
+
audioSetting,
|
|
69
|
+
videoSetting,
|
|
70
|
+
screenshareSetting,
|
|
71
|
+
chatSetting,
|
|
72
|
+
screenAction,
|
|
73
|
+
screenAlreadyOn,
|
|
74
|
+
screenRequestState,
|
|
75
|
+
screenRequestTime,
|
|
76
|
+
audioOnlyRoom,
|
|
77
|
+
updateRequestIntervalSeconds,
|
|
78
|
+
updateScreenRequestState,
|
|
79
|
+
updateScreenAlreadyOn,
|
|
80
|
+
checkPermission,
|
|
81
|
+
checkScreenShare,
|
|
82
|
+
stopShareScreen,
|
|
83
|
+
} = parameters;
|
|
84
|
+
|
|
85
|
+
if (audioOnlyRoom) {
|
|
86
|
+
showAlert?.({
|
|
87
|
+
message: 'You cannot turn on your camera in an audio-only event.',
|
|
88
|
+
type: 'danger',
|
|
89
|
+
duration: 3000,
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (screenAlreadyOn) {
|
|
95
|
+
screenAlreadyOn = false;
|
|
96
|
+
updateScreenAlreadyOn(screenAlreadyOn);
|
|
97
|
+
await stopShareScreen({ parameters });
|
|
98
|
+
} else {
|
|
99
|
+
if (adminRestrictSetting) {
|
|
100
|
+
showAlert?.({
|
|
101
|
+
message: 'You cannot start screen share. Access denied by host.',
|
|
102
|
+
type: 'danger',
|
|
103
|
+
duration: 3000,
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let response = 2;
|
|
109
|
+
if (!screenAction && islevel !== '2' && !youAreCoHost) {
|
|
110
|
+
response = await checkPermission({
|
|
111
|
+
permissionType: 'screenshareSetting',
|
|
112
|
+
audioSetting,
|
|
113
|
+
videoSetting,
|
|
114
|
+
screenshareSetting,
|
|
115
|
+
chatSetting,
|
|
116
|
+
permissionConfig: parameters.permissionConfig,
|
|
117
|
+
participantLevel: islevel,
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
response = 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
switch (response) {
|
|
124
|
+
case 0:
|
|
125
|
+
checkScreenShare({ parameters });
|
|
126
|
+
break;
|
|
127
|
+
case 1: {
|
|
128
|
+
if (screenRequestState === 'pending') {
|
|
129
|
+
showAlert?.({
|
|
130
|
+
message: 'A request is already pending. Please wait for the host to respond.',
|
|
131
|
+
type: 'danger',
|
|
132
|
+
duration: 3000,
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
screenRequestState === 'rejected' &&
|
|
139
|
+
Date.now() - screenRequestTime < updateRequestIntervalSeconds
|
|
140
|
+
) {
|
|
141
|
+
showAlert?.({
|
|
142
|
+
message: 'You cannot send another request at this time.',
|
|
143
|
+
type: 'danger',
|
|
144
|
+
duration: 3000,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
showAlert?.({
|
|
150
|
+
message: 'Your request has been sent to the host.',
|
|
151
|
+
type: 'success',
|
|
152
|
+
duration: 3000,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
screenRequestState = 'pending';
|
|
156
|
+
updateScreenRequestState(screenRequestState);
|
|
157
|
+
|
|
158
|
+
const userRequest = { id: socket.id, name: member, icon: 'fa-desktop' };
|
|
159
|
+
socket.emit('participantRequest', { userRequest, roomName });
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 2:
|
|
163
|
+
showAlert?.({
|
|
164
|
+
message: 'You are not allowed to start screen share.',
|
|
165
|
+
type: 'danger',
|
|
166
|
+
duration: 3000,
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
@@ -148,6 +148,21 @@ export const clickVideo = async ({ parameters }: ClickVideoOptions): Promise<voi
|
|
|
148
148
|
checkPermission,
|
|
149
149
|
} = parameters;
|
|
150
150
|
|
|
151
|
+
const resolvedMediaDevices =
|
|
152
|
+
typeof mediaDevices?.getUserMedia === "function"
|
|
153
|
+
? mediaDevices
|
|
154
|
+
: globalThis.navigator?.mediaDevices;
|
|
155
|
+
|
|
156
|
+
if (typeof resolvedMediaDevices?.getUserMedia !== "function") {
|
|
157
|
+
showAlert?.({
|
|
158
|
+
message:
|
|
159
|
+
"Camera access is unavailable in this browser session. Please refresh and try again.",
|
|
160
|
+
type: "danger",
|
|
161
|
+
duration: 3000,
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
151
166
|
if (audioOnlyRoom) {
|
|
152
167
|
showAlert?.({
|
|
153
168
|
message: "You cannot turn on your camera in an audio-only event.",
|
|
@@ -173,8 +188,10 @@ export const clickVideo = async ({ parameters }: ClickVideoOptions): Promise<voi
|
|
|
173
188
|
|
|
174
189
|
videoAlreadyOn = false;
|
|
175
190
|
updateVideoAlreadyOn(videoAlreadyOn);
|
|
176
|
-
localStream
|
|
177
|
-
|
|
191
|
+
if (localStream && localStream.getVideoTracks().length > 0) {
|
|
192
|
+
localStream.getVideoTracks()[0].enabled = false;
|
|
193
|
+
updateLocalStream(localStream);
|
|
194
|
+
}
|
|
178
195
|
await disconnectSendTransportVideo({ parameters });
|
|
179
196
|
} else {
|
|
180
197
|
if (adminRestrictSetting) {
|
|
@@ -299,13 +316,13 @@ export const clickVideo = async ({ parameters }: ClickVideoOptions): Promise<voi
|
|
|
299
316
|
}
|
|
300
317
|
}
|
|
301
318
|
|
|
302
|
-
await
|
|
319
|
+
await resolvedMediaDevices
|
|
303
320
|
.getUserMedia(mediaConstraints)
|
|
304
321
|
.then(async (stream) => {
|
|
305
322
|
await streamSuccessVideo({ stream, parameters });
|
|
306
323
|
})
|
|
307
324
|
.catch(async () => {
|
|
308
|
-
await
|
|
325
|
+
await resolvedMediaDevices
|
|
309
326
|
.getUserMedia(altMediaConstraints)
|
|
310
327
|
.then(async (stream) => {
|
|
311
328
|
await streamSuccessVideo({ stream, parameters });
|
|
@@ -316,7 +333,7 @@ export const clickVideo = async ({ parameters }: ClickVideoOptions): Promise<voi
|
|
|
316
333
|
video: { ...vidCons },
|
|
317
334
|
audio: false,
|
|
318
335
|
};
|
|
319
|
-
await
|
|
336
|
+
await resolvedMediaDevices
|
|
320
337
|
.getUserMedia(altMediaConstraints)
|
|
321
338
|
.then(async (stream) => {
|
|
322
339
|
await streamSuccessVideo({ stream, parameters });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface SoundPlayerOptions {
|
|
2
|
+
soundUrl: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type SoundPlayerType = (options: SoundPlayerOptions) => void | Promise<void>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Attempts to play a remote sound asset when the runtime supports browser audio playback.
|
|
9
|
+
*
|
|
10
|
+
* @param {SoundPlayerOptions} options - The sound asset URL to play.
|
|
11
|
+
* @returns {void | Promise<void>} Completes after playback attempt starts/completes or is skipped.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* await SoundPlayer({
|
|
16
|
+
* soundUrl: 'https://www.mediasfu.com/sounds/record-progress.mp3',
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const SoundPlayer = async ({ soundUrl }: SoundPlayerOptions): Promise<void> => {
|
|
21
|
+
if (!soundUrl || typeof Audio === 'undefined') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const audio = new Audio(soundUrl);
|
|
27
|
+
await audio.play();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Error playing sound:', error);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PreJoinPageParameters } from "../../types/types";
|
|
2
|
-
import { Socket } from "socket.io-client";
|
|
2
|
+
import type { Socket } from "socket.io-client";
|
|
3
3
|
import Cookies from "universal-cookie";
|
|
4
4
|
|
|
5
5
|
|
|
@@ -7,6 +7,160 @@ const cookies = new Cookies();
|
|
|
7
7
|
const MAX_ATTEMPTS = 10; // Maximum number of unsuccessful attempts before rate limiting
|
|
8
8
|
const RATE_LIMIT_DURATION = 3 * 60 * 60 * 1000; // 3 hours in milliseconds
|
|
9
9
|
|
|
10
|
+
export interface CheckLimitsStorageAdapter {
|
|
11
|
+
getItem: (key: string) => Promise<string | null | undefined> | string | null | undefined;
|
|
12
|
+
setItem: (key: string, value: string) => Promise<void> | void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CheckLimitsAndMakeRequestWithStorageOptions {
|
|
16
|
+
apiUserName: string;
|
|
17
|
+
apiToken: string;
|
|
18
|
+
link: string;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
userName: string;
|
|
21
|
+
parameters: PreJoinPageParameters;
|
|
22
|
+
validate?: boolean;
|
|
23
|
+
storageAdapter: CheckLimitsStorageAdapter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CheckLimitsAndMakeRequestWithStorageType = (
|
|
27
|
+
options: CheckLimitsAndMakeRequestWithStorageOptions,
|
|
28
|
+
) => Promise<void>;
|
|
29
|
+
|
|
30
|
+
const readStoredNumber = async (
|
|
31
|
+
adapter: CheckLimitsStorageAdapter,
|
|
32
|
+
key: string,
|
|
33
|
+
): Promise<number> => {
|
|
34
|
+
const value = await adapter.getItem(key);
|
|
35
|
+
const parsed = parseInt((value ?? '0').toString(), 10);
|
|
36
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const writeStoredNumber = async (
|
|
40
|
+
adapter: CheckLimitsStorageAdapter,
|
|
41
|
+
key: string,
|
|
42
|
+
value: number,
|
|
43
|
+
): Promise<void> => {
|
|
44
|
+
await adapter.setItem(key, value.toString());
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const hasConnectedSocketId = (socket: unknown): socket is Socket & { id: string } => {
|
|
48
|
+
if (!socket || typeof socket !== 'object') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const candidate = socket as { id?: unknown };
|
|
53
|
+
return typeof candidate.id === 'string' && candidate.id.length > 0;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mobile/storage-adapter variant used by RN/Expo wrappers to share one canonical implementation.
|
|
58
|
+
*/
|
|
59
|
+
export const checkLimitsAndMakeRequestWithStorage: CheckLimitsAndMakeRequestWithStorageType = async ({
|
|
60
|
+
apiUserName,
|
|
61
|
+
apiToken,
|
|
62
|
+
link,
|
|
63
|
+
apiKey = '',
|
|
64
|
+
userName,
|
|
65
|
+
parameters,
|
|
66
|
+
validate = true,
|
|
67
|
+
storageAdapter,
|
|
68
|
+
}) => {
|
|
69
|
+
const TIMEOUT_DURATION = 10000; // 10 seconds
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
let unsuccessfulAttempts = await readStoredNumber(storageAdapter, 'unsuccessfulAttempts');
|
|
73
|
+
const lastRequestTimestamp = await readStoredNumber(storageAdapter, 'lastRequestTimestamp');
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
unsuccessfulAttempts >= MAX_ATTEMPTS
|
|
77
|
+
&& Date.now() - lastRequestTimestamp < RATE_LIMIT_DURATION
|
|
78
|
+
) {
|
|
79
|
+
parameters.showAlert?.({
|
|
80
|
+
message: 'Too many unsuccessful attempts. Please try again later.',
|
|
81
|
+
type: 'danger',
|
|
82
|
+
duration: 3000,
|
|
83
|
+
});
|
|
84
|
+
await writeStoredNumber(storageAdapter, 'lastRequestTimestamp', Date.now());
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (unsuccessfulAttempts >= MAX_ATTEMPTS) {
|
|
89
|
+
unsuccessfulAttempts = 0;
|
|
90
|
+
await writeStoredNumber(storageAdapter, 'unsuccessfulAttempts', unsuccessfulAttempts);
|
|
91
|
+
await writeStoredNumber(storageAdapter, 'lastRequestTimestamp', Date.now());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
parameters.updateIsLoadingModalVisible(true);
|
|
95
|
+
|
|
96
|
+
const socketPromise = parameters.connectSocket({
|
|
97
|
+
apiUserName,
|
|
98
|
+
apiKey,
|
|
99
|
+
apiToken,
|
|
100
|
+
link,
|
|
101
|
+
});
|
|
102
|
+
const timeoutPromise = new Promise<never>((_, reject) => setTimeout(
|
|
103
|
+
() => reject(new Error('Request timed out')),
|
|
104
|
+
TIMEOUT_DURATION,
|
|
105
|
+
));
|
|
106
|
+
|
|
107
|
+
const socket = await Promise.race([socketPromise, timeoutPromise]);
|
|
108
|
+
|
|
109
|
+
if (hasConnectedSocketId(socket)) {
|
|
110
|
+
unsuccessfulAttempts = 0;
|
|
111
|
+
await writeStoredNumber(storageAdapter, 'unsuccessfulAttempts', unsuccessfulAttempts);
|
|
112
|
+
await writeStoredNumber(storageAdapter, 'lastRequestTimestamp', Date.now());
|
|
113
|
+
|
|
114
|
+
if (validate) {
|
|
115
|
+
parameters.updateSocket(socket);
|
|
116
|
+
} else {
|
|
117
|
+
parameters.updateLocalSocket?.(socket);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
parameters.updateApiUserName(apiUserName);
|
|
121
|
+
parameters.updateApiToken(apiToken);
|
|
122
|
+
parameters.updateLink(link);
|
|
123
|
+
parameters.updateRoomName(apiUserName);
|
|
124
|
+
parameters.updateMember(userName);
|
|
125
|
+
if (validate) {
|
|
126
|
+
parameters.updateValidated(true);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
unsuccessfulAttempts += 1;
|
|
130
|
+
await writeStoredNumber(storageAdapter, 'unsuccessfulAttempts', unsuccessfulAttempts);
|
|
131
|
+
await writeStoredNumber(storageAdapter, 'lastRequestTimestamp', Date.now());
|
|
132
|
+
parameters.updateIsLoadingModalVisible(false);
|
|
133
|
+
|
|
134
|
+
if (unsuccessfulAttempts >= MAX_ATTEMPTS) {
|
|
135
|
+
parameters.showAlert?.({
|
|
136
|
+
message: 'Too many unsuccessful attempts. Please try again later.',
|
|
137
|
+
type: 'danger',
|
|
138
|
+
duration: 3000,
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
parameters.showAlert?.({
|
|
142
|
+
message: 'Invalid credentials.',
|
|
143
|
+
type: 'danger',
|
|
144
|
+
duration: 3000,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Error connecting to socket:', error);
|
|
150
|
+
parameters.showAlert?.({
|
|
151
|
+
message: 'Unable to connect. Check your credentials and try again.',
|
|
152
|
+
type: 'danger',
|
|
153
|
+
duration: 3000,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
let unsuccessfulAttempts = await readStoredNumber(storageAdapter, 'unsuccessfulAttempts');
|
|
157
|
+
unsuccessfulAttempts += 1;
|
|
158
|
+
await writeStoredNumber(storageAdapter, 'unsuccessfulAttempts', unsuccessfulAttempts);
|
|
159
|
+
await writeStoredNumber(storageAdapter, 'lastRequestTimestamp', Date.now());
|
|
160
|
+
parameters.updateIsLoadingModalVisible(false);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
10
164
|
|
|
11
165
|
export const checkLimitsAndMakeRequest = async ({
|
|
12
166
|
apiUserName,
|
|
@@ -63,7 +217,7 @@ export const checkLimitsAndMakeRequest = async ({
|
|
|
63
217
|
);
|
|
64
218
|
|
|
65
219
|
const socket = await Promise.race([socketPromise, timeoutPromise]);
|
|
66
|
-
if (socket
|
|
220
|
+
if (hasConnectedSocketId(socket)) {
|
|
67
221
|
unsuccessfulAttempts = 0;
|
|
68
222
|
cookies.set("unsuccessfulAttempts", unsuccessfulAttempts.toString());
|
|
69
223
|
cookies.set("lastRequestTimestamp", Date.now().toString());
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ResponseJoinLocalRoom, ResponseJoinRoom } from '../../types/types';
|
|
2
|
+
|
|
3
|
+
export interface CreateResponseJoinRoomOptions {
|
|
4
|
+
localRoom: ResponseJoinLocalRoom;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type CreateResponseJoinRoomType = (options: CreateResponseJoinRoomOptions) => Promise<ResponseJoinRoom>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts a local-room join response into the broader MediaSFU join response shape.
|
|
11
|
+
*
|
|
12
|
+
* This helper is useful when local/demo or self-hosted room flows need to be
|
|
13
|
+
* normalized to the same response contract consumed by the main runtime.
|
|
14
|
+
*
|
|
15
|
+
* @param options Response conversion options.
|
|
16
|
+
* @returns A normalized `ResponseJoinRoom` object.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const normalized = await createResponseJoinRoom({ localRoom });
|
|
21
|
+
* if (normalized.success) {
|
|
22
|
+
* console.log(normalized.meetingRoomParams);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const createResponseJoinRoom: CreateResponseJoinRoomType = async ({
|
|
27
|
+
localRoom,
|
|
28
|
+
}: CreateResponseJoinRoomOptions): Promise<ResponseJoinRoom> => {
|
|
29
|
+
return {
|
|
30
|
+
rtpCapabilities: localRoom.rtpCapabilities ?? null,
|
|
31
|
+
success: localRoom.rtpCapabilities !== null,
|
|
32
|
+
roomRecvIPs: [],
|
|
33
|
+
meetingRoomParams: localRoom.eventRoomParams,
|
|
34
|
+
recordingParams: localRoom.recordingParams,
|
|
35
|
+
secureCode: localRoom.secureCode,
|
|
36
|
+
recordOnly: false,
|
|
37
|
+
isHost: localRoom.isHost,
|
|
38
|
+
safeRoom: false,
|
|
39
|
+
autoStartSafeRoom: false,
|
|
40
|
+
safeRoomStarted: false,
|
|
41
|
+
safeRoomEnded: false,
|
|
42
|
+
reason: localRoom.isBanned ? 'User is banned from the room.' : undefined,
|
|
43
|
+
banned: localRoom.isBanned,
|
|
44
|
+
suspended: false,
|
|
45
|
+
noAdmin: localRoom.hostNotJoined,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateRoomOnMediaSFUOptions,
|
|
3
|
+
CreateRoomOnMediaSFUType,
|
|
4
|
+
PendingRequestStorage,
|
|
5
|
+
} from '../../types/types';
|
|
6
|
+
import type { CreateJoinRoomError, CreateJoinRoomResponse } from './joinRoomOnMediaSFU';
|
|
7
|
+
import { resolveMediaSFURoomApi } from './resolveMediaSFURoomApi';
|
|
8
|
+
|
|
9
|
+
const readResponseError = async (response: Response): Promise<string> => {
|
|
10
|
+
const fallbackMessage = `HTTP error! Status: ${response.status}`;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const responseText = await response.text();
|
|
14
|
+
|
|
15
|
+
if (!responseText) {
|
|
16
|
+
return fallbackMessage;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parsedResponse = JSON.parse(responseText) as { error?: string; message?: string };
|
|
20
|
+
return parsedResponse.error || parsedResponse.message || responseText;
|
|
21
|
+
} catch {
|
|
22
|
+
return fallbackMessage;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const createBrowserPendingRequestStorage = (): PendingRequestStorage | undefined => {
|
|
27
|
+
if (typeof localStorage === 'undefined') {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
getItem: async (key: string) => localStorage.getItem(key),
|
|
33
|
+
setItem: async (key: string, value: string) => {
|
|
34
|
+
localStorage.setItem(key, value);
|
|
35
|
+
},
|
|
36
|
+
removeItem: async (key: string) => {
|
|
37
|
+
localStorage.removeItem(key);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Calls the MediaSFU create-room API and guards against duplicate in-flight room requests.
|
|
44
|
+
*
|
|
45
|
+
* A short-lived `localStorage` marker is used to prevent accidental duplicate
|
|
46
|
+
* submissions while a room create request is still pending.
|
|
47
|
+
*
|
|
48
|
+
* @param options API request options including credentials and create payload.
|
|
49
|
+
* @returns A result object containing either parsed response data or an error payload.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const created = await createRoomOnMediaSFU({
|
|
54
|
+
* payload: {
|
|
55
|
+
* action: 'create',
|
|
56
|
+
* userName: 'Ada',
|
|
57
|
+
* duration: 60,
|
|
58
|
+
* capacity: 10,
|
|
59
|
+
* } as CreateMediaSFURoomOptions,
|
|
60
|
+
* apiUserName: 'sampleuser',
|
|
61
|
+
* apiKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* console.log(created.success);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export const createRoomOnMediaSFU: CreateRoomOnMediaSFUType = async ({
|
|
68
|
+
payload,
|
|
69
|
+
apiUserName,
|
|
70
|
+
apiKey,
|
|
71
|
+
localLink = '',
|
|
72
|
+
pendingRequestStorage,
|
|
73
|
+
}: CreateRoomOnMediaSFUOptions): Promise<{
|
|
74
|
+
data: CreateJoinRoomResponse | CreateJoinRoomError | null;
|
|
75
|
+
success: boolean;
|
|
76
|
+
}> => {
|
|
77
|
+
const storage = pendingRequestStorage ?? createBrowserPendingRequestStorage();
|
|
78
|
+
const roomIdentifier = `create_${payload.userName}_${payload.duration}_${payload.capacity}`;
|
|
79
|
+
|
|
80
|
+
const pendingKey = `mediasfu_pending_${roomIdentifier}`;
|
|
81
|
+
const pendingTimeout = 30 * 1000;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const pendingRequest = storage ? await storage.getItem(pendingKey) : null;
|
|
85
|
+
if (storage && pendingRequest) {
|
|
86
|
+
const pendingData = JSON.parse(pendingRequest) as { timestamp: number };
|
|
87
|
+
const timeSincePending = Date.now() - pendingData.timestamp;
|
|
88
|
+
|
|
89
|
+
if (timeSincePending < pendingTimeout) {
|
|
90
|
+
return {
|
|
91
|
+
data: { error: 'Room creation already in progress' },
|
|
92
|
+
success: false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await storage.removeItem(pendingKey);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
!apiUserName ||
|
|
101
|
+
!apiKey ||
|
|
102
|
+
apiUserName === 'yourAPIUSERNAME' ||
|
|
103
|
+
apiKey === 'yourAPIKEY' ||
|
|
104
|
+
apiKey.length !== 64 ||
|
|
105
|
+
apiUserName.length < 6
|
|
106
|
+
) {
|
|
107
|
+
return { data: { error: 'Invalid credentials' }, success: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const finalLink = resolveMediaSFURoomApi(localLink, 'createRoom');
|
|
111
|
+
|
|
112
|
+
if (storage) {
|
|
113
|
+
await storage.setItem(pendingKey, JSON.stringify({
|
|
114
|
+
timestamp: Date.now(),
|
|
115
|
+
payload: {
|
|
116
|
+
action: payload.action,
|
|
117
|
+
userName: payload.userName,
|
|
118
|
+
meetingID: 'create',
|
|
119
|
+
},
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
storage.removeItem(pendingKey).catch(() => {
|
|
124
|
+
});
|
|
125
|
+
}, pendingTimeout);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const response = await fetch(finalLink, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
Authorization: `Bearer ${apiUserName}:${apiKey}`,
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify(payload),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(await readResponseError(response));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const data: CreateJoinRoomResponse = await response.json();
|
|
142
|
+
if (storage) {
|
|
143
|
+
await storage.removeItem(pendingKey);
|
|
144
|
+
}
|
|
145
|
+
return { data, success: true };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (storage) {
|
|
148
|
+
try {
|
|
149
|
+
await storage.removeItem(pendingKey);
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const errorMessage = (error as Error).message || 'unknown error';
|
|
155
|
+
return {
|
|
156
|
+
data: { error: `Unable to create room, ${errorMessage}` },
|
|
157
|
+
success: false,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface FormatNumberOptions {
|
|
2
|
+
number: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type FormatNumberType = (options: FormatNumberOptions) => Promise<string | undefined>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Formats large numbers into compact display strings.
|
|
9
|
+
*
|
|
10
|
+
* Values are converted to `K`, `M`, or `B` suffixes for UI-friendly display.
|
|
11
|
+
* Falsy values return `undefined` to match existing MediaSFU display behavior.
|
|
12
|
+
*
|
|
13
|
+
* @param options Number formatting options.
|
|
14
|
+
* @returns A compact string such as `1.2K` or `3.4M`, or `undefined` for empty input.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const viewers = await formatNumber({ number: 1530 });
|
|
19
|
+
* // viewers === '1.5K'
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const formatNumber = async ({ number }: FormatNumberOptions): Promise<string | undefined> => {
|
|
23
|
+
if (number) {
|
|
24
|
+
if (number < 1e3) {
|
|
25
|
+
return number.toString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (number < 1e6) {
|
|
29
|
+
return `${(number / 1e3).toFixed(1)}K`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (number < 1e9) {
|
|
33
|
+
return `${(number / 1e6).toFixed(1)}M`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (number < 1e12) {
|
|
37
|
+
return `${(number / 1e9).toFixed(1)}B`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|