Merge pull request #5157 from jfsiebel/implement-start-stop-recording-html5

Implement start stop recording html5
This commit is contained in:
Anton Georgiev 2018-03-09 14:28:14 -05:00 committed by GitHub
commit d17ca71a60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 200 additions and 41 deletions

View File

@ -1,7 +1,9 @@
import { Meteor } from 'meteor/meteor';
import mapToAcl from '/imports/startup/mapToAcl';
import endMeeting from './methods/endMeeting';
import toggleRecording from './methods/toggleRecording';
Meteor.methods(mapToAcl(['methods.endMeeting'], {
Meteor.methods(mapToAcl(['methods.endMeeting', 'methods.toggleRecording'], {
endMeeting,
toggleRecording,
}));

View File

@ -0,0 +1,44 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import Meetings from '/imports/api/meetings';
export default function toggleRecording(credentials) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const EVENT_NAME = 'SetRecordingStatusCmdMsg';
let meetingRecorded;
let allowedToRecord;
const meetingObject = Meetings.findOne({ meetingId });
if (meetingObject != null) {
const {
allowStartStopRecording,
recording,
record,
} = meetingObject.recordProp;
meetingRecorded = recording;
allowedToRecord = record && allowStartStopRecording;
}
const payload = {
recording: !meetingRecorded,
setBy: requesterUserId,
};
if (allowedToRecord) {
Logger.info(`Setting the record parameter to ${!meetingRecorded} for ${meetingId} by ${requesterUserId}`);
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}
return null;
}

View File

@ -46,6 +46,14 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.actionsDropdown.stopDesktopShareDesc',
description: 'adds context to stop desktop share option',
},
startRecording: {
id: 'app.actionsBar.actionsDropdown.startRecording',
description: 'start recording option',
},
stopRecording: {
id: 'app.actionsBar.actionsDropdown.stopRecording',
description: 'stop recording option',
},
});
class ActionsDropdown extends Component {
@ -57,6 +65,7 @@ class ActionsDropdown extends Component {
componentWillMount() {
this.presentationItemId = _.uniqueId('action-item-');
this.videoItemId = _.uniqueId('action-item-');
this.recordId = _.uniqueId('action-item-');
}
componentWillUpdate(nextProps) {
@ -67,50 +76,69 @@ class ActionsDropdown extends Component {
}
}
handlePresentationClick() {
this.props.mountModal(<PresentationUploaderContainer />);
}
getAvailableActions() {
const {
intl,
handleShareScreen,
handleUnshareScreen,
isVideoBroadcasting,
isUserPresenter,
isUserModerator,
allowStartStopRecording,
isRecording,
record,
toggleRecording,
} = this.props;
return _.compact([
(<DropdownListItem
icon="presentation"
label={intl.formatMessage(intlMessages.presentationLabel)}
description={intl.formatMessage(intlMessages.presentationDesc)}
key={this.presentationItemId}
onClick={this.handlePresentationClick}
/>),
(Meteor.settings.public.kurento.enableScreensharing ?
(isUserPresenter ?
<DropdownListItem
icon="presentation"
label={intl.formatMessage(intlMessages.presentationLabel)}
description={intl.formatMessage(intlMessages.presentationDesc)}
key={this.presentationItemId}
onClick={this.handlePresentationClick}
/>
: null),
(Meteor.settings.public.kurento.enableScreensharing && isUserPresenter ?
<DropdownListItem
icon="desktop"
label={intl.formatMessage(isVideoBroadcasting ? intlMessages.stopDesktopShareLabel : intlMessages.desktopShareLabel)}
description={intl.formatMessage(isVideoBroadcasting ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc)}
label={intl.formatMessage(isVideoBroadcasting ?
intlMessages.stopDesktopShareLabel : intlMessages.desktopShareLabel)}
description={intl.formatMessage(isVideoBroadcasting ?
intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc)}
key={this.videoItemId}
onClick={isVideoBroadcasting ? handleUnshareScreen : handleShareScreen }
onClick={isVideoBroadcasting ? handleUnshareScreen : handleShareScreen}
/>
: null),
: null),
(record && isUserModerator && allowStartStopRecording ?
<DropdownListItem
icon="record"
label={intl.formatMessage(isRecording ?
intlMessages.stopRecording : intlMessages.startRecording)}
description={intl.formatMessage(isRecording ?
intlMessages.stopRecording : intlMessages.startRecording)}
key={this.recordId}
onClick={toggleRecording}
/>
: null),
]);
}
handlePresentationClick() {
this.props.mountModal(<PresentationUploaderContainer />);
}
render() {
const {
intl,
isUserPresenter,
handleShareScreen,
handleUnshareScreen,
isVideoBroadcasting,
isUserModerator,
} = this.props;
const availableActions = this.getAvailableActions();
if (!isUserPresenter) return null;
if ((!isUserPresenter && !isUserModerator) || availableActions.length === 0) return null;
return (
<Dropdown ref={(ref) => { this._dropdown = ref; }} >

View File

@ -18,8 +18,17 @@ class ActionsBar extends React.PureComponent {
emojiList,
emojiSelected,
handleEmojiChange,
isUserModerator,
recordSettingsList,
toggleRecording,
} = this.props;
const {
allowStartStopRecording,
recording: isRecording,
record,
} = recordSettingsList;
const actionBarClasses = {};
actionBarClasses[styles.centerWithActions] = isUserPresenter;
actionBarClasses[styles.center] = true;
@ -32,6 +41,11 @@ class ActionsBar extends React.PureComponent {
handleShareScreen,
handleUnshareScreen,
isVideoBroadcasting,
isUserModerator,
allowStartStopRecording,
isRecording,
record,
toggleRecording,
}}
/>
</div>

View File

@ -9,6 +9,7 @@ const ActionsBarContainer = props => <ActionsBar {...props} />;
export default withTracker(() => ({
isUserPresenter: Service.isUserPresenter(),
isUserModerator: Service.isUserModerator(),
emojiList: Service.getEmojiList(),
emojiSelected: Service.getEmoji(),
handleEmojiChange: Service.setEmoji,
@ -17,5 +18,6 @@ export default withTracker(() => ({
handleShareScreen: () => shareScreen(),
handleUnshareScreen: () => unshareScreen(),
isVideoBroadcasting: isVideoBroadcasting(),
recordSettingsList: Service.recordSettingsList(),
toggleRecording: Service.toggleRecording,
}))(ActionsBarContainer);

View File

@ -2,10 +2,14 @@ import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import { makeCall } from '/imports/ui/services/api';
import { EMOJI_STATUSES } from '/imports/utils/statuses';
import Meetings from '/imports/api/meetings';
export default {
isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter,
getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji,
setEmoji: status => makeCall('setEmojiStatus', Auth.userID, status),
getEmojiList: () => EMOJI_STATUSES,
isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator,
recordSettingsList: () => Meetings.findOne({ meetingId: Auth.meetingID }).recordProp,
toggleRecording: () => makeCall('toggleRecording'),
};

View File

@ -29,12 +29,24 @@ const intlMessages = defineMessages({
id: 'app.navBar.toggleUserList.newMessages',
description: 'label for toggleUserList btn when showing red notification',
},
recordingSession: {
id: 'app.navBar.recording',
description: 'label for when the session is being recorded',
},
recordingIndicatorOn: {
id: 'app.navBar.recording.on',
description: 'label for indicator when the session is being recorded',
},
recordingIndicatorOff: {
id: 'app.navBar.recording.off',
description: 'label for indicator when the session is not being recorded',
},
});
const propTypes = {
presentationTitle: PropTypes.string.isRequired,
hasUnreadMessages: PropTypes.bool.isRequired,
beingRecorded: PropTypes.bool.isRequired,
beingRecorded: PropTypes.object.isRequired,
};
const defaultProps = {
@ -165,6 +177,8 @@ class NavBar extends Component {
render() {
const { hasUnreadMessages, beingRecorded, isExpanded, intl } = this.props;
const recordingMessage = beingRecorded.recording ? 'recordingIndicatorOn' : 'recordingIndicatorOff';
const toggleBtnClasses = {};
toggleBtnClasses[styles.btn] = true;
toggleBtnClasses[styles.btnWithNotificationDot] = hasUnreadMessages;
@ -192,7 +206,13 @@ class NavBar extends Component {
</div>
<div className={styles.center}>
{this.renderPresentationTitle()}
<RecordingIndicator beingRecorded={beingRecorded} />
{beingRecorded.record ?
<span className={styles.presentationTitleSeparator}>|</span>
: null}
<RecordingIndicator
{...beingRecorded}
title={intl.formatMessage(intlMessages[recordingMessage])}
/>
</div>
<div className={styles.right}>
<SettingsDropdownContainer />

View File

@ -29,7 +29,7 @@ export default withRouter(withTracker(({ location, router }) => {
if (meetingObject != null) {
meetingTitle = meetingObject.meetingProp.name;
meetingRecorded = meetingObject.recordProp.recording;
meetingRecorded = meetingObject.recordProp;
}
const checkUnreadMessages = () => {

View File

@ -1,9 +1,17 @@
import React from 'react';
import { styles } from './styles';
const RecordingIndicator = ({ beingRecorded }) => {
if (!beingRecorded) return null;
return <div className={styles.indicator} />;
const RecordingIndicator = ({
record, title, recording,
}) => {
if (!record) return null;
return (
<div>
<div className={recording ? styles.recordIndicator : styles.notRecording} />
<span className={recording ? styles.recordingLabel : styles.notRecordingLabel}>{title}</span>
</div>
);
};
export default RecordingIndicator;

View File

@ -1,22 +1,12 @@
@import "../../../stylesheets/variables/_all";
.indicator {
%baseIndicator {
position: relative;
display: inline-block;
width: $font-size-base;
height: $font-size-base;
border-radius: 50%;
border: 1px solid $color-white;
margin-left: $line-height-computed;
&:before {
content: '';
position: absolute;
left: calc( -1px - #{($line-height-computed / 2)});
width: 1px;
height: 100%;
background-color: $color-white;
}
&:after {
content: '';
@ -31,3 +21,37 @@
background-color: $color-danger;
}
}
%baseIndicatorLabel {
font-weight: 200;
font-size: $font-size-base;
margin: 0 0 0 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
.recordIndicator {
@extend %baseIndicator;
}
.notRecording {
@extend %baseIndicator;
border: 1px solid $color-gray;
&:after {
background-color: $color-gray;
}
}
.recordingLabel {
@extend %baseIndicatorLabel;
color: $color-white;
}
.notRecordingLabel {
@extend %baseIndicatorLabel;
color: $color-gray;
}

View File

@ -39,6 +39,12 @@
}
}
.presentationTitleSeparator {
color: $color-gray;
font-size: $font-size-base;
margin: 0 1rem;
}
.btnWithNotificationDot {
position: relative;

View File

@ -96,7 +96,8 @@
"toggleVoice",
"clearPublicChatHistory",
"changeRole",
"ejectUserFromVoice"
"ejectUserFromVoice",
"toggleRecording"
]
},
"presenter": {

View File

@ -96,7 +96,8 @@
"toggleVoice",
"clearPublicChatHistory",
"changeRole",
"ejectUserFromVoice"
"ejectUserFromVoice",
"toggleRecording"
]
},
"presenter": {

View File

@ -93,6 +93,9 @@
"app.navBar.userListToggleBtnLabel": "User List Toggle",
"app.navBar.toggleUserList.ariaLabel": "Users / Conversations Toggle",
"app.navBar.toggleUserList.newMessages": "with new message notification",
"app.navBar.recording": "This session is being recorded",
"app.navBar.recording.on": "Recording",
"app.navBar.recording.off": "Not recording",
"app.leaveConfirmation.title": "Leave Session",
"app.leaveConfirmation.message": "Do you want to leave this meeting?",
"app.leaveConfirmation.confirmLabel": "Leave",
@ -185,6 +188,8 @@
"app.actionsBar.actionsDropdown.initPollDesc": "Initiate a poll",
"app.actionsBar.actionsDropdown.desktopShareDesc": "Share your screen with others",
"app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with",
"app.actionsBar.actionsDropdown.startRecording": "Start recording",
"app.actionsBar.actionsDropdown.stopRecording": "Stop recording",
"app.actionsBar.emojiMenu.statusTriggerLabel": "Status",
"app.actionsBar.emojiMenu.awayLabel": "Away",
"app.actionsBar.emojiMenu.awayDesc": "Change your status to away",