mediasfu-shared 1.2.7 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/ProducerClient/producerClientEmits/updateRoomParametersClient.d.ts +2 -0
- package/dist/ProducerClient/producerClientEmits/updateRoomParametersClient.d.ts.map +1 -1
- package/dist/audioProcessing-Ca5wm1nV.js +150 -0
- package/dist/audioProcessing-Ca5wm1nV.js.map +1 -0
- package/dist/audioProcessing-VCbu70Jt.cjs +149 -0
- package/dist/audioProcessing-VCbu70Jt.cjs.map +1 -0
- package/dist/consumers/closeAndResize.d.ts +3 -0
- package/dist/consumers/closeAndResize.d.ts.map +1 -1
- package/dist/consumers/connectIps.d.ts +6 -0
- package/dist/consumers/connectIps.d.ts.map +1 -1
- package/dist/consumers/connectSendTransportScreenAudio.d.ts +65 -0
- package/dist/consumers/connectSendTransportScreenAudio.d.ts.map +1 -0
- package/dist/consumers/consumerBackupPreference.d.ts +14 -0
- package/dist/consumers/consumerBackupPreference.d.ts.map +1 -0
- package/dist/consumers/disconnectSendTransportScreen.d.ts.map +1 -1
- package/dist/consumers/getPipedProducersAlt.d.ts.map +1 -1
- package/dist/consumers/index.cjs +23 -1
- package/dist/consumers/index.cjs.map +1 -1
- package/dist/consumers/index.d.ts +3 -0
- package/dist/consumers/index.d.ts.map +1 -1
- package/dist/consumers/index.js +61 -39
- package/dist/consumers/index.js.map +1 -1
- package/dist/consumers/screenAudio.d.ts +64 -0
- package/dist/consumers/screenAudio.d.ts.map +1 -0
- package/dist/consumers/socketReceiveMethods/producerClosed.d.ts.map +1 -1
- package/dist/consumers/startShareScreen.d.ts +14 -0
- package/dist/consumers/startShareScreen.d.ts.map +1 -1
- package/dist/consumers/streamSuccessScreen.d.ts.map +1 -1
- package/dist/{getParticipantMedia-c2bbdT5f.cjs → getParticipantMedia-1okyF4H7.cjs} +4 -4
- package/dist/{getParticipantMedia-c2bbdT5f.cjs.map → getParticipantMedia-1okyF4H7.cjs.map} +1 -1
- package/dist/{getParticipantMedia-DTZg8n5U.js → getParticipantMedia-DEJwXXhU.js} +4 -4
- package/dist/{getParticipantMedia-DTZg8n5U.js.map → getParticipantMedia-DEJwXXhU.js.map} +1 -1
- package/dist/index.cjs +36 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1523 -1493
- package/dist/index.native.cjs +36 -6
- package/dist/index.native.cjs.map +1 -1
- package/dist/index.native.d.ts +4 -0
- package/dist/index.native.d.ts.map +1 -1
- package/dist/index.native.js +105 -75
- package/dist/{joinLocalRoom-1dRd2PCN.js → joinLocalRoom-DS6_6lJL.js} +6 -4
- package/dist/joinLocalRoom-DS6_6lJL.js.map +1 -0
- package/dist/{joinLocalRoom-DwY-3ZsH.cjs → joinLocalRoom-Dsn39SwS.cjs} +6 -4
- package/dist/joinLocalRoom-Dsn39SwS.cjs.map +1 -0
- package/dist/{joinRoomOnMediaSFU-BnAWLciF.js → joinRoomOnMediaSFU-B6tTA0Mx.js} +17 -13
- package/dist/joinRoomOnMediaSFU-B6tTA0Mx.js.map +1 -0
- package/dist/{joinRoomOnMediaSFU-D2C7q1GK.cjs → joinRoomOnMediaSFU-CxwD9fBz.cjs} +17 -13
- package/dist/joinRoomOnMediaSFU-CxwD9fBz.cjs.map +1 -0
- package/dist/methods/index.cjs +11 -8
- package/dist/methods/index.cjs.map +1 -1
- package/dist/methods/index.js +13 -9
- package/dist/methods/index.js.map +1 -1
- package/dist/methods/socketReceive/getDomains.d.ts.map +1 -1
- package/dist/methods/utils/checkLimitsAndMakeRequest.d.ts.map +1 -1
- package/dist/methods/welcome/handleWelcomeRequest.d.ts.map +1 -1
- package/dist/sockets/SocketManager.d.ts.map +1 -1
- package/dist/sockets/mediaSocketErrors.d.ts +62 -0
- package/dist/sockets/mediaSocketErrors.d.ts.map +1 -0
- package/dist/types/index.cjs +4 -3
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +39 -38
- package/dist/types/shared-base-types.d.ts +4 -0
- package/dist/types/shared-base-types.d.ts.map +1 -1
- package/dist/{updateParticipantAudioDecibels-DiJ_LFfH.js → updateParticipantAudioDecibels-BBfdHWQ0.js} +344 -61
- package/dist/updateParticipantAudioDecibels-BBfdHWQ0.js.map +1 -0
- package/dist/{updateParticipantAudioDecibels-BJ2PxJJD.cjs → updateParticipantAudioDecibels-Dni8i6AY.cjs} +306 -23
- package/dist/updateParticipantAudioDecibels-Dni8i6AY.cjs.map +1 -0
- package/package.json +3 -2
- package/src/ProducerClient/producerClientEmits/updateRoomParametersClient.ts +4 -0
- package/src/consumers/closeAndResize.ts +14 -0
- package/src/consumers/connectIps.ts +81 -8
- package/src/consumers/connectSendTransportScreenAudio.ts +184 -0
- package/src/consumers/consumerBackupPreference.ts +43 -0
- package/src/consumers/disconnectSendTransportScreen.ts +8 -0
- package/src/consumers/getPipedProducersAlt.ts +4 -0
- package/src/consumers/index.ts +3 -0
- package/src/consumers/screenAudio.ts +132 -0
- package/src/consumers/socketReceiveMethods/producerClosed.ts +3 -0
- package/src/consumers/startShareScreen.ts +44 -8
- package/src/consumers/streamSuccessScreen.ts +11 -0
- package/src/index.native.ts +4 -0
- package/src/index.ts +4 -0
- package/src/methods/socketReceive/getDomains.ts +4 -0
- package/src/methods/utils/checkLimitsAndMakeRequest.ts +24 -11
- package/src/methods/welcome/handleWelcomeRequest.ts +15 -6
- package/src/sockets/SocketManager.ts +76 -12
- package/src/sockets/mediaSocketErrors.ts +156 -0
- package/src/types/shared-base-types.ts +4 -0
- package/dist/audioProcessing-0z5KlcmO.js +0 -35
- package/dist/audioProcessing-0z5KlcmO.js.map +0 -1
- package/dist/audioProcessing-BpbrdCk-.cjs +0 -34
- package/dist/audioProcessing-BpbrdCk-.cjs.map +0 -1
- package/dist/joinLocalRoom-1dRd2PCN.js.map +0 -1
- package/dist/joinLocalRoom-DwY-3ZsH.cjs.map +0 -1
- package/dist/joinRoomOnMediaSFU-BnAWLciF.js.map +0 -1
- package/dist/joinRoomOnMediaSFU-D2C7q1GK.cjs.map +0 -1
- package/dist/updateParticipantAudioDecibels-BJ2PxJJD.cjs.map +0 -1
- package/dist/updateParticipantAudioDecibels-DiJ_LFfH.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Transport, CloseAndResizeParameters, CloseAndResizeType } from '../../types/types';
|
|
2
|
+
import { forgetScreenAudioProducer } from '../screenAudio';
|
|
2
3
|
|
|
3
4
|
export interface ProducerClosedParameters extends CloseAndResizeParameters {
|
|
4
5
|
consumerTransports: Transport[];
|
|
@@ -63,6 +64,8 @@ export const producerClosed = async ({
|
|
|
63
64
|
}: ProducerClosedOptions): Promise<void> => {
|
|
64
65
|
let { consumerTransports, closeAndResize, screenId, updateConsumerTransports } = parameters;
|
|
65
66
|
|
|
67
|
+
forgetScreenAudioProducer(remoteProducerId);
|
|
68
|
+
|
|
66
69
|
const activeTranslationProducerIds = parameters.activeTranslationProducerIds;
|
|
67
70
|
const isTranslationProducer = activeTranslationProducerIds?.has?.(remoteProducerId);
|
|
68
71
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
|
|
2
2
|
import { StreamSuccessScreenType, StreamSuccessScreenParameters, ShowAlert } from "../types/types";
|
|
3
|
+
import {
|
|
4
|
+
buildDisplayMediaOptions,
|
|
5
|
+
canCaptureDisplayAudio,
|
|
6
|
+
shouldShareScreenAudio,
|
|
7
|
+
ScreenAudioChoice,
|
|
8
|
+
ScreenAudioMode,
|
|
9
|
+
} from "./screenAudio";
|
|
3
10
|
export interface StartShareScreenParameters extends StreamSuccessScreenParameters {
|
|
4
11
|
shared: boolean;
|
|
5
12
|
showAlert?: ShowAlert;
|
|
@@ -8,6 +15,19 @@ export interface StartShareScreenParameters extends StreamSuccessScreenParameter
|
|
|
8
15
|
onWeb: boolean;
|
|
9
16
|
targetWidth?: number;
|
|
10
17
|
targetHeight?: number;
|
|
18
|
+
/** Set from the join response: the media server accepts screen audio. */
|
|
19
|
+
supportsScreenAudio?: boolean;
|
|
20
|
+
/** Ask the browser for the screen's audio too (default true; the browser's picker still lets the user decide). */
|
|
21
|
+
screenAudioEnabled?: boolean;
|
|
22
|
+
/** `ask` (default): offer "Screen + audio" / "Screen only" first; `always`: request audio; `never`: video only. */
|
|
23
|
+
screenAudioMode?: ScreenAudioMode;
|
|
24
|
+
/**
|
|
25
|
+
* The "share audio too?" dialog: resolve `audio`, `video` or `null` (cancel). Each UI SDK supplies its own. Without
|
|
26
|
+
* one, `ask` requests audio and leaves the choice to the browser picker's own "share audio" checkbox.
|
|
27
|
+
*/
|
|
28
|
+
screenAudioPrompt?: () => Promise<ScreenAudioChoice>;
|
|
29
|
+
/** Override browser detection of display-audio capture (Chromium desktop by default). */
|
|
30
|
+
displayAudioCapable?: boolean;
|
|
11
31
|
|
|
12
32
|
// mediasfu functions
|
|
13
33
|
streamSuccessScreen: StreamSuccessScreenType;
|
|
@@ -71,6 +91,11 @@ export async function startShareScreen({ parameters }: StartShareScreenOptions):
|
|
|
71
91
|
onWeb,
|
|
72
92
|
targetWidth = 1280,
|
|
73
93
|
targetHeight = 720,
|
|
94
|
+
supportsScreenAudio,
|
|
95
|
+
screenAudioEnabled,
|
|
96
|
+
screenAudioMode = "ask",
|
|
97
|
+
screenAudioPrompt,
|
|
98
|
+
displayAudioCapable,
|
|
74
99
|
|
|
75
100
|
streamSuccessScreen,
|
|
76
101
|
} = parameters;
|
|
@@ -86,16 +111,27 @@ export async function startShareScreen({ parameters }: StartShareScreenOptions):
|
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
if (mediaDevices && mediaDevices.getDisplayMedia) {
|
|
114
|
+
// Screen audio needs a server that accepts it and a browser that can capture it (Chromium desktop).
|
|
115
|
+
let withAudio =
|
|
116
|
+
shouldShareScreenAudio({ supportsScreenAudio, screenAudioEnabled, screenAudioMode }) &&
|
|
117
|
+
(displayAudioCapable ?? canCaptureDisplayAudio());
|
|
118
|
+
if (withAudio && screenAudioMode === "ask" && screenAudioPrompt) {
|
|
119
|
+
const choice = await screenAudioPrompt();
|
|
120
|
+
if (choice === null) {
|
|
121
|
+
updateShared(false);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
withAudio = choice === "audio";
|
|
125
|
+
}
|
|
89
126
|
shared = true;
|
|
90
127
|
await mediaDevices
|
|
91
|
-
.getDisplayMedia(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
})
|
|
128
|
+
.getDisplayMedia(
|
|
129
|
+
buildDisplayMediaOptions({
|
|
130
|
+
targetWidth,
|
|
131
|
+
targetHeight,
|
|
132
|
+
withAudio,
|
|
133
|
+
})
|
|
134
|
+
)
|
|
99
135
|
.then(async (stream: MediaStream) => {
|
|
100
136
|
await streamSuccessScreen({ stream, parameters });
|
|
101
137
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
import { Socket } from "socket.io-client";
|
|
4
|
+
import { connectSendTransportScreenAudio } from "./connectSendTransportScreenAudio";
|
|
4
5
|
import {
|
|
5
6
|
SleepType, CreateSendTransportType, ConnectSendTransportScreenType, DisconnectSendTransportScreenType, StopShareScreenType, ReorderStreamsType, PrepopulateUserMediaType, RePortType,
|
|
6
7
|
ShowAlert, CreateSendTransportParameters, ConnectSendTransportScreenParameters, DisconnectSendTransportScreenParameters, StopShareScreenParameters, ReorderStreamsParameters, PrepopulateUserMediaParameters, RePortParameters,
|
|
@@ -179,6 +180,16 @@ export const streamSuccessScreen = async ({
|
|
|
179
180
|
|
|
180
181
|
// Alert the socket that you are sharing the screen
|
|
181
182
|
socket.emit("startScreenShare");
|
|
183
|
+
|
|
184
|
+
// Share the screen's audio too, when the browser captured it and the server supports it.
|
|
185
|
+
try {
|
|
186
|
+
await connectSendTransportScreenAudio({
|
|
187
|
+
stream: localStreamScreen,
|
|
188
|
+
parameters: getUpdatedAllParams() as any,
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.log("Screen audio not shared:", error);
|
|
192
|
+
}
|
|
182
193
|
} catch (error: any) {
|
|
183
194
|
showAlert?.({
|
|
184
195
|
message: error.message,
|
package/src/index.native.ts
CHANGED
|
@@ -17,6 +17,9 @@ export * from './consumers/connectSendTransport';
|
|
|
17
17
|
export * from './consumers/connectSendTransportAudio';
|
|
18
18
|
export * from './consumers/consumerResume';
|
|
19
19
|
export * from './consumers/connectSendTransportScreen';
|
|
20
|
+
export * from './consumers/connectSendTransportScreenAudio';
|
|
21
|
+
export * from './consumers/screenAudio';
|
|
22
|
+
export * from './consumers/consumerBackupPreference';
|
|
20
23
|
export * from './consumers/connectSendTransportVideo';
|
|
21
24
|
export * from './consumers/controlMedia';
|
|
22
25
|
export * from './consumers/createSendTransport';
|
|
@@ -217,6 +220,7 @@ export * from './producers/producerEmits/joinLocalRoom';
|
|
|
217
220
|
export * from './producers/producerEmits/joinRoom';
|
|
218
221
|
|
|
219
222
|
export * from './sockets/SocketManager';
|
|
223
|
+
export * from './sockets/mediaSocketErrors';
|
|
220
224
|
|
|
221
225
|
export * from './methods/stream/clickVideo';
|
|
222
226
|
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,9 @@ export * from './consumers/connectSendTransport';
|
|
|
20
20
|
export * from './consumers/connectSendTransportAudio';
|
|
21
21
|
export * from './consumers/consumerResume';
|
|
22
22
|
export * from './consumers/connectSendTransportScreen';
|
|
23
|
+
export * from './consumers/connectSendTransportScreenAudio';
|
|
24
|
+
export * from './consumers/screenAudio';
|
|
25
|
+
export * from './consumers/consumerBackupPreference';
|
|
23
26
|
export * from './consumers/connectSendTransportVideo';
|
|
24
27
|
export * from './consumers/controlMedia';
|
|
25
28
|
export * from './consumers/createSendTransport';
|
|
@@ -222,6 +225,7 @@ export * from './producers/producerEmits/joinLocalRoom';
|
|
|
222
225
|
export * from './producers/producerEmits/joinRoom';
|
|
223
226
|
// Socket exports
|
|
224
227
|
export * from './sockets/SocketManager';
|
|
228
|
+
export * from './sockets/mediaSocketErrors';
|
|
225
229
|
// Stream method exports (clickVideo)
|
|
226
230
|
export * from './methods/stream/clickVideo';
|
|
227
231
|
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AltDomains,
|
|
4
4
|
ConsumeSocket,
|
|
5
5
|
} from '../../types/types';
|
|
6
|
+
import { rememberConsumerBackups } from "../../consumers/consumerBackupPreference";
|
|
6
7
|
|
|
7
8
|
export interface ConnectIpsLikeOptions<
|
|
8
9
|
TParameters = unknown,
|
|
@@ -88,6 +89,9 @@ export const getDomains = async <
|
|
|
88
89
|
let { roomRecvIPs, consume_sockets, connectIps } = parameters;
|
|
89
90
|
const ipsToConnect: string[] = [];
|
|
90
91
|
|
|
92
|
+
// Busy node -> backup map: new streams are preferred from the backup (consumer-backup-v1).
|
|
93
|
+
rememberConsumerBackups(alt_domains);
|
|
94
|
+
|
|
91
95
|
try {
|
|
92
96
|
consume_sockets = parameters.getUpdatedAllParams().consume_sockets;
|
|
93
97
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { PreJoinPageParameters } from "../../types/types";
|
|
2
|
+
import {
|
|
3
|
+
describeMediaSocketError,
|
|
4
|
+
isMediaSocketConnectError,
|
|
5
|
+
isNetworkOnlyMediaSocketError,
|
|
6
|
+
} from "../../sockets/mediaSocketErrors";
|
|
2
7
|
import type { Socket } from "socket.io-client";
|
|
3
8
|
import Cookies from "universal-cookie";
|
|
4
9
|
|
|
@@ -148,15 +153,19 @@ export const checkLimitsAndMakeRequestWithStorage: CheckLimitsAndMakeRequestWith
|
|
|
148
153
|
} catch (error) {
|
|
149
154
|
console.error('Error connecting to socket:', error);
|
|
150
155
|
parameters.showAlert?.({
|
|
151
|
-
message:
|
|
156
|
+
message: describeMediaSocketError(error),
|
|
152
157
|
type: 'danger',
|
|
153
|
-
duration: 3000,
|
|
158
|
+
duration: isMediaSocketConnectError(error) ? 6000 : 3000,
|
|
154
159
|
});
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
// Only a refusal or an unknown failure counts toward the attempt limit; a device/network problem is not a
|
|
162
|
+
// credential guess.
|
|
163
|
+
if (!isNetworkOnlyMediaSocketError(error)) {
|
|
164
|
+
let unsuccessfulAttempts = await readStoredNumber(storageAdapter, 'unsuccessfulAttempts');
|
|
165
|
+
unsuccessfulAttempts += 1;
|
|
166
|
+
await writeStoredNumber(storageAdapter, 'unsuccessfulAttempts', unsuccessfulAttempts);
|
|
167
|
+
await writeStoredNumber(storageAdapter, 'lastRequestTimestamp', Date.now());
|
|
168
|
+
}
|
|
160
169
|
parameters.updateIsLoadingModalVisible(false);
|
|
161
170
|
}
|
|
162
171
|
};
|
|
@@ -244,14 +253,18 @@ export const checkLimitsAndMakeRequest = async ({
|
|
|
244
253
|
duration: 3000,
|
|
245
254
|
});
|
|
246
255
|
}
|
|
247
|
-
} catch {
|
|
256
|
+
} catch (error) {
|
|
248
257
|
parameters.showAlert?.({
|
|
249
|
-
message:
|
|
258
|
+
message: describeMediaSocketError(error),
|
|
250
259
|
type: "danger",
|
|
251
|
-
duration: 3000,
|
|
260
|
+
duration: isMediaSocketConnectError(error) ? 6000 : 3000,
|
|
252
261
|
});
|
|
253
|
-
|
|
254
|
-
|
|
262
|
+
// Only a refusal or an unknown failure counts toward the attempt limit; a device/network problem is not a
|
|
263
|
+
// credential guess.
|
|
264
|
+
if (!isNetworkOnlyMediaSocketError(error)) {
|
|
265
|
+
unsuccessfulAttempts += 1;
|
|
266
|
+
cookies.set("unsuccessfulAttempts", unsuccessfulAttempts.toString());
|
|
267
|
+
}
|
|
255
268
|
parameters.updateIsLoadingModalVisible(false);
|
|
256
269
|
}
|
|
257
270
|
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Socket } from 'socket.io-client';
|
|
2
|
+
import {
|
|
3
|
+
describeMediaSocketError,
|
|
4
|
+
isMediaSocketConnectError,
|
|
5
|
+
isNetworkOnlyMediaSocketError,
|
|
6
|
+
} from '../../sockets/mediaSocketErrors';
|
|
2
7
|
import Cookies from 'universal-cookie';
|
|
3
8
|
import type { PreJoinPageParameters } from '../../types/types';
|
|
4
9
|
|
|
@@ -183,16 +188,20 @@ export async function handleWelcomeRequest({
|
|
|
183
188
|
});
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
|
-
} catch {
|
|
191
|
+
} catch (error) {
|
|
187
192
|
showAlert?.({
|
|
188
|
-
message:
|
|
193
|
+
message: describeMediaSocketError(error),
|
|
189
194
|
type: 'danger',
|
|
190
|
-
duration: 3000,
|
|
195
|
+
duration: isMediaSocketConnectError(error) ? 6000 : 3000,
|
|
191
196
|
});
|
|
192
197
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
// Only a refusal or an unknown failure counts toward the attempt limit; a device/network problem is not a
|
|
199
|
+
// credential guess.
|
|
200
|
+
if (!isNetworkOnlyMediaSocketError(error)) {
|
|
201
|
+
unsuccessfulAttempts += 1;
|
|
202
|
+
cookies.set('unsuccessfulAttempts', unsuccessfulAttempts.toString());
|
|
203
|
+
cookies.set('lastRequestTimestamp', Date.now().toString());
|
|
204
|
+
}
|
|
196
205
|
updateIsLoadingModalVisible(false);
|
|
197
206
|
}
|
|
198
207
|
}
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
// Socket manager for media socket.
|
|
3
3
|
import { MeetingRoomParams, RecordingParams } from '../types/types';
|
|
4
4
|
import io, { Socket } from 'socket.io-client'; // Importing socket type
|
|
5
|
+
import {
|
|
6
|
+
MediaSocketConnectError,
|
|
7
|
+
MediaSocketConnectPhase,
|
|
8
|
+
MediaConnectionRejectionReason,
|
|
9
|
+
currentOnlineState,
|
|
10
|
+
normalizeRejectionReason,
|
|
11
|
+
} from './mediaSocketErrors';
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* Validates the provided API key or token.
|
|
@@ -62,6 +69,10 @@ export type ConnectLocalSocketType = (options: ConnectLocalSocketOptions) => Pro
|
|
|
62
69
|
* @param {string} options.link - The socket link.
|
|
63
70
|
*
|
|
64
71
|
* @returns {Promise<Socket>} A promise that resolves to the connected socket.
|
|
72
|
+
* @throws {MediaSocketConnectError} When the connection fails. `phase` says where (`before-open`, `after-open`,
|
|
73
|
+
* `rejected`, `timeout`), `reason`/`stage` carry the server's `connection-rejected` verdict, `elapsedMs` and `online`
|
|
74
|
+
* help tell a device/network problem from a refusal, and `retryable` is false for rejections that cannot succeed
|
|
75
|
+
* with the same credentials. `describeMediaSocketError(error)` turns it into a user-facing sentence.
|
|
65
76
|
*
|
|
66
77
|
* @example
|
|
67
78
|
* ```typescript
|
|
@@ -135,16 +146,32 @@ async function connectSocket(
|
|
|
135
146
|
});
|
|
136
147
|
}
|
|
137
148
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
149
|
+
// Every path below runs at most once, clears handshake listeners/timers,
|
|
150
|
+
// and closes failed clients so Socket.IO cannot keep reconnecting an
|
|
151
|
+
// orphan that the room lifecycle never received.
|
|
140
152
|
let settled = false;
|
|
141
153
|
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
154
|
+
const startedAt = Date.now();
|
|
155
|
+
let transportOpened = false;
|
|
156
|
+
let rejection: { reason: MediaConnectionRejectionReason; stage?: string } | null = null;
|
|
142
157
|
const cleanupHandshake = () => {
|
|
143
158
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
144
159
|
socket.off('connection-success', handleConnectionSuccess);
|
|
145
160
|
socket.off('connect_error', handleConnectError);
|
|
146
161
|
socket.off('disconnect', handleDisconnectBeforeReady);
|
|
162
|
+
socket.off('connect', handleTransportOpen);
|
|
163
|
+
socket.off('connection-rejected', handleConnectionRejected);
|
|
147
164
|
};
|
|
165
|
+
const failure = (phase: MediaSocketConnectPhase, transportError?: string) =>
|
|
166
|
+
new MediaSocketConnectError({
|
|
167
|
+
phase: rejection ? 'rejected' : phase,
|
|
168
|
+
reason: rejection?.reason,
|
|
169
|
+
stage: rejection?.stage,
|
|
170
|
+
elapsedMs: Date.now() - startedAt,
|
|
171
|
+
online: currentOnlineState(),
|
|
172
|
+
transportError,
|
|
173
|
+
link,
|
|
174
|
+
});
|
|
148
175
|
const settle = (action: () => void) => {
|
|
149
176
|
if (settled) return;
|
|
150
177
|
settled = true;
|
|
@@ -152,6 +179,7 @@ async function connectSocket(
|
|
|
152
179
|
action();
|
|
153
180
|
};
|
|
154
181
|
|
|
182
|
+
// Handle socket connection events
|
|
155
183
|
const handleConnectionSuccess = ({ socketId }: { socketId: string }) => {
|
|
156
184
|
//check if link contains mediasfu.com and contains more than one c
|
|
157
185
|
let conn = 'media';
|
|
@@ -170,24 +198,47 @@ async function connectSocket(
|
|
|
170
198
|
const handleConnectError = (error: Error) => {
|
|
171
199
|
settle(() => {
|
|
172
200
|
socket.disconnect();
|
|
173
|
-
reject(
|
|
201
|
+
reject(failure(transportOpened ? 'after-open' : 'before-open', error?.message));
|
|
174
202
|
});
|
|
175
203
|
};
|
|
176
204
|
|
|
205
|
+
const handleTransportOpen = () => {
|
|
206
|
+
transportOpened = true;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Media admission v2: the node says why before it closes the handshake. Socket.IO's own reconnection never
|
|
210
|
+
// retries a server-side close, and settling here stops this attempt from being retried as a network error.
|
|
211
|
+
const handleConnectionRejected = (payload?: { reason?: string; stage?: string }) => {
|
|
212
|
+
rejection = {
|
|
213
|
+
reason: normalizeRejectionReason(payload?.reason),
|
|
214
|
+
stage: typeof payload?.stage === 'string' ? payload.stage : undefined,
|
|
215
|
+
};
|
|
216
|
+
settle(() => {
|
|
217
|
+
try {
|
|
218
|
+
socket.disconnect();
|
|
219
|
+
} catch {
|
|
220
|
+
// Already closing.
|
|
221
|
+
}
|
|
222
|
+
reject(failure('rejected'));
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// The transport can connect and then be dropped by the server before it
|
|
227
|
+
// ever sends `connection-success` — a rejected room, an expired token, a
|
|
228
|
+
// bad handshake. Without this the promise stayed pending forever, so the
|
|
229
|
+
// caller's loading modal never came down and no error was ever surfaced.
|
|
177
230
|
const handleDisconnectBeforeReady = (reason: string) => {
|
|
178
|
-
settle(() =>
|
|
179
|
-
reject(
|
|
180
|
-
new Error(
|
|
181
|
-
'Media socket closed before the room was ready (' + reason + ').'
|
|
182
|
-
)
|
|
183
|
-
)
|
|
184
|
-
);
|
|
231
|
+
settle(() => reject(failure('after-open', reason)));
|
|
185
232
|
};
|
|
186
233
|
|
|
187
234
|
socket.on('connection-success', handleConnectionSuccess);
|
|
188
235
|
socket.on('connect_error', handleConnectError);
|
|
189
236
|
socket.on('disconnect', handleDisconnectBeforeReady);
|
|
237
|
+
socket.on('connect', handleTransportOpen);
|
|
238
|
+
socket.on('connection-rejected', handleConnectionRejected);
|
|
190
239
|
|
|
240
|
+
// Last resort, for the case where the socket neither connects, errors nor
|
|
241
|
+
// disconnects. Nothing above guarantees a settle on its own.
|
|
191
242
|
timeoutHandle = setTimeout(() => {
|
|
192
243
|
settle(() => {
|
|
193
244
|
try {
|
|
@@ -195,7 +246,7 @@ async function connectSocket(
|
|
|
195
246
|
} catch {
|
|
196
247
|
// Nothing useful to do here.
|
|
197
248
|
}
|
|
198
|
-
reject(
|
|
249
|
+
reject(failure('timeout'));
|
|
199
250
|
});
|
|
200
251
|
}, CONNECT_TIMEOUT_MS);
|
|
201
252
|
});
|
|
@@ -237,6 +288,11 @@ async function connectLocalSocket({ link }: ConnectLocalSocketOptions): Promise<
|
|
|
237
288
|
socket = io(`${link}/media`, {
|
|
238
289
|
transports: ['websocket'],
|
|
239
290
|
});
|
|
291
|
+
const startedAt = Date.now();
|
|
292
|
+
let transportOpened = false;
|
|
293
|
+
socket.on('connect', () => {
|
|
294
|
+
transportOpened = true;
|
|
295
|
+
});
|
|
240
296
|
|
|
241
297
|
|
|
242
298
|
// Handle socket connection events
|
|
@@ -245,7 +301,15 @@ async function connectLocalSocket({ link }: ConnectLocalSocketOptions): Promise<
|
|
|
245
301
|
});
|
|
246
302
|
|
|
247
303
|
socket.on('connect_error', (error: Error) => {
|
|
248
|
-
reject(
|
|
304
|
+
reject(
|
|
305
|
+
new MediaSocketConnectError({
|
|
306
|
+
phase: transportOpened ? 'after-open' : 'before-open',
|
|
307
|
+
elapsedMs: Date.now() - startedAt,
|
|
308
|
+
online: currentOnlineState(),
|
|
309
|
+
transportError: error?.message,
|
|
310
|
+
link,
|
|
311
|
+
})
|
|
312
|
+
);
|
|
249
313
|
});
|
|
250
314
|
});
|
|
251
315
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media socket connection errors.
|
|
3
|
+
*
|
|
4
|
+
* A media node that refuses a handshake emits `connection-rejected { reason, stage }` just before it closes the socket
|
|
5
|
+
* (media admission v2). `connectSocket` turns that, and every other way a connection can fail, into a
|
|
6
|
+
* `MediaSocketConnectError` that says what happened, when and whether trying again can help.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Reasons a media node sends with `connection-rejected`. */
|
|
10
|
+
export type MediaConnectionRejectionReason =
|
|
11
|
+
| 'invalid-credentials'
|
|
12
|
+
| 'meeting-minutes-unavailable'
|
|
13
|
+
| 'api-check-failed'
|
|
14
|
+
| 'invalid-request'
|
|
15
|
+
| 'rejected-before-connection-success';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where the attempt stopped:
|
|
19
|
+
* - `before-open`: the transport never opened (offline, DNS, TLS, firewall, server down);
|
|
20
|
+
* - `after-open`: the transport opened but closed or errored before `connection-success`;
|
|
21
|
+
* - `rejected`: the server refused the handshake and said why (`reason`);
|
|
22
|
+
* - `timeout`: nothing settled in time.
|
|
23
|
+
*/
|
|
24
|
+
export type MediaSocketConnectPhase = 'before-open' | 'after-open' | 'rejected' | 'timeout';
|
|
25
|
+
|
|
26
|
+
export const MEDIA_CONNECTION_REJECTION_REASONS: readonly MediaConnectionRejectionReason[] = [
|
|
27
|
+
'invalid-credentials',
|
|
28
|
+
'meeting-minutes-unavailable',
|
|
29
|
+
'api-check-failed',
|
|
30
|
+
'invalid-request',
|
|
31
|
+
'rejected-before-connection-success',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/** Rejections that will fail the same way if retried with the same credentials. */
|
|
35
|
+
const PERMANENT_REJECTIONS = new Set<string>(['invalid-credentials', 'meeting-minutes-unavailable', 'invalid-request']);
|
|
36
|
+
|
|
37
|
+
export interface MediaSocketConnectErrorDetails {
|
|
38
|
+
phase: MediaSocketConnectPhase;
|
|
39
|
+
/** Rejection reason from the server (phase `rejected`), else undefined. */
|
|
40
|
+
reason?: MediaConnectionRejectionReason;
|
|
41
|
+
/** Server-side admission stage from `connection-rejected`, e.g. `api-check-rejected`. */
|
|
42
|
+
stage?: string;
|
|
43
|
+
/** Milliseconds from the start of the attempt to the failure. */
|
|
44
|
+
elapsedMs: number;
|
|
45
|
+
/** `navigator.onLine` when available. `false` points at the device's network, not the server. */
|
|
46
|
+
online?: boolean;
|
|
47
|
+
/** Underlying transport error or disconnect reason, e.g. `websocket error`, `io server disconnect`. */
|
|
48
|
+
transportError?: string;
|
|
49
|
+
/** Socket link the attempt targeted. */
|
|
50
|
+
link?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class MediaSocketConnectError extends Error implements MediaSocketConnectErrorDetails {
|
|
54
|
+
readonly phase: MediaSocketConnectPhase;
|
|
55
|
+
readonly reason?: MediaConnectionRejectionReason;
|
|
56
|
+
readonly stage?: string;
|
|
57
|
+
readonly elapsedMs: number;
|
|
58
|
+
readonly online?: boolean;
|
|
59
|
+
readonly transportError?: string;
|
|
60
|
+
readonly link?: string;
|
|
61
|
+
/** False when retrying with the same credentials cannot succeed (bad credentials, no minutes, bad request). */
|
|
62
|
+
readonly retryable: boolean;
|
|
63
|
+
|
|
64
|
+
constructor(details: MediaSocketConnectErrorDetails) {
|
|
65
|
+
super(formatMediaSocketError(details));
|
|
66
|
+
Object.setPrototypeOf(this, MediaSocketConnectError.prototype);
|
|
67
|
+
this.name = 'MediaSocketConnectError';
|
|
68
|
+
this.phase = details.phase;
|
|
69
|
+
this.reason = details.reason;
|
|
70
|
+
this.stage = details.stage;
|
|
71
|
+
this.elapsedMs = details.elapsedMs;
|
|
72
|
+
this.online = details.online;
|
|
73
|
+
this.transportError = details.transportError;
|
|
74
|
+
this.link = details.link;
|
|
75
|
+
this.retryable = !(details.phase === 'rejected' && details.reason && PERMANENT_REJECTIONS.has(details.reason));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Maps any reason string from the server onto a known reason (unknown values become `rejected-before-connection-success`). */
|
|
80
|
+
export const normalizeRejectionReason = (reason: unknown): MediaConnectionRejectionReason =>
|
|
81
|
+
(MEDIA_CONNECTION_REJECTION_REASONS as readonly string[]).includes(String(reason))
|
|
82
|
+
? (reason as MediaConnectionRejectionReason)
|
|
83
|
+
: 'rejected-before-connection-success';
|
|
84
|
+
|
|
85
|
+
/** `navigator.onLine` where it exists (browsers, some React Native runtimes), else undefined. */
|
|
86
|
+
export const currentOnlineState = (): boolean | undefined => {
|
|
87
|
+
try {
|
|
88
|
+
const nav = (globalThis as { navigator?: { onLine?: unknown } }).navigator;
|
|
89
|
+
return typeof nav?.onLine === 'boolean' ? nav.onLine : undefined;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Technical one-liner for logs; keeps the historic "Error connecting to media socket:" prefix for transport failures. */
|
|
96
|
+
export function formatMediaSocketError(details: MediaSocketConnectErrorDetails): string {
|
|
97
|
+
const facts = [`phase: ${details.phase}`, `after ${Math.max(0, Math.round(details.elapsedMs))} ms`];
|
|
98
|
+
if (details.online !== undefined) facts.push(`online: ${details.online}`);
|
|
99
|
+
if (details.stage) facts.push(`stage: ${details.stage}`);
|
|
100
|
+
const tail = ` (${facts.join(', ')})`;
|
|
101
|
+
switch (details.phase) {
|
|
102
|
+
case 'rejected':
|
|
103
|
+
return `Media server rejected the connection: ${details.reason || 'rejected-before-connection-success'}${tail}`;
|
|
104
|
+
case 'timeout':
|
|
105
|
+
return `Timed out connecting to media socket${tail}`;
|
|
106
|
+
case 'after-open':
|
|
107
|
+
return `Media socket closed before the room was ready: ${details.transportError || 'unknown'}${tail}`;
|
|
108
|
+
default:
|
|
109
|
+
return `Error connecting to media socket: ${details.transportError || 'unknown'}${tail}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const isMediaSocketConnectError = (error: unknown): error is MediaSocketConnectError =>
|
|
114
|
+
error instanceof MediaSocketConnectError ||
|
|
115
|
+
(typeof error === 'object' && error !== null && (error as { name?: unknown }).name === 'MediaSocketConnectError');
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* True when the attempt failed because of the device's network (offline, or the transport never opened) rather than
|
|
119
|
+
* a server refusal. Join pages do not count these toward the attempt limit: they are not credential guesses.
|
|
120
|
+
*/
|
|
121
|
+
export const isNetworkOnlyMediaSocketError = (error: unknown): boolean =>
|
|
122
|
+
isMediaSocketConnectError(error) &&
|
|
123
|
+
error.phase !== 'rejected' &&
|
|
124
|
+
(error.online === false || error.phase === 'before-open');
|
|
125
|
+
|
|
126
|
+
/** False only for rejections that cannot succeed on retry with the same credentials. */
|
|
127
|
+
export const isRetryableMediaSocketError = (error: unknown): boolean =>
|
|
128
|
+
!isMediaSocketConnectError(error) || error.retryable;
|
|
129
|
+
|
|
130
|
+
/** A sentence suitable for an end user (alert/toast). Falls back to `fallback` for unknown errors. */
|
|
131
|
+
export function describeMediaSocketError(
|
|
132
|
+
error: unknown,
|
|
133
|
+
fallback = 'Unable to connect. Check your credentials and try again.',
|
|
134
|
+
): string {
|
|
135
|
+
if (!isMediaSocketConnectError(error)) return fallback;
|
|
136
|
+
if (error.phase === 'rejected') {
|
|
137
|
+
switch (error.reason) {
|
|
138
|
+
case 'invalid-credentials':
|
|
139
|
+
return 'The room did not accept these credentials. Check the room ID and your access, or rejoin the room.';
|
|
140
|
+
case 'meeting-minutes-unavailable':
|
|
141
|
+
return 'This room cannot start because its account has no meeting minutes left. Contact the room host.';
|
|
142
|
+
case 'invalid-request':
|
|
143
|
+
return 'The connection request was incomplete or invalid. Check the room details and try again.';
|
|
144
|
+
case 'api-check-failed':
|
|
145
|
+
return 'The media server could not verify this room right now. Please try again shortly.';
|
|
146
|
+
default:
|
|
147
|
+
return 'The media server closed the connection before the room was ready. Please try again.';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (error.online === false) return 'You appear to be offline. Check your internet connection and try again.';
|
|
151
|
+
if (error.phase === 'before-open') {
|
|
152
|
+
return 'Could not reach the media server. Check your network or firewall and try again.';
|
|
153
|
+
}
|
|
154
|
+
if (error.phase === 'timeout') return 'Connecting to the media server timed out. Please try again.';
|
|
155
|
+
return 'The media server closed the connection before the room was ready. Please try again.';
|
|
156
|
+
}
|
|
@@ -8,6 +8,8 @@ export interface Participant {
|
|
|
8
8
|
videoID: string;
|
|
9
9
|
ScreenID?: string;
|
|
10
10
|
ScreenOn?: boolean;
|
|
11
|
+
/** The participant's screen-audio producer id while they share screen audio ("" or absent otherwise). */
|
|
12
|
+
ScreenAudioID?: string;
|
|
11
13
|
islevel?: string;
|
|
12
14
|
isAdmin?: boolean;
|
|
13
15
|
isHost?: boolean;
|
|
@@ -405,6 +407,8 @@ export interface ResponseJoinRoom {
|
|
|
405
407
|
banned?: boolean;
|
|
406
408
|
suspended?: boolean;
|
|
407
409
|
noAdmin?: boolean;
|
|
410
|
+
/** The media server accepts audio shared with a screen share ("screen-audio" producers). */
|
|
411
|
+
supportsScreenAudio?: boolean;
|
|
408
412
|
}
|
|
409
413
|
|
|
410
414
|
export interface AllMembersData {
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
const validateAlphanumeric = async ({ str }) => {
|
|
2
|
-
let code, i, len;
|
|
3
|
-
for (i = 0, len = str.length; i < len; i++) {
|
|
4
|
-
code = str.charCodeAt(i);
|
|
5
|
-
if (!(code > 47 && code < 58) && // numeric (0-9)
|
|
6
|
-
!(code > 64 && code < 91) && // upper alpha (A-Z)
|
|
7
|
-
!(code > 96 && code < 123)) {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return true;
|
|
12
|
-
};
|
|
13
|
-
function audioProcessingConstraints(options) {
|
|
14
|
-
const result = {};
|
|
15
|
-
if (options === void 0) return result;
|
|
16
|
-
if (!options || typeof options !== "object" || Array.isArray(options)) throw new TypeError("Invalid audioProcessing");
|
|
17
|
-
for (const key of Object.keys(options)) {
|
|
18
|
-
if (!["echoCancellation", "noiseSuppression", "autoGainControl"].includes(key)) throw new TypeError("Unsupported audioProcessing option");
|
|
19
|
-
const value = options[key];
|
|
20
|
-
if (value !== void 0 && typeof value !== "boolean") throw new TypeError("Audio processing options must be boolean");
|
|
21
|
-
if (value !== void 0) result[key] = value;
|
|
22
|
-
}
|
|
23
|
-
return result;
|
|
24
|
-
}
|
|
25
|
-
async function applyAudioProcessing(track, options) {
|
|
26
|
-
const selected = audioProcessingConstraints(options);
|
|
27
|
-
if (!Object.keys(selected).length) return;
|
|
28
|
-
await track.applyConstraints({ ...track.getConstraints(), ...selected });
|
|
29
|
-
}
|
|
30
|
-
export {
|
|
31
|
-
applyAudioProcessing as a,
|
|
32
|
-
audioProcessingConstraints as b,
|
|
33
|
-
validateAlphanumeric as v
|
|
34
|
-
};
|
|
35
|
-
//# sourceMappingURL=audioProcessing-0z5KlcmO.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"audioProcessing-0z5KlcmO.js","sources":["../src/methods/utils/validateAlphanumeric.ts","../src/consumers/audioProcessing.ts"],"sourcesContent":["\nexport interface ValidateAlphanumericOptions {\n str: string;\n}\n\n// Export the type definition for the function\nexport type ValidateAlphanumericType = (options: ValidateAlphanumericOptions) => Promise<boolean>;\n\n/**\n * Validates if the given string contains only alphanumeric characters.\n *\n * @param {ValidateAlphanumericOptions} options - The options containing the string to validate.\n * @param {string} options.str - The string to be validated.\n * @returns {Promise<boolean>} - A promise that resolves to `true` if the string is alphanumeric, otherwise `false`.\n * \n * @example\n * ```typescript \n * const isValid = await validateAlphanumeric({ str: \"abc123\" });\n * console.log(isValid);\n * // Output: true\n * ```\n */\n\n\nconst validateAlphanumeric = async ({ str }: ValidateAlphanumericOptions): Promise<boolean> => {\n let code: number, i: number, len: number;\n\n for (i = 0, len = str.length; i < len; i++) {\n code = str.charCodeAt(i);\n if (\n !(code > 47 && code < 58) && // numeric (0-9)\n !(code > 64 && code < 91) && // upper alpha (A-Z)\n !(code > 96 && code < 123) // lower alpha (a-z)\n ) {\n return false;\n }\n }\n return true;\n};\n\nexport { validateAlphanumeric };\n","export type AudioProcessingOptions = {\n echoCancellation?: boolean;\n noiseSuppression?: boolean;\n autoGainControl?: boolean;\n};\n\nexport function audioProcessingConstraints(options?: AudioProcessingOptions): MediaTrackConstraints {\n const result: MediaTrackConstraints = {};\n if (options === undefined) return result;\n if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Invalid audioProcessing');\n for (const key of Object.keys(options)) {\n if (!['echoCancellation', 'noiseSuppression', 'autoGainControl'].includes(key)) throw new TypeError('Unsupported audioProcessing option');\n const value = options[key as keyof AudioProcessingOptions];\n if (value !== undefined && typeof value !== 'boolean') throw new TypeError('Audio processing options must be boolean');\n if (value !== undefined) result[key as keyof AudioProcessingOptions] = value;\n }\n return result;\n}\n\n// applyConstraints replaces the constraint set: merge to retain device and\n// capture choices unrelated to the three processing preferences.\nexport async function applyAudioProcessing(track: MediaStreamTrack, options?: AudioProcessingOptions): Promise<void> {\n const selected = audioProcessingConstraints(options);\n if (!Object.keys(selected).length) return;\n await track.applyConstraints({ ...track.getConstraints(), ...selected });\n}\n"],"names":[],"mappings":"AAwBA,MAAM,uBAAuB,OAAO,EAAE,UAAyD;AAC7F,MAAI,MAAc,GAAW;AAE7B,OAAK,IAAI,GAAG,MAAM,IAAI,QAAQ,IAAI,KAAK,KAAK;AAC1C,WAAO,IAAI,WAAW,CAAC;AACvB,QACE,EAAE,OAAO,MAAM,OAAO;AAAA,IACtB,EAAE,OAAO,MAAM,OAAO;AAAA,IACtB,EAAE,OAAO,MAAM,OAAO,MACtB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AChCO,SAAS,2BAA2B,SAAyD;AAClG,QAAM,SAAgC,CAAA;AACtC,MAAI,YAAY,OAAW,QAAO;AAClC,MAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,EAAG,OAAM,IAAI,UAAU,yBAAyB;AACpH,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,QAAI,CAAC,CAAC,oBAAoB,oBAAoB,iBAAiB,EAAE,SAAS,GAAG,EAAG,OAAM,IAAI,UAAU,oCAAoC;AACxI,UAAM,QAAQ,QAAQ,GAAmC;AACzD,QAAI,UAAU,UAAa,OAAO,UAAU,UAAW,OAAM,IAAI,UAAU,0CAA0C;AACrH,QAAI,UAAU,OAAW,QAAO,GAAmC,IAAI;AAAA,EACzE;AACA,SAAO;AACT;AAIA,eAAsB,qBAAqB,OAAyB,SAAiD;AACnH,QAAM,WAAW,2BAA2B,OAAO;AACnD,MAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,OAAQ;AACnC,QAAM,MAAM,iBAAiB,EAAE,GAAG,MAAM,eAAA,GAAkB,GAAG,UAAU;AACzE;"}
|