diff options
Diffstat (limited to 'webclient/components')
-rw-r--r-- | webclient/components/fileInput/file-input-directive.js | 22 | ||||
-rw-r--r-- | webclient/components/matrix/event-handler-service.js | 22 | ||||
-rw-r--r-- | webclient/components/matrix/matrix-call.js | 144 | ||||
-rw-r--r-- | webclient/components/matrix/matrix-filter.js | 68 | ||||
-rw-r--r-- | webclient/components/matrix/matrix-phone-service.js | 12 |
5 files changed, 213 insertions, 55 deletions
diff --git a/webclient/components/fileInput/file-input-directive.js b/webclient/components/fileInput/file-input-directive.js index 14e2f772f7..9c849a140f 100644 --- a/webclient/components/fileInput/file-input-directive.js +++ b/webclient/components/fileInput/file-input-directive.js @@ -31,13 +31,23 @@ angular.module('mFileInput', []) }, link: function(scope, element, attrs, ctrl) { - element.bind("click", function() { - element.find("input")[0].click(); - element.find("input").bind("change", function(e) { - scope.selectedFile = this.files[0]; - scope.$apply(); + + // Check if HTML5 file selection is supported + if (window.FileList) { + element.bind("click", function() { + element.find("input")[0].click(); + element.find("input").bind("change", function(e) { + scope.selectedFile = this.files[0]; + scope.$apply(); + }); }); - }); + } + else { + setTimeout(function() { + element.attr("disabled", true); + element.attr("title", "The app uses the HTML5 File API to send files. Your browser does not support it."); + }, 1); + } // Change the mouse icon on mouseover on this element element.css("cursor", "pointer"); diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js index 5e95f34f4e..98003e97bf 100644 --- a/webclient/components/matrix/event-handler-service.js +++ b/webclient/components/matrix/event-handler-service.js @@ -189,21 +189,27 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) { if (window.Notification && event.user_id != matrixService.config().user_id) { var shouldBing = $rootScope.containsBingWord(event.content.body); - - // TODO: Binging every message when idle doesn't make much sense. Can we use this more sensibly? - // Unfortunately document.hidden = false on ubuntu chrome if chrome is minimised / does not have focus; - // true when you swap tabs though. However, for the case where the chat screen is OPEN and there is - // another window on top, we want to be notifying for those events. This DOES mean that there will be - // notifications when currently viewing the chat screen though, but that is preferable to the alternative imo. + + // Ideally we would notify only when the window is hidden (i.e. document.hidden = true). + // + // However, Chrome on Linux and OSX currently returns document.hidden = false unless the window is + // explicitly showing a different tab. So we need another metric to determine hiddenness - we + // simply use idle time. If the user has been idle enough that their presence goes to idle, then + // we also display notifs when things happen. + // + // This is far far better than notifying whenever anything happens anyway, otherwise you get spammed + // to death with notifications when the window is in the foreground, which is horrible UX (especially + // if you have not defined any bingers and so get notified for everything). var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState()); - // always bing if there are 0 bing words... apparently. + // We need a way to let people get notifications for everything, if they so desire. The way to do this + // is to specify zero bingwords. var bingWords = matrixService.config().bingWords; if (bingWords === undefined || bingWords.length === 0) { shouldBing = true; } - if (shouldBing) { + if (shouldBing && isIdle) { console.log("Displaying notification for "+JSON.stringify(event)); var member = $rootScope.events.rooms[event.room_id].members[event.user_id]; var displayname = undefined; diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index d05047eebb..7b5d9cffef 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -40,8 +40,15 @@ window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConne window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription; window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate; +// Returns true if the browser supports all required features to make WebRTC call +var isWebRTCSupported = function () { + return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate); +}; + angular.module('MatrixCall', []) .factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope, $timeout) { + $rootScope.isWebRTCSupported = isWebRTCSupported(); + var MatrixCall = function(room_id) { this.room_id = room_id; this.call_id = "c" + new Date().getTime(); @@ -51,6 +58,12 @@ angular.module('MatrixCall', []) // a queue for candidates waiting to go out. We try to amalgamate candidates into a single candidate message where possible this.candidateSendQueue = []; this.candidateSendTries = 0; + + var self = this; + $rootScope.$watch(this.remoteVideoElement, function (oldValue, newValue) { + self.tryPlayRemoteStream(); + }); + } MatrixCall.CALL_TIMEOUT = 60000; @@ -71,13 +84,39 @@ angular.module('MatrixCall', []) return pc; } - MatrixCall.prototype.placeCall = function(config) { + MatrixCall.prototype.getUserMediaVideoContraints = function(callType) { + switch (callType) { + case 'voice': + return ({audio: true, video: false}); + case 'video': + return ({audio: true, video: { + mandatory: { + minWidth: 640, + maxWidth: 640, + minHeight: 360, + maxHeight: 360, + } + }}); + } + }; + + MatrixCall.prototype.placeVoiceCall = function() { + this.placeCallWithConstraints(this.getUserMediaVideoContraints('voice')); + this.type = 'voice'; + }; + + MatrixCall.prototype.placeVideoCall = function(config) { + this.placeCallWithConstraints(this.getUserMediaVideoContraints('video')); + this.type = 'video'; + }; + + MatrixCall.prototype.placeCallWithConstraints = function(constraints) { var self = this; matrixPhoneService.callPlaced(this); - navigator.getUserMedia({audio: config.audio, video: config.video}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); }); + navigator.getUserMedia(constraints, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); }); this.state = 'wait_local_media'; this.direction = 'outbound'; - this.config = config; + this.config = constraints; }; MatrixCall.prototype.initWithInvite = function(event) { @@ -86,6 +125,17 @@ angular.module('MatrixCall', []) this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError); this.state = 'ringing'; this.direction = 'inbound'; + + if (window.mozRTCPeerConnection) { + // firefox's RTCPeerConnection doesn't add streams until it starts getting media on them + // so we need to figure out whether a video channel has been offered by ourselves. + if (this.msg.offer.sdp.indexOf('m=video') > -1) { + this.type = 'video'; + } else { + this.type = 'voice'; + } + } + var self = this; $timeout(function() { if (self.state == 'ringing') { @@ -108,9 +158,24 @@ angular.module('MatrixCall', []) MatrixCall.prototype.answer = function() { console.log("Answering call "+this.call_id); + var self = this; + + var roomMembers = $rootScope.events.rooms[this.room_id].members; + if (roomMembers[matrixService.config().user_id].membership != 'join') { + console.log("We need to join the room before we can accept this call"); + matrixService.join(this.room_id).then(function() { + self.answer(); + }, function() { + console.log("Failed to join room: can't answer call!"); + self.onError("Unable to join room to answer call!"); + self.hangup(); + }); + return; + } + if (!this.localAVStream && !this.waitForLocalAVStream) { - navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); }); + navigator.getUserMedia(this.getUserMediaVideoContraints(this.type), function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); }); this.state = 'wait_local_media'; } else if (this.localAVStream) { this.gotUserMediaForAnswer(this.localAVStream); @@ -132,17 +197,24 @@ angular.module('MatrixCall', []) } }; - MatrixCall.prototype.hangup = function(suppressEvent) { + MatrixCall.prototype.hangup = function(reason, suppressEvent) { console.log("Ending call "+this.call_id); + // pausing now keeps the last frame (ish) of the video call in the video element + // rather than it just turning black straight away + if (this.remoteVideoElement) this.remoteVideoElement.pause(); + if (this.localVideoElement) this.localVideoElement.pause(); + this.stopAllMedia(); if (this.peerConn) this.peerConn.close(); this.hangupParty = 'local'; + this.hangupReason = reason; var content = { version: 0, call_id: this.call_id, + reason: reason }; this.sendEventWithRetry('m.call.hangup', content); this.state = 'ended'; @@ -156,6 +228,13 @@ angular.module('MatrixCall', []) } if (this.state == 'ended') return; + if (this.localVideoElement && this.type == 'video') { + var vidTrack = stream.getVideoTracks()[0]; + this.localVideoElement.src = URL.createObjectURL(stream); + this.localVideoElement.muted = true; + this.localVideoElement.play(); + } + this.localAVStream = stream; var audioTracks = stream.getAudioTracks(); for (var i = 0; i < audioTracks.length; i++) { @@ -177,6 +256,13 @@ angular.module('MatrixCall', []) MatrixCall.prototype.gotUserMediaForAnswer = function(stream) { if (this.state == 'ended') return; + if (this.localVideoElement && this.type == 'video') { + var vidTrack = stream.getVideoTracks()[0]; + this.localVideoElement.src = URL.createObjectURL(stream); + this.localVideoElement.muted = true; + this.localVideoElement.play(); + } + this.localAVStream = stream; var audioTracks = stream.getAudioTracks(); for (var i = 0; i < audioTracks.length; i++) { @@ -187,7 +273,7 @@ angular.module('MatrixCall', []) var constraints = { 'mandatory': { 'OfferToReceiveAudio': true, - 'OfferToReceiveVideo': false + 'OfferToReceiveVideo': this.type == 'video' }, }; this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints); @@ -196,14 +282,14 @@ angular.module('MatrixCall', []) }; MatrixCall.prototype.gotLocalIceCandidate = function(event) { - console.log(event); if (event.candidate) { + console.log("Got local ICE "+event.candidate.sdpMid+" candidate: "+event.candidate.candidate); this.sendCandidate(event.candidate); } } MatrixCall.prototype.gotRemoteIceCandidate = function(cand) { - console.log("Got ICE candidate from remote: "+cand); + console.log("Got remote ICE "+cand.sdpMid+" candidate: "+cand.candidate); if (this.state == 'ended') { console.log("Ignoring remote ICE candidate because call has ended"); return; @@ -218,6 +304,7 @@ angular.module('MatrixCall', []) this.state = 'connecting'; }; + MatrixCall.prototype.gotLocalOffer = function(description) { console.log("Created offer: "+description); @@ -239,8 +326,7 @@ angular.module('MatrixCall', []) var self = this; $timeout(function() { if (self.state == 'invite_sent') { - self.hangupReason = 'invite_timeout'; - self.hangup(); + self.hangup('invite_timeout'); } }, MatrixCall.CALL_TIMEOUT); @@ -269,7 +355,7 @@ angular.module('MatrixCall', []) }; MatrixCall.prototype.getUserMediaFailed = function() { - this.onError("Couldn't start capturing audio! Is your microphone set up?"); + this.onError("Couldn't start capturing! Is your microphone set up?"); this.hangup(); }; @@ -283,6 +369,8 @@ angular.module('MatrixCall', []) self.state = 'connected'; self.didConnect = true; }); + } else if (this.peerConn.iceConnectionState == 'failed') { + this.hangup('ice_failed'); } }; @@ -305,6 +393,14 @@ angular.module('MatrixCall', []) this.remoteAVStream = s; + if (this.direction == 'inbound') { + if (s.getVideoTracks().length > 0) { + this.type = 'video'; + } else { + this.type = 'voice'; + } + } + var self = this; forAllTracksOnStream(s, function(t) { // not currently implemented in chrome @@ -314,9 +410,16 @@ angular.module('MatrixCall', []) event.stream.onended = function(e) { self.onRemoteStreamEnded(e); }; // not currently implemented in chrome event.stream.onstarted = function(e) { self.onRemoteStreamStarted(e); }; - var player = new Audio(); - player.src = URL.createObjectURL(s); - player.play(); + + this.tryPlayRemoteStream(); + }; + + MatrixCall.prototype.tryPlayRemoteStream = function(event) { + if (this.remoteVideoElement && this.remoteAVStream) { + var player = this.remoteVideoElement; + player.src = URL.createObjectURL(this.remoteAVStream); + player.play(); + } }; MatrixCall.prototype.onRemoteStreamStarted = function(event) { @@ -345,12 +448,15 @@ angular.module('MatrixCall', []) }); }; - MatrixCall.prototype.onHangupReceived = function() { + MatrixCall.prototype.onHangupReceived = function(msg) { console.log("Hangup received"); + if (this.remoteVideoElement) this.remoteVideoElement.pause(); + if (this.localVideoElement) this.localVideoElement.pause(); this.state = 'ended'; this.hangupParty = 'remote'; + this.hangupReason = msg.reason; this.stopAllMedia(); - if (this.peerConn.signalingState != 'closed') this.peerConn.close(); + if (this.peerConn && this.peerConn.signalingState != 'closed') this.peerConn.close(); if (this.onHangup) this.onHangup(this); }; @@ -361,13 +467,15 @@ angular.module('MatrixCall', []) newCall.waitForLocalAVStream = true; } else if (this.state == 'create_offer') { console.log("Handing local stream to new call"); - newCall.localAVStream = this.localAVStream; + newCall.gotUserMediaForAnswer(this.localAVStream); delete(this.localAVStream); } else if (this.state == 'invite_sent') { console.log("Handing local stream to new call"); - newCall.localAVStream = this.localAVStream; + newCall.gotUserMediaForAnswer(this.localAVStream); delete(this.localAVStream); } + newCall.localVideoElement = this.localVideoElement; + newCall.remoteVideoElement = this.remoteVideoElement; this.successor = newCall; this.hangup(true); }; diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js index 8b168cdedb..328e3a7086 100644 --- a/webclient/components/matrix/matrix-filter.js +++ b/webclient/components/matrix/matrix-filter.js @@ -38,13 +38,15 @@ angular.module('matrixFilter', []) roomName = alias; } else if (room.members) { + + var user_id = matrixService.config().user_id; + // Else, build the name from its users - // FIXME: Is it still required? // Limit the room renaming to 1:1 room if (2 === Object.keys(room.members).length) { for (var i in room.members) { var member = room.members[i]; - if (member.state_key !== matrixService.config().user_id) { + if (member.state_key !== user_id) { if (member.state_key in $rootScope.presence) { // If the user is available in presence, use the displayname there @@ -61,30 +63,44 @@ angular.module('matrixFilter', []) } } else if (1 === Object.keys(room.members).length) { - // The other member may be in the invite list, get all invited users - var invitedUserIDs = []; - for (var i in room.messages) { - var message = room.messages[i]; - if ("m.room.member" === message.type && "invite" === message.membership) { - // Make sure there is no duplicate user - if (-1 === invitedUserIDs.indexOf(message.state_key)) { - invitedUserIDs.push(message.state_key); - } - } - } - - // For now, only 1:1 room needs to be renamed. It means only 1 invited user - if (1 === invitedUserIDs.length) { - var userID = invitedUserIDs[0]; + var otherUserId; - // Try to resolve his displayname in presence global data - if (userID in $rootScope.presence) { - roomName = $rootScope.presence[userID].content.displayname; + if (Object.keys(room.members)[0] !== user_id) { + otherUserId = Object.keys(room.members)[0]; + } + else { + // The other member may be in the invite list, get all invited users + var invitedUserIDs = []; + for (var i in room.messages) { + var message = room.messages[i]; + if ("m.room.member" === message.type && "invite" === message.membership) { + // Filter out the current user + var member_id = message.state_key; + if (member_id === user_id) { + member_id = message.user_id; + } + if (member_id !== user_id) { + // Make sure there is no duplicate user + if (-1 === invitedUserIDs.indexOf(member_id)) { + invitedUserIDs.push(member_id); + } + } + } } - else { - roomName = userID; + + // For now, only 1:1 room needs to be renamed. It means only 1 invited user + if (1 === invitedUserIDs.length) { + otherUserId = invitedUserIDs[0]; } } + + // Try to resolve his displayname in presence global data + if (otherUserId in $rootScope.presence) { + roomName = $rootScope.presence[otherUserId].content.displayname; + } + else { + roomName = otherUserId; + } } } } @@ -97,6 +113,14 @@ angular.module('matrixFilter', []) if (undefined === roomName) { // By default, use the room ID roomName = room_id; + + // XXX: this is *INCREDIBLY* heavy logging for a function that calls every single + // time any kind of digest runs which refreshes a room name... + // commenting it out for now. + + // Log some information that lead to this leak + // console.log("Room ID leak for " + room_id); + // console.log("room object: " + JSON.stringify(room, undefined, 4)); } return roomName; diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js index d05eecf72a..06465ed821 100644 --- a/webclient/components/matrix/matrix-phone-service.js +++ b/webclient/components/matrix/matrix-phone-service.js @@ -59,6 +59,16 @@ angular.module('matrixPhoneService', []) var MatrixCall = $injector.get('MatrixCall'); var call = new MatrixCall(event.room_id); + + if (!isWebRTCSupported()) { + console.log("Incoming call ID "+msg.call_id+" but this browser doesn't support WebRTC"); + // don't hang up the call: there could be other clients connected that do support WebRTC and declining the + // the call on their behalf would be really annoying. + // instead, we broadcast a fake call event with a non-functional call object + $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call); + return; + } + call.call_id = msg.call_id; call.initWithInvite(event); matrixPhoneService.allCalls[call.call_id] = call; @@ -135,7 +145,7 @@ angular.module('matrixPhoneService', []) call.initWithHangup(event); matrixPhoneService.allCalls[msg.call_id] = call; } else { - call.onHangupReceived(); + call.onHangupReceived(msg); delete(matrixPhoneService.allCalls[msg.call_id]); } } |