1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/modules/RokuApp.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,231 @@ 1.4 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +"use strict"; 1.10 + 1.11 +this.EXPORTED_SYMBOLS = ["RokuApp"]; 1.12 + 1.13 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.14 + 1.15 +Cu.import("resource://gre/modules/Services.jsm"); 1.16 + 1.17 +function log(msg) { 1.18 + //Services.console.logStringMessage(msg); 1.19 +} 1.20 + 1.21 +const PROTOCOL_VERSION = 1; 1.22 + 1.23 +/* RokuApp is a wrapper for interacting with a Roku channel. 1.24 + * The basic interactions all use a REST API. 1.25 + * spec: http://sdkdocs.roku.com/display/sdkdoc/External+Control+Guide 1.26 + */ 1.27 +function RokuApp(service, app) { 1.28 + this.service = service; 1.29 + this.resourceURL = this.service.location; 1.30 + this.app = app; 1.31 + this.appID = -1; 1.32 +} 1.33 + 1.34 +RokuApp.prototype = { 1.35 + status: function status(callback) { 1.36 + // We have no way to know if the app is running, so just return "unknown" 1.37 + // but we use this call to fetch the appID for the given app name 1.38 + let url = this.resourceURL + "query/apps"; 1.39 + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); 1.40 + xhr.open("GET", url, true); 1.41 + xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; 1.42 + xhr.overrideMimeType("text/xml"); 1.43 + 1.44 + xhr.addEventListener("load", (function() { 1.45 + if (xhr.status == 200) { 1.46 + let doc = xhr.responseXML; 1.47 + let apps = doc.querySelectorAll("app"); 1.48 + for (let app of apps) { 1.49 + if (app.textContent == this.app) { 1.50 + this.appID = app.id; 1.51 + } 1.52 + } 1.53 + } 1.54 + 1.55 + // Since ECP has no way of telling us if an app is running, we always return "unknown" 1.56 + if (callback) { 1.57 + callback({ state: "unknown" }); 1.58 + } 1.59 + }).bind(this), false); 1.60 + 1.61 + xhr.addEventListener("error", (function() { 1.62 + if (callback) { 1.63 + callback({ state: "unknown" }); 1.64 + } 1.65 + }).bind(this), false); 1.66 + 1.67 + xhr.send(null); 1.68 + }, 1.69 + 1.70 + start: function start(callback) { 1.71 + // We need to make sure we have cached the appID 1.72 + if (this.appID == -1) { 1.73 + this.status(function() { 1.74 + // If we found the appID, use it to make a new start call 1.75 + if (this.appID != -1) { 1.76 + this.start(callback); 1.77 + } else { 1.78 + // We failed to start the app, so let the caller know 1.79 + callback(false); 1.80 + } 1.81 + }.bind(this)); 1.82 + return; 1.83 + } 1.84 + 1.85 + // Start a given app with any extra query data. Each app uses it's own data scheme. 1.86 + // NOTE: Roku will also pass "source=external-control" as a param 1.87 + let url = this.resourceURL + "launch/" + this.appID + "?version=" + parseInt(PROTOCOL_VERSION); 1.88 + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); 1.89 + xhr.open("POST", url, true); 1.90 + xhr.overrideMimeType("text/plain"); 1.91 + 1.92 + xhr.addEventListener("load", (function() { 1.93 + if (callback) { 1.94 + callback(xhr.status === 200); 1.95 + } 1.96 + }).bind(this), false); 1.97 + 1.98 + xhr.addEventListener("error", (function() { 1.99 + if (callback) { 1.100 + callback(false); 1.101 + } 1.102 + }).bind(this), false); 1.103 + 1.104 + xhr.send(null); 1.105 + }, 1.106 + 1.107 + stop: function stop(callback) { 1.108 + // Roku doesn't seem to support stopping an app, so let's just go back to 1.109 + // the Home screen 1.110 + let url = this.resourceURL + "keypress/Home"; 1.111 + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); 1.112 + xhr.open("POST", url, true); 1.113 + xhr.overrideMimeType("text/plain"); 1.114 + 1.115 + xhr.addEventListener("load", (function() { 1.116 + if (callback) { 1.117 + callback(xhr.status === 200); 1.118 + } 1.119 + }).bind(this), false); 1.120 + 1.121 + xhr.addEventListener("error", (function() { 1.122 + if (callback) { 1.123 + callback(false); 1.124 + } 1.125 + }).bind(this), false); 1.126 + 1.127 + xhr.send(null); 1.128 + }, 1.129 + 1.130 + remoteMedia: function remoteMedia(callback, listener) { 1.131 + if (this.appID != -1) { 1.132 + if (callback) { 1.133 + callback(new RemoteMedia(this.resourceURL, listener)); 1.134 + } 1.135 + } else { 1.136 + if (callback) { 1.137 + callback(); 1.138 + } 1.139 + } 1.140 + } 1.141 +} 1.142 + 1.143 +/* RemoteMedia provides a wrapper for using TCP socket to control Roku apps. 1.144 + * The server implementation must be built into the Roku receiver app. 1.145 + */ 1.146 +function RemoteMedia(url, listener) { 1.147 + this._url = url; 1.148 + this._listener = listener; 1.149 + this._status = "uninitialized"; 1.150 + 1.151 + let serverURI = Services.io.newURI(this._url , null, null); 1.152 + this._socket = Cc["@mozilla.org/network/socket-transport-service;1"].getService(Ci.nsISocketTransportService).createTransport(null, 0, serverURI.host, 9191, null); 1.153 + this._outputStream = this._socket.openOutputStream(0, 0, 0); 1.154 + 1.155 + this._scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); 1.156 + 1.157 + this._inputStream = this._socket.openInputStream(0, 0, 0); 1.158 + this._pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(Ci.nsIInputStreamPump); 1.159 + this._pump.init(this._inputStream, -1, -1, 0, 0, true); 1.160 + this._pump.asyncRead(this, null); 1.161 +} 1.162 + 1.163 +RemoteMedia.prototype = { 1.164 + onStartRequest: function(request, context) { 1.165 + }, 1.166 + 1.167 + onDataAvailable: function(request, context, stream, offset, count) { 1.168 + this._scriptableStream.init(stream); 1.169 + let data = this._scriptableStream.read(count); 1.170 + if (!data) { 1.171 + return; 1.172 + } 1.173 + 1.174 + let msg = JSON.parse(data); 1.175 + if (this._status === msg._s) { 1.176 + return; 1.177 + } 1.178 + 1.179 + this._status = msg._s; 1.180 + 1.181 + if (this._listener) { 1.182 + // Check to see if we are getting the initial "connected" message 1.183 + if (this._status == "connected" && "onRemoteMediaStart" in this._listener) { 1.184 + this._listener.onRemoteMediaStart(this); 1.185 + } 1.186 + 1.187 + if ("onRemoteMediaStatus" in this._listener) { 1.188 + this._listener.onRemoteMediaStatus(this); 1.189 + } 1.190 + } 1.191 + }, 1.192 + 1.193 + onStopRequest: function(request, context, result) { 1.194 + if (this._listener && "onRemoteMediaStop" in this._listener) 1.195 + this._listener.onRemoteMediaStop(this); 1.196 + }, 1.197 + 1.198 + _sendMsg: function _sendMsg(data) { 1.199 + if (!data) 1.200 + return; 1.201 + 1.202 + // Add the protocol version 1.203 + data["_v"] = PROTOCOL_VERSION; 1.204 + 1.205 + let raw = JSON.stringify(data); 1.206 + this._outputStream.write(raw, raw.length); 1.207 + }, 1.208 + 1.209 + shutdown: function shutdown() { 1.210 + this._outputStream.close(); 1.211 + this._inputStream.close(); 1.212 + }, 1.213 + 1.214 + get active() { 1.215 + return (this._socket && this._socket.isAlive()); 1.216 + }, 1.217 + 1.218 + play: function play() { 1.219 + // TODO: add position support 1.220 + this._sendMsg({ type: "PLAY" }); 1.221 + }, 1.222 + 1.223 + pause: function pause() { 1.224 + this._sendMsg({ type: "STOP" }); 1.225 + }, 1.226 + 1.227 + load: function load(aData) { 1.228 + this._sendMsg({ type: "LOAD", title: aData.title, source: aData.source, poster: aData.poster }); 1.229 + }, 1.230 + 1.231 + get status() { 1.232 + return this._status; 1.233 + } 1.234 +}