[RN] Implement Recording on mobile
This commit is contained in:
parent
4ac367d403
commit
7164cd49e4
|
@ -1906,31 +1906,6 @@ export default {
|
|||
}
|
||||
});
|
||||
|
||||
/* eslint-enable max-params */
|
||||
room.on(
|
||||
JitsiConferenceEvents.RECORDER_STATE_CHANGED,
|
||||
recorderSession => {
|
||||
if (!recorderSession) {
|
||||
logger.error(
|
||||
'Received invalid recorder status update',
|
||||
recorderSession);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// These errors fire when the local participant has requested a
|
||||
// recording but the request itself failed, hence the missing
|
||||
// session ID because the recorder never started.
|
||||
if (recorderSession.getError()) {
|
||||
this._showRecordingErrorNotification(recorderSession);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(
|
||||
'Received a recorder status update with no ID or error');
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.KICKED, () => {
|
||||
APP.UI.hideStats();
|
||||
APP.UI.notifyKicked();
|
||||
|
@ -2728,57 +2703,5 @@ export default {
|
|||
if (score === -1 || (score >= 1 && score <= 5)) {
|
||||
APP.store.dispatch(submitFeedback(score, message, room));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {Object} recorderSession - The recorder session model from the
|
||||
* lib.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_showRecordingErrorNotification(recorderSession) {
|
||||
const isStreamMode
|
||||
= recorderSession.getMode()
|
||||
=== JitsiMeetJS.constants.recording.mode.STREAM;
|
||||
|
||||
switch (recorderSession.getError()) {
|
||||
case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
|
||||
APP.UI.messageHandler.showError({
|
||||
descriptionKey: 'recording.unavailable',
|
||||
descriptionArguments: {
|
||||
serviceName: isStreamMode
|
||||
? 'Live Streaming service'
|
||||
: 'Recording service'
|
||||
},
|
||||
titleKey: isStreamMode
|
||||
? 'liveStreaming.unavailableTitle'
|
||||
: 'recording.unavailableTitle'
|
||||
});
|
||||
break;
|
||||
case JitsiMeetJS.constants.recording.error.RESOURCE_CONSTRAINT:
|
||||
APP.UI.messageHandler.showError({
|
||||
descriptionKey: isStreamMode
|
||||
? 'liveStreaming.busy'
|
||||
: 'recording.busy',
|
||||
titleKey: isStreamMode
|
||||
? 'liveStreaming.busyTitle'
|
||||
: 'recording.busyTitle'
|
||||
});
|
||||
break;
|
||||
default:
|
||||
APP.UI.messageHandler.showError({
|
||||
descriptionKey: isStreamMode
|
||||
? 'liveStreaming.error'
|
||||
: 'recording.error',
|
||||
titleKey: isStreamMode
|
||||
? 'liveStreaming.failedToStart'
|
||||
: 'recording.failedToStart'
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,15 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* The type of Redux action which clears all the data of every sessions.
|
||||
*
|
||||
* {
|
||||
* type: CLEAR_RECORDING_SESSIONS
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const CLEAR_RECORDING_SESSIONS = Symbol('CLEAR_RECORDING_SESSIONS');
|
||||
|
||||
/**
|
||||
* The type of Redux action which updates the current known state of a recording
|
||||
* session.
|
||||
|
@ -9,3 +21,17 @@
|
|||
* @public
|
||||
*/
|
||||
export const RECORDING_SESSION_UPDATED = Symbol('RECORDING_SESSION_UPDATED');
|
||||
|
||||
/**
|
||||
* The type of Redux action which sets the pending recording notification UID to
|
||||
* use it for when hiding the notification is necessary, or unsets it when
|
||||
* undefined (or no param) is passed.
|
||||
*
|
||||
* {
|
||||
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
* uid: ?number
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_PENDING_RECORDING_NOTIFICATION_UID
|
||||
= Symbol('SET_PENDING_RECORDING_NOTIFICATION_UID');
|
||||
|
|
|
@ -1,4 +1,109 @@
|
|||
import { RECORDING_SESSION_UPDATED } from './actionTypes';
|
||||
// @flow
|
||||
|
||||
import {
|
||||
hideNotification,
|
||||
showErrorNotification,
|
||||
showNotification
|
||||
} from '../notifications';
|
||||
|
||||
import {
|
||||
CLEAR_RECORDING_SESSIONS,
|
||||
RECORDING_SESSION_UPDATED,
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Clears the data of every recording sessions.
|
||||
*
|
||||
* @returns {{
|
||||
* type: CLEAR_RECORDING_SESSIONS
|
||||
* }}
|
||||
*/
|
||||
export function clearRecordingSessions() {
|
||||
return {
|
||||
type: CLEAR_RECORDING_SESSIONS
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the pending recording notification should be removed from the
|
||||
* screen.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hidePendingRecordingNotification() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const { pendingNotificationUid } = getState()['features/recording'];
|
||||
|
||||
if (pendingNotificationUid) {
|
||||
dispatch(hideNotification(pendingNotificationUid));
|
||||
dispatch(setPendingRecordingNotificationUid());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets UID of the the pending recording 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.
|
||||
* redux.
|
||||
* @returns {{
|
||||
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
* uid: number
|
||||
* }}
|
||||
*/
|
||||
export function setPendingRecordingNotificationUid(uid: ?number) {
|
||||
return {
|
||||
type: SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||
uid
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the pending recording notification should be shown on the
|
||||
* screen.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showPendingRecordingNotification() {
|
||||
return (dispatch: Function) => {
|
||||
const showNotificationAction = showNotification({
|
||||
descriptionKey: 'recording.pending',
|
||||
isDismissAllowed: false,
|
||||
titleKey: 'dialog.recording'
|
||||
});
|
||||
|
||||
dispatch(showNotificationAction);
|
||||
|
||||
dispatch(setPendingRecordingNotificationUid(
|
||||
showNotificationAction.uid));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 stopped recording notification should be shown on the
|
||||
* screen for a given period.
|
||||
*
|
||||
* @returns {showNotification}
|
||||
*/
|
||||
export function showStoppedRecordingNotification() {
|
||||
return showNotification({
|
||||
descriptionKey: 'recording.off',
|
||||
titleKey: 'dialog.recording'
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the known state for a given recording session.
|
||||
|
@ -10,7 +115,7 @@ import { RECORDING_SESSION_UPDATED } from './actionTypes';
|
|||
* sessionData: Object
|
||||
* }}
|
||||
*/
|
||||
export function updateRecordingSessionData(session) {
|
||||
export function updateRecordingSessionData(session: Object) {
|
||||
return {
|
||||
type: RECORDING_SESSION_UPDATED,
|
||||
sessionData: {
|
||||
|
|
|
@ -34,8 +34,45 @@ export type Props = {
|
|||
/**
|
||||
* Abstract class for the {@code RecordingLabel} component.
|
||||
*/
|
||||
export default class AbstractRecordingLabel<P: Props, S: *>
|
||||
extends Component<P, S> {
|
||||
export default class AbstractRecordingLabel<P: Props>
|
||||
extends Component<P> {
|
||||
|
||||
/**
|
||||
* Implements React {@code Component}'s render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return this.props._visible ? this._renderLabel() : null;
|
||||
}
|
||||
|
||||
_getLabelKey: () => ?string
|
||||
|
||||
/**
|
||||
* Returns the label key that this indicator should render.
|
||||
*
|
||||
* @protected
|
||||
* @returns {?string}
|
||||
*/
|
||||
_getLabelKey() {
|
||||
switch (this.props.mode) {
|
||||
case JitsiRecordingConstants.mode.STREAM:
|
||||
return 'recording.live';
|
||||
case JitsiRecordingConstants.mode.FILE:
|
||||
return 'recording.rec';
|
||||
default:
|
||||
// Invalid mode is passed to the component.
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific label component.
|
||||
*
|
||||
* @protected
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderLabel: () => React$Element<*>
|
||||
|
||||
}
|
||||
|
||||
|
@ -50,7 +87,7 @@ export default class AbstractRecordingLabel<P: Props, S: *>
|
|||
* _visible: boolean
|
||||
* }}
|
||||
*/
|
||||
export function _abstractMapStateToProps(state: Object, ownProps: Props) {
|
||||
export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { mode } = ownProps;
|
||||
const _recordingSessions = state['features/recording'].sessionDatas;
|
||||
const _visible
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
createRecordingDialogEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The {@code JitsiConference} for the current conference.
|
||||
*/
|
||||
_conference: Object,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for {@code StartRecordingDialog} components.
|
||||
*/
|
||||
export default class AbstractStartRecordingDialog<P: Props>
|
||||
extends Component<P> {
|
||||
/**
|
||||
* Initializes a new {@code StartRecordingDialog} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
okTitleKey = 'dialog.confirm'
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.recording'
|
||||
width = 'small'>
|
||||
{ this._renderDialogContent() }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
|
||||
/**
|
||||
* Starts a file recording session.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
sendAnalytics(
|
||||
createRecordingDialogEvent('start', 'confirm.button')
|
||||
);
|
||||
|
||||
this.props._conference.startRecording({
|
||||
mode: JitsiRecordingConstants.mode.FILE
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog content.
|
||||
*
|
||||
* @protected
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
_renderDialogContent: () => React$Component<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code StartRecordingDialog} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: JitsiConference
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference
|
||||
};
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
createRecordingDialogEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
|
||||
import { getActiveSession } from '../../functions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link AbstractStopRecordingDialog}.
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The {@code JitsiConference} for the current conference.
|
||||
*/
|
||||
_conference: Object,
|
||||
|
||||
/**
|
||||
* The redux representation of the recording session to be stopped.
|
||||
*/
|
||||
_fileRecordingSession: Object,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstract React Component for getting confirmation to stop a file recording
|
||||
* session in progress.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class AbstractStopRecordingDialog<P: Props>
|
||||
extends Component<P> {
|
||||
/**
|
||||
* Initializes a new {@code AbstrStopRecordingDialog} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
okTitleKey = 'dialog.confirm'
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.recording'
|
||||
width = 'small'>
|
||||
{ this._renderDialogContent() }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
|
||||
/**
|
||||
* Stops the recording session.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
|
||||
|
||||
const { _fileRecordingSession } = this.props;
|
||||
|
||||
if (_fileRecordingSession) {
|
||||
this.props._conference.stopRecording(_fileRecordingSession.id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog content.
|
||||
*
|
||||
* @protected
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
_renderDialogContent: () => React$Component<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code StopRecordingDialog} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: JitsiConference,
|
||||
* _fileRecordingSession: Object
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference,
|
||||
_fileRecordingSession:
|
||||
getActiveSession(state, JitsiRecordingConstants.mode.FILE)
|
||||
};
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
// @flow
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openDialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import {
|
||||
isLocalParticipantModerator
|
||||
} from '../../../base/participants';
|
||||
import {
|
||||
AbstractButton,
|
||||
type AbstractButtonProps
|
||||
} from '../../../base/toolbox';
|
||||
|
||||
import { getActiveSession } from '../../functions';
|
||||
|
||||
import StartRecordingDialog from './StartRecordingDialog';
|
||||
import StopRecordingDialog from './StopRecordingDialog';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RecordButton}.
|
||||
*/
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* The current conference object.
|
||||
*/
|
||||
_conference: Object,
|
||||
|
||||
/**
|
||||
* True if there is a running active recording, false otherwise.
|
||||
*/
|
||||
_isRecordingRunning: boolean,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The i18n translate function.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* An implementation of a button for starting and stopping recording.
|
||||
*/
|
||||
class RecordButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'Recording';
|
||||
iconName = 'recEnable';
|
||||
label = 'dialog.startRecording';
|
||||
toggledIconName = 'recDisable';
|
||||
toggledLabel = 'dialog.stopRecording';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { _isRecordingRunning, dispatch } = this.props;
|
||||
|
||||
dispatch(openDialog(
|
||||
_isRecordingRunning ? StopRecordingDialog : StartRecordingDialog
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isToggled() {
|
||||
return this.props._isRecordingRunning;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for the
|
||||
* {@code RecordButton} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: Object,
|
||||
* _isRecordingRunning: boolean,
|
||||
* visible: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state): Object {
|
||||
const isModerator = isLocalParticipantModerator(state);
|
||||
const { fileRecordingsEnabled } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference,
|
||||
_isRecordingRunning:
|
||||
Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE)),
|
||||
visible: isModerator && fileRecordingsEnabled
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RecordButton));
|
|
@ -0,0 +1,40 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import styles from '../styles';
|
||||
|
||||
import AbstractStartRecordingDialog, {
|
||||
type Props,
|
||||
_mapStateToProps
|
||||
} from './AbstractStartRecordingDialog';
|
||||
|
||||
/**
|
||||
* React Component for getting confirmation to start a file recording session.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
|
||||
/**
|
||||
* Renders the platform specific dialog content.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderDialogContent() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.messageContainer }>
|
||||
<Text>
|
||||
{ t('recording.startRecordingBody') }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(StartRecordingDialog));
|
|
@ -1,103 +1,33 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
createRecordingDialogEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link StartRecordingDialog}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The {@code JitsiConference} for the current conference.
|
||||
*/
|
||||
_conference: Object,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
import AbstractStartRecordingDialog, {
|
||||
type Props,
|
||||
_mapStateToProps
|
||||
} from './AbstractStartRecordingDialog';
|
||||
|
||||
/**
|
||||
* React Component for getting confirmation to start a file recording session.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StartRecordingDialog extends Component<Props> {
|
||||
class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
|
||||
/**
|
||||
* Initializes a new {@code StartRecordingDialog} instance.
|
||||
* Renders the platform specific dialog content.
|
||||
*
|
||||
* @param {Props} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
* @protected
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
_renderDialogContent() {
|
||||
const { t } = this.props;
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
okTitleKey = 'dialog.confirm'
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.recording'
|
||||
width = 'small'>
|
||||
{ this.props.t('recording.startRecordingBody') }
|
||||
</Dialog>
|
||||
t('recording.startRecordingBody')
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
|
||||
/**
|
||||
* Starts a file recording session.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
sendAnalytics(createRecordingDialogEvent('start', 'confirm.button'));
|
||||
|
||||
this.props._conference.startRecording({
|
||||
mode: JitsiRecordingConstants.mode.FILE
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code StartRecordingDialog} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: JitsiConference
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(StartRecordingDialog));
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import styles from '../styles';
|
||||
|
||||
import AbstractStopRecordingDialog, {
|
||||
type Props,
|
||||
_mapStateToProps
|
||||
} from './AbstractStopRecordingDialog';
|
||||
|
||||
/**
|
||||
* React Component for getting confirmation to stop a file recording session in
|
||||
* progress.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog content.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderDialogContent() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.messageContainer }>
|
||||
<Text>
|
||||
{ t('dialog.stopRecordingWarning') }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(StopRecordingDialog));
|
|
@ -1,35 +1,13 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
createRecordingDialogEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link StopRecordingDialog}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The {@code JitsiConference} for the current conference.
|
||||
*/
|
||||
_conference: Object,
|
||||
|
||||
/**
|
||||
* The redux representation of the recording session to be stopped.
|
||||
*/
|
||||
session: Object,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
import AbstractStopRecordingDialog, {
|
||||
type Props,
|
||||
_mapStateToProps
|
||||
} from './AbstractStopRecordingDialog';
|
||||
|
||||
/**
|
||||
* React Component for getting confirmation to stop a file recording session in
|
||||
|
@ -37,73 +15,21 @@ type Props = {
|
|||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StopRecordingDialog extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code StopRecordingDialog} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
* Renders the platform specific dialog content.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
* @protected
|
||||
* @returns {React$Component}
|
||||
*/
|
||||
render() {
|
||||
_renderDialogContent() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
okTitleKey = 'dialog.stopRecording'
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.recording'
|
||||
width = 'small'>
|
||||
{ this.props.t('dialog.stopRecordingWarning') }
|
||||
</Dialog>
|
||||
t('dialog.stopRecordingWarning')
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
|
||||
/**
|
||||
* Stops the recording session.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
|
||||
|
||||
const { session } = this.props;
|
||||
|
||||
if (session) {
|
||||
this.props._conference.stopRecording(session.id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code StopRecordingDialog} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: JitsiConference
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_conference: state['features/base/conference'].conference
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(StopRecordingDialog));
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { default as RecordButton } from './RecordButton';
|
||||
export { default as StartRecordingDialog } from './StartRecordingDialog';
|
||||
export { default as StopRecordingDialog } from './StopRecordingDialog';
|
||||
|
|
|
@ -6,52 +6,34 @@ import { connect } from 'react-redux';
|
|||
import { translate } from '../../base/i18n';
|
||||
import { CircularLabel } from '../../base/label';
|
||||
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
|
||||
import { combineStyles } from '../../base/styles';
|
||||
|
||||
import AbstractRecordingLabel, {
|
||||
type Props as AbstractProps,
|
||||
_abstractMapStateToProps
|
||||
type Props,
|
||||
_mapStateToProps
|
||||
} from './AbstractRecordingLabel';
|
||||
import styles from './styles';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* Style of the component passed as props.
|
||||
*/
|
||||
style: ?Object
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays the current state of
|
||||
* conference recording.
|
||||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
class RecordingLabel extends AbstractRecordingLabel<Props, *> {
|
||||
class RecordingLabel extends AbstractRecordingLabel<Props> {
|
||||
|
||||
/**
|
||||
* Implements React {@code Component}'s render.
|
||||
* Renders the platform specific label component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _visible, mode, style, t } = this.props;
|
||||
|
||||
if (!_visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let labelKey;
|
||||
_renderLabel() {
|
||||
let indicatorStyle;
|
||||
|
||||
switch (mode) {
|
||||
switch (this.props.mode) {
|
||||
case JitsiRecordingConstants.mode.STREAM:
|
||||
labelKey = 'recording.live';
|
||||
indicatorStyle = styles.indicatorLive;
|
||||
break;
|
||||
case JitsiRecordingConstants.mode.FILE:
|
||||
labelKey = 'recording.rec';
|
||||
indicatorStyle = styles.indicatorRecording;
|
||||
break;
|
||||
default:
|
||||
|
@ -61,31 +43,12 @@ class RecordingLabel extends AbstractRecordingLabel<Props, *> {
|
|||
|
||||
return (
|
||||
<CircularLabel
|
||||
label = { t(labelKey) }
|
||||
style = {
|
||||
combineStyles(indicatorStyle, style)
|
||||
} />
|
||||
label = { this.props.t(this._getLabelKey()) }
|
||||
style = { indicatorStyle } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code RecordingLabel}'s props.
|
||||
*
|
||||
* NOTE: This component has no props other than the abstract ones but keeping
|
||||
* the coding style the same for consistency reasons.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The component's own props.
|
||||
* @private
|
||||
* @returns {{
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: Object, ownProps: Object) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state, ownProps)
|
||||
};
|
||||
_getLabelKey: () => ?string
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RecordingLabel));
|
||||
|
|
|
@ -5,243 +5,35 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { CircularLabel } from '../../base/label';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
|
||||
|
||||
import AbstractRecordingLabel, {
|
||||
type Props as AbstractProps,
|
||||
_abstractMapStateToProps
|
||||
type Props,
|
||||
_mapStateToProps
|
||||
} from './AbstractRecordingLabel';
|
||||
|
||||
/**
|
||||
* The translation keys to use when displaying messages. The values are set
|
||||
* lazily to work around circular dependency issues with lib-jitsi-meet causing
|
||||
* undefined imports.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
let TRANSLATION_KEYS_BY_MODE = null;
|
||||
|
||||
/**
|
||||
* Lazily initializes TRANSLATION_KEYS_BY_MODE with translation keys to be used
|
||||
* by the {@code RecordingLabel} for messaging recording session state.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _getTranslationKeysByMode() {
|
||||
if (!TRANSLATION_KEYS_BY_MODE) {
|
||||
const {
|
||||
error: errorConstants,
|
||||
mode: modeConstants,
|
||||
status: statusConstants
|
||||
} = JitsiRecordingConstants;
|
||||
|
||||
TRANSLATION_KEYS_BY_MODE = {
|
||||
[modeConstants.FILE]: {
|
||||
status: {
|
||||
[statusConstants.PENDING]: 'recording.pending',
|
||||
[statusConstants.OFF]: 'recording.off'
|
||||
},
|
||||
errors: {
|
||||
[errorConstants.BUSY]: 'recording.failedToStart',
|
||||
[errorConstants.ERROR]: 'recording.error'
|
||||
}
|
||||
},
|
||||
[modeConstants.STREAM]: {
|
||||
status: {
|
||||
[statusConstants.PENDING]: 'liveStreaming.pending',
|
||||
[statusConstants.OFF]: 'liveStreaming.off'
|
||||
},
|
||||
errors: {
|
||||
[errorConstants.BUSY]: 'liveStreaming.busy',
|
||||
[errorConstants.ERROR]: 'liveStreaming.error'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return TRANSLATION_KEYS_BY_MODE;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RecordingLabel}.
|
||||
*/
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* The redux representation of a recording session.
|
||||
*/
|
||||
session: Object,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link RecordingLabel}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Whether or not the {@link RecordingLabel} should be invisible.
|
||||
*/
|
||||
hidden: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays the current state of
|
||||
* conference recording.
|
||||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
class RecordingLabel extends AbstractRecordingLabel<Props, State> {
|
||||
_autohideTimeout: TimeoutID;
|
||||
|
||||
state = {
|
||||
hidden: false
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
session: {}
|
||||
};
|
||||
|
||||
class RecordingLabel extends AbstractRecordingLabel<Props> {
|
||||
/**
|
||||
* Sets a timeout to automatically hide the {@link RecordingLabel} if the
|
||||
* recording session started as failed.
|
||||
* Renders the platform specific label component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this.props.session.status === JitsiRecordingConstants.status.OFF) {
|
||||
this._setHideTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout to automatically hide {the @link RecordingLabel} if it has
|
||||
* transitioned to off.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { status } = this.props.session;
|
||||
const nextStatus = nextProps.session.status;
|
||||
|
||||
if (status !== JitsiRecordingConstants.status.OFF
|
||||
&& nextStatus === JitsiRecordingConstants.status.OFF) {
|
||||
this._setHideTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the timeout for automatically hiding the {@link RecordingLabel}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._clearAutoHideTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
if (this.state.hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
error: errorConstants,
|
||||
mode: modeConstants,
|
||||
status: statusConstants
|
||||
} = JitsiRecordingConstants;
|
||||
const { session } = this.props;
|
||||
const allTranslationKeys = _getTranslationKeysByMode();
|
||||
const translationKeys = allTranslationKeys[session.mode];
|
||||
let circularLabelClass, circularLabelKey, messageKey;
|
||||
|
||||
switch (session.status) {
|
||||
case statusConstants.OFF: {
|
||||
if (session.error) {
|
||||
messageKey = translationKeys.errors[session.error]
|
||||
|| translationKeys.errors[errorConstants.ERROR];
|
||||
} else {
|
||||
messageKey = translationKeys.status[statusConstants.OFF];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case statusConstants.ON:
|
||||
circularLabelClass = session.mode;
|
||||
circularLabelKey = session.mode === modeConstants.STREAM
|
||||
? 'recording.live' : 'recording.rec';
|
||||
break;
|
||||
case statusConstants.PENDING:
|
||||
messageKey = translationKeys.status[statusConstants.PENDING];
|
||||
break;
|
||||
}
|
||||
|
||||
const className = `recording-label ${
|
||||
messageKey ? 'center-message' : ''}`;
|
||||
|
||||
_renderLabel() {
|
||||
return (
|
||||
<div className = { className }>
|
||||
{ messageKey
|
||||
? <div>
|
||||
{ this.props.t(messageKey) }
|
||||
</div>
|
||||
: <CircularLabel
|
||||
className = { circularLabelClass }
|
||||
label = { this.props.t(circularLabelKey) } /> }
|
||||
<div>
|
||||
<CircularLabel
|
||||
className = { this.props.mode }
|
||||
label = { this.props.t(this._getLabelKey()) } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the timeout for automatically hiding {@link RecordingLabel}.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_clearAutoHideTimeout() {
|
||||
clearTimeout(this._autohideTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout to automatically hide {@link RecordingLabel}.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setHideTimeout() {
|
||||
this._autohideTimeout = setTimeout(() => {
|
||||
this.setState({ hidden: true });
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code RecordingLabel}'s props.
|
||||
*
|
||||
* NOTE: This component has no props other than the abstract ones but keeping
|
||||
* the coding style the same for consistency reasons.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The component's own props.
|
||||
* @private
|
||||
* @returns {{
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: Object, ownProps: Object) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state, ownProps)
|
||||
};
|
||||
_getLabelKey: () => ?string
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RecordingLabel));
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
export { StartLiveStreamDialog, StopLiveStreamDialog } from './LiveStream';
|
||||
export { StartRecordingDialog, StopRecordingDialog } from './Recording';
|
||||
export {
|
||||
RecordButton,
|
||||
StartRecordingDialog,
|
||||
StopRecordingDialog
|
||||
} from './Recording';
|
||||
export { default as RecordingLabel } from './RecordingLabel';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { ColorPalette, createStyleSheet } from '../../base/styles';
|
||||
import { BoxModel, ColorPalette, createStyleSheet } from '../../base/styles';
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Components} of the feature recording.
|
||||
|
@ -19,5 +19,10 @@ export default createStyleSheet({
|
|||
*/
|
||||
indicatorRecording: {
|
||||
backgroundColor: ColorPalette.red
|
||||
},
|
||||
|
||||
messageContainer: {
|
||||
paddingHorizontal: BoxModel.padding,
|
||||
paddingVertical: 1.5 * BoxModel.padding
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/* @flow */
|
||||
|
||||
import { CONFERENCE_WILL_JOIN } from '../base/conference';
|
||||
import {
|
||||
import { CONFERENCE_WILL_JOIN, getCurrentConference } from '../base/conference';
|
||||
import JitsiMeetJS, {
|
||||
JitsiConferenceEvents,
|
||||
JitsiRecordingConstants
|
||||
} from '../base/lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import {
|
||||
playSound,
|
||||
registerSound,
|
||||
|
@ -15,7 +15,14 @@ import {
|
|||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
||||
|
||||
import { updateRecordingSessionData } from './actions';
|
||||
import {
|
||||
clearRecordingSessions,
|
||||
hidePendingRecordingNotification,
|
||||
showPendingRecordingNotification,
|
||||
showRecordingError,
|
||||
showStoppedRecordingNotification,
|
||||
updateRecordingSessionData
|
||||
} from './actions';
|
||||
import { RECORDING_SESSION_UPDATED } from './actionTypes';
|
||||
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from './constants';
|
||||
import { getSessionById } from './functions';
|
||||
|
@ -24,37 +31,50 @@ import {
|
|||
RECORDING_ON_SOUND_FILE
|
||||
} from './sounds';
|
||||
|
||||
/**
|
||||
* 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(store => next => action => {
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
let oldSessionData;
|
||||
|
||||
if (action.type === RECORDING_SESSION_UPDATED) {
|
||||
oldSessionData
|
||||
= getSessionById(store.getState(), action.sessionData.id);
|
||||
= getSessionById(getState(), action.sessionData.id);
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
store.dispatch(registerSound(
|
||||
dispatch(registerSound(
|
||||
RECORDING_OFF_SOUND_ID,
|
||||
RECORDING_OFF_SOUND_FILE));
|
||||
|
||||
store.dispatch(registerSound(
|
||||
dispatch(registerSound(
|
||||
RECORDING_ON_SOUND_ID,
|
||||
RECORDING_ON_SOUND_FILE));
|
||||
|
||||
break;
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
store.dispatch(unregisterSound(RECORDING_OFF_SOUND_ID));
|
||||
store.dispatch(unregisterSound(RECORDING_ON_SOUND_ID));
|
||||
dispatch(unregisterSound(RECORDING_OFF_SOUND_ID));
|
||||
dispatch(unregisterSound(RECORDING_ON_SOUND_ID));
|
||||
|
||||
break;
|
||||
|
||||
|
@ -65,12 +85,17 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
JitsiConferenceEvents.RECORDER_STATE_CHANGED,
|
||||
recorderSession => {
|
||||
|
||||
if (recorderSession && recorderSession.getID()) {
|
||||
store.dispatch(
|
||||
updateRecordingSessionData(recorderSession));
|
||||
if (recorderSession) {
|
||||
recorderSession.getID()
|
||||
&& dispatch(
|
||||
updateRecordingSessionData(recorderSession));
|
||||
|
||||
return;
|
||||
recorderSession.getError()
|
||||
&& _showRecordingErrorNotification(
|
||||
recorderSession, dispatch);
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
break;
|
||||
|
@ -78,18 +103,26 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
case RECORDING_SESSION_UPDATED: {
|
||||
const updatedSessionData
|
||||
= getSessionById(store.getState(), action.sessionData.id);
|
||||
= getSessionById(getState(), action.sessionData.id);
|
||||
|
||||
if (updatedSessionData.mode === JitsiRecordingConstants.mode.FILE) {
|
||||
const { OFF, ON } = JitsiRecordingConstants.status;
|
||||
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
|
||||
|
||||
if (updatedSessionData.status === ON
|
||||
&& (!oldSessionData || oldSessionData.status !== ON)) {
|
||||
store.dispatch(playSound(RECORDING_ON_SOUND_ID));
|
||||
} else if (updatedSessionData.status === OFF
|
||||
&& (!oldSessionData || oldSessionData.status !== OFF)) {
|
||||
store.dispatch(stopSound(RECORDING_ON_SOUND_ID));
|
||||
store.dispatch(playSound(RECORDING_OFF_SOUND_ID));
|
||||
if (updatedSessionData.status === PENDING
|
||||
&& (!oldSessionData || oldSessionData.status !== PENDING)) {
|
||||
dispatch(showPendingRecordingNotification());
|
||||
} else if (updatedSessionData.status !== PENDING) {
|
||||
dispatch(hidePendingRecordingNotification());
|
||||
|
||||
if (updatedSessionData.status === ON
|
||||
&& (!oldSessionData || oldSessionData.status !== ON)) {
|
||||
dispatch(playSound(RECORDING_ON_SOUND_ID));
|
||||
} else if (updatedSessionData.status === OFF
|
||||
&& (!oldSessionData || oldSessionData.status !== OFF)) {
|
||||
dispatch(stopSound(RECORDING_ON_SOUND_ID));
|
||||
dispatch(playSound(RECORDING_OFF_SOUND_ID));
|
||||
dispatch(showStoppedRecordingNotification());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,3 +132,56 @@ MiddlewareRegistry.register(store => 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} recorderSession - The recorder session model from the
|
||||
* lib.
|
||||
* @param {Dispatch} dispatch - The Redux Dispatch function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _showRecordingErrorNotification(recorderSession, dispatch) {
|
||||
const isStreamMode
|
||||
= recorderSession.getMode()
|
||||
=== JitsiMeetJS.constants.recording.mode.STREAM;
|
||||
|
||||
switch (recorderSession.getError()) {
|
||||
case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
|
||||
dispatch(showRecordingError({
|
||||
descriptionKey: 'recording.unavailable',
|
||||
descriptionArguments: {
|
||||
serviceName: isStreamMode
|
||||
? 'Live Streaming service'
|
||||
: 'Recording service'
|
||||
},
|
||||
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;
|
||||
default:
|
||||
dispatch(showRecordingError({
|
||||
descriptionKey: isStreamMode
|
||||
? 'liveStreaming.error'
|
||||
: 'recording.error',
|
||||
titleKey: isStreamMode
|
||||
? 'liveStreaming.failedToStart'
|
||||
: 'recording.failedToStart'
|
||||
}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
import { RECORDING_SESSION_UPDATED } from './actionTypes';
|
||||
import {
|
||||
CLEAR_RECORDING_SESSIONS,
|
||||
RECORDING_SESSION_UPDATED,
|
||||
SET_PENDING_RECORDING_NOTIFICATION_UID
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
sessionDatas: []
|
||||
|
@ -11,6 +15,13 @@ const DEFAULT_STATE = {
|
|||
ReducerRegistry.register('features/recording',
|
||||
(state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
|
||||
case CLEAR_RECORDING_SESSIONS:
|
||||
return {
|
||||
...state,
|
||||
sessionDatas: []
|
||||
};
|
||||
|
||||
case RECORDING_SESSION_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
|
@ -18,6 +29,12 @@ ReducerRegistry.register('features/recording',
|
|||
_updateSessionDatas(state.sessionDatas, action.sessionData)
|
||||
};
|
||||
|
||||
case SET_PENDING_RECORDING_NOTIFICATION_UID:
|
||||
return {
|
||||
...state,
|
||||
pendingNotificationUid: action.uid
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { connect } from 'react-redux';
|
|||
import { BottomSheet, hideDialog } from '../../../base/dialog';
|
||||
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
||||
import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
|
||||
import { RecordButton } from '../../../recording';
|
||||
import { RoomLockButton } from '../../../room-lock';
|
||||
|
||||
import AudioOnlyButton from './AudioOnlyButton';
|
||||
|
@ -68,6 +69,7 @@ class OverflowMenu extends Component<Props> {
|
|||
<ToggleCameraButton { ...buttonProps } />
|
||||
<AudioOnlyButton { ...buttonProps } />
|
||||
<RoomLockButton { ...buttonProps } />
|
||||
<RecordButton { ...buttonProps } />
|
||||
<PictureInPictureButton { ...buttonProps } />
|
||||
</BottomSheet>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue