Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 "use strict";
8 this.EXPORTED_SYMBOLS = ["RokuApp"];
10 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
12 Cu.import("resource://gre/modules/Services.jsm");
14 function log(msg) {
15 //Services.console.logStringMessage(msg);
16 }
18 const PROTOCOL_VERSION = 1;
20 /* RokuApp is a wrapper for interacting with a Roku channel.
21 * The basic interactions all use a REST API.
22 * spec: http://sdkdocs.roku.com/display/sdkdoc/External+Control+Guide
23 */
24 function RokuApp(service, app) {
25 this.service = service;
26 this.resourceURL = this.service.location;
27 this.app = app;
28 this.appID = -1;
29 }
31 RokuApp.prototype = {
32 status: function status(callback) {
33 // We have no way to know if the app is running, so just return "unknown"
34 // but we use this call to fetch the appID for the given app name
35 let url = this.resourceURL + "query/apps";
36 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
37 xhr.open("GET", url, true);
38 xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
39 xhr.overrideMimeType("text/xml");
41 xhr.addEventListener("load", (function() {
42 if (xhr.status == 200) {
43 let doc = xhr.responseXML;
44 let apps = doc.querySelectorAll("app");
45 for (let app of apps) {
46 if (app.textContent == this.app) {
47 this.appID = app.id;
48 }
49 }
50 }
52 // Since ECP has no way of telling us if an app is running, we always return "unknown"
53 if (callback) {
54 callback({ state: "unknown" });
55 }
56 }).bind(this), false);
58 xhr.addEventListener("error", (function() {
59 if (callback) {
60 callback({ state: "unknown" });
61 }
62 }).bind(this), false);
64 xhr.send(null);
65 },
67 start: function start(callback) {
68 // We need to make sure we have cached the appID
69 if (this.appID == -1) {
70 this.status(function() {
71 // If we found the appID, use it to make a new start call
72 if (this.appID != -1) {
73 this.start(callback);
74 } else {
75 // We failed to start the app, so let the caller know
76 callback(false);
77 }
78 }.bind(this));
79 return;
80 }
82 // Start a given app with any extra query data. Each app uses it's own data scheme.
83 // NOTE: Roku will also pass "source=external-control" as a param
84 let url = this.resourceURL + "launch/" + this.appID + "?version=" + parseInt(PROTOCOL_VERSION);
85 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
86 xhr.open("POST", url, true);
87 xhr.overrideMimeType("text/plain");
89 xhr.addEventListener("load", (function() {
90 if (callback) {
91 callback(xhr.status === 200);
92 }
93 }).bind(this), false);
95 xhr.addEventListener("error", (function() {
96 if (callback) {
97 callback(false);
98 }
99 }).bind(this), false);
101 xhr.send(null);
102 },
104 stop: function stop(callback) {
105 // Roku doesn't seem to support stopping an app, so let's just go back to
106 // the Home screen
107 let url = this.resourceURL + "keypress/Home";
108 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
109 xhr.open("POST", url, true);
110 xhr.overrideMimeType("text/plain");
112 xhr.addEventListener("load", (function() {
113 if (callback) {
114 callback(xhr.status === 200);
115 }
116 }).bind(this), false);
118 xhr.addEventListener("error", (function() {
119 if (callback) {
120 callback(false);
121 }
122 }).bind(this), false);
124 xhr.send(null);
125 },
127 remoteMedia: function remoteMedia(callback, listener) {
128 if (this.appID != -1) {
129 if (callback) {
130 callback(new RemoteMedia(this.resourceURL, listener));
131 }
132 } else {
133 if (callback) {
134 callback();
135 }
136 }
137 }
138 }
140 /* RemoteMedia provides a wrapper for using TCP socket to control Roku apps.
141 * The server implementation must be built into the Roku receiver app.
142 */
143 function RemoteMedia(url, listener) {
144 this._url = url;
145 this._listener = listener;
146 this._status = "uninitialized";
148 let serverURI = Services.io.newURI(this._url , null, null);
149 this._socket = Cc["@mozilla.org/network/socket-transport-service;1"].getService(Ci.nsISocketTransportService).createTransport(null, 0, serverURI.host, 9191, null);
150 this._outputStream = this._socket.openOutputStream(0, 0, 0);
152 this._scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);
154 this._inputStream = this._socket.openInputStream(0, 0, 0);
155 this._pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(Ci.nsIInputStreamPump);
156 this._pump.init(this._inputStream, -1, -1, 0, 0, true);
157 this._pump.asyncRead(this, null);
158 }
160 RemoteMedia.prototype = {
161 onStartRequest: function(request, context) {
162 },
164 onDataAvailable: function(request, context, stream, offset, count) {
165 this._scriptableStream.init(stream);
166 let data = this._scriptableStream.read(count);
167 if (!data) {
168 return;
169 }
171 let msg = JSON.parse(data);
172 if (this._status === msg._s) {
173 return;
174 }
176 this._status = msg._s;
178 if (this._listener) {
179 // Check to see if we are getting the initial "connected" message
180 if (this._status == "connected" && "onRemoteMediaStart" in this._listener) {
181 this._listener.onRemoteMediaStart(this);
182 }
184 if ("onRemoteMediaStatus" in this._listener) {
185 this._listener.onRemoteMediaStatus(this);
186 }
187 }
188 },
190 onStopRequest: function(request, context, result) {
191 if (this._listener && "onRemoteMediaStop" in this._listener)
192 this._listener.onRemoteMediaStop(this);
193 },
195 _sendMsg: function _sendMsg(data) {
196 if (!data)
197 return;
199 // Add the protocol version
200 data["_v"] = PROTOCOL_VERSION;
202 let raw = JSON.stringify(data);
203 this._outputStream.write(raw, raw.length);
204 },
206 shutdown: function shutdown() {
207 this._outputStream.close();
208 this._inputStream.close();
209 },
211 get active() {
212 return (this._socket && this._socket.isAlive());
213 },
215 play: function play() {
216 // TODO: add position support
217 this._sendMsg({ type: "PLAY" });
218 },
220 pause: function pause() {
221 this._sendMsg({ type: "STOP" });
222 },
224 load: function load(aData) {
225 this._sendMsg({ type: "LOAD", title: aData.title, source: aData.source, poster: aData.poster });
226 },
228 get status() {
229 return this._status;
230 }
231 }