Files
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

609 lines
20 KiB
TypeScript

import { IStore } from '../app/types';
import { getMeetingRegion, getRecordingSharingUrl } from '../base/config/functions';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
import { BUTTON_TYPES } from '../base/ui/constants.any';
import { copyText } from '../base/util/copyText';
import { getVpaasTenant, isVpaasMeeting } from '../jaas/functions';
import {
hideNotification,
showErrorNotification,
showNotification,
showWarningNotification
} from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
import { INotificationProps } from '../notifications/types';
import { setRequestingSubtitles } from '../subtitles/actions.any';
import { isRecorderTranscriptionsRunning } from '../transcribing/functions';
import {
CLEAR_RECORDING_SESSIONS,
MARK_CONSENT_REQUESTED,
RECORDING_SESSION_UPDATED,
SET_LOCAL_RECORDING_RUNNING,
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_SELECTED_RECORDING_SERVICE,
SET_START_RECORDING_INTENT,
SET_START_RECORDING_NOTIFICATION_SHOWN,
SET_STOP_RECORDING_INTENT,
SET_STREAM_KEY,
START_LOCAL_RECORDING,
STOP_LOCAL_RECORDING
} from './actionTypes';
import { RECORDING_METADATA_ID, START_RECORDING_NOTIFICATION_ID } from './constants';
import {
getRecordButtonProps,
getRecordingLink,
getResourceId,
isRecordingRunning,
isRecordingSharingEnabled,
isSavingRecordingOnDropbox,
sendMeetingHighlight,
shouldAutoTranscribeOnRecord
} from './functions';
import logger from './logger';
import { IStartRecordingIntent, IStopRecordingIntent } from './reducer';
/**
* Clears the data of every recording sessions.
*
* @returns {{
* type: CLEAR_RECORDING_SESSIONS
* }}
*/
export function clearRecordingSessions() {
return {
type: CLEAR_RECORDING_SESSIONS
};
}
/**
* Marks the start recording notification as shown.
*
* @returns {{
* type: SET_START_RECORDING_NOTIFICATION_SHOWN
* }}
*/
export function setStartRecordingNotificationShown() {
return {
type: SET_START_RECORDING_NOTIFICATION_SHOWN
};
}
/**
* Sets the meeting highlight button disable state.
*
* @param {boolean} disabled - The disabled state value.
* @returns {{
* type: CLEAR_RECORDING_SESSIONS
* }}
*/
export function setHighlightMomentButtonState(disabled: boolean) {
return {
type: SET_MEETING_HIGHLIGHT_BUTTON_STATE,
disabled
};
}
/**
* Signals that the pending recording notification should be removed from the
* screen.
*
* @param {string} streamType - The type of the stream ({@code 'file'} or
* {@code 'stream'}).
* @returns {Function}
*/
export function hidePendingRecordingNotification(streamType: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { pendingNotificationUids } = getState()['features/recording'];
const pendingNotificationUid = pendingNotificationUids[streamType];
if (pendingNotificationUid) {
dispatch(hideNotification(pendingNotificationUid));
dispatch(
_setPendingRecordingNotificationUid(
undefined, streamType));
}
};
}
/**
* Sets the stream key last used by the user for later reuse.
*
* @param {string} streamKey - The stream key to set.
* @returns {{
* type: SET_STREAM_KEY,
* streamKey: string
* }}
*/
export function setLiveStreamKey(streamKey: string) {
return {
type: SET_STREAM_KEY,
streamKey
};
}
/**
* Signals that the pending recording notification should be shown on the
* screen.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {Function}
*/
export function showPendingRecordingNotification(streamType: string) {
return (dispatch: IStore['dispatch']) => {
const isLiveStreaming
= streamType === JitsiMeetJS.constants.recording.mode.STREAM;
const dialogProps = isLiveStreaming ? {
descriptionKey: 'liveStreaming.pending',
titleKey: 'dialog.liveStreaming'
} : {
descriptionKey: 'recording.pending',
titleKey: 'dialog.recording'
};
const notification = dispatch(showNotification({
...dialogProps
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
if (notification) {
dispatch(_setPendingRecordingNotificationUid(notification.uid, streamType));
}
};
}
/**
* Highlights a meeting moment.
*
* {@code stream}).
*
* @returns {Function}
*/
export function highlightMeetingMoment() {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
dispatch(setHighlightMomentButtonState(true));
const success = await sendMeetingHighlight(getState());
if (success) {
dispatch(showNotification({
descriptionKey: 'recording.highlightMomentSucessDescription',
titleKey: 'recording.highlightMomentSuccess'
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
dispatch(setHighlightMomentButtonState(false));
};
}
/**
* Signals that the recording error notification should be shown.
*
* @param {Object} props - The Props needed to render the notification.
* @returns {showErrorNotification}
*/
export function showRecordingError(props: Object) {
return showErrorNotification(props);
}
/**
* Signals that the recording warning notification should be shown.
*
* @param {Object} props - The Props needed to render the notification.
* @returns {showWarningNotification}
*/
export function showRecordingWarning(props: Object) {
return showWarningNotification(props);
}
/**
* Signals that the stopped recording notification should be shown on the
* screen for a given period.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @param {string?} participantName - The participant name stopping the recording.
* @param {boolean} wasWithTranscription - Whether transcription was active alongside
* recording when it was started.
* @returns {showNotification}
*/
export function showStoppedRecordingNotification(
streamType: string, participantName?: string, wasWithTranscription?: boolean) {
const isLiveStreaming
= streamType === JitsiMeetJS.constants.recording.mode.STREAM;
const descriptionArguments = { name: participantName };
let dialogProps;
if (isLiveStreaming) {
dialogProps = {
descriptionKey: participantName ? 'liveStreaming.offBy' : 'liveStreaming.off',
descriptionArguments,
titleKey: 'dialog.liveStreaming'
};
} else if (wasWithTranscription) {
dialogProps = {
descriptionKey: participantName ? 'recording.offByWithTranscription' : 'recording.offWithTranscription',
descriptionArguments,
titleKey: 'dialog.recording'
};
} else {
dialogProps = {
descriptionKey: participantName ? 'recording.offBy' : 'recording.off',
descriptionArguments,
titleKey: 'dialog.recording'
};
}
return showNotification(dialogProps, NOTIFICATION_TIMEOUT_TYPE.SHORT);
}
/**
* Signals that a started recording notification should be shown on the
* screen for a given period.
*
* @param {string} mode - The type of the recording: Stream of File.
* @param {string | Object } initiator - The participant who started recording.
* @param {string} sessionId - The recording session id.
* @param {boolean} willTranscribe - Whether transcription is/will be active alongside recording,
* as determined by the middleware when the sound was played. Ensures the notification text matches
* the audio cue.
* @returns {Function}
*/
export interface INudge {
actionNameKey: string;
descriptionText: string;
handler: () => void;
}
export function showStartedRecordingNotification(
mode: string,
initiator: { getId: Function; } | string,
sessionId: string,
willTranscribe?: boolean,
nudge?: INudge) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const initiatorId = getResourceId(initiator);
const participantName = getParticipantDisplayName(state, initiatorId);
const notifyProps: {
dialogProps: INotificationProps;
type: string;
} = {
dialogProps: {
descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
descriptionArguments: { name: participantName },
titleKey: 'dialog.liveStreaming'
},
type: NOTIFICATION_TIMEOUT_TYPE.SHORT
};
if (mode !== JitsiMeetJS.constants.recording.mode.STREAM) {
const recordingSharingUrl = getRecordingSharingUrl(state);
const iAmRecordingInitiator = getLocalParticipant(state)?.id === initiatorId;
const { showRecordingLink } = state['features/base/config'].recordings || {};
// Use the willTranscribe value passed from the middleware for consistency
// with the sound that was already played. Fall back to live state check
// if not provided.
const isTranscribing = willTranscribe ?? isRecorderTranscriptionsRunning(state);
const isRecording = isRecordingRunning(state);
// Case 1: Transcription only (no recording)
if (isTranscribing && !isRecording) {
notifyProps.dialogProps = {
customActionHandler: undefined,
customActionNameKey: undefined,
descriptionKey: participantName ? 'transcribing.onBy' : 'transcribing.on',
descriptionArguments: { name: participantName },
titleKey: 'dialog.recording'
};
} else if (isTranscribing && isRecording) {
// Case 2: Recording + transcription
notifyProps.dialogProps = {
customActionHandler: undefined,
customActionNameKey: undefined,
descriptionKey: participantName ? 'recording.onByWithTranscription' : 'recording.onWithTranscription',
descriptionArguments: { name: participantName },
titleKey: 'dialog.recording'
};
} else {
// Case 3: Recording only (no transcription)
notifyProps.dialogProps = {
customActionHandler: undefined,
customActionNameKey: undefined,
descriptionKey: participantName ? 'recording.onBy' : 'recording.on',
descriptionArguments: { name: participantName },
titleKey: 'dialog.recording'
};
}
// Merge nudge action when only one service started.
if (nudge) {
notifyProps.dialogProps = {
...notifyProps.dialogProps,
description: nudge.descriptionText,
customActionNameKey: [ nudge.actionNameKey ],
customActionHandler: [ nudge.handler ]
};
notifyProps.type = NOTIFICATION_TIMEOUT_TYPE.LONG;
}
// fetch the recording link from the server for recording initiators in jaas meetings
if (recordingSharingUrl
&& isVpaasMeeting(state)
&& iAmRecordingInitiator
&& !isSavingRecordingOnDropbox(state)) {
const region = getMeetingRegion(state);
const tenant = getVpaasTenant(state);
try {
const response = await getRecordingLink(recordingSharingUrl, sessionId, region, tenant);
const { url: link, urlExpirationTimeMillis: ttl } = response;
if (typeof APP === 'object') {
APP.API.notifyRecordingLinkAvailable(link, ttl);
}
// add the option to copy recording link
if (showRecordingLink) {
const actions = [
...notifyProps.dialogProps.customActionNameKey ?? [],
'recording.copyLink'
];
const handlers = [
...notifyProps.dialogProps.customActionHandler ?? [],
() => copyText(link)
];
notifyProps.dialogProps = {
...notifyProps.dialogProps,
customActionNameKey: actions,
customActionHandler: handlers,
titleKey: 'recording.on',
descriptionKey: 'recording.linkGenerated'
};
notifyProps.type = NOTIFICATION_TIMEOUT_TYPE.STICKY;
}
} catch (err) {
dispatch(showErrorNotification({
titleKey: 'recording.errorFetchingLink'
}));
return logger.error('Could not fetch recording link', err);
}
}
}
dispatch(showNotification(notifyProps.dialogProps, notifyProps.type));
};
}
/**
* Updates the known state for a given recording session.
*
* @param {Object} session - The new state to merge with the existing state in
* redux.
* @returns {{
* type: RECORDING_SESSION_UPDATED,
* sessionData: Object
* }}
*/
export function updateRecordingSessionData(session: any) {
const status = session.getStatus();
const timestamp
= status === JitsiRecordingConstants.status.ON
? Date.now() / 1000
: undefined;
return {
type: RECORDING_SESSION_UPDATED,
sessionData: {
error: session.getError(),
id: session.getID(),
initiator: session.getInitiator(),
liveStreamViewURL: session.getLiveStreamViewURL(),
mode: session.getMode(),
status,
terminator: session.getTerminator(),
timestamp
}
};
}
/**
* Sets the selected recording service.
*
* @param {string} selectedRecordingService - The new selected recording service.
* @returns {Object}
*/
export function setSelectedRecordingService(selectedRecordingService: string) {
return {
type: SET_SELECTED_RECORDING_SERVICE,
selectedRecordingService
};
}
/**
* Sets UID of the the pending streaming notification to use it when hinding
* the notification is necessary, or unsets it when undefined (or no param) is
* passed.
*
* @param {?number} uid - The UID of the notification.
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {{
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
* streamType: string,
* uid: number
* }}
*/
function _setPendingRecordingNotificationUid(uid: string | undefined, streamType: string) {
return {
type: SET_PENDING_RECORDING_NOTIFICATION_UID,
streamType,
uid
};
}
/**
* Starts local recording.
*
* @param {boolean} onlySelf - Whether to only record the local streams.
* @returns {Object}
*/
export function startLocalVideoRecording(onlySelf?: boolean) {
return {
type: START_LOCAL_RECORDING,
onlySelf
};
}
/**
* Stops local recording.
*
* @returns {Object}
*/
export function stopLocalVideoRecording() {
return {
type: STOP_LOCAL_RECORDING
};
}
/**
* Displays the notification suggesting to start the recording.
*
* @param {Function} openRecordingDialog - The callback to open the recording dialog.
* @returns {void}
*/
export function showStartRecordingNotificationWithCallback(openRecordingDialog: Function) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
let state = getState();
const { recordings } = state['features/base/config'];
const { suggestRecording } = recordings || {};
const recordButtonProps = getRecordButtonProps(state);
const isAlreadyRecording = isRecordingRunning(state) || isRecorderTranscriptionsRunning(state);
const wasNotificationShown = state['features/recording'].wasStartRecordingSuggested;
if (!suggestRecording
|| isAlreadyRecording
|| !recordButtonProps.visible
|| recordButtonProps.disabled
|| wasNotificationShown) {
return;
}
dispatch(setStartRecordingNotificationShown());
dispatch(showNotification({
titleKey: 'notify.suggestRecordingTitle',
descriptionKey: 'notify.suggestRecordingDescription',
uid: START_RECORDING_NOTIFICATION_ID,
customActionType: [ BUTTON_TYPES.PRIMARY ],
customActionNameKey: [ 'notify.suggestRecordingAction' ],
customActionHandler: [ () => {
state = getState();
const { recordingService } = state['features/base/config'];
const canBypassDialog = recordingService?.enabled
&& isJwtFeatureEnabled(state, MEET_FEATURES.RECORDING, false);
if (canBypassDialog) {
const options = {
'file_recording_metadata': {
share: isRecordingSharingEnabled(state)
}
};
const { conference } = state['features/base/conference'];
const autoTranscribeOnRecord = shouldAutoTranscribeOnRecord(state);
dispatch(setStartRecordingIntent({
recording: true,
transcription: autoTranscribeOnRecord
}));
conference?.startRecording({
mode: JitsiRecordingConstants.mode.FILE,
appData: JSON.stringify(options)
});
if (autoTranscribeOnRecord) {
dispatch(setRequestingSubtitles(true, false, null, false, true));
} else {
conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isRecordingRequested: true,
isTranscribingEnabled: false
});
}
} else {
openRecordingDialog();
}
dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
} ],
appearance: NOTIFICATION_TYPE.NORMAL
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
};
}
/**
* Marks the given session as consent requested. No further consent requests will be
* made for this session.
*
* @param {string} sessionId - The session id.
* @returns {Object}
*/
export function markConsentRequested(sessionId: string) {
return {
type: MARK_CONSENT_REQUESTED,
sessionId
};
}
/**
* Sets the user's recording + transcription intent from the dialog.
* Used to coordinate sound/notification timing when both services are requested.
*
* @param {Object|null} intent - The intent, or null to clear.
* @returns {Object}
*/
export function setStartRecordingIntent(intent: IStartRecordingIntent | null) {
return {
type: SET_START_RECORDING_INTENT,
intent
};
}
/**
* Sets what the user is stopping (recording and/or transcription). Mirrors
* {@link setStartRecordingIntent}. Consumed by {@code maybeNotifyRecordingStop}
* to coordinate the off-sound/notification across stop events.
*
* @param {Object|null} intent - The intent, or null to clear.
* @returns {Object}
*/
export function setStopRecordingIntent(intent: IStopRecordingIntent | null) {
return {
type: SET_STOP_RECORDING_INTENT,
intent
};
}
/**
* Tracks whether a local recording is active in Redux state so that selectors
* can react to it without polling the LocalRecordingManager singleton directly.
*
* @param {boolean} running - Whether local recording is now running.
* @returns {Object}
*/
export function setLocalRecordingRunning(running: boolean) {
return {
type: SET_LOCAL_RECORDING_RUNNING,
running
};
}