diff options
Diffstat (limited to 'dom/presentation/PresentationDataChannelSessionTransport.js')
-rw-r--r-- | dom/presentation/PresentationDataChannelSessionTransport.js | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/dom/presentation/PresentationDataChannelSessionTransport.js b/dom/presentation/PresentationDataChannelSessionTransport.js new file mode 100644 index 000000000..461e4f2cb --- /dev/null +++ b/dom/presentation/PresentationDataChannelSessionTransport.js @@ -0,0 +1,384 @@ +/* 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; + try { + timeout = Services.prefs.getIntPref("presentation.receiver.loading.timeout"); + } catch (e) { + // This happens if the pref doesn't exist, so we have a default value. + 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]); |