WebRTC audio/video call and conferencing server.
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

/*
* 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;
});