mediasfu-shared 1.0.1 → 1.0.3
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 +7634 -2180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4203 -273
- package/dist/index.js +7663 -2209
- package/dist/index.js.map +1 -1
- package/package.json +96 -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/connectIps.ts +3 -1
- package/src/consumers/connectLocalIps.ts +3 -1
- package/src/consumers/connectRecvTransport.ts +96 -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 +146 -17
- 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 +321 -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 +158 -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 +28 -6
- 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 +28 -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) {
|
|
@@ -191,7 +208,12 @@ export const clickVideo = async ({ parameters }: ClickVideoOptions): Promise<voi
|
|
|
191
208
|
if (!videoAction && islevel !== "2" && !youAreCoHost) {
|
|
192
209
|
response = await checkPermission({
|
|
193
210
|
permissionType: "videoSetting",
|
|
194
|
-
audioSetting,
|
|
211
|
+
audioSetting,
|
|
212
|
+
videoSetting,
|
|
213
|
+
screenshareSetting,
|
|
214
|
+
chatSetting,
|
|
215
|
+
permissionConfig: parameters.permissionConfig,
|
|
216
|
+
participantLevel: islevel,
|
|
195
217
|
});
|
|
196
218
|
} else {
|
|
197
219
|
response = 0;
|
|
@@ -299,13 +321,13 @@ export const clickVideo = async ({ parameters }: ClickVideoOptions): Promise<voi
|
|
|
299
321
|
}
|
|
300
322
|
}
|
|
301
323
|
|
|
302
|
-
await
|
|
324
|
+
await resolvedMediaDevices
|
|
303
325
|
.getUserMedia(mediaConstraints)
|
|
304
326
|
.then(async (stream) => {
|
|
305
327
|
await streamSuccessVideo({ stream, parameters });
|
|
306
328
|
})
|
|
307
329
|
.catch(async () => {
|
|
308
|
-
await
|
|
330
|
+
await resolvedMediaDevices
|
|
309
331
|
.getUserMedia(altMediaConstraints)
|
|
310
332
|
.then(async (stream) => {
|
|
311
333
|
await streamSuccessVideo({ stream, parameters });
|
|
@@ -316,7 +338,7 @@ export const clickVideo = async ({ parameters }: ClickVideoOptions): Promise<voi
|
|
|
316
338
|
video: { ...vidCons },
|
|
317
339
|
audio: false,
|
|
318
340
|
};
|
|
319
|
-
await
|
|
341
|
+
await resolvedMediaDevices
|
|
320
342
|
.getUserMedia(altMediaConstraints)
|
|
321
343
|
.then(async (stream) => {
|
|
322
344
|
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
|
+
};
|