From a7c96e302f303bc63d6fd128e50d57faa63ee513 Mon Sep 17 00:00:00 2001 From: Robert Pintilii Date: Fri, 24 Jun 2022 13:07:40 +0100 Subject: [PATCH] feat(local-recording) Add self local recording (#11706) Only record local participant audio/ video streams --- css/_recording.scss | 4 + lang/main.json | 5 + react/features/app/middlewares.web.js | 1 + .../{middleware.js => middleware.any.js} | 0 .../features/base/media/middleware.native.ts | 1 + react/features/base/media/middleware.web.ts | 40 +++++ .../features/base/participants/actionTypes.ts | 3 +- react/features/base/participants/actions.js | 6 +- .../features/base/participants/middleware.js | 4 +- react/features/recording/actionTypes.ts | 3 +- react/features/recording/actions.any.js | 6 +- .../Recording/AbstractStartRecordingDialog.js | 19 ++- .../Recording/AbstractStopRecordingDialog.js | 9 ++ .../Recording/LocalRecordingManager.web.ts | 150 +++++++++++------- .../Recording/StartRecordingDialogContent.js | 109 +++++++++---- .../Recording/web/StartRecordingDialog.js | 4 + .../Recording/web/StopRecordingDialog.js | 4 +- react/features/recording/middleware.js | 23 ++- 18 files changed, 285 insertions(+), 106 deletions(-) rename react/features/base/media/{middleware.js => middleware.any.js} (100%) create mode 100644 react/features/base/media/middleware.native.ts create mode 100644 react/features/base/media/middleware.web.ts diff --git a/css/_recording.scss b/css/_recording.scss index 9674f31cd..02f9ae55a 100644 --- a/css/_recording.scss +++ b/css/_recording.scss @@ -19,6 +19,10 @@ font-size: 14px; margin-left: 16px; } + + &.space-top { + margin-top: 10px; + } } .recording-header-line { diff --git a/lang/main.json b/lang/main.json index b1c8fe9ac..d5725a550 100644 --- a/lang/main.json +++ b/lang/main.json @@ -895,12 +895,17 @@ "linkGenerated": "We have generated a link to your recording.", "live": "LIVE", "localRecordingNoNotificationWarning": "The recording will not be announced to other participants. You will need to let them know that the meeting is recorded.", + "localRecordingNoVideo": "Video is not being recorded", + "localRecordingVideoStop": "Stopping your video will also stop the local recording. Are you sure you want to continue?", + "localRecordingVideoWarning": "To record your video you must have it on when starting the recording", "localRecordingWarning": "Make sure you select the current tab in order to use the right video and audio. The recording is currently limited to 1GB, which is around 100 minutes.", "loggedIn": "Logged in as {{userName}}", + "noStreams": "No audio or video stream detected.", "off": "Recording stopped", "offBy": "{{name}} stopped the recording", "on": "Recording started", "onBy": "{{name}} started the recording", + "onlyRecordSelf": "Record only my audio and video streams", "pending": "Preparing to record the meeting...", "rec": "REC", "saveLocalRecording": "Save recording file locally", diff --git a/react/features/app/middlewares.web.js b/react/features/app/middlewares.web.js index 978076582..18f8b0814 100644 --- a/react/features/app/middlewares.web.js +++ b/react/features/app/middlewares.web.js @@ -3,6 +3,7 @@ import '../authentication/middleware'; import '../base/i18n/middleware'; import '../base/devices/middleware'; +import '../base/media/middleware'; import '../dynamic-branding/middleware'; import '../e2ee/middleware'; import '../external-api/middleware'; diff --git a/react/features/base/media/middleware.js b/react/features/base/media/middleware.any.js similarity index 100% rename from react/features/base/media/middleware.js rename to react/features/base/media/middleware.any.js diff --git a/react/features/base/media/middleware.native.ts b/react/features/base/media/middleware.native.ts new file mode 100644 index 000000000..15c62d75c --- /dev/null +++ b/react/features/base/media/middleware.native.ts @@ -0,0 +1 @@ +import './middleware.any.js'; diff --git a/react/features/base/media/middleware.web.ts b/react/features/base/media/middleware.web.ts new file mode 100644 index 000000000..8e3314e3c --- /dev/null +++ b/react/features/base/media/middleware.web.ts @@ -0,0 +1,40 @@ +import './middleware.any.js'; +// @ts-ignore +import { MiddlewareRegistry } from '../redux'; +import { IStore } from '../../app/types'; +import { SET_VIDEO_MUTED } from './actionTypes'; +import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager.web'; +// @ts-ignore +import { openDialog } from '../dialog'; +// @ts-ignore +import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../../notifications'; +// @ts-ignore +import StopRecordingDialog from '../../recording/components/Recording/web/StopRecordingDialog'; + +/** + * Implements the entry point of the middleware of the feature base/media. + * + * @param {IStore} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any) => { + const { dispatch } = store; + switch(action.type) { + case SET_VIDEO_MUTED: { + if (LocalRecordingManager.isRecordingLocally() && LocalRecordingManager.selfRecording.on) { + if (action.muted && LocalRecordingManager.selfRecording.withVideo) { + dispatch(openDialog(StopRecordingDialog, { localRecordingVideoStop: true })); + + return; + } else if (!action.muted && !LocalRecordingManager.selfRecording.withVideo) { + dispatch(showNotification({ + titleKey: 'recording.localRecordingNoVideo', + descriptionKey: 'recording.localRecordingVideoWarning', + uid: 'recording.localRecordingNoVideo' + }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM)); + } + } + } + } + return next(action); +}); diff --git a/react/features/base/participants/actionTypes.ts b/react/features/base/participants/actionTypes.ts index bd0f2f8ca..ca233b816 100644 --- a/react/features/base/participants/actionTypes.ts +++ b/react/features/base/participants/actionTypes.ts @@ -236,7 +236,8 @@ export const OVERWRITE_PARTICIPANTS_NAMES = 'OVERWRITE_PARTICIPANTS_NAMES'; * Updates participants local recording status. * { * type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS, - * recording: boolean + * recording: boolean, + * onlySelf: boolean * } */ export const SET_LOCAL_PARTICIPANT_RECORDING_STATUS = 'SET_LOCAL_PARTICIPANT_RECORDING_STATUS'; diff --git a/react/features/base/participants/actions.js b/react/features/base/participants/actions.js index fc8b25def..3177e15ee 100644 --- a/react/features/base/participants/actions.js +++ b/react/features/base/participants/actions.js @@ -689,14 +689,16 @@ export function overwriteParticipantsNames(participantList) { * Local video recording status for the local participant. * * @param {boolean} recording - If local recording is ongoing. + * @param {boolean} onlySelf - If recording only local streams. * @returns {{ * type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS, * recording: boolean * }} */ -export function updateLocalRecordingStatus(recording) { +export function updateLocalRecordingStatus(recording, onlySelf) { return { type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS, - recording + recording, + onlySelf }; } diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index b4e29e010..9ba3130ac 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -179,11 +179,11 @@ MiddlewareRegistry.register(store => next => action => { case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: { const state = store.getState(); - const { recording } = action; + const { recording, onlySelf } = action; const localId = getLocalParticipant(state)?.id; const { localRecording } = state['features/base/config']; - if (localRecording.notifyAllParticipants) { + if (localRecording?.notifyAllParticipants && !onlySelf) { store.dispatch(participantUpdated({ // XXX Only the local participant is allowed to update without // stating the JitsiConference instance (i.e. participant property diff --git a/react/features/recording/actionTypes.ts b/react/features/recording/actionTypes.ts index 829d1b37a..26beca91e 100644 --- a/react/features/recording/actionTypes.ts +++ b/react/features/recording/actionTypes.ts @@ -71,7 +71,8 @@ export const SET_MEETING_HIGHLIGHT_BUTTON_STATE = 'SET_MEETING_HIGHLIGHT_BUTTON_ * Attempts to start the local recording. * * { - * type: START_LOCAL_RECORDING + * type: START_LOCAL_RECORDING, + * onlySelf: boolean * } */ export const START_LOCAL_RECORDING = 'START_LOCAL_RECORDING'; diff --git a/react/features/recording/actions.any.js b/react/features/recording/actions.any.js index d6d572d01..12822dd0f 100644 --- a/react/features/recording/actions.any.js +++ b/react/features/recording/actions.any.js @@ -338,11 +338,13 @@ function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) { /** * Starts local recording. * + * @param {boolean} onlySelf - Whether to only record the local streams. * @returns {Object} */ -export function startLocalVideoRecording() { +export function startLocalVideoRecording(onlySelf) { return { - type: START_LOCAL_RECORDING + type: START_LOCAL_RECORDING, + onlySelf }; } diff --git a/react/features/recording/components/Recording/AbstractStartRecordingDialog.js b/react/features/recording/components/Recording/AbstractStartRecordingDialog.js index 84a426723..906686388 100644 --- a/react/features/recording/components/Recording/AbstractStartRecordingDialog.js +++ b/react/features/recording/components/Recording/AbstractStartRecordingDialog.js @@ -139,6 +139,7 @@ class AbstractStartRecordingDialog extends Component { = this._onSelectedRecordingServiceChanged.bind(this); this._onSharingSettingChanged = this._onSharingSettingChanged.bind(this); this._toggleScreenshotCapture = this._toggleScreenshotCapture.bind(this); + this._onLocalRecordingSelfChange = this._onLocalRecordingSelfChange.bind(this); let selectedRecordingService; @@ -157,7 +158,8 @@ class AbstractStartRecordingDialog extends Component { userName: undefined, sharingEnabled: true, spaceLeft: undefined, - selectedRecordingService + selectedRecordingService, + localRecordingOnlySelf: false }; } @@ -211,6 +213,19 @@ class AbstractStartRecordingDialog extends Component { }); } + _onLocalRecordingSelfChange: () => void; + + /** + * Callback to handle local recording only self setting change. + * + * @returns {void} + */ + _onLocalRecordingSelfChange() { + this.setState({ + localRecordingOnlySelf: !this.state.localRecordingOnlySelf + }); + } + _onSelectedRecordingServiceChanged: (string) => void; /** @@ -326,7 +341,7 @@ class AbstractStartRecordingDialog extends Component { break; } case RECORDING_TYPES.LOCAL: { - dispatch(startLocalVideoRecording()); + dispatch(startLocalVideoRecording(this.state.localRecordingOnlySelf)); return true; } diff --git a/react/features/recording/components/Recording/AbstractStopRecordingDialog.js b/react/features/recording/components/Recording/AbstractStopRecordingDialog.js index 483dc12f5..2fded2936 100644 --- a/react/features/recording/components/Recording/AbstractStopRecordingDialog.js +++ b/react/features/recording/components/Recording/AbstractStopRecordingDialog.js @@ -7,6 +7,7 @@ import { sendAnalytics } from '../../../analytics'; import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet'; +import { setVideoMuted } from '../../../base/media'; import { stopLocalVideoRecording } from '../../actions'; import { getActiveSession } from '../../functions'; @@ -38,6 +39,11 @@ export type Props = { */ dispatch: Function, + /** + * The user trying to stop the video while local recording is running. + */ + localRecordingVideoStop?: boolean, + /** * Invoked to obtain translated strings. */ @@ -78,6 +84,9 @@ export default class AbstractStopRecordingDialog if (this.props._localRecording) { this.props.dispatch(stopLocalVideoRecording()); + if (this.props.localRecordingVideoStop) { + this.props.dispatch(setVideoMuted(true)); + } } else { const { _fileRecordingSession } = this.props; diff --git a/react/features/recording/components/Recording/LocalRecordingManager.web.ts b/react/features/recording/components/Recording/LocalRecordingManager.web.ts index 5648e5a85..454542298 100644 --- a/react/features/recording/components/Recording/LocalRecordingManager.web.ts +++ b/react/features/recording/components/Recording/LocalRecordingManager.web.ts @@ -6,16 +6,23 @@ import { getRoomName } from '../../../base/conference'; // @ts-ignore import { MEDIA_TYPE } from '../../../base/media'; // @ts-ignore -import { getTrackState } from '../../../base/tracks'; +import { getTrackState, getLocalTrack } from '../../../base/tracks'; import { inIframe } from '../../../base/util/iframeUtils'; // @ts-ignore import { stopLocalVideoRecording } from '../../actions.any'; +declare var APP: any; + interface IReduxStore { dispatch: Function; getState: Function; } +interface SelfRecording { + on: boolean; + withVideo: boolean; +} + interface ILocalRecordingManager { recordingData: Blob[]; recorder: MediaRecorder|undefined; @@ -30,9 +37,10 @@ interface ILocalRecordingManager { getFilename: () => string; saveRecording: (recordingData: Blob[], filename: string) => void; stopLocalRecording: () => void; - startLocalRecording: (store: IReduxStore) => void; + startLocalRecording: (store: IReduxStore, onlySelf: boolean) => void; isRecordingLocally: () => boolean; totalSize: number; + selfRecording: SelfRecording; } const getMimeType = (): string => { @@ -63,6 +71,10 @@ const LocalRecordingManager: ILocalRecordingManager = { audioDestination: undefined, roomName: '', totalSize: 1073741824, // 1GB in bytes + selfRecording: { + on: false, + withVideo: false + }, get mediaType() { if (!preferredMediaType) { @@ -93,6 +105,9 @@ const LocalRecordingManager: ILocalRecordingManager = { * Adds audio track to the recording stream. */ addAudioTrackToLocalRecording(track) { + if(this.selfRecording.on) { + return; + } if (track) { const stream = new MediaStream([ track ]); @@ -143,58 +158,85 @@ const LocalRecordingManager: ILocalRecordingManager = { /** * Starts a local recording. */ - async startLocalRecording(store) { + async startLocalRecording(store, onlySelf) { const { dispatch, getState } = store; // @ts-ignore const supportsCaptureHandle = Boolean(navigator.mediaDevices.setCaptureHandleConfig) && !inIframe(); const tabId = uuidV4(); - if (supportsCaptureHandle) { - // @ts-ignore - navigator.mediaDevices.setCaptureHandleConfig({ - handle: `JitsiMeet-${tabId}`, - permittedOrigins: [ '*' ] - }); - } - + this.selfRecording.on = onlySelf; this.recordingData = []; - // @ts-ignore - const gdmStream = await navigator.mediaDevices.getDisplayMedia({ - // @ts-ignore - video: { displaySurface: 'browser', frameRate: 30 }, - audio: { - autoGainControl: false, - channelCount: 2, - echoCancellation: false, - noiseSuppression: false - } - }); - // @ts-ignore - const isBrowser = gdmStream.getVideoTracks()[0].getSettings().displaySurface === 'browser'; - - if (!isBrowser || (supportsCaptureHandle // @ts-ignore - && gdmStream.getVideoTracks()[0].getCaptureHandle()?.handle !== `JitsiMeet-${tabId}`)) { - gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop()); - throw new Error('WrongSurfaceSelected'); - } - - this.initializeAudioMixer(); - this.mixAudioStream(gdmStream); this.roomName = getRoomName(getState()); + let gdmStream: MediaStream = new MediaStream(); const tracks = getTrackState(getState()); - tracks.forEach((track: any) => { - if (track.mediaType === MEDIA_TYPE.AUDIO) { - const audioTrack = track?.jitsiTrack?.track; - - this.addAudioTrackToLocalRecording(audioTrack); + if(onlySelf) { + let audioTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track; + let videoTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.VIDEO)?.jitsiTrack?.track; + if(!audioTrack) { + APP.conference.muteAudio(false); + setTimeout(() => APP.conference.muteAudio(true), 100); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + if(videoTrack && videoTrack.readyState !== 'live') { + videoTrack = undefined; + } + audioTrack = getLocalTrack(getTrackState(getState()), MEDIA_TYPE.AUDIO)?.jitsiTrack?.track; + if(!audioTrack && !videoTrack) { + throw new Error('NoLocalStreams') + } + this.selfRecording.withVideo = Boolean(videoTrack); + const localTracks = []; + audioTrack && localTracks.push(audioTrack); + videoTrack && localTracks.push(videoTrack); + this.stream = new MediaStream(localTracks); + } else { + if (supportsCaptureHandle) { + // @ts-ignore + navigator.mediaDevices.setCaptureHandleConfig({ + handle: `JitsiMeet-${tabId}`, + permittedOrigins: [ '*' ] + }); } - }); - this.stream = new MediaStream([ - ...(this.audioDestination?.stream.getAudioTracks() || []), - gdmStream.getVideoTracks()[0] - ]); + // @ts-ignore + gdmStream = await navigator.mediaDevices.getDisplayMedia({ + // @ts-ignore + video: { displaySurface: 'browser', frameRate: 30 }, + audio: { + autoGainControl: false, + channelCount: 2, + echoCancellation: false, + noiseSuppression: false + } + }); + // @ts-ignore + const isBrowser = gdmStream.getVideoTracks()[0].getSettings().displaySurface === 'browser'; + + if (!isBrowser || (supportsCaptureHandle // @ts-ignore + && gdmStream.getVideoTracks()[0].getCaptureHandle()?.handle !== `JitsiMeet-${tabId}`)) { + gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop()); + throw new Error('WrongSurfaceSelected'); + } + + this.initializeAudioMixer(); + this.mixAudioStream(gdmStream); + + tracks.forEach((track: any) => { + if (track.mediaType === MEDIA_TYPE.AUDIO) { + const audioTrack = track?.jitsiTrack?.track; + + this.addAudioTrackToLocalRecording(audioTrack); + } + }); + this.stream = new MediaStream([ + ...(this.audioDestination?.stream.getAudioTracks() || []), + gdmStream.getVideoTracks()[0] + ]); + } + this.recorder = new MediaRecorder(this.stream, { mimeType: this.mediaType, videoBitsPerSecond: VIDEO_BIT_RATE @@ -209,18 +251,20 @@ const LocalRecordingManager: ILocalRecordingManager = { } }); - this.recorder.addEventListener('stop', () => { - this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop()); - gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop()); - }); + if(!onlySelf) { + this.recorder.addEventListener('stop', () => { + this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop()); + gdmStream?.getTracks().forEach((track: MediaStreamTrack) => track.stop()); + }); - gdmStream.addEventListener('inactive', () => { - dispatch(stopLocalVideoRecording()); - }); + gdmStream?.addEventListener('inactive', () => { + dispatch(stopLocalVideoRecording()); + }); - this.stream.addEventListener('inactive', () => { - dispatch(stopLocalVideoRecording()); - }); + this.stream.addEventListener('inactive', () => { + dispatch(stopLocalVideoRecording()); + }); + } this.recorder.start(5000); }, diff --git a/react/features/recording/components/Recording/StartRecordingDialogContent.js b/react/features/recording/components/Recording/StartRecordingDialogContent.js index 24abf2cce..71cb542e2 100644 --- a/react/features/recording/components/Recording/StartRecordingDialogContent.js +++ b/react/features/recording/components/Recording/StartRecordingDialogContent.js @@ -96,12 +96,22 @@ type Props = { */ isVpaas: boolean, + /** + * Whether or not we should only record the local streams. + */ + localRecordingOnlySelf: boolean, + /** * The function will be called when there are changes related to the * switches. */ onChange: Function, + /** + * Callback to change the local recording only self setting. + */ + onLocalRecordingSelfChange: Function, + /** * Callback to be invoked on sharing setting change. */ @@ -629,45 +639,76 @@ class StartRecordingDialogContent extends Component { } return ( - - + <> + - + className = 'recording-header recording-header-line' + style = { styles.header }> + + + + + { t('recording.saveLocalRecording') } + + - - { t('recording.saveLocalRecording') } - - - {selectedRecordingService === RECORDING_TYPES.LOCAL - && <> + {selectedRecordingService === RECORDING_TYPES.LOCAL && ( + <> + + + + + + + {t('recording.onlyRecordSelf')} + + + + {t('recording.localRecordingWarning')} - {_localRecordingNoNotification && - {t('recording.localRecordingNoNotificationWarning')} - } + {_localRecordingNoNotification && !this.props.localRecordingOnlySelf + && + {t('recording.localRecordingNoNotificationWarning')} + + } - } - + )} + ); } @@ -707,8 +748,8 @@ function _mapStateToProps(state) { return { ..._abstractMapStateToProps(state), isVpaas: isVpaasMeeting(state), - _localRecordingEnabled: !state['features/base/config'].localRecording.disable, - _localRecordingNoNotification: !state['features/base/config'].localRecording.notifyAllParticipants, + _localRecordingEnabled: !state['features/base/config'].localRecording?.disable, + _localRecordingNoNotification: !state['features/base/config'].localRecording?.notifyAllParticipants, _styles: ColorSchemeRegistry.get(state, 'StartRecordingDialogContent') }; } diff --git a/react/features/recording/components/Recording/web/StartRecordingDialog.js b/react/features/recording/components/Recording/web/StartRecordingDialog.js index aa01c44e3..2f9135487 100644 --- a/react/features/recording/components/Recording/web/StartRecordingDialog.js +++ b/react/features/recording/components/Recording/web/StartRecordingDialog.js @@ -55,6 +55,7 @@ class StartRecordingDialog extends AbstractStartRecordingDialog { const { isTokenValid, isValidating, + localRecordingOnlySelf, selectedRecordingService, sharingEnabled, spaceLeft, @@ -78,7 +79,9 @@ class StartRecordingDialog extends AbstractStartRecordingDialog { integrationsEnabled = { this._areIntegrationsEnabled() } isTokenValid = { isTokenValid } isValidating = { isValidating } + localRecordingOnlySelf = { localRecordingOnlySelf } onChange = { this._onSelectedRecordingServiceChanged } + onLocalRecordingSelfChange = { this._onLocalRecordingSelfChange } onSharingSettingChanged = { this._onSharingSettingChanged } selectedRecordingService = { selectedRecordingService } sharingSetting = { sharingEnabled } @@ -105,6 +108,7 @@ class StartRecordingDialog extends AbstractStartRecordingDialog { _onSubmit: () => boolean; _onSelectedRecordingServiceChanged: (string) => void; _onSharingSettingChanged: () => void; + _onLocalRecordingSelfChange: () => void; } /** diff --git a/react/features/recording/components/Recording/web/StopRecordingDialog.js b/react/features/recording/components/Recording/web/StopRecordingDialog.js index 21db4f22a..ea1b34cc1 100644 --- a/react/features/recording/components/Recording/web/StopRecordingDialog.js +++ b/react/features/recording/components/Recording/web/StopRecordingDialog.js @@ -25,7 +25,7 @@ class StopRecordingDialog extends AbstractStopRecordingDialog { * @returns {ReactElement} */ render() { - const { t } = this.props; + const { t, localRecordingVideoStop } = this.props; return ( { onSubmit = { this._onSubmit } titleKey = 'dialog.recording' width = 'small'> - { t('dialog.stopRecordingWarning') } + {t(localRecordingVideoStop ? 'recording.localRecordingVideoStop' : 'dialog.stopRecordingWarning') } ); } diff --git a/react/features/recording/middleware.js b/react/features/recording/middleware.js index 138d8f0cf..06fe808cc 100644 --- a/react/features/recording/middleware.js +++ b/react/features/recording/middleware.js @@ -133,27 +133,35 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action => case START_LOCAL_RECORDING: { const { localRecording } = getState()['features/base/config']; + const { onlySelf } = action; try { await LocalRecordingManager.startLocalRecording({ dispatch, - getState }); + getState }, action.onlySelf); const props = { descriptionKey: 'recording.on', titleKey: 'dialog.recording' }; - if (localRecording.notifyAllParticipants) { + if (localRecording?.notifyAllParticipants && !onlySelf) { dispatch(playSound(RECORDING_ON_SOUND_ID)); } dispatch(showNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM)); - dispatch(updateLocalRecordingStatus(true)); - sendAnalytics(createRecordingEvent('started', 'local')); + dispatch(updateLocalRecordingStatus(true, onlySelf)); + sendAnalytics(createRecordingEvent('started', `local${onlySelf ? '.self' : ''}`)); } catch (err) { logger.error('Capture failed', err); - const noTabError = err.message === 'WrongSurfaceSelected'; + let descriptionKey = 'recording.error'; + + if (err.message === 'WrongSurfaceSelected') { + descriptionKey = 'recording.surfaceError'; + + } else if (err.message === 'NoLocalStreams') { + descriptionKey = 'recording.noStreams'; + } const props = { - descriptionKey: noTabError ? 'recording.surfaceError' : 'recording.error', + descriptionKey, titleKey: 'recording.failedToStart' }; @@ -164,11 +172,12 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action => case STOP_LOCAL_RECORDING: { const { localRecording } = getState()['features/base/config']; + const { onlySelf } = action; if (LocalRecordingManager.isRecordingLocally()) { LocalRecordingManager.stopLocalRecording(); dispatch(updateLocalRecordingStatus(false)); - if (localRecording.notifyAllParticipants) { + if (localRecording?.notifyAllParticipants && !onlySelf) { dispatch(playSound(RECORDING_OFF_SOUND_ID)); } }