/*global mozRTCPeerConnection:false, webkitRTCPeerConnection:false, webkitURL:false, RTCSessionDescription:true, RTCIceCandidate:true, User:false, UserConnection:false, log4javascript:false, toJSON:false, webkitMediaStream:false, assert:false, Queue:false, mozRTCSessionDescription:false, mozRTCIceCandidate:false, MediaStream:false */ var RTCPeerConnection = null; var getUserMedia = null; var attachMediaStream = null; var reattachMediaStream = null; var webrtcDetectedBrowser = null; var log = log4javascript.getLogger("PeerConnection"); log.setLevel(log4javascript.Level.ERROR); var appender = new log4javascript.BrowserConsoleAppender(); appender.setLayout(new log4javascript.PatternLayout("[%d{mm:ss}] %c - %m")); log.addAppender(appender); var apprtc = log4javascript.getLogger("apprtc"); appender = new log4javascript.BrowserConsoleAppender(); appender.setLayout(new log4javascript.PatternLayout("[%d{mm:ss}] %m")); apprtc.addAppender(appender); //apprtc.setLevel(log4javascript.Level.ERROR); // Based on https://code.google.com/p/webrtc-samples/source/detail?r=84 if (navigator.mozGetUserMedia) { apprtc.info("This appears to be Firefox browser"); webrtcDetectedBrowser = "firefox"; // The RTCPeerConnection object. RTCPeerConnection = mozRTCPeerConnection; // The RTCSessionDescription object. RTCSessionDescription = mozRTCSessionDescription; // The RTCIceCandidate object. RTCIceCandidate = mozRTCIceCandidate; // Get UserMedia (only difference is the prefix). // Code from Adam Barth. getUserMedia = navigator.mozGetUserMedia.bind(navigator); // Attach a media stream to an element. var attachMediaStream = function(element, stream) { "use strict"; element.mozSrcObject = stream; element.play(); }; reattachMediaStream = function(to, from) { "use strict"; apprtc.info("Reattaching media stream"); to.mozSrcObject = from.mozSrcObject; to.play(); }; // Fake get{Video,Audio}Tracks MediaStream.prototype.getVideoTracks = function() { "use strict"; return []; }; MediaStream.prototype.getAudioTracks = function() { "use strict"; return []; }; } else if (navigator.webkitGetUserMedia) { apprtc.info("This appears to be Chrome"); webrtcDetectedBrowser = "chrome"; // The RTCPeerConnection object. RTCPeerConnection = webkitRTCPeerConnection; // Get UserMedia (only difference is the prefix). // Code from Adam Barth. getUserMedia = navigator.webkitGetUserMedia.bind(navigator); // Attach a media stream to an element. var attachMediaStream = function(element, stream) { "use strict"; element.src = webkitURL.createObjectURL(stream); }; reattachMediaStream = function(to, from) { "use strict"; to.src = from.src; }; // The representation of tracks in a stream is changed in M26. // Unify them for earlier Chrome versions in the coexisting period. if (!webkitMediaStream.prototype.getVideoTracks) { webkitMediaStream.prototype.getVideoTracks = function() { "use strict"; return this.videoTracks; }; webkitMediaStream.prototype.getAudioTracks = function() { "use strict"; return this.audioTracks; }; } // New syntax of getXXXStreams method in M26. if (!webkitRTCPeerConnection.prototype.getLocalStreams) { webkitRTCPeerConnection.prototype.getLocalStreams = function() { "use strict"; return this.localStreams; }; webkitRTCPeerConnection.prototype.getRemoteStreams = function() { "use strict"; return this.remoteStreams; }; } } else throw new Error("Browser does not appear to be WebRTC-capable"); /* * WebRTC workflow: * * Initiator (New participant) * --------- * * 1) localStream = getUserMedia(); * 2) PeerConnection.addStream(localStream); * 3) sdp = PeerConnection.createOffer(); * 4) PeerConnection.setLocalDescription(sdp); * 5) createOutgoingConnection(); * 6) PeerConnection.addIceCandidate(0 or more times); * 7) remoteSdp = incomingConnection.sdp; * 8) PeerConnection.setRemoteDescription(remoteSdp) * * Receiver (Incoming connection) * -------- * * 1) localStream = getUserMedia(); * 7) remoteSdp = incomingConnection.sdp; * 2) PeerConnection.addStream(localStream); * 8) PeerConnection.setRemoteDescription(remoteSdp) * 3) sdp = PeerConnection.createAnswer(); * 4) PeerConnection.setLocalDescription(sdp); * 5) createOutgoingConnection(); * 6) PeerConnection.addIceCandidate(0 or more times); * * Q: Who offers and who answers? * A: The user who joins a call should send offers to users already in the call. * 1) Add user to participant list. * 2) Send offer to all other participants. * 3) When new participants arrive, do nothing (they'll send you an offer). * 4) Answer all incoming connections. * * According to "derf" on #webrtc at http://irc.w3.org/ * * Normal ICE http://tools.ietf.org/html/rfc5245 requires it to wait for no more data * Acting on candidates as they're gathered is called "trickle ice", and the details of that * are still being worked out in the standards bodies. * * Trickle ICE can be found at http://tools.ietf.org/html/draft-rescorla-mmusic-ice-trickle-01 * Anyway, 5245 Section 4 has details on how the candidate gathering proceeds. Section * 16 has recommendations for pacing and retransmission timeouts. * So you can use that to get a pretty good idea what is required. The minimum * retransmission timeout is 100 ms, for example, so a 300 ms timeout doesn't give you a lot * of time. */ /** * Returns a stream to a local device. If the peer wishes to share the stream, it must pass the same * reference into multiple connections as opposed to calling getLocalStream multiple times. * * @param {MediaStreamConstraints} audioConstraints true if the stream may contain audio * @param {MediaStreamConstraints} videoConstraints the video constraints * @param {NavigatorUserMediaSuccessCallback} onSuccess invoked on success * @param {NavigatorUserMediaErrorCallback} onFailure (optional) invoked on failure * @returns {undefined} * @see http://www.w3.org/TR/mediacapture-streams/#idl-def-MediaStreamConstraints * @see http://stackoverflow.com/a/13937187/14731 */ function getLocalStream(audioConstraints, videoConstraints, onSuccess, onFailure) { "use strict"; if (typeof(audioConstraints) !== "boolean") throw new TypeError("Invalid audioConstraints: " + audioConstraints); if (!videoConstraints) throw new TypeError("Invalid videoConstraints: " + videoConstraints); if (!onSuccess || typeof(onFailure) !== "function") throw new TypeError("Invalid onSuccess: " + onSuccess); if (onFailure && typeof(onFailure) !== "function") throw new TypeError("Invalid onFailure: " + onFailure); var log = log4javascript.getLogger("PeerConnection"); try { getUserMedia({"audio": audioConstraints, "video": videoConstraints}, onSuccess, onFailure); apprtc.info("Requested access to local media with videoConstraints:\n" + " \"" + JSON.stringify(videoConstraints) + "\""); } catch (e) { alert("getUserMedia() failed. Is this a WebRTC capable browser?"); log.warn("getUserMedia failed with exception: ", e.stack); // @see http://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermediaerror-and-navigatorusermediaerrorcallback if (onFailure) { onFailure( { code: 1 //PERMISSION_DENIED }); } } } /** * Invoked when a remote stream is added. * * @callback RemoteStreamAdded * @param {MediaStreamEvent} event the event * @returns {undefined} */ /** * Invoked when a remote stream is removed. * * @callback RemoteStreamRemoved * @param {MediaStreamEvent} event the event * @returns {undefined} */ /** * Event listeners that are invoked when a remote stream is added or removed. * * @typedef remoteStreamListener * @param {RemoteStreamAdded} onRemoteStreamAdded invoked when a remote stream is added * @param {RemoteStreamRemoved} onRemoteStreamRemoved invoked when a remote stream is removed * @returns {undefined} */ /** * Notified when the connection signaling state changes. * * @callback SignalingStateChangeListener * @param {MediaStreamEvent} event the event * @returns {undefined} */ /** * Creates a new connection. Each connection is associated with one local and one remote peer. * To connect to multiple remote peers, use multiple PeerConnections. * * @class A network connection between two peers. * @param {uri:user} localUser the local user * @param {uri:user} remoteUser the remote user * @property {uri:user} localUser the local user * @property {uri:user} remoteUser the remote user * @property {RemoteStreamListener[]} remoteStreamListeners notified when remote streams are added * or removed * @property {SignalingStateChangeListener[]} signalingStateChangeListeners notified when the * connection signaling state changes * * @returns {PeerConnection} the connection * @see http://stackoverflow.com/a/13937187/14731 */ function PeerConnection(localUser, remoteUser) { "use strict"; if (!(this instanceof PeerConnection)) { throw new Error("PeerConnection constructor may not be invoked directly. You must use the " + "\"new\" operator."); } if (!localUser || typeof(localUser) !== "string") throw new TypeError("Invalid localUser: " + localUser); if (!remoteUser || typeof(remoteUser) !== "string") throw new TypeError("Invalid remoteUser: " + remoteUser); if (localUser === remoteUser) throw new Error("localUser and remoteUser refer to the same user: " + localUser); var outgoingConnection; var log = log4javascript.getLogger("PeerConnection"); Object.defineProperty(this, "localUser", { value: localUser, enumerable: true }); Object.defineProperty(this, "remoteUser", { value: remoteUser, enumerable: true }); Object.defineProperty(this, "remoteStreamListeners", { value: [], writable: true, enumerable: true }); Object.defineProperty(this, "signalingStateChangeListeners", { value: [], writable: true, enumerable: true }); var config; if (webrtcDetectedBrowser === "firefox") { // Force the use of a number IP address for Firefox. if (webrtcDetectedBrowser === "firefox") { config = { "iceServers": [ { "url": "stun:23.21.150.121" } ] }; } } else { config = { "iceServers": [ { url: "stun:stun.l.google.com:19302" } ] }; } /** * Invoked if something in the browser changes that causes the RTCPeerConnection object to need * to initiate a new session description negotiation. * @param {Event} event the event * @returns {undefined} */ function onNegotiationNeeded(event) { log.info("Stream added or removed. Negotiation needed"); log.debug("event: ", toJSON(event)); } /** * Queues local ICE candidates. * * @param {RTCPeerConnectionIceEvent} event an event containing the local peer's ICE candidate * @returns {undefined} */ function queueLocalIceCandidates(event) { // We need to queue events for two reasons: // // 1. PeerConnection events may arrive before the associated UserConnection is created. // 2. We need to send the events in the same order we receive them, otherwise // "no more candidates" might arrive before the last candidate. localIceCandidates.push(event); } /** * Invoked every time the connection's signalingState changes. * * @param {Event} event the event * @returns {undefined} */ function onSignalingStateChange(event) { log.debug("signalingState: ", event.target.getSignalingState()); } /** * Invoked when a remote streamer is added. * * @param {MediaStreamEvent} event the event * @returns {undefined} */ function onRemoteStreamAdded(event) { log.info("Remote stream added."); } /** * Invoked when a remote stream is removed. * * @param {MediaStreamEvent} event the event * @returns {undefined} */ function onRemoteStreamRemoved(event) { log.info("Remote stream removed."); } /** * Invoked when a remote stream is removed. * * @param {Event} event the event * @returns {undefined} */ function onIceConnectionStateChanged(event) { log.debug("iceConnectionState changed: ", event.target.iceConnectionState); if (event.target.iceConnectionState === "disconnected") self.close(); } /** * Invoked when the ICE candidate gathering state has changed. * * @param {Event} event the event triggered by RTCPeerConnection * @returns {undefined} */ function onGatheringChange(event) { log.debug("iceGatheringState changed: ", event.target.iceGatheringState); } /** * Set Opus as the default audio codec if it's present. * * @param {RTCSessionDescription} sdp the session descriptor * @returns {string} the updated SDP */ function preferOpus(sdp) { /** * Places a codec first in an m-line. * * @param {string} mLine the m-line to parse * @param {type} codec the codec that should be the default * @returns {string} the resulting m-line */ function setDefaultCodec(mLine, codec) { var elements = mLine.split(" "); var newLine = []; var index = 0; for (var i = 0; i < elements.length; ++i) { if (index === 3) // Format of media starts from the fourth. newLine[index++] = codec; // Put target payload to the first. if (elements[i] !== codec) newLine[index++] = elements[i]; } return newLine.join(" "); } /** * Strip CN (Comfort Noise) attributes from the SDP because they aren't handled yet. * * @param {string[]} sdpLines an array of strings making up the sdp * @param {Number} mLineIndex the line to begin scanning from * @returns {string[]} the updated sdp lines * @see http://tools.ietf.org/html/rfc3389 */ function removeCN(sdpLines, mLineIndex) { var mLineElements = sdpLines[mLineIndex].split(" "); // Scan from end for the convenience of removing an item. for (var i = sdpLines.length - 1; i >= 0; --i) { var payload = getUniqueMatch(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); if (payload) { var cnPos = mLineElements.indexOf(payload); if (cnPos !== -1) { // Remove CN payload from m-line. mLineElements.splice(cnPos, 1); } // Remove CN line in sdp sdpLines.splice(i, 1); } } sdpLines[mLineIndex] = mLineElements.join(" "); return sdpLines; } var sdpLines = sdp.split("\r\n"); // Search for m-line var mLineIndex; var i; for (i = 0; i < sdpLines.length; ++i) { if (sdpLines[i].search("m=audio") !== -1) { mLineIndex = i; break; } } if (mLineIndex === null) return sdp; // If Opus is available, set it as the default in m-line for (i = 0; i < sdpLines.length; ++i) { if (sdpLines[i].search("opus/48000") !== -1) { var opusPayload = getUniqueMatch(sdpLines[i], /:(\d+) opus\/48000/i); if (opusPayload) sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload); break; } } // Remove CN in m-line and SDP. sdpLines = removeCN(sdpLines, mLineIndex); sdp = sdpLines.join("\r\n"); return sdp; } /** * Work around Chrome bug by inserting a=crypto line into offer by Firefox. * * @param {String} sdp the offer sdp * @return {String} the resulting sdp */ function addFakeCryptoIfNecessary(sdp) { var payload = getUniqueMatch(sdp, /a=crypto/i); if (!payload) return sdp; var cryptoLine = "a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:" + "BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\\r\\n"; // reverse find for multiple find and insert operations var index = sdp.lastIndexOf("c=IN", 0, sdp.length()); while (index !== -1) { sdp = sdp.substr(0, index) + cryptoLine + sdp.substring(index); index = sdp.lastIndexOf("c=IN", 0, index); } return sdp; } /** * Returns the result of a regular expression with a single result. * * @param {string} text the text to search * @param {string} pattern the regular expression to match * @returns {string} null unless a single result is found, in which case the matched string is * returned */ function getUniqueMatch(text, pattern) { var result = text.match(pattern); return (result && result.length === 2) ? result[1] : null; } /** * Create an outgoing connection. * * @param {MediaConstraints} constraints (optional) provides additional control over the offer * generated * @return {Future} a Future that returns {@link UserConnection} on success and {@link Error} on * failure */ this.createOffer = function(constraints) { if (constraints && typeof(constraints) !== "object") throw new TypeError("Invalid constraints: " + constraints); return new Future(function(future) { // Improve interoperability between Chrome and Firefox clients constraints = Object.merge(constraints, { "mandatory": { "MozDontOfferDataChannel": true }, "optional": [] }); // temporary measure to remove Moz* constraints in Chrome if (webrtcDetectedBrowser === "chrome") for (var prop in constraints.mandatory) if (prop.indexOf("Moz") !== -1) delete constraints.mandatory[prop]; apprtc.info("Sending offer to peer, with constraints: \n" + " \"" + JSON.stringify(constraints) + "\"."); log.debug("constraints: ", constraints); delegate.createOffer(function(sdp) { // Set Opus as the preferred codec in SDP if Opus is present log.info("3) peerConnection.createOffer"); log.debug("sdp: ", sdp.sdp); sdp.sdp = preferOpus(sdp.sdp); delegate.setLocalDescription(sdp, function() { log.info("4) setLocalDescription"); apprtc.info("C->S: " + JSON.stringify(sdp)); }, RtcToAjaxFailureAdapter(future)); }, RtcToAjaxFailureAdapter(future), constraints); }).then(function() { // After delegate.createOffer(), delegate.setLocalDescription()... return UserConnection.create(undefined, self.localUser, self.remoteUser, sdp.sdp). then(function(userConnection) { log.info("Offer sent to: ", self.remoteUser); self.setOutgoingConnection(userConnection); return userConnection; }); }); }; /** * Accept an incoming connection. * * @param {UserConnection} offer the connection associated with the offer * @param {MediaConstraints} constraints (optional) provides additional control over the offer * generated * @return {Future} a Future that returns {@link UserConnection} on success and {@link Error} on * failure */ this.createAnswer = function(offer, constraints) { if (!offer || offer.constructor !== UserConnection) throw new TypeError("Invalid offer: " + offer); return new Future(function(future) { var modifiedSdp = addFakeCryptoIfNecessary(offer.sdp); return self.setRemoteSdp(modifiedSdp, "offer").then(function() { apprtc.info("Sending answer to peer."); delegate.createAnswer(function(sdp) { switch (sdp.type) { case "answer": break; case "pranswer": return; default: throw new Error("Unexpected answer type: " + toJSON(sdp)); } // WebRTC defines type = "pranswer" but it seems to only get used by SIP implementations. // The event, as described by http://tools.ietf.org/html/draft-ietf-rtcweb-jsep-02, sounds // safe to ignore. // Also, according to http://www.ietf.org/mail-archive/web/rtcweb/current/msg05432.html // "pranswer" might get removed in the future. // Set Opus as the preferred codec in SDP if Opus is present sdp.sdp = preferOpus(sdp.sdp); delegate.setLocalDescription(sdp, function() { apprtc.info("C->S: " + JSON.stringify(sdp)); }, RtcToAjaxFailureAdapter(future)); }, RtcToAjaxFailureAdapter(future), constraints); }).then(function() { return UserConnection.create(offer.uri, self.localUser, self.remoteUser, sdp.sdp). then(function(userConnection) { log.info("Answer sent to: ", self.remoteUser); self.setOutgoingConnection(userConnection); return userConnection; }); }); }); }; /** * Sets the SDP describing the remote peer. * * @param {String} sdp a description of the remote peer's capabilities * @param {String} type the SDP type: ["offer", "answer"] * @param {function} onSuccess invoked once the SDP is applied. The return type is undefined. * @param {RTCPeerConnectionErrorCallback} onFailure invoked if the SDP is valid but cannot be * applied (e.g. due to insufficient resources) * @throws {Error} if the SDP is invalid * @returns {undefined} */ this.setRemoteSdp = function(sdp, type, onSuccess, onFailure) { apprtc.info("S->C: " + JSON.stringify( { sdp: sdp, type: type })); delegate.setRemoteDescription(new RTCSessionDescription( { type: type, sdp: sdp }), onSuccess, onFailure); }; /** * Sets the audio/video stream to share with remote peers. * * @param {LocalMediaStream} stream the local audio/video stream * @param {MediaConstraints} constraints (optional) constraints the outgoing stream * @returns {undefined} */ this.setLocalStream = function(stream, constraints) { if (!stream || stream.className !== "LocalMediaStream") throw new TypeError("Invalid stream: " + stream); if (constraints && typeof(constraints) !== "object") throw new TypeError("Invalid constraints: " + constraints); apprtc.info("Adding local stream."); delegate.addStream(stream, constraints); log.debug("stream: " + toJSON(stream)); log.debug("constraints: " + toJSON(constraints)); }; /** * Sets the incomingConnection property. * * @param {UserConnection} userConnection the new value * @return {undefined} */ this.setIncomingConnection = function(userConnection) { Object.defineProperty(this, "incomingConnection", { value: userConnection, enumerable: true }); /** * Queues remote ICE candidates. * * @param {IceCandidate} candidate an ICE candidate containing three keys: * candidate, mediaIndex, and mediaId. null if no further * candidates will be forthcoming. * @returns {undefined} */ function queueAndSendRemoteIceCandidates(candidate) { // We need to queue events for two reasons: // // 1. PeerConnection events may arrive before the associated UserConnection is created. // 2. We need to send the events in the same order we receive them, otherwise // "no more candidates" might arrive before the last candidate. remoteIceCandidates.push(candidate); if (remoteIceCandidates.length === 1) { // Fire an event if one isn't already in progress sendRemoteIceCandidate(); } } /** * Notifies the local peer of a remote ICE candidate. * * @returns {unresolved} */ function sendRemoteIceCandidate() { while (!remoteIceCandidates.isEmpty()) { var candidate = remoteIceCandidates.pop(); if (!candidate) { assert(remoteIceCandidates.isEmpty(), "Unexpected queue value: " + toJSON(remoteIceCandidates)); apprtc.info(userConnection.uri + ": S->C: No more ICE candidates"); } else { apprtc.info(userConnection.uri + ": S->C: " + JSON.stringify( { type: "candidate", label: candidate.mediaIndex, id: candidate.mediaId, candidate: candidate.candidate })); var iceCandidate = new RTCIceCandidate( { candidate: candidate.candidate, sdpMLineIndex: candidate.mediaIndex, sdpMid: candidate.mediaId }); delegate.addIceCandidate(iceCandidate); } } } // No need to monitor "counterpart" since we are the ones setting it. userConnection.onCounterpart = undefined; // Queue existing ICE candidates userConnection.iceCandidates.each(function(element) { remoteIceCandidates.push(element); return true; }); // Listen for future ICE candidates userConnection.onIceCandidate = queueAndSendRemoteIceCandidates; // Fire the first event and it'll process the rest of the queue sendRemoteIceCandidate(); }; /** * Sets the outgoingConnection property. * * @param {UserConnection} userConnection the new value * @return {undefined} */ this.setOutgoingConnection = function(userConnection) { /** * Invoked after sending an ICE candidate. * * @param {IceCandidate} candidate the ICE candidate * @return {undefined} */ function onIceCandidateSent(candidate) { log.debug(userConnection.uri + ": ICE candidate sent: ", toJSON(candidate)); localIceCandidates.pop(); // Check if the queue contains more candidates sendLocalIceCandidate(); } /** * Notify the remote peer of a new local ICE candidate. * * @param {RTCPeerConnectionIceEvent} event the event * @returns {unresolved} */ function queueAndSendLocalIceCandidates(event) { queueLocalIceCandidates(event); if (localIceCandidates.length === 1) { // Fire an event if one isn't already in progress sendLocalIceCandidate(); } } /** * Notifies the remote peer of a local ICE candidate. * * @returns {unresolved} */ function sendLocalIceCandidate() { // element is removed once it is delivered var event = localIceCandidates.peek(); if (event === undefined) return; if (!event.candidate) { assert(localIceCandidates.length === 1, "Unexpected queue value: " + toJSON(localIceCandidates)); apprtc.info(userConnection.uri + ": C->S: No more ICE candidates"); userConnection.noMoreIceCandidates().done(function() { log.debug(userConnection.uri + ": No more ICE candidates sent: ", toJSON(candidate)); localIceCandidates.pop(); }, onInternalFailure); } else { apprtc.info(userConnection.uri + ": C->S: " + JSON.stringify( { type: "candidate", label: event.candidate.sdpMLineIndex, id: event.candidate.sdpMid, candidate: event.candidate.candidate })); var candidate = { "candidate": event.candidate.candidate, "mediaIndex": event.candidate.sdpMLineIndex, "mediaId": event.candidate.sdpMid }; userConnection.addIceCandidate(candidate).done(onIceCandidateSent, onInternalFailure); } } // No need to monitor iceCandidates since we are the ones sending them. userConnection.onIceCandidate = undefined; userConnection.onCounterpart = function(counterpart) { UserConnection.getByUri(counterpart).done(function(userConnection) { self.setIncomingConnection(userConnection); self.setRemoteSdp(userConnection.sdp, "answer", undefined, onInternalRtcFailure); }, onInternalFailure); }; outgoingConnection = userConnection; log.info("5) createOutgoingConnection: " + userConnection); delegate.onicecandidate = queueAndSendLocalIceCandidates; // Fire the first event and it'll process the rest of the queue sendLocalIceCandidate(); }; /** * Invoked when an error occurs inside an internal event listener. * * @param {Error} error the reason the operation failed * @returns {undefined} */ function onInternalFailure(error) { self.close(); throw new error; } /** * Invoked when an RTC operation fails inside an internal event listener. * * @param {string} reason the reason the operation failed * @return {undefined} */ function onInternalRtcFailure(reason) { throw new Error(reason); } /** * @returns {String} one of ["stable", "have-local-offer", "have-remote-offer", * "have-local-pranswer", "have-remote-pranswer", "closed"] * @see http://dev.w3.org/2011/webrtc/editor/webrtc.html#idl-def-RTCSignalingState */ this.getSignalingState = function() { return delegate.signalingState; }; /** * @returns {MediaStream[]} the remote streams associated with the connection * @see http://dev.w3.org/2011/webrtc/editor/webrtc.html#idl-def-RTCSignalingState */ this.getRemoteStreams = function() { return delegate.getRemoteStreams(); }; /** * Closes the connection. * * @returns {undefined} */ this.close = function() { if (outgoingConnection) outgoingConnection.onCounterpart = undefined; delegate.close(); }; var delegate; try { // Improve interoperability between Chrome and Firefox clients var constraints = { "optional": [ { "DtlsSrtpKeyAgreement": true } ] }; apprtc.info("Creating PeerConnection."); delegate = new RTCPeerConnection(config, constraints); apprtc.info("Created RTCPeerConnection with:\n" + " config: \"" + JSON.stringify(config) + "\";\n" + " constraints: \"" + JSON.stringify(constraints) + "\"."); } catch (e) { log.warn("Failed to create PeerConnection, exception: ", e.stack); alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser."); return; } var self = this; var localIceCandidates = new Queue(); var remoteIceCandidates = new Queue(); this.signalingStateChangeListeners.push(function(event) { onSignalingStateChange(event); }); this.remoteStreamListeners.push( { onRemoteStreamAdded: onRemoteStreamAdded, onRemoteStreamRemoved: onRemoteStreamRemoved }); delegate.onnegotiationneeded = onNegotiationNeeded; delegate.onicecandidate = queueLocalIceCandidates; delegate.onstatechange = function(event) { var eventWrapper = {}; Object.merge(eventWrapper, event); eventWrapper.target = self; for (var i = 0; i < self.signalingStateChangeListeners.length; ++i) self.signalingStateChangeListeners[i](eventWrapper); }; delegate.onaddstream = function(event) { for (var i = 0; i < self.remoteStreamListeners.length; ++i) self.remoteStreamListeners[i].onRemoteStreamAdded(event); }; delegate.onremovestream = function(event) { for (var i = 0; i < self.remoteStreamListeners.length; ++i) self.remoteStreamListeners[i].onRemoteStreamRemoved(event); }; delegate.onicechange = onIceConnectionStateChanged; delegate.ongatheringchange = onGatheringChange; /** * @callback AjaxFailure * @param {Error} error the reason the operation failed * @returns {undefined} */ /** * @callback WebRtcFailure * @param {string} reason the reason the operation failed * @returns {undefined} */ /** * Notifies a Future of a WebRTC failure. * * @param {Future} future the {@code Future} to notify of the failure * @return {WebRtcFailure} a WebRTC failure callback */ function RtcToAjaxFailureAdapter(future) { return function(reason) { future.reject(new Error(reason + ". " + settings.type + " " + settings.url + " returned HTTP " + xhr.status + ": " + xhr.responseText)); }; } return this; } PeerConnection.prototype.toString = function() { "use strict"; return toJSON(this); };