diff --git a/skins/base/views/atoms/voip/VideoFeed.js b/skins/base/views/atoms/voip/VideoFeed.js new file mode 100644 index 0000000000..7fbee43699 --- /dev/null +++ b/skins/base/views/atoms/voip/VideoFeed.js @@ -0,0 +1,34 @@ +/* +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 React = require('react'); + +var VideoFeedController = require("../../../../../src/controllers/atoms/voip/VideoFeed"); + +module.exports = React.createClass({ + displayName: 'VideoFeed', + mixins: [VideoFeedController], + + render: function() { + return ( + + ); + }, +}); + diff --git a/skins/base/views/molecules/RoomHeader.js b/skins/base/views/molecules/RoomHeader.js index 23bd63d3ca..1708bd1c39 100644 --- a/skins/base/views/molecules/RoomHeader.js +++ b/skins/base/views/molecules/RoomHeader.js @@ -30,6 +30,32 @@ module.exports = React.createClass({ var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); topic = topic ?
{ topic.getContent().topic }
: null; + var callButtons; + if (this.state) { + switch (this.state.call_state) { + case "ringing": + callButtons = ( +
+
+ YUP +
+
+ NOPE +
+
+ ); + break; + case "ringback": + case "connected": + callButtons = ( +
+ BYEBYE +
+ ); + break; + } + } + return (
@@ -49,10 +75,11 @@ module.exports = React.createClass({
-
+ {callButtons} +
-
+
diff --git a/skins/base/views/molecules/voip/CallView.js b/skins/base/views/molecules/voip/CallView.js new file mode 100644 index 0000000000..3642e6b58b --- /dev/null +++ b/skins/base/views/molecules/voip/CallView.js @@ -0,0 +1,41 @@ +/* +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 React = require('react'); + +var MatrixClientPeg = require("../../../../../src/MatrixClientPeg"); +var ComponentBroker = require('../../../../../src/ComponentBroker'); +var CallViewController = require( + "../../../../../src/controllers/molecules/voip/CallView" +); +var VideoView = ComponentBroker.get('molecules/voip/VideoView'); + +module.exports = React.createClass({ + displayName: 'CallView', + mixins: [CallViewController], + + getVideoView: function() { + return this.refs.video; + }, + + render: function(){ + return ( + + ); + } +}); \ No newline at end of file diff --git a/skins/base/views/molecules/voip/VideoView.js b/skins/base/views/molecules/voip/VideoView.js new file mode 100644 index 0000000000..954ad3648f --- /dev/null +++ b/skins/base/views/molecules/voip/VideoView.js @@ -0,0 +1,50 @@ +/* +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 React = require('react'); + +var MatrixClientPeg = require("../../../../../src/MatrixClientPeg"); +var ComponentBroker = require('../../../../../src/ComponentBroker'); +var VideoViewController = require("../../../../../src/controllers/molecules/voip/VideoView"); +var VideoFeed = ComponentBroker.get('atoms/voip/VideoFeed'); + +module.exports = React.createClass({ + displayName: 'VideoView', + mixins: [VideoViewController], + + getRemoteVideoElement: function() { + return this.refs.remote.getDOMNode(); + }, + + getLocalVideoElement: function() { + return this.refs.local.getDOMNode(); + }, + + render: function() { + return ( +
+
+ +
+
+ +
+
+ ); + } +}); \ No newline at end of file diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js index 7a46f88dea..27b37138dd 100644 --- a/skins/base/views/organisms/RoomView.js +++ b/skins/base/views/organisms/RoomView.js @@ -26,6 +26,7 @@ var classNames = require("classnames"); var MessageTile = ComponentBroker.get('molecules/MessageTile'); var RoomHeader = ComponentBroker.get('molecules/RoomHeader'); var MessageComposer = ComponentBroker.get('molecules/MessageComposer'); +var CallView = ComponentBroker.get("molecules/voip/CallView"); var RoomViewController = require("../../../../src/controllers/organisms/RoomView"); @@ -73,7 +74,9 @@ module.exports = React.createClass({ return (
-
+
+ +
diff --git a/src/CallHandler.js b/src/CallHandler.js new file mode 100644 index 0000000000..c041f64628 --- /dev/null +++ b/src/CallHandler.js @@ -0,0 +1,187 @@ +/* +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/updated/removed from this list: + * { + * action: 'call_state' + * room_id: , + * status: ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing + * } + * + * 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: + * } + * + * { + * action: 'incoming_call' + * call: MatrixCall + * } + * + * { + * action: 'hangup' + * room_id: + * } + * + * { + * action: 'answer' + * room_id: + * } + */ + +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, "ended"); + }); + call.on("hangup", function() { + _setCallState(undefined, call.roomId, "ended"); + }); + // map web rtc states to dummy UI state + // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing + call.on("state", function(newState, oldState) { + if (newState === "ringing") { + _setCallState(call, call.roomId, "ringing"); + } + else if (newState === "invite_sent") { + _setCallState(call, call.roomId, "ringback"); + } + else if (newState === "ended" && oldState === "connected") { + _setCallState(call, call.roomId, "ended"); + } + else if (newState === "ended" && oldState === "invite_sent" && + (call.hangupParty === "remote" || + (call.hangupParty === "local" && call.hangupReason === "invite_timeout") + )) { + _setCallState(call, call.roomId, "busy"); + } + else if (oldState === "invite_sent") { + _setCallState(call, call.roomId, "stop_ringback"); + } + else if (oldState === "ringing") { + _setCallState(call, call.roomId, "stop_ringing"); + } + else if (newState === "connected") { + _setCallState(call, call.roomId, "connected"); + } + }); +} + +function _setCallState(call, roomId, status) { + console.log( + "Call state in %s changed to %s (%s)", roomId, status, (call ? call.state : "-") + ); + calls[roomId] = call; + if (call) { + call.call_state = status; + } + dis.dispatch({ + action: 'call_state', + room_id: roomId, + status: status + }); +} + +dis.register(function(payload) { + switch (payload.action) { + case 'place_call': + if (calls[payload.room_id]) { + return; // don't allow >1 call to be placed. + } + var room = MatrixClientPeg.get().getRoom(payload.room_id); + if (!room) { + console.error("Room %s does not exist.", payload.room_id); + return; + } + if (room.getJoinedMembers().length !== 2) { + console.error( + "Fail: There are %s joined members in this room, not 2.", + room.getJoinedMembers().length + ); + return; + } + 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, "ringback"); + 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, "ringing"); + break; + case 'hangup': + if (!calls[payload.room_id]) { + return; // no call to hangup + } + calls[payload.room_id].hangup(); + _setCallState(null, payload.room_id, "ended"); + break; + case 'answer': + if (!calls[payload.room_id]) { + return; // no call to answer + } + calls[payload.room_id].answer(); + _setCallState(calls[payload.room_id], payload.room_id, "connected"); + break; + } +}); + +module.exports = { + getCall: function(roomId) { + return calls[roomId] || null; + } +}; \ No newline at end of file diff --git a/src/ComponentBroker.js b/src/ComponentBroker.js index dfbcf2e217..80506ea518 100644 --- a/src/ComponentBroker.js +++ b/src/ComponentBroker.js @@ -98,5 +98,9 @@ require('../skins/base/views/organisms/RightPanel'); require('../skins/base/views/molecules/RoomCreate'); require('../skins/base/views/molecules/RoomDropTarget'); require('../skins/base/views/molecules/DirectoryMenu'); +require('../skins/base/views/atoms/voip/VideoFeed'); +require('../skins/base/views/molecules/voip/VideoView'); +require('../skins/base/views/molecules/voip/CallView'); + } diff --git a/src/controllers/atoms/voip/VideoFeed.js b/src/controllers/atoms/voip/VideoFeed.js new file mode 100644 index 0000000000..8aa688b21e --- /dev/null +++ b/src/controllers/atoms/voip/VideoFeed.js @@ -0,0 +1,21 @@ +/* +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'; + +module.exports = { +}; + diff --git a/src/controllers/molecules/RoomHeader.js b/src/controllers/molecules/RoomHeader.js index 8aa688b21e..24f0d47abe 100644 --- a/src/controllers/molecules/RoomHeader.js +++ b/src/controllers/molecules/RoomHeader.js @@ -16,6 +16,72 @@ limitations under the License. 'use strict'; +/* + * State vars: + * this.state.call_state = the UI state of the call (see CallHandler) + */ + +var dis = require("../../dispatcher"); +var CallHandler = require("../../CallHandler"); + module.exports = { + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + if (this.props.room) { + var call = CallHandler.getCall(this.props.room.roomId); + var callState = call ? call.call_state : "ended"; + this.setState({ + call_state: callState + }); + } + }, + + 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; + } + var call = CallHandler.getCall(payload.room_id); + var callState = call ? call.call_state : "ended"; + this.setState({ + call_state: callState + }); + }, + + onVideoClick: function() { + dis.dispatch({ + action: 'place_call', + type: "video", + room_id: this.props.room.roomId + }); + }, + onVoiceClick: function() { + dis.dispatch({ + action: 'place_call', + type: "voice", + room_id: this.props.room.roomId + }); + }, + onHangupClick: function() { + dis.dispatch({ + action: 'hangup', + room_id: this.props.room.roomId + }); + }, + onAnswerClick: function() { + dis.dispatch({ + action: 'answer', + room_id: this.props.room.roomId + }); + } }; diff --git a/src/controllers/molecules/voip/CallView.js b/src/controllers/molecules/voip/CallView.js new file mode 100644 index 0000000000..485782e943 --- /dev/null +++ b/src/controllers/molecules/voip/CallView.js @@ -0,0 +1,64 @@ +/* +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; + } + var call = CallHandler.getCall(payload.room_id); + if (call && call.type === "video") { + this.getVideoView().getLocalVideoElement().style.display = "initial"; + this.getVideoView().getRemoteVideoElement().style.display = "initial"; + call.setLocalVideoElement(this.getVideoView().getLocalVideoElement()); + call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement()); + } + else { + this.getVideoView().getLocalVideoElement().style.display = "none"; + this.getVideoView().getRemoteVideoElement().style.display = "none"; + } + } +}; + diff --git a/src/controllers/molecules/voip/VideoView.js b/src/controllers/molecules/voip/VideoView.js new file mode 100644 index 0000000000..8aa688b21e --- /dev/null +++ b/src/controllers/molecules/voip/VideoView.js @@ -0,0 +1,21 @@ +/* +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'; + +module.exports = { +}; + diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 9367872b2a..dd8594bbe8 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -168,6 +168,12 @@ module.exports = { that.setState({ready: true, currentRoom: firstRoom}); dis.dispatch({action: 'focus_composer'}); }); + cli.on('Call.incoming', function(call) { + dis.dispatch({ + action: 'incoming_call', + call: call + }); + }); Notifier.start(); cli.startClient(); },