Merge remote-tracking branch 'upstream/v2.0.x-release' into z-02-accessibility-zeeshan-3

This commit is contained in:
KDSBrowne 2018-03-27 07:33:58 -07:00
commit 51c3dbb7a1
32 changed files with 897 additions and 820 deletions

View File

@ -60,6 +60,7 @@
const y = e.outerHeight() - 1;
const videos = $("#" + tagId + " > div:visible");
const isPortrait = ( $(document).width() < $(document).height() );
if (isPortrait) {

View File

@ -4,7 +4,7 @@ import { styles } from './styles.scss';
import EmojiSelect from './emoji-select/component';
import ActionsDropdown from './actions-dropdown/component';
import AudioControlsContainer from '../audio/audio-controls/container';
import JoinVideoOptionsContainer from '../video-dock/video-menu/container';
import JoinVideoOptionsContainer from '../video-provider/video-menu/container';
class ActionsBar extends React.PureComponent {
render() {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import ActionsBar from './component';
import Service from './service';
import VideoService from '../video-dock/service';
import VideoService from '../video-provider/service';
import { shareScreen, unshareScreen, isVideoBroadcasting } from '../screenshare/service';
const ActionsBarContainer = props => <ActionsBar {...props} />;

View File

@ -24,6 +24,12 @@ const propTypes = {
isConnected: PropTypes.bool.isRequired,
inputDeviceId: PropTypes.string,
outputDeviceId: PropTypes.string,
showPermissionsOvelay: PropTypes.bool.isRequired,
listenOnlyMode: PropTypes.bool.isRequired,
skipCheck: PropTypes.bool.isRequired,
joinFullAudioImmediately: PropTypes.bool.isRequired,
joinFullAudioEchoTest: PropTypes.bool.isRequired,
forceListenOnlyAttendee: PropTypes.bool.isRequired,
};
const defaultProps = {
@ -76,12 +82,12 @@ class AudioModal extends Component {
this.state = {
content: null,
hasError: false,
};
const {
intl,
closeModal,
joinListenOnly,
joinEchoTest,
exitAudio,
leaveEchoTest,
@ -94,6 +100,7 @@ class AudioModal extends Component {
this.handleGoToEchoTest = this.handleGoToEchoTest.bind(this);
this.handleJoinMicrophone = this.handleJoinMicrophone.bind(this);
this.handleJoinListenOnly = this.handleJoinListenOnly.bind(this);
this.skipAudioOptions = this.skipAudioOptions.bind(this);
this.closeModal = closeModal;
this.joinEchoTest = joinEchoTest;
this.exitAudio = exitAudio;
@ -113,10 +120,30 @@ class AudioModal extends Component {
help: {
title: intl.formatMessage(intlMessages.helpTitle),
component: () => this.renderHelp(),
}
},
};
}
componentWillMount() {
const {
joinFullAudioImmediately,
joinFullAudioEchoTest,
forceListenOnlyAttendee,
} = this.props;
if (joinFullAudioImmediately) {
this.handleJoinMicrophone();
}
if (joinFullAudioEchoTest) {
this.handleGoToEchoTest();
}
if (forceListenOnlyAttendee) {
this.handleJoinListenOnly();
}
}
componentWillUnmount() {
const {
isEchoTest,
@ -130,6 +157,7 @@ class AudioModal extends Component {
handleGoToAudioOptions() {
this.setState({
content: null,
hasError: true,
});
}
@ -147,12 +175,16 @@ class AudioModal extends Component {
outputDeviceId,
} = this.props;
this.setState({
hasError: false,
});
return this.joinEchoTest().then(() => {
console.log(inputDeviceId, outputDeviceId);
this.setState({
content: 'echoTest',
});
}).catch(err => {
}).catch((err) => {
if (err.type === 'MEDIA_ERROR') {
this.setState({
content: 'help',
@ -166,7 +198,7 @@ class AudioModal extends Component {
joinListenOnly,
} = this.props;
return joinListenOnly().catch(err => {
return joinListenOnly().catch((err) => {
if (err.type === 'MEDIA_ERROR') {
this.setState({
content: 'help',
@ -180,50 +212,78 @@ class AudioModal extends Component {
joinMicrophone,
} = this.props;
this.setState({
hasError: false,
});
joinMicrophone().catch(this.handleGoToAudioOptions);
}
skipAudioOptions() {
const {
isConnecting,
joinFullAudioImmediately,
joinFullAudioEchoTest,
forceListenOnlyAttendee,
} = this.props;
const {
content,
hasError,
} = this.state;
return (
isConnecting ||
forceListenOnlyAttendee ||
joinFullAudioImmediately ||
joinFullAudioEchoTest
) && !content && !hasError;
}
renderAudioOptions() {
const {
intl,
listenOnlyMode,
forceListenOnlyAttendee,
skipCheck,
} = this.props;
return (
<span className={styles.audioOptions}>
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.microphoneLabel)}
icon="unmute"
circle
size="jumbo"
onClick={this.handleGoToEchoTest}
accessKey="m"
/>
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
icon="listen"
circle
size="jumbo"
onClick={this.handleJoinListenOnly}
accessKey="l"
/>
{!forceListenOnlyAttendee ?
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.microphoneLabel)}
icon="unmute"
circle
size="jumbo"
onClick={skipCheck ? this.handleJoinMicrophone : this.handleGoToEchoTest}
/>
: null}
{listenOnlyMode ?
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
icon="listen"
circle
size="jumbo"
onClick={this.handleJoinListenOnly}
/>
: null}
</span>
);
}
renderContent() {
const {
isConnecting,
isEchoTest,
intl,
} = this.props;
const {
content,
} = this.state;
const { content } = this.state;
if (isConnecting) {
if (this.skipAudioOptions()) {
return (
<span className={styles.connecting}>
{ !isEchoTest ?
@ -237,15 +297,8 @@ class AudioModal extends Component {
}
renderEchoTest() {
const {
isConnecting,
} = this.props;
return (
<EchoTest
isConnecting={isConnecting}
joinEchoTest={this.joinEchoTest}
leaveEchoTest={this.leaveEchoTest}
handleNo={this.handleGoToAudioSettings}
handleYes={this.handleJoinMicrophone}
/>
@ -289,13 +342,10 @@ class AudioModal extends Component {
render() {
const {
intl,
isConnecting,
showPermissionsOvelay,
} = this.props;
const {
content,
} = this.state;
const { content } = this.state;
return (
<span>
@ -305,26 +355,27 @@ class AudioModal extends Component {
className={styles.modal}
onRequestClose={this.closeModal}
>
{ isConnecting ? null :
<header
data-test="audioModalHeader"
className={styles.header}
>
<h3 className={styles.title}>
{ content ?
this.contents[content].title :
intl.formatMessage(intlMessages.audioChoiceLabel)}
</h3>
<Button
data-test="modalBaseCloseButton"
className={styles.closeBtn}
label={intl.formatMessage(intlMessages.closeLabel)}
icon={'close'}
size={'md'}
hideLabel
onClick={this.closeModal}
/>
</header>
{ !this.skipAudioOptions() ?
<header
data-test="audioModalHeader"
className={styles.header}
>
<h3 className={styles.title}>
{ content ?
this.contents[content].title :
intl.formatMessage(intlMessages.audioChoiceLabel)}
</h3>
<Button
data-test="modalBaseCloseButton"
className={styles.closeBtn}
label={intl.formatMessage(intlMessages.closeLabel)}
icon="close"
size="md"
hideLabel
onClick={this.closeModal}
/>
</header>
: null
}
<div className={styles.content}>
{ this.renderContent() }

View File

@ -6,21 +6,33 @@ import Service from '../service';
const AudioModalContainer = props => <AudioModal {...props} />;
const APP_CONFIG = Meteor.settings.public.app;
const { listenOnlyMode, forceListenOnly, skipCheck } = APP_CONFIG;
export default withModalMounter(withTracker(({ mountModal }) =>
({
closeModal: () => {
if (!Service.isConnecting()) mountModal(null);
},
joinMicrophone: () =>
new Promise((resolve, reject) => {
Service.transferCall().then(() => {
mountModal(null);
resolve();
}).catch(() => {
joinMicrophone: () => {
const call = new Promise((resolve, reject) => {
if (skipCheck) {
resolve(Service.joinMicrophone());
} else {
resolve(Service.transferCall());
}
reject(() => {
Service.exitAudio();
reject();
});
}),
})
});
return call.then(() => {
mountModal(null);
}).catch((error) => {
throw error;
});
},
joinListenOnly: () => Service.joinListenOnly().then(() => mountModal(null)),
leaveEchoTest: () => {
if (!Service.isEchoTest()) {
@ -38,4 +50,9 @@ export default withModalMounter(withTracker(({ mountModal }) =>
inputDeviceId: Service.inputDeviceId(),
outputDeviceId: Service.outputDeviceId(),
showPermissionsOvelay: Service.isWaitingPermissions(),
listenOnlyMode,
skipCheck,
joinFullAudioImmediately: !listenOnlyMode && skipCheck,
joinFullAudioEchoTest: !listenOnlyMode && !skipCheck,
forceListenOnlyAttendee: listenOnlyMode && forceListenOnly && !Service.isUserModerator(),
}))(AudioModalContainer));

View File

@ -24,6 +24,9 @@
margin-right: 1rem;
}
}
.audioBtn:only-child {
margin-right: 0;
}
}
.audioOptions {

View File

@ -63,7 +63,7 @@ let didMountAutoJoin = false;
export default withModalMounter(injectIntl(withTracker(({ mountModal, intl }) => {
const APP_CONFIG = Meteor.settings.public.app;
const { autoJoinAudio } = APP_CONFIG;
const { autoJoin } = APP_CONFIG;
const openAudioModal = mountModal.bind(
null,
<AudioModalContainer />,
@ -94,7 +94,7 @@ export default withModalMounter(injectIntl(withTracker(({ mountModal, intl }) =>
init: () => {
Service.init(messages);
Service.changeOutputDevice(document.querySelector('#remote-media').sinkId);
if (!autoJoinAudio || didMountAutoJoin) return;
if (!autoJoin || didMountAutoJoin) return;
openAudioModal();
didMountAutoJoin = true;
},

View File

@ -8,7 +8,7 @@ const init = (messages) => {
if (AudioManager.initialized) return;
const meetingId = Auth.meetingID;
const userId = Auth.userID;
const sessionToken = Auth.sessionToken;
const { sessionToken } = Auth;
const User = Users.findOne({ userId });
const username = User.name;
const Meeting = Meetings.findOne({ meetingId: User.meetingId });
@ -53,4 +53,5 @@ export default {
outputDeviceId: () => AudioManager.outputDeviceId,
isEchoTest: () => AudioManager.isEchoTest,
error: () => AudioManager.error,
isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator,
};

View File

@ -8,7 +8,7 @@ import { notify } from '/imports/ui/services/notification';
import Media from './component';
import MediaService from './service';
import PresentationAreaContainer from '../presentation/container';
import VideoDockContainer from '../video-dock/container';
import VideoProviderContainer from '../video-provider/container';
import ScreenshareContainer from '../screenshare/container';
import DefaultContent from '../presentation/default-content/component';
@ -80,7 +80,7 @@ MediaContainer.defaultProps = defaultProps;
export default withTracker(() => {
const { dataSaving } = Settings;
const { viewParticipantsWebcams: viewVideoDock, viewScreenshare } = dataSaving;
const { viewParticipantsWebcams, viewScreenshare } = dataSaving;
const data = {};
data.currentPresentation = MediaService.getPresentationInfo();
@ -98,8 +98,8 @@ export default withTracker(() => {
data.content = <ScreenshareContainer />;
}
if (MediaService.shouldShowOverlay() && viewVideoDock && !webcamOnlyModerator) {
data.overlay = <VideoDockContainer />;
if (MediaService.shouldShowOverlay() && viewParticipantsWebcams && !webcamOnlyModerator) {
data.overlay = <VideoProviderContainer />;
}
data.isScreensharing = MediaService.isVideoBroadcasting();

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import Button from '/imports/ui/components/button/component';
import { defineMessages, injectIntl } from 'react-intl';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import Tooltip from '/imports/ui/components/tooltip/component';
import { styles } from './styles.scss';
const intlMessages = defineMessages({
@ -43,15 +44,19 @@ class PollingComponent extends Component {
style={calculatedStyles}
className={styles.pollButtonWrapper}
>
<Button
className={styles.pollingButton}
label={pollAnswer.key}
size="lg"
color="primary"
onClick={() => this.props.handleVote(poll.pollId, pollAnswer)}
aria-labelledby={`pollAnswerLabel${pollAnswer.key}`}
aria-describedby={`pollAnswerDesc${pollAnswer.key}`}
/>
<Tooltip
title={pollAnswer.key}
>
<Button
className={styles.pollingButton}
size="lg"
color="primary"
label={pollAnswer.key}
onClick={() => this.props.handleVote(poll.pollId, pollAnswer)}
aria-labelledby={`pollAnswerLabel${pollAnswer.key}`}
aria-describedby={`pollAnswerDesc${pollAnswer.key}`}
/>
</Tooltip>
<div
className={styles.hidden}
id={`pollAnswerLabel${pollAnswer.key}`}

View File

@ -13,6 +13,9 @@
.pollingButton {
width: 100%;
height: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pollingTitle {

View File

@ -1,692 +0,0 @@
import React, { Component } from 'react';
import { styles } from './styles';
import { defineMessages, injectIntl } from 'react-intl';
import VideoService from './service';
import { log } from '/imports/ui/services/api';
import { notify } from '/imports/ui/services/notification';
import { toast } from 'react-toastify';
import { styles as mediaStyles } from '/imports/ui/components/media/styles';
import Toast from '/imports/ui/components/toast/component';
import _ from 'lodash';
const intlMessages = defineMessages({
iceCandidateError: {
id: 'app.video.iceCandidateError',
description: 'Error message for ice candidate fail',
},
permissionError: {
id: 'app.video.permissionError',
description: 'Error message for webcam permission',
},
sharingError: {
id: 'app.video.sharingError',
description: 'Error on sharing webcam',
},
chromeExtensionError: {
id: 'app.video.chromeExtensionError',
description: 'Error message for Chrome Extension not installed',
},
chromeExtensionErrorLink: {
id: 'app.video.chromeExtensionErrorLink',
description: 'Error message for Chrome Extension not installed',
},
});
const RECONNECT_WAIT_TIME = 5000;
const INITIAL_SHARE_WAIT_TIME = 2000;
const CAMERA_SHARE_FAILED_WAIT_TIME = 10000;
class VideoElement extends Component {
constructor(props) {
super(props);
}
render() {
let cssClass;
if (this.props.shared || !this.props.localCamera) {
cssClass = styles.sharedWebcamVideoLocal;
} else {
cssClass = styles.sharedWebcamVideo;
}
return (
<div className={`${styles.videoContainer} ${cssClass}`} >
{ this.props.localCamera ?
<video id="shareWebcam" muted autoPlay playsInline />
:
<video id={`video-elem-${this.props.videoId}`} autoPlay playsInline />
}
<div className={styles.videoText}>
<div className={styles.userName}>{this.props.name}</div>
{/* <Button
label=""
className={styles.pauseButton}
icon={'unmute'}
size={'sm'}
circle
onClick={() => {}}
/> */}
</div>
</div>
);
}
componentDidMount() {
if (typeof this.props.onMount === 'function' && !this.props.localCamera) {
this.props.onMount(this.props.videoId, false);
}
}
}
class VideoDock extends Component {
constructor(props) {
super(props);
// Set a valid bbb-webrtc-sfu application server socket in the settings
this.ws = new ReconnectingWebSocket(Meteor.settings.public.kurento.wsUrl);
this.wsQueue = [];
this.webRtcPeers = {};
this.reconnectWebcam = false;
this.reconnectList = [];
this.cameraTimeouts = {};
this.state = {
videos: {},
sharedWebcam: false,
userNames: {},
};
this.unshareWebcam = this.unshareWebcam.bind(this);
this.shareWebcam = this.shareWebcam.bind(this);
this.onWsOpen = this.onWsOpen.bind(this);
this.onWsClose = this.onWsClose.bind(this);
this.onWsMessage = this.onWsMessage.bind(this);
}
setupReconnectVideos() {
for (id in this.webRtcPeers) {
this.disconnected(id);
this.stop(id);
}
}
reconnectVideos() {
for (i in this.reconnectList) {
const id = this.reconnectList[i];
// TODO: base this on BBB API users instead of using memory
if (id != this.myId) {
setTimeout(() => {
log('debug', ` [camera] Trying to reconnect camera ${id}`);
this.start(id, false);
}, RECONNECT_WAIT_TIME);
}
}
if (this.reconnectWebcam) {
log('debug', ` [camera] Trying to re-share ${this.myId} after reconnect.`);
this.start(this.myId, true);
}
this.reconnectWebcam = false;
this.reconnectList = [];
}
componentDidMount() {
const ws = this.ws;
const { users, userId } = this.props;
users.forEach((user) => {
if (user.has_stream && user.userId !== userId) {
// FIX: Really ugly hack, but sometimes the ICE candidates aren't
// generated properly when we send videos right after componentDidMount
setTimeout(() => {
this.start(user.userId, false);
}, INITIAL_SHARE_WAIT_TIME);
}
});
document.addEventListener('joinVideo', this.shareWebcam.bind(this)); // TODO find a better way to do this
document.addEventListener('exitVideo', this.unshareWebcam.bind(this));
document.addEventListener('installChromeExtension', this.installChromeExtension.bind(this));
window.addEventListener('resize', this.adjustVideos);
window.addEventListener('orientationchange', this.adjustVideos);
ws.addEventListener('message', this.onWsMessage);
}
componentWillMount() {
this.ws.addEventListener('open', this.onWsOpen);
this.ws.addEventListener('close', this.onWsClose);
window.addEventListener('online', this.ws.open.bind(this.ws));
window.addEventListener('offline', this.onWsClose);
}
componentWillUpdate(nextProps) {
const { isLocked } = nextProps;
if (isLocked && VideoService.isConnected()) {
this.unshareWebcam();
}
}
componentWillUnmount() {
document.removeEventListener('joinVideo', this.shareWebcam);
document.removeEventListener('exitVideo', this.unshareWebcam);
document.removeEventListener('installChromeExtension', this.installChromeExtension);
window.removeEventListener('resize', this.adjustVideos);
window.removeEventListener('orientationchange', this.adjustVideos);
this.ws.removeEventListener('message', this.onWsMessage);
this.ws.removeEventListener('open', this.onWsOpen);
this.ws.removeEventListener('close', this.onWsClose);
window.removeEventListener('online', this.ws.open.bind(this.ws));
window.removeEventListener('offline', this.onWsClose);
// Unshare user webcam
if (this.state.sharedWebcam) {
this.unshareWebcam();
this.stop(this.props.userId);
}
Object.keys(this.webRtcPeers).forEach((id) => {
this.destroyWebRTCPeer(id);
});
// Close websocket connection to prevent multiple reconnects from happening
this.ws.close();
}
adjustVideos() {
setTimeout(() => {
window.adjustVideos('webcamArea', true, mediaStyles.moreThan4Videos, mediaStyles.container, mediaStyles.overlayWrapper, 'presentationAreaData', 'screenshareVideo');
}, 0);
}
onWsOpen() {
log('debug', '------ Websocket connection opened.');
// -- Resend queued messages that happened when socket was not connected
while (this.wsQueue.length > 0) {
this.sendMessage(this.wsQueue.pop());
}
this.reconnectVideos();
}
onWsClose(error) {
log('debug', '------ Websocket connection closed.');
this.setupReconnectVideos();
}
onWsMessage(msg) {
const { intl } = this.props;
const parsedMessage = JSON.parse(msg.data);
console.log('Received message new ws message: ');
console.log(parsedMessage);
switch (parsedMessage.id) {
case 'startResponse':
this.startResponse(parsedMessage);
break;
case 'playStart':
this.handlePlayStart(parsedMessage);
break;
case 'playStop':
this.handlePlayStop(parsedMessage);
break;
case 'iceCandidate':
const webRtcPeer = this.webRtcPeers[parsedMessage.cameraId];
if (webRtcPeer) {
if (webRtcPeer.didSDPAnswered) {
webRtcPeer.addIceCandidate(parsedMessage.candidate, (err) => {
if (err) {
this.notifyError(intl.formatMessage(intlMessages.iceCandidateError));
return log('error', `Error adding candidate: ${err}`);
}
});
} else {
webRtcPeer.iceQueue.push(parsedMessage.candidate);
}
} else {
log('error', ' [ICE] Message arrived after the peer was already thrown out, discarding it...');
}
break;
case 'error':
default:
this.handleError(parsedMessage);
break;
}
}
start(id, shareWebcam) {
const { users } = this.props;
const that = this;
const { intl } = this.props;
console.log(`Starting video call for video: ${id} with ${shareWebcam}`);
const userNames = this.state.userNames;
users.forEach((user) => {
if (user.userId === id) {
userNames[id] = user.name;
}
});
this.setState({ userNames });
this.cameraTimeouts[id] = setTimeout(() => {
log('error', `Camera share has not suceeded in ${CAMERA_SHARE_FAILED_WAIT_TIME}`);
if (that.myId == id) {
that.notifyError(intl.formatMessage(intlMessages.sharingError));
that.unshareWebcam();
} else {
that.stop(id);
that.start(id, shareWebcam);
}
}, CAMERA_SHARE_FAILED_WAIT_TIME);
if (shareWebcam) {
VideoService.joiningVideo();
this.setState({ sharedWebcam: true });
this.myId = id;
this.initWebRTC(id, true);
} else {
// initWebRTC with shareWebcam false will be called after react mounts the element
this.createVideoTag(id);
}
}
initWebRTC(id, shareWebcam) {
const that = this;
const { intl } = this.props;
const onIceCandidate = function (candidate) {
const message = {
type: 'video',
role: shareWebcam ? 'share' : 'viewer',
id: 'onIceCandidate',
candidate,
cameraId: id,
};
that.sendMessage(message);
};
let videoConstraints = {};
if (navigator.userAgent.match(/Version\/[\d\.]+.*Safari/)) {
// Custom constraints for Safari
videoConstraints = {
width: {
min: 320,
max: 640,
},
height: {
min: 240,
max: 480,
},
};
} else {
videoConstraints = {
width: {
min: 320,
ideal: 640,
},
height: {
min: 240,
ideal: 480,
},
frameRate: {
min: 5,
ideal: 10,
},
};
}
const options = {
mediaConstraints: {
audio: false,
video: videoConstraints,
},
onicecandidate: onIceCandidate,
};
let peerObj;
if (shareWebcam) {
options.localVideo = document.getElementById('shareWebcam');
peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly;
} else {
peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly;
options.remoteVideo = document.getElementById(`video-elem-${id}`);
}
const webRtcPeer = new peerObj(options, function (error) {
if (error) {
log('error', ' WebRTC peerObj create error');
log('error', error);
that.notifyError(intl.formatMessage(intlMessages.permissionError));
/* This notification error is displayed considering kurento-utils
* returned the error 'The request is not allowed by the user agent
* or the platform in the current context.', but there are other
* errors that could be returned. */
that.destroyWebRTCPeer(id);
that.destroyVideoTag(id);
VideoService.resetState();
return log('error', error);
}
this.didSDPAnswered = false;
this.iceQueue = [];
that.webRtcPeers[id] = webRtcPeer;
if (shareWebcam) {
that.sharedWebcam = webRtcPeer;
}
this.generateOffer((error, offerSdp) => {
if (error) {
log('error', ' WebRtc generate offer error');
that.destroyWebRTCPeer(id);
that.destroyVideoTag(id);
return log('error', error);
}
console.log(`Invoking SDP offer callback function ${location.host}`);
const message = {
type: 'video',
role: shareWebcam ? 'share' : 'viewer',
id: 'start',
sdpOffer: offerSdp,
cameraId: id,
};
that.sendMessage(message);
});
while (this.iceQueue.length) {
const candidate = this.iceQueue.shift();
this.addIceCandidate(candidate, (err) => {
if (err) {
this.notifyError(intl.formatMessage(intlMessages.iceCandidateError));
return console.error(`Error adding candidate: ${err}`);
}
});
}
this.didSDPAnswered = true;
});
}
disconnected(id) {
if (this.sharedWebcam) {
log('debug', ' [camera] Webcam disconnected, will try re-share webcam later.');
this.reconnectWebcam = true;
} else {
this.reconnectList.push(id);
log('debug', ` [camera] ${id} disconnected, will try re-subscribe later.`);
}
}
stop(id) {
const { userId } = this.props;
this.sendMessage({
type: 'video',
role: id == userId ? 'share' : 'viewer',
id: 'stop',
cameraId: id,
});
if (id === userId) {
VideoService.exitedVideo();
}
this.destroyWebRTCPeer(id);
this.destroyVideoTag(id);
}
createVideoTag(id) {
const videos = this.state.videos;
videos[id] = true;
this.setState({ videos });
}
destroyVideoTag(id) {
const { videos, userNames } = this.state;
this.setState({
videos: _.omit(videos, id),
userNames: _.omit(userNames, id),
});
if (id == this.myId) {
this.setState({ sharedWebcam: false });
}
}
destroyWebRTCPeer(id) {
const webRtcPeer = this.webRtcPeers[id];
// Clear the shared camera fail timeout when destroying
clearTimeout(this.cameraTimeouts[id]);
this.cameraTimeouts[id] = null;
if (webRtcPeer) {
log('info', 'Stopping WebRTC peer');
if (id == this.myId && this.sharedWebcam) {
this.sharedWebcam.dispose();
this.sharedWebcam = null;
}
webRtcPeer.dispose();
delete this.webRtcPeers[id];
} else {
log('info', 'No WebRTC peer to stop (not an error)');
}
}
shareWebcam() {
const { users, userId } = this.props;
if (this.connectedToMediaServer()) {
this.start(userId, true);
} else {
log('error', 'Not connected to media server');
}
}
unshareWebcam() {
log('info', 'Unsharing webcam');
const { userId } = this.props;
VideoService.sendUserUnshareWebcam(userId);
this.stop(userId);
}
startResponse(message) {
const id = message.cameraId;
const webRtcPeer = this.webRtcPeers[id];
if (message.sdpAnswer == null) {
return log('debug', 'Null sdp answer. Camera unplugged?');
}
if (webRtcPeer == null) {
return log('debug', 'Null webrtc peer ????');
}
log('info', 'SDP answer received from server. Processing ...');
webRtcPeer.processAnswer(message.sdpAnswer, (error) => {
if (error) {
return log('error', error);
}
if (message.cameraId == this.props.userId) {
log('info', 'camera id sendusershare ', id);
VideoService.sendUserShareWebcam(id);
}
});
}
sendMessage(message) {
const ws = this.ws;
if (this.connectedToMediaServer()) {
const jsonMessage = JSON.stringify(message);
console.log(`Sending message: ${jsonMessage}`);
ws.send(jsonMessage, (error) => {
if (error) {
console.error(`client: Websocket error "${error}" on message "${jsonMessage.id}"`);
}
});
} else {
// No need to queue video stop messages
if (message.id != 'stop') {
this.wsQueue.push(message);
}
}
}
connectedToMediaServer() {
return this.ws.readyState === WebSocket.OPEN;
}
connectionStatus() {
return this.ws.readyState;
}
handlePlayStop(message) {
log('info', 'Handle play stop <--------------------');
log('error', message);
if (message.cameraId == this.props.userId) {
this.unshareWebcam();
} else {
this.stop(message.cameraId);
}
}
handlePlayStart(message) {
log('info', 'Handle play start <===================');
// Clear camera shared timeout when camera succesfully starts
clearTimeout(this.cameraTimeouts[message.cameraId]);
this.cameraTimeouts[message.cameraId] = null;
if (message.cameraId == this.props.userId) {
VideoService.joinedVideo();
}
}
handleError(message) {
const { intl, userId } = this.props;
if (message.cameraId == userId) {
this.notifyError(intl.formatMessage(intlMessages.sharingError));
this.unshareWebcam();
} else {
this.stop(message.cameraId);
}
console.error(' Handle error --------------------->');
log('debug', message.message);
}
notifyError(message) {
notify(message, 'error', 'video');
}
installChromeExtension() {
const { intl } = this.props;
const CHROME_EXTENSION_LINK = Meteor.settings.public.kurento.chromeExtensionLink;
this.notifyError(<div>
{intl.formatMessage(intlMessages.chromeExtensionError)}{' '}
<a href={CHROME_EXTENSION_LINK} target="_blank">
{intl.formatMessage(intlMessages.chromeExtensionErrorLink)}
</a>
</div>);
}
componentDidUpdate() {
this.adjustVideos();
}
render() {
return (
<div className={styles.videoDock}>
<div id="webcamArea" className={styles.webcamArea}>
{Object.keys(this.state.videos).map(id => (
<VideoElement videoId={id} key={id} name={this.state.userNames[id]} localCamera={false} onMount={this.initWebRTC.bind(this)} />
))}
<VideoElement shared={this.state.sharedWebcam} name={this.state.userNames[this.myId]} localCamera />
</div>
</div>
);
}
shouldComponentUpdate(nextProps, nextState) {
const { userId } = this.props;
const currentUsers = this.props.users || {};
const nextUsers = nextProps.users;
const users = {};
const present = {};
if (!currentUsers) { return false; }
// Map user objectos to an object in the form {userId: has_stream}
currentUsers.forEach((user) => {
users[user.userId] = user.has_stream;
});
// Keep instances where the flag has changed or next user adds it
nextUsers.forEach((user) => {
const id = user.userId;
// The case when a user exists and stream status has not changed
if (users[id] === user.has_stream) {
delete users[id];
} else {
// Case when a user has been added to the list
users[id] = user.has_stream;
}
// Mark the ids which are present in nextUsers
present[id] = true;
});
const userIds = Object.keys(users);
for (let i = 0; i < userIds.length; i++) {
const id = userIds[i];
// If a userId is not present in nextUsers let's stop it
if (!present[id]) {
this.stop(id);
continue;
}
console.log(`User ${users[id] ? '' : 'un'}shared webcam ${id}`);
// If a user stream is true, changed and was shared by other
// user we'll start it. If it is false and changed we stop it
if (users[id]) {
if (userId !== id) {
this.start(id, false);
}
} else {
this.stop(id);
}
}
return true;
}
}
export default injectIntl(VideoDock);

View File

@ -0,0 +1,446 @@
import React, { Component } from 'react';
import { styles } from './styles';
import { defineMessages, injectIntl } from 'react-intl';
import { log } from '/imports/ui/services/api';
import { notify } from '/imports/ui/services/notification';
import { toast } from 'react-toastify';
import Toast from '/imports/ui/components/toast/component';
import _ from 'lodash';
import VideoService from './service';
import VideoDockContainer from './video-dock/container';
const intlMessages = defineMessages({
iceCandidateError: {
id: 'app.video.iceCandidateError',
description: 'Error message for ice candidate fail',
},
permissionError: {
id: 'app.video.permissionError',
description: 'Error message for webcam permission',
},
sharingError: {
id: 'app.video.sharingError',
description: 'Error on sharing webcam',
},
chromeExtensionError: {
id: 'app.video.chromeExtensionError',
description: 'Error message for Chrome Extension not installed',
},
chromeExtensionErrorLink: {
id: 'app.video.chromeExtensionErrorLink',
description: 'Error message for Chrome Extension not installed',
},
});
const RECONNECT_WAIT_TIME = 5000;
const CAMERA_SHARE_FAILED_WAIT_TIME = 10000;
class VideoProvider extends Component {
constructor(props) {
super(props);
this.state = {
sharedWebcam: false,
socketOpen: false,
};
// Set a valid bbb-webrtc-sfu application server socket in the settings
this.ws = new ReconnectingWebSocket(Meteor.settings.public.kurento.wsUrl);
this.wsQueue = [];
this.reconnectWebcam = false;
this.cameraTimeouts = {};
this.webRtcPeers = {};
this.onWsOpen = this.onWsOpen.bind(this);
this.onWsClose = this.onWsClose.bind(this);
this.onWsMessage = this.onWsMessage.bind(this);
this.unshareWebcam = this.unshareWebcam.bind(this);
this.shareWebcam = this.shareWebcam.bind(this);
}
componentWillMount() {
this.ws.addEventListener('open', this.onWsOpen);
this.ws.addEventListener('close', this.onWsClose);
window.addEventListener('online', this.ws.open.bind(this.ws));
window.addEventListener('offline', this.onWsClose);
}
componentDidMount() {
document.addEventListener('joinVideo', this.shareWebcam.bind(this)); // TODO find a better way to do this
document.addEventListener('exitVideo', this.unshareWebcam.bind(this));
this.ws.addEventListener('message', this.onWsMessage);
}
componentWillUnmount() {
document.removeEventListener('joinVideo', this.shareWebcam);
document.removeEventListener('exitVideo', this.unshareWebcam);
this.ws.removeEventListener('message', this.onWsMessage);
this.ws.removeEventListener('open', this.onWsOpen);
this.ws.removeEventListener('close', this.onWsClose);
window.removeEventListener('online', this.ws.open.bind(this.ws));
window.removeEventListener('offline', this.onWsClose);
// Unshare user webcam
if (this.state.sharedWebcam) {
this.unshareWebcam();
this.stop(this.props.userId);
}
Object.keys(this.webRtcPeers).forEach((id) => {
this.destroyWebRTCPeer(id);
});
// Close websocket connection to prevent multiple reconnects from happening
this.ws.close();
}
onWsOpen() {
log('debug', '------ Websocket connection opened.');
// -- Resend queued messages that happened when socket was not connected
while (this.wsQueue.length > 0) {
this.sendMessage(this.wsQueue.pop());
}
this.setState({ socketOpen: true });
}
onWsClose(error) {
log('debug', '------ Websocket connection closed.');
this.unshareWebcam();
VideoService.exitedVideo();
this.setState({ socketOpen: false });
}
disconnected(id) {
this.reconnectList.push(id);
log('debug', ` [camera] ${id} disconnected, will try re-subscribe later.`);
}
onWsMessage(msg) {
const { intl } = this.props;
const parsedMessage = JSON.parse(msg.data);
console.log('Received message new ws message: ');
console.log(parsedMessage);
switch (parsedMessage.id) {
case 'startResponse':
this.startResponse(parsedMessage);
break;
case 'playStart':
this.handlePlayStart(parsedMessage);
break;
case 'playStop':
this.handlePlayStop(parsedMessage);
break;
case 'iceCandidate':
this.handleIceCandidate(parsedMessage);
break;
case 'error':
default:
this.handleError(parsedMessage);
break;
}
}
sendMessage(message) {
const ws = this.ws;
if (this.connectedToMediaServer()) {
const jsonMessage = JSON.stringify(message);
console.log(`Sending message: ${jsonMessage}`);
ws.send(jsonMessage, (error) => {
if (error) {
console.error(`client: Websocket error "${error}" on message "${jsonMessage.id}"`);
}
});
} else {
// No need to queue video stop messages
if (message.id != 'stop') {
this.wsQueue.push(message);
}
}
}
connectedToMediaServer() {
return this.ws.readyState === WebSocket.OPEN;
}
startResponse(message) {
const id = message.cameraId;
const webRtcPeer = this.webRtcPeers[id];
if (message.sdpAnswer == null || webRtcPeer == null) {
return log('debug', 'Null sdp answer or null webrtcpeer');
}
log('info', 'SDP answer received from server. Processing ...');
webRtcPeer.processAnswer(message.sdpAnswer, (error) => {
if (error) {
return log('error', error);
}
if (message.cameraId == this.props.userId) {
log('info', 'camera id sendusershare ', id);
VideoService.sendUserShareWebcam(id);
}
});
}
handleIceCandidate(message) {
const { intl } = this.props;
const webRtcPeer = this.webRtcPeers[message.cameraId];
if (webRtcPeer) {
if (webRtcPeer.didSDPAnswered) {
webRtcPeer.addIceCandidate(message.candidate, (err) => {
if (err) {
this.notifyError(intl.formatMessage(intlMessages.iceCandidateError));
return log('error', `Error adding candidate: ${err}`);
}
});
} else {
webRtcPeer.iceQueue.push(message.candidate);
}
} else {
log('error', ' [ICE] Message arrived after the peer was already thrown out, discarding it...');
}
}
destroyWebRTCPeer(id) {
const webRtcPeer = this.webRtcPeers[id];
// Clear the shared camera fail timeout when destroying
clearTimeout(this.cameraTimeouts[id]);
this.cameraTimeouts[id] = null;
if (webRtcPeer) {
log('info', 'Stopping WebRTC peer');
if (id == this.props.userId && this.state.sharedWebcam) {
this.setState({ sharedWebcam: false });
}
webRtcPeer.dispose();
delete this.webRtcPeers[id];
} else {
log('info', 'No WebRTC peer to stop (not an error)');
}
}
initWebRTC(id, shareWebcam, videoOptions, tag) {
const that = this;
const { intl, meetingId } = this.props;
const options = {
mediaConstraints: {
audio: false,
video: videoOptions,
},
onicecandidate: this.getOnIceCandidateCallback(id, shareWebcam),
};
let peerObj;
if (shareWebcam) {
peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly;
options.localVideo = tag;
} else {
peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly;
options.remoteVideo = tag;
}
this.cameraTimeouts[id] = setTimeout(() => {
log('error', `Camera share has not suceeded in ${CAMERA_SHARE_FAILED_WAIT_TIME}`);
if (this.props.userId == id) {
this.notifyError(intl.formatMessage(intlMessages.sharingError));
this.unshareWebcam();
} else {
this.stop(id);
this.initWebRTC(id, shareWebcam, videoOptions, tag);
}
}, CAMERA_SHARE_FAILED_WAIT_TIME);
const webRtcPeer = new peerObj(options, function (error) {
if (error) {
log('error', ' WebRTC peerObj create error');
log('error', error);
that.notifyError(intl.formatMessage(intlMessages.permissionError));
/* This notification error is displayed considering kurento-utils
* returned the error 'The request is not allowed by the user agent
* or the platform in the current context.', but there are other
* errors that could be returned. */
that.destroyWebRTCPeer(id);
if (shareWebcam) {
VideoService.exitVideo();
VideoService.exitedVideo();
that.unshareWebcam();
}
return log('error', error);
}
this.didSDPAnswered = false;
this.iceQueue = [];
that.webRtcPeers[id] = webRtcPeer;
if (shareWebcam) {
that.sharedWebcam = webRtcPeer;
}
this.generateOffer((error, offerSdp) => {
if (error) {
log('error', ' WebRtc generate offer error');
that.destroyWebRTCPeer(id);
return log('error', error);
}
console.log(`Invoking SDP offer callback function ${location.host}`);
const message = {
type: 'video',
role: shareWebcam ? 'share' : 'viewer',
id: 'start',
sdpOffer: offerSdp,
cameraId: id,
meetingId,
};
that.sendMessage(message);
});
while (this.iceQueue.length) {
const candidate = this.iceQueue.shift();
this.addIceCandidate(candidate, (err) => {
if (err) {
this.notifyError(intl.formatMessage(intlMessages.iceCandidateError));
return console.error(`Error adding candidate: ${err}`);
}
});
}
this.didSDPAnswered = true;
});
}
getOnIceCandidateCallback(id, shareWebcam) {
const that = this;
return function (candidate) {
const message = {
type: 'video',
role: shareWebcam ? 'share' : 'viewer',
id: 'onIceCandidate',
candidate,
cameraId: id,
};
that.sendMessage(message);
};
}
stop(id) {
const userId = this.props.userId;
if (id === userId) {
this.sendMessage({
type: 'video',
role: id == userId ? 'share' : 'viewer',
id: 'stop',
cameraId: id,
});
this.unshareWebcam();
VideoService.exitedVideo();
}
this.destroyWebRTCPeer(id);
}
handlePlayStop(message) {
const id = message.cameraId;
log('info', 'Handle play stop <--------------------');
log('error', message);
this.stop(id);
}
handlePlayStart(message) {
log('info', 'Handle play start <===================');
// Clear camera shared timeout when camera succesfully starts
clearTimeout(this.cameraTimeouts[message.cameraId]);
this.cameraTimeouts[message.cameraId] = null;
if (message.cameraId == this.props.userId) {
VideoService.joinedVideo();
}
}
handleError(message) {
const { intl } = this.props;
const userId = this.props.userId;
if (message.cameraId == userId) {
this.unshareWebcam();
this.notifyError(intl.formatMessage(intlMessages.sharingError));
} else {
this.stop(message.cameraId);
}
console.error(' Handle error --------------------->');
log('debug', message.message);
}
notifyError(message) {
notify(message, 'error', 'video');
}
shareWebcam() {
let { intl } = this.props;
log('info', 'Sharing webcam');
if (this.connectedToMediaServer()) {
this.setState({ sharedWebcam: true });
VideoService.joiningVideo();
} else {
this.notifyError(intl.formatMessage(intlMessages.sharingError));
}
}
unshareWebcam() {
log('info', 'Unsharing webcam');
this.setState({ ...this.state, sharedWebcam: false });
VideoService.sendUserUnshareWebcam(this.props.userId);
}
render() {
return (
<VideoDockContainer
onStart={this.initWebRTC.bind(this)}
onStop={this.stop.bind(this)}
sharedWebcam={this.state.sharedWebcam}
onShareWebcam={this.shareWebcam.bind(this)}
socketOpen={this.state.socketOpen}
isLocked={this.props.isLocked}
/>
);
}
}
export default injectIntl(VideoProvider);

View File

@ -0,0 +1,12 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import VideoProvider from './component';
import VideoService from './service';
const VideoProviderContainer = ({ children, ...props }) => <VideoProvider {...props}>{children}</VideoProvider>;
export default withTracker(() => ({
isLocked: VideoService.isLocked(),
meetingId: VideoService.meetingId(),
userId: VideoService.userId(),
}))(VideoProviderContainer);

View File

@ -1,6 +1,7 @@
import { Tracker } from 'meteor/tracker';
import { makeCall } from '/imports/ui/services/api';
import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings/';
import Auth from '/imports/ui/services/auth';
class VideoService {
@ -33,7 +34,7 @@ class VideoService {
}
joinVideo() {
var joinVideoEvent = new Event('joinVideo');
const joinVideoEvent = new Event('joinVideo');
document.dispatchEvent(joinVideoEvent);
}
@ -47,7 +48,7 @@ class VideoService {
}
exitVideo() {
var exitVideoEvent = new Event('exitVideo');
const exitVideoEvent = new Event('exitVideo');
document.dispatchEvent(exitVideoEvent);
}
@ -56,11 +57,6 @@ class VideoService {
this.isConnected = false;
}
resetState() {
this.isWaitingResponse = false;
this.isConnected = false;
}
sendUserShareWebcam(stream) {
makeCall('userShareWebcam', stream);
}
@ -74,10 +70,24 @@ class VideoService {
return Users.find().fetch();
}
webcamOnlyModerator() {
const m = Meetings.findOne({meetingId: Auth.meetingID});
return m.usersProp.webcamsOnlyForModerator;
}
isLocked() {
const m = Meetings.findOne({meetingId: Auth.meetingID});
return m.lockSettingsProp ? m.lockSettingsProp.disableCam : false;
}
userId() {
return Auth.userID;
}
meetingId() {
return Auth.meetingID;
}
isConnected() {
return this.isConnected;
}
@ -94,13 +104,15 @@ export default {
exitingVideo: () => videoService.exitingVideo(),
exitedVideo: () => videoService.exitedVideo(),
getAllUsers: () => videoService.getAllUsers(),
webcamOnlyModerator: () => videoService.webcamOnlyModerator(),
isLocked: () => videoService.isLocked(),
isConnected: () => videoService.isConnected,
isWaitingResponse: () => videoService.isWaitingResponse,
joinVideo: () => videoService.joinVideo(),
joiningVideo: () => videoService.joiningVideo(),
joinedVideo: () => videoService.joinedVideo(),
resetState: () => videoService.resetState(),
sendUserShareWebcam: (stream) => videoService.sendUserShareWebcam(stream),
sendUserUnshareWebcam: (stream) => videoService.sendUserUnshareWebcam(stream),
sendUserShareWebcam: stream => videoService.sendUserShareWebcam(stream),
sendUserUnshareWebcam: stream => videoService.sendUserUnshareWebcam(stream),
userId: () => videoService.userId(),
meetingId: () => videoService.meetingId(),
};

View File

@ -0,0 +1,105 @@
import React, { Component } from 'react';
import { styles } from '../styles';
import { defineMessages, injectIntl } from 'react-intl';
import { log } from '/imports/ui/services/api';
import { notify } from '/imports/ui/services/notification';
import { toast } from 'react-toastify';
import { styles as mediaStyles } from '/imports/ui/components/media/styles';
import Toast from '/imports/ui/components/toast/component';
import _ from 'lodash';
import VideoElement from '../video-element/component';
const intlMessages = defineMessages({
chromeExtensionError: {
id: 'app.video.chromeExtensionError',
description: 'Error message for Chrome Extension not installed',
},
chromeExtensionErrorLink: {
id: 'app.video.chromeExtensionErrorLink',
description: 'Error message for Chrome Extension not installed',
},
});
class VideoDock extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const { users, userId } = this.props;
document.addEventListener('installChromeExtension', this.installChromeExtension.bind(this));
window.addEventListener('resize', this.adjustVideos);
window.addEventListener('orientationchange', this.adjustVideos);
}
componentWillUnmount() {
window.removeEventListener('resize', this.adjustVideos);
window.removeEventListener('orientationchange', this.adjustVideos);
document.removeEventListener('installChromeExtension', this.installChromeExtension.bind(this));
}
componentDidUpdate() {
this.adjustVideos();
}
notifyError(message) {
notify(message, 'error', 'video');
}
installChromeExtension() {
console.log(intlMessages);
const { intl } = this.props;
const CHROME_EXTENSION_LINK = Meteor.settings.public.kurento.chromeExtensionLink;
this.notifyError(<div>
{intl.formatMessage(intlMessages.chromeExtensionError)}{' '}
<a href={CHROME_EXTENSION_LINK} target="_blank">
{intl.formatMessage(intlMessages.chromeExtensionErrorLink)}
</a>
</div>);
}
// TODO
// Find a better place to put this piece of code
adjustVideos() {
setTimeout(() => {
window.adjustVideos('webcamArea', true, mediaStyles.moreThan4Videos, mediaStyles.container, mediaStyles.overlayWrapper, 'presentationAreaData', 'screenshareVideo');
}, 0);
}
render() {
if (!this.props.socketOpen) {
// TODO: return something when disconnected
return null;
}
const id = this.props.userId;
const sharedWebcam = this.props.sharedWebcam;
return (
<div className={styles.videoDock} id={this.props.sharedWebcam.toString()}>
<div id="webcamArea" className={styles.webcamArea}>
{this.props.users.map(user => (
<VideoElement
shared={id === user.userId && sharedWebcam}
videoId={user.userId}
key={user.userId}
name={user.name}
localCamera={id === user.userId}
onShareWebcam={this.props.onShareWebcam.bind(this)}
onMount={this.props.onStart.bind(this)}
onUnmount={this.props.onStop.bind(this)}
/>
))}
</div>
</div>
);
}
}
export default injectIntl(VideoDock);

View File

@ -1,23 +1,34 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth';
import mapUser from '../../services/user/mapUser';
import mapUser from '../../../services/user/mapUser';
import Meetings from '/imports/api/meetings/';
import Users from '/imports/api/users/';
import VideoDock from './component';
import VideoService from './service';
import VideoService from '../service';
const VideoDockContainer = ({ children, ...props }) => <VideoDock {...props}>{children}</VideoDock>;
export default withTracker(() => {
export default withTracker(({sharedWebcam}) => {
const meeting = Meetings.findOne({ meetingId: Auth.meetingID });
const lockCam = meeting.lockSettingsProp ? meeting.lockSettingsProp.disableCam : false;
const user = Users.findOne({ userId: Auth.userID });
const userId = Auth.userID;
const user = Users.findOne({ userId });
const userLocked = mapUser(user).isLocked;
const withActiveStreams = (users) => {
const activeFilter = (user) => {
const isLocked = lockCam && user.locked;
return !isLocked && (user.has_stream || (sharedWebcam && user.userId == userId));
};
return users.filter(activeFilter);
}
const users = withActiveStreams(VideoService.getAllUsers());
return {
users: VideoService.getAllUsers(),
userId: VideoService.userId(),
isLocked: userLocked && lockCam,
users,
userId
};
})(VideoDockContainer);

View File

@ -0,0 +1,68 @@
import React, { Component } from 'react';
import cx from 'classnames';
import { styles } from '../styles';
class VideoElement extends Component {
constructor(props) {
super(props);
}
render() {
const tagId = this.props.localCamera ? 'shareWebcam' : `video-elem-${this.props.videoId}`;
return (
<div className={cx({
[styles.videoContainer]: true,
[styles.sharedWebcamVideo]: !this.props.shared && this.props.localCamera,
[styles.sharedWebcamVideoLocal]: this.props.shared || !this.props.localCamera })}>
<video id={tagId} muted={this.props.localCamera} autoPlay playsInline />
<div className={styles.videoText}>
<div className={styles.userName}>{this.props.name}</div>
</div>
</div>
);
}
componentDidMount() {
const { videoId, localCamera } = this.props;
const tagId = localCamera ? 'shareWebcam' : `video-elem-${videoId}`;
const tag = document.getElementById(tagId);
if (localCamera && this.props.onShareWebcam === 'function') {
this.props.onShareWebcam();
}
if (typeof this.props.onMount === 'function') {
this.props.onMount(videoId, localCamera, this.getVideoConstraints(), tag);
}
}
componentWillUnmount() {
if (typeof this.props.onUnmount === 'function') {
this.props.onUnmount(this.props.videoId);
}
}
getVideoConstraints() {
let videoConstraints = {
width: {
min: 320,
max: 640,
},
height: {
min: 240,
max: 480,
},
};
if (!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/)) {
videoConstraints.frameRate = { min: 5, ideal: 10, };
}
return videoConstraints;
}
}
export default VideoElement;

View File

@ -1,7 +1,6 @@
import Settings from '/imports/ui/services/settings';
import mapUser from '/imports/ui/services/user/mapUser';
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/api/meetings/';
import Users from '/imports/api/users/';
import VideoService from '../service';
@ -19,22 +18,20 @@ const isDisabled = () => {
const videoSettings = Settings.dataSaving;
const enableShare = !videoSettings.viewParticipantsWebcams;
const meeting = Meetings.findOne({ meetingId: Auth.meetingID });
const LockCam = meeting.lockSettingsProp ? meeting.lockSettingsProp.disableCam : false;
const webcamOnlyModerator = meeting.usersProp.webcamsOnlyForModerator;
const LockCam = VideoService.isLocked()
const webcamOnlyModerator = VideoService.webcamOnlyModerator();
const user = Users.findOne({ userId: Auth.userID });
const userLocked = mapUser(user).isLocked;
const isConecting = (!isSharingVideo && isConnected);
const isConnecting = (!isSharingVideo && isConnected);
const isLocked = (LockCam && userLocked) || webcamOnlyModerator;
return isLocked
|| isWaitingResponse
|| isConecting
|| isConnecting
|| enableShare;
};
export default {
isSharingVideo,
isDisabled,

View File

@ -5,8 +5,9 @@
"mobileFont": 16,
"desktopFont": 14,
"audioChatNotification": false,
"autoJoinAudio": true,
"listenOnly": false,
"autoJoin": true,
"listenOnlyMode": true,
"forceListenOnly": false,
"skipCheck": false,
"appName": "BigBlueButton HTML5 Client",
"bbbServerVersion": "2.0-rc",

View File

@ -5,8 +5,9 @@
"mobileFont": 16,
"desktopFont": 14,
"audioChatNotification": false,
"autoJoinAudio": true,
"listenOnly": false,
"autoJoin": true,
"listenOnlyMode": true,
"forceListenOnly": false,
"skipCheck": false,
"appName": "BigBlueButton HTML5 Client",
"bbbServerVersion": "2.0-rc",

View File

@ -14,6 +14,7 @@ from-video: "from-video-sfu"
to-video: "to-video-sfu"
from-audio: "from-audio-sfu"
to-audio: "to-audio-sfu"
to-akka: "to-akka-apps-redis-channel"
log:
filename: '/var/log/bigbluebutton/bbb-webrtc-sfu/bbb-webrtc-sfu.log'

View File

@ -25,6 +25,7 @@ const config = require('config');
TO_VIDEO: config.get('to-video'),
FROM_AUDIO: config.get('from-audio'),
TO_AUDIO: config.get('to-audio'),
TO_AKKA_APPS: config.get('to-akka'),
// RedisWrapper events
REDIS_MESSAGE : "redis_message",
@ -47,6 +48,8 @@ const config = require('config');
STOP_TRANSCODER_REQ_2x: "StopTranscoderSysReqMsg",
STOP_TRANSCODER_RESP_2x: "StopTranscoderSysRespMsg",
USER_CAM_BROADCAST_STOPPED_2x: "UserBroadcastCamStopMsg",
// Redis messages fields
// Transcoder 1x
USER_ID : "user_id",

View File

@ -20,11 +20,12 @@ let ScreenshareRTMPBroadcastStartedEventMessage2x =
require('./screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js')(Constants);
let ScreenshareRTMPBroadcastStoppedEventMessage2x =
require('./screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js')(Constants);
let UserCamBroadcastStoppedEventMessage2x =
require('./video/UserCamBroadcastStoppedEventMessage2x.js')(Constants);
/**
* @classdesc
* Messaging utils to assemble JSON/Redis BigBlueButton messages
* Messaging utils to assemble JSON/Redis BigBlueButton messages
* @constructor
*/
function Messaging() {}
@ -65,4 +66,10 @@ Messaging.prototype.generateScreenshareRTMPBroadcastStoppedEvent2x =
return stodrbem.toJson();
}
Messaging.prototype.generateUserCamBroadcastStoppedEventMessage2x =
function(meetingId, userId, streamUrl) {
let stodrbem = new UserCamBroadcastStoppedEventMessage2x(meetingId, userId, streamUrl);
return stodrbem.toJson();
}
module.exports = new Messaging();

View File

@ -0,0 +1,18 @@
/*
*
*/
var inherits = require('inherits');
var OutMessage2x = require('../OutMessage2x');
module.exports = function (C) {
function UserCamBroadcastStoppedEventMessage2x (meetingId, userId, stream) {
UserCamBroadcastStoppedEventMessage2x.super_.call(this, C.USER_CAM_BROADCAST_STOPPED_2x, {sender: 'bbb-webrtc-sfu'}, {meetingId, userId});
this.core.body = {};
this.core.body[C.STREAM_URL] = stream;
};
inherits(UserCamBroadcastStoppedEventMessage2x, OutMessage2x);
return UserCamBroadcastStoppedEventMessage2x;
}

View File

@ -113,7 +113,7 @@ module.exports = class MediaServer extends EventEmitter {
if (source && sink) {
return new Promise((resolve, reject) => {
switch (type) {
case 'ALL':
case 'ALL':
source.connect(sink, (error) => {
if (error) {
error = this._handleError(error);
@ -156,6 +156,7 @@ module.exports = class MediaServer extends EventEmitter {
Logger.info("[mcs-media] Releasing endpoint", elementId, "from room", room);
let mediaElement = this._mediaElements[elementId];
let pipeline = this._mediaPipelines[room];
if (mediaElement && typeof mediaElement.release === 'function') {
pipeline.activeElements--;

View File

@ -64,7 +64,7 @@ let _onMessage = async function (_message) {
Logger.info('[VideoManager] Received message [' + message.id + '] from connection ' + sessionId);
Logger.debug('[VideoManager] Message =>', JSON.stringify(message, null, 2));
video = new Video(bbbGW, message.cameraId, shared, message.connectionId);
video = new Video(bbbGW, message.meetingId, message.cameraId, shared, message.connectionId);
// Empty ice queue after starting video
if (iceQueue) {
@ -164,7 +164,9 @@ let stopVideo = async function(sessionId, role, cameraId) {
if (sharedVideo) {
Logger.info('[VideoManager] Stopping sharer [', sessionId, '][', cameraId,']');
await sharedVideo.stop();
delete sessions[sessionId][cameraId+'-shared'];
if (sessions[sessionId][cameraId+'-shared']) {
delete sessions[sessionId][cameraId+'-shared'];
}
}
}
else if (role === 'viewer') {
@ -175,6 +177,9 @@ let stopVideo = async function(sessionId, role, cameraId) {
delete sessions[sessionId][cameraId];
}
}
if (sessions[sessionId]) {
delete sessions[sessionId];
}
}
catch (err) {
Logger.error("[VideoManager] Stop error => ", err);

View File

@ -5,17 +5,18 @@ const kurentoUrl = config.get('kurentoUrl');
const MCSApi = require('../mcs-core/lib/media/MCSApiStub');
const C = require('../bbb/messages/Constants');
const Logger = require('../utils/Logger');
const Messaging = require('../bbb/messages/Messaging');
const h264_sdp = require('../h264-sdp');
var sharedWebcams = {};
module.exports = class Video {
constructor(_bbbGW, _id, _shared, _sessionId) {
constructor(_bbbGW, _meetingId, _id, _shared, _sessionId) {
this.mcs = new MCSApi();
this.bbbGW = _bbbGW;
this.id = _id;
this.sessionId = _sessionId;
this.meetingId = _id;
this.meetingId = _meetingId;
this.shared = _shared;
this.role = this.shared? 'share' : 'view'
this.mediaId = null;
@ -83,13 +84,12 @@ module.exports = class Video {
Logger.warn("Setting up a timeout for " + this.sessionId + " camera " + this.id);
if (!this.notFlowingTimeout) {
this.notFlowingTimeout = setTimeout(() => {
this.bbbGW.publish(JSON.stringify({
connectionId: this.sessionId,
type: 'video',
role: this.role,
id : 'playStop',
cameraId: this.id,
}), C.FROM_VIDEO);
if (this.shared) {
let userCamEvent =
Messaging.generateUserCamBroadcastStoppedEventMessage2x(this.meetingId, this.id, this.id);
this.bbbGW.publish(userCamEvent, C.TO_AKKA_APPS, function(error) {});
}
}, config.get('mediaFlowTimeoutDuration'));
}
}