Files
upstream-jitsi-meet/react/features/audio-translation/middleware.any.ts
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

169 lines
6.9 KiB
TypeScript

import { JitsiAudioTranslationErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { showErrorNotification, showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import {
CLEAR_AUDIO_TRANSLATION,
SET_AUDIO_TRANSLATION_LANGUAGE,
SET_PARTICIPANT_AUDIO_TRANSLATION_LANGUAGE
} from './actionTypes';
import {
clearAudioTranslation,
clearReceivingTranslatedSources,
setTranslationListeners,
updateTranslatedSourceSending
} from './actions';
import { isAudioTranslationRoomEnabled } from './functions';
import logger from './logger';
/**
* Maps an audio-translation component error condition to a notification message key. Unmapped conditions
* fall back to {@link DEFAULT_ERROR_KEY}.
*/
const ERROR_NOTIFICATION_KEYS: { [condition: string]: string; } = {
[JitsiAudioTranslationErrors.FORBIDDEN]: 'audioTranslation.errorForbidden',
[JitsiAudioTranslationErrors.SPEAKER_UNAVAILABLE]: 'audioTranslation.errorSpeakerUnavailable',
[JitsiAudioTranslationErrors.SUBSCRIPTION_LIMIT_REACHED]: 'audioTranslation.errorLimitReached'
};
const DEFAULT_ERROR_KEY = 'audioTranslation.errorGeneric';
/**
* Middleware that drives the bridge-side translation when the local user changes
* the default or a per-participant audio-translation target language.
*/
MiddlewareRegistry.register(store => next => action => {
// Block enabling a translation language while a moderator has disabled the feature for the room. Managers
// keep the translation UI even when the room flag is off, and the bridge silently drops such requests, so
// surface a notification here instead. Disabling (language === null) is always allowed through.
if ((action.type === SET_AUDIO_TRANSLATION_LANGUAGE || action.type === SET_PARTICIPANT_AUDIO_TRANSLATION_LANGUAGE)
&& action.language
&& !isAudioTranslationRoomEnabled(store.getState())) {
store.dispatch(showErrorNotification({
titleKey: 'audioTranslation.errorRoomDisabled'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
return;
}
const result = next(action);
switch (action.type) {
case CLEAR_AUDIO_TRANSLATION: {
const { conference } = store.getState()['features/base/conference'];
if (conference) {
logger.info('Clearing all audio translation state');
conference.clearTranslation();
}
break;
}
case SET_AUDIO_TRANSLATION_LANGUAGE: {
const { conference } = store.getState()['features/base/conference'];
if (conference) {
logger.info(`Setting audio translation language to ${action.language ?? 'off'}`);
conference.setReceiverTranslationLanguage(action.language);
}
break;
}
case SET_PARTICIPANT_AUDIO_TRANSLATION_LANGUAGE: {
const { conference } = store.getState()['features/base/conference'];
if (conference) {
logger.info(`Setting audio translation language for ${action.participantId} to ${action.language ?? 'off'}`);
conference.setParticipantTranslationLanguage(action.participantId, action.language);
}
break;
}
}
return result;
});
/**
* Surfaces audio-translation request failures reported by the bridge-side component: shows a notification and
* resets translation state (redux + lib-jitsi-meet) so the UI reflects the failure and re-enabling works.
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch }, previousConference) => {
if (!conference || previousConference) {
return;
}
conference.on(JitsiConferenceEvents.AUDIO_TRANSLATION_FAILED,
({ endpointIds, error }: { endpointIds: string[]; error: string; }) => {
logger.warn(`Audio translation request failed: ${error} for [${endpointIds.join(', ')}]`);
dispatch(showErrorNotification({
titleKey: ERROR_NOTIFICATION_KEYS[error] ?? DEFAULT_ERROR_KEY
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
// The optimistic selection didn't take. Reset everything: clears redux (UI reflects off)
// and, via conference.clearTranslation(), resets lib-jitsi-meet's language state so
// re-enabling isn't deduped to a no-op.
dispatch(clearAudioTranslation());
});
});
/**
* Ingests the two per-participant translation signals from the conference: which translated sources the bridge
* is forwarding to us (receiving), and which remote participants are translating us (enabled). Both are cleared
* when the conference goes away.
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch, getState }, previousConference) => {
if (previousConference) {
dispatch(clearReceivingTranslatedSources());
dispatch(setTranslationListeners([]));
}
if (!conference) {
return;
}
conference.on(JitsiConferenceEvents.TRANSLATED_SOURCE_SENDING_CHANGED,
({ sending, sourceName, timestamp }: { sending: boolean; sourceName: string; timestamp: number; }) => {
// The bridge currently emits only sending=true, which would leave the receiving state
// stuck on; ignore the events unless explicitly enabled.
if (!getState()['features/base/config'].audioTranslation?.enableSendingChangeEvents) {
return;
}
dispatch(updateTranslatedSourceSending(sourceName, sending, timestamp));
});
conference.on(JitsiConferenceEvents.AUDIO_TRANSLATION_LISTENERS_CHANGED,
(ids: string[]) => {
dispatch(setTranslationListeners(ids));
});
});
/**
* When a moderator disables audio translation for the room, clear the local user's active translation and notify
* them. The room flag only blocks new requests on the bridge; it does not tear down active subscriptions. Clearing
* locally drops the synthetic-source Include subscription (so the bridge stops forwarding the translated audio) and
* un-ducks the original.
*/
StateListenerRegistry.register(
state => isAudioTranslationRoomEnabled(state),
(roomEnabled, { dispatch, getState }) => {
if (roomEnabled) {
return;
}
const { language, participantLanguages } = getState()['features/audio-translation'];
if (language || Object.keys(participantLanguages).length > 0) {
dispatch(clearAudioTranslation());
dispatch(showNotification({
titleKey: 'audioTranslation.disabledForRoom'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}
});