summaryrefslogtreecommitdiffstats
path: root/dom/presentation/provider/AndroidCastDeviceProvider.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/presentation/provider/AndroidCastDeviceProvider.js')
-rw-r--r--dom/presentation/provider/AndroidCastDeviceProvider.js461
1 files changed, 461 insertions, 0 deletions
diff --git a/dom/presentation/provider/AndroidCastDeviceProvider.js b/dom/presentation/provider/AndroidCastDeviceProvider.js
new file mode 100644
index 000000000..cf555f77b
--- /dev/null
+++ b/dom/presentation/provider/AndroidCastDeviceProvider.js
@@ -0,0 +1,461 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components, dump */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+// globals XPCOMUtils
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+// globals Services
+Cu.import("resource://gre/modules/Services.jsm");
+// globals Messaging
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+function log(str) {
+ // dump("-*- AndroidCastDeviceProvider -*-: " + str + "\n");
+}
+
+// Helper function: transfer nsIPresentationChannelDescription to json
+function descriptionToString(aDescription) {
+ let json = {};
+ json.type = aDescription.type;
+ switch(aDescription.type) {
+ case Ci.nsIPresentationChannelDescription.TYPE_TCP:
+ let addresses = aDescription.tcpAddress.QueryInterface(Ci.nsIArray);
+ json.tcpAddress = [];
+ for (let idx = 0; idx < addresses.length; idx++) {
+ let address = addresses.queryElementAt(idx, Ci.nsISupportsCString);
+ json.tcpAddress.push(address.data);
+ }
+ json.tcpPort = aDescription.tcpPort;
+ break;
+ case Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL:
+ json.dataChannelSDP = aDescription.dataChannelSDP;
+ break;
+ }
+ return JSON.stringify(json);
+}
+
+const TOPIC_ANDROID_CAST_DEVICE_SYNCDEVICE = "AndroidCastDevice:SyncDevice";
+const TOPIC_ANDROID_CAST_DEVICE_ADDED = "AndroidCastDevice:Added";
+const TOPIC_ANDROID_CAST_DEVICE_REMOVED = "AndroidCastDevice:Removed";
+const TOPIC_ANDROID_CAST_DEVICE_START = "AndroidCastDevice:Start";
+const TOPIC_ANDROID_CAST_DEVICE_STOP = "AndroidCastDevice:Stop";
+const TOPIC_PRESENTATION_VIEW_READY = "presentation-view-ready";
+
+function LocalControlChannel(aProvider, aDeviceId, aRole) {
+ log("LocalControlChannel - create new LocalControlChannel for : "
+ + aRole);
+ this._provider = aProvider;
+ this._deviceId = aDeviceId;
+ this._role = aRole;
+}
+
+LocalControlChannel.prototype = {
+ _listener: null,
+ _provider: null,
+ _deviceId: null,
+ _role: null,
+ _isOnTerminating: false,
+ _isOnDisconnecting: false,
+ _pendingConnected: false,
+ _pendingDisconnect: null,
+ _pendingOffer: null,
+ _pendingCandidate: null,
+ /* For the controller, it would be the control channel of the receiver.
+ * For the receiver, it would be the control channel of the controller. */
+ _correspondingControlChannel: null,
+
+ set correspondingControlChannel(aCorrespondingControlChannel) {
+ this._correspondingControlChannel = aCorrespondingControlChannel;
+ },
+
+ get correspondingControlChannel() {
+ return this._correspondingControlChannel;
+ },
+
+ notifyConnected: function LCC_notifyConnected() {
+ this._pendingDisconnect = null;
+
+ if (!this._listener) {
+ this._pendingConnected = true;
+ } else {
+ this._listener.notifyConnected();
+ }
+ },
+
+ onOffer: function LCC_onOffer(aOffer) {
+ if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+ log("LocalControlChannel - onOffer of controller should not be called.");
+ return;
+ }
+ if (!this._listener) {
+ this._pendingOffer = aOffer;
+ } else {
+ this._listener.onOffer(aOffer);
+ }
+ },
+
+ onAnswer: function LCC_onAnswer(aAnswer) {
+ if (this._role == Ci.nsIPresentationService.ROLE_RECEIVER) {
+ log("LocalControlChannel - onAnswer of receiver should not be called.");
+ return;
+ }
+ this._listener.onAnswer(aAnswer);
+ },
+
+ notifyIceCandidate: function LCC_notifyIceCandidate(aCandidate) {
+ if (!this._listener) {
+ this._pendingCandidate = aCandidate;
+ } else {
+ this._listener.onIceCandidate(aCandidate);
+ }
+ },
+
+ // nsIPresentationControlChannel
+ get listener() {
+ return this._listener;
+ },
+
+ set listener(aListener) {
+ this._listener = aListener;
+
+ if (!this._listener) {
+ return;
+ }
+
+ if (this._pendingConnected) {
+ this.notifyConnected();
+ this._pendingConnected = false;
+ }
+
+ if (this._pendingOffer) {
+ this.onOffer(this._pendingOffer);
+ this._pendingOffer = null;
+ }
+
+ if (this._pendingCandidate) {
+ this.notifyIceCandidate(this._pendingCandidate);
+ this._pendingCandidate = null;
+ }
+
+ if (this._pendingDisconnect != null) {
+ this.disconnect(this._pendingDisconnect);
+ this._pendingDisconnect = null;
+ }
+ },
+
+ sendOffer: function LCC_sendOffer(aOffer) {
+ if (this._role == Ci.nsIPresentationService.ROLE_RECEIVER) {
+ log("LocalControlChannel - sendOffer of receiver should not be called.");
+ return;
+ }
+ log("LocalControlChannel - sendOffer aOffer=" + descriptionToString(aOffer));
+ this._correspondingControlChannel.onOffer(aOffer);
+ },
+
+ sendAnswer: function LCC_sendAnswer(aAnswer) {
+ if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+ log("LocalControlChannel - sendAnswer of controller should not be called.");
+ return;
+ }
+ log("LocalControlChannel - sendAnswer aAnswer=" + descriptionToString(aAnswer));
+ this._correspondingControlChannel.onAnswer(aAnswer);
+ },
+
+ sendIceCandidate: function LCC_sendIceCandidate(aCandidate) {
+ log("LocalControlChannel - sendAnswer aCandidate=" + aCandidate);
+ this._correspondingControlChannel.notifyIceCandidate(aCandidate);
+ },
+
+ launch: function LCC_launch(aPresentationId, aUrl) {
+ log("LocalControlChannel - launch aPresentationId="
+ + aPresentationId + " aUrl=" + aUrl);
+ // Create control channel for receiver directly.
+ let controlChannel = new LocalControlChannel(this._provider,
+ this._deviceId,
+ Ci.nsIPresentationService.ROLE_RECEIVER);
+
+ // Set up the corresponding control channels for both controller and receiver.
+ this._correspondingControlChannel = controlChannel;
+ controlChannel._correspondingControlChannel = this;
+
+ this._provider.onSessionRequest(this._deviceId,
+ aUrl,
+ aPresentationId,
+ controlChannel);
+ controlChannel.notifyConnected();
+ },
+
+ terminate: function LCC_terminate(aPresentationId) {
+ log("LocalControlChannel - terminate aPresentationId="
+ + aPresentationId);
+
+ if (this._isOnTerminating) {
+ return;
+ }
+
+ // Create control channel for corresponding role directly.
+ let correspondingRole = this._role == Ci.nsIPresentationService.ROLE_CONTROLLER
+ ? Ci.nsIPresentationService.ROLE_RECEIVER
+ : Ci.nsIPresentationService.ROLE_CONTROLLER;
+ let controlChannel = new LocalControlChannel(this._provider,
+ this._deviceId,
+ correspondingRole);
+ // Prevent the termination recursion.
+ controlChannel._isOnTerminating = true;
+
+ // Set up the corresponding control channels for both controller and receiver.
+ this._correspondingControlChannel = controlChannel;
+ controlChannel._correspondingControlChannel = this;
+
+ this._provider.onTerminateRequest(this._deviceId,
+ aPresentationId,
+ controlChannel,
+ this._role == Ci.nsIPresentationService.ROLE_RECEIVER);
+ controlChannel.notifyConnected();
+ },
+
+ disconnect: function LCC_disconnect(aReason) {
+ log("LocalControlChannel - disconnect aReason=" + aReason);
+
+ if (this._isOnDisconnecting) {
+ return;
+ }
+
+ this._pendingOffer = null;
+ this._pendingCandidate = null;
+ this._pendingConnected = false;
+
+ // this._pendingDisconnect is a nsresult.
+ // If it is null, it means no pending disconnect.
+ // If it is NS_OK, it means this control channel is disconnected normally.
+ // If it is other nsresult value, it means this control channel is
+ // disconnected abnormally.
+
+ // Remote endpoint closes the control channel with abnormal reason.
+ if (aReason == Cr.NS_OK &&
+ this._pendingDisconnect != null &&
+ this._pendingDisconnect != Cr.NS_OK) {
+ aReason = this._pendingDisconnect;
+ }
+
+ if (!this._listener) {
+ this._pendingDisconnect = aReason;
+ return;
+ }
+
+ this._isOnDisconnecting = true;
+ this._correspondingControlChannel.disconnect(aReason);
+ this._listener.notifyDisconnected(aReason);
+ },
+
+ reconnect: function LCC_reconnect(aPresentationId, aUrl) {
+ log("1-UA on Android doesn't support reconnect.");
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+ classID: Components.ID("{c9be9450-e5c7-4294-a287-376971b017fd}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+};
+
+function ChromecastRemoteDisplayDevice(aProvider, aId, aName, aRole) {
+ this._provider = aProvider;
+ this._id = aId;
+ this._name = aName;
+ this._role = aRole;
+}
+
+ChromecastRemoteDisplayDevice.prototype = {
+ _id: null,
+ _name: null,
+ _role: null,
+ _provider: null,
+ _ctrlChannel: null,
+
+ update: function CRDD_update(aName) {
+ this._name = aName || this._name;
+ },
+
+ // nsIPresentationDevice
+ get id() { return this._id; },
+
+ get name() { return this._name; },
+
+ get type() { return "chromecast"; },
+
+ establishControlChannel: function CRDD_establishControlChannel() {
+ this._ctrlChannel = new LocalControlChannel(this._provider,
+ this._id,
+ this._role);
+
+ if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) {
+ // Only connect to Chromecast for controller.
+ // Monitor the receiver being ready.
+ Services.obs.addObserver(this, TOPIC_PRESENTATION_VIEW_READY, true);
+
+ // Launch Chromecast service in Android.
+ Messaging.sendRequestForResult({
+ type: TOPIC_ANDROID_CAST_DEVICE_START,
+ id: this.id
+ }).then(result => {
+ log("Chromecast is connected.");
+ }).catch(error => {
+ log("Can not connect to Chromecast.");
+ // If Chromecast can not be launched, remove the observer.
+ Services.obs.removeObserver(this, TOPIC_PRESENTATION_VIEW_READY);
+ this._ctrlChannel.disconnect(Cr.NS_ERROR_FAILURE);
+ });
+ } else {
+ // If establishControlChannel called from the receiver, we don't need to
+ // wait the 'presentation-view-ready' event.
+ this._ctrlChannel.notifyConnected();
+ }
+
+ return this._ctrlChannel;
+ },
+
+ disconnect: function CRDD_disconnect() {
+ // Disconnect from Chromecast.
+ Messaging.sendRequestForResult({
+ type: TOPIC_ANDROID_CAST_DEVICE_STOP,
+ id: this.id
+ });
+ },
+
+ isRequestedUrlSupported: function CRDD_isRequestedUrlSupported(aUrl) {
+ let url = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(aUrl, null, null);
+ return url.scheme == "http" || url.scheme == "https";
+ },
+
+ // nsIPresentationLocalDevice
+ get windowId() { return this._id; },
+
+ // nsIObserver
+ observe: function CRDD_observe(aSubject, aTopic, aData) {
+ if (aTopic == TOPIC_PRESENTATION_VIEW_READY) {
+ log("ChromecastRemoteDisplayDevice - observe: aTopic="
+ + aTopic + " data=" + aData);
+ if (this.windowId === aData) {
+ Services.obs.removeObserver(this, TOPIC_PRESENTATION_VIEW_READY);
+ this._ctrlChannel.notifyConnected();
+ }
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice,
+ Ci.nsIPresentationLocalDevice,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIObserver]),
+};
+
+function AndroidCastDeviceProvider() {
+}
+
+AndroidCastDeviceProvider.prototype = {
+ _listener: null,
+ _deviceList: new Map(),
+
+ onSessionRequest: function APDP_onSessionRequest(aDeviceId,
+ aUrl,
+ aPresentationId,
+ aControlChannel) {
+ log("AndroidCastDeviceProvider - onSessionRequest"
+ + " aDeviceId=" + aDeviceId);
+ let device = this._deviceList.get(aDeviceId);
+ let receiverDevice = new ChromecastRemoteDisplayDevice(this,
+ device.id,
+ device.name,
+ Ci.nsIPresentationService.ROLE_RECEIVER);
+ this._listener.onSessionRequest(receiverDevice,
+ aUrl,
+ aPresentationId,
+ aControlChannel);
+ },
+
+ onTerminateRequest: function APDP_onTerminateRequest(aDeviceId,
+ aPresentationId,
+ aControlChannel,
+ aIsFromReceiver) {
+ log("AndroidCastDeviceProvider - onTerminateRequest"
+ + " aDeviceId=" + aDeviceId
+ + " aPresentationId=" + aPresentationId
+ + " aIsFromReceiver=" + aIsFromReceiver);
+ let device = this._deviceList.get(aDeviceId);
+ this._listener.onTerminateRequest(device,
+ aPresentationId,
+ aControlChannel,
+ aIsFromReceiver);
+ },
+
+ // nsIPresentationDeviceProvider
+ set listener(aListener) {
+ this._listener = aListener;
+
+ // When unload this provider.
+ if (!this._listener) {
+ // remove observer
+ Services.obs.removeObserver(this, TOPIC_ANDROID_CAST_DEVICE_ADDED);
+ Services.obs.removeObserver(this, TOPIC_ANDROID_CAST_DEVICE_REMOVED);
+ return;
+ }
+
+ // Sync all device already found by Android.
+ Services.obs.notifyObservers(null, TOPIC_ANDROID_CAST_DEVICE_SYNCDEVICE, "");
+ // Observer registration
+ Services.obs.addObserver(this, TOPIC_ANDROID_CAST_DEVICE_ADDED, false);
+ Services.obs.addObserver(this, TOPIC_ANDROID_CAST_DEVICE_REMOVED, false);
+ },
+
+ get listener() {
+ return this._listener;
+ },
+
+ forceDiscovery: function APDP_forceDiscovery() {
+ // There is no API to do force discovery in Android SDK.
+ },
+
+ // nsIObserver
+ observe: function APDP_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case TOPIC_ANDROID_CAST_DEVICE_ADDED: {
+ let deviceInfo = JSON.parse(aData);
+ let deviceId = deviceInfo.uuid;
+
+ if (!this._deviceList.has(deviceId)) {
+ let device = new ChromecastRemoteDisplayDevice(this,
+ deviceInfo.uuid,
+ deviceInfo.friendlyName,
+ Ci.nsIPresentationService.ROLE_CONTROLLER);
+ this._deviceList.set(device.id, device);
+ this._listener.addDevice(device);
+ } else {
+ let device = this._deviceList.get(deviceId);
+ device.update(deviceInfo.friendlyName);
+ this._listener.updateDevice(device);
+ }
+ break;
+ }
+ case TOPIC_ANDROID_CAST_DEVICE_REMOVED: {
+ let deviceId = aData;
+ let device = this._deviceList.get(deviceId);
+ this._listener.removeDevice(device);
+ this._deviceList.delete(deviceId);
+ break;
+ }
+ }
+ },
+
+ classID: Components.ID("{7394f24c-dbc3-48c8-8a47-cd10169b7c6b}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIPresentationDeviceProvider]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AndroidCastDeviceProvider]);