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();
},