123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- import { createRecordingEvent } from '../analytics/AnalyticsEvents';
- import { sendAnalytics } from '../analytics/functions';
- import { IStore } from '../app/types';
- import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
- import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
- import { getCurrentConference } from '../base/conference/functions';
- import JitsiMeetJS, {
- JitsiConferenceEvents,
- JitsiRecordingConstants
- } from '../base/lib-jitsi-meet';
- import { MEDIA_TYPE } from '../base/media/constants';
- import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
- import { updateLocalRecordingStatus } from '../base/participants/actions';
- import { PARTICIPANT_ROLE } from '../base/participants/constants';
- import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
- import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
- import StateListenerRegistry from '../base/redux/StateListenerRegistry';
- import {
- playSound,
- stopSound
- } from '../base/sounds/actions';
- import { TRACK_ADDED } from '../base/tracks/actionTypes';
- import { hideNotification, showErrorNotification, showNotification } from '../notifications/actions';
- import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
- import { isRecorderTranscriptionsRunning } from '../transcribing/functions';
-
- import { RECORDING_SESSION_UPDATED, START_LOCAL_RECORDING, STOP_LOCAL_RECORDING } from './actionTypes';
- import {
- clearRecordingSessions,
- hidePendingRecordingNotification,
- showPendingRecordingNotification,
- showRecordingError,
- showRecordingLimitNotification,
- showRecordingWarning,
- showStartRecordingNotification,
- showStartedRecordingNotification,
- showStoppedRecordingNotification,
- updateRecordingSessionData
- } from './actions';
- import LocalRecordingManager from './components/Recording/LocalRecordingManager';
- import {
- LIVE_STREAMING_OFF_SOUND_ID,
- LIVE_STREAMING_ON_SOUND_ID,
- RECORDING_OFF_SOUND_ID,
- RECORDING_ON_SOUND_ID,
- START_RECORDING_NOTIFICATION_ID
- } from './constants';
- import {
- getResourceId,
- getSessionById,
- registerRecordingAudioFiles,
- unregisterRecordingAudioFiles
- } from './functions';
- import logger from './logger';
-
- /**
- * StateListenerRegistry provides a reliable way to detect the leaving of a
- * conference, where we need to clean up the recording sessions.
- */
- StateListenerRegistry.register(
- /* selector */ state => getCurrentConference(state),
- /* listener */ (conference, { dispatch }) => {
- if (!conference) {
- dispatch(clearRecordingSessions());
- }
- }
- );
-
- /**
- * The redux middleware to handle the recorder updates in a React way.
- *
- * @param {Store} store - The redux store.
- * @returns {Function}
- */
- MiddlewareRegistry.register(({ dispatch, getState }) => next => async action => {
- let oldSessionData;
-
- if (action.type === RECORDING_SESSION_UPDATED) {
- oldSessionData
- = getSessionById(getState(), action.sessionData.id);
- }
-
- const result = next(action);
-
- switch (action.type) {
- case APP_WILL_MOUNT:
- registerRecordingAudioFiles(dispatch);
-
- break;
-
- case APP_WILL_UNMOUNT:
- unregisterRecordingAudioFiles(dispatch);
-
- break;
-
- case CONFERENCE_JOIN_IN_PROGRESS: {
- const { conference } = action;
-
- conference.on(
- JitsiConferenceEvents.RECORDER_STATE_CHANGED,
- (recorderSession: any) => {
- if (recorderSession) {
- recorderSession.getID() && dispatch(updateRecordingSessionData(recorderSession));
- recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch, getState);
- }
-
- return;
- });
-
- break;
- }
-
- case START_LOCAL_RECORDING: {
- const { localRecording } = getState()['features/base/config'];
- const { onlySelf } = action;
-
- try {
- await LocalRecordingManager.startLocalRecording({ dispatch,
- getState }, action.onlySelf);
- const props = {
- descriptionKey: 'recording.on',
- titleKey: 'dialog.recording'
- };
-
- if (localRecording?.notifyAllParticipants && !onlySelf) {
- dispatch(playSound(RECORDING_ON_SOUND_ID));
- }
- dispatch(showNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
- dispatch(showNotification({
- titleKey: 'recording.localRecordingStartWarningTitle',
- descriptionKey: 'recording.localRecordingStartWarning'
- }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
- dispatch(updateLocalRecordingStatus(true, onlySelf));
- sendAnalytics(createRecordingEvent('started', `local${onlySelf ? '.self' : ''}`));
- if (typeof APP !== 'undefined') {
- APP.API.notifyRecordingStatusChanged(
- true, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
- }
- } catch (err: any) {
- logger.error('Capture failed', err);
-
- let descriptionKey = 'recording.error';
-
- if (err.message === 'WrongSurfaceSelected') {
- descriptionKey = 'recording.surfaceError';
-
- } else if (err.message === 'NoLocalStreams') {
- descriptionKey = 'recording.noStreams';
- } else if (err.message === 'NoMicTrack') {
- descriptionKey = 'recording.noMicPermission';
- }
- const props = {
- descriptionKey,
- titleKey: 'recording.failedToStart'
- };
-
- if (typeof APP !== 'undefined') {
- APP.API.notifyRecordingStatusChanged(
- false, 'local', err.message, isRecorderTranscriptionsRunning(getState()));
- }
-
- dispatch(showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
- }
- break;
- }
-
- case STOP_LOCAL_RECORDING: {
- const { localRecording } = getState()['features/base/config'];
-
- if (LocalRecordingManager.isRecordingLocally()) {
- LocalRecordingManager.stopLocalRecording();
- dispatch(updateLocalRecordingStatus(false));
- if (localRecording?.notifyAllParticipants && !LocalRecordingManager.selfRecording) {
- dispatch(playSound(RECORDING_OFF_SOUND_ID));
- }
- if (typeof APP !== 'undefined') {
- APP.API.notifyRecordingStatusChanged(
- false, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
- }
- }
- break;
- }
-
- case RECORDING_SESSION_UPDATED: {
- const state = getState();
-
- // When in recorder mode no notifications are shown
- // or extra sounds are also not desired
- // but we want to indicate those in case of sip gateway
- const {
- iAmRecorder,
- iAmSipGateway,
- recordingLimit
- } = state['features/base/config'];
-
- if (iAmRecorder && !iAmSipGateway) {
- break;
- }
-
- const updatedSessionData
- = getSessionById(state, action.sessionData.id);
- const { initiator, mode = '', terminator } = updatedSessionData ?? {};
- const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
-
- if (updatedSessionData?.status === PENDING && oldSessionData?.status !== PENDING) {
- dispatch(showPendingRecordingNotification(mode));
- dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
- } else {
- dispatch(hidePendingRecordingNotification(mode));
-
- if (updatedSessionData?.status === ON) {
-
- // We receive 2 updates of the session status ON. The first one is from jibri when it joins.
- // The second one is from jicofo which will deliever the initiator value. Since the start
- // recording notification uses the initiator value we skip the jibri update and show the
- // notification on the update from jicofo.
- // FIXE: simplify checks when the backend start sending only one status ON update containing the
- // initiator.
- if (initiator && !oldSessionData?.initiator) {
- if (typeof recordingLimit === 'object') {
- dispatch(showRecordingLimitNotification(mode));
- } else {
- dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
- }
- }
-
- if (oldSessionData?.status !== ON) {
- sendAnalytics(createRecordingEvent('start', mode));
-
- let soundID;
-
- if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
- soundID = RECORDING_ON_SOUND_ID;
- } else if (mode === JitsiRecordingConstants.mode.STREAM) {
- soundID = LIVE_STREAMING_ON_SOUND_ID;
- }
-
- if (soundID) {
- dispatch(playSound(soundID));
- }
-
- if (typeof APP !== 'undefined') {
- APP.API.notifyRecordingStatusChanged(
- true, mode, undefined, isRecorderTranscriptionsRunning(state));
- }
- }
- } else if (updatedSessionData?.status === OFF && oldSessionData?.status !== OFF) {
- if (terminator) {
- dispatch(
- showStoppedRecordingNotification(
- mode, getParticipantDisplayName(state, getResourceId(terminator))));
- }
-
- let duration = 0, soundOff, soundOn;
-
- if (oldSessionData?.timestamp) {
- duration
- = (Date.now() / 1000) - oldSessionData.timestamp;
- }
- sendAnalytics(createRecordingEvent('stop', mode, duration));
-
- if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
- soundOff = RECORDING_OFF_SOUND_ID;
- soundOn = RECORDING_ON_SOUND_ID;
- } else if (mode === JitsiRecordingConstants.mode.STREAM) {
- soundOff = LIVE_STREAMING_OFF_SOUND_ID;
- soundOn = LIVE_STREAMING_ON_SOUND_ID;
- }
-
- if (soundOff && soundOn) {
- dispatch(stopSound(soundOn));
- dispatch(playSound(soundOff));
- }
-
- if (typeof APP !== 'undefined') {
- APP.API.notifyRecordingStatusChanged(
- false, mode, undefined, isRecorderTranscriptionsRunning(state));
- }
- }
- }
-
- break;
- }
- case TRACK_ADDED: {
- const { track } = action;
-
- if (LocalRecordingManager.isRecordingLocally() && track.mediaType === MEDIA_TYPE.AUDIO) {
- const audioTrack = track.jitsiTrack.track;
-
- LocalRecordingManager.addAudioTrackToLocalRecording(audioTrack);
- }
- break;
- }
- case PARTICIPANT_UPDATED: {
- const { id, role } = action.participant;
- const state = getState();
- const localParticipant = getLocalParticipant(state);
-
- if (localParticipant?.id !== id) {
- return next(action);
- }
-
- if (role === PARTICIPANT_ROLE.MODERATOR) {
- dispatch(showStartRecordingNotification());
- }
-
- return next(action);
- }
- }
-
- return result;
- });
-
- /**
- * Shows a notification about an error in the recording session. A
- * default notification will display if no error is specified in the passed
- * in recording session.
- *
- * @private
- * @param {Object} session - The recorder session model from the
- * lib.
- * @param {Dispatch} dispatch - The Redux Dispatch function.
- * @param {Function} getState - The Redux getState function.
- * @returns {void}
- */
- function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
- const mode = session.getMode();
- const error = session.getError();
- const isStreamMode = mode === JitsiMeetJS.constants.recording.mode.STREAM;
-
- switch (error) {
- case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
- dispatch(showRecordingError({
- descriptionKey: 'recording.unavailable',
- descriptionArguments: {
- serviceName: isStreamMode
- ? '$t(liveStreaming.serviceName)'
- : '$t(recording.serviceName)'
- },
- titleKey: isStreamMode
- ? 'liveStreaming.unavailableTitle'
- : 'recording.unavailableTitle'
- }));
- break;
- case JitsiMeetJS.constants.recording.error.RESOURCE_CONSTRAINT:
- dispatch(showRecordingError({
- descriptionKey: isStreamMode
- ? 'liveStreaming.busy'
- : 'recording.busy',
- titleKey: isStreamMode
- ? 'liveStreaming.busyTitle'
- : 'recording.busyTitle'
- }));
- break;
- case JitsiMeetJS.constants.recording.error.UNEXPECTED_REQUEST:
- dispatch(showRecordingWarning({
- descriptionKey: isStreamMode
- ? 'liveStreaming.sessionAlreadyActive'
- : 'recording.sessionAlreadyActive',
- titleKey: isStreamMode ? 'liveStreaming.inProgress' : 'recording.inProgress'
- }));
- break;
- default:
- dispatch(showRecordingError({
- descriptionKey: isStreamMode
- ? 'liveStreaming.error'
- : 'recording.error',
- titleKey: isStreamMode
- ? 'liveStreaming.failedToStart'
- : 'recording.failedToStart'
- }));
- break;
- }
-
- if (typeof APP !== 'undefined') {
- APP.API.notifyRecordingStatusChanged(false, mode, error, isRecorderTranscriptionsRunning(getState()));
- }
- }
|