snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
608
react/features/recording/actions.any.ts
Normal file
608
react/features/recording/actions.any.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user