feat(local-recording) Add self local recording (#11706)
Only record local participant audio/ video streams
This commit is contained in:
parent
b85da1e1bb
commit
a7c96e302f
|
@ -19,6 +19,10 @@
|
|||
font-size: 14px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&.space-top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.recording-header-line {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
import './middleware.any.js';
|
|
@ -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);
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -139,6 +139,7 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
|
|||
= 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<Props, State> {
|
|||
userName: undefined,
|
||||
sharingEnabled: true,
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -326,7 +341,7 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
|
|||
break;
|
||||
}
|
||||
case RECORDING_TYPES.LOCAL: {
|
||||
dispatch(startLocalVideoRecording());
|
||||
dispatch(startLocalVideoRecording(this.state.localRecordingOnlySelf));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -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<P: Props>
|
|||
|
||||
if (this.props._localRecording) {
|
||||
this.props.dispatch(stopLocalVideoRecording());
|
||||
if (this.props.localRecordingVideoStop) {
|
||||
this.props.dispatch(setVideoMuted(true));
|
||||
}
|
||||
} else {
|
||||
const { _fileRecordingSession } = this.props;
|
||||
|
||||
|
|
|
@ -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,12 +158,41 @@ 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();
|
||||
|
||||
this.selfRecording.on = onlySelf;
|
||||
this.recordingData = [];
|
||||
this.roomName = getRoomName(getState());
|
||||
let gdmStream: MediaStream = new MediaStream();
|
||||
const tracks = getTrackState(getState());
|
||||
|
||||
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({
|
||||
|
@ -157,9 +201,8 @@ const LocalRecordingManager: ILocalRecordingManager = {
|
|||
});
|
||||
}
|
||||
|
||||
this.recordingData = [];
|
||||
// @ts-ignore
|
||||
const gdmStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
gdmStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
// @ts-ignore
|
||||
video: { displaySurface: 'browser', frameRate: 30 },
|
||||
audio: {
|
||||
|
@ -180,8 +223,6 @@ const LocalRecordingManager: ILocalRecordingManager = {
|
|||
|
||||
this.initializeAudioMixer();
|
||||
this.mixAudioStream(gdmStream);
|
||||
this.roomName = getRoomName(getState());
|
||||
const tracks = getTrackState(getState());
|
||||
|
||||
tracks.forEach((track: any) => {
|
||||
if (track.mediaType === MEDIA_TYPE.AUDIO) {
|
||||
|
@ -190,11 +231,12 @@ const LocalRecordingManager: ILocalRecordingManager = {
|
|||
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 = {
|
|||
}
|
||||
});
|
||||
|
||||
if(!onlySelf) {
|
||||
this.recorder.addEventListener('stop', () => {
|
||||
this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
|
||||
gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
|
||||
gdmStream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
|
||||
});
|
||||
|
||||
gdmStream.addEventListener('inactive', () => {
|
||||
gdmStream?.addEventListener('inactive', () => {
|
||||
dispatch(stopLocalVideoRecording());
|
||||
});
|
||||
|
||||
this.stream.addEventListener('inactive', () => {
|
||||
dispatch(stopLocalVideoRecording());
|
||||
});
|
||||
}
|
||||
|
||||
this.recorder.start(5000);
|
||||
},
|
||||
|
|
|
@ -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,6 +639,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Container
|
||||
className = 'recording-header recording-header-line'
|
||||
|
@ -657,17 +668,47 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||
value = { this.props.selectedRecordingService
|
||||
=== RECORDING_TYPES.LOCAL } />
|
||||
</Container>
|
||||
{selectedRecordingService === RECORDING_TYPES.LOCAL
|
||||
&& <>
|
||||
</Container>
|
||||
{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'>
|
||||
{t('recording.localRecordingWarning')}
|
||||
</Text>
|
||||
{_localRecordingNoNotification && <Text className = 'local-recording-warning notification'>
|
||||
{_localRecordingNoNotification && !this.props.localRecordingOnlySelf
|
||||
&& <Text className = 'local-recording-warning notification'>
|
||||
{t('recording.localRecordingNoNotificationWarning')}
|
||||
</Text>}
|
||||
</>
|
||||
</Text>
|
||||
}
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
@ -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')
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,7 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { t, localRecordingVideoStop } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -33,7 +33,7 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
|
|||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.recording'
|
||||
width = 'small'>
|
||||
{ t('dialog.stopRecordingWarning') }
|
||||
{t(localRecordingVideoStop ? 'recording.localRecordingVideoStop' : 'dialog.stopRecordingWarning') }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue