Merge pull request #6 from matrix-org/voip

VoIP addition
This commit is contained in:
David Baker 2015-07-16 12:31:36 +01:00
commit a6f857e9d8
12 changed files with 527 additions and 3 deletions

View File

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

View File

@ -30,6 +30,32 @@ module.exports = React.createClass({
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
topic = topic ? <div className="mx_RoomHeader_topic">{ topic.getContent().topic }</div> : null;
var callButtons;
if (this.state) {
switch (this.state.call_state) {
case "ringing":
callButtons = (
<div>
<div className="mx_RoomHeader_button" onClick={this.onAnswerClick}>
YUP
</div>
<div className="mx_RoomHeader_button" onClick={this.onHangupClick}>
NOPE
</div>
</div>
);
break;
case "ringback":
case "connected":
callButtons = (
<div className="mx_RoomHeader_button" onClick={this.onHangupClick}>
BYEBYE
</div>
);
break;
}
}
return (
<div className="mx_RoomHeader">
<div className="mx_RoomHeader_wrapper">
@ -49,10 +75,11 @@ module.exports = React.createClass({
<div className="mx_RoomHeader_button">
<img src="img/search.png" width="32" height="32"/>
</div>
<div className="mx_RoomHeader_button">
{callButtons}
<div className="mx_RoomHeader_button" onClick={this.onVideoClick}>
<img src="img/video.png" width="32" height="32"/>
</div>
<div className="mx_RoomHeader_button">
<div className="mx_RoomHeader_button" onClick={this.onVoiceClick}>
<img src="img/voip.png" width="32" height="32"/>
</div>
</div>

View File

@ -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 (
<VideoView ref="video"/>
);
}
});

View File

@ -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 (
<div>
<div>
<VideoFeed ref="remote"/>
</div>
<div>
<VideoFeed ref="local"/>
</div>
</div>
);
}
});

View File

@ -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 (
<div className="mx_RoomView">
<RoomHeader room={this.state.room} />
<div className="mx_RoomView_auxPanel"></div>
<div className="mx_RoomView_auxPanel">
<CallView room={this.state.room}/>
</div>
<div ref="messageWrapper" className="mx_RoomView_messagePanel" onScroll={this.onMessageListScroll}>
<div className="mx_RoomView_messageListWrapper">
<div className="mx_RoomView_MessageList" aria-live="polite">

187
src/CallHandler.js Normal file
View File

@ -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: <room ID of the call>,
* 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: <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, "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;
}
};

View File

@ -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');
}

View File

@ -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 = {
};

View File

@ -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
});
}
};

View File

@ -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";
}
}
};

View File

@ -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 = {
};

View File

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