Bug 1148307 - Part 3 - implement session transport with DataChannel. r=jib.
☠☠ backed out by 699007f8060a ☠ ☠
authorJunior Hsu <juhsu@mozilla.com>
Mon, 11 Apr 2016 11:20:55 +0800
changeset 330463 820f92f08f69087043c3afbc4b3319721096dfe2
parent 330462 dfe4cc7062d1954f3ce54a5e330375442766f9bd
child 330464 3cb658c3e3f93eaa1403e9227b6f6a02630d4bf3
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjib
bugs1148307
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1148307 - Part 3 - implement session transport with DataChannel. r=jib.
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
dom/presentation/PresentationDataChannelSessionTransport.js
dom/presentation/PresentationDataChannelSessionTransport.manifest
dom/presentation/interfaces/nsIPresentationControlChannel.idl
dom/presentation/interfaces/nsIPresentationSessionTransportBuilder.idl
dom/presentation/moz.build
dom/presentation/provider/TCPPresentationServer.js
dom/presentation/tests/mochitest/chrome.ini
dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html
mobile/android/installer/package-manifest.in
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -416,16 +416,18 @@
 @RESPATH@/components/nsAsyncShutdown.manifest
 @RESPATH@/components/nsAsyncShutdown.js
 @RESPATH@/components/htmlMenuBuilder.js
 @RESPATH@/components/htmlMenuBuilder.manifest
 @RESPATH@/components/PresentationDeviceInfoManager.manifest
 @RESPATH@/components/PresentationDeviceInfoManager.js
 @RESPATH@/components/BuiltinProviders.manifest
 @RESPATH@/components/TCPPresentationServer.js
+@BINPATH@/components/PresentationDataChannelSessionTransport.js
+@BINPATH@/components/PresentationDataChannelSessionTransport.manifest
 
 #ifdef MOZ_SECUREELEMENT
 @RESPATH@/components/ACEService.js
 @RESPATH@/components/ACEService.manifest
 @RESPATH@/components/GPAccessRulesManager.js
 @RESPATH@/components/GPAccessRulesManager.manifest
 @RESPATH@/components/SecureElement.js
 @RESPATH@/components/SecureElement.manifest
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -577,16 +577,18 @@
 
 @RESPATH@/components/nsAsyncShutdown.manifest
 @RESPATH@/components/nsAsyncShutdown.js
 
 @RESPATH@/components/PresentationDeviceInfoManager.manifest
 @RESPATH@/components/PresentationDeviceInfoManager.js
 @RESPATH@/components/BuiltinProviders.manifest
 @RESPATH@/components/TCPPresentationServer.js
+@BINPATH@/components/PresentationDataChannelSessionTransport.js
+@BINPATH@/components/PresentationDataChannelSessionTransport.manifest
 
 ; InputMethod API
 @RESPATH@/components/MozKeyboard.js
 @RESPATH@/components/InputMethod.manifest
 
 #ifdef MOZ_DEBUG
 @RESPATH@/components/TestInterfaceJS.js
 @RESPATH@/components/TestInterfaceJS.manifest
new file mode 100644
--- /dev/null
+++ b/dom/presentation/PresentationDataChannelSessionTransport.js
@@ -0,0 +1,353 @@
+/* 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 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.nsIPresentationDataChannelSessionTransportBuilder,
+                                         Ci.nsIPresentationControlChannelListener,
+                                         Ci.nsITimerCallback]),
+
+  buildDataChannelTransport: function(aType, aWindow, aControlChannel, aListener) {
+    if (!aType || !aWindow || !aControlChannel || !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 type " + aType);
+    this._type = aType;
+    this._window = aWindow;
+    this._controlChannel = aControlChannel.QueryInterface(Ci.nsIPresentationControlChannel);
+    this._controlChannel.listener = this;
+    this._listener = aListener.QueryInterface(Ci.nsIPresentationSessionTransportBuilderListener);
+
+    // TODO bug 1227053 set iceServers from |nsIPresentationDevice|
+    this._peerConnection = new this._window.RTCPeerConnection();
+
+    // |this._controlChannel == null| will throw since the control channel is
+    // abnormally closed.
+    this._peerConnection.onicecandidate = aEvent => aEvent.candidate &&
+      this._controlChannel.sendIceCandidate(JSON.stringify(aEvent.candidate));
+
+    this._peerConnection.onnegotiationneeded = () => {
+      log("onnegotiationneeded with type " + this._type);
+      this._peerConnection.createOffer()
+          .then(aOffer => this._peerConnection.setLocalDescription(aOffer))
+          .then(() => this._controlChannel
+                          .sendOffer(new PresentationDataChannelDescription(this._peerConnection.localDescription)))
+          .catch(e => this._reportError(e));
+    }
+
+    switch (this._type) {
+      case Ci.nsIPresentationSessionTransportBuilder.TYPE_SENDER:
+        this._dataChannel = this._peerConnection.createDataChannel("presentationAPI");
+        this._setDataChannel();
+        break;
+
+      case Ci.nsIPresentationSessionTransportBuilder.TYPE_RECEIVER:
+        this._peerConnection.ondatachannel = aEvent => {
+          this._dataChannel = aEvent.channel;
+          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, type " + this._type);
+
+      // Handoff the ownership of _peerConnection and _dataChannel to
+      // _sessionTransport
+      this._sessionTransport = new PresentationTransport();
+      this._sessionTransport.init(this._peerConnection, this._dataChannel);
+      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._type = null;
+    this._window = null;
+
+    if (this._controlChannel) {
+      this._controlChannel.close(aReason);
+      this._controlChannel = null;
+    }
+
+    this._listener = null;
+    this._sessionTransport = null;
+
+    if (this._timer) {
+      this._timer.cancel();
+      this._timer = null;
+    }
+  },
+
+  // nsIPresentationControlChannelListener
+  onOffer: function(aOffer) {
+    if (this._type !== Ci.nsIPresentationSessionTransportBuilder.TYPE_RECEIVER ||
+          this._sessionTransport) {
+      log("onOffer status error");
+      this._cleanup(Cr.NS_ERROR_FAILURE);
+    }
+
+    log("onOffer: " + aOffer.dataChannelSDP + " with type " + this._type);
+
+    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._controlChannel
+              .sendAnswer(new PresentationDataChannelDescription(this._peerConnection.localDescription))
+        }).catch(e => this._reportError(e));
+  },
+
+  onAnswer: function(aAnswer) {
+    if (this._type !== Ci.nsIPresentationSessionTransportBuilder.TYPE_SENDER ||
+          this._sessionTransport) {
+      log("onAnswer status error");
+      this._cleanup(Cr.NS_ERROR_FAILURE);
+    }
+
+    log("onAnswer: " + aAnswer.dataChannelSDP + " with type " + this._type);
+
+    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 type " + this._type);
+    let candidate = new this._window.RTCIceCandidate(JSON.parse(aCandidate));
+    this._peerConnection.addIceCandidate(candidate).catch(e => this._reportError(e));
+  },
+
+  notifyOpened: function() {
+    log("notifyOpened, should be opened beforehand");
+  },
+
+  notifyClosed: function(aReason) {
+    log("notifyClosed reason: " + aReason);
+
+    if (aReason != Cr.NS_OK) {
+      this._cleanup(aReason);
+    } else if (this._isControlChannelNeeded) {
+      this._cleanup(Cr.NS_ERROR_FAILURE);
+    }
+    this._controlChannel = null;
+  },
+};
+
+
+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) {
+    log("initWithDataChannel");
+    this._enableDataNotification = false;
+    this._dataChannel = aDataChannel;
+    this._peerConnection = aPeerConnection;
+
+    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._callback.notifyData(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);
+  },
+
+  enableDataNotification: function() {
+    log("enableDataNotification");
+    if (this._enableDataNotification) {
+      return;
+    }
+
+    if (!this._callback) {
+      throw NS_ERROR_NOT_AVAILABLE;
+    }
+
+    this._enableDataNotification = true;
+
+    this._messageQueue.forEach(aData => this._callback.notifyData(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.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationTransportBuilder,
+                                                     PresentationTransport]);
new file mode 100644
--- /dev/null
+++ b/dom/presentation/PresentationDataChannelSessionTransport.manifest
@@ -0,0 +1,6 @@
+# PresentationDataChannelSessionTransport.js
+component {dd2bbf2f-3399-4389-8f5f-d382afb8b2d6} PresentationDataChannelSessionTransport.js
+contract @mozilla.org/presentation/datachanneltransport;1 {dd2bbf2f-3399-4389-8f5f-d382afb8b2d6}
+
+component {215b2f62-46e2-4004-a3d1-6858e56c20f3} PresentationDataChannelSessionTransport.js
+contract @mozilla.org/presentation/datachanneltransportbuilder;1 {215b2f62-46e2-4004-a3d1-6858e56c20f3}
--- a/dom/presentation/interfaces/nsIPresentationControlChannel.idl
+++ b/dom/presentation/interfaces/nsIPresentationControlChannel.idl
@@ -62,17 +62,18 @@ interface nsIPresentationControlChannelL
    * The callback for notifying channel closed.
    * @param reason The reason of channel close, NS_OK represents normal close.
    */
   void notifyClosed(in nsresult reason);
 };
 
 /*
  * The control channel for establishing RTCPeerConnection for a presentation
- * session. SDP Offer/Answer will be exchanged through this interface.
+ * session. SDP Offer/Answer will be exchanged through this interface. The
+ * control channel should be in-order.
  */
 [scriptable, uuid(e60e208c-a9f5-4bc6-9a3e-47f3e4ae9c57)]
 interface nsIPresentationControlChannel: nsISupports
 {
   // The listener for handling events of this control channel.
   // All the events should be pending until listener is assigned.
   attribute nsIPresentationControlChannelListener listener;
 
--- a/dom/presentation/interfaces/nsIPresentationSessionTransportBuilder.idl
+++ b/dom/presentation/interfaces/nsIPresentationSessionTransportBuilder.idl
@@ -48,15 +48,17 @@ interface nsIPresentationTCPSessionTrans
  * Builder for WebRTC data channel session transport
  */
 [scriptable, uuid(8131c4e0-3a8c-4bc1-a92a-8431473d2fe8)]
 interface nsIPresentationDataChannelSessionTransportBuilder : nsIPresentationSessionTransportBuilder
 {
   /**
    * The following creation function will trigger |listener.onSessionTransport|
    * if the session transport is successfully built, |listener.onError| if some
-   * error occurs during creating session transport.
+   * error occurs during creating session transport. The |notifyOpened| of
+   * |aControlChannel| should be called before calling
+   * |buildDataChannelTransport|.
    */
   void buildDataChannelTransport(in uint8_t aType,
                                  in nsIDOMWindow aWindow,
                                  in nsIPresentationControlChannel aControlChannel,
                                  in nsIPresentationSessionTransportBuilderListener aListener);
 };
--- a/dom/presentation/moz.build
+++ b/dom/presentation/moz.build
@@ -3,16 +3,17 @@
 # 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/.
 
 DIRS += ['interfaces', 'provider']
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
 MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
+MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
 
 EXPORTS.mozilla.dom += [
     'ipc/PresentationChild.h',
     'ipc/PresentationIPCService.h',
     'ipc/PresentationParent.h',
     'Presentation.h',
     'PresentationAvailability.h',
     'PresentationCallbacks.h',
@@ -38,16 +39,18 @@ UNIFIED_SOURCES += [
     'PresentationRequest.cpp',
     'PresentationService.cpp',
     'PresentationSessionInfo.cpp',
     'PresentationSessionRequest.cpp',
     'PresentationTCPSessionTransport.cpp',
 ]
 
 EXTRA_COMPONENTS += [
+    'PresentationDataChannelSessionTransport.js',
+    'PresentationDataChannelSessionTransport.manifest',
     'PresentationDeviceInfoManager.js',
     'PresentationDeviceInfoManager.manifest',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
     EXTRA_COMPONENTS += [
         'PresentationNetworkHelper.js',
         'PresentationNetworkHelper.manifest',
--- a/dom/presentation/provider/TCPPresentationServer.js
+++ b/dom/presentation/provider/TCPPresentationServer.js
@@ -484,17 +484,17 @@ TCPControlChannel.prototype = {
     DEBUG && log("TCPControlChannel - onStartRequest with role: "
                  + this._direction);
   },
 
   // nsIRequestObserver (Triggered by nsIInputStreamPump.asyncRead)
   onStopRequest: function(aRequest, aContext, aStatus) {
     DEBUG && log("TCPControlChannel - onStopRequest: " + aStatus
                  + " with role: " + this._direction);
-    this.close(Cr.NS_OK);
+    this.close(aStatus);
     this._notifyClosed(aStatus);
   },
 
   // nsIStreamListener (Triggered by nsIInputStreamPump.asyncRead)
   onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
     let data = NetUtil.readInputStreamToString(aInputStream,
                                                aInputStream.available());
     DEBUG && log("TCPControlChannel - onDataAvailable: " + data);
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+skip-if = buildapp == 'b2g' || os == 'android'
+
+[test_presentation_datachannel_sessiontransport.html]
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html
@@ -0,0 +1,244 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+  <meta charset="utf-8">
+  <title>Test for data channel as session transport in Presentation API</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1148307">Test for data channel as session transport in Presentation API</a>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+
+const loadingTimeoutPref = "presentation.receiver.loading.timeout";
+
+var clientBuilder;
+var serverBuilder;
+var clientTransport;
+var serverTransport;
+var clientControlChannel;
+var serverControlChannel;
+
+const clientMessage = "Client Message";
+const serverMessage = "Server Message";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+const { Services } = Cu.import("resource://gre/modules/Services.jsm");
+
+function TestControlChannel() {
+  this._listener = null;
+}
+
+TestControlChannel.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+  set listener(aListener) {
+    this._listener = aListener;
+  },
+  get listener() {
+    return this._listener;
+  },
+  sendOffer: function(aOffer) {
+    setTimeout(()=>this._remote.listener.onOffer(aOffer), 0);
+  },
+  sendAnswer: function(aAnswer) {
+    setTimeout(()=>this._remote.listener.onAnswer(aAnswer), 0);
+  },
+  sendIceCandidate: function(aCandidate) {
+    setTimeout(()=>this._remote.listener.onIceCandidate(aCandidate), 0);
+  },
+  close: function(aReason) {
+    setTimeout(()=>this._listener.notifyClosed(aReason), 0);
+    setTimeout(()=>this._remote.listener.notifyClosed(aReason), 0);
+  },
+  set remote(aRemote) {
+    this._remote = aRemote;
+  },
+};
+
+var isClientReady = false;
+var isServerReady = false;
+var isClientClosed = false;
+var isServerClosed = false;
+
+var gResolve;
+var gReject;
+
+const clientCallback = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportCallback]),
+  notifyTransportReady: function () {
+    info("Client transport ready.");
+
+    isClientReady = true;
+    if (isClientReady && isServerReady) {
+      gResolve();
+    }
+  },
+  notifyTransportClosed: function (aReason) {
+    info("Client transport is closed.");
+
+    isClientClosed = true;
+    if (isClientClosed && isServerClosed) {
+      gResolve();
+    }
+  },
+  notifyData: function(aData) {
+    is(aData, serverMessage, "Client transport receives data.");
+    gResolve();
+  },
+};
+
+const serverCallback = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportCallback]),
+  notifyTransportReady: function () {
+    info("Server transport ready.");
+
+    isServerReady = true;
+    if (isClientReady && isServerReady) {
+      gResolve();
+    }
+  },
+  notifyTransportClosed: function (aReason) {
+    info("Server transport is closed.");
+
+    isServerClosed = true;
+    if (isClientClosed && isServerClosed) {
+      gResolve();
+    }
+  },
+  notifyData: function(aData) {
+    is(aData, clientMessage, "Server transport receives data.");
+    gResolve()
+  },
+};
+
+
+const clientListener = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportBuilderListener]),
+  onSessionTransport: function(aTransport) {
+    info("Client Transport is built.");
+    clientTransport = aTransport;
+    clientTransport.callback = clientCallback;
+  },
+  onError: function(aError)  {
+    ok(false, "client's builder reports error " + aError);
+  }
+}
+
+const serverListener = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportBuilderListener]),
+  onSessionTransport: function(aTransport) {
+    info("Server Transport is built.");
+    serverTransport = aTransport;
+    serverTransport.callback = serverCallback;
+    serverTransport.enableDataNotification();
+  },
+  onError: function(aError)  {
+    ok(false, "server's builder reports error " + aError);
+  }
+}
+
+
+function testBuilder() {
+  return new Promise(function(aResolve, aReject) {
+    gResolve = aResolve;
+    gReject = aReject;
+
+    clientControlChannel = new TestControlChannel();
+    serverControlChannel = new TestControlChannel();
+    clientControlChannel.remote = serverControlChannel;
+    serverControlChannel.remote = clientControlChannel;
+
+    clientBuilder = Cc["@mozilla.org/presentation/datachanneltransportbuilder;1"]
+                      .createInstance(Ci.nsIPresentationDataChannelSessionTransportBuilder);
+    serverBuilder = Cc["@mozilla.org/presentation/datachanneltransportbuilder;1"]
+                      .createInstance(Ci.nsIPresentationDataChannelSessionTransportBuilder);
+
+    clientBuilder
+      .buildDataChannelTransport(Ci.nsIPresentationSessionTransportBuilder.TYPE_SENDER,
+                                 window,
+                                 clientControlChannel,
+                                 clientListener);
+
+    serverBuilder
+      .buildDataChannelTransport(Ci.nsIPresentationSessionTransportBuilder.TYPE_RECEIVER,
+                                 window,
+                                 serverControlChannel,
+                                 serverListener);
+  });
+}
+
+function testClientSendMessage() {
+  return new Promise(function(aResolve, aReject) {
+    info("client sends message");
+    gResolve = aResolve;
+    gReject = aReject;
+
+    clientTransport.send(clientMessage);
+  });
+}
+
+function testServerSendMessage() {
+  return new Promise(function(aResolve, aReject) {
+    info("server sends message");
+    gResolve = aResolve;
+    gReject = aReject;
+
+    serverTransport.send(serverMessage);
+    setTimeout(()=>clientTransport.enableDataNotification(), 0);
+  });
+}
+
+function testCloseSessionTransport() {
+  return new Promise(function(aResolve, aReject) {
+    info("close session transport");
+    gResolve = aResolve;
+    gReject = aReject;
+
+    serverTransport.close(Cr.NS_OK);
+  });
+}
+
+function finish() {
+  info("test finished, teardown");
+  Services.prefs.clearUserPref(loadingTimeoutPref);
+
+  SimpleTest.finish();
+}
+
+function error(aError) {
+  ok(false, "report Error " + aError.name + ":" + aError.message);
+}
+
+function runTests() {
+  Services.prefs.setIntPref(loadingTimeoutPref, 30000);
+
+  testBuilder()
+  .then(testClientSendMessage)
+  .then(testServerSendMessage)
+  .then(testCloseSessionTransport)
+  .then(finish)
+  .catch(error);
+
+
+}
+
+window.addEventListener("load", function() {
+  runTests();
+});
+
+</script>
+</pre>
+</body>
+</html>
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -412,16 +412,18 @@
 @BINPATH@/components/DownloadLegacy.js
 
 @BINPATH@/components/PresentationDeviceInfoManager.manifest
 @BINPATH@/components/PresentationDeviceInfoManager.js
 @BINPATH@/components/BuiltinProviders.manifest
 @BINPATH@/components/TCPPresentationServer.js
 @BINPATH@/components/PresentationNetworkHelper.js
 @BINPATH@/components/PresentationNetworkHelper.manifest
+@BINPATH@/components/PresentationDataChannelSessionTransport.js
+@BINPATH@/components/PresentationDataChannelSessionTransport.manifest
 
 @BINPATH@/components/PACGenerator.js
 @BINPATH@/components/PACGenerator.manifest
 
 @BINPATH@/components/TVSimulatorService.js
 @BINPATH@/components/TVSimulatorService.manifest
 
 ; Modules