'use strict';
var Call = function(params) {
this.params_ = params;
this.roomServer_ = params.roomServer || '';
this.channel_ = new SignalingChannel(params.wssUrl, params.wssPostUrl);
this.channel_.onmessage = this.onRecvSignalingChannelMessage_.bind(this);
this.pcClient_ = null;
this.localStream_ = null;
this.errorMessageQueue_ = [];
this.startTime = null;
this.oncallerstarted = null;
this.onerror = null;
this.oniceconnectionstatechange = null;
this.onlocalstreamadded = null;
this.onnewicecandidate = null;
this.onremotehangup = null;
this.onremotesdpset = null;
this.onremotestreamadded = null;
this.onsignalingstatechange = null;
this.onturnstatusmessage = null;
this.getMediaPromise_ = null;
this.getIceServersPromise_ = null;
this.requestMediaAndIceServers_();
};
Call.prototype.requestMediaAndIceServers_ = function() {
this.getMediaPromise_ = this.maybeGetMedia_();
this.getIceServersPromise_ = this.maybeGetIceServers_();
};
Call.prototype.isInitiator = function() {
return this.params_.isInitiator;
};
Call.prototype.start = function(roomId) {
this.connectToRoom_(roomId);
if (this.params_.isLoopback) {
setupLoopback(this.params_.wssUrl, roomId);
}
};
Call.prototype.restart = function() {
this.requestMediaAndIceServers_();
this.start(this.params_.previousRoomId);
};
Call.prototype.hangup = function(async) {
this.startTime = null;
if (this.localStream_) {
if (typeof this.localStream_.getTracks === 'undefined') {
this.localStream_.stop();
} else {
this.localStream_.getTracks().forEach(function(track) {
track.stop();
});
}
this.localStream_ = null;
}
if (!this.params_.roomId) {
return;
}
if (this.pcClient_) {
this.pcClient_.close();
this.pcClient_ = null;
}
var steps = [];
steps.push({
step: function() {
var path = this.getLeaveUrl_();
return sendUrlRequest('POST', path, async);
}.bind(this),
errorString: 'Error sending /leave:'
});
steps.push({
step: function() {
this.channel_.send(JSON.stringify({type: 'bye'}));
}.bind(this),
errorString: 'Error sending bye:'
});
steps.push({
step: function() {
return this.channel_.close(async);
}.bind(this),
errorString: 'Error closing signaling channel:'
});
steps.push({
step: function() {
this.params_.previousRoomId = this.params_.roomId;
this.params_.roomId = null;
this.params_.clientId = null;
}.bind(this),
errorString: 'Error setting params:'
});
if (async) {
var errorHandler = function(errorString, error) {
trace(errorString + ' ' + error.message);
};
var promise = Promise.resolve();
for (var i = 0; i < steps.length; ++i) {
promise = promise.then(steps[i].step).catch(
errorHandler.bind(this, steps[i].errorString));
}
return promise;
}
var executeStep = function(executor, errorString) {
try {
executor();
} catch (ex) {
trace(errorString + ' ' + ex);
}
};
for (var j = 0; j < steps.length; ++j) {
executeStep(steps[j].step, steps[j].errorString);
}
if (this.params_.roomId !== null || this.params_.clientId !== null) {
trace('ERROR: sync cleanup tasks did not complete successfully.');
} else {
trace('Cleanup completed.');
}
return Promise.resolve();
};
Call.prototype.getLeaveUrl_ = function() {
return this.roomServer_ + '/leave/' + this.params_.roomId +
'/' + this.params_.clientId;
};
Call.prototype.onRemoteHangup = function() {
this.startTime = null;
this.params_.isInitiator = true;
if (this.pcClient_) {
this.pcClient_.close();
this.pcClient_ = null;
}
this.startSignaling_();
};
Call.prototype.getPeerConnectionStates = function() {
if (!this.pcClient_) {
return null;
}
return this.pcClient_.getPeerConnectionStates();
};
Call.prototype.getPeerConnectionStats = function(callback) {
if (!this.pcClient_) {
return;
}
this.pcClient_.getPeerConnectionStats(callback);
};
Call.prototype.toggleVideoMute = function() {
var videoTracks = this.localStream_.getVideoTracks();
if (videoTracks.length === 0) {
trace('No local video available.');
return;
}
trace('Toggling video mute state.');
for (var i = 0; i < videoTracks.length; ++i) {
videoTracks[i].enabled = !videoTracks[i].enabled;
}
trace('Video ' + (videoTracks[0].enabled ? 'unmuted.' : 'muted.'));
};
Call.prototype.toggleAudioMute = function() {
var audioTracks = this.localStream_.getAudioTracks();
if (audioTracks.length === 0) {
trace('No local audio available.');
return;
}
trace('Toggling audio mute state.');
for (var i = 0; i < audioTracks.length; ++i) {
audioTracks[i].enabled = !audioTracks[i].enabled;
}
trace('Audio ' + (audioTracks[0].enabled ? 'unmuted.' : 'muted.'));
};
Call.prototype.connectToRoom_ = function(roomId) {
this.params_.roomId = roomId;
var channelPromise = this.channel_.open().catch(function(error) {
this.onError_('WebSocket open error: ' + error.message);
return Promise.reject(error);
}.bind(this));
var joinPromise =
this.joinRoom_().then(function(roomParams) {
this.params_.clientId = roomParams.client_id;
this.params_.roomId = roomParams.room_id;
this.params_.roomLink = roomParams.room_link;
this.params_.isInitiator = roomParams.is_initiator === 'true';
this.params_.messages = roomParams.messages;
}.bind(this)).catch(function(error) {
this.onError_('Room server join error: ' + error.message);
return Promise.reject(error);
}.bind(this));
Promise.all([channelPromise, joinPromise]).then(function() {
this.channel_.register(this.params_.roomId, this.params_.clientId);
Promise.all([this.getIceServersPromise_, this.getMediaPromise_])
.then(function() {
this.startSignaling_();
}.bind(this)).catch(function(error) {
this.onError_('Failed to start signaling: ' + error.message);
}.bind(this));
}.bind(this)).catch(function(error) {
this.onError_('WebSocket register error: ' + error.message);
}.bind(this));
};
Call.prototype.maybeGetMedia_ = function() {
var needStream = (this.params_.mediaConstraints.audio !== false ||
this.params_.mediaConstraints.video !== false);
var mediaPromise = null;
if (needStream) {
var mediaConstraints = this.params_.mediaConstraints;
mediaPromise = navigator.mediaDevices.getUserMedia(mediaConstraints)
.catch(function(error) {
if (error.name !== 'NotFoundError') {
throw error;
}
return navigator.mediaDevices.enumerateDevices()
.then(function(devices) {
var cam = devices.find(function(device) {
return device.kind === 'videoinput';
});
var mic = devices.find(function(device) {
return device.kind === 'audioinput';
});
var constraints = {
video: cam && mediaConstraints.video,
audio: mic && mediaConstraints.audio
};
return navigator.mediaDevices.getUserMedia(constraints);
});
})
.then(function(stream) {
trace('Got access to local media with mediaConstraints:\n' +
' \'' + JSON.stringify(mediaConstraints) + '\'');
this.onUserMediaSuccess_(stream);
}.bind(this)).catch(function(error) {
this.onError_('Error getting user media: ' + error.message);
this.onUserMediaError_(error);
}.bind(this));
} else {
mediaPromise = Promise.resolve();
}
return mediaPromise;
};
Call.prototype.maybeGetIceServers_ = function() {
var shouldRequestIceServers =
(this.params_.iceServerRequestUrl &&
this.params_.iceServerRequestUrl.length > 0 &&
this.params_.peerConnectionConfig.iceServers &&
this.params_.peerConnectionConfig.iceServers.length === 0);
var iceServerPromise = null;
if (shouldRequestIceServers) {
var requestUrl = this.params_.iceServerRequestUrl;
iceServerPromise =
requestIceServers(requestUrl, this.params_.iceServerTransports).then(
function(iceServers) {
var servers = this.params_.peerConnectionConfig.iceServers;
this.params_.peerConnectionConfig.iceServers =
servers.concat(iceServers);
}.bind(this)).catch(function(error) {
if (this.onturnstatusmessage) {
var subject =
encodeURIComponent('AppRTC demo ICE servers not working');
this.onturnstatusmessage(
'No TURN server; unlikely that media will traverse networks. ' +
'If this persists please ' +
'<a href="mailto:discuss-webrtc@googlegroups.com?' +
'subject=' + subject + '">' +
'report it to discuss-webrtc@googlegroups.com</a>.');
}
trace(error.message);
}.bind(this));
} else {
iceServerPromise = Promise.resolve();
}
return iceServerPromise;
};
Call.prototype.onUserMediaSuccess_ = function(stream) {
this.localStream_ = stream;
if (this.onlocalstreamadded) {
this.onlocalstreamadded(stream);
}
};
Call.prototype.onUserMediaError_ = function(error) {
var errorMessage = 'Failed to get access to local media. Error name was ' +
error.name + '. Continuing without sending a stream.';
this.onError_('getUserMedia error: ' + errorMessage);
this.errorMessageQueue_.push(error);
alert(errorMessage);
};
Call.prototype.maybeCreatePcClientAsync_ = function() {
return new Promise(function(resolve, reject) {
if (this.pcClient_) {
resolve();
return;
}
if (typeof RTCPeerConnection.generateCertificate === 'function') {
var certParams = {name: 'ECDSA', namedCurve: 'P-256'};
RTCPeerConnection.generateCertificate(certParams)
.then(function(cert) {
trace('ECDSA certificate generated successfully.');
this.params_.peerConnectionConfig.certificates = [cert];
this.createPcClient_();
resolve();
}.bind(this))
.catch(function(error) {
trace('ECDSA certificate generation failed.');
reject(error);
});
} else {
this.createPcClient_();
resolve();
}
}.bind(this));
};
Call.prototype.createPcClient_ = function() {
this.pcClient_ = new PeerConnectionClient(this.params_, this.startTime);
this.pcClient_.onsignalingmessage = this.sendSignalingMessage_.bind(this);
this.pcClient_.onremotehangup = this.onremotehangup;
this.pcClient_.onremotesdpset = this.onremotesdpset;
this.pcClient_.onremotestreamadded = this.onremotestreamadded;
this.pcClient_.onsignalingstatechange = this.onsignalingstatechange;
this.pcClient_.oniceconnectionstatechange = this.oniceconnectionstatechange;
this.pcClient_.onnewicecandidate = this.onnewicecandidate;
this.pcClient_.onerror = this.onerror;
trace('Created PeerConnectionClient');
};
Call.prototype.startSignaling_ = function() {
trace('Starting signaling.');
if (this.isInitiator() && this.oncallerstarted) {
this.oncallerstarted(this.params_.roomId, this.params_.roomLink);
}
this.startTime = window.performance.now();
this.maybeCreatePcClientAsync_()
.then(function() {
if (this.localStream_) {
trace('Adding local stream.');
this.pcClient_.addStream(this.localStream_);
}
if (this.params_.isInitiator) {
this.pcClient_.startAsCaller(this.params_.offerOptions);
} else {
this.pcClient_.startAsCallee(this.params_.messages);
}
}.bind(this))
.catch(function(e) {
this.onError_('Create PeerConnection exception: ' + e);
alert('Cannot create RTCPeerConnection: ' + e.message);
}.bind(this));
};
Call.prototype.joinRoom_ = function() {
return new Promise(function(resolve, reject) {
if (!this.params_.roomId) {
reject(Error('Missing room id.'));
}
var path = this.roomServer_ + '/join/' +
this.params_.roomId + window.location.search;
sendAsyncUrlRequest('POST', path).then(function(response) {
var responseObj = parseJSON(response);
if (!responseObj) {
reject(Error('Error parsing response JSON.'));
return;
}
if (responseObj.result !== 'SUCCESS') {
reject(Error('Registration error: ' + responseObj.result));
if (responseObj.result === 'FULL') {
var getPath = this.roomServer_ + '/r/' +
this.params_.roomId + window.location.search;
window.location.assign(getPath);
}
return;
}
trace('Joined the room.');
resolve(responseObj.params);
}.bind(this)).catch(function(error) {
reject(Error('Failed to join the room: ' + error.message));
return;
}.bind(this));
}.bind(this));
};
Call.prototype.onRecvSignalingChannelMessage_ = function(msg) {
this.maybeCreatePcClientAsync_()
.then(this.pcClient_.receiveSignalingMessage(msg));
};
Call.prototype.sendSignalingMessage_ = function(message) {
var msgString = JSON.stringify(message);
if (this.params_.isInitiator) {
var path = this.roomServer_ + '/message/' + this.params_.roomId +
'/' + this.params_.clientId + window.location.search;
var xhr = new XMLHttpRequest();
xhr.open('POST', path, true);
xhr.send(msgString);
trace('C->GAE: ' + msgString);
} else {
this.channel_.send(msgString);
}
};
Call.prototype.onError_ = function(message) {
if (this.onerror) {
this.onerror(message);
}
};