/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); // Bug 1228209 - plan to remove this eventually function log(aMsg) { //dump("-*- PresentationDataChannelSessionTransport.js : " + aMsg + "\n"); } const PRESENTATIONTRANSPORT_CID = Components.ID("{dd2bbf2f-3399-4389-8f5f-d382afb8b2d6}"); const PRESENTATIONTRANSPORT_CONTRACTID = "mozilla.org/presentation/datachanneltransport;1"; const PRESENTATIONTRANSPORTBUILDER_CID = Components.ID("{215b2f62-46e2-4004-a3d1-6858e56c20f3}"); const PRESENTATIONTRANSPORTBUILDER_CONTRACTID = "mozilla.org/presentation/datachanneltransportbuilder;1"; function PresentationDataChannelDescription(aDataChannelSDP) { this._dataChannelSDP = JSON.stringify(aDataChannelSDP); } PresentationDataChannelDescription.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]), get type() { return Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL; }, get tcpAddress() { return null; }, get tcpPort() { return null; }, get dataChannelSDP() { return this._dataChannelSDP; } }; function PresentationTransportBuilder() { log("PresentationTransportBuilder construct"); this._isControlChannelNeeded = true; } PresentationTransportBuilder.prototype = { classID: PRESENTATIONTRANSPORTBUILDER_CID, contractID: PRESENTATIONTRANSPORTBUILDER_CONTRACTID, QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportBuilder, Ci.nsIPresentationDataChannelSessionTransportBuilder, Ci.nsITimerCallback]), buildDataChannelTransport: function(aRole, aWindow, aListener) { if (!aRole || !aWindow || !aListener) { log("buildDataChannelTransport with illegal parameters"); throw Cr.NS_ERROR_ILLEGAL_VALUE; } if (this._window) { log("buildDataChannelTransport has started."); throw Cr.NS_ERROR_UNEXPECTED; } log("buildDataChannelTransport with role " + aRole); this._role = aRole; this._window = aWindow; this._listener = aListener.QueryInterface(Ci.nsIPresentationSessionTransportBuilderListener); // TODO bug 1227053 set iceServers from |nsIPresentationDevice| this._peerConnection = new this._window.RTCPeerConnection(); // |this._listener == null| will throw since the control channel is // abnormally closed. this._peerConnection.onicecandidate = aEvent => aEvent.candidate && this._listener.sendIceCandidate(JSON.stringify(aEvent.candidate)); this._peerConnection.onnegotiationneeded = () => { log("onnegotiationneeded with role " + this._role); if (!this._peerConnection) { log("ignoring negotiationneeded without PeerConnection"); return; } this._peerConnection.createOffer() .then(aOffer => this._peerConnection.setLocalDescription(aOffer)) .then(() => this._listener .sendOffer(new PresentationDataChannelDescription(this._peerConnection.localDescription))) .catch(e => this._reportError(e)); } switch (this._role) { case Ci.nsIPresentationService.ROLE_CONTROLLER: this._dataChannel = this._peerConnection.createDataChannel("presentationAPI"); this._setDataChannel(); break; case Ci.nsIPresentationService.ROLE_RECEIVER: this._peerConnection.ondatachannel = aEvent => { this._dataChannel = aEvent.channel; // Ensure the binaryType of dataChannel is blob. this._dataChannel.binaryType = "blob"; this._setDataChannel(); } break; default: throw Cr.NS_ERROR_ILLEGAL_VALUE; } // TODO bug 1228235 we should have a way to let device providers customize // the time-out duration. let timeout = Services.prefs.getIntPref("presentation.receiver.loading.timeout", 10000); // The timer is to check if the negotiation finishes on time. this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this._timer.initWithCallback(this, timeout, this._timer.TYPE_ONE_SHOT); }, notify: function() { if (!this._sessionTransport) { this._cleanup(Cr.NS_ERROR_NET_TIMEOUT); } }, _reportError: function(aError) { log("report Error " + aError.name + ":" + aError.message); this._cleanup(Cr.NS_ERROR_FAILURE); }, _setDataChannel: function() { this._dataChannel.onopen = () => { log("data channel is open, notify the listener, role " + this._role); // Handoff the ownership of _peerConnection and _dataChannel to // _sessionTransport this._sessionTransport = new PresentationTransport(); this._sessionTransport.init(this._peerConnection, this._dataChannel, this._window); this._peerConnection.onicecandidate = null; this._peerConnection.onnegotiationneeded = null; this._peerConnection = this._dataChannel = null; this._listener.onSessionTransport(this._sessionTransport); this._sessionTransport.callback.notifyTransportReady(); this._cleanup(Cr.NS_OK); }; this._dataChannel.onerror = aError => { log("data channel onerror " + aError.name + ":" + aError.message); this._cleanup(Cr.NS_ERROR_FAILURE); } }, _cleanup: function(aReason) { if (aReason != Cr.NS_OK) { this._listener.onError(aReason); } if (this._dataChannel) { this._dataChannel.close(); this._dataChannel = null; } if (this._peerConnection) { this._peerConnection.close(); this._peerConnection = null; } this._role = null; this._window = null; this._listener = null; this._sessionTransport = null; if (this._timer) { this._timer.cancel(); this._timer = null; } }, // nsIPresentationControlChannelListener onOffer: function(aOffer) { if (this._role !== Ci.nsIPresentationService.ROLE_RECEIVER || this._sessionTransport) { log("onOffer status error"); this._cleanup(Cr.NS_ERROR_FAILURE); } log("onOffer: " + aOffer.dataChannelSDP + " with role " + this._role); let offer = new this._window .RTCSessionDescription(JSON.parse(aOffer.dataChannelSDP)); this._peerConnection.setRemoteDescription(offer) .then(() => this._peerConnection.signalingState == "stable" || this._peerConnection.createAnswer()) .then(aAnswer => this._peerConnection.setLocalDescription(aAnswer)) .then(() => { this._isControlChannelNeeded = false; this._listener .sendAnswer(new PresentationDataChannelDescription(this._peerConnection.localDescription)) }).catch(e => this._reportError(e)); }, onAnswer: function(aAnswer) { if (this._role !== Ci.nsIPresentationService.ROLE_CONTROLLER || this._sessionTransport) { log("onAnswer status error"); this._cleanup(Cr.NS_ERROR_FAILURE); } log("onAnswer: " + aAnswer.dataChannelSDP + " with role " + this._role); let answer = new this._window .RTCSessionDescription(JSON.parse(aAnswer.dataChannelSDP)); this._peerConnection.setRemoteDescription(answer).catch(e => this._reportError(e)); this._isControlChannelNeeded = false; }, onIceCandidate: function(aCandidate) { log("onIceCandidate: " + aCandidate + " with role " + this._role); if (!this._window || !this._peerConnection) { log("ignoring ICE candidate after connection"); return; } let candidate = new this._window.RTCIceCandidate(JSON.parse(aCandidate)); this._peerConnection.addIceCandidate(candidate).catch(e => this._reportError(e)); }, notifyDisconnected: function(aReason) { log("notifyDisconnected reason: " + aReason); if (aReason != Cr.NS_OK) { this._cleanup(aReason); } else if (this._isControlChannelNeeded) { this._cleanup(Cr.NS_ERROR_FAILURE); } }, }; function PresentationTransport() { this._messageQueue = []; this._closeReason = Cr.NS_OK; } PresentationTransport.prototype = { classID: PRESENTATIONTRANSPORT_CID, contractID: PRESENTATIONTRANSPORT_CONTRACTID, QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransport]), init: function(aPeerConnection, aDataChannel, aWindow) { log("initWithDataChannel"); this._enableDataNotification = false; this._dataChannel = aDataChannel; this._peerConnection = aPeerConnection; this._window = aWindow; this._dataChannel.onopen = () => { log("data channel reopen. Should never touch here"); }; this._dataChannel.onclose = () => { log("data channel onclose"); if (this._callback) { this._callback.notifyTransportClosed(this._closeReason); } this._cleanup(); } this._dataChannel.onmessage = aEvent => { log("data channel onmessage " + aEvent.data); if (!this._enableDataNotification || !this._callback) { log("queue message"); this._messageQueue.push(aEvent.data); return; } this._doNotifyData(aEvent.data); }; this._dataChannel.onerror = aError => { log("data channel onerror " + aError.name + ":" + aError.message); if (this._callback) { this._callback.notifyTransportClosed(Cr.NS_ERROR_FAILURE); } this._cleanup(); } }, // nsIPresentationTransport get selfAddress() { throw NS_ERROR_NOT_AVAILABLE; }, get callback() { return this._callback; }, set callback(aCallback) { this._callback = aCallback; }, send: function(aData) { log("send " + aData); this._dataChannel.send(aData); }, sendBinaryMsg: function(aData) { log("sendBinaryMsg"); let array = new Uint8Array(aData.length); for (let i = 0; i < aData.length; i++) { array[i] = aData.charCodeAt(i); } this._dataChannel.send(array); }, sendBlob: function(aBlob) { log("sendBlob"); this._dataChannel.send(aBlob); }, enableDataNotification: function() { log("enableDataNotification"); if (this._enableDataNotification) { return; } if (!this._callback) { throw NS_ERROR_NOT_AVAILABLE; } this._enableDataNotification = true; this._messageQueue.forEach(aData => this._doNotifyData(aData)); this._messageQueue = []; }, close: function(aReason) { this._closeReason = aReason; this._dataChannel.close(); }, _cleanup: function() { this._dataChannel = null; if (this._peerConnection) { this._peerConnection.close(); this._peerConnection = null; } this._callback = null; this._messageQueue = []; this._window = null; }, _doNotifyData: function(aData) { if (!this._callback) { throw NS_ERROR_NOT_AVAILABLE; } if (aData instanceof this._window.Blob) { let reader = new this._window.FileReader(); reader.addEventListener("load", (aEvent) => { this._callback.notifyData(aEvent.target.result, true); }); reader.readAsBinaryString(aData); } else { this._callback.notifyData(aData, false); } }, }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationTransportBuilder, PresentationTransport]);