2014-08-27 19:57:54 +02:00
/ *
2014-09-03 18:29:13 +02:00
Copyright 2014 OpenMarket Ltd
2014-08-27 19:57:54 +02:00
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' ;
2014-08-29 12:29:36 +02:00
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 ) ;
}
2014-09-09 15:53:47 +02:00
navigator . getUserMedia = navigator . getUserMedia || navigator . webkitGetUserMedia || navigator . mozGetUserMedia ;
window . RTCPeerConnection = window . RTCPeerConnection || window . webkitRTCPeerConnection ; // but not mozRTCPeerConnection because its interface is not compatible
window . RTCSessionDescription = window . RTCSessionDescription || window . webkitRTCSessionDescription || window . mozRTCSessionDescription ;
window . RTCIceCandidate = window . RTCIceCandidate || window . webkitRTCIceCandidate || window . mozRTCIceCandidate ;
2014-08-27 19:57:54 +02:00
angular . module ( 'MatrixCall' , [ ] )
2014-09-12 17:31:56 +02:00
. factory ( 'MatrixCall' , [ 'matrixService' , 'matrixPhoneService' , '$rootScope' , '$timeout' , function MatrixCallFactory ( matrixService , matrixPhoneService , $rootScope , $timeout ) {
2014-08-27 19:57:54 +02:00
var MatrixCall = function ( room _id ) {
this . room _id = room _id ;
this . call _id = "c" + new Date ( ) . getTime ( ) ;
2014-08-28 20:03:34 +02:00
this . state = 'fledgling' ;
2014-09-06 01:14:02 +02:00
this . didConnect = false ;
2014-09-12 19:16:24 +02:00
// 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 ;
2014-08-27 19:57:54 +02:00
}
2014-09-16 15:46:13 +02:00
MatrixCall . CALL _TIMEOUT = 60000 ;
2014-09-11 16:23:06 +02:00
MatrixCall . prototype . createPeerConnection = function ( ) {
var stunServer = 'stun:stun.l.google.com:19302' ;
var pc ;
if ( window . mozRTCPeerConnection ) {
pc = window . mozRTCPeerConnection ( { 'url' : stunServer } ) ;
} else {
pc = new window . RTCPeerConnection ( { "iceServers" : [ { "urls" : "stun:stun.l.google.com:19302" } ] } ) ;
}
var self = this ;
pc . oniceconnectionstatechange = function ( ) { self . onIceConnectionStateChanged ( ) ; } ;
pc . onsignalingstatechange = function ( ) { self . onSignallingStateChanged ( ) ; } ;
pc . onicecandidate = function ( c ) { self . gotLocalIceCandidate ( c ) ; } ;
pc . onaddstream = function ( s ) { self . onAddStream ( s ) ; } ;
return pc ;
}
2014-09-09 18:37:50 +02:00
MatrixCall . prototype . placeCall = function ( config ) {
2014-09-11 16:23:06 +02:00
var self = this ;
2014-08-27 19:57:54 +02:00
matrixPhoneService . callPlaced ( this ) ;
2014-09-09 18:37:50 +02:00
navigator . getUserMedia ( { audio : config . audio , video : config . video } , function ( s ) { self . gotUserMediaForInvite ( s ) ; } , function ( e ) { self . getUserMediaFailed ( e ) ; } ) ;
this . state = 'wait_local_media' ;
2014-09-06 01:14:02 +02:00
this . direction = 'outbound' ;
2014-09-09 18:37:50 +02:00
this . config = config ;
2014-08-27 19:57:54 +02:00
} ;
2014-09-16 16:25:51 +02:00
MatrixCall . prototype . initWithInvite = function ( event ) {
this . msg = event . content ;
2014-09-11 16:23:06 +02:00
this . peerConn = this . createPeerConnection ( ) ;
this . peerConn . setRemoteDescription ( new RTCSessionDescription ( this . msg . offer ) , this . onSetRemoteDescriptionSuccess , this . onSetRemoteDescriptionError ) ;
2014-08-28 20:03:34 +02:00
this . state = 'ringing' ;
2014-09-06 01:14:02 +02:00
this . direction = 'inbound' ;
2014-09-16 16:25:51 +02:00
var self = this ;
$timeout ( function ( ) {
if ( self . state == 'ringing' ) {
self . state = 'ended' ;
self . hangupParty = 'remote' ; // effectively
self . stopAllMedia ( ) ;
if ( self . peerConn . signalingState != 'closed' ) self . peerConn . close ( ) ;
if ( self . onHangup ) self . onHangup ( self ) ;
}
} , this . msg . lifetime - event . age ) ;
2014-08-28 20:03:34 +02:00
} ;
2014-09-16 15:46:13 +02:00
// perverse as it may seem, sometimes we want to instantiate a call with a hangup message
// (because when getting the state of the room on load, events come in reverse order and
// we want to remember that a call has been hung up)
2014-09-16 16:25:51 +02:00
MatrixCall . prototype . initWithHangup = function ( event ) {
this . msg = event . content ;
2014-09-16 15:46:13 +02:00
this . state = 'ended' ;
} ;
2014-08-28 20:03:34 +02:00
MatrixCall . prototype . answer = function ( ) {
2014-09-11 16:23:06 +02:00
console . log ( "Answering call " + this . call _id ) ;
var self = this ;
if ( ! this . localAVStream && ! this . waitForLocalAVStream ) {
navigator . getUserMedia ( { audio : true , video : false } , function ( s ) { self . gotUserMediaForAnswer ( s ) ; } , function ( e ) { self . getUserMediaFailed ( e ) ; } ) ;
this . state = 'wait_local_media' ;
} else if ( this . localAVStream ) {
this . gotUserMediaForAnswer ( this . localAVStream ) ;
} else if ( this . waitForLocalAVStream ) {
this . state = 'wait_local_media' ;
}
2014-08-28 20:03:34 +02:00
} ;
2014-08-29 16:18:37 +02:00
MatrixCall . prototype . stopAllMedia = function ( ) {
2014-08-29 14:28:04 +02:00
if ( this . localAVStream ) {
forAllTracksOnStream ( this . localAVStream , function ( t ) {
2014-09-09 15:53:47 +02:00
if ( t . stop ) t . stop ( ) ;
2014-08-29 14:28:04 +02:00
} ) ;
}
if ( this . remoteAVStream ) {
forAllTracksOnStream ( this . remoteAVStream , function ( t ) {
2014-09-09 15:53:47 +02:00
if ( t . stop ) t . stop ( ) ;
2014-08-29 14:28:04 +02:00
} ) ;
}
2014-08-29 16:18:37 +02:00
} ;
2014-09-11 16:23:06 +02:00
MatrixCall . prototype . hangup = function ( suppressEvent ) {
console . log ( "Ending call " + this . call _id ) ;
2014-08-29 16:18:37 +02:00
this . stopAllMedia ( ) ;
2014-09-09 18:52:01 +02:00
if ( this . peerConn ) this . peerConn . close ( ) ;
2014-08-29 12:29:36 +02:00
2014-09-09 18:37:50 +02:00
this . hangupParty = 'local' ;
2014-08-28 20:03:34 +02:00
var content = {
version : 0 ,
call _id : this . call _id ,
} ;
2014-09-12 17:31:56 +02:00
this . sendEventWithRetry ( 'm.call.hangup' , content ) ;
2014-08-28 20:03:34 +02:00
this . state = 'ended' ;
2014-09-11 16:23:06 +02:00
if ( this . onHangup && ! suppressEvent ) this . onHangup ( this ) ;
2014-08-28 20:03:34 +02:00
} ;
MatrixCall . prototype . gotUserMediaForInvite = function ( stream ) {
2014-09-11 16:23:06 +02:00
if ( this . successor ) {
this . successor . gotUserMediaForAnswer ( stream ) ;
return ;
}
if ( this . state == 'ended' ) return ;
2014-09-09 18:58:26 +02:00
2014-08-29 12:29:36 +02:00
this . localAVStream = stream ;
2014-08-28 20:03:34 +02:00
var audioTracks = stream . getAudioTracks ( ) ;
for ( var i = 0 ; i < audioTracks . length ; i ++ ) {
audioTracks [ i ] . enabled = true ;
}
2014-09-11 16:23:06 +02:00
this . peerConn = this . createPeerConnection ( ) ;
2014-09-11 19:59:22 +02:00
this . peerConn . addStream ( stream ) ;
2014-09-11 16:23:06 +02:00
var self = this ;
2014-08-27 19:57:54 +02:00
this . peerConn . createOffer ( function ( d ) {
self . gotLocalOffer ( d ) ;
} , function ( e ) {
self . getLocalOfferFailed ( e ) ;
} ) ;
2014-09-08 17:10:36 +02:00
$rootScope . $apply ( function ( ) {
self . state = 'create_offer' ;
} ) ;
2014-08-28 20:03:34 +02:00
} ;
MatrixCall . prototype . gotUserMediaForAnswer = function ( stream ) {
2014-09-11 16:23:06 +02:00
if ( this . state == 'ended' ) return ;
2014-09-09 18:58:26 +02:00
2014-08-29 12:29:36 +02:00
this . localAVStream = stream ;
2014-08-28 20:03:34 +02:00
var audioTracks = stream . getAudioTracks ( ) ;
for ( var i = 0 ; i < audioTracks . length ; i ++ ) {
audioTracks [ i ] . enabled = true ;
}
this . peerConn . addStream ( stream ) ;
2014-09-11 16:23:06 +02:00
var self = this ;
2014-08-28 20:03:34 +02:00
var constraints = {
'mandatory' : {
'OfferToReceiveAudio' : true ,
'OfferToReceiveVideo' : false
} ,
} ;
this . peerConn . createAnswer ( function ( d ) { self . createdAnswer ( d ) ; } , function ( e ) { } , constraints ) ;
2014-09-11 20:16:57 +02:00
// This can't be in an apply() because it's called by a predecessor call under glare conditions :(
self . state = 'create_answer' ;
2014-08-27 19:57:54 +02:00
} ;
MatrixCall . prototype . gotLocalIceCandidate = function ( event ) {
2014-09-11 16:23:06 +02:00
console . log ( event ) ;
2014-08-27 19:57:54 +02:00
if ( event . candidate ) {
2014-09-12 19:16:24 +02:00
this . sendCandidate ( event . candidate ) ;
2014-08-27 19:57:54 +02:00
}
}
MatrixCall . prototype . gotRemoteIceCandidate = function ( cand ) {
2014-09-11 16:23:06 +02:00
console . log ( "Got ICE candidate from remote: " + cand ) ;
2014-09-10 12:12:02 +02:00
if ( this . state == 'ended' ) {
2014-09-11 16:23:06 +02:00
console . log ( "Ignoring remote ICE candidate because call has ended" ) ;
2014-09-10 12:12:02 +02:00
return ;
}
2014-09-16 15:46:13 +02:00
this . peerConn . addIceCandidate ( new RTCIceCandidate ( cand ) , function ( ) { } , function ( e ) { } ) ;
2014-08-28 20:03:34 +02:00
} ;
MatrixCall . prototype . receivedAnswer = function ( msg ) {
2014-09-16 15:46:13 +02:00
if ( this . state == 'ended' ) return ;
2014-09-11 16:23:06 +02:00
this . peerConn . setRemoteDescription ( new RTCSessionDescription ( msg . answer ) , this . onSetRemoteDescriptionSuccess , this . onSetRemoteDescriptionError ) ;
2014-08-28 20:03:34 +02:00
this . state = 'connecting' ;
2014-08-27 19:57:54 +02:00
} ;
MatrixCall . prototype . gotLocalOffer = function ( description ) {
2014-09-11 16:23:06 +02:00
console . log ( "Created offer: " + description ) ;
2014-09-12 15:06:35 +02:00
if ( this . state == 'ended' ) {
console . log ( "Ignoring newly created offer on call ID " + this . call _id + " because the call has ended" ) ;
return ;
}
2014-08-27 19:57:54 +02:00
this . peerConn . setLocalDescription ( description ) ;
var content = {
version : 0 ,
call _id : this . call _id ,
2014-09-16 15:46:13 +02:00
offer : description ,
lifetime : MatrixCall . CALL _TIMEOUT
2014-08-27 19:57:54 +02:00
} ;
2014-09-12 17:31:56 +02:00
this . sendEventWithRetry ( 'm.call.invite' , content ) ;
2014-09-08 17:10:36 +02:00
2014-09-11 16:23:06 +02:00
var self = this ;
2014-09-16 15:46:13 +02:00
$timeout ( function ( ) {
2014-09-16 16:25:51 +02:00
if ( self . state == 'invite_sent' ) {
self . hangupReason = 'invite_timeout' ;
self . hangup ( ) ;
}
2014-09-16 15:46:13 +02:00
} , MatrixCall . CALL _TIMEOUT ) ;
2014-09-08 17:10:36 +02:00
$rootScope . $apply ( function ( ) {
self . state = 'invite_sent' ;
} ) ;
2014-08-28 20:03:34 +02:00
} ;
MatrixCall . prototype . createdAnswer = function ( description ) {
2014-09-11 16:23:06 +02:00
console . log ( "Created answer: " + description ) ;
2014-08-28 20:03:34 +02:00
this . peerConn . setLocalDescription ( description ) ;
var content = {
version : 0 ,
call _id : this . call _id ,
answer : description
} ;
2014-09-12 17:31:56 +02:00
this . sendEventWithRetry ( 'm.call.answer' , content ) ;
2014-09-11 16:23:06 +02:00
var self = this ;
2014-09-08 17:10:36 +02:00
$rootScope . $apply ( function ( ) {
self . state = 'connecting' ;
} ) ;
2014-08-27 19:57:54 +02:00
} ;
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?" ) ;
2014-09-09 19:21:03 +02:00
this . hangup ( ) ;
2014-08-27 19:57:54 +02:00
} ;
2014-08-28 20:03:34 +02:00
MatrixCall . prototype . onIceConnectionStateChanged = function ( ) {
2014-09-06 01:14:02 +02:00
if ( this . state == 'ended' ) return ; // because ICE can still complete as we're ending the call
2014-09-11 16:23:06 +02:00
console . log ( "Ice connection state changed to: " + this . peerConn . iceConnectionState ) ;
2014-08-29 12:29:36 +02:00
// 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' ) {
2014-09-11 16:23:06 +02:00
var self = this ;
2014-09-08 17:10:36 +02:00
$rootScope . $apply ( function ( ) {
self . state = 'connected' ;
self . didConnect = true ;
} ) ;
2014-08-28 20:03:34 +02:00
}
} ;
MatrixCall . prototype . onSignallingStateChanged = function ( ) {
2014-09-11 16:23:06 +02:00
console . log ( "call " + this . call _id + ": Signalling state changed to: " + this . peerConn . signalingState ) ;
2014-08-28 20:03:34 +02:00
} ;
MatrixCall . prototype . onSetRemoteDescriptionSuccess = function ( ) {
2014-09-11 16:23:06 +02:00
console . log ( "Set remote description" ) ;
2014-08-28 20:03:34 +02:00
} ;
2014-08-27 19:57:54 +02:00
2014-08-28 20:03:34 +02:00
MatrixCall . prototype . onSetRemoteDescriptionError = function ( e ) {
2014-09-11 16:23:06 +02:00
console . log ( "Failed to set remote description" + e ) ;
2014-08-28 20:03:34 +02:00
} ;
MatrixCall . prototype . onAddStream = function ( event ) {
2014-09-11 16:23:06 +02:00
console . log ( "Stream added" + event ) ;
2014-08-29 12:29:36 +02:00
var s = event . stream ;
this . remoteAVStream = s ;
var self = this ;
forAllTracksOnStream ( s , function ( t ) {
// not currently implemented in chrome
t . onstarted = self . onRemoteStreamTrackStarted ;
} ) ;
2014-08-29 16:18:37 +02:00
event . stream . onended = function ( e ) { self . onRemoteStreamEnded ( e ) ; } ;
2014-08-29 12:29:36 +02:00
// not currently implemented in chrome
2014-08-29 16:18:37 +02:00
event . stream . onstarted = function ( e ) { self . onRemoteStreamStarted ( e ) ; } ;
2014-08-28 20:03:34 +02:00
var player = new Audio ( ) ;
2014-08-29 12:29:36 +02:00
player . src = URL . createObjectURL ( s ) ;
2014-08-28 20:03:34 +02:00
player . play ( ) ;
} ;
2014-08-29 12:29:36 +02:00
MatrixCall . prototype . onRemoteStreamStarted = function ( event ) {
2014-09-11 16:23:06 +02:00
var self = this ;
2014-09-08 17:10:36 +02:00
$rootScope . $apply ( function ( ) {
self . state = 'connected' ;
} ) ;
2014-08-29 12:29:36 +02:00
} ;
2014-08-29 16:18:37 +02:00
MatrixCall . prototype . onRemoteStreamEnded = function ( event ) {
2014-09-11 16:23:06 +02:00
console . log ( "Remote stream ended" ) ;
var self = this ;
2014-09-08 17:10:36 +02:00
$rootScope . $apply ( function ( ) {
self . state = 'ended' ;
2014-09-10 12:12:02 +02:00
self . hangupParty = 'remote' ;
2014-09-08 17:10:36 +02:00
self . stopAllMedia ( ) ;
2014-09-10 12:12:02 +02:00
if ( self . peerConn . signalingState != 'closed' ) self . peerConn . close ( ) ;
if ( self . onHangup ) self . onHangup ( self ) ;
2014-09-08 17:10:36 +02:00
} ) ;
2014-08-29 16:18:37 +02:00
} ;
2014-08-29 12:29:36 +02:00
MatrixCall . prototype . onRemoteStreamTrackStarted = function ( event ) {
2014-09-11 16:23:06 +02:00
var self = this ;
2014-09-08 17:10:36 +02:00
$rootScope . $apply ( function ( ) {
self . state = 'connected' ;
} ) ;
2014-08-29 12:29:36 +02:00
} ;
MatrixCall . prototype . onHangupReceived = function ( ) {
2014-09-11 16:23:06 +02:00
console . log ( "Hangup received" ) ;
2014-08-29 12:29:36 +02:00
this . state = 'ended' ;
2014-09-09 18:37:50 +02:00
this . hangupParty = 'remote' ;
2014-08-29 16:18:37 +02:00
this . stopAllMedia ( ) ;
2014-09-11 20:16:57 +02:00
if ( this . peerConn . signalingState != 'closed' ) this . peerConn . close ( ) ;
2014-09-11 16:23:06 +02:00
if ( this . onHangup ) this . onHangup ( this ) ;
} ;
MatrixCall . prototype . replacedBy = function ( newCall ) {
2014-09-12 12:51:57 +02:00
console . log ( this . call _id + " being replaced by " + newCall . call _id ) ;
2014-09-11 16:23:06 +02:00
if ( this . state == 'wait_local_media' ) {
2014-09-12 12:51:57 +02:00
console . log ( "Telling new call to wait for local media" ) ;
2014-09-11 16:23:06 +02:00
newCall . waitForLocalAVStream = true ;
} else if ( this . state == 'create_offer' ) {
2014-09-12 12:51:57 +02:00
console . log ( "Handing local stream to new call" ) ;
2014-09-11 16:23:06 +02:00
newCall . localAVStream = this . localAVStream ;
2014-09-12 12:51:57 +02:00
delete ( this . localAVStream ) ;
2014-09-11 16:23:06 +02:00
} else if ( this . state == 'invite_sent' ) {
2014-09-12 12:51:57 +02:00
console . log ( "Handing local stream to new call" ) ;
2014-09-11 16:23:06 +02:00
newCall . localAVStream = this . localAVStream ;
2014-09-12 12:51:57 +02:00
delete ( this . localAVStream ) ;
2014-09-11 16:23:06 +02:00
}
this . successor = newCall ;
this . hangup ( true ) ;
2014-08-29 12:29:36 +02:00
} ;
2014-09-12 17:31:56 +02:00
MatrixCall . prototype . sendEventWithRetry = function ( evType , content ) {
var ev = { type : evType , content : content , tries : 1 } ;
var self = this ;
matrixService . sendEvent ( this . room _id , evType , undefined , content ) . then ( this . eventSent , function ( error ) { self . eventSendFailed ( ev , error ) ; } ) ;
} ;
MatrixCall . prototype . eventSent = function ( ) {
} ;
MatrixCall . prototype . eventSendFailed = function ( ev , error ) {
if ( ev . tries > 5 ) {
console . log ( "Failed to send event of type " + ev . type + " on attempt " + ev . tries + ". Giving up." ) ;
return ;
}
var delayMs = 500 * Math . pow ( 2 , ev . tries ) ;
console . log ( "Failed to send event of type " + ev . type + ". Retrying in " + delayMs + "ms" ) ;
++ ev . tries ;
var self = this ;
$timeout ( function ( ) {
matrixService . sendEvent ( self . room _id , ev . type , undefined , ev . content ) . then ( self . eventSent , function ( error ) { self . eventSendFailed ( ev , error ) ; } ) ;
} , delayMs ) ;
} ;
2014-09-12 19:16:24 +02:00
// Sends candidates with are sent in a special way because we try to amalgamate them into one message
MatrixCall . prototype . sendCandidate = function ( content ) {
this . candidateSendQueue . push ( content ) ;
var self = this ;
if ( this . candidateSendTries == 0 ) $timeout ( function ( ) { self . sendCandidateQueue ( ) ; } , 100 ) ;
} ;
MatrixCall . prototype . sendCandidateQueue = function ( content ) {
if ( this . candidateSendQueue . length == 0 ) return ;
var cands = this . candidateSendQueue ;
this . candidateSendQueue = [ ] ;
++ this . candidateSendTries ;
var content = {
version : 0 ,
call _id : this . call _id ,
candidates : cands
} ;
var self = this ;
console . log ( "Attempting to send " + cands . length + " candidates" ) ;
matrixService . sendEvent ( self . room _id , 'm.call.candidates' , undefined , content ) . then ( function ( ) { self . candsSent ( ) ; } , function ( error ) { self . candsSendFailed ( cands , error ) ; } ) ;
} ;
MatrixCall . prototype . candsSent = function ( ) {
this . candidateSendTries = 0 ;
this . sendCandidateQueue ( ) ;
} ;
MatrixCall . prototype . candsSendFailed = function ( cands , error ) {
for ( var i = 0 ; i < cands . length ; ++ i ) {
this . candidateSendQueue . push ( cands [ i ] ) ;
}
if ( this . candidateSendTries > 5 ) {
console . log ( "Failed to send candidates on attempt " + ev . tries + ". Giving up for now." ) ;
this . candidateSendTries = 0 ;
return ;
}
var delayMs = 500 * Math . pow ( 2 , this . candidateSendTries ) ;
++ this . candidateSendTries ;
console . log ( "Failed to send candidates. Retrying in " + delayMs + "ms" ) ;
var self = this ;
$timeout ( function ( ) {
self . sendCandidateQueue ( ) ;
} , delayMs ) ;
} ;
2014-08-27 19:57:54 +02:00
return MatrixCall ;
} ] ) ;