You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
416 lines
13 KiB
416 lines
13 KiB
/* |
|
* Spreed WebRTC. |
|
* Copyright (C) 2013-2015 struktur AG |
|
* |
|
* This file is part of Spreed WebRTC. |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU Affero General Public License as published by |
|
* the Free Software Foundation, either version 3 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU Affero General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU Affero General Public License |
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
* |
|
*/ |
|
|
|
"use strict"; |
|
define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection'], function($, _, utils, PeerConnection) { |
|
|
|
var PeerCall = function(webrtc, id, from, to) { |
|
|
|
this.webrtc = webrtc; |
|
this.id = id; |
|
this.from = from; |
|
this.to = to; |
|
|
|
this.e = $({}) // events |
|
|
|
this.mediaConstraints = $.extend(true, {}, this.webrtc.settings.mediaConstraints); |
|
this.pcConfig = $.extend(true, {}, this.webrtc.settings.pcConfig); |
|
this.pcConstraints = $.extend(true, {}, this.webrtc.settings.pcConstraints); |
|
this.sdpParams = $.extend(true, {}, this.webrtc.settings.sdpParams); |
|
this.offerOptions = $.extend(true, {}, this.webrtc.settings.offerOptions); |
|
|
|
this.peerconnection = null; |
|
this.pendingCandidates = []; |
|
this.datachannels = {}; |
|
this.streams = {}; |
|
|
|
this.negotiationNeeded = false; |
|
this.initiate = false; |
|
this.closed = false; |
|
|
|
}; |
|
|
|
PeerCall.prototype.isOutgoing = function() { |
|
return !!this.from; |
|
}; |
|
|
|
PeerCall.prototype.setInitiate = function(initiate) { |
|
this.initiate = !! initiate; |
|
//console.log("Set initiate", this.initiate, this); |
|
}; |
|
|
|
PeerCall.prototype.getStreamId = function(stream) { |
|
var streamid = stream.id; |
|
var id = this.id + "-" + streamid; |
|
if (!this.streams.hasOwnProperty(streamid) || this.streams[streamid] === stream) { |
|
this.streams[streamid] = stream; |
|
} else { |
|
console.warn("A different stream is already registered, not replacing", stream, this.streams[streamid]) |
|
} |
|
//console.log("Created stream ID", id); |
|
return id; |
|
}; |
|
|
|
PeerCall.prototype.createPeerConnection = function(success_cb, error_cb) { |
|
|
|
var peerconnection = this.peerconnection = new PeerConnection(this.webrtc, this); |
|
if (success_cb && peerconnection.pc) { |
|
success_cb(peerconnection); |
|
} |
|
if (error_cb && !peerconnection.pc) { |
|
// TODO(longsleep): Check if this can happen? |
|
error_cb(peerconnection); |
|
} |
|
while (this.pendingCandidates.length > 0) { |
|
var candidate = this.pendingCandidates.shift(); |
|
this.addIceCandidate(candidate); |
|
} |
|
return peerconnection; |
|
|
|
}; |
|
|
|
PeerCall.prototype.createOffer = function(cb) { |
|
|
|
var options = this.offerOptions; |
|
console.log('Creating offer with options: \n' + |
|
' \'' + JSON.stringify(options, null, '\t') + '\'.', this.negotiationNeeded); |
|
this.peerconnection.createOffer(_.bind(this.onCreateAnswerOffer, this, cb), _.bind(this.onErrorAnswerOffer, this), options); |
|
|
|
}; |
|
|
|
PeerCall.prototype.createAnswer = function(cb) { |
|
|
|
console.log("Creating answer.", this.negotiationNeeded); |
|
this.peerconnection.createAnswer(_.bind(this.onCreateAnswerOffer, this, cb), _.bind(this.onErrorAnswerOffer, this)); |
|
|
|
}; |
|
|
|
PeerCall.prototype.onCreateAnswerOffer = function(cb, sessionDescription) { |
|
|
|
if (sessionDescription.type === "answer") { |
|
// We processed the incoming Offer by creating an answer, so it's safe |
|
// to create own Offers to perform renegotiation. |
|
this.peerconnection.setReadyForRenegotiation(true); |
|
} |
|
|
|
this.setLocalSdp(sessionDescription); |
|
|
|
// Convert to object to allow custom property injection. |
|
var sessionDescriptionObj = sessionDescription; |
|
if (sessionDescriptionObj.toJSON) { |
|
sessionDescriptionObj = JSON.parse(JSON.stringify(sessionDescriptionObj)); |
|
} |
|
console.log("Created offer/answer", JSON.stringify(sessionDescriptionObj, null, "\t")); |
|
|
|
// Allow external session description modifications. |
|
this.e.triggerHandler("sessiondescription", [sessionDescriptionObj, this]); |
|
// Always set local description. |
|
this.peerconnection.setLocalDescription(sessionDescription, _.bind(function() { |
|
console.log("Set local session description.", sessionDescription, this); |
|
if (cb) { |
|
cb(sessionDescriptionObj, this); |
|
} |
|
}, this), _.bind(function(err) { |
|
console.error("Set local session description failed", err); |
|
this.close(); |
|
this.e.triggerHandler("error", "failed_peerconnection_setup"); |
|
}, this)); |
|
|
|
if (this.negotiationNeeded) { |
|
this.negotiationNeeded = false; |
|
console.log("Negotiation complete.", this); |
|
} |
|
|
|
}; |
|
|
|
PeerCall.prototype.onErrorAnswerOffer = function(event) { |
|
|
|
console.error("Failed to create answer/offer", event); |
|
|
|
// Even though the Offer/Answer could not be created, we now allow |
|
// to create own Offers to perform renegotiation again. |
|
this.peerconnection.setReadyForRenegotiation(true); |
|
|
|
}; |
|
|
|
PeerCall.prototype.setRemoteDescription = function(sessionDescription, cb) { |
|
|
|
var peerconnection = this.peerconnection; |
|
if (!peerconnection) { |
|
console.log("Got a remote description but not connected -> ignored."); |
|
return; |
|
} |
|
|
|
this.setRemoteSdp(sessionDescription); |
|
|
|
if (sessionDescription.type === "offer") { |
|
// Prevent creation of Offer messages to renegotiate streams while the |
|
// remote Offer is being processed. |
|
peerconnection.setReadyForRenegotiation(false); |
|
} |
|
|
|
peerconnection.setRemoteDescription(sessionDescription, _.bind(function() { |
|
console.log("Set remote session description.", sessionDescription, this); |
|
if (cb) { |
|
cb(sessionDescription, this); |
|
} |
|
// NOTE(longsleep): There are several szenarios where onaddstream is never fired, when |
|
// the peer does not provide a certain stream type (eg. has no camera). See |
|
// for example https://bugzilla.mozilla.org/show_bug.cgi?id=998546. For this |
|
// reason we always trigger onRemoteStream added for all streams which are available |
|
// after the remote SDP was set successfully. |
|
_.defer(_.bind(function() { |
|
var streams = 0; |
|
_.each(peerconnection.getRemoteStreams(), _.bind(function(stream) { |
|
if (!this.streams.hasOwnProperty(stream.id) && (stream.getAudioTracks().length > 0 || stream.getVideoTracks().length > 0)) { |
|
// NOTE(longsleep): Add stream here when it has at least one audio or video track, to avoid FF >= 33 to add it multiple times. |
|
console.log("Adding stream after remote SDP success.", stream); |
|
this.onRemoteStreamAdded(stream); |
|
streams++; |
|
} |
|
}, this)); |
|
if (streams === 0 && (this.offerOptions.offerToReceiveAudio || this.offerOptions.offerToReceiveVideo)) { |
|
// We assume that we will eventually receive a stream, so we trigger the event to let the UI prepare for it. |
|
this.e.triggerHandler("remoteStreamAdded", [null, this]); |
|
} |
|
}, this)); |
|
}, this), _.bind(function(err) { |
|
console.error("Set remote session description failed", err); |
|
this.close(); |
|
this.e.triggerHandler("error", "failed_peerconnection_setup"); |
|
}, this)); |
|
|
|
}; |
|
|
|
PeerCall.prototype.setLocalSdp = function(sessionDescription) { |
|
|
|
var params = this.sdpParams; |
|
sessionDescription.sdp = utils.maybePreferAudioReceiveCodec(sessionDescription.sdp, params); |
|
sessionDescription.sdp = utils.maybePreferVideoReceiveCodec(sessionDescription.sdp, params); |
|
sessionDescription.sdp = utils.maybeSetAudioReceiveBitRate(sessionDescription.sdp, params); |
|
sessionDescription.sdp = utils.maybeSetVideoReceiveBitRate(sessionDescription.sdp, params); |
|
// Apply workarounds. |
|
sessionDescription.sdp = utils.fixLocal(sessionDescription.sdp, params); |
|
|
|
}; |
|
|
|
PeerCall.prototype.setRemoteSdp = function(sessionDescription) { |
|
|
|
var params = this.sdpParams; |
|
sessionDescription.sdp = utils.maybeSetOpusOptions(sessionDescription.sdp, params); |
|
sessionDescription.sdp = utils.maybePreferAudioSendCodec(sessionDescription.sdp, params); |
|
sessionDescription.sdp = utils.maybePreferVideoSendCodec(sessionDescription.sdp, params); |
|
sessionDescription.sdp = utils.maybeSetAudioSendBitRate(sessionDescription.sdp, params); |
|
sessionDescription.sdp = utils.maybeSetVideoSendBitRate(sessionDescription.sdp, params); |
|
sessionDescription.sdp = utils.maybeSetVideoSendInitialBitRate(sessionDescription.sdp, params); |
|
// Apply workarounds. |
|
sessionDescription.sdp = utils.fixRemote(sessionDescription.sdp, params); |
|
|
|
}; |
|
|
|
PeerCall.prototype.onIceCandidate = function(event) { |
|
if (event.candidate) { |
|
//console.log("ice candidate", event.candidate); |
|
var payload = { |
|
type: 'candidate', |
|
sdpMLineIndex: event.candidate.sdpMLineIndex, |
|
sdpMid: event.candidate.sdpMid, |
|
candidate: event.candidate.candidate |
|
}; |
|
// Allow external payload modifications. |
|
this.e.triggerHandler("icecandidate", [payload, this]); |
|
// Send it. |
|
// TODO(longsleep): This id needs to be different for PeerXfers. |
|
// XXX(longsleep): This seems to be breaking conferences when this.to and not this.id. |
|
this.webrtc.api.sendCandidate(this.to, payload); |
|
//console.log("Sent candidate", event.candidate.sdpMid, event.candidate.sdpMLineIndex, event.candidate.candidate); |
|
} else { |
|
console.log('End of candidates.'); |
|
} |
|
}; |
|
|
|
PeerCall.prototype.onSignalingStateChange = function(signalingState) { |
|
|
|
this.e.triggerHandler("signalingStateChange", [signalingState, this]); |
|
|
|
}; |
|
|
|
PeerCall.prototype.onIceConnectionStateChange = function(iceConnectionState) { |
|
|
|
this.e.triggerHandler("connectionStateChange", [iceConnectionState, this]); |
|
|
|
}; |
|
|
|
PeerCall.prototype.onRemoteStreamAdded = function(stream) { |
|
|
|
var id = stream.id; |
|
if (this.streams.hasOwnProperty(id)) { |
|
return; |
|
} |
|
this.streams[id] = stream; |
|
this.e.triggerHandler("remoteStreamAdded", [stream, this]); |
|
|
|
}; |
|
|
|
PeerCall.prototype.onRemoteStreamRemoved = function(stream) { |
|
|
|
this.e.triggerHandler("remoteStreamRemoved", [stream, this]); |
|
if (stream) { |
|
delete this.streams[stream.id]; |
|
} |
|
|
|
}; |
|
|
|
PeerCall.prototype.onNegotiationNeeded = function() { |
|
if (!this.peerconnection.readyForRenegotiation) { |
|
console.log("PeerConnection is not ready for renegotiation yet", this); |
|
return; |
|
} |
|
|
|
if (!this.negotiationNeeded) { |
|
this.negotiationNeeded = true; |
|
console.log("Negotiation needed.", this); |
|
this.e.triggerHandler("negotiationNeeded", [this]); |
|
} |
|
|
|
}; |
|
|
|
PeerCall.prototype.addIceCandidate = function(candidate) { |
|
|
|
if (this.closed) { |
|
// Avoid errors when still receiving candidates but closed. |
|
return; |
|
} |
|
if (!this.peerconnection) { |
|
this.pendingCandidates.push(candidate); |
|
return; |
|
} |
|
this.peerconnection.addIceCandidate(candidate, function() { |
|
//console.log("Remote candidate added successfully.", candidate); |
|
}, function(error) { |
|
console.warn("Failed to add remote candidate:", error, candidate); |
|
}); |
|
|
|
}; |
|
|
|
PeerCall.prototype.onDatachannel = function(datachannel) { |
|
|
|
//console.log("onDatachannel", datachannel); |
|
var label = datachannel.label; |
|
if (this.datachannels.hasOwnProperty(label)) { |
|
console.warn("Received duplicated datachannel label", label, datachannel, this.datachannels); |
|
return; |
|
} |
|
// Remember it for correct cleanups. |
|
this.datachannels[label] = datachannel; |
|
this.e.triggerHandler("datachannel", ["new", datachannel, this]); |
|
|
|
}; |
|
|
|
PeerCall.prototype.onDatachannelDefault = function(state, datachannel) { |
|
|
|
if (state === "open") { |
|
//console.log("Data ready", this); |
|
_.defer(_.bind(function() { |
|
this.e.triggerHandler("dataReady", [this]); |
|
}, this)); |
|
} |
|
this.e.triggerHandler("datachannel.default", [state, datachannel, this]); |
|
|
|
}; |
|
|
|
PeerCall.prototype.onMessage = function(event) { |
|
|
|
//console.log("Peer to peer channel message", event); |
|
var data = event.data; |
|
|
|
if (data instanceof Blob) { |
|
console.warn("Blob data received - not implemented.", data); |
|
} else if (data instanceof ArrayBuffer) { |
|
console.warn("ArrayBuffer data received - not implemented.", data); |
|
} else if (typeof data === "string") { |
|
if (data.charCodeAt(0) === 2) { |
|
// Ignore whatever shit is this (sent by Chrome 34 and FireFox). Feel free to |
|
// change the comment it you know what this is. |
|
return; |
|
} |
|
//console.log("Datachannel message", [event.data, event.data.length, event.data.charCodeAt(0)]); |
|
var msg = JSON.parse(event.data); |
|
this.webrtc.api.received({ |
|
Type: msg.Type, |
|
Data: msg, |
|
To: this.webrtc.api.id, |
|
From: this.id, |
|
p2p: true |
|
}); //XXX(longsleep): use event for this? |
|
} else { |
|
console.warn("Unknow data type received -> igored", typeof data, [data]); |
|
} |
|
|
|
}; |
|
|
|
PeerCall.prototype.getDatachannel = function(label, init, create_cb) { |
|
|
|
//console.log("getDatachannel", label); |
|
var datachannel = this.datachannels[label]; |
|
if (!datachannel) { |
|
console.log("Creating new datachannel", label, init); |
|
datachannel = this.peerconnection.createDatachannel(label, init); |
|
if (create_cb) { |
|
create_cb(datachannel); |
|
} |
|
} |
|
return datachannel; |
|
|
|
}; |
|
|
|
PeerCall.prototype.close = function() { |
|
|
|
if (this.closed) { |
|
return; |
|
} |
|
|
|
this.closed = true; |
|
|
|
_.each(this.datachannels, function(datachannel) { |
|
datachannel.close(); |
|
}); |
|
this.datachannels = {}; |
|
|
|
if (this.peerconnection) { |
|
this.peerconnection.close(); |
|
this.peerconnection = null; |
|
} |
|
|
|
// Trigger event for all previously added streams. |
|
_.each(this.streams, _.bind(function(stream, id) { |
|
this.e.triggerHandler("remoteStreamRemoved", [stream, this]); |
|
}, this)); |
|
this.streams = {}; |
|
|
|
console.log("Peercall close", this); |
|
this.e.triggerHandler("closed", [this]); |
|
|
|
}; |
|
|
|
return PeerCall; |
|
|
|
});
|
|
|