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