summaryrefslogtreecommitdiffstats
path: root/dom/presentation/PresentationDataChannelSessionTransport.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'dom/presentation/PresentationDataChannelSessionTransport.jsm')
-rw-r--r--dom/presentation/PresentationDataChannelSessionTransport.jsm421
1 files changed, 421 insertions, 0 deletions
diff --git a/dom/presentation/PresentationDataChannelSessionTransport.jsm b/dom/presentation/PresentationDataChannelSessionTransport.jsm
new file mode 100644
index 0000000000..f3e788a1a7
--- /dev/null
+++ b/dom/presentation/PresentationDataChannelSessionTransport.jsm
@@ -0,0 +1,421 @@
+/* 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 { Services } = ChromeUtils.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: ChromeUtils.generateQI(["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: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransportBuilder",
+ "nsIPresentationDataChannelSessionTransportBuilder",
+ "nsITimerCallback",
+ ]),
+
+ buildDataChannelTransport(aRole, aWindow, aListener) {
+ if (!aRole || !aWindow || !aListener) {
+ log("buildDataChannelTransport with illegal parameters");
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ if (this._window) {
+ log("buildDataChannelTransport has started.");
+ throw Components.Exception("", 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 Components.Exception("", 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() {
+ if (!this._sessionTransport) {
+ this._cleanup(Cr.NS_ERROR_NET_TIMEOUT);
+ }
+ },
+
+ _reportError(aError) {
+ log("report Error " + aError.name + ":" + aError.message);
+ this._cleanup(Cr.NS_ERROR_FAILURE);
+ },
+
+ _setDataChannel() {
+ 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(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(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(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(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(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: ChromeUtils.generateQI(["nsIPresentationSessionTransport"]),
+
+ init(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 Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ },
+
+ get callback() {
+ return this._callback;
+ },
+
+ set callback(aCallback) {
+ this._callback = aCallback;
+ },
+
+ send(aData) {
+ log("send " + aData);
+ this._dataChannel.send(aData);
+ },
+
+ sendBinaryMsg(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(aBlob) {
+ log("sendBlob");
+
+ this._dataChannel.send(aBlob);
+ },
+
+ enableDataNotification() {
+ log("enableDataNotification");
+ if (this._enableDataNotification) {
+ return;
+ }
+
+ if (!this._callback) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ this._enableDataNotification = true;
+
+ this._messageQueue.forEach(aData => this._doNotifyData(aData));
+ this._messageQueue = [];
+ },
+
+ close(aReason) {
+ this._closeReason = aReason;
+
+ this._dataChannel.close();
+ },
+
+ _cleanup() {
+ this._dataChannel = null;
+
+ if (this._peerConnection) {
+ this._peerConnection.close();
+ this._peerConnection = null;
+ }
+ this._callback = null;
+ this._messageQueue = [];
+ this._window = null;
+ },
+
+ _doNotifyData(aData) {
+ if (!this._callback) {
+ throw Components.Exception("", Cr.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);
+ }
+ },
+};
+
+var EXPORTED_SYMBOLS = [
+ "PresentationTransportBuilder",
+ "PresentationTransport",
+];