roku-ecp
Version:
A Node package designed to control Roku devices using TypeScript
202 lines (201 loc) • 9.32 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Roku = void 0;
const types_1 = require("./types");
const xml_js_1 = require("xml-js");
const util_1 = require("./util");
const app_1 = require("./app");
class Roku {
/**
* Initializes a new Roku given its location
* @param {string} location The URL of the device
*/
constructor(location) {
this.queue = [];
this.processing = false;
this.toString = () => `Roku (${this.location})`;
// sequentially execute keypresses
this.execute = (recursing, cmd) => __awaiter(this, void 0, void 0, function* () {
if (cmd)
this.queue.push(cmd);
if (!recursing && this.processing)
return;
if (!this.queue.length)
return (this.processing = false);
this.processing = true;
this.queue
.shift()()
.then(() => this.execute(true));
});
/**
* Sends a GET request
* @param {string} path Request URL
* @param {{}} params Request parameters
*/
this.get = (path, params) => fetch(`${this.location}${path}${util_1.queryString(params)}`);
/**
* Sends a POST request with no body
* @param {string} path Request URL
* @param {{}} params Request parameters
*/
this.post = (path, params) => fetch(`${this.location}${path}${util_1.queryString(params)}`, { method: "post" });
/**
* Enables an external client to drive the Roku Search UI to find and (optionally) launch content from an available provider.
* @param {{}} options Search parameters (keyword required)
*/
this.search = (options) => this.post("search/browse", options);
/**
* Equivalent to pressing down and releasing the identified remote control key. Can accept keyboard alphanumeric characters when a keyboard screen is active (see type)
* @param {string[]} keys A list of keys to press
*/
this.press = (...keys) => {
keys.forEach((key) => {
const literalKey = Object.keys(types_1.Keys).includes(key.toUpperCase())
? key
: `LIT_${key}`;
this.execute(false, () => this.post(`keypress/${literalKey}`));
});
};
/**
* Equivalent to pressing the identified remote control key
* @param {string} key Key to be pressed (case insensitive)
*/
this.keyDown = (key) => this.post(`keydown/${key}`);
/**
* Equivalent to releasing the identified remote control key
* @param {string} key Key to be released (case insensitive)
*/
this.keyUp = (key) => this.post(`keyup/${key}`);
/**
* Launches the identified app. Can accept launch parameters for deep linking.
* @param {number} id The id of the app
* @param {{}} options Launch parameters (for deep linking)
*/
this.launch = (id, options) => this.post(`launch/${id}`, options);
/**
* Sends custom events to the current application
* @param {{}} options Input parameters
*/
this.input = (options) => this.post("input", options);
/**
* Retrieves information about the device
* @returns {{}} Device details
*/
this.info = () => this.get("query/device-info")
.then((res) => res.text())
.then((xml) => util_1.parse(xml_js_1.xml2js(xml, { compact: true })["device-info"]));
/**
* Retrieves information about the current application
* @returns {{}} App details
*/
this.activeApp = () => this.get("query/active-app")
.then((res) => res.text())
.then((xml) => util_1.parse(xml_js_1.xml2js(xml, { compact: true })["active-app"]["app"]));
/**
* Retrieves information about the device's applications
* @returns {{}[]} An array of app details
*/
this.apps = () => this.get("query/apps")
.then((res) => res.text())
.then((xml) => xml_js_1.xml2js(xml, { compact: true })["apps"]["app"].map(util_1.parse));
/**
* Retrieves information about the currently tuned TV channel
* @remarks Restricted to Roku TV devices that support live TV
* @returns {{}} Channel details
*/
this.activeChannel = () => this.get("query/tv-active-channel")
.then((res) => res.text())
.then((xml) => util_1.parse(xml_js_1.xml2js(xml, { compact: true })["tv-channel"]["channel"]));
/**
* Retrieves information about the TV channel / line-up available for viewing in the TV tuner UI
* @remarks Restricted to Roku TV devices that support live TV
* @returns {{}[]} An array of channel details
*/
this.channels = () => this.get("query/tv-channels")
.then((res) => res.text())
.then((xml) => {
const parsed = xml_js_1.xml2js(xml, { compact: true })["tv-channels"];
if (parsed["channel"])
return parsed["channel"].map(util_1.parse);
return [];
});
/**
* Launches the identified channel
* @remarks Restricted to Roku TV devices that support live TV
* @param {number} id The channel number
*/
this.launchChannel = (id) => this.post("launch/tvinput.dtv", { ch: id });
/**
* Wait between key presses
* @param {number} ms Delay time in milliseconds
*/
this.wait = (ms) => this.execute(false, () => new Promise((resolve) => setTimeout(resolve, ms)));
/**
* Exits the current channel, and launches the Channel Store details screen of the identified app.
* @param {number} id The id of the app
*/
this.install = (id) => this.post(`install/${id}`);
/**
* Types alphanumeric characters, provided a keyboard screen is active
* @param {string} input Text to be typed
*/
this.type = (input) => input.split("").forEach((char) => this.press(char));
/**
* Sends custom sensor events to the device
* @param {"acceleration" | "orientation" | "rotation" | "magnetic"} input The sensor type
* @param {{x: number, y: number, z: number}} values The sensor input values
*/
this.sensor = (input, values) => {
const params = {};
Object.keys(values).forEach((key) => (params[`${input}.${key}`] = values[key]));
return this.input(params);
};
/**
* Sends custom touch or multi-touch events to the device
* @param {{x: number, y: number}} values The touch input values
* @param {"up" | "down" | "press" | "move" | "cancel"} op The touch operation
*/
this.touch = (values, op) => {
const params = op ? { "touch.0.op": op } : {};
Object.keys(values).forEach((key) => (params[`touch.0.${key}`] = values[key]));
return this.input(params);
};
/**
* Creates a new instance of the `App` class
* @param {{}} appInfo App information (id required)
* @returns {App} The new App
*/
this.app = (appInfo) => new app_1.App(appInfo, this);
/**
* Returns an icon corresponding to the identified application
* @param {number} id The id of the app
*/
this.icon = (id) => this.get(`query/icon/${id}`);
/**
* Retrieves information about the current stream segment and position of the content being played, the running time of the content, audio format, and buffering
* @returns {{}} Media player details
*/
this.mediaPlayer = () => this.get("query/media-player")
.then((res) => res.text())
.then((xml) => util_1.parse(xml_js_1.xml2js(xml, { compact: true })["player"]));
// if (!location) throw "No location provided" // -> regex should match http://*:8060 | empty
if (!location)
throw new Error("No device location provided");
if (!/http:\/\/(localhost|\d+\.\d+\.\d+\.\d+):8060\//.test(location))
throw new Error("Invalid device location should match http://*:8060/");
this.location = location;
}
}
exports.Roku = Roku;
// static members
Roku.discover = util_1.discover;
Roku.keys = types_1.Keys;