cordova-plugin-connectsdk
Version:
Connect SDK allows discovery and communication with LG TVs and other smart devices on the local network.
1,696 lines (1,470 loc) • 60.2 kB
JavaScript
/*
* ConnectSDK.js
* Connect SDK
*
* Copyright (c) 2015 LG Electronics.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
var PLUGIN_ID = "ConnectSDK";
/** @mixin */
var SimpleEventEmitter = {
/**
* Add event listener.
* @param {string} event - name of event
* @param {function} callback - function to call when event is fired
* @param {object} [context] - object to bind to "this" value when calling function
* @returns {object} reference to the same object to allow chaining
*/
addListener: function (event, callback, context) {
if (!event) { throw new Error("missing parameter: event"); }
if (!callback) { throw new Error("missing parameter: callback"); }
this._listeners = this._listeners || {};
if (!this._listeners) this._listeners = {};
if (!this._listeners[event]) this._listeners[event] = [];
this._listeners[event].push({callback: callback, context: context});
this.emit("_addListener", event);
return this;
},
/**
* Remove event listener with the specified callback and context.
* If callback is null or undefined, all callbacks for this event will be removed.
* @param {string} event - name of event
* @param {function} [callback] - function originally passed to addListener
* @param {object} [context] - context object originally passed to addListener
* @returns {object} reference to the same object to allow chaining
*/
removeListener: function (event, callback, context) {
if (this._listeners && this._listeners[event]) {
this._listeners[event] = this._listeners[event].filter(function (l) {
return (callback && callback !== l.callback) && (context && context !== l.context);
});
}
this.emit("_removeListener", event);
return this;
},
hasListeners: function (event) {
if (event) {
return (this._listeners && this._listeners[event] && this._listeners[event].length > 0);
} else {
for (event in this._listeners) {
if (event[0] !== "_" && this._listeners.hasOwnProperty(event) && this._listeners[event].length > 0) {
return true;
}
}
return false;
}
},
emit: function (event) {
var listeners = this._listeners && this._listeners[event];
var args = Array.prototype.slice.call(arguments, 1);
if (listeners) {
listeners.forEach(function (l) {
l.callback.apply(l.context || null, args);
});
}
if (this["on" + event]) {
this["on" + event].apply(null, args);
}
},
/**
* Alias for addListener.
* @param {string} event - name of event
* @param {function} callback - function to call when event is fired
* @param {object} [context] - object to bind to "this" value when calling function
* @returns {object} reference to the same object to allow chaining
*/
on: function (event, callback, context) {
return this.addListener(event, callback, context);
},
/**
* Alias for removeListener.
* @param {string} event - event name
* @param {function} [callback] - function originally passed to on
* @param {object} [context] - context object originally passed to on
* @returns {object} reference to the same object to allow chaining
*/
off: function (event, callback, context) {
return this.removeListener(event, callback, context);
}
};
/** @mixin */
var SuccessCallbacks = {
/**
* Register a callback for the "success" event.
* The success callback may be called with zero or more arguments
* depending on the type of response.
*
* Example:
* ```js
* obj.success(function (result) {
* this.report("I got a result: " + result);
* }, this);
* ```
*
* @param {function} callback - function to call when event is fired
* @param {*} [context] - object to bind to "this" value when calling function
* @returns {object} reference to the same object to allow chaining
*/
success: function (callback, context) {
return this.on("success", callback, context);
},
/**
* Register a callback for the "error" event.
* The error callback will be called with a error object
* as the only argument.
*
* Example:
* ```js
* obj.error(function (err) {
* this.reportError("I got an error: " + err);
* }, this);
* ```
*
* @param {function} callback - function to call when event is fired
* @param {*} [context] - object to bind to "this" value when calling function
* @returns {object} reference to the same object to allow chaining
*/
error: function (callback, context) {
return this.on("error", callback, context);
},
/**
* Register a callback for the "complete" event.
* The complete callback will be called with
*
* Example:
* ```js
* obj.complete(function (err, result) {
* if (err) {
* this.report("I got an error: " + err);
* } else {
* console.log("I got a result: " + result);
* }
* }, this);
* ```
*
* @param {function} callback - function to call when event is fired
* @param {*} [context] - object to bind to "this" value when calling function
* @returns {object} reference to the same object to allow chaining
*/
complete: function (callback, context) {
return this.on("complete", callback, context);
}
};
// very simple class maker
var createClass = function (desc) {
var constructor;
if (desc.constructor) {
constructor = desc.constructor;
delete desc.constructor;
} else {
constructor = function () {};
throw new Error("no constructor");
}
var prototype = constructor.prototype;
if (desc.inherits) {
var inProto = desc.inherits.prototype;
for (var inProp in inProto) {
if (inProto.hasOwnProperty(inProp)) {
prototype[inProp] = inProto[inProp];
}
}
}
if (desc.mixins) {
desc.mixins.forEach(function (mixin) {
for (var mixinProp in mixin) {
if (mixin.hasOwnProperty(mixinProp)) {
prototype[mixinProp] = mixin[mixinProp];
}
}
});
delete desc.mixins;
}
if (desc.statics) {
for (var staticProp in desc.statics) {
if (desc.statics.hasOwnProperty(staticProp)) {
constructor[staticProp] = desc.statics[staticProp];
}
}
delete desc.statics;
}
for (var p in desc) {
if (desc.hasOwnProperty(p)) {
prototype[p] = desc[p];
}
}
return constructor;
};
/**
* Create a CapabilityFilter
* @param {string[]} capabilities - array of capabilities
*
* @class CapabilityFilter
* @classdesc
* CapabilityFilter consists of a list of capabilities which
* must all be present in order for the filter to match.
*
* For example, `new ConnectSDK.CapabilityFilter([ConnectSDK.Capabilities.MediaPlayer.Play.Video, ConnectSDK.Capabilities.MediaControl.Pause])`
* describes a device that supports showing a video and pausing it.
*/
var CapabilityFilter = createClass(
/** @lends CapabilityFilter.prototype */
{
constructor: function (capabilities) {
if (!capabilities) {
throw new Error("missing argument to CapabilityFilter constructor");
}
if (typeof capabilities === "string") {
capabilities = [capabilities];
}
this._capabilities = capabilities;
},
/**
* @returns {string[]} list of capabilities in filter
*/
getCapabilities: function () {
return this._capabilities.slice(0);
}
});
/**
* @class
* @classdesc
* DevicePicker represents a picker UI widget created by calling
* `DiscoveryManager.pickDevice()`.
*
* Example:
* ```js
* var devicePicker = ConnectSDK.discoveryManager.pickDevice()
* devicePicker.success(function (device) {
* console.log("picked device " + device.getFriendlyName());
* });
* ```
*
* @mixes SimpleEventEmitter
* @mixes SuccessCallbacks
*/
var DevicePicker = createClass(
/** @lends DevicePicker.prototype */
{
mixins: [SimpleEventEmitter, SuccessCallbacks],
constructor: function () {
},
/**
* Close the device picker.
*/
close: function () {
cordova.exec(null, null, PLUGIN_ID, "closeDevicePicker", []);
}
});
/**
* @constant
* @property {string} ON - access to capabilities that require pairing
* @property {string} OFF - access to capabilities that don't require pairing
*/
var PairingLevel = {
ON: "on",
OFF: "off"
};
/**
* @constant
* @property {string} NONE - Only connect if no pairing is required
* @property {string} FIRST_SCREEN - Prompt the user on the TV to accept paring
* @property {string} PIN - Display a PIN on the TV, require user to enter it on the device
* @property {string} MIXED - Prompt the user on the TV to accept pairing. Also display a pin on the TV that the user can enter on the device.
* @property {string} AIRPLAY_MIRRORING - Require AirPlay mirroring to be enabled for connection (iOS only)
*/
var PairingType = {
NONE: "NONE",
FIRST_SCREEN: "FIRST_SCREEN",
PIN: "PIN",
MIXED: "MIXED",
AIRPLAY_MIRRORING: "AIRPLAY_MIRRORING"
};
/**
* @constant
* @property {string} WEBAPP - display media using a web app mirrored to the TV (iOS only)
* @property {string} MEDIA - display media using AirPlay media playback APIs
*/
var AirPlayServiceMode = {
WEBAPP: "webapp",
MEDIA: "media"
};
/**
* @constant
* @property {string} Chromecast - Chromecast
* @property {string} DIAL - DIAL
* @property {string} DLNA - DLNA
* @property {string} NetcastTV - LG 2012/2013 Smart TV with Netcast
* @property {string} Roku - Roku
* @property {string} WebOSTV - LG 2014 Smart TV with webOS
* @property {string} FireTV - Amazon FireTV
* @property {string} AirPlay - Apple AirPlay
*/
var Services = {
Chromecast: "Chromecast",
DIAL: "DIAL",
DLNA: "DLNA",
NetcastTV: "NetcastTV",
Roku: "Roku",
WebOSTV: "webOS TV",
FireTV: "FireTV",
AirPlay: "AirPlay"
};
/**
* @constant
* @property {number} NUM_0
* @property {number} NUM_1
* @property {number} NUM_2
* @property {number} NUM_3
* @property {number} NUM_4
* @property {number} NUM_5
* @property {number} NUM_6
* @property {number} NUM_7
* @property {number} NUM_8
* @property {number} NUM_9
* @property {number} DASH
* @property {number} ENTER
*/
var KeyCodes = {
NUM_0: 0,
NUM_1: 1,
NUM_2: 2,
NUM_3: 3,
NUM_4: 4,
NUM_5: 5,
NUM_6: 6,
NUM_7: 7,
NUM_8: 8,
NUM_9: 9,
DASH: 10,
ENTER: 11
};
/**
* @class DiscoveryManager
* @classdesc ConnectSDK.discoveryManager is the main entry point into ConnectSDK.
* It allows finding devices on the local network and displaying a picker to
* select and connect to a device. DiscoveryManager should always be accessed
* through its singleton instance, ConnectSDK.discoveryManager.
*
* DiscoveryManager emits the following events while active:
*
* * startdiscovery
* * stopdiscovery
* * devicelistchanged
* * devicefound (device)
* * devicelost (device)
* * deviceupdated (device)
*
* @mixes SimpleEventEmitter
*/
var DiscoveryManager = createClass(
/** @lends DiscoveryManager.prototype */
{
mixins: [SimpleEventEmitter],
constructor: function () {
this._config = {};
this._devices = {};
},
_getDeviceByDesc: function (desc) {
var device = this._devices[desc.deviceId];
if (!device) {
device = new ConnectableDevice(desc);
this._devices[desc.deviceId] = device;
}
return device;
},
_handleDiscoveryUpdate: function (args) {
var event = args[0];
var update = args[1];
if (event) {
if (event === "startdiscovery") {
this._started = true;
} else if (event === "stopdiscovery") {
this._started = false;
}
if (event === "devicefound" || event === "devicelost" || event === "deviceupdated") {
var deviceId = update.device.deviceId;
var device = this._getDeviceByDesc(update.device);
if (event === "devicelost") {
delete this._devices[deviceId];
} else {
this._devices[deviceId] = device;
if (event === "deviceupdated") {
device._handleDiscoveryUpdate(update.device);
}
}
this.emit(event, device);
this.emit("devicelistchanged", this.getDeviceList());
} else {
this.emit.apply(this, args);
}
}
},
_handleDiscoveryError: function (error) {
//console.error("got discovery error " + error);
this.emit("error", error);
},
_setPairingLevel: function (pairingLevel, updateNow) {
if (!pairingLevel || (Object.prototype.toString.call(pairingLevel) !== "[object String]")) {
throw new TypeError("expected pairingLevel to be a string");
}
this._config.pairingLevel = pairingLevel;
if (updateNow) {
cordova.exec(null, null, PLUGIN_ID, "setDiscoveryConfig",
[{pairingLevel: this._config.pairingLevel}]);
}
},
_setAirPlayServiceMode: function (mode, updateNow) {
this._config.airPlayServiceMode = mode;
if (updateNow) {
cordova.exec(null, null, PLUGIN_ID, "setDiscoveryConfig",
[{airPlayServiceMode: this._config.airPlayServiceMode}]);
}
},
_setCapabilityFilters: function (filters, updateNow) {
filters = filters || [];
if (Object.prototype.toString.call(filters) !== "[object Array]") {
throw new TypeError("capabilityFilters should be an array");
}
filters = filters.map(function (filter) {
if (filter instanceof CapabilityFilter) {
return filter.getCapabilities();
} else if (Object.prototype.toString.call(filter) === "[object Array]") {
return filter;
} else {
throw new TypeError("filter objects must be CapabilityFilter instances or arrays of strings");
}
});
this._config.capabilityFilters = filters;
if (updateNow && this._started) {
cordova.exec(null, null, PLUGIN_ID, "setDiscoveryConfig",
[{capabilityFilters: this._config.capabilityFilters}]);
}
},
/**
* Start searching for devices. DiscoveryManager will start emitting events as the device
* list changes, and populates the device list used by pickDevice().
*
* @param {Object} [config] - Dictionary of settings to configure before starting discovery.
* Supported keys are "pairingLevel" and "capabilityFilters".
* See setPairingLevel and setCapabilityFilter for more details.
*/
startDiscovery: function (config) {
if (config) {
if (config.pairingLevel) {
this._setPairingLevel(config.pairingLevel, false);
}
if (config.airPlayServiceMode) {
this._setAirPlayServiceMode(config.airPlayServiceMode, false);
}
if (config.capabilityFilters) {
this._setCapabilityFilters(config.capabilityFilters, false);
}
}
cordova.exec(this._handleDiscoveryUpdate.bind(this), this._handleDiscoveryError.bind(this),
PLUGIN_ID, "startDiscovery", [this._config]);
},
/**
* Stop searching for devices.
*/
stopDiscovery: function () {
cordova.exec(null, this._handleDiscoveryError.bind(this), PLUGIN_ID, "stopDiscovery", [this.config]);
},
/**
* Set pairing level. If set to ConnectSDK.PairingLevel.OFF, the SDK will
* request device capabilities that do not require entering a pairing code/confirmation.
*
* @param {string} pairingLevel - Valid values are the constants ConnectSDK.PairingLevel.ON and ConnectSDk.PairingLevel.OFF
*/
setPairingLevel: function (pairingLevel) {
this._setPairingLevel(pairingLevel, true);
},
/**
* Set mode for AirPlay support. If set to ConnectSDK.AirPlayServiceMode.WebApp, a web app will
* will be mirrored to the TV. If set to ConnectSDK.AirPlayServiceMode.Media, only media
* APIs will be available. On Android, media mode is the only option.
*
* NOTE: This setting must be configured before calling startDiscovery(), or passed in the options
* parameter to startDiscovery(). The mode should not be changed once configured.
*/
setAirPlayServiceMode: function (mode) {
this._setAirPlayServiceMode(mode, true);
},
/**
* Set capability filters. DiscoveryManager will only show devices that match at least one of the
* CapabilityFilter instances.
*
* Example:
* ```js
* // Show devices that support playing videos and pausing OR support launching YouTube with a video id
* ConnectSDK.discoveryManager.setCapabilityFilters([
* new ConnectSDK.CapabilityFilter([ConnectSDK.Capabilities.MediaPlayer.Play.Video, ConnectSDK.Capabilities.MediaControl.Pause])
* new ConnectSDK.CapabilityFilter([ConnectSDK.Capabilities.Launcher.YouTube.Params])
* ])
* ```
*
* @param {CapabilityFilter[]} filters - array of CapabilityFilter objects
*/
setCapabilityFilters: function (filters) {
this._setCapabilityFilters(filters, true);
},
/**
* Show device picker popup. To get notified when the user has selected a device, add a success/error
* listener to the DevicePicker returned when calling this method.
*
* @param {Object} [options] - All keys are optional
*
* - pairingType (string): PairingType to use
* @returns {DevicePicker}
*/
pickDevice: function (options, successCallback, errorCallback) {
var self = this;
var picker = new DevicePicker();
if (successCallback) {
picker.on("success", successCallback);
}
if (errorCallback) {
picker.on("error", errorCallback);
}
var success = function (deviceDesc) {
var device = self._getDeviceByDesc(deviceDesc);
device._handleDiscoveryUpdate(deviceDesc);
picker.emit("success", device);
picker.emit("complete", undefined, device);
};
var failure = function (error) {
picker.emit("error", error);
picker.emit("complete", error);
};
cordova.exec(success, failure, PLUGIN_ID, "pickDevice", [options]);
return picker;
},
/**
* Get a list of discovered devices available on the network.
*
* @returns {ConnectableDevice[]}
*/
getDeviceList: function () {
var list = [];
for (var id in this._devices) {
list.push(this._devices[id]);
}
return list;
}
});
/**
* @class ConnectableDevice
* @classdesc ConnectableDevice represents a device on the network. It provides several
* _capability interfaces_ which allow the developer to get information from and control
* the device.
*
* These interfaces are accessed using getter methods like device.getLauncher(). Not all of
* the capabilities or methods are available on every device; you should check if the
* functionality is supported using device.supports(capabilityName).
*
* If the device was selected from the built-in picker, it will already be connected;
* if the device was obtained from elsewhere then you must call device.connect()
* and wait for the "ready" event before trying to use the device.
*
* Example:
* ```js
* device.on("ready", function () {
* // ready to send commands now
* device.getLauncher().launchYouTube(videoId);
* });
*
* device.connect();
* ```
*
* ConnectableDevice emits the following high-level events:
*
* * ready - device is ready to use
* * disconnect - device is no longer connected
* * capabilitieschanged - some capabilities may be available or unavailable now
*
* Internally, ConnectableDevice uses one or more _services_ to control a device on the network.
* Services speak a specific protocol like DIAL or DLNA or other vendor-specific protocols.
* Services are not directly accessible from the Connect SDK Cordova plugin at this time.
*
* There are several events related to the process of connecting to individual services:
*
* * serviceconnectionrequired - pending connection
* * serviceconnectionerror - error connecting to a service
* * servicepairingrequired - pairing is required for a service
* * servicepairingsuccess - pairing successful for a service
* * servicepairingerror - error pairing with a service
*
* @mixes SimpleEventEmitter
*/
var ConnectableDevice = createClass(
/** @lends ConnectableDevice.prototype */
{
mixins: [SimpleEventEmitter],
statics: {
_interfaceClasses: {},
_serviceWrappers: {},
_registerInterface: function (name, ifaceClass) {
var getterName = "get" + name[0].toUpperCase() + name.substr(1);
this._interfaceClasses[name] = ifaceClass;
this.prototype[getterName] = function () {
return this._interfaces[name];
};
},
_registerServiceWrapper: function (serviceName, wrapperFunc) {
if (!serviceName) {
throw new TypeError("invalid name: " + serviceName);
}
this._serviceWrappers[serviceName] = wrapperFunc;
}
},
constructor: function (desc) {
this._deviceId = desc.deviceId;
this._nextCommandId = 1;
this._interfaces = {};
this._desc = desc;
this._ready = desc.ready || false;
for (var name in ConnectableDevice._interfaceClasses) {
var IfaceClass = ConnectableDevice._interfaceClasses[name];
this._interfaces[name] = new IfaceClass(this);
}
this._capabilities = {};
this._subscribedToEvents = false;
this.on("_addListener", this._handleAddListener, this);
this._cacheServices();
this._cacheCapabilities();
},
_handleAddListener: function () {
if (this.hasListeners() && !this._subscribedToEvents) {
// Subscribe to events on the native device object
this._subscribedToEvents = true;
cordova.exec(this._handleUpdate.bind(this), this._handleError.bind(this), PLUGIN_ID, "setDeviceListener", [this._deviceId]);
}
},
_cacheServices: function () {
var services = this._desc.services;
delete this._desc.services;
if (services) {
this._services = {};
for (var i = 0; i < services.length; i += 1) {
var service = services[i];
if (service.name) {
this._services[service.name] = service;
}
}
}
},
_cacheCapabilities: function () {
var caps = this._desc.capabilities;
delete this._desc.capabilities;
if (caps) {
var capsHash = {};
for (var i = 0; i < caps.length; i += 1) {
capsHash[caps[i]] = true;
}
this._capabilities = capsHash;
}
},
_handleDiscoveryUpdate: function (desc) {
this._desc = desc;
this._cacheServices();
this._cacheCapabilities();
if (desc.ready !== undefined) {
this._ready = desc.ready;
}
},
_handleUpdate: function (args) {
var event = args[0];
var update = args[1];
if (event === "capabilitieschanged") {
var i, cap;
var added = update.added || [];
var removed = update.removed || [];
for (i = 0; i < added.length; i += 1) {
cap = added[i];
this._capabilities[cap] = true;
}
for (i = 0; i < removed.length; i += 1) {
cap = removed[i];
delete this._capabilities[cap];
}
args = [event]; // don't pass changes
} else if (event === "disconnect") {
this._capabilities = {};
this._ready = false;
} else if (event === "ready") {
this._ready = true;
}
this.emit.apply(this, args);
},
_handleError: function (error) {
this.emit("error", error);
},
/**
* Connect to the device.
*/
connect: function () {
this._subscribedToEvents = true;
cordova.exec(this._handleUpdate.bind(this), this._handleError.bind(this), PLUGIN_ID, "connectDevice", [this._deviceId]);
},
/**
* Disconnect from the device.
*/
disconnect: function () {
cordova.exec(null, this._handleError.bind(this), PLUGIN_ID, "disconnectDevice", [this._deviceId]);
},
/**
* Set a desirable pairing type to the device.
* @param pairingType (string): PairingType to use
*/
setPairingType: function (pairingType) {
cordova.exec(this._handleUpdate.bind(this), this._handleError.bind(this), PLUGIN_ID, "setPairingType", [this._deviceId, pairingType]);
},
/**
* Returns true if device is ready to use.
*/
isReady: function () {
return this._ready;
},
/**
* Get the human-readable name of the device.
* @returns {string}
*/
getFriendlyName: function () {
return this._desc.friendlyName;
},
/**
* Get the last known IP address of the device.
* @returns {string}
*/
getIPAddress: function () {
return this._desc.ipAddress || this._desc.lastKnownIPAddress;
},
/**
* Get the device model name.
* @returns {string}
*/
getModelName: function () {
return this._desc.modelName;
},
/**
* Get the device model number.
* @returns {string}
*/
getModelNumber: function () {
return this._desc.modelNumber;
},
/**
* Get a list of capabilities supported by this device.
* @returns {string[]} array of capabilities supported by this device
*/
getCapabilities: function () {
return Object.keys(this._capabilities);
},
/**
* @param {string} name of capability. You should use the ConnectSDK.Capabilities constant to reference strings.
* @returns {boolean} true if device supports the given capability
*/
hasCapability: function (cap) {
return !!this._capabilities[cap.toString()];
},
/**
* Flexible version of hasCapability which returns true
* if all of the capabilities specified are supported.
*
* * supports(ConnectSDK.Capabilities.MediaControl.Any)
* * supports(ConnectSDK.Capabilities.VolumeControl.Set, ConnectSDK.Capabilities.Launcher.Any)
* * supports([ConnectSDK.Capabilities.TVControl.Any, ConnectSDK.Capabilities.Launcher.Any])
* @param [...] - array of capability names. You should use the ConnectSDK.Capabilities constant to reference strings.
* @returns {boolean} true if all specified capabilities are supported
*/
supports: function (arg) {
var caps = [];
if (arguments.length === 1) {
if (Object.prototype.toString.call(arg) === "[object Array]") {
caps = arg;
} else {
caps = [arg];
}
} else if (arguments.length > 0) {
caps = arguments;
}
for (var i = 0; i < caps.length; i += 1) {
if (!this.hasCapability(caps[i])) {
return false;
}
}
return true;
},
/**
* Like supports() but returns true if any specified capability
* is supported.
* @param [...] - array of capability names. You should use the ConnectSDK.Capabilities constant to reference strings.
* @returns {boolean} true if any specified capability is supported
*/
supportsAny: function (arg) {
var caps = [];
if (arguments.length === 1) {
if (Object.prototype.toString.call(arg) === "[object Array]") {
caps = arg;
} else {
caps = [arg];
}
} else if (arguments.length > 0) {
caps = arguments;
}
for (var i = 0; i < caps.length; i += 1) {
if (this.hasCapability(caps[i])) {
return true;
}
}
return false;
},
/**
* Returns true if the device supports the specified service.
* See ConnectSDK.Services for a list of constants.
*
* @param {string} serviceName
* @returns {boolean} true if service is supported
*/
hasService: function (serviceName) {
if (!serviceName) {
throw new Error("hasService: service name argument is null or undefined");
}
return serviceName in this._services;
},
/**
* Returns a wrapper for a service which gives access to low-level
* functionality. Only a limited subset of the services supported
* by the native SDK are available through this plugin.
*
* @param {string} serviceName
* @returns {object} service object or null if not supported
*/
getService: function (serviceName) {
if (this.hasService(serviceName) && serviceName in ConnectableDevice._serviceWrappers) {
return ConnectableDevice._serviceWrappers[serviceName](this);
} else {
return null;
}
},
/**
* Returns an internal id assigned by the SDK to this device.
* For devices that have been connected to or paired, this
* id will be persisted to disk in the device store to allow
* the app to identify the device later (such as reconnecting
* to the last connected device when starting the app).
*/
getId: function () {
return this._deviceId;
},
_createCommandId: function () {
return this._deviceId + "_" + this._nextCommandId++;
},
_sendCommand: function (ifaceName, methodName, args, subscribe, responseWrapper) {
var command = this._createCommand(subscribe, responseWrapper);
command._send(ifaceName, methodName, args);
return command;
},
_createCommand: function (subscribe, responseWrapper) {
var options = {responseWrapper: responseWrapper};
var commandId = this._createCommandId();
var command = subscribe ? new Subscription(this, commandId, options) : new Command(this, commandId, options);
return command;
}
});
/**
* @class Command
* @classdesc
* Command objects are returned when calling capability methods.
* Command objects allow listening for success/cancel events from the
* request.
*
* Example:
* ```{.js}
* var command = device.getLauncher().launchBrowser(url);
*
* command.success(function (launchSession) {
* console.log("command was successful");
* }).error(function (err) {
* console.error("command failed");
* });
* ```
* @mixes SimpleEventEmitter
* @mixes SuccessCallbacks
*/
var Command = createClass(
/** @lends Command.prototype */
{
mixins: [SimpleEventEmitter, SuccessCallbacks],
_subscribe: false,
constructor: function (device, commandId, options) {
this._device = device;
this._commandId = commandId;
this._responseWrapper = options && options.responseWrapper;
},
_send: function (ifaceName, methodName, args) {
var self = this;
var success = function (update) {
var event = update[0];
if (event === "success") {
var data = update.slice(1);
if (self._responseWrapper) {
// call responseWrapper with [device, arg1, ...]
data = self._responseWrapper.apply(null, [self._device].concat(data));
}
self.emit.apply(self, ["success"].concat(data));
self.emit.apply(self, ["complete", undefined].concat(data));
} else {
self.emit.apply(self, update);
}
};
var failure = function (error) {
self.emit("error", error);
self.emit("complete", error);
};
cordova.exec(success, failure, PLUGIN_ID, "sendCommand",
[self._device._deviceId, self._commandId, ifaceName, methodName, args, self._subscribe]
);
}
});
/**
* @class Subscription
* @extends Command
* @classdesc
* Subscription objects are returned when calling capability subscription
* methods.
*
* Subscription objects allow listening for success/error events from the
* request. Success events may be emitted multiple times when updates to the
* subscription are received.
*
* Example:
* ```{.js}
* var subscription = device.getVolumeControl().subscribeVolume();
* var updateCount = 0;
*
* subscription.success(function (volume) {
* // this may be called multiple times
* console.log("got volume update: " + volume);
*
* updateCount++;
* if (updateCount > 5) {
* // unsubscribe after 5 updates
* subscription.unsubscribe();
* }
* }).error(function (err) {
* console.error("subscription failed");
* });
* ```
* @mixes SimpleEventEmitter
* @mixes SuccessCallbacks
*/
var Subscription = createClass(
/** @lends Subscription.prototype */
{
inherits: Command,
_subscribe: true,
constructor: function () {
Command.apply(this, arguments);
},
/**
* Unsubscribes from this subscription. Notifies the device that updates are no longer needed,
* and stops emitting events from this Subscription object.
*/
unsubscribe: function () {
cordova.exec(null, null, PLUGIN_ID, "cancelCommand", [this._device._deviceId, this._commandId]);
}
});
/**
* @mixin WrappedObject
*/
var WrappedObject = {
_scheduleCleanup: function () {
var self = this;
setTimeout(function () {
if (!self._acquireRequested) {
self.release();
}
}, 0);
},
_checkAcquired: function () {
if (!this._objectId) {
if (this._acquired) {
throw new Error(this._typeName + " instance was released and is no longer valid to use");
} else {
throw new Error(this._typeName + " instance not valid; you must call .acquire() immediately upon obtaining the object (such as in a success callback) and call .release() when done using it");
}
}
},
/**
* Indicate that you would like to keep an active reference to this object. Wrapped objects that are not acquired may be freed after the success callback returns.
* @returns {object} reference to object
*/
acquire: function () {
if (this._objectId === undefined) {
return this;
}
this._checkAcquired();
if (!this._acquired && !this._acquireRequested) {
this._acquireRequested = true;
// FIXME report error
cordova.exec(this._handleEvent.bind(this), null, PLUGIN_ID, "acquireWrappedObject", [this._objectId]);
}
return this;
},
/**
* Release the reference to this object. After calling .release(), this object may no longer be used.
* You should always release objects when you no longer need them, to avoid memory leaks.
*/
release: function () {
if (this._objectId) {
cordova.exec(null, null, PLUGIN_ID, "releaseWrappedObject", [this._objectId]);
this._objectId = null;
}
return this;
},
_handleEvent: function (args) {
this.emit.apply(this, args);
}
};
/**
* @class LaunchSession
* @mixes WrappedObject
* @classdesc A LaunchSession represents the result of an app launch. Its primary purpose is to be able to close an app that was previously launched, using the launchSession.close() method.
*/
var LaunchSession = createClass(
/** @lends LaunchSession.prototype */
{
mixins: [SimpleEventEmitter, WrappedObject],
_typeName: "LaunchSession",
constructor: function (device, data) {
this._device = device;
this._data = data;
this._objectId = data.objectId;
},
getAppId: function () {
return this._data.appId;
},
/** Close the app/media associated with this launch session. */
close: function () {
return this._device._sendCommand("CORDOVAPLUGIN", "closeLaunchSession", {"launchSession": this._data}, false);
},
toJSON: function () {
return this._data;
}
});
/**
* @class MediaControlWrapper
*/
var MediaControlWrapper = createClass(
/** @lends MediaControl.prototype */
{
mixins: [SimpleEventEmitter, WrappedObject],
constructor: function (device, data) {
this._device = device;
this._data = data;
this._objectId = data.objectId;
},
_sendCommand: function (command, params) {
params = params || {};
params.objectId = this._objectId;
return this._device._sendCommand("mediaControl", command, params);
},
play: function () {
return this._sendCommand("play");
},
pause: function () {
return this._sendCommand("pause");
},
stop: function () {
return this._sendCommand("stop");
},
rewind: function () {
return this._sendCommand("rewind");
},
fastForward: function () {
return this._sendCommand("fastForward");
},
seek: function (position) {
return this._sendCommand("seek", {position: position});
},
getDuration: function () {
return this._sendCommand("getDuration");
},
getPosition: function () {
return this._sendCommand("getPosition");
},
subscribePlayState: function () {
return this._sendCommand("subscribePlayState");
}
});
/**
* @class PlaylistControlWrapper
*/
var PlaylistControlWrapper = createClass(
/** @lends PlaylistControl.prototype */
{
mixins: [SimpleEventEmitter, WrappedObject],
constructor: function (device, data) {
this._device = device;
this._data = data;
this._objectId = data.objectId;
},
_sendCommand: function (command, params) {
params = params || {};
params.objectId = this._objectId;
return this._device._sendCommand("playlistControl", command, params);
},
next: function() {
return this._sendCommand("next");
},
previous: function() {
return this._sendCommand("previous");
},
jumpToTrack: function (index) {
return this._sendCommand("jumpToTrack", {"index": index});
}
});
/**
* @class WebAppSession
* @mixes SimpleEventEmitter
* @mixes WrappedObject
* @classdesc A WebAppSession represents a web-based app running on a TV.
* You can communicate with a web app by first calling connect() to establish a communication channel,
* and then listening for "message" events as well as sending your own messages using sendText and
* sendJSON.
*
* Example:
* ```js
* device.getWebAppLauncher().launchWebApp(webAppId).success(function (session) {
* this.session = session.acquire(); // hold on to a reference
*
* session.connect().success(function () {
* session.sendText("Hello world");
* });
*
* session.on('message', function (message) {
* // message could be either a string or an object
* if (typeof message === 'string') {
* console.log("received string message: " + message);
* } else {
* console.log("received object message: " + JSON.stringify(message);
* }
* }, this);
*
* session.on('disconnect', function () {
* console.log("session disconnected");
* this.session = null;
* }, this);
*
* }, this);
* ```
*/
var WebAppSession = createClass(
/** @lends WebAppSession.prototype */
{
mixins: [SimpleEventEmitter, WrappedObject],
_typeName: "WebAppSession",
constructor: function (device, data) {
this._device = device;
this._data = data;
this._objectId = data.objectId;
this._scheduleCleanup();
},
/**
* Open a message channel to the app.
* @returns {Command}
*/
connect: function () {
this.acquire();
return this._device._sendCommand("webAppSession", "connect", {objectId: this._objectId});
},
/**
* Close channel to app.
* @returns {Command}
*/
disconnect: function () {
this.acquire();
return this._device._sendCommand("webAppSession", "disconnect", {objectId: this._objectId});
},
/**
* Set web app session listener to app
* @returns {Command}
*/
setWebAppSessionListener: function() {
this.acquire();
return this._device._sendCommand("webAppSession", "setWebAppSessionListener", {objectId: this._objectId});
},
/**
* Send a text string to the app. Must be connected first.
* @param {string} text - Text to send to the app
* @returns {Command}
*/
sendText: function (text) {
this.acquire();
return this._device._sendCommand("webAppSession", "sendText", {objectId: this._objectId, text: text});
},
/**
* Send a plain JavaScript object to the app. Must be connected first.
* If the receiving app does not support non-string messages, the object will be serialized into a string in JSON format.
*
* @param {object} object - Plain JavaScript object to send to the app
* @returns {Command}
*/
sendJSON: function (obj) {
this.acquire();
return this._device._sendCommand("webAppSession", "sendJSON", {objectId: this._objectId, jsonObject: obj});
},
/**
* Close the web app.
* @returns {Command}
*/
close: function () {
return this._device._sendCommand("CORDOVAPLUGIN", "closeLaunchSession", {"launchSession": this._data.launchSession}, false);
},
toJSON: function () {
return this._data;
}
});
function wrapLaunchSession(device, launchSessionData) {
return [new LaunchSession(device, launchSessionData)];
}
function wrapMediaLaunchSession(device, launchSessionData, mediaControlData, playlistControlData) {
return [new LaunchSession(device, launchSessionData),
mediaControlData && new MediaControlWrapper(device, mediaControlData),
playlistControlData && new PlaylistControlWrapper(device, playlistControlData)];
}
function wrapWebAppSession(device, sessionData) {
return [new WebAppSession(device, sessionData)];
}
function createDeviceMethod(ifaceName, name, method) {
var f = function () {
var params = {};
var args = method.args || [];
for (var i = 0; i < args.length; i++) {
var arg = args[i];
var optional = false;
if (arg[arg.length - 1] === '?') {
arg = arg.substr(0, arg.length - 1);
optional = true;
}
if (!optional && (i > arguments.length)) {
if (console) { console.warn("missing parameter to " + name + ": " + arg); }
}
params[arg] = arguments[i];
}
return this._device._sendCommand(ifaceName, name, params, method.subscribe, method.responseWrapper);
};
return f;
}
function registerDeviceInterface (ifaceName, methods) {
var desc = {
constructor: function (device) {
this._device = device;
}
};
for (var name in methods) {
var method = methods[name];
if (typeof method === 'function') {
desc[name] = method;
} else if (typeof method === 'object') {
desc[name] = createDeviceMethod(ifaceName, name, method);
}
}
var ifaceClass = createClass(desc);
if (!exports.interfaces) { exports.interfaces = {}; }
exports.interfaces[ifaceName] = ifaceClass;
// Add getter method to ConnectableDevice
ConnectableDevice._registerInterface(ifaceName, ifaceClass);
}
/**
* @method ConnectableDevice.getLauncher
* @returns {Launcher}
*/
/**
* @callback launchCallback
* @param {LaunchSession} launchSession
*/
/**
* @callback appListCallback
* @param {AppInfo[]} appList - Each AppInfo object contains:
*
* - id (string): platform-specific appId
* - name (string): human-readable name of app
*/
/** @class Launcher */
registerDeviceInterface("launcher",
/** @lends Launcher.prototype */
{
/**
* @method
* @param {string} appId
* @success {launchCallback}
*/
launchApp: {
args: ["appId", "params?"],
responseWrapper: wrapLaunchSession
},
/**
* @method
* @param {string} appId
*/
closeApp: {
args: ["appId"],
responseWrapper: wrapLaunchSession
},
/**
* @method
* @param {string} appId
* @success {launchCallback}
*/
launchAppStore: {
args: ["appId"],
responseWrapper: wrapLaunchSession
},
/**
* @method
* @param {string} url
* @success {launchCallback}
*/
launchBrowser: {
args: ["url?"],
responseWrapper: wrapLaunchSession
},
/**
* @method
* @param {string} contentId
* @success {launchCallback}
*/
launchHulu: {
args: ["contentId?"],
responseWrapper: wrapLaunchSession
},
/**
* @method
* @param {string} contentId
* @success {launchCallback}
*/
launchNetflix: {
args: ["contentId?"],
responseWrapper: wrapLaunchSession
},
/**
* @method
* @param {string} contentId
* @success {launchCallback}
*/
launchYouTube: {
args: ["contentId?"],
responseWrapper: wrapLaunchSession
},
/**
* @method
* @success {appListCallback}
*/
getAppList: {
}
});
/**
* @method ConnectableDevice.getMediaPlayer
* @returns {MediaPlayer}
*/
/**
* @callback mediaLaunchCallback
* @param {LaunchSession} launchSession
* @param {MediaControl} mediaControl
*/
/** @class MediaPlayer */
registerDeviceInterface("mediaPlayer",
/** @lends MediaPlayer.prototype */
{
/**
* @method
* @param {string} url
* @param {string} mimeType
* @param {object} [options] All properties are optional:
*
* - title (string): Title text to display
* - description (string): Description text to display
* - iconUrl (string): URL of icon to show next to the title
* @success {mediaLaunchCallback}
*/
displayImage: {
args: ["url", "mimeType", "options?"],
responseWrapper: wrapMediaLaunchSession,
subscribe: true
},
/**
* @method
* @param {string} url
* @param {string} mimeType
* @param {object} [options] All properties are optional:
*
* - title (string): Title text to display
* - description (string): Description paragraph to display
* - iconUrl (string): URL of icon to show next to the title
* - shouldLoop (boolean): Whether to automatically loop playback
* - subtitles {object} subtitle track with options (properties are
* optional unless specified otherwise):
* - url (string) [required]: must be a valid URL
* - mimeType (string)
* - language (string)
* - label (string)
* @success {mediaLaunchCallback}
*/
playMedia: {
args: ["url", "mimeType", "options?"],
responseWrapper: wrapMediaLaunchSession,
subscribe: true
},
// deprecated
closeMedia: {
args: ["appInfo"]
}
});
/**
* @method ConnectableDevice.getExternalInputControl
* @returns {ExternalInputControl}
*/
/**
* @typedef {object} ExternalInputInfo
* @property {string} id
* @property {string} name
*/
/**
* @callback externalInputListCallback
* @param {ExternalInputInfo[]} externalInputList
*/
/**
* @class ExternalInputControl
* @classdesc
* ExternalInputInfo objects are plain JavaScript objects with the following properties:
*
* - id (string): A platform-specific id representing an input device
* - name (string): A human-readable name for the input device
*/
registerDeviceInterface("externalInputControl",
/** @lends ExternalInputControl.prototype */
{
/**
* @method
* @success {externalInputListCallback}
*/
getExternalInputList: {},
/**
* @method
* @param {object} externalInputInfo
*/
setExternalInput: {
args: