diff options
Diffstat (limited to 'dom/presentation/provider/AndroidCastDeviceProvider.js')
-rw-r--r-- | dom/presentation/provider/AndroidCastDeviceProvider.js | 461 |
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]); |