summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/secondscreen/RokuApp.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/secondscreen/RokuApp.jsm')
-rw-r--r--toolkit/modules/secondscreen/RokuApp.jsm255
1 files changed, 255 insertions, 0 deletions
diff --git a/toolkit/modules/secondscreen/RokuApp.jsm b/toolkit/modules/secondscreen/RokuApp.jsm
new file mode 100644
index 0000000000..7c458ebe34
--- /dev/null
+++ b/toolkit/modules/secondscreen/RokuApp.jsm
@@ -0,0 +1,255 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["RokuApp"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]);
+
+// function log(msg) {
+// Services.console.logStringMessage(msg);
+// }
+
+const PROTOCOL_VERSION = 1;
+
+/* RokuApp is a wrapper for interacting with a Roku channel.
+ * The basic interactions all use a REST API.
+ * spec: http://sdkdocs.roku.com/display/sdkdoc/External+Control+Guide
+ */
+function RokuApp(service) {
+ this.service = service;
+ this.resourceURL = this.service.location;
+ this.app = AppConstants.RELEASE_OR_BETA ? "Firefox" : "Firefox Nightly";
+ this.mediaAppID = -1;
+}
+
+RokuApp.prototype = {
+ status: function status(callback) {
+ // We have no way to know if the app is running, so just return "unknown"
+ // but we use this call to fetch the mediaAppID for the given app name
+ let url = this.resourceURL + "query/apps";
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ xhr.overrideMimeType("text/xml");
+
+ xhr.addEventListener("load", () => {
+ if (xhr.status == 200) {
+ let doc = xhr.responseXML;
+ let apps = doc.querySelectorAll("app");
+ for (let app of apps) {
+ if (app.textContent == this.app) {
+ this.mediaAppID = app.id;
+ }
+ }
+ }
+
+ // Since ECP has no way of telling us if an app is running, we always return "unknown"
+ if (callback) {
+ callback({ state: "unknown" });
+ }
+ });
+
+ xhr.addEventListener("error", function() {
+ if (callback) {
+ callback({ state: "unknown" });
+ }
+ });
+
+ xhr.send(null);
+ },
+
+ start: function start(callback) {
+ // We need to make sure we have cached the mediaAppID
+ if (this.mediaAppID == -1) {
+ this.status(() => {
+ // If we found the mediaAppID, use it to make a new start call
+ if (this.mediaAppID != -1) {
+ this.start(callback);
+ } else {
+ // We failed to start the app, so let the caller know
+ callback(false);
+ }
+ });
+ return;
+ }
+
+ // Start a given app with any extra query data. Each app uses it's own data scheme.
+ // NOTE: Roku will also pass "source=external-control" as a param
+ let url =
+ this.resourceURL +
+ "launch/" +
+ this.mediaAppID +
+ "?version=" +
+ parseInt(PROTOCOL_VERSION);
+ let xhr = new XMLHttpRequest();
+ xhr.open("POST", url, true);
+ xhr.overrideMimeType("text/plain");
+
+ xhr.addEventListener("load", function() {
+ if (callback) {
+ callback(xhr.status === 200);
+ }
+ });
+
+ xhr.addEventListener("error", function() {
+ if (callback) {
+ callback(false);
+ }
+ });
+
+ xhr.send(null);
+ },
+
+ stop: function stop(callback) {
+ // Roku doesn't seem to support stopping an app, so let's just go back to
+ // the Home screen
+ let url = this.resourceURL + "keypress/Home";
+ let xhr = new XMLHttpRequest();
+ xhr.open("POST", url, true);
+ xhr.overrideMimeType("text/plain");
+
+ xhr.addEventListener("load", function() {
+ if (callback) {
+ callback(xhr.status === 200);
+ }
+ });
+
+ xhr.addEventListener("error", function() {
+ if (callback) {
+ callback(false);
+ }
+ });
+
+ xhr.send(null);
+ },
+
+ remoteMedia: function remoteMedia(callback, listener) {
+ if (this.mediaAppID != -1) {
+ if (callback) {
+ callback(new RemoteMedia(this.resourceURL, listener));
+ }
+ } else if (callback) {
+ callback();
+ }
+ },
+};
+
+/* RemoteMedia provides a wrapper for using TCP socket to control Roku apps.
+ * The server implementation must be built into the Roku receiver app.
+ */
+function RemoteMedia(url, listener) {
+ this._url = url;
+ this._listener = listener;
+ this._status = "uninitialized";
+
+ let serverURI = Services.io.newURI(this._url);
+ this._socket = Cc["@mozilla.org/network/socket-transport-service;1"]
+ .getService(Ci.nsISocketTransportService)
+ .createTransport([], serverURI.host, 9191, null);
+ this._outputStream = this._socket.openOutputStream(0, 0, 0);
+
+ this._scriptableStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+
+ this._inputStream = this._socket.openInputStream(0, 0, 0);
+ this._pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ this._pump.init(this._inputStream, 0, 0, true);
+ this._pump.asyncRead(this);
+}
+
+RemoteMedia.prototype = {
+ onStartRequest(request) {},
+
+ onDataAvailable(request, stream, offset, count) {
+ this._scriptableStream.init(stream);
+ let data = this._scriptableStream.read(count);
+ if (!data) {
+ return;
+ }
+
+ let msg = JSON.parse(data);
+ if (this._status === msg._s) {
+ return;
+ }
+
+ this._status = msg._s;
+
+ if (this._listener) {
+ // Check to see if we are getting the initial "connected" message
+ if (
+ this._status == "connected" &&
+ "onRemoteMediaStart" in this._listener
+ ) {
+ this._listener.onRemoteMediaStart(this);
+ }
+
+ if ("onRemoteMediaStatus" in this._listener) {
+ this._listener.onRemoteMediaStatus(this);
+ }
+ }
+ },
+
+ onStopRequest(request, result) {
+ if (this._listener && "onRemoteMediaStop" in this._listener) {
+ this._listener.onRemoteMediaStop(this);
+ }
+ },
+
+ _sendMsg: function _sendMsg(data) {
+ if (!data) {
+ return;
+ }
+
+ // Add the protocol version
+ data._v = PROTOCOL_VERSION;
+
+ let raw = JSON.stringify(data);
+ this._outputStream.write(raw, raw.length);
+ },
+
+ shutdown: function shutdown() {
+ this._outputStream.close();
+ this._inputStream.close();
+ },
+
+ get active() {
+ return this._socket && this._socket.isAlive();
+ },
+
+ play: function play() {
+ // TODO: add position support
+ this._sendMsg({ type: "PLAY" });
+ },
+
+ pause: function pause() {
+ this._sendMsg({ type: "STOP" });
+ },
+
+ load: function load(data) {
+ this._sendMsg({
+ type: "LOAD",
+ title: data.title,
+ source: data.source,
+ poster: data.poster,
+ });
+ },
+
+ get status() {
+ return this._status;
+ },
+};