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.
Files changed (125) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +103 -222
  3. package/dist/index.cjs +7500 -2163
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +4203 -273
  6. package/dist/index.js +7521 -2184
  7. package/dist/index.js.map +1 -1
  8. package/package.json +85 -78
  9. package/src/ProducerClient/producerClientEmits/joinRoomClient.ts +57 -0
  10. package/src/ProducerClient/producerClientEmits/updateRoomParametersClient.ts +401 -0
  11. package/src/consumers/addVideosGrid.ts +3 -2
  12. package/src/consumers/changeVids.ts +111 -41
  13. package/src/consumers/checkPermission.ts +35 -1
  14. package/src/consumers/connectRecvTransport.ts +42 -1
  15. package/src/consumers/consumerResume.ts +2 -2
  16. package/src/consumers/dispStreams.ts +83 -37
  17. package/src/consumers/frameworkConsumerContract.ts +6 -0
  18. package/src/consumers/generatePageContent.ts +24 -10
  19. package/src/consumers/getPipedProducersAlt.ts +112 -16
  20. package/src/consumers/gridLayout/addVideosGrid.engine.ts +42 -0
  21. package/src/consumers/gridLayout/prepopulateUserMedia.engine.ts +444 -0
  22. package/src/consumers/mixStreams.ts +45 -14
  23. package/src/consumers/onScreenChanges.ts +25 -10
  24. package/src/consumers/prepopulateUserMedia.ts +3 -2
  25. package/src/consumers/processConsumerTransports.ts +68 -23
  26. package/src/consumers/processConsumerTransportsAudio.ts +53 -16
  27. package/src/consumers/reUpdateInter.ts +61 -21
  28. package/src/consumers/readjust.ts +30 -14
  29. package/src/consumers/reorderStreams.ts +76 -42
  30. package/src/consumers/resumePauseAudioStreams.ts +66 -17
  31. package/src/consumers/resumePauseStreams.ts +53 -10
  32. package/src/consumers/socketReceiveMethods/joinConsumeRoom.ts +8 -0
  33. package/src/consumers/socketReceiveMethods/newPipeProducer.ts +114 -0
  34. package/src/consumers/socketReceiveMethods/producerClosed.ts +13 -0
  35. package/src/consumers/streamSuccessScreen.ts +2 -2
  36. package/src/consumers/streamSuccessVideo.ts +5 -0
  37. package/src/consumers/translationConsumerSwitch.ts +299 -0
  38. package/src/index.ts +85 -1
  39. package/src/methods/coHostMethods/modifyCoHostSettings.ts +9 -9
  40. package/src/methods/displaySettings/modifyDisplaySettings.ts +5 -0
  41. package/src/methods/index.ts +66 -0
  42. package/src/methods/message/sendMessage.ts +12 -29
  43. package/src/methods/panelists/focusPanelists.ts +83 -0
  44. package/src/methods/panelists/index.ts +3 -0
  45. package/src/methods/panelists/launchPanelists.ts +13 -0
  46. package/src/methods/panelists/updatePanelists.ts +135 -0
  47. package/src/methods/permissions/index.ts +3 -0
  48. package/src/methods/permissions/launchPermissions.ts +13 -0
  49. package/src/methods/permissions/updateParticipantPermission.ts +127 -0
  50. package/src/methods/permissions/updatePermissionConfig.ts +52 -0
  51. package/src/methods/polls/pollUpdated.ts +88 -0
  52. package/src/methods/recording/confirmRecording.ts +15 -12
  53. package/src/methods/recording/recordResumeTimer.ts +2 -2
  54. package/src/methods/recording/recordStartTimer.ts +2 -2
  55. package/src/methods/recording/timeLeftRecording.ts +25 -0
  56. package/src/methods/requests/hostRequestResponse.ts +153 -0
  57. package/src/methods/settings/modifySettings.ts +17 -17
  58. package/src/methods/socketReceive/allMembers.ts +450 -0
  59. package/src/methods/socketReceive/allMembersRest.ts +480 -0
  60. package/src/methods/socketReceive/allWaitingRoomMembers.ts +35 -0
  61. package/src/methods/socketReceive/banParticipant.ts +73 -0
  62. package/src/methods/socketReceive/controlMediaHost.ts +280 -0
  63. package/src/methods/socketReceive/disconnect.ts +40 -0
  64. package/src/methods/socketReceive/disconnectUserSelf.ts +56 -0
  65. package/src/methods/socketReceive/getDomains.ts +112 -0
  66. package/src/methods/socketReceive/meetingEnded.ts +49 -0
  67. package/src/methods/socketReceive/meetingStillThere.ts +26 -0
  68. package/src/methods/socketReceive/panelistReceiveMethods.ts +195 -0
  69. package/src/methods/socketReceive/participantRequested.ts +48 -0
  70. package/src/methods/socketReceive/permissionReceiveMethods.ts +59 -0
  71. package/src/methods/socketReceive/personJoined.ts +35 -0
  72. package/src/methods/socketReceive/producerMediaClosed.ts +223 -0
  73. package/src/methods/socketReceive/producerMediaPaused.ts +267 -0
  74. package/src/methods/socketReceive/producerMediaResumed.ts +157 -0
  75. package/src/methods/socketReceive/reInitiateRecording.ts +53 -0
  76. package/src/methods/socketReceive/receiveMessage.ts +117 -0
  77. package/src/methods/socketReceive/recordingNotice.ts +286 -0
  78. package/src/methods/socketReceive/roomRecordParams.ts +122 -0
  79. package/src/methods/socketReceive/screenProducerId.ts +61 -0
  80. package/src/methods/socketReceive/startRecords.ts +46 -0
  81. package/src/methods/socketReceive/stoppedRecording.ts +44 -0
  82. package/src/methods/socketReceive/translationReceiveMethods.ts +581 -0
  83. package/src/methods/socketReceive/updateConsumingDomains.ts +128 -0
  84. package/src/methods/socketReceive/updateMediaSettings.ts +45 -0
  85. package/src/methods/socketReceive/updatedCoHost.ts +75 -0
  86. package/src/methods/socketReceive/userWaiting.ts +45 -0
  87. package/src/methods/stream/clickAudio.ts +380 -0
  88. package/src/methods/stream/clickChat.ts +36 -0
  89. package/src/methods/stream/clickScreenShare.ts +173 -0
  90. package/src/methods/stream/clickVideo.ts +22 -5
  91. package/src/methods/stream/index.ts +1 -0
  92. package/src/methods/utils/SoundPlayer.ts +31 -0
  93. package/src/methods/utils/checkLimitsAndMakeRequest.ts +156 -2
  94. package/src/methods/utils/createResponseJoinRoom.ts +47 -0
  95. package/src/methods/utils/createRoomOnMediaSFU.ts +160 -0
  96. package/src/methods/utils/formatNumber.ts +42 -0
  97. package/src/methods/utils/generateRandomMessages.ts +70 -0
  98. package/src/methods/utils/generateRandomParticipants.ts +100 -0
  99. package/src/methods/utils/generateRandomPolls.ts +43 -0
  100. package/src/methods/utils/generateRandomRequestList.ts +51 -0
  101. package/src/methods/utils/generateRandomWaitingRoomList.ts +17 -0
  102. package/src/methods/utils/getModalPosition.ts +23 -0
  103. package/src/methods/utils/getOverlayPosition.ts +37 -0
  104. package/src/methods/utils/initialValuesState.ts +405 -0
  105. package/src/methods/utils/joinRoomOnMediaSFU.ts +124 -0
  106. package/src/methods/utils/liveSubtitle.ts +107 -0
  107. package/src/methods/utils/meetingTimeRemaining.ts +33 -0
  108. package/src/methods/utils/meetingTimer/startMeetingProgressTimer.ts +72 -0
  109. package/src/methods/utils/producer/aParams.ts +10 -0
  110. package/src/methods/utils/producer/hParams.ts +26 -0
  111. package/src/methods/utils/producer/screenParams.ts +13 -0
  112. package/src/methods/utils/producer/vParams.ts +26 -0
  113. package/src/methods/utils/producer/videoCaptureConstraints.ts +65 -0
  114. package/src/methods/utils/resolveMediaSFURoomApi.ts +16 -0
  115. package/src/methods/utils/sleep.ts +24 -0
  116. package/src/methods/utils/translationLanguages.ts +308 -0
  117. package/src/methods/utils/webrtc.ts +44 -0
  118. package/src/methods/welcome/handleWelcomeRequest.ts +11 -2
  119. package/src/methods/welcome/index.ts +5 -1
  120. package/src/methods/whiteboard/captureCanvasStream.ts +128 -0
  121. package/src/producers/producerEmits/joinConRoom.ts +2 -2
  122. package/src/producers/producerEmits/joinLocalRoom.ts +240 -0
  123. package/src/producers/producerEmits/joinRoom.ts +129 -0
  124. package/src/types/shared-base-types.ts +14 -3
  125. 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!.getVideoTracks()[0].enabled = false;
177
- updateLocalStream(localStream);
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 mediaDevices
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 mediaDevices
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 mediaDevices
336
+ await resolvedMediaDevices
320
337
  .getUserMedia(altMediaConstraints)
321
338
  .then(async (stream) => {
322
339
  await streamSuccessVideo({ stream, parameters });
@@ -1,3 +1,4 @@
1
1
  export * from './switchAudio'
2
2
  export * from './switchVideo'
3
3
  export * from './switchVideoAlt'
4
+ export * from './clickChat'
@@ -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 && socket instanceof Socket && socket.id) {
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
+ };