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