Compare commits

...

1 Commits

Author SHA1 Message Date
Hristo Terezov
e7cb719be0 feat(jibri-queue): JibriQueue support. 2020-08-18 12:57:48 -05:00
21 changed files with 710 additions and 118 deletions

74
css/_notifications.scss Normal file
View File

@@ -0,0 +1,74 @@
@include keyframes(exiting) {
from {
opacity: 1;
transform: translate(0, 0);
}
to {
opacity: 0;
transform: translate(-200px, 0);
}
}
.notificationsContainer {
display: flex;
position: absolute;
bottom: 0px;
flex-direction: column;
width: 400px;
left: 80px;
.topContainer {
padding-bottom: 16px;
// transition: height 0.4s ease-in-out;
.notification {
width: 400px;
z-index: 5;
&:nth-child(n+2) {
margin-top: 16px;
}
&.exiting {
animation-name: exiting;
animation-duration: 0.4s;
}
}
}
.bottomContainer {
margin-bottom: 64px;
position: relative;
height: 100%;
.notification {
width: 400px;
bottom: 0px;
&:nth-child(1) {
z-index: 5;
}
&:nth-child(n+2) {
transition: transform 0.4s ease-in-out;
z-index: 4;
position: absolute;
transform: translateY(100%) translateY(16px);
}
&:nth-child(n+4) {
display: none;
}
&.exiting {
animation-name: exiting;
animation-duration: 0.4s;
}
&.exiting+.notification {
transform: translateY(0);
}
}
}
}

View File

@@ -115,3 +115,19 @@
font-size: 12px;
}
}
.jibri-queue-info {
display: flex;
flex-flow: column;
.footer {
background: #a4b8a4D1;
border-radius: 3px;
font-weight: bold;
color: #5e6d7a;
padding: 5px;
margin-right: 30px;
margin-top: 5px;
}
}

View File

@@ -102,5 +102,6 @@ $flagsImagePath: "../images/";
@import 'premeeting-screens';
@import 'e2ee';
@import 'responsive';
@import 'notifications';
/* Modules END */

View File

@@ -216,7 +216,9 @@
"kickParticipantDialog": "Are you sure you want to kick this participant?",
"kickParticipantTitle": "Kick this participant?",
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
"leaveJibriQueue": "Exit queue",
"liveStreaming": "Live Streaming",
"leaveJibriQueueWarning": "Are you sure you would like to exit the queue?",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Not possible while recording is active",
"liveStreamingDisabledForGuestTooltip": "Guests can't start live streaming.",
"liveStreamingDisabledTooltip": "Start live stream disabled.",
@@ -908,5 +910,22 @@
"passwordJoinButton": "Join",
"reject": "Reject",
"toggleLabel": "Enable lobby"
},
"jibriQueue": {
"recording": {
"title": "You have joined a recording queue!",
"time": "Estimated time for starting the recording: {{time}}",
"footer": "For unlimited recordings you should subscribe to 8x8 Meetings",
"left": "You have left the recording queue!"
},
"livestreaming": {
"title": "You have joined a live streaming queue!",
"time": "Estimated time for starting the live streaming: {{time}}",
"footer": "For unlimited live streaming you should subscribe to 8x8 Meetings",
"left": "You have left the live streaming queue!"
},
"position": "{{count}} more person is waiting in front of you.",
"position_plural": "{{count}} more people are waiting in front of you.",
"exit": "Exit queue"
}
}

View File

@@ -238,6 +238,7 @@ function initCommands() {
return;
}
const jibriQueueJID = state['features/base/config'].jibriQueueJID;
let recordingConfig;
if (mode === JitsiRecordingConstants.mode.FILE) {
@@ -251,7 +252,8 @@ function initCommands() {
'token': dropboxToken
}
}
})
}),
jibriQueueJID
};
} else {
recordingConfig = {
@@ -260,12 +262,14 @@ function initCommands() {
'file_recording_metadata': {
'share': shouldShare
}
})
}),
jibriQueueJID
};
}
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
recordingConfig = {
broadcastId: youtubeBroadcastID,
jibriQueueJID,
mode: JitsiRecordingConstants.mode.STREAM,
streamId: youtubeStreamKey
};
@@ -275,7 +279,9 @@ function initCommands() {
return;
}
conference.startRecording(recordingConfig);
conference.startRecording(recordingConfig).catch(() => {
// prevent unhandled promise rejection.
});
},
/**
@@ -302,8 +308,10 @@ function initCommands() {
const activeSession = getActiveSession(state, mode);
if (activeSession && activeSession.id) {
conference.stopRecording(activeSession.id);
if (activeSession && (activeSession.id || activeSession.queueID)) {
conference.stopRecording(activeSession.id, activeSession.queueID).catch(() => {
// prevent unhandled promise rejection.
});
} else {
logger.error('No recording or streaming session found');
}

12
package-lock.json generated
View File

@@ -982,9 +982,9 @@
}
},
"@atlaskit/portal": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/portal/-/portal-4.0.1.tgz",
"integrity": "sha512-dYe/YozUkFZ0NitZ2dfnHE/d8i1Tq+pcdlwXd3+PyhzvPbnPoseZpQ/jC+KAbxZ4wCzLwSF+SXapdAJSbwkLfg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/portal/-/portal-4.0.0.tgz",
"integrity": "sha512-FLvq90T2zt7bUOUkOb90xbB1JGOI77456euDwrz1d9NYVoe+kSQr4Xau7kQLUgmpzmH4Sd4BHOvp50JHJ0qezw==",
"requires": {
"@atlaskit/theme": "^10.0.0",
"exenv": "^1.2.2",
@@ -993,9 +993,9 @@
},
"dependencies": {
"@atlaskit/theme": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-10.0.1.tgz",
"integrity": "sha512-XUor9lYlX0yTRSxd/rvaL8i2gdm0PDbOV+KhuezpuGBaS0opzseRrCnEc+OMGmpWRYHjCRyEugp6FwSSquFb8w==",
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-10.0.2.tgz",
"integrity": "sha512-TfzWnISbO9R9BkBpu2PD1bvGku2LqFkzhcBiJUAjWcL2E5nIt9NZIlnnCZ4A94510Lnd4B/orcghh5iid5R/LA==",
"requires": {
"exenv": "^1.2.2",
"prop-types": "^15.5.10",

View File

@@ -31,6 +31,7 @@
"@atlaskit/tabs": "8.0.11",
"@atlaskit/theme": "7.0.2",
"@atlaskit/toggle": "5.0.14",
"@atlaskit/portal": "4.0.0",
"@atlaskit/tooltip": "12.1.13",
"@jitsi/js-utils": "1.0.1",
"@microsoft/microsoft-graph-client": "1.1.0",

View File

@@ -1,7 +1,8 @@
// @flow
import { FlagGroup } from '@atlaskit/flag';
import Portal from '@atlaskit/portal';
import React from 'react';
import { Transition, TransitionGroup } from 'react-transition-group';
import { connect } from '../../../base/redux';
import AbstractNotificationsContainer, {
@@ -27,6 +28,16 @@ type Props = AbstractProps & {
* @extends {Component}
*/
class NotificationsContainer extends AbstractNotificationsContainer<Props> {
/**
* Creates new NotificationContainer instance.
*
* @param {Props} props - The props of the react component.
*/
constructor(props: Props) {
super(props);
this._renderNotification = this._renderNotification.bind(this);
}
/**
* Implements React's {@link Component#render()}.
@@ -40,39 +51,92 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
}
return (
<FlagGroup onDismissed = { this._onDismissed }>
{ this._renderFlags() }
</FlagGroup>
<Portal zIndex = { 600 }>
<div className = 'notificationsContainer'>
{ this._renderTopNotificationsContainer() }
{ this._renderBottomNotificationsContainer() }
</div>
</Portal>
);
}
_onDismissed: number => void;
/**
* Renders notifications to display as ReactElements. An empty array will
* be returned if notifications are disabled.
* Renders the bottom notification container.
*
* @private
* @returns {ReactElement[]}
* @returns {ReactElement}
*/
_renderFlags() {
_renderBottomNotificationsContainer() {
const { _notifications } = this.props;
return _notifications.map(notification => {
const { props, uid } = notification;
return (
<TransitionGroup className = 'bottomContainer'>
{
_notifications.filter(n => n.props.position !== 'top').map((notification, index) => {
const { props, uid } = notification;
// The id attribute is necessary as {@code FlagGroup} looks for
// either id or key to set a key on notifications, but accessing
// props.key will cause React to print an error.
return (
<Notification
{ ...props }
id = { uid }
key = { uid }
uid = { uid } />
return this._renderNotification({
...props,
isDismissAllowed: index > 0 ? false : props.isDismissAllowed
}, uid);
})
}
</TransitionGroup>
);
}
);
});
_renderNotification: (string, number) => Function;
/**
* Renders a notification.
*
* @param {Object} props - The props for the Notification component.
* @param {string} uid - A unique ID for the notification.
* @returns {Function} - Returns a transition function for the Transition component.
*/
_renderNotification(props, uid) {
return (
<Transition
key = { uid }
timeout = { 400 }>
{
transitionState => (
<div className = { `notification ${transitionState}` }>
<Notification
{ ...props }
id = { uid }
key = { uid }
onDismissed = { this._onDismissed }
uid = { uid } />
</div>
)
}
</Transition>
);
}
/**
* Renders the top notifications container.
*
* @private
* @returns {ReactElement}
*/
_renderTopNotificationsContainer() {
const { _notifications } = this.props;
return (
<TransitionGroup className = 'topContainer'>
{
_notifications.filter(n => n.props.position === 'top').map(notification => {
const { props, uid } = notification;
return this._renderNotification(props, uid);
})
}
</TransitionGroup>
);
}
}

View File

@@ -46,3 +46,17 @@ export const SET_PENDING_RECORDING_NOTIFICATION_UID
* }
*/
export const SET_STREAM_KEY = 'SET_STREAM_KEY';
/**
* The type of Redux action which sets the waiting in queue 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_WAITING_IN_RECORDING_NOTIFICATION_UID,
* streamType: string,
* uid: ?number
* }
* @public
*/
export const SET_WAITING_IN_RECORDING_NOTIFICATION_UID = 'SET_WAITING_IN_RECORDING_NOTIFICATION_UID';

View File

@@ -1,8 +1,12 @@
// @flow
import React from 'react';
import { openDialog } from '../base/dialog';
import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import {
NOTIFICATION_TIMEOUT,
NOTIFICATION_TYPE,
hideNotification,
showErrorNotification,
showNotification
@@ -12,8 +16,10 @@ import {
CLEAR_RECORDING_SESSIONS,
RECORDING_SESSION_UPDATED,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_STREAM_KEY
SET_STREAM_KEY,
SET_WAITING_IN_RECORDING_NOTIFICATION_UID
} from './actionTypes';
import { QueueInfo, StopLiveStreamDialog, StopRecordingDialog } from './components';
/**
* Clears the data of every recording sessions.
@@ -50,6 +56,25 @@ export function hidePendingRecordingNotification(streamType: string) {
};
}
/**
* Signals that the waiting in queue recording notification should be removed from the screen.
*
* @param {string} streamType - The type of the stream ({@code 'file'} or
* {@code 'stream'}).
* @returns {Function}
*/
export function hideWaitingInQueueRecordingNotification(streamType: string) {
return (dispatch: Function, getState: Function) => {
const { waitingInQueueNotificationUids } = getState()['features/recording'];
const waitingInQueueNotificationUid = waitingInQueueNotificationUids[streamType];
if (waitingInQueueNotificationUid) {
dispatch(hideNotification(waitingInQueueNotificationUid));
dispatch(_setWaitingInQueueRecordingNotificationUid(undefined, streamType));
}
};
}
/**
* Sets the stream key last used by the user for later reuse.
*
@@ -97,6 +122,22 @@ export function showPendingRecordingNotification(streamType: string) {
};
}
/**
* Signals that the jibri queue has been left and notification should be shown on the
* screen.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {showNotification}
*/
export function showQueueLeftRecordingNotification(streamType: string) {
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
return showNotification({
titleKey: `jibriQueue.${isLiveStreaming ? 'livestreaming' : 'recording'}.left`
}, NOTIFICATION_TIMEOUT);
}
/**
* Signals that the recording error notification should be shown.
*
@@ -175,6 +216,15 @@ export function updateRecordingSessionData(session: Object) {
= status === JitsiRecordingConstants.status.ON
? Date.now() / 1000
: undefined;
const queueID = session.getQueueID();
let queueEstimatedTimeOfStart, queuePosition;
if (status === JitsiRecordingConstants.status.WAITING_IN_QUEUE) {
const { position, estimatedTimeLeft } = session.getQueueMetrics();
queuePosition = position;
queueEstimatedTimeOfStart = (new Date()).getTime() + (estimatedTimeLeft * 1000);
}
return {
type: RECORDING_SESSION_UPDATED,
@@ -186,7 +236,10 @@ export function updateRecordingSessionData(session: Object) {
mode: session.getMode(),
status,
terminator: session.getTerminator(),
timestamp
timestamp,
queueID,
queuePosition,
queueEstimatedTimeOfStart
}
};
}
@@ -212,3 +265,57 @@ function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) {
uid
};
}
/**
* Sets UID of the the pending streaming notification to use it when hiding
* the notification is necessary, or unsets it when undefined (or no param) is
* passed.
*
* @param {?number} uid - The UID of the notification.
* @param {string} streamType - The type of the stream ({@code file} or {@code stream}).
* @returns {{
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
* streamType: string,
* uid: number
* }}
*/
function _setWaitingInQueueRecordingNotificationUid(uid: ?number, streamType: string) {
return {
type: SET_WAITING_IN_RECORDING_NOTIFICATION_UID,
streamType,
uid
};
}
/**
* Signals that the recording queue notification should be shown on the screen.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {Function}
*/
export function showWaitingInQueueRecordingNotification(streamType: string) {
return (dispatch: Function) => {
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
const showNotificationAction = showNotification({
appearance: NOTIFICATION_TYPE.INFO,
customActionNameKey: 'jibriQueue.exit',
customActionHandler: () => {
if (isLiveStreaming) {
dispatch(openDialog(StopLiveStreamDialog));
} else {
dispatch(openDialog(StopRecordingDialog));
}
return false;
},
position: 'top',
titleKey: `jibriQueue.${isLiveStreaming ? 'livestreaming' : 'recording'}.title`,
description: <QueueInfo />
});
dispatch(showNotificationAction);
dispatch(_setWaitingInQueueRecordingNotificationUid(
showNotificationAction.uid, streamType));
};
}

View File

@@ -217,6 +217,8 @@ export default class AbstractStartLiveStreamDialog<P: Props>
broadcastId: selectedBroadcastID,
mode: JitsiRecordingConstants.mode.STREAM,
streamId: key
}).catch(() => {
// prevent unhandled promise rejection.
});
return true;

View File

@@ -65,7 +65,9 @@ export default class AbstractStopLiveStreamDialog extends Component<Props> {
const { _session } = this.props;
if (_session) {
this.props._conference.stopRecording(_session.id);
this.props._conference.stopRecording(_session.id, _session.queueID).catch(() => {
// prevent unhandled promise rejection.
});
}
return true;

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux';
import AbstractStopLiveStreamDialog, {
_mapStateToProps
@@ -24,13 +25,17 @@ class StopLiveStreamDialog extends AbstractStopLiveStreamDialog {
* @returns {ReactElement}
*/
render() {
const { _session = {}, t } = this.props;
const isInQueue = _session.status === JitsiRecordingConstants.status.WAITING_IN_QUEUE;
return (
<Dialog
okKey = 'dialog.stopLiveStreaming'
okKey = { isInQueue ? 'dialog.leaveJibriQueue' : 'dialog.stopLiveStreaming' }
onSubmit = { this._onSubmit }
titleKey = 'dialog.liveStreaming'
width = 'small'>
{ this.props.t('dialog.stopStreamingWarning') }
{ t(isInQueue ? 'dialog.leaveJibriQueueWarning' : 'dialog.stopStreamingWarning') }
</Dialog>
);
}

View File

@@ -280,6 +280,8 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
_conference.startRecording({
mode: JitsiRecordingConstants.mode.FILE,
appData
}).catch(() => {
// prevent unhandled promise rejection.
});
if (_autoCaptionOnRecord) {

View File

@@ -65,7 +65,9 @@ export default class AbstractStopRecordingDialog<P: Props>
const { _fileRecordingSession } = this.props;
if (_fileRecordingSession) {
this.props._conference.stopRecording(_fileRecordingSession.id);
this.props._conference.stopRecording(_fileRecordingSession.id, _fileRecordingSession.queueID).catch(() => {
// prevent unhandled promise rejection.
});
}
return true;

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux';
import AbstractStopRecordingDialog, {
type Props,
@@ -24,15 +25,17 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { _fileRecordingSession = {}, t } = this.props;
const isInQueue = _fileRecordingSession.status === JitsiRecordingConstants.status.WAITING_IN_QUEUE;
return (
<Dialog
okKey = 'dialog.confirm'
okKey = { isInQueue ? 'dialog.leaveJibriQueue' : 'dialog.confirm' }
onSubmit = { this._onSubmit }
titleKey = 'dialog.recording'
width = 'small'>
{ t('dialog.stopRecordingWarning') }
{ t(isInQueue ? 'dialog.leaveJibriQueueWarning' : 'dialog.stopRecordingWarning') }
</Dialog>
);
}

View File

@@ -0,0 +1,230 @@
// @flow
import React, { Component } from 'react';
import { getLocalizedDurationFormatter, translate } from '../../../base/i18n';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { connect } from '../../../base/redux';
import { getActiveSession } from '../../functions';
type Props = {
/**
* The current position of the participant in the queue.
*/
_position: ?string,
/**
* The recording mode.
*/
_mode: string,
/**
* The ID of the queue.
*/
_queueID: string,
/**
* The time when the recording is expected to start.
*/
_estimatedTimeOfStart: number,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link QueueInfo}.
*/
type State = {
/**
* The current value of the timer for estimated time left.
*/
timerValue: ?string
};
/**
* Implements a React {@link Component} which displays the current state of the Jibri Queue.
*
* @extends {Component}
*/
class QueueInfo extends Component<Props, State> {
/**
* Handle for setInterval timer.
*/
_interval: IntervalID;
/**
* Initializes a new {@code QueueInfo} instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
timerValue: undefined
};
}
/**
* Stops the timer when component will be unmounted.
*
* @inheritdoc
*/
componentWillUnmount() {
this._stopTimer();
}
/**
* Starts the timer when component will be mounted.
*
* @inheritdoc
*/
componentDidMount() {
if (typeof this.props._estimatedTimeOfStart !== 'undefined') {
this._startTimer();
}
}
/**
* Implements React Component's componentDidUpdate.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (this.props._estimatedTimeOfStart !== prevProps._estimatedTimeOfStart) {
this._stopTimer(false);
this._startTimer();
}
}
/**
* Sets the current state values that will be used to render the timer.
*
* @param {number} refValueUTC - The initial UTC timestamp value.
* @param {number} currentValueUTC - The current UTC timestamp value.
*
* @returns {void}
*/
_setStateFromUTC(refValueUTC, currentValueUTC) {
if (!refValueUTC || !currentValueUTC) {
return;
}
const timerMsValue = currentValueUTC > refValueUTC ? 0 : refValueUTC - currentValueUTC;
const localizedTime = getLocalizedDurationFormatter(timerMsValue);
this.setState({
timerValue: localizedTime
});
}
/**
* Starts the timer.
*
* @returns {void}
*/
_startTimer() {
const { _estimatedTimeOfStart } = this.props;
if (!this._interval && typeof _estimatedTimeOfStart !== 'undefined') {
this._setStateFromUTC(_estimatedTimeOfStart, (new Date()).getTime());
this._interval = setInterval(() => {
this._setStateFromUTC(_estimatedTimeOfStart, (new Date()).getTime());
}, 1000);
}
}
/**
* Stops the timer.
*
* @param {boolean} [clearState] - If true, the timer value in the state will be cleared.
* @returns {void}
*/
_stopTimer(clearState = true) {
if (this._interval) {
clearInterval(this._interval);
delete this._interval;
}
if (clearState) {
this.setState({
timerValue: undefined
});
}
}
/**
* Implements React {@code Component}'s render.
*
* @inheritdoc
*/
render() {
const { _estimatedTimeOfStart, _mode, _position = 0, t } = this.props;
const { STREAM } = JitsiRecordingConstants.mode;
const timeTextKey = `jibriQueue.${_mode === STREAM ? 'livestreaming' : 'recording'}.time`;
const { timerValue } = this.state;
const footerText = t(`jibriQueue.${_mode === STREAM ? 'livestreaming' : 'recording'}.footer`);
const showFooter = typeof footerText === 'string' && footerText.length > 0;
return (
<div className = 'jibri-queue-info'>
<span className = 'position'>
{ t('jibriQueue.position', { count: _position }) }
</span>
{
typeof _estimatedTimeOfStart === 'undefined' || timerValue === 'undefined'
? null : <span className = 'time'>
{ t(timeTextKey, { time: timerValue }) }
</span>
}
{
showFooter ? <div className = 'footer'>{ footerText }</div> : null
}
</div>
);
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code AbstractRecordingLabel}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _estimatedTimeOfStart: number,
* _mode: string,
* _position: string,
* _queueID: string,
* t: Function
* }}
*/
export function _mapStateToProps(state: Object) {
const session = getActiveSession(state);
if (!session) {
return {};
}
const { id, mode, queueEstimatedTimeOfStart, queueID, queuePosition } = session;
return {
_sessionID: id,
_mode: mode,
_queueID: queueID,
_position: queuePosition,
_estimatedTimeOfStart: queueEstimatedTimeOfStart
};
}
export default translate(connect(_mapStateToProps)(QueueInfo));

View File

@@ -2,3 +2,4 @@
export { default as RecordingLabel } from './RecordingLabel';
export { default as RecordingLimitNotificationDescription } from './RecordingLimitNotificationDescription';
export { default as QueueInfo } from './QueueInfo';

View File

@@ -9,16 +9,17 @@ import { RECORDING_STATUS_PRIORITIES } from './constants';
* passed in mode.
*
* @param {Object} state - The redux state to search in.
* @param {string} mode - Find an active recording session of the given mode.
* @param {string|undefined} mode - Find an active recording session of the given mode.
* @returns {Object|undefined}
*/
export function getActiveSession(state: Object, mode: string) {
export function getActiveSession(state: Object, mode: ?string) {
const { sessionDatas } = state['features/recording'];
const { status: statusConstants } = JitsiRecordingConstants;
return sessionDatas.find(sessionData => sessionData.mode === mode
return sessionDatas.find(sessionData => (typeof mode === 'undefined' || sessionData.mode === mode)
&& (sessionData.status === statusConstants.ON
|| sessionData.status === statusConstants.PENDING));
|| sessionData.status === statusConstants.PENDING
|| sessionData.status === statusConstants.WAITING_IN_QUEUE));
}
/**
@@ -37,6 +38,8 @@ export function getRecordingDurationEstimation(size: ?number) {
* Searches in the passed in redux state for a recording session that matches
* the passed in recording session ID.
*
* NOTE: The sessoins in WAITING_IN_QUEUE status don't have ID yet.
*
* @param {Object} state - The redux state to search in.
* @param {string} id - The ID of the recording session to find.
* @returns {Object|undefined}
@@ -51,6 +54,8 @@ export function getSessionById(state: Object, id: string) {
* there is a session with the status OFF and one with PENDING, then the PENDING
* one will be shown, because that is likely more important for the user to see.
*
* NOTE: For all "queue" statuses the function returns undefined because we don't want to show label.
*
* @param {Object} state - The redux state to search in.
* @param {string} mode - The recording mode to get status for.
* @returns {string|undefined}

View File

@@ -24,11 +24,14 @@ import { RECORDING_SESSION_UPDATED } from './actionTypes';
import {
clearRecordingSessions,
hidePendingRecordingNotification,
hideWaitingInQueueRecordingNotification,
showPendingRecordingNotification,
showQueueLeftRecordingNotification,
showRecordingError,
showRecordingLimitNotification,
showStartedRecordingNotification,
showStoppedRecordingNotification,
showWaitingInQueueRecordingNotification,
updateRecordingSessionData
} from './actions';
import {
@@ -110,15 +113,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
conference.on(
JitsiConferenceEvents.RECORDER_STATE_CHANGED,
recorderSession => {
if (recorderSession) {
recorderSession.getID()
&& dispatch(
if (recorderSession.getID() || recorderSession.getQueueID()) {
dispatch(
updateRecordingSessionData(recorderSession));
}
recorderSession.getError()
&& _showRecordingErrorNotification(
if (recorderSession.getError()) {
_showRecordingErrorNotification(
recorderSession, dispatch);
}
}
return;
@@ -142,75 +146,91 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
break;
}
const updatedSessionData
= getSessionById(getState(), action.sessionData.id);
const { initiator, mode, terminator } = updatedSessionData;
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
const updatedSessionData = getSessionById(getState(), action.sessionData.id);
const { initiator, mode, status: newStatus, terminator } = updatedSessionData;
const { PENDING, OFF, ON, WAITING_IN_QUEUE, QUEUE_LEFT } = JitsiRecordingConstants.status;
if (updatedSessionData.status === PENDING
&& (!oldSessionData || oldSessionData.status !== PENDING)) {
dispatch(showPendingRecordingNotification(mode));
} else if (updatedSessionData.status !== PENDING) {
if (oldSessionData && oldSessionData.status === newStatus) {
return result;
}
if (newStatus !== WAITING_IN_QUEUE) {
dispatch(hideWaitingInQueueRecordingNotification(mode));
}
if (newStatus !== PENDING) {
dispatch(hidePendingRecordingNotification(mode));
}
if (updatedSessionData.status === ON
&& (!oldSessionData || oldSessionData.status !== ON)) {
if (initiator) {
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
switch (newStatus) {
case WAITING_IN_QUEUE:
dispatch(showWaitingInQueueRecordingNotification(mode));
break;
case QUEUE_LEFT:
dispatch(showQueueLeftRecordingNotification(mode));
break;
case PENDING:
dispatch(showPendingRecordingNotification(mode));
break;
case ON: {
if (initiator) {
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
} else if (typeof recordingLimit === 'object') {
// Show notification with additional information to the initiator.
dispatch(showRecordingLimitNotification(mode));
}
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {
break;
}
let soundID;
if (mode === JitsiRecordingConstants.mode.FILE) {
soundID = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundID = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundID) {
dispatch(playSound(soundID));
}
} else if (updatedSessionData.status === OFF
&& (!oldSessionData || oldSessionData.status !== OFF)) {
dispatch(showStoppedRecordingNotification(
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
let duration = 0, soundOff, soundOn;
if (oldSessionData && oldSessionData.timestamp) {
duration
= (Date.now() / 1000) - oldSessionData.timestamp;
}
sendAnalytics(createRecordingEvent('stop', mode, duration));
if (disableRecordAudioNotification) {
break;
}
if (mode === JitsiRecordingConstants.mode.FILE) {
soundOff = RECORDING_OFF_SOUND_ID;
soundOn = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundOff = LIVE_STREAMING_OFF_SOUND_ID;
soundOn = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundOff && soundOn) {
dispatch(stopSound(soundOn));
dispatch(playSound(soundOff));
}
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
} else if (typeof recordingLimit === 'object') {
// Show notification with additional information to the initiator.
dispatch(showRecordingLimitNotification(mode));
}
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {
return result;
}
let soundID;
if (mode === JitsiRecordingConstants.mode.FILE) {
soundID = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundID = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundID) {
dispatch(playSound(soundID));
}
break;
}
case OFF: {
dispatch(showStoppedRecordingNotification(
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
let duration = 0, soundOff, soundOn;
if (oldSessionData && oldSessionData.timestamp) {
duration
= (Date.now() / 1000) - oldSessionData.timestamp;
}
sendAnalytics(createRecordingEvent('stop', mode, duration));
if (disableRecordAudioNotification) {
return result;
}
if (mode === JitsiRecordingConstants.mode.FILE) {
soundOff = RECORDING_OFF_SOUND_ID;
soundOn = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundOff = LIVE_STREAMING_OFF_SOUND_ID;
soundOn = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundOff && soundOn) {
dispatch(stopSound(soundOn));
dispatch(playSound(soundOff));
}
break;
}
}
break;

View File

@@ -4,12 +4,14 @@ import {
CLEAR_RECORDING_SESSIONS,
RECORDING_SESSION_UPDATED,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_STREAM_KEY
SET_STREAM_KEY,
SET_WAITING_IN_RECORDING_NOTIFICATION_UID
} from './actionTypes';
const DEFAULT_STATE = {
pendingNotificationUids: {},
sessionDatas: []
sessionDatas: [],
waitingInQueueNotificationUids: {}
};
/**
@@ -56,6 +58,20 @@ ReducerRegistry.register(STORE_NAME,
streamKey: action.streamKey
};
case SET_WAITING_IN_RECORDING_NOTIFICATION_UID: {
const waitingInQueueNotificationUids = {
...state.waitingInQueueNotificationUids
};
waitingInQueueNotificationUids[action.streamType] = action.uid;
return {
...state,
waitingInQueueNotificationUids
};
}
default:
return state;
}
@@ -71,12 +87,12 @@ ReducerRegistry.register(STORE_NAME,
*/
function _updateSessionDatas(sessionDatas, newSessionData) {
const hasExistingSessionData = sessionDatas.find(
sessionData => sessionData.id === newSessionData.id);
sessionData => sessionData.id === newSessionData.id || sessionData.queueID === newSessionData.queueID);
let newSessionDatas;
if (hasExistingSessionData) {
newSessionDatas = sessionDatas.map(sessionData => {
if (sessionData.id === newSessionData.id) {
if (sessionData.id === newSessionData.id || sessionData.queueID === newSessionData.queueID) {
return {
...newSessionData
};