|
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/. */ |
|
5 |
|
6 "use strict"; |
|
7 |
|
8 this.EXPORTED_SYMBOLS = ["RokuApp"]; |
|
9 |
|
10 const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
|
11 |
|
12 Cu.import("resource://gre/modules/Services.jsm"); |
|
13 |
|
14 function log(msg) { |
|
15 //Services.console.logStringMessage(msg); |
|
16 } |
|
17 |
|
18 const PROTOCOL_VERSION = 1; |
|
19 |
|
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 } |
|
30 |
|
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"); |
|
40 |
|
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 } |
|
51 |
|
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); |
|
57 |
|
58 xhr.addEventListener("error", (function() { |
|
59 if (callback) { |
|
60 callback({ state: "unknown" }); |
|
61 } |
|
62 }).bind(this), false); |
|
63 |
|
64 xhr.send(null); |
|
65 }, |
|
66 |
|
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 } |
|
81 |
|
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"); |
|
88 |
|
89 xhr.addEventListener("load", (function() { |
|
90 if (callback) { |
|
91 callback(xhr.status === 200); |
|
92 } |
|
93 }).bind(this), false); |
|
94 |
|
95 xhr.addEventListener("error", (function() { |
|
96 if (callback) { |
|
97 callback(false); |
|
98 } |
|
99 }).bind(this), false); |
|
100 |
|
101 xhr.send(null); |
|
102 }, |
|
103 |
|
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"); |
|
111 |
|
112 xhr.addEventListener("load", (function() { |
|
113 if (callback) { |
|
114 callback(xhr.status === 200); |
|
115 } |
|
116 }).bind(this), false); |
|
117 |
|
118 xhr.addEventListener("error", (function() { |
|
119 if (callback) { |
|
120 callback(false); |
|
121 } |
|
122 }).bind(this), false); |
|
123 |
|
124 xhr.send(null); |
|
125 }, |
|
126 |
|
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 } |
|
139 |
|
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"; |
|
147 |
|
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); |
|
151 |
|
152 this._scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); |
|
153 |
|
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 } |
|
159 |
|
160 RemoteMedia.prototype = { |
|
161 onStartRequest: function(request, context) { |
|
162 }, |
|
163 |
|
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 } |
|
170 |
|
171 let msg = JSON.parse(data); |
|
172 if (this._status === msg._s) { |
|
173 return; |
|
174 } |
|
175 |
|
176 this._status = msg._s; |
|
177 |
|
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 } |
|
183 |
|
184 if ("onRemoteMediaStatus" in this._listener) { |
|
185 this._listener.onRemoteMediaStatus(this); |
|
186 } |
|
187 } |
|
188 }, |
|
189 |
|
190 onStopRequest: function(request, context, result) { |
|
191 if (this._listener && "onRemoteMediaStop" in this._listener) |
|
192 this._listener.onRemoteMediaStop(this); |
|
193 }, |
|
194 |
|
195 _sendMsg: function _sendMsg(data) { |
|
196 if (!data) |
|
197 return; |
|
198 |
|
199 // Add the protocol version |
|
200 data["_v"] = PROTOCOL_VERSION; |
|
201 |
|
202 let raw = JSON.stringify(data); |
|
203 this._outputStream.write(raw, raw.length); |
|
204 }, |
|
205 |
|
206 shutdown: function shutdown() { |
|
207 this._outputStream.close(); |
|
208 this._inputStream.close(); |
|
209 }, |
|
210 |
|
211 get active() { |
|
212 return (this._socket && this._socket.isAlive()); |
|
213 }, |
|
214 |
|
215 play: function play() { |
|
216 // TODO: add position support |
|
217 this._sendMsg({ type: "PLAY" }); |
|
218 }, |
|
219 |
|
220 pause: function pause() { |
|
221 this._sendMsg({ type: "STOP" }); |
|
222 }, |
|
223 |
|
224 load: function load(aData) { |
|
225 this._sendMsg({ type: "LOAD", title: aData.title, source: aData.source, poster: aData.poster }); |
|
226 }, |
|
227 |
|
228 get status() { |
|
229 return this._status; |
|
230 } |
|
231 } |