Bug 1148307 - Part 3 - implement session transport with DataChannel. r=jib.

This commit is contained in:
Junior Hsu 2016-04-11 11:20:55 +08:00
Родитель 248ceef0ca
Коммит fee05caf8c
11 изменённых файлов: 622 добавлений и 3 удалений

Просмотреть файл

@ -421,6 +421,8 @@
@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

Просмотреть файл

@ -582,6 +582,8 @@
@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

Просмотреть файл

@ -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]);

Просмотреть файл

@ -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}

Просмотреть файл

@ -67,7 +67,8 @@ interface nsIPresentationControlChannelListener: nsISupports
/*
* 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

Просмотреть файл

@ -53,7 +53,9 @@ interface nsIPresentationDataChannelSessionTransportBuilder : nsIPresentationSes
/**
* 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,

Просмотреть файл

@ -8,6 +8,7 @@ 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',
@ -43,6 +44,8 @@ UNIFIED_SOURCES += [
]
EXTRA_COMPONENTS += [
'PresentationDataChannelSessionTransport.js',
'PresentationDataChannelSessionTransport.manifest',
'PresentationDeviceInfoManager.js',
'PresentationDeviceInfoManager.manifest',
]

Просмотреть файл

@ -489,7 +489,7 @@ TCPControlChannel.prototype = {
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);
},

Просмотреть файл

@ -0,0 +1,4 @@
[DEFAULT]
skip-if = buildapp == 'b2g' || os == 'android'
[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>

Просмотреть файл

@ -417,6 +417,8 @@
@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