Bug 1148307 - Part 3, implement session transport with DataChannel. r=jib.
authorJunior Hsu <juhsu@mozilla.com>
Mon, 11 Apr 2016 11:20:55 +0800
changeset 292784 0db5c9b171c7e1dd40d0f8c8d6272578d9b75361
parent 292783 ee4c84bfbf69d1cb709e84a25c3a92e62889d288
child 292785 e0a1ad6c114e2540b536f414d7d277a7aa683d1a
push id30167
push userkwierso@gmail.com
push dateTue, 12 Apr 2016 22:28:26 +0000
treeherdermozilla-central@fb125ff927ea [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
+@RESPATH@/components/PresentationDataChannelSessionTransport.js
+@RESPATH@/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
+@RESPATH@/components/PresentationDataChannelSessionTransport.js
+@RESPATH@/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