Mypal/dom/presentation/provider/AndroidCastDeviceProvider.js

462 lines
16 KiB
JavaScript

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