2017-06-04 10:40:14 +08:00
|
|
|
import React, { Component } from 'react';
|
2017-07-25 03:29:34 +08:00
|
|
|
import ScreenshareContainer from '/imports/ui/components/screenshare/container';
|
2016-05-04 04:40:46 +08:00
|
|
|
import styles from './styles.scss';
|
|
|
|
|
2017-09-01 23:26:57 +08:00
|
|
|
function adjustVideos(centerVideos) {
|
|
|
|
const _minContentAspectRatio = 4 / 3.0;
|
|
|
|
|
|
|
|
function calculateOccupiedArea(canvasWidth, canvasHeight, numColumns, numRows, numChildren) {
|
|
|
|
const obj = calculateCellDimensions(canvasWidth, canvasHeight, numColumns, numRows);
|
|
|
|
obj.occupiedArea = obj.width * obj.height * numChildren;
|
|
|
|
obj.numColumns = numColumns;
|
|
|
|
obj.numRows = numRows;
|
|
|
|
obj.cellAspectRatio = _minContentAspectRatio;
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
function calculateCellDimensions(canvasWidth, canvasHeight, numColumns, numRows) {
|
|
|
|
const obj = {
|
|
|
|
width: Math.floor(canvasWidth / numColumns),
|
|
|
|
height: Math.floor(canvasHeight / numRows),
|
|
|
|
};
|
|
|
|
|
|
|
|
if (obj.width / obj.height > _minContentAspectRatio) {
|
|
|
|
obj.width = Math.min(Math.floor(obj.height * _minContentAspectRatio), Math.floor(canvasWidth / numColumns));
|
|
|
|
} else {
|
|
|
|
obj.height = Math.min(Math.floor(obj.width / _minContentAspectRatio), Math.floor(canvasHeight / numRows));
|
|
|
|
}
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
function findBestConfiguration(canvasWidth, canvasHeight, numChildrenInCanvas) {
|
|
|
|
let bestConfiguration = {
|
|
|
|
occupiedArea: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
for (let cols = 1; cols <= numChildrenInCanvas; cols++) {
|
|
|
|
let rows = Math.floor(numChildrenInCanvas / cols);
|
|
|
|
|
|
|
|
// That's a small HACK, different from the original algorithm
|
|
|
|
// Sometimes numChildren will be bigger than cols*rows, this means that this configuration
|
|
|
|
// can't show all the videos and shouldn't be considered. So we just increment the number of rows
|
|
|
|
// and get a configuration which shows all the videos albeit with a few missing slots in the end.
|
|
|
|
// For example: with numChildren == 8 the loop will generate cols == 3 and rows == 2
|
|
|
|
// cols * rows is 6 so we bump rows to 3 and then cols*rows is 9 which is bigger than 8
|
|
|
|
if (numChildrenInCanvas > cols * rows) {
|
|
|
|
rows += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
const currentConfiguration = calculateOccupiedArea(canvasWidth, canvasHeight, cols, rows, numChildrenInCanvas);
|
|
|
|
|
|
|
|
if (currentConfiguration.occupiedArea > bestConfiguration.occupiedArea) {
|
|
|
|
bestConfiguration = currentConfiguration;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return bestConfiguration;
|
|
|
|
}
|
|
|
|
|
|
|
|
// http://stackoverflow.com/a/3437825/414642
|
|
|
|
const e = $('#webcamArea').parent();
|
|
|
|
const x = e.outerWidth();
|
|
|
|
const y = e.outerHeight();
|
|
|
|
|
|
|
|
const videos = $('video:visible');
|
|
|
|
|
|
|
|
const best = findBestConfiguration(x, y, videos.length);
|
|
|
|
|
|
|
|
videos.each(function (i) {
|
|
|
|
const row = Math.floor(i / best.numColumns);
|
|
|
|
const col = Math.floor(i % best.numColumns);
|
|
|
|
|
|
|
|
// Free width space remaining to the right and below of the videos
|
|
|
|
const remX = (x - best.width * best.numColumns);
|
|
|
|
const remY = (y - best.height * best.numRows);
|
|
|
|
|
|
|
|
// Center videos
|
|
|
|
const top = ((best.height) * row) + remY / 2;
|
|
|
|
const left = ((best.width) * col) + remX / 2;
|
|
|
|
|
|
|
|
const videoTop = `top: ${top}px;`;
|
|
|
|
const videoLeft = `left: ${left}px;`;
|
|
|
|
|
|
|
|
$(this).attr('style', videoTop + videoLeft);
|
|
|
|
});
|
|
|
|
|
|
|
|
videos.attr('width', best.width);
|
|
|
|
videos.attr('height', best.height);
|
|
|
|
}
|
|
|
|
|
2016-05-04 04:40:46 +08:00
|
|
|
export default class VideoDock extends Component {
|
2017-09-01 23:26:57 +08:00
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
// Set a valid kurento application server socket in the settings
|
|
|
|
ws: new ReconnectingWebSocket(Meteor.settings.public.kurento.wsUrl),
|
|
|
|
webRtcPeers: {},
|
|
|
|
};
|
|
|
|
|
|
|
|
this.sendUserShareWebcam = props.sendUserShareWebcam.bind(this);
|
|
|
|
this.sendUserUnshareWebcam = props.sendUserUnshareWebcam.bind(this);
|
|
|
|
|
|
|
|
this.unshareWebcam = this.unshareWebcam.bind(this);
|
|
|
|
this.shareWebcam = this.shareWebcam.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
const ws = this.state.ws;
|
2017-09-20 00:29:48 +08:00
|
|
|
const { users } = this.props;
|
|
|
|
|
|
|
|
for (let i = 0; i < users.length; i++) {
|
|
|
|
if (users[i].has_stream) {
|
|
|
|
if (users[i].has_stream) {
|
|
|
|
this.start(users[i].userId, false, this.refs.videoInput);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-09-01 23:26:57 +08:00
|
|
|
|
|
|
|
ws.addEventListener('message', (msg) => {
|
|
|
|
const parsedMessage = JSON.parse(msg.data);
|
|
|
|
|
|
|
|
console.debug('Received message new ws message: ');
|
|
|
|
console.debug(parsedMessage);
|
|
|
|
|
|
|
|
switch (parsedMessage.id) {
|
|
|
|
|
|
|
|
case 'startResponse':
|
|
|
|
this.startResponse(parsedMessage);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'error':
|
|
|
|
this.handleError(parsedMessage);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'playStart':
|
|
|
|
this.handlePlayStart(parsedMessage);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'playStop':
|
|
|
|
this.handlePlayStop(parsedMessage);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'iceCandidate':
|
|
|
|
|
|
|
|
const webRtcPeer = this.state.webRtcPeers[parsedMessage.cameraId];
|
|
|
|
|
|
|
|
if (webRtcPeer !== null) {
|
|
|
|
webRtcPeer.addIceCandidate(parsedMessage.candidate, (err) => {
|
|
|
|
if (err) {
|
|
|
|
return console.error(`Error adding candidate: ${err}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
console.error(' [ICE] Message arrived before webRtcPeer?');
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
start(id, shareWebcam, videoInput) {
|
|
|
|
const that = this;
|
|
|
|
|
|
|
|
const ws = this.state.ws;
|
|
|
|
|
|
|
|
console.log(`Starting video call for video: ${id}`);
|
|
|
|
console.log('Creating WebRtcPeer and generating local sdp offer ...');
|
|
|
|
|
|
|
|
const onIceCandidate = function (candidate) {
|
|
|
|
const message = {
|
|
|
|
id: 'onIceCandidate',
|
|
|
|
candidate,
|
|
|
|
cameraId: id,
|
|
|
|
};
|
|
|
|
that.sendMessage(message);
|
|
|
|
};
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
mediaConstraints: { audio: false, video: true },
|
|
|
|
onicecandidate: onIceCandidate,
|
|
|
|
};
|
|
|
|
|
|
|
|
let peerObj;
|
|
|
|
if (shareWebcam) {
|
|
|
|
options.localVideo = videoInput;
|
|
|
|
peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly;
|
|
|
|
} else {
|
|
|
|
peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly;
|
|
|
|
|
|
|
|
options.remoteVideo = document.createElement('video');
|
|
|
|
options.remoteVideo.id = `video-elem-${id}`;
|
|
|
|
options.remoteVideo.width = 120;
|
|
|
|
options.remoteVideo.height = 90;
|
|
|
|
options.remoteVideo.autoplay = true;
|
|
|
|
|
|
|
|
document.getElementById('webcamArea').appendChild(options.remoteVideo);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.state.webRtcPeers[id] = peerObj(options, function (error) {
|
|
|
|
if (error) {
|
|
|
|
console.error(' [ERROR] Webrtc error');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.generateOffer((error, offerSdp) => {
|
|
|
|
if (error) {
|
|
|
|
return console.error(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
console.info(`Invoking SDP offer callback function ${location.host}`);
|
|
|
|
const message = {
|
|
|
|
id: 'start',
|
|
|
|
sdpOffer: offerSdp,
|
|
|
|
cameraId: id,
|
|
|
|
cameraShared: shareWebcam,
|
|
|
|
};
|
|
|
|
that.sendMessage(message);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
stop(id) {
|
|
|
|
const webRtcPeer = this.state.webRtcPeers[id];
|
|
|
|
|
|
|
|
if (webRtcPeer) {
|
|
|
|
console.log('Stopping WebRTC peer');
|
|
|
|
|
|
|
|
webRtcPeer.dispose();
|
|
|
|
delete this.state.webRtcPeers[id];
|
|
|
|
} else {
|
|
|
|
console.log('NO WEBRTC PEER TO STOP?');
|
|
|
|
}
|
|
|
|
|
|
|
|
const videoTag = document.getElementById(`video-elem-${id}`);
|
|
|
|
if (videoTag) {
|
|
|
|
document.getElementById('webcamArea').removeChild(videoTag);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.sendMessage({ id: 'stop', cameraId: id });
|
2017-09-20 00:29:48 +08:00
|
|
|
|
|
|
|
adjustVideos(true);
|
2017-09-01 23:26:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
shareWebcam() {
|
|
|
|
const { users } = this.props;
|
2017-09-19 21:53:27 +08:00
|
|
|
const id = users[0].userId;
|
2017-09-01 23:26:57 +08:00
|
|
|
|
|
|
|
this.start(id, true, this.refs.videoInput);
|
|
|
|
}
|
|
|
|
|
|
|
|
unshareWebcam() {
|
2017-09-19 21:53:27 +08:00
|
|
|
const { users } = this.props;
|
|
|
|
const id = users[0].userId;
|
|
|
|
this.sendUserUnshareWebcam(id);
|
2017-09-01 23:26:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
startResponse(message) {
|
|
|
|
const id = message.cameraId;
|
|
|
|
const webRtcPeer = this.state.webRtcPeers[id];
|
|
|
|
|
|
|
|
if (message.sdpAnswer == null) {
|
|
|
|
return console.debug('Null sdp answer. Camera unplugged?');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (webRtcPeer == null) {
|
|
|
|
return console.debug('Null webrtc peer ????');
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('SDP answer received from server. Processing ...');
|
|
|
|
|
|
|
|
webRtcPeer.processAnswer(message.sdpAnswer, (error) => {
|
|
|
|
if (error) {
|
|
|
|
return console.error(error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-09-19 21:53:27 +08:00
|
|
|
this.sendUserShareWebcam(id);
|
2017-09-01 23:26:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
sendMessage(message) {
|
|
|
|
const ws = this.state.ws;
|
|
|
|
|
|
|
|
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}"`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handlePlayStop(message) {
|
|
|
|
console.log('Handle play stop <--------------------');
|
|
|
|
|
|
|
|
this.stop(message.cameraId);
|
|
|
|
}
|
|
|
|
|
|
|
|
handlePlayStart(message) {
|
|
|
|
console.log('Handle play start <===================');
|
|
|
|
|
|
|
|
adjustVideos(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleError(message) {
|
|
|
|
console.log(` Handle error ---------------------> ${message.message}`);
|
|
|
|
}
|
|
|
|
|
2016-05-04 04:40:46 +08:00
|
|
|
render() {
|
|
|
|
return (
|
2017-09-01 23:26:57 +08:00
|
|
|
|
2016-05-04 04:40:46 +08:00
|
|
|
<div className={styles.videoDock}>
|
2017-09-01 23:26:57 +08:00
|
|
|
<div className={styles.secretButtons}>
|
|
|
|
<button type="button" onClick={this.shareWebcam} > Share Webcam </button>
|
|
|
|
<button type="button" onClick={this.unshareWebcam} > Unshare Webcam </button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div id="webcamArea" />
|
|
|
|
|
|
|
|
<video id="shareWebcamVideo" className={styles.sharedWebcamVideo} ref="videoInput" />
|
2016-05-04 04:40:46 +08:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2017-09-01 23:26:57 +08:00
|
|
|
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
|
|
const { users } = this.props;
|
|
|
|
const nextUsers = nextProps.users;
|
|
|
|
|
|
|
|
if (users) {
|
|
|
|
let suc = false;
|
|
|
|
|
|
|
|
for (let i = 0; i < users.length; i++) {
|
2017-09-19 21:53:27 +08:00
|
|
|
if (users && users[i] &&
|
|
|
|
nextUsers && nextUsers[i]) {
|
|
|
|
if (users[i].has_stream !== nextUsers[i].has_stream) {
|
|
|
|
console.log(`User ${nextUsers[i].has_stream ? '' : 'un'}shared webcam ${users[i].userId}`);
|
2017-09-01 23:26:57 +08:00
|
|
|
|
2017-09-19 21:53:27 +08:00
|
|
|
if (nextUsers[i].has_stream) {
|
|
|
|
this.start(users[i].userId, false, this.refs.videoInput);
|
2017-09-01 23:26:57 +08:00
|
|
|
} else {
|
2017-09-19 21:53:27 +08:00
|
|
|
this.stop(users[i].userId);
|
2017-09-01 23:26:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
suc = suc || true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2016-05-04 04:40:46 +08:00
|
|
|
}
|