Add CallHandler singleton and add CallView.

CallView is the container for either VideoViews or WaveformViews. All UI
elements listen for 'call_state' payloads and then call
CallHandler.getCall(roomId) to extract the current MatrixCall for that room.
We can't do this via stateful dispatches because dispatching does not preserve
ordering empirically (probably due to setTimeout).
This commit is contained in:
Kegan Dougal 2015-07-15 16:52:23 +01:00
parent 6316f1b195
commit 37c9c8fbb4
8 changed files with 251 additions and 131 deletions

View file

@ -49,6 +49,13 @@ module.exports = React.createClass({
<div className="mx_RoomHeader_button"> <div className="mx_RoomHeader_button">
<img src="img/search.png" width="32" height="32"/> <img src="img/search.png" width="32" height="32"/>
</div> </div>
{
this.state && this.state.inCall ?
<div className="mx_RoomHeader_button" onClick={this.onHangupClick}>
<img src="img/video.png" width="64" height="32"/>
</div>
: null
}
<div className="mx_RoomHeader_button" onClick={this.onVideoClick}> <div className="mx_RoomHeader_button" onClick={this.onVideoClick}>
<img src="img/video.png" width="32" height="32"/> <img src="img/video.png" width="32" height="32"/>
</div> </div>

View file

@ -20,20 +20,24 @@ var React = require('react');
var MatrixClientPeg = require("../../../../../src/MatrixClientPeg"); var MatrixClientPeg = require("../../../../../src/MatrixClientPeg");
var ComponentBroker = require('../../../../../src/ComponentBroker'); var ComponentBroker = require('../../../../../src/ComponentBroker');
var CallHandlerController = require( var CallViewController = require(
"../../../../../src/controllers/molecules/voip/CallHandler" "../../../../../src/controllers/molecules/voip/CallView"
); );
var VideoView = ComponentBroker.get('molecules/voip/VideoView'); var VideoView = ComponentBroker.get('molecules/voip/VideoView');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CallHandler', displayName: 'CallView',
mixins: [CallHandlerController], mixins: [CallViewController],
getVideoView: function() { getVideoView: function() {
return this.refs.video; return this.refs.video;
}, },
render: function(){ render: function(){
return (
<VideoView ref="video"/>
);
/*
if (this.state && this.state.call) { if (this.state && this.state.call) {
if (this.state.call.type === "video") { if (this.state.call.type === "video") {
return ( return (
@ -49,6 +53,6 @@ module.exports = React.createClass({
} }
return ( return (
<div></div> <div></div>
); ); */
} }
}); });

View file

@ -26,7 +26,7 @@ var classNames = require("classnames");
var MessageTile = ComponentBroker.get('molecules/MessageTile'); var MessageTile = ComponentBroker.get('molecules/MessageTile');
var RoomHeader = ComponentBroker.get('molecules/RoomHeader'); var RoomHeader = ComponentBroker.get('molecules/RoomHeader');
var MessageComposer = ComponentBroker.get('molecules/MessageComposer'); var MessageComposer = ComponentBroker.get('molecules/MessageComposer');
var CallHandler = ComponentBroker.get("molecules/voip/CallHandler"); var CallView = ComponentBroker.get("molecules/voip/CallView");
var RoomViewController = require("../../../../src/controllers/organisms/RoomView"); var RoomViewController = require("../../../../src/controllers/organisms/RoomView");
@ -69,7 +69,7 @@ module.exports = React.createClass({
<div className="mx_RoomView"> <div className="mx_RoomView">
<RoomHeader room={this.state.room} /> <RoomHeader room={this.state.room} />
<div className="mx_RoomView_auxPanel"> <div className="mx_RoomView_auxPanel">
<CallHandler room={this.state.room}/> <CallView room={this.state.room}/>
</div> </div>
<div ref="messageWrapper" className="mx_RoomView_messagePanel" onScroll={this.onMessageListScroll}> <div ref="messageWrapper" className="mx_RoomView_messagePanel" onScroll={this.onMessageListScroll}>
<div className="mx_RoomView_messageListWrapper"> <div className="mx_RoomView_messageListWrapper">

139
src/CallHandler.js Normal file
View file

@ -0,0 +1,139 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
/*
* Manages a list of all the currently active calls.
*
* This handler dispatches when voip calls are added/removed from this list:
* {
* action: 'call_state'
* room_id: <room ID of the call>
* }
*
* To know if the call was added/removed, this handler exposes a getter to
* obtain the call for a room:
* CallHandler.getCall(roomId)
*
* This handler listens for and handles the following actions:
* {
* action: 'place_call',
* type: 'voice|video',
* room_id: <room that the place call button was pressed in>
* }
*
* {
* action: 'incoming_call'
* call: MatrixCall
* }
*
* {
* action: 'hangup'
* room_id: <room that the hangup button was pressed in>
* }
*
* {
* action: 'answer'
* room_id: <room that the answer button was pressed in>
* }
*/
var MatrixClientPeg = require("./MatrixClientPeg");
var Matrix = require("matrix-js-sdk");
var dis = require("./dispatcher");
var calls = {
//room_id: MatrixCall
};
function _setCallListeners(call) {
call.on("error", function(err) {
console.error("Call error: %s", err);
console.error(err.stack);
call.hangup();
_setCallState(undefined, call.roomId);
});
call.on("hangup", function() {
_setCallState(undefined, call.roomId);
});
}
function _setCallState(call, roomId) {
console.log("_setState >>> %s >>> %s ", call, roomId);
calls[roomId] = call;
dis.dispatch({
action: 'call_state',
room_id: roomId
});
}
dis.register(function(payload) {
switch (payload.action) {
case 'place_call':
if (calls[payload.room_id]) {
return; // don't allow >1 call to be placed.
}
console.log("Place %s call in %s", payload.type, payload.room_id);
var call = Matrix.createNewMatrixCall(
MatrixClientPeg.get(), payload.room_id
);
_setCallListeners(call);
_setCallState(call, call.roomId);
if (payload.type === 'voice') {
call.placeVoiceCall();
}
else if (payload.type === 'video') {
call.placeVideoCall(
payload.remote_element,
payload.local_element
);
}
else {
console.error("Unknown call type: %s", payload.type);
}
break;
case 'incoming_call':
if (calls[payload.call.roomId]) {
payload.call.hangup("busy");
return; // don't allow >1 call to be received, hangup newer one.
}
var call = payload.call;
_setCallListeners(call);
_setCallState(call, call.roomId);
break;
case 'hangup':
if (!calls[payload.room_id]) {
return; // no call to hangup
}
calls[payload.room_id].hangup();
_setCallState(null, payload.room_id);
break;
case 'answer':
if (!calls[payload.room_id]) {
return; // no call to answer
}
calls[payload.room_id].answer();
break;
}
});
module.exports = {
getCall: function(roomId) {
return calls[roomId] || null;
}
};

View file

@ -96,7 +96,7 @@ require('../skins/base/views/molecules/RoomDropTarget');
require('../skins/base/views/molecules/DirectoryMenu'); require('../skins/base/views/molecules/DirectoryMenu');
require('../skins/base/views/atoms/voip/VideoFeed'); require('../skins/base/views/atoms/voip/VideoFeed');
require('../skins/base/views/molecules/voip/VideoView'); require('../skins/base/views/molecules/voip/VideoView');
require('../skins/base/views/molecules/voip/CallHandler'); require('../skins/base/views/molecules/voip/CallView');
} }

View file

@ -16,9 +16,40 @@ limitations under the License.
'use strict'; 'use strict';
/*
* State vars:
* this.state.inCall = boolean
*/
var dis = require("../../dispatcher"); var dis = require("../../dispatcher");
var CallHandler = require("../../CallHandler");
module.exports = { module.exports = {
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.setState({
inCall: false
});
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
// if we were given a room_id to track, don't handle anything else.
if (payload.room_id && this.props.room &&
this.props.room.roomId !== payload.room_id) {
return;
}
if (payload.action !== 'call_state') {
return;
}
this.setState({
inCall: (CallHandler.getCall(payload.room_id) !== null)
});
},
onVideoClick: function() { onVideoClick: function() {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
@ -32,6 +63,12 @@ module.exports = {
type: "voice", type: "voice",
room_id: this.props.room.roomId room_id: this.props.room.roomId
}); });
},
onHangupClick: function() {
dis.dispatch({
action: 'hangup',
room_id: this.props.room.roomId
});
} }
}; };

View file

@ -1,123 +0,0 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Matrix = require("matrix-js-sdk");
var dis = require("../../../dispatcher");
/*
* State vars:
* this.state.call = MatrixCall|null
*
* Props:
* this.props.room = Room (JS SDK) - can be null (for singleton views)
*/
module.exports = {
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.setState({
call: null
});
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
// if we were given a room_id to track, don't handle anything else.
if (payload.room_id && this.props.room &&
this.props.room.roomId !== payload.room_id) {
return;
}
switch (payload.action) {
case 'place_call':
if (this.state.call) {
return; // don't allow >1 call to be placed.
}
console.log("Place %s call in %s", payload.type, payload.room_id);
var call = Matrix.createNewMatrixCall(
MatrixClientPeg.get(), payload.room_id
);
this._setCallListeners(call);
this.setState({
call: call
});
if (payload.type === 'voice') {
call.placeVoiceCall();
}
else if (payload.type === 'video') {
var videoView = this.getVideoView();
call.placeVideoCall(
videoView.getRemoteVideoElement(),
videoView.getLocalVideoElement()
);
}
else {
console.error("Unknown call type: %s", payload.type);
}
break;
case 'incoming_call':
if (this.state.call) {
payload.call.hangup("busy");
return; // don't allow >1 call to be received.
}
this._setCallListeners(call);
this.setState({
call: call
});
console.log("Incoming call: %s", payload.call);
break;
case 'hangup':
if (!this.state.call) {
return; // no call to hangup
}
this.state.call.hangup();
this.setState({
call: null
});
break;
case 'answer':
if (!this.state.call) {
return; // no call to answer
}
this.state.call.answer();
break;
}
},
_setCallListeners: function(call) {
var self = this;
call.on("error", function(err) {
console.error("Call error: %s", err);
console.error(err.stack);
call.hangup();
self.setState({
call: null
});
});
call.on("hangup", function() {
self.setState({
call: null
});
})
}
};

View file

@ -0,0 +1,56 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
/*
* State vars:
* this.state.call = MatrixCall|null
*
* Props:
* this.props.room = Room (JS SDK)
*/
module.exports = {
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.setState({
call: null
});
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
// if we were given a room_id to track, don't handle anything else.
if (payload.room_id && this.props.room &&
this.props.room.roomId !== payload.room_id) {
return;
}
if (payload.action !== 'call_state') {
return;
}
this.setState({
call: CallHandler.getCall(payload.room_id)
});
}
};