feat(local-recording) Add self local recording (#11706)

Only record local participant audio/ video streams
This commit is contained in:
Robert Pintilii 2022-06-24 13:07:40 +01:00 committed by GitHub
parent b85da1e1bb
commit a7c96e302f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 285 additions and 106 deletions

View File

@ -19,6 +19,10 @@
font-size: 14px; font-size: 14px;
margin-left: 16px; margin-left: 16px;
} }
&.space-top {
margin-top: 10px;
}
} }
.recording-header-line { .recording-header-line {

View File

@ -895,12 +895,17 @@
"linkGenerated": "We have generated a link to your recording.", "linkGenerated": "We have generated a link to your recording.",
"live": "LIVE", "live": "LIVE",
"localRecordingNoNotificationWarning": "The recording will not be announced to other participants. You will need to let them know that the meeting is recorded.", "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.", "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}}", "loggedIn": "Logged in as {{userName}}",
"noStreams": "No audio or video stream detected.",
"off": "Recording stopped", "off": "Recording stopped",
"offBy": "{{name}} stopped the recording", "offBy": "{{name}} stopped the recording",
"on": "Recording started", "on": "Recording started",
"onBy": "{{name}} started the recording", "onBy": "{{name}} started the recording",
"onlyRecordSelf": "Record only my audio and video streams",
"pending": "Preparing to record the meeting...", "pending": "Preparing to record the meeting...",
"rec": "REC", "rec": "REC",
"saveLocalRecording": "Save recording file locally", "saveLocalRecording": "Save recording file locally",

View File

@ -3,6 +3,7 @@
import '../authentication/middleware'; import '../authentication/middleware';
import '../base/i18n/middleware'; import '../base/i18n/middleware';
import '../base/devices/middleware'; import '../base/devices/middleware';
import '../base/media/middleware';
import '../dynamic-branding/middleware'; import '../dynamic-branding/middleware';
import '../e2ee/middleware'; import '../e2ee/middleware';
import '../external-api/middleware'; import '../external-api/middleware';

View File

@ -0,0 +1 @@
import './middleware.any.js';

View File

@ -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);
});

View File

@ -236,7 +236,8 @@ export const OVERWRITE_PARTICIPANTS_NAMES = 'OVERWRITE_PARTICIPANTS_NAMES';
* Updates participants local recording status. * Updates participants local recording status.
* { * {
* type: SET_LOCAL_PARTICIPANT_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'; export const SET_LOCAL_PARTICIPANT_RECORDING_STATUS = 'SET_LOCAL_PARTICIPANT_RECORDING_STATUS';

View File

@ -689,14 +689,16 @@ export function overwriteParticipantsNames(participantList) {
* Local video recording status for the local participant. * Local video recording status for the local participant.
* *
* @param {boolean} recording - If local recording is ongoing. * @param {boolean} recording - If local recording is ongoing.
* @param {boolean} onlySelf - If recording only local streams.
* @returns {{ * @returns {{
* type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS, * type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
* recording: boolean * recording: boolean
* }} * }}
*/ */
export function updateLocalRecordingStatus(recording) { export function updateLocalRecordingStatus(recording, onlySelf) {
return { return {
type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS, type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
recording recording,
onlySelf
}; };
} }

View File

@ -179,11 +179,11 @@ MiddlewareRegistry.register(store => next => action => {
case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: { case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: {
const state = store.getState(); const state = store.getState();
const { recording } = action; const { recording, onlySelf } = action;
const localId = getLocalParticipant(state)?.id; const localId = getLocalParticipant(state)?.id;
const { localRecording } = state['features/base/config']; const { localRecording } = state['features/base/config'];
if (localRecording.notifyAllParticipants) { if (localRecording?.notifyAllParticipants && !onlySelf) {
store.dispatch(participantUpdated({ store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without // XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property // stating the JitsiConference instance (i.e. participant property

View File

@ -71,7 +71,8 @@ export const SET_MEETING_HIGHLIGHT_BUTTON_STATE = 'SET_MEETING_HIGHLIGHT_BUTTON_
* Attempts to start the local recording. * Attempts to start the local recording.
* *
* { * {
* type: START_LOCAL_RECORDING * type: START_LOCAL_RECORDING,
* onlySelf: boolean
* } * }
*/ */
export const START_LOCAL_RECORDING = 'START_LOCAL_RECORDING'; export const START_LOCAL_RECORDING = 'START_LOCAL_RECORDING';

View File

@ -338,11 +338,13 @@ function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) {
/** /**
* Starts local recording. * Starts local recording.
* *
* @param {boolean} onlySelf - Whether to only record the local streams.
* @returns {Object} * @returns {Object}
*/ */
export function startLocalVideoRecording() { export function startLocalVideoRecording(onlySelf) {
return { return {
type: START_LOCAL_RECORDING type: START_LOCAL_RECORDING,
onlySelf
}; };
} }

View File

@ -139,6 +139,7 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
= this._onSelectedRecordingServiceChanged.bind(this); = this._onSelectedRecordingServiceChanged.bind(this);
this._onSharingSettingChanged = this._onSharingSettingChanged.bind(this); this._onSharingSettingChanged = this._onSharingSettingChanged.bind(this);
this._toggleScreenshotCapture = this._toggleScreenshotCapture.bind(this); this._toggleScreenshotCapture = this._toggleScreenshotCapture.bind(this);
this._onLocalRecordingSelfChange = this._onLocalRecordingSelfChange.bind(this);
let selectedRecordingService; let selectedRecordingService;
@ -157,7 +158,8 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
userName: undefined, userName: undefined,
sharingEnabled: true, sharingEnabled: true,
spaceLeft: undefined, spaceLeft: undefined,
selectedRecordingService selectedRecordingService,
localRecordingOnlySelf: false
}; };
} }
@ -211,6 +213,19 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
}); });
} }
_onLocalRecordingSelfChange: () => void;
/**
* Callback to handle local recording only self setting change.
*
* @returns {void}
*/
_onLocalRecordingSelfChange() {
this.setState({
localRecordingOnlySelf: !this.state.localRecordingOnlySelf
});
}
_onSelectedRecordingServiceChanged: (string) => void; _onSelectedRecordingServiceChanged: (string) => void;
/** /**
@ -326,7 +341,7 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
break; break;
} }
case RECORDING_TYPES.LOCAL: { case RECORDING_TYPES.LOCAL: {
dispatch(startLocalVideoRecording()); dispatch(startLocalVideoRecording(this.state.localRecordingOnlySelf));
return true; return true;
} }

View File

@ -7,6 +7,7 @@ import {
sendAnalytics sendAnalytics
} from '../../../analytics'; } from '../../../analytics';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet'; import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { setVideoMuted } from '../../../base/media';
import { stopLocalVideoRecording } from '../../actions'; import { stopLocalVideoRecording } from '../../actions';
import { getActiveSession } from '../../functions'; import { getActiveSession } from '../../functions';
@ -38,6 +39,11 @@ export type Props = {
*/ */
dispatch: Function, dispatch: Function,
/**
* The user trying to stop the video while local recording is running.
*/
localRecordingVideoStop?: boolean,
/** /**
* Invoked to obtain translated strings. * Invoked to obtain translated strings.
*/ */
@ -78,6 +84,9 @@ export default class AbstractStopRecordingDialog<P: Props>
if (this.props._localRecording) { if (this.props._localRecording) {
this.props.dispatch(stopLocalVideoRecording()); this.props.dispatch(stopLocalVideoRecording());
if (this.props.localRecordingVideoStop) {
this.props.dispatch(setVideoMuted(true));
}
} else { } else {
const { _fileRecordingSession } = this.props; const { _fileRecordingSession } = this.props;

View File

@ -6,16 +6,23 @@ import { getRoomName } from '../../../base/conference';
// @ts-ignore // @ts-ignore
import { MEDIA_TYPE } from '../../../base/media'; import { MEDIA_TYPE } from '../../../base/media';
// @ts-ignore // @ts-ignore
import { getTrackState } from '../../../base/tracks'; import { getTrackState, getLocalTrack } from '../../../base/tracks';
import { inIframe } from '../../../base/util/iframeUtils'; import { inIframe } from '../../../base/util/iframeUtils';
// @ts-ignore // @ts-ignore
import { stopLocalVideoRecording } from '../../actions.any'; import { stopLocalVideoRecording } from '../../actions.any';
declare var APP: any;
interface IReduxStore { interface IReduxStore {
dispatch: Function; dispatch: Function;
getState: Function; getState: Function;
} }
interface SelfRecording {
on: boolean;
withVideo: boolean;
}
interface ILocalRecordingManager { interface ILocalRecordingManager {
recordingData: Blob[]; recordingData: Blob[];
recorder: MediaRecorder|undefined; recorder: MediaRecorder|undefined;
@ -30,9 +37,10 @@ interface ILocalRecordingManager {
getFilename: () => string; getFilename: () => string;
saveRecording: (recordingData: Blob[], filename: string) => void; saveRecording: (recordingData: Blob[], filename: string) => void;
stopLocalRecording: () => void; stopLocalRecording: () => void;
startLocalRecording: (store: IReduxStore) => void; startLocalRecording: (store: IReduxStore, onlySelf: boolean) => void;
isRecordingLocally: () => boolean; isRecordingLocally: () => boolean;
totalSize: number; totalSize: number;
selfRecording: SelfRecording;
} }
const getMimeType = (): string => { const getMimeType = (): string => {
@ -63,6 +71,10 @@ const LocalRecordingManager: ILocalRecordingManager = {
audioDestination: undefined, audioDestination: undefined,
roomName: '', roomName: '',
totalSize: 1073741824, // 1GB in bytes totalSize: 1073741824, // 1GB in bytes
selfRecording: {
on: false,
withVideo: false
},
get mediaType() { get mediaType() {
if (!preferredMediaType) { if (!preferredMediaType) {
@ -93,6 +105,9 @@ const LocalRecordingManager: ILocalRecordingManager = {
* Adds audio track to the recording stream. * Adds audio track to the recording stream.
*/ */
addAudioTrackToLocalRecording(track) { addAudioTrackToLocalRecording(track) {
if(this.selfRecording.on) {
return;
}
if (track) { if (track) {
const stream = new MediaStream([ track ]); const stream = new MediaStream([ track ]);
@ -143,58 +158,85 @@ const LocalRecordingManager: ILocalRecordingManager = {
/** /**
* Starts a local recording. * Starts a local recording.
*/ */
async startLocalRecording(store) { async startLocalRecording(store, onlySelf) {
const { dispatch, getState } = store; const { dispatch, getState } = store;
// @ts-ignore // @ts-ignore
const supportsCaptureHandle = Boolean(navigator.mediaDevices.setCaptureHandleConfig) && !inIframe(); const supportsCaptureHandle = Boolean(navigator.mediaDevices.setCaptureHandleConfig) && !inIframe();
const tabId = uuidV4(); const tabId = uuidV4();
if (supportsCaptureHandle) { this.selfRecording.on = onlySelf;
// @ts-ignore
navigator.mediaDevices.setCaptureHandleConfig({
handle: `JitsiMeet-${tabId}`,
permittedOrigins: [ '*' ]
});
}
this.recordingData = []; 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()); this.roomName = getRoomName(getState());
let gdmStream: MediaStream = new MediaStream();
const tracks = getTrackState(getState()); const tracks = getTrackState(getState());
tracks.forEach((track: any) => { if(onlySelf) {
if (track.mediaType === MEDIA_TYPE.AUDIO) { let audioTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
const audioTrack = track?.jitsiTrack?.track; let videoTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.VIDEO)?.jitsiTrack?.track;
if(!audioTrack) {
this.addAudioTrackToLocalRecording(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([ // @ts-ignore
...(this.audioDestination?.stream.getAudioTracks() || []), gdmStream = await navigator.mediaDevices.getDisplayMedia({
gdmStream.getVideoTracks()[0] // @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, { this.recorder = new MediaRecorder(this.stream, {
mimeType: this.mediaType, mimeType: this.mediaType,
videoBitsPerSecond: VIDEO_BIT_RATE videoBitsPerSecond: VIDEO_BIT_RATE
@ -209,18 +251,20 @@ const LocalRecordingManager: ILocalRecordingManager = {
} }
}); });
this.recorder.addEventListener('stop', () => { if(!onlySelf) {
this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop()); this.recorder.addEventListener('stop', () => {
gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop()); this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
}); gdmStream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
});
gdmStream.addEventListener('inactive', () => { gdmStream?.addEventListener('inactive', () => {
dispatch(stopLocalVideoRecording()); dispatch(stopLocalVideoRecording());
}); });
this.stream.addEventListener('inactive', () => { this.stream.addEventListener('inactive', () => {
dispatch(stopLocalVideoRecording()); dispatch(stopLocalVideoRecording());
}); });
}
this.recorder.start(5000); this.recorder.start(5000);
}, },

View File

@ -96,12 +96,22 @@ type Props = {
*/ */
isVpaas: boolean, 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 * The function will be called when there are changes related to the
* switches. * switches.
*/ */
onChange: Function, onChange: Function,
/**
* Callback to change the local recording only self setting.
*/
onLocalRecordingSelfChange: Function,
/** /**
* Callback to be invoked on sharing setting change. * Callback to be invoked on sharing setting change.
*/ */
@ -629,45 +639,76 @@ class StartRecordingDialogContent extends Component<Props> {
} }
return ( return (
<Container> <>
<Container <Container>
className = 'recording-header recording-header-line'
style = { styles.header }>
<Container <Container
className = 'recording-icon-container'> className = 'recording-header recording-header-line'
<Image style = { styles.header }>
className = 'recording-icon' <Container
src = { LOCAL_RECORDING } className = 'recording-icon-container'>
style = { styles.recordingIcon } /> <Image
className = 'recording-icon'
src = { LOCAL_RECORDING }
style = { styles.recordingIcon } />
</Container>
<Text
className = 'recording-title'
style = {{
..._dialogStyles.text,
...styles.title
}}>
{ t('recording.saveLocalRecording') }
</Text>
<Switch
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { this._onLocalRecordingSwitchChange }
style = { styles.switch }
trackColor = {{ false: TRACK_COLOR }}
value = { this.props.selectedRecordingService
=== RECORDING_TYPES.LOCAL } />
</Container> </Container>
<Text
className = 'recording-title'
style = {{
..._dialogStyles.text,
...styles.title
}}>
{ t('recording.saveLocalRecording') }
</Text>
<Switch
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { this._onLocalRecordingSwitchChange }
style = { styles.switch }
trackColor = {{ false: TRACK_COLOR }}
value = { this.props.selectedRecordingService
=== RECORDING_TYPES.LOCAL } />
</Container> </Container>
{selectedRecordingService === RECORDING_TYPES.LOCAL {selectedRecordingService === RECORDING_TYPES.LOCAL && (
&& <> <>
<Container>
<Container
className = 'recording-header space-top'
style = { styles.header }>
<Container className = 'recording-icon-container file-sharing-icon-container'>
<Image
className = 'recording-file-sharing-icon'
src = { ICON_USERS }
style = { styles.recordingIcon } />
</Container>
<Text
className = 'recording-title'
style = {{
..._dialogStyles.text,
...styles.title
}}>
{t('recording.onlyRecordSelf')}
</Text>
<Switch
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { this.props.onLocalRecordingSelfChange }
style = { styles.switch }
trackColor = {{ false: TRACK_COLOR }}
value = { this.props.localRecordingOnlySelf } />
</Container>
</Container>
<Text className = 'local-recording-warning text'> <Text className = 'local-recording-warning text'>
{t('recording.localRecordingWarning')} {t('recording.localRecordingWarning')}
</Text> </Text>
{_localRecordingNoNotification && <Text className = 'local-recording-warning notification'> {_localRecordingNoNotification && !this.props.localRecordingOnlySelf
{t('recording.localRecordingNoNotificationWarning')} && <Text className = 'local-recording-warning notification'>
</Text>} {t('recording.localRecordingNoNotificationWarning')}
</Text>
}
</> </>
} )}
</Container> </>
); );
} }
@ -707,8 +748,8 @@ function _mapStateToProps(state) {
return { return {
..._abstractMapStateToProps(state), ..._abstractMapStateToProps(state),
isVpaas: isVpaasMeeting(state), isVpaas: isVpaasMeeting(state),
_localRecordingEnabled: !state['features/base/config'].localRecording.disable, _localRecordingEnabled: !state['features/base/config'].localRecording?.disable,
_localRecordingNoNotification: !state['features/base/config'].localRecording.notifyAllParticipants, _localRecordingNoNotification: !state['features/base/config'].localRecording?.notifyAllParticipants,
_styles: ColorSchemeRegistry.get(state, 'StartRecordingDialogContent') _styles: ColorSchemeRegistry.get(state, 'StartRecordingDialogContent')
}; };
} }

View File

@ -55,6 +55,7 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
const { const {
isTokenValid, isTokenValid,
isValidating, isValidating,
localRecordingOnlySelf,
selectedRecordingService, selectedRecordingService,
sharingEnabled, sharingEnabled,
spaceLeft, spaceLeft,
@ -78,7 +79,9 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
integrationsEnabled = { this._areIntegrationsEnabled() } integrationsEnabled = { this._areIntegrationsEnabled() }
isTokenValid = { isTokenValid } isTokenValid = { isTokenValid }
isValidating = { isValidating } isValidating = { isValidating }
localRecordingOnlySelf = { localRecordingOnlySelf }
onChange = { this._onSelectedRecordingServiceChanged } onChange = { this._onSelectedRecordingServiceChanged }
onLocalRecordingSelfChange = { this._onLocalRecordingSelfChange }
onSharingSettingChanged = { this._onSharingSettingChanged } onSharingSettingChanged = { this._onSharingSettingChanged }
selectedRecordingService = { selectedRecordingService } selectedRecordingService = { selectedRecordingService }
sharingSetting = { sharingEnabled } sharingSetting = { sharingEnabled }
@ -105,6 +108,7 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
_onSubmit: () => boolean; _onSubmit: () => boolean;
_onSelectedRecordingServiceChanged: (string) => void; _onSelectedRecordingServiceChanged: (string) => void;
_onSharingSettingChanged: () => void; _onSharingSettingChanged: () => void;
_onLocalRecordingSelfChange: () => void;
} }
/** /**

View File

@ -25,7 +25,7 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const { t } = this.props; const { t, localRecordingVideoStop } = this.props;
return ( return (
<Dialog <Dialog
@ -33,7 +33,7 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
onSubmit = { this._onSubmit } onSubmit = { this._onSubmit }
titleKey = 'dialog.recording' titleKey = 'dialog.recording'
width = 'small'> width = 'small'>
{ t('dialog.stopRecordingWarning') } {t(localRecordingVideoStop ? 'recording.localRecordingVideoStop' : 'dialog.stopRecordingWarning') }
</Dialog> </Dialog>
); );
} }

View File

@ -133,27 +133,35 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
case START_LOCAL_RECORDING: { case START_LOCAL_RECORDING: {
const { localRecording } = getState()['features/base/config']; const { localRecording } = getState()['features/base/config'];
const { onlySelf } = action;
try { try {
await LocalRecordingManager.startLocalRecording({ dispatch, await LocalRecordingManager.startLocalRecording({ dispatch,
getState }); getState }, action.onlySelf);
const props = { const props = {
descriptionKey: 'recording.on', descriptionKey: 'recording.on',
titleKey: 'dialog.recording' titleKey: 'dialog.recording'
}; };
if (localRecording.notifyAllParticipants) { if (localRecording?.notifyAllParticipants && !onlySelf) {
dispatch(playSound(RECORDING_ON_SOUND_ID)); dispatch(playSound(RECORDING_ON_SOUND_ID));
} }
dispatch(showNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM)); dispatch(showNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch(updateLocalRecordingStatus(true)); dispatch(updateLocalRecordingStatus(true, onlySelf));
sendAnalytics(createRecordingEvent('started', 'local')); sendAnalytics(createRecordingEvent('started', `local${onlySelf ? '.self' : ''}`));
} catch (err) { } catch (err) {
logger.error('Capture failed', 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 = { const props = {
descriptionKey: noTabError ? 'recording.surfaceError' : 'recording.error', descriptionKey,
titleKey: 'recording.failedToStart' titleKey: 'recording.failedToStart'
}; };
@ -164,11 +172,12 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
case STOP_LOCAL_RECORDING: { case STOP_LOCAL_RECORDING: {
const { localRecording } = getState()['features/base/config']; const { localRecording } = getState()['features/base/config'];
const { onlySelf } = action;
if (LocalRecordingManager.isRecordingLocally()) { if (LocalRecordingManager.isRecordingLocally()) {
LocalRecordingManager.stopLocalRecording(); LocalRecordingManager.stopLocalRecording();
dispatch(updateLocalRecordingStatus(false)); dispatch(updateLocalRecordingStatus(false));
if (localRecording.notifyAllParticipants) { if (localRecording?.notifyAllParticipants && !onlySelf) {
dispatch(playSound(RECORDING_OFF_SOUND_ID)); dispatch(playSound(RECORDING_OFF_SOUND_ID));
} }
} }