diff --git a/lang/main-es.json b/lang/main-es.json index deaf52d65984..ab1424be594c 100644 --- a/lang/main-es.json +++ b/lang/main-es.json @@ -954,8 +954,8 @@ "videoUnmuteBlockedTitle": "¡Desactivar cámara y compartir pantalla bloqueados!", "viewLobby": "Ver lobby", "waitingParticipants": "{{waitingParticipants}} personas", - "encryptionKeySyncFailed": "La sincronización de la clave de cifrado ha fallado. Se recomienda salir de la reunión y vuelva a unirse para restaurar la comunicación segura.", - "encryptionKeySyncFailedTitle": "Error de Sincronización de Cifrado", + "encryptionKeySyncFailed": "El establecimiento de la sesión segura ha fallado. Asegúrese de que todos los participantes tengan una conexión a Internet confiable.", + "encryptionKeySyncFailedTitle": "Error de Establecimiento de la Sesión", "cryptoFailedTitle": "La operación criptográfica ha fallado", "cryptoFailed": "La operación criptográfica ha fallado. No puede acceder al video ni al audio de la reunión. Se recomienda reiniciar el navegador. Si eres el organizador de la reunión, vuelva a crearla.", "encryptionKeySyncRestored": "La sincronización de claves para el cifrado se ha restaurado con éxito. Su comunicación segura está ahora activa.", diff --git a/lang/main.json b/lang/main.json index 683af4d8bdce..6ef59a870b2c 100644 --- a/lang/main.json +++ b/lang/main.json @@ -1096,8 +1096,8 @@ "waitingVisitorsTitle": "The meeting is not live yet!", "whiteboardLimitDescription": "Please save your progress, as the user limit will soon be reached and the whiteboard will close.", "whiteboardLimitTitle": "Whiteboard usage", - "encryptionKeySyncFailed": "The encryption key synchronization has failed. It is recommended that you leave the meeting and rejoin to restore secure communication.", - "encryptionKeySyncFailedTitle": "Encryption Sync Failed", + "encryptionKeySyncFailed": "The secure session establishement has failed. Please ensure that all participants have a reliable internet connection.", + "encryptionKeySyncFailedTitle": "Session Establishment Failed", "cryptoFailedTitle": "Cryptographic operation failed", "cryptoFailed": "Cryptographic operation has failed. You cannot access video or audio of the meeting. It is recommended that you restart the browser. If you are the meeting organizer, recreate it.", "encryptionKeySyncRestored": "The encryption key synchronization has been successfully restored. Your secure communication is now active.", diff --git a/package.json b/package.json index b629cbdea321..ce7bf80a03ef 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20/lib-jitsi-meet-0.0.20.tgz", + "lib-jitsi-meet": "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20-debug/lib-jitsi-meet-0.0.20-debug.tgz", "lodash-es": "4.17.23", "moment": "2.29.4", "moment-duration-format": "2.2.2", diff --git a/react/features/base/connection/actions.web.ts b/react/features/base/connection/actions.web.ts index a69fb11f29b3..6cbd43676cbe 100644 --- a/react/features/base/connection/actions.web.ts +++ b/react/features/base/connection/actions.web.ts @@ -39,26 +39,20 @@ export function connect(id?: string, password?: string) { return getJaasJWT(state); } }) - .then((j) => j && dispatch(setJWT(j))) - .then(() => - dispatch( + .then((j) => { + j && dispatch(setJWT(j)); + return dispatch( _connectInternal({ id, password, name: user?.name, lastname: user?.lastname, isAnonymous: !user, - }) - ) - ) - // latest jitsi changes, test if not works current ones - // .then(j => { - // j && dispatch(setJWT(j)); - - // return dispatch(_connectInternal(id, password)); - // }) - .catch(e => { - logger.error('Connection error', e); + }), + ); + }) + .catch((e) => { + logger.error("Connection error", e); }); } diff --git a/react/features/base/connection/middleware.web.ts b/react/features/base/connection/middleware.web.ts index a7189a5e0270..ae26056df8f0 100644 --- a/react/features/base/connection/middleware.web.ts +++ b/react/features/base/connection/middleware.web.ts @@ -1,9 +1,6 @@ -import { redirectToStaticPage } from '../../app/actions.any'; -import { CONFERENCE_WILL_LEAVE } from "../conference/actionTypes"; -import { isLeavingConferenceManually, setLeaveConferenceManually } from "../meet/general/utils/conferenceState"; import MiddlewareRegistry from "../redux/MiddlewareRegistry"; -import { CONNECTION_DISCONNECTED, CONNECTION_WILL_CONNECT } from "./actionTypes"; +import { CONNECTION_WILL_CONNECT } from "./actionTypes"; /** * The feature announced so we can distinguish jibri participants. @@ -12,7 +9,7 @@ import { CONNECTION_DISCONNECTED, CONNECTION_WILL_CONNECT } from "./actionTypes" */ export const DISCO_JIBRI_FEATURE = "http://jitsi.org/protocol/jibri"; -MiddlewareRegistry.register(({ getState, dispatch }) => (next) => (action) => { +MiddlewareRegistry.register(({ getState }) => (next) => (action) => { switch (action.type) { case CONNECTION_WILL_CONNECT: { const { connection } = action; @@ -25,26 +22,6 @@ MiddlewareRegistry.register(({ getState, dispatch }) => (next) => (action) => { // @ts-ignore APP.connection = connection; - setLeaveConferenceManually(false); - break; - } - - case CONFERENCE_WILL_LEAVE: { - setLeaveConferenceManually(true); - break; - } - - case CONNECTION_DISCONNECTED: { - if (isLeavingConferenceManually()) { - setLeaveConferenceManually(false); - - setTimeout(() => { - dispatch(redirectToStaticPage("/")); - }, 2000); - } else { - console.warn("Connection disconnected unexpectedly - waiting for reconnection"); - } - break; } } diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.conference.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.conference.ts deleted file mode 100644 index ab11bf51fbc2..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.conference.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IStore } from '../../../../../app/types'; -import { - showConnectionLostNotification, - showConnectionRestoredNotification, - showDeviceSuspendedNotification, -} from './notification-helpers'; -import { ConnectionState } from "./types"; - -/** - * Handles when ICE connection is interrupted (packet loss, network issue) - * User can still see frozen video but quality degrades - * - * @param dispatch - Redux dispatch function - * @param state - Connection state to mark that interruption occurred - */ -export const handleMediaConnectionInterrupted = (dispatch: IStore["dispatch"], state: ConnectionState) => { - console.log("[CONNECTION_NOTIFICATIONS] Media connection interrupted (ICE)"); - state.wasMediaConnectionInterrupted = true; - showConnectionLostNotification(dispatch); -}; - -/** - * Handles when ICE connection is restored after interruption - * Video/audio quality returns to normal - * Only shows notification if there was a previous interruption (not on initial join) - * - * @param dispatch - Redux dispatch function - * @param state - Connection state to check if there was a previous interruption - */ -export const handleMediaConnectionRestored = (dispatch: IStore["dispatch"], state: ConnectionState) => { - console.log("[CONNECTION_NOTIFICATIONS] Media connection restored (ICE)"); - - if (state.wasMediaConnectionInterrupted) { - showConnectionRestoredNotification(dispatch); - state.wasMediaConnectionInterrupted = false; - } else { - console.log("[CONNECTION_NOTIFICATIONS] Skipping notification - no previous interruption"); - } -}; - -/** - * Handles when device is suspended (laptop closed, mobile app backgrounded) - * Connection will be restored when device wakes up - * - * @param dispatch - Redux dispatch function - */ -export const handleDeviceSuspended = (dispatch: IStore['dispatch']) => { - console.log('[CONNECTION_NOTIFICATIONS] Device suspended detected'); - showDeviceSuspendedNotification(dispatch); -}; diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.connection.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.connection.ts deleted file mode 100644 index 7bff10440763..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.connection.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IStore } from '../../../../../app/types'; -import { isLeavingConferenceManually } from "../../../general/utils/conferenceState"; -import { isAutoReconnecting } from "../middleware.auto-reconnect"; -import { showConnectionFailedNotification, showConnectionLostNotification } from "./notification-helpers"; - -/** - * Handles when XMPP connection is established - * This is the signaling connection, not media - */ -export const handleXMPPConnected = () => { - console.log("[CONNECTION_NOTIFICATIONS] XMPP connection established"); -}; - -/** - * Handles when XMPP WebSocket is disconnected - * Only shows notification if not a manual disconnect (user clicking hangup) - * - * @param dispatch - Redux dispatch function - * @param message - Disconnect message from lib-jitsi-meet - */ -export const handleXMPPDisconnected = (dispatch: IStore["dispatch"], message: string) => { - console.log("[CONNECTION_NOTIFICATIONS] XMPP disconnected:", message); - - if (isLeavingConferenceManually()) return; - if (isAutoReconnecting()) return; - - showConnectionLostNotification(dispatch); -}; - -/** - * Handles when XMPP connection fails to establish or encounters fatal error - * This is more severe than disconnect - connection couldn't be made at all - * - * @param dispatch - Redux dispatch function - * @param error - Error object from lib-jitsi-meet - * @param message - Error message - */ -export const handleXMPPConnectionFailed = (dispatch: IStore["dispatch"], error: any, message: string) => { - console.error("[CONNECTION_NOTIFICATIONS] XMPP connection failed:", error, message); - if (isAutoReconnecting()) return; - - showConnectionFailedNotification(dispatch, message); -}; diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts deleted file mode 100644 index b464d8ddede8..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { AnyAction } from 'redux'; -import { IStore } from '../../../../../app/types'; -import { CONFERENCE_JOINED, CONFERENCE_WILL_LEAVE } from '../../../../conference/actionTypes'; -import { setLeaveConferenceManually } from '../../../general/utils/conferenceState'; -import { CONNECTION_WILL_CONNECT } from '../../../../connection/actionTypes'; -import MiddlewareRegistry from '../../../../redux/MiddlewareRegistry'; -import { setupConferenceMediaListeners, setupXMPPConnectionListeners } from './listener-setup'; -import { createConnectionState } from './state'; - -/** - * Middleware that listens to Redux actions and sets up lib-jitsi-meet event listeners - * - * Flow: - * 1. CONNECTION_WILL_CONNECT -> Setup XMPP listeners - * 2. CONFERENCE_JOINED -> Setup ICE/media listeners - * 3. CONFERENCE_WILL_LEAVE -> Mark as manual disconnect, cleanup flags - */ -MiddlewareRegistry.register(({ dispatch }: IStore) => { - const connectionState = createConnectionState(); - - return (next: Function) => - (action: AnyAction) => { - const result = next(action); - - switch (action.type) { - case CONNECTION_WILL_CONNECT: { - setLeaveConferenceManually(false); - connectionState.hasConnectionListeners = false; - - const { connection } = action; - - setupXMPPConnectionListeners(connection, dispatch, connectionState); - break; - } - - case CONFERENCE_JOINED: { - const { conference } = action; - - setupConferenceMediaListeners(conference, dispatch, connectionState); - break; - } - - case CONFERENCE_WILL_LEAVE: { - // User clicked hangup button - don't show reconnection notifications - setLeaveConferenceManually(true); - connectionState.hasConferenceListeners = false; - connectionState.wasMediaConnectionInterrupted = false; - break; - } - } - - return result; - }; -}); diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts deleted file mode 100644 index e133dcc815aa..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { IStore } from '../../../../../app/types'; -import { JitsiConferenceEvents, JitsiConnectionEvents } from '../../../../lib-jitsi-meet'; -import { - handleDeviceSuspended, - handleMediaConnectionInterrupted, - handleMediaConnectionRestored, -} from './event-handlers.conference'; -import { - handleXMPPConnected, - handleXMPPConnectionFailed, - handleXMPPDisconnected, -} from './event-handlers.connection'; -import { ConnectionState } from './types'; - -/** - * Attaches event listeners for conference media connection events - * These events track the ICE connection state (actual audio/video transport) - * - * @param conference - Jitsi conference instance - * @param dispatch - Redux dispatch function - * @param state - Connection state to track listener registration - */ -export const setupConferenceMediaListeners = ( - conference: any, - dispatch: IStore["dispatch"], - state: ConnectionState -) => { - if (state.hasConferenceListeners || !conference) { - return; - } - - conference.addEventListener(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => - handleMediaConnectionInterrupted(dispatch, state) - ); - - conference.addEventListener(JitsiConferenceEvents.CONNECTION_RESTORED, () => - handleMediaConnectionRestored(dispatch, state) - ); - - conference.addEventListener(JitsiConferenceEvents.SUSPEND_DETECTED, () => handleDeviceSuspended(dispatch)); - - state.hasConferenceListeners = true; -}; - -/** - * Attaches event listeners for XMPP connection events - * These events track the signaling connection (WebSocket to XMPP server) - * - * @param connection - Jitsi connection instance - * @param dispatch - Redux dispatch function - * @param state - Connection state to track listener registration - */ -export const setupXMPPConnectionListeners = (connection: any, dispatch: IStore["dispatch"], state: ConnectionState) => { - if (!connection || state.hasConnectionListeners) { - return; - } - - connection.addEventListener(JitsiConnectionEvents.CONNECTION_ESTABLISHED, () => handleXMPPConnected()); - - connection.addEventListener(JitsiConnectionEvents.CONNECTION_DISCONNECTED, (message: string) => - handleXMPPDisconnected(dispatch, message) - ); - - connection.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, (error: any, message: string) => - handleXMPPConnectionFailed(dispatch, error, message) - ); - - state.hasConnectionListeners = true; -}; diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/notification-helpers.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/notification-helpers.ts deleted file mode 100644 index 0af1999255b8..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/notification-helpers.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { IStore } from '../../../../../app/types'; -import { showNotification, showWarningNotification } from '../../../../../notifications/actions'; -import { NOTIFICATION_TIMEOUT_TYPE } from '../../../../../notifications/constants'; - -/** - * Shows a "Connection lost" warning notification with reconnection message - */ -export const showConnectionLostNotification = (dispatch: IStore['dispatch']) => { - dispatch( - showWarningNotification( - { - titleKey: 'notify.connectionLost', - descriptionKey: 'notify.reconnecting', - }, - NOTIFICATION_TIMEOUT_TYPE.LONG - ) - ); -}; - -/** - * Shows a "Connected" success notification when connection is restored - */ -export const showConnectionRestoredNotification = (dispatch: IStore['dispatch']) => { - dispatch( - showNotification( - { - titleKey: 'notify.connectedTitle', - descriptionKey: 'notify.connectedMessage', - }, - NOTIFICATION_TIMEOUT_TYPE.SHORT - ) - ); -}; - -/** - * Shows a "Connection failed" error notification with custom message - */ -export const showConnectionFailedNotification = (dispatch: IStore['dispatch'], errorMessage?: string) => { - dispatch( - showWarningNotification( - { - titleKey: 'notify.connectionFailed', - ...(errorMessage && { descriptionKey: errorMessage }), - }, - NOTIFICATION_TIMEOUT_TYPE.LONG - ) - ); -}; - -/** - * Shows a "Device suspended" warning notification - */ -export const showDeviceSuspendedNotification = (dispatch: IStore['dispatch']) => { - dispatch( - showWarningNotification( - { - titleKey: 'notify.connectionLost', - descriptionKey: 'notify.deviceSuspended', - }, - NOTIFICATION_TIMEOUT_TYPE.LONG - ) - ); -}; diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/state.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/state.ts deleted file mode 100644 index 6a75107177aa..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/state.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ConnectionState } from './types'; - -/** - * Factory function to create a new connection state instance - * - * @returns A new ConnectionState object with default values - */ -export const createConnectionState = (): ConnectionState => ({ - hasConferenceListeners: false, - hasConnectionListeners: false, - wasMediaConnectionInterrupted: false, -}); diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/types.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/types.ts deleted file mode 100644 index 48a615197a3f..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * State interface for tracking connection notification behavior - */ -export interface ConnectionState { - /** - * True when conference media listeners (ICE/Media events) have been attached - * Prevents duplicate event listener registration - */ - hasConferenceListeners: boolean; - - /** - * True when connection listeners (XMPP/WebSocket events) have been attached - * Prevents duplicate event listener registration - */ - hasConnectionListeners: boolean; - - /** - * True when media connection (ICE) was interrupted - * Used to only show "connection restored" notification if there was a previous interruption - */ - wasMediaConnectionInterrupted: boolean; -} diff --git a/react/features/base/meet/middlewares/connection-stability/index.ts b/react/features/base/meet/middlewares/connection-stability/index.ts deleted file mode 100644 index bacad5c7140c..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Collection of middlewares to handle connection stability and user notifications. - * - Poor connection detection when joining meetings - * - DataChannel reconnection detection and user notifications - * - XMPP/WebSocket connection failure handling (via lib-jitsi-meet events) - * - Automatic reconnection when connection is lost unexpectedly - * - Error handling to prevent middleware crashes - * - Connection guard to prevent invalid events during disconnection - */ - -import './middleware.connection-guard'; -import './connection-notifications'; -import './middleware.datachannel'; -import './middleware.error-handling'; -import './middleware.poor-connection'; -import './middleware.auto-reconnect'; - -export { }; - -console.log('Connection stability middlewares loaded'); \ No newline at end of file diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts b/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts deleted file mode 100644 index 8f4bd01c70bf..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { batch } from "react-redux"; -import { AnyAction } from "redux"; -import { IStore } from "../../../../app/types"; -import { hideNotification } from "../../../../notifications/actions"; -import { CONFERENCE_WILL_LEAVE } from "../../../conference/actionTypes"; -import { isLeavingConferenceManually, setLeaveConferenceManually } from "../../general/utils/conferenceState"; -import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED, CONNECTION_FAILED } from "../../../connection/actionTypes"; -import { connect } from "../../../connection/actions.web"; -import { setJWT } from "../../../jwt/actions"; -import MiddlewareRegistry from "../../../redux/MiddlewareRegistry"; -import { trackRemoved } from "../../../tracks/actions.any"; -import { hideLoader, showLoader } from "../../loader"; - -const RECONNECTION_NOTIFICATION_ID = "connection.reconnecting"; -const RECONNECTION_LOADER_ID = "auto-reconnect"; -const RECONNECTION_WAIT_TIME_MS = 15000; -const MAX_RECONNECTION_ATTEMPTS = 2; -const RECONNECTION_DELAY_MS = 3000; -const JWT_EXPIRED_ERROR = "connection.passwordRequired"; - -let reconnectionTimer: number | null = null; -let isReconnecting = false; -let reconnectionAttempts = 0; - -export const isAutoReconnecting = () => isReconnecting; - -const hideReconnectionNotification = (store: IStore) => { - store.dispatch(hideNotification(RECONNECTION_NOTIFICATION_ID)); -}; - -const showReconnectionLoader = (store: IStore, attempt: number) => { - const textKey = attempt <= MAX_RECONNECTION_ATTEMPTS ? "loader.reconnecting" : "loader.reloading"; - - store.dispatch(showLoader(undefined, textKey, RECONNECTION_LOADER_ID)); -}; - -const hideReconnectionLoader = (store: IStore) => { - store.dispatch(hideLoader(RECONNECTION_LOADER_ID)); -}; - -const reloadPage = () => { - window.location.reload(); -}; - -const clearExpiredJWT = (store: IStore) => { - store.dispatch(setJWT(undefined)); -}; - -const clearRemoteTracks = (store: IStore) => { - const state = store.getState(); - const remoteTracks = state["features/base/tracks"].filter((t) => !t.local); - - batch(() => { - for (const track of remoteTracks) { - store.dispatch(trackRemoved(track.jitsiTrack)); - } - }); -}; - -const triggerReconnection = (store: IStore) => { - store.dispatch(connect()); -}; - -const scheduleRetry = (store: IStore) => { - reconnectionTimer = window.setTimeout(() => { - if (!isLeavingConferenceManually() && isReconnecting) { - attemptReconnection(store); - } - }, RECONNECTION_DELAY_MS); -}; - -const handleMaxAttemptsReached = (store: IStore) => { - isReconnecting = true; - showReconnectionLoader(store, reconnectionAttempts + 1); - reconnectionTimer = window.setTimeout(reloadPage, 2000); -}; - -/** - * Attempts to reconnect by clearing JWT and connecting to conference again. - * If max attempts reached, reloads the page. - */ -const attemptReconnection = async (store: IStore) => { - if (isLeavingConferenceManually()) return; - - if (reconnectionAttempts >= MAX_RECONNECTION_ATTEMPTS) { - handleMaxAttemptsReached(store); - return; - } - - reconnectionAttempts++; - isReconnecting = true; - showReconnectionLoader(store, reconnectionAttempts); - - try { - clearRemoteTracks(store); - clearExpiredJWT(store); - await new Promise((resolve) => setTimeout(resolve, 100)); - triggerReconnection(store); - scheduleRetry(store); - } catch (error) { - console.error("[AUTO_RECONNECT] Reconnection error:", error); - scheduleRetry(store); - } -}; - -const clearTimer = () => { - if (reconnectionTimer !== null) { - clearTimeout(reconnectionTimer); - reconnectionTimer = null; - } -}; - -const resetReconnectionState = () => { - clearTimer(); - reconnectionAttempts = 0; - isReconnecting = false; -}; - -/** - * Middleware that handles automatic reconnection when JWT expires or connection is lost. - */ -MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyAction) => { - const result = next(action); - - switch (action.type) { - case CONFERENCE_WILL_LEAVE: { - setLeaveConferenceManually(true); - resetReconnectionState(); - hideReconnectionNotification(store); - hideReconnectionLoader(store); - break; - } - - case CONNECTION_DISCONNECTED: { - if (isLeavingConferenceManually()) break; - - clearTimer(); - reconnectionAttempts = 0; - isReconnecting = true; - - reconnectionTimer = window.setTimeout(() => { - if (!isLeavingConferenceManually() && isReconnecting) { - attemptReconnection(store); - } - }, RECONNECTION_WAIT_TIME_MS); - - break; - } - - case CONNECTION_ESTABLISHED: { - if (isReconnecting) { - hideReconnectionNotification(store); - hideReconnectionLoader(store); - } - - resetReconnectionState(); - setLeaveConferenceManually(false); - break; - } - - case CONNECTION_FAILED: { - const { error } = action; - console.log("[AUTO_RECONNECT] Connection failed with error:", error); - if (error?.name === JWT_EXPIRED_ERROR && !isLeavingConferenceManually() && !isReconnecting) { - attemptReconnection(store); - } - - break; - } - } - - return result; -}); - -export default {}; diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.connection-guard.ts b/react/features/base/meet/middlewares/connection-stability/middleware.connection-guard.ts deleted file mode 100644 index 1de40c966a56..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/middleware.connection-guard.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { AnyAction } from "redux"; -import { DOMINANT_SPEAKER_CHANGED } from "../../../participants/actionTypes"; -import MiddlewareRegistry from "../../../redux/MiddlewareRegistry"; - -/** - * Middleware to prevent dominant speaker events when connection is unstable. - * This prevents the cascade of "Not connected" errors that kick users out. - */ -MiddlewareRegistry.register((store) => (next) => (action: AnyAction) => { - if (action.type === DOMINANT_SPEAKER_CHANGED) { - const state = store.getState(); - const { connection } = state["features/base/connection"]; - const { conference } = state["features/base/conference"]; - - // Check 1: Connection must exist - if (!connection) { - console.warn("Dominant speaker event suppressed - no connection"); - return action; - } - - // Check 2: Conference must exist and be joined - if (!conference) { - console.warn("Dominant speaker event suppressed - no conference"); - return action; - } - - // Check 3: Connection must be in a good state (has jitsi id) - try { - const isConnectionReady = connection && typeof connection.getJid === "function"; - if (isConnectionReady) { - const jid = connection.getJid(); - if (!jid) { - console.warn("Dominant speaker event suppressed - connection not ready (no JID)"); - return action; - } - } - } catch (error) { - console.warn("Dominant speaker event suppressed - connection error:", error); - return action; - } - } - - return next(action); -}); diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.datachannel.ts b/react/features/base/meet/middlewares/connection-stability/middleware.datachannel.ts deleted file mode 100644 index bd2efd10e74a..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/middleware.datachannel.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { AnyAction } from 'redux'; -import { IStore } from '../../../../app/types'; -import { hideNotification, showNotification } from '../../../../notifications/actions'; -import { NOTIFICATION_TIMEOUT_TYPE } from '../../../../notifications/constants'; -import { DATA_CHANNEL_CLOSED, DATA_CHANNEL_OPENED } from '../../../conference/actionTypes'; -import MiddlewareRegistry from '../../../redux/MiddlewareRegistry'; - -// Notification IDs -const DATACHANNEL_RECONNECTION_NOTIFICATION_ID = 'datachannel.reconnecting'; -const DATACHANNEL_FAILED_NOTIFICATION_ID = 'datachannel.failed'; - -// Timing constants -const RECONNECTION_TIMEOUT_MS = 15000; -const RESET_COUNTER_TIMEOUT_MS = 60000; -const PERSISTENT_ISSUE_THRESHOLD = 3; - -// State tracking -let isDataChannelClosed = false; -let dataChannelCloseCount = 0; -let resetCounterTimer: number | null = null; -let reconnectionTimeoutTimer: number | null = null; - -/** - * Clears all active timers. - */ -const clearAllTimers = () => { - if (resetCounterTimer) { - clearTimeout(resetCounterTimer); - resetCounterTimer = null; - } - if (reconnectionTimeoutTimer) { - clearTimeout(reconnectionTimeoutTimer); - reconnectionTimeoutTimer = null; - } -}; - -/** - * Resets the data channel state to initial values. - */ -const resetDataChannelState = () => { - dataChannelCloseCount = 0; - isDataChannelClosed = false; -}; - -/** - * Shows the initial data channel closed notification. - */ -const showDataChannelClosedNotification = (store: IStore) => { - store.dispatch( - showNotification( - { - titleKey: "notify.dataChannelClosed", - descriptionKey: "notify.dataChannelClosedDescription", - uid: DATACHANNEL_RECONNECTION_NOTIFICATION_ID, - }, - NOTIFICATION_TIMEOUT_TYPE.STICKY - ) - ); -}; - -/** - * Shows the reconnection failed notification after timeout. - */ -const showReconnectionFailedNotification = (store: IStore) => { - store.dispatch(hideNotification(DATACHANNEL_RECONNECTION_NOTIFICATION_ID)); - store.dispatch( - showNotification( - { - titleKey: "dialog.conferenceDisconnectTitle", - descriptionKey: "dialog.conferenceDisconnectMsg", - uid: DATACHANNEL_FAILED_NOTIFICATION_ID, - }, - NOTIFICATION_TIMEOUT_TYPE.STICKY - ) - ); -}; - -/** - * Shows the persistent issue notification when channel closes multiple times. - */ -const showPersistentIssueNotification = (store: IStore) => { - store.dispatch(hideNotification(DATACHANNEL_RECONNECTION_NOTIFICATION_ID)); - store.dispatch( - showNotification( - { - titleKey: "notify.connectionFailed", - descriptionKey: "dialog.conferenceDisconnectMsg", - uid: DATACHANNEL_FAILED_NOTIFICATION_ID, - }, - NOTIFICATION_TIMEOUT_TYPE.STICKY - ) - ); -}; - -/** - * Hides all data channel notifications. - */ -const hideAllNotifications = (store: IStore) => { - store.dispatch(hideNotification(DATACHANNEL_RECONNECTION_NOTIFICATION_ID)); - store.dispatch(hideNotification(DATACHANNEL_FAILED_NOTIFICATION_ID)); -}; - -/** - * Schedules a timeout to detect reconnection failure. - */ -const scheduleReconnectionTimeout = (store: IStore) => { - if (reconnectionTimeoutTimer) { - clearTimeout(reconnectionTimeoutTimer); - } - reconnectionTimeoutTimer = setTimeout(() => { - if (isDataChannelClosed) { - console.error(`Data channel failed to reconnect after ${RECONNECTION_TIMEOUT_MS / 1000} seconds`); - showReconnectionFailedNotification(store); - } - }, RECONNECTION_TIMEOUT_MS); -}; - -/** - * Schedules a timeout to reset the close counter. - */ -const scheduleCounterReset = () => { - if (resetCounterTimer) { - clearTimeout(resetCounterTimer); - } - resetCounterTimer = setTimeout(() => { - dataChannelCloseCount = 0; - }, RESET_COUNTER_TIMEOUT_MS); -}; - -/** - * Middleware to handle BridgeChannel (DataChannel) connection issues. - * When the DataChannel closes unexpectedly, notify the user. - * lib-jitsi-meet will automatically attempt to reconnect the datachannel. - */ -MiddlewareRegistry.register((store) => (next) => (action: AnyAction) => { - const result = next(action); - - switch (action.type) { - case DATA_CHANNEL_OPENED: { - console.log("Data channel opened successfully"); - - clearAllTimers(); - resetDataChannelState(); - - if (isDataChannelClosed) { - hideAllNotifications(store); - } - - break; - } - - case DATA_CHANNEL_CLOSED: { - const { code, reason } = action; - console.warn(`Data channel closed unexpectedly - code: ${code}, reason: ${reason}`); - - dataChannelCloseCount++; - isDataChannelClosed = true; - - const isPersistentIssue = dataChannelCloseCount >= PERSISTENT_ISSUE_THRESHOLD; - - if (isPersistentIssue) { - console.error(`Data channel closed ${dataChannelCloseCount} times, persistent connection issue`); - showPersistentIssueNotification(store); - } else { - showDataChannelClosedNotification(store); - scheduleReconnectionTimeout(store); - } - - scheduleCounterReset(); - - break; - } - } - - return result; -}); - -// Export something to prevent tree-shaking -export default {}; diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.error-handling.ts b/react/features/base/meet/middlewares/connection-stability/middleware.error-handling.ts deleted file mode 100644 index 9dc88606b0c4..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/middleware.error-handling.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AnyAction } from 'redux'; -import MiddlewareRegistry from '../../../redux/MiddlewareRegistry'; - -/** - * Middleware to handle errors gracefully during action processing. - * Prevents middleware crashes from breaking the entire Redux flow. - */ -MiddlewareRegistry.register(() => next => (action: AnyAction) => { - try { - return next(action); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error('Middleware error:', errorMessage, 'for action:', action.type); - return undefined; - } -}); diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.poor-connection.ts b/react/features/base/meet/middlewares/connection-stability/middleware.poor-connection.ts deleted file mode 100644 index 5f8f0f395638..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/middleware.poor-connection.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { AnyAction } from 'redux'; -import { IStore } from '../../../../app/types'; -import { showNotification, hideNotification } from '../../../../notifications/actions'; -import { NOTIFICATION_TIMEOUT_TYPE } from '../../../../notifications/constants'; -import { CONFERENCE_JOINED, CONFERENCE_WILL_LEAVE } from '../../../conference/actionTypes'; -import { getLocalParticipant } from '../../../participants/functions'; -import MiddlewareRegistry from '../../../redux/MiddlewareRegistry'; -import statsEmitter from '../../../../connection-indicator/statsEmitter'; - -const POOR_CONNECTION_NOTIFICATION_ID = 'connection.poor'; -const GOOD_CONNECTION_THRESHOLD = 30; -const MIN_TIME_BETWEEN_WARNINGS_MS = 60000; - -let conferenceJoinTime: number | null = null; -let lastWarningTime: number | null = null; -let isNotificationCurrentlyShown = false; -let isSubscribedToStats = false; - -interface IConnectionStats { - connectionQuality?: number; - bandwidth?: { - download?: number; - upload?: number; - }; - bitrate?: { - download?: number; - upload?: number; - }; -} - -const showPoorConnectionWarning = (store: IStore) => { - const now = Date.now(); - - if (lastWarningTime && (now - lastWarningTime) < MIN_TIME_BETWEEN_WARNINGS_MS) { - return; - } - - store.dispatch( - showNotification( - { - titleKey: 'notify.poorConnection', - descriptionKey: 'notify.poorConnectionDescription', - uid: POOR_CONNECTION_NOTIFICATION_ID, - }, - NOTIFICATION_TIMEOUT_TYPE.LONG - ) - ); - - lastWarningTime = now; - isNotificationCurrentlyShown = true; -}; - -const hidePoorConnectionWarning = (store: IStore) => { - if (!isNotificationCurrentlyShown) { - return; - } - - store.dispatch(hideNotification(POOR_CONNECTION_NOTIFICATION_ID)); - isNotificationCurrentlyShown = false; -}; - -const checkConnectionQuality = (store: IStore, connectionQuality: number) => { - if (!conferenceJoinTime) { - return; - } - - if (connectionQuality < GOOD_CONNECTION_THRESHOLD) { - showPoorConnectionWarning(store); - } else { - hidePoorConnectionWarning(store); - } -}; - -const onStatsUpdated = (store: IStore) => (stats: IConnectionStats) => { - if (!conferenceJoinTime) { - return; - } - - const connectionQuality = stats.connectionQuality; - - if (typeof connectionQuality === 'number') { - checkConnectionQuality(store, connectionQuality); - } -}; - -MiddlewareRegistry.register((store: IStore) => (next) => (action: AnyAction) => { - const result = next(action); - - switch (action.type) { - case CONFERENCE_JOINED: { - const state = store.getState(); - const localParticipant = getLocalParticipant(state); - - if (!localParticipant) { - break; - } - - conferenceJoinTime = Date.now(); - lastWarningTime = null; - isNotificationCurrentlyShown = false; - - if (localParticipant.id && !isSubscribedToStats) { - statsEmitter.subscribeToClientStats(localParticipant.id, onStatsUpdated(store)); - isSubscribedToStats = true; - } - - break; - } - - case CONFERENCE_WILL_LEAVE: { - // User manually hung up - hide notification and reset state - hidePoorConnectionWarning(store); - conferenceJoinTime = null; - lastWarningTime = null; - isNotificationCurrentlyShown = false; - break; - } - } - - return result; -}); - -export default {}; diff --git a/react/features/base/meet/middlewares/index.ts b/react/features/base/meet/middlewares/index.ts index bcc0dd6f96de..5a70a5bfac2b 100644 --- a/react/features/base/meet/middlewares/index.ts +++ b/react/features/base/meet/middlewares/index.ts @@ -1,3 +1,2 @@ export * from "./meeting.middleware"; -import './connection-stability'; diff --git a/react/features/base/meet/middlewares/meeting.middleware.test.ts b/react/features/base/meet/middlewares/meeting.middleware.test.ts deleted file mode 100644 index fcb58481c4c6..000000000000 --- a/react/features/base/meet/middlewares/meeting.middleware.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import MiddlewareRegistry from "../../redux/MiddlewareRegistry"; -import { setPlanName, setUserTier, updateMeetingConfig } from "../general/store/meeting/actions"; -import { MEETING_REDUCER } from "../general/store/meeting/reducer"; -import { LocalStorageManager } from "../LocalStorageManager"; -import { PaymentsService } from "../services/payments.service"; -import { AUTH_ACTIONS, meetingConfigMiddleware } from "./meeting.middleware"; - -vi.mock("../../redux/MiddlewareRegistry", () => ({ - default: { - register: vi.fn(), - }, -})); - -vi.mock("../general/store/meeting/actions", () => ({ - updateMeetingConfig: vi.fn((config) => ({ - type: "UPDATE_MEETING_CONFIG", - payload: config, - })), - setPlanName: vi.fn((planName) => ({ - type: "SET_PLAN_NAME", - payload: { planName }, - })), - setUserTier: vi.fn((userTier) => ({ - type: "SET_USER_TIER", - payload: { userTier }, - })), -})); - -vi.mock("../LocalStorageManager", () => { - const mockInstance = { - get: vi.fn(), - set: vi.fn(), - clearStorage: vi.fn(), - clearCredentials: vi.fn(), - getUser: vi.fn(), - getToken: vi.fn(), - }; - return { - default: mockInstance, - LocalStorageManager: { instance: mockInstance }, - STORAGE_KEYS: { - LAST_CONFIG_CHECK: "lastMeetingConfigCheck", - CACHED_MEETING_CONFIG: "cachedMeetingConfig", - LAST_USER_REFRESH: "lastUserRefresh", - }, - }; -}); - -vi.mock("../services/payments.service", () => ({ - PaymentsService: { - instance: { - getUserTier: vi.fn(), - checkMeetAvailability: vi.fn(), - }, - }, -})); - -// TODO: UNCOMMENT COMMENTED TESTS WHEN MEET BACKEND IS READY -describe("meetingConfigMiddleware", () => { - const originalConsoleError = console.error; - const originalConsoleInfo = console.info; - - const dispatchMock = vi.fn(); - const nextMock = vi.fn((action) => action); - const getStateMock = vi.fn(); - const storeMock = { - dispatch: dispatchMock, - getState: getStateMock, - }; - - const sampleUserTier = { - id: "tier-123", - label: "premium", - productId: "product-456", - billingType: "subscription" as const, - featuresPerService: { - meet: { - enabled: true, - paxPerCall: 10, - }, - drive: {}, - backups: {}, - antivirus: {}, - mail: {}, - vpn: {}, - cleaner: {}, - }, - }; - - beforeEach(() => { - console.error = vi.fn(); - console.info = vi.fn(); - - vi.clearAllMocks(); - - vi.spyOn(Date, "now").mockImplementation(() => 1600000000000); - - (LocalStorageManager.instance.get as ReturnType).mockReturnValue(0); - - (PaymentsService.instance.getUserTier as ReturnType).mockResolvedValue(sampleUserTier); - - getStateMock.mockReturnValue({ - [MEETING_REDUCER]: { - enabled: false, - }, - }); - }); - - afterEach(() => { - console.error = originalConsoleError; - console.info = originalConsoleInfo; - }); - - describe("Middleware Registration", () => { - it("When middleware is initialized, then it should register with MiddlewareRegistry", () => { - MiddlewareRegistry.register(meetingConfigMiddleware); - expect(MiddlewareRegistry.register).toHaveBeenCalledWith(meetingConfigMiddleware); - }); - }); - - describe("Action Handling", () => { - it("When any action is passed, then it should pass the action to next middleware", () => { - const action = { type: "TEST_ACTION" }; - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - middleware(action); - expect(nextMock).toHaveBeenCalledWith(action); - }); - }); - - describe("LOGIN_SUCCESS Action", () => { - it("When LOGIN_SUCCESS action is dispatched, then it should update meeting config and plan name", async () => { - const action = { type: AUTH_ACTIONS.LOGIN_SUCCESS }; - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - middleware(action); - - await vi.waitFor(() => { - expect(PaymentsService.instance.getUserTier).toHaveBeenCalled(); - }); - - expect(PaymentsService.instance.getUserTier).toHaveBeenCalledTimes(1); - expect(updateMeetingConfig).toHaveBeenCalledWith({ - enabled: true, - paxPerCall: 10, - }); - expect(setPlanName).toHaveBeenCalledWith("premium"); - expect(setUserTier).toHaveBeenCalledWith(sampleUserTier); - expect(LocalStorageManager.instance.set).toHaveBeenCalledWith("lastMeetingConfigCheck", expect.any(Number)); - }); - - it("When LOGIN_SUCCESS action is dispatched and last check was recent, then it should still force update", async () => { - const action = { type: AUTH_ACTIONS.LOGIN_SUCCESS }; - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - - const recentCheckTime = Date.now() - 1000; - (LocalStorageManager.instance.get as ReturnType).mockReturnValue(recentCheckTime); - - middleware(action); - - await vi.waitFor(() => { - expect(PaymentsService.instance.getUserTier).toHaveBeenCalled(); - }); - - expect(PaymentsService.instance.getUserTier).toHaveBeenCalled(); - }); - }); - - describe("REFRESH_TOKEN_SUCCESS Action", () => { - it("When REFRESH_TOKEN_SUCCESS action is dispatched and interval has expired, then it should update meeting config", async () => { - const action = { type: AUTH_ACTIONS.REFRESH_TOKEN_SUCCESS }; - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - - const oldCheckTime = Date.now() - 61 * 60 * 1000; - (LocalStorageManager.instance.get as ReturnType).mockReturnValue(oldCheckTime); - - middleware(action); - - await vi.waitFor(() => { - expect(PaymentsService.instance.getUserTier).toHaveBeenCalled(); - }); - - expect(updateMeetingConfig).toHaveBeenCalled(); - expect(setPlanName).toHaveBeenCalled(); - expect(setUserTier).toHaveBeenCalled(); - }); - - it("When REFRESH_TOKEN_SUCCESS action is dispatched and interval has not expired, then it should not update meeting config", async () => { - const action = { type: AUTH_ACTIONS.REFRESH_TOKEN_SUCCESS }; - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - - const recentCheckTime = Date.now() - 1000; - (LocalStorageManager.instance.get as ReturnType).mockReturnValue(recentCheckTime); - - middleware(action); - - await vi.waitFor(() => {}, { timeout: 100 }); - - expect(PaymentsService.instance.getUserTier).not.toHaveBeenCalled(); - }); - }); - - describe("INITIALIZE_AUTH Action", () => { - it("When INITIALIZE_AUTH action is dispatched with authenticated user and meeting not enabled, then it should force update", async () => { - const action = { - type: AUTH_ACTIONS.INITIALIZE_AUTH, - payload: { isAuthenticated: true }, - }; - - getStateMock.mockReturnValue({ - [MEETING_REDUCER]: { - enabled: false, - }, - }); - - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - middleware(action); - - await vi.waitFor(() => { - expect(PaymentsService.instance.getUserTier).toHaveBeenCalled(); - }); - - expect(updateMeetingConfig).toHaveBeenCalled(); - expect(setPlanName).toHaveBeenCalled(); - expect(setUserTier).toHaveBeenCalled(); - }); - - it("When INITIALIZE_AUTH action is dispatched with authenticated user, meeting enabled, and interval not expired, then it should not update", async () => { - const action = { - type: AUTH_ACTIONS.INITIALIZE_AUTH, - payload: { isAuthenticated: true }, - }; - - getStateMock.mockReturnValue({ - [MEETING_REDUCER]: { - enabled: true, - }, - }); - - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - - const recentCheckTime = Date.now() - 1000; - (LocalStorageManager.instance.get as ReturnType).mockReturnValue(recentCheckTime); - - middleware(action); - - await vi.waitFor(() => {}, { timeout: 100 }); - - expect(PaymentsService.instance.getUserTier).not.toHaveBeenCalled(); - }); - - it("When INITIALIZE_AUTH action is dispatched with non-authenticated user, then it should not update meeting config", async () => { - const action = { - type: AUTH_ACTIONS.INITIALIZE_AUTH, - payload: { isAuthenticated: false }, - }; - - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - middleware(action); - - await vi.waitFor(() => {}, { timeout: 100 }); - - expect(PaymentsService.instance.getUserTier).not.toHaveBeenCalled(); - }); - }); - - describe("LOGOUT Action", () => { - it("When LOGOUT action is dispatched, then it should clear storage and credentials", () => { - const action = { type: AUTH_ACTIONS.LOGOUT }; - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - middleware(action); - - expect(LocalStorageManager.instance.clearCredentials).toHaveBeenCalledTimes(1); - }); - - it("When LOGOUT action is dispatched and clearStorage fails, then it should handle the error", () => { - const action = { type: AUTH_ACTIONS.LOGOUT }; - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - - vi.spyOn(LocalStorageManager.instance, 'clearStorage' as any).mockImplementation(() => { - throw new Error("localStorage error"); - }); - - middleware(action); - - expect(console.error).toHaveBeenCalledWith( - "Error clearing cached data from localStorage", - expect.any(Error) - ); - }); - }); - - describe("Error Handling", () => { - it("When updating meeting config and API call fails, then it should handle the error", async () => { - const action = { type: AUTH_ACTIONS.LOGIN_SUCCESS }; - const middleware = meetingConfigMiddleware(storeMock)(nextMock); - - (PaymentsService.instance.getUserTier as ReturnType).mockRejectedValue( - new Error("API error") - ); - - middleware(action); - - await vi.waitFor(() => { - expect(console.error).toHaveBeenCalled(); - }); - - expect(console.error).toHaveBeenCalledWith("Error checking meeting configuration", expect.any(Error)); - }); - }); -}); diff --git a/yarn.lock b/yarn.lock index 3a191af5bdac..3b282a6fcde0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11081,9 +11081,9 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -"lib-jitsi-meet@https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20/lib-jitsi-meet-0.0.20.tgz": - version "0.0.20" - resolved "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20/lib-jitsi-meet-0.0.20.tgz#5048eba36fa1f6b1884c00d6d5a21e847c9d2c46" +"lib-jitsi-meet@https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20-debug/lib-jitsi-meet-0.0.20-debug.tgz": + version "0.0.20-debug" + resolved "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20-debug/lib-jitsi-meet-0.0.20-debug.tgz#3e5a6b196a215d2dc449c588c23ad32bded2b1de" dependencies: "@hexagon/base64" "^2.0.4" "@jitsi/js-utils" "^2.6.7"