Merge branch 'voip' into develop

Conflicts:
	webclient/room/room-controller.js
This commit is contained in:
David Baker 2014-08-29 11:33:36 +01:00
commit 171d8b032f
7 changed files with 379 additions and 3 deletions

View File

@ -24,6 +24,8 @@ var matrixWebClient = angular.module('matrixWebClient', [
'SettingsController', 'SettingsController',
'UserController', 'UserController',
'matrixService', 'matrixService',
'matrixPhoneService',
'MatrixCall',
'eventStreamService', 'eventStreamService',
'eventHandlerService', 'eventHandlerService',
'infinite-scroll' 'infinite-scroll'

View File

@ -95,7 +95,6 @@ angular.module('eventHandlerService', [])
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent); $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
}; };
return { return {
MSG_EVENT: MSG_EVENT, MSG_EVENT: MSG_EVENT,
MEMBER_EVENT: MEMBER_EVENT, MEMBER_EVENT: MEMBER_EVENT,

View File

@ -0,0 +1,264 @@
/*
Copyright 2014 matrix.org
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 forAllVideoTracksOnStream = function(s, f) {
var tracks = s.getVideoTracks();
for (var i = 0; i < tracks.length; i++) {
f(tracks[i]);
}
}
var forAllAudioTracksOnStream = function(s, f) {
var tracks = s.getAudioTracks();
for (var i = 0; i < tracks.length; i++) {
f(tracks[i]);
}
}
var forAllTracksOnStream = function(s, f) {
forAllVideoTracksOnStream(s, f);
forAllAudioTracksOnStream(s, f);
}
angular.module('MatrixCall', [])
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', function MatrixCallFactory(matrixService, matrixPhoneService) {
var MatrixCall = function(room_id) {
this.room_id = room_id;
this.call_id = "c" + new Date().getTime();
this.state = 'fledgling';
}
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
MatrixCall.prototype.placeCall = function() {
self = this;
matrixPhoneService.callPlaced(this);
navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); });
self.state = 'wait_local_media';
};
MatrixCall.prototype.initWithInvite = function(msg) {
this.msg = msg;
this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]})
self= this;
this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
this.state = 'ringing';
};
MatrixCall.prototype.answer = function() {
console.trace("Answering call "+this.call_id);
self = this;
navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); });
this.state = 'wait_local_media';
};
MatrixCall.prototype.hangup = function() {
console.trace("Ending call "+this.call_id);
forAllTracksOnStream(this.localAVStream, function(t) {
t.stop();
});
forAllTracksOnStream(this.remoteAVStream, function(t) {
t.stop();
});
var content = {
msgtype: "m.call.hangup",
version: 0,
call_id: this.call_id,
};
matrixService.sendMessage(this.room_id, undefined, content).then(this.messageSent, this.messageSendFailed);
this.state = 'ended';
};
MatrixCall.prototype.gotUserMediaForInvite = function(stream) {
this.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]})
self = this;
this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
this.peerConn.addStream(stream);
this.peerConn.createOffer(function(d) {
self.gotLocalOffer(d);
}, function(e) {
self.getLocalOfferFailed(e);
});
this.state = 'create_offer';
};
MatrixCall.prototype.gotUserMediaForAnswer = function(stream) {
this.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
this.peerConn.addStream(stream);
self = this;
var constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': false
},
};
this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints);
this.state = 'create_answer';
};
MatrixCall.prototype.gotLocalIceCandidate = function(event) {
console.trace(event);
if (event.candidate) {
var content = {
msgtype: "m.call.candidate",
version: 0,
call_id: this.call_id,
candidate: event.candidate
};
matrixService.sendMessage(this.room_id, undefined, content).then(this.messageSent, this.messageSendFailed);
}
}
MatrixCall.prototype.gotRemoteIceCandidate = function(cand) {
console.trace("Got ICE candidate from remote: "+cand);
var candidateObject = new RTCIceCandidate({
sdpMLineIndex: cand.label,
candidate: cand.candidate
});
this.peerConn.addIceCandidate(candidateObject, function() {}, function(e) {});
};
MatrixCall.prototype.receivedAnswer = function(msg) {
this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
this.state = 'connecting';
};
MatrixCall.prototype.gotLocalOffer = function(description) {
console.trace("Created offer: "+description);
this.peerConn.setLocalDescription(description);
var content = {
msgtype: "m.call.invite",
version: 0,
call_id: this.call_id,
offer: description
};
matrixService.sendMessage(this.room_id, undefined, content).then(this.messageSent, this.messageSendFailed);
this.state = 'invite_sent';
};
MatrixCall.prototype.createdAnswer = function(description) {
console.trace("Created answer: "+description);
this.peerConn.setLocalDescription(description);
var content = {
msgtype: "m.call.answer",
version: 0,
call_id: this.call_id,
answer: description
};
matrixService.sendMessage(this.room_id, undefined, content).then(this.messageSent, this.messageSendFailed);
this.state = 'connecting';
};
MatrixCall.prototype.messageSent = function() {
};
MatrixCall.prototype.messageSendFailed = function(error) {
};
MatrixCall.prototype.getLocalOfferFailed = function(error) {
this.onError("Failed to start audio for call!");
};
MatrixCall.prototype.getUserMediaFailed = function() {
this.onError("Couldn't start capturing audio! Is your microphone set up?");
};
MatrixCall.prototype.onIceConnectionStateChanged = function() {
console.trace("Ice connection state changed to: "+this.peerConn.iceConnectionState);
// ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
this.state = 'connected';
}
};
MatrixCall.prototype.onSignallingStateChanged = function() {
console.trace("Signalling state changed to: "+this.peerConn.signalingState);
};
MatrixCall.prototype.onSetRemoteDescriptionSuccess = function() {
console.trace("Set remote description");
};
MatrixCall.prototype.onSetRemoteDescriptionError = function(e) {
console.trace("Failed to set remote description"+e);
};
MatrixCall.prototype.onAddStream = function(event) {
console.trace("Stream added"+event);
var s = event.stream;
this.remoteAVStream = s;
var self = this;
forAllTracksOnStream(s, function(t) {
// not currently implemented in chrome
t.onstarted = self.onRemoteStreamTrackStarted;
});
// not currently implemented in chrome
event.stream.onstarted = this.onRemoteStreamStarted;
var player = new Audio();
player.src = URL.createObjectURL(s);
player.play();
};
MatrixCall.prototype.onRemoteStreamStarted = function(event) {
this.state = 'connected';
};
MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) {
this.state = 'connected';
};
MatrixCall.prototype.onHangupReceived = function() {
this.state = 'ended';
forAllTracksOnStream(this.localAVStream, function(t) {
t.stop();
});
forAllTracksOnStream(this.remoteAVStream, function(t) {
t.stop();
});
this.onHangup();
};
return MatrixCall;
}]);

View File

@ -0,0 +1,68 @@
/*
Copyright 2014 matrix.org
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';
angular.module('matrixPhoneService', [])
.factory('matrixPhoneService', ['$rootScope', '$injector', 'matrixService', 'eventHandlerService', function MatrixPhoneService($rootScope, $injector, matrixService, eventHandlerService) {
var matrixPhoneService = function() {
};
matrixPhoneService.CALL_EVENT = "CALL_EVENT";
matrixPhoneService.allCalls = {};
matrixPhoneService.callPlaced = function(call) {
matrixPhoneService.allCalls[call.call_id] = call;
};
$rootScope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
if (!isLive) return; // until matrix supports expiring messages
if (event.user_id == matrixService.config().user_id) return;
var msg = event.content;
if (msg.msgtype == 'm.call.invite') {
var MatrixCall = $injector.get('MatrixCall');
var call = new MatrixCall(event.room_id);
call.call_id = msg.call_id;
call.initWithInvite(msg);
matrixPhoneService.allCalls[call.call_id] = call;
$rootScope.$broadcast(matrixPhoneService.CALL_EVENT, call);
} else if (msg.msgtype == 'm.call.answer') {
var call = matrixPhoneService.allCalls[msg.call_id];
if (!call) {
console.trace("Got answer for unknown call ID "+msg.call_id);
return;
}
call.receivedAnswer(msg);
} else if (msg.msgtype == 'm.call.candidate') {
var call = matrixPhoneService.allCalls[msg.call_id];
if (!call) {
console.trace("Got candidate for unknown call ID "+msg.call_id);
return;
}
call.gotRemoteIceCandidate(msg.candidate);
} else if (msg.msgtype == 'm.call.hangup') {
var call = matrixPhoneService.allCalls[msg.call_id];
if (!call) {
console.trace("Got hangup for unknown call ID "+msg.call_id);
return;
}
call.onHangupReceived();
matrixPhoneService.allCalls[msg.call_id] = undefined;
}
});
return matrixPhoneService;
}]);

View File

@ -26,6 +26,8 @@
<script src="settings/settings-controller.js"></script> <script src="settings/settings-controller.js"></script>
<script src="user/user-controller.js"></script> <script src="user/user-controller.js"></script>
<script src="components/matrix/matrix-service.js"></script> <script src="components/matrix/matrix-service.js"></script>
<script src="components/matrix/matrix-call.js"></script>
<script src="components/matrix/matrix-phone-service.js"></script>
<script src="components/matrix/event-stream-service.js"></script> <script src="components/matrix/event-stream-service.js"></script>
<script src="components/matrix/event-handler-service.js"></script> <script src="components/matrix/event-handler-service.js"></script>
<script src="components/matrix/presence-service.js"></script> <script src="components/matrix/presence-service.js"></script>

View File

@ -15,8 +15,8 @@ limitations under the License.
*/ */
angular.module('RoomController', ['ngSanitize', 'mFileInput']) angular.module('RoomController', ['ngSanitize', 'mFileInput'])
.controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', .controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall',
function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload) { function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
'use strict'; 'use strict';
var MESSAGES_PER_PAGINATION = 30; var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320; var THUMBNAIL_SIZE = 320;
@ -83,6 +83,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
updatePresence(event); updatePresence(event);
}); });
$rootScope.$on(matrixPhoneService.CALL_EVENT, function(ngEvent, call) {
console.trace("incoming call");
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
$scope.currentCall = call;
});
$scope.paginateMore = function() { $scope.paginateMore = function() {
if ($scope.state.can_paginate) { if ($scope.state.can_paginate) {
// console.log("Paginating more."); // console.log("Paginating more.");
@ -90,6 +97,15 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
} }
}; };
$scope.answerCall = function() {
$scope.currentCall.answer();
};
$scope.hangupCall = function() {
$scope.currentCall.hangup();
$scope.currentCall = undefined;
};
var paginate = function(numItems) { var paginate = function(numItems) {
// console.log("paginate " + numItems); // console.log("paginate " + numItems);
if ($scope.state.paginating || !$scope.room_id) { if ($scope.state.paginating || !$scope.room_id) {
@ -454,4 +470,21 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
$scope.loadMoreHistory = function() { $scope.loadMoreHistory = function() {
paginate(MESSAGES_PER_PAGINATION); paginate(MESSAGES_PER_PAGINATION);
}; };
$scope.startVoiceCall = function() {
var call = new MatrixCall($scope.room_id);
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
call.placeCall();
$scope.currentCall = call;
}
$scope.onCallError = function(errStr) {
$scope.feedback = errStr;
}
$scope.onCallHangup = function() {
$scope.feedback = "Call ended";
$scope.currentCall = undefined;
}
}]); }]);

View File

@ -98,6 +98,14 @@
<button ng-click="inviteUser(userIDToInvite)">Invite</button> <button ng-click="inviteUser(userIDToInvite)">Invite</button>
</span> </span>
<button ng-click="leaveRoom()">Leave</button> <button ng-click="leaveRoom()">Leave</button>
<button ng-click="startVoiceCall()" ng-show="currentCall == undefined">Voice Call</button>
<div ng-show="currentCall.state == 'ringing'">
Incoming call from {{ currentCall.user_id }}
<button ng-click="answerCall()">Answer</button>
<button ng-click="hangupCall()">Reject</button>
</div>
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing'">Hang up</button>
{{ currentCall.state }}
</div> </div>
{{ feedback }} {{ feedback }}