'use strict';
var PeerConnectionClient = function(params, startTime) {
this.params_ = params;
this.startTime_ = startTime;
trace('Creating RTCPeerConnnection with:\n' +
' config: \'' + JSON.stringify(params.peerConnectionConfig) + '\';\n' +
' constraints: \'' + JSON.stringify(params.peerConnectionConstraints) +
'\'.');
this.pc_ = new RTCPeerConnection(
params.peerConnectionConfig, params.peerConnectionConstraints);
this.pc_.onicecandidate = this.onIceCandidate_.bind(this);
this.pc_.ontrack = this.onRemoteStreamAdded_.bind(this);
this.pc_.onremovestream = trace.bind(null, 'Remote stream removed.');
this.pc_.onsignalingstatechange = this.onSignalingStateChanged_.bind(this);
this.pc_.oniceconnectionstatechange =
this.onIceConnectionStateChanged_.bind(this);
this.pc_.onnegotiationneeded = this.onNegotiationNeeded_.bind(this);
window.dispatchEvent(new CustomEvent('pccreated', {
detail: {
pc: this,
time: new Date(),
userId: this.params_.roomId + (this.isInitiator_ ? '-0' : '-1'),
sessionId: this.params_.roomId
}
}));
this.hasRemoteSdp_ = false;
this.isDrainingMessages_ = false;
this.messageQueue_ = [];
this.isInitiator_ = false;
this.started_ = false;
this.sfuMode_ = params.sfuMode === true;
this.pendingRemoteRequestId_ = null;
this.perfectNegotiation_ = params.signalingVersion === 2;
this.polite_ = false;
this.makingOffer_ = false;
this.ignoreOffer_ = false;
this.isSettingRemoteAnswerPending_ = false;
this.offerRevision_ = 0;
this.renegotiationPending_ = false;
this.onerror = null;
this.oniceconnectionstatechange = null;
this.onnewicecandidate = null;
this.onremotehangup = null;
this.onremotesdpset = null;
this.onremotestreamadded = null;
this.onremotetrack = null;
this.onsfunegotiated = null;
this.onsignalingmessage = null;
this.onsignalingstatechange = null;
};
PeerConnectionClient.DEFAULT_SDP_OFFER_OPTIONS_ = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1,
voiceActivityDetection: false
};
PeerConnectionClient.prototype.addStream = function(stream) {
if (!this.pc_) {
return;
}
stream.getTracks().forEach(function(track) {
this.pc_.addTrack(track, stream);
}.bind(this));
};
PeerConnectionClient.prototype.startAsCaller = function(offerOptions) {
if (!this.pc_) {
return false;
}
if (this.started_) {
return false;
}
this.isInitiator_ = true;
this.started_ = true;
this.polite_ = this.perfectNegotiation_ && this.sfuMode_;
var constraints = mergeConstraints(
PeerConnectionClient.DEFAULT_SDP_OFFER_OPTIONS_, offerOptions);
trace('Sending offer to peer, with constraints: \n\'' +
JSON.stringify(constraints) + '\'.');
this.makeOffer_(constraints)
.catch(this.onError_.bind(this, 'createOffer'));
return true;
};
PeerConnectionClient.prototype.startAsCallee = function(initialMessages) {
if (!this.pc_) {
return false;
}
if (this.started_) {
return false;
}
this.isInitiator_ = false;
this.started_ = true;
this.polite_ = this.perfectNegotiation_;
if (initialMessages && initialMessages.length > 0) {
for (var i = 0, len = initialMessages.length; i < len; i++) {
this.receiveSignalingMessage(initialMessages[i]);
}
return true;
}
if (this.messageQueue_.length > 0) {
this.drainMessageQueue_();
}
return true;
};
PeerConnectionClient.prototype.receiveSignalingMessage = function(message) {
var messageObj = parseJSON(message);
if (!messageObj) {
return;
}
if (this.perfectNegotiation_ &&
(messageObj.type === 'offer' || messageObj.type === 'answer')) {
this.hasRemoteSdp_ = true;
this.messageQueue_.push(messageObj);
} else if (this.sfuMode_ && messageObj.type === 'offer') {
this.hasRemoteSdp_ = true;
this.messageQueue_.push(messageObj);
} else if ((this.isInitiator_ && messageObj.type === 'answer') ||
(!this.isInitiator_ && messageObj.type === 'offer')) {
this.hasRemoteSdp_ = true;
this.messageQueue_.unshift(messageObj);
} else if (messageObj.type === 'candidate' ||
messageObj.type === 'end-of-candidates') {
this.messageQueue_.push(messageObj);
} else if (messageObj.type === 'bye') {
if (this.onremotehangup) {
this.onremotehangup();
}
}
this.drainMessageQueue_();
};
PeerConnectionClient.prototype.close = function() {
if (!this.pc_) {
return;
}
this.pc_.close();
window.dispatchEvent(new CustomEvent('pcclosed', {
detail: {
pc: this,
time: new Date(),
}
}));
this.pc_ = null;
};
PeerConnectionClient.prototype.getPeerConnectionStates = function() {
if (!this.pc_) {
return null;
}
return {
'signalingState': this.pc_.signalingState,
'iceGatheringState': this.pc_.iceGatheringState,
'iceConnectionState': this.pc_.iceConnectionState
};
};
PeerConnectionClient.prototype.getPeerConnectionStats = function(callback) {
if (!this.pc_) {
return;
}
this.pc_.getStats(null)
.then(callback);
};
PeerConnectionClient.prototype.setupCodecs_ = function() {
if (!this.sfuMode_ || !this.pc_ ||
typeof RTCRtpSender === 'undefined' ||
!RTCRtpSender.getCapabilities) {
return;
}
var videoCodecs = RTCRtpSender.getCapabilities('video').codecs
.filter(function(codec) {
return codec.mimeType.toLowerCase() === 'video/vp8';
});
var audioCodecs = RTCRtpSender.getCapabilities('audio').codecs
.filter(function(codec) {
return codec.mimeType.toLowerCase() === 'audio/opus';
});
this.pc_.getTransceivers().forEach(function(transceiver) {
if (!transceiver.setCodecPreferences) {
return;
}
var kind = (transceiver.sender && transceiver.sender.track &&
transceiver.sender.track.kind) ||
(transceiver.receiver && transceiver.receiver.track &&
transceiver.receiver.track.kind);
try {
if (kind === 'video' && videoCodecs.length > 0) {
transceiver.setCodecPreferences(videoCodecs);
} else if (kind === 'audio' && audioCodecs.length > 0) {
transceiver.setCodecPreferences(audioCodecs);
}
} catch (e) {
trace('setCodecPreferences failed: ' + e);
}
});
};
PeerConnectionClient.prototype.doAnswer_ = function() {
trace('Sending answer to peer.');
this.setupCodecs_();
return this.pc_.createAnswer()
.then(this.setLocalSdpAndNotify_.bind(this))
.then(this.notifySfuNegotiated_.bind(this));
};
PeerConnectionClient.prototype.notifySfuNegotiated_ = function() {
if (!this.sfuMode_ || !this.pc_ || !this.onsfunegotiated) {
return;
}
var live = {};
this.pc_.getTransceivers().forEach(function(transceiver) {
var track = transceiver.receiver && transceiver.receiver.track;
if (track && (transceiver.currentDirection === 'recvonly' ||
transceiver.currentDirection === 'sendrecv')) {
live[track.id] = true;
}
});
this.onsfunegotiated(live);
};
PeerConnectionClient.prototype.makeOffer_ = function(offerOptions) {
if (!this.pc_ || this.makingOffer_) {
return Promise.resolve();
}
this.makingOffer_ = true;
this.setupCodecs_();
var revision = ++this.offerRevision_;
return this.pc_.createOffer(offerOptions).then(function(offer) {
if (revision !== this.offerRevision_ ||
(this.perfectNegotiation_ && this.pc_.signalingState !== 'stable')) {
trace('Discarding superseded local offer.');
return;
}
return this.setLocalSdpAndNotify_(offer);
}.bind(this)).finally(function() {
this.makingOffer_ = false;
this.maybeRenegotiate_();
}.bind(this));
};
PeerConnectionClient.prototype.setLocalSdpAndNotify_ =
function(sessionDescription) {
sessionDescription.sdp = maybeSetOpusOptions(
sessionDescription.sdp,
this.params_);
sessionDescription.sdp = maybePreferAudioReceiveCodec(
sessionDescription.sdp,
this.params_);
sessionDescription.sdp = maybePreferVideoReceiveCodec(
sessionDescription.sdp,
this.params_);
sessionDescription.sdp = maybeSetAudioReceiveBitRate(
sessionDescription.sdp,
this.params_);
sessionDescription.sdp = maybeSetVideoReceiveBitRate(
sessionDescription.sdp,
this.params_);
sessionDescription.sdp = maybeRemoveVideoFec(
sessionDescription.sdp,
this.params_);
return this.pc_.setLocalDescription(sessionDescription)
.then(trace.bind(null, 'Set session description success.'))
.then(function() {
if (this.onsignalingmessage) {
var message = {
sdp: sessionDescription.sdp,
type: sessionDescription.type
};
if (sessionDescription.type === 'answer' &&
this.pendingRemoteRequestId_ !== null) {
message.requestid = this.pendingRemoteRequestId_.toString();
this.pendingRemoteRequestId_ = null;
}
this.onsignalingmessage(message);
}
}.bind(this));
};
PeerConnectionClient.prototype.setRemoteSdp_ = function(message) {
message.sdp = maybeSetOpusOptions(message.sdp, this.params_);
message.sdp = maybePreferAudioSendCodec(message.sdp, this.params_);
message.sdp = maybePreferVideoSendCodec(message.sdp, this.params_);
message.sdp = maybeSetAudioSendBitRate(message.sdp, this.params_);
message.sdp = maybeSetVideoSendBitRate(message.sdp, this.params_);
message.sdp = maybeSetVideoSendInitialBitRate(message.sdp, this.params_);
message.sdp = maybeRemoveVideoFec(message.sdp, this.params_);
return this.pc_.setRemoteDescription(new RTCSessionDescription(message))
.then(this.onSetRemoteDescriptionSuccess_.bind(this));
};
PeerConnectionClient.prototype.onSetRemoteDescriptionSuccess_ = function() {
trace('Set remote session description success.');
var hasRemoteVideo = this.pc_.getReceivers().some(function(receiver) {
return receiver.track && receiver.track.kind === 'video';
});
if (this.onremotesdpset) {
this.onremotesdpset(hasRemoteVideo);
}
};
PeerConnectionClient.prototype.processSignalingMessage_ = function(message) {
if (this.perfectNegotiation_ &&
(message.type === 'offer' || message.type === 'answer')) {
return this.processV2Description_(message);
} else if (message.type === 'offer' && this.sfuMode_) {
return this.answerSfuOffer_(message);
} else if (message.type === 'offer' && !this.isInitiator_) {
if (this.pc_.signalingState !== 'stable') {
trace('ERROR: remote offer received in unexpected state: ' +
this.pc_.signalingState);
return Promise.resolve();
}
return this.setRemoteSdp_(message).then(this.doAnswer_.bind(this));
} else if (message.type === 'answer' && this.isInitiator_) {
if (this.pc_.signalingState !== 'have-local-offer') {
trace('ERROR: remote answer received in unexpected state: ' +
this.pc_.signalingState);
return Promise.resolve();
}
return this.setRemoteSdp_(message);
} else if (message.type === 'candidate') {
if (this.perfectNegotiation_ && this.ignoreOffer_) {
return Promise.resolve();
}
var candidate = new RTCIceCandidate({
sdpMLineIndex: message.label,
candidate: message.candidate
});
this.recordIceCandidate_('Remote', candidate);
return this.pc_.addIceCandidate(candidate)
.then(trace.bind(null, 'Remote candidate added successfully.'));
} else if (message.type === 'end-of-candidates') {
if (this.perfectNegotiation_ && this.ignoreOffer_) {
return Promise.resolve();
}
return this.pc_.addIceCandidate(null)
.then(trace.bind(null, 'Remote end-of-candidates added successfully.'));
} else {
trace('WARNING: unexpected message: ' + JSON.stringify(message));
return Promise.resolve();
}
};
PeerConnectionClient.prototype.processV2Description_ = function(message) {
var isOffer = message.type === 'offer';
var readyForOffer = !this.makingOffer_ &&
(this.pc_.signalingState === 'stable' ||
this.isSettingRemoteAnswerPending_);
var offerCollision = isOffer && !readyForOffer;
this.ignoreOffer_ = !this.polite_ && offerCollision;
if (this.ignoreOffer_) {
trace('Ignoring colliding V2 offer as the impolite peer.');
return Promise.resolve();
}
if (offerCollision) {
++this.offerRevision_;
if (this.sfuMode_) {
this.renegotiationPending_ = true;
}
}
if (this.sfuMode_ && isOffer) {
this.pendingRemoteRequestId_ = message.requestid === undefined ?
null : message.requestid;
}
var ready = Promise.resolve();
if (offerCollision && this.pc_.signalingState !== 'stable') {
trace('Rolling back a local V2 offer as the polite peer.');
ready = this.pc_.setLocalDescription({type: 'rollback'});
}
this.isSettingRemoteAnswerPending_ = message.type === 'answer';
return ready.then(function() {
return this.setRemoteSdp_(message);
}.bind(this)).then(function() {
this.ignoreOffer_ = false;
if (isOffer) {
return this.doAnswer_();
}
}.bind(this)).finally(function() {
this.isSettingRemoteAnswerPending_ = false;
this.maybeRenegotiate_();
}.bind(this));
};
PeerConnectionClient.prototype.answerSfuOffer_ = function(message) {
var ready = Promise.resolve();
if (this.pc_.signalingState === 'have-local-offer') {
trace('Rolling back a local SFU offer to answer a subscribe offer.');
ready = this.pc_.setLocalDescription({type: 'rollback'});
} else if (this.pc_.signalingState !== 'stable') {
trace('WARNING: SFU offer received in unexpected state: ' +
this.pc_.signalingState);
return Promise.resolve();
}
this.pendingRemoteRequestId_ = message.requestid === undefined ?
null : message.requestid;
return ready.then(function() {
return this.setRemoteSdp_(message);
}.bind(this)).then(function() {
return this.doAnswer_();
}.bind(this));
};
PeerConnectionClient.prototype.onNegotiationNeeded_ = function() {
if (!this.sfuMode_ || !this.started_ || !this.pc_ ||
this.pc_.signalingState !== 'stable' || this.makingOffer_) {
return;
}
trace('SFU negotiation needed; publishing a fresh offer.');
this.makeOffer_(this.params_.offerOptions)
.catch(this.onError_.bind(this, 'createOffer'));
};
PeerConnectionClient.prototype.maybeRenegotiate_ = function() {
if (!this.renegotiationPending_ || !this.pc_ || this.makingOffer_ ||
this.pc_.signalingState !== 'stable') {
return;
}
this.renegotiationPending_ = false;
trace('Publishing after a polite SFU offer collision.');
this.makeOffer_(this.params_.offerOptions)
.catch(this.onError_.bind(this, 'createOffer'));
};
PeerConnectionClient.prototype.drainMessageQueue_ = function() {
if (!this.pc_ || !this.started_ || !this.hasRemoteSdp_ ||
this.isDrainingMessages_) {
return;
}
this.isDrainingMessages_ = true;
var messages = this.messageQueue_;
this.messageQueue_ = [];
var sequence = Promise.resolve();
messages.forEach(function(message) {
sequence = sequence.then(function() {
return this.processSignalingMessage_(message);
}.bind(this));
}.bind(this));
sequence.catch(this.onError_.bind(this, 'processSignalingMessage'))
.then(function() {
this.isDrainingMessages_ = false;
if (this.messageQueue_.length > 0) {
this.drainMessageQueue_();
}
}.bind(this));
};
PeerConnectionClient.prototype.onIceCandidate_ = function(event) {
if (event.candidate) {
if (this.filterIceCandidate_(event.candidate)) {
var message = {
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
};
if (this.onsignalingmessage) {
this.onsignalingmessage(message);
}
this.recordIceCandidate_('Local', event.candidate);
}
} else {
trace('End of candidates.');
if (this.onsignalingmessage) {
this.onsignalingmessage({type: 'end-of-candidates'});
}
}
};
PeerConnectionClient.prototype.onSignalingStateChanged_ = function() {
if (!this.pc_) {
return;
}
trace('Signaling state changed to: ' + this.pc_.signalingState);
if (this.onsignalingstatechange) {
this.onsignalingstatechange();
}
};
PeerConnectionClient.prototype.onIceConnectionStateChanged_ = function() {
if (!this.pc_) {
return;
}
trace('ICE connection state changed to: ' + this.pc_.iceConnectionState);
if (this.pc_.iceConnectionState === 'completed') {
trace('ICE complete time: ' +
(window.performance.now() - this.startTime_).toFixed(0) + 'ms.');
}
if (this.oniceconnectionstatechange) {
this.oniceconnectionstatechange();
}
};
PeerConnectionClient.prototype.filterIceCandidate_ = function(candidateObj) {
var candidateStr = candidateObj.candidate;
if (candidateStr.indexOf('tcp') !== -1) {
return false;
}
if (this.params_.peerConnectionConfig.iceTransports === 'relay' &&
iceCandidateType(candidateStr) !== 'relay') {
return false;
}
return true;
};
PeerConnectionClient.prototype.recordIceCandidate_ =
function(location, candidateObj) {
if (this.onnewicecandidate) {
this.onnewicecandidate(location, candidateObj.candidate);
}
};
PeerConnectionClient.prototype.onRemoteStreamAdded_ = function(event) {
if (this.sfuMode_) {
if (this.onremotetrack) {
this.onremotetrack(event);
}
return;
}
if (!this.onremotestreamadded) {
return;
}
if (event.streams && event.streams.length > 0) {
this.onremotestreamadded(event.streams[0]);
return;
}
if (event.track) {
this.onremotestreamadded(new MediaStream([event.track]));
}
};
PeerConnectionClient.prototype.onError_ = function(tag, error) {
if (this.onerror) {
this.onerror(tag + ': ' + error.toString());
}
};