gecko-dev/dom/presentation/PresentationDataChannelSess...

422 строки
11 KiB
JavaScript

/* 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",
];