1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * @fileoverview Description of this file. 7 * Class handling interaction with the cast extension session of the Chromoting 8 * host. It receives and sends extension messages from/to the host through 9 * the client session. It uses the Google Cast Chrome Sender API library to 10 * interact with nearby Cast receivers. 11 * 12 * Once it establishes connection with a Cast device (upon user choice), it 13 * creates a session, loads our registered receiver application and then becomes 14 * a message proxy between the host and cast device, helping negotiate 15 * their peer connection. 16 */ 17 18'use strict'; 19 20/** @suppress {duplicate} */ 21var remoting = remoting || {}; 22 23/** 24 * @constructor 25 * @param {!remoting.ClientSession} clientSession The client session to send 26 * cast extension messages to. 27 */ 28remoting.CastExtensionHandler = function(clientSession) { 29 /** @private */ 30 this.clientSession_ = clientSession; 31 32 /** @type {chrome.cast.Session} @private */ 33 this.session_ = null; 34 35 /** @type {string} @private */ 36 this.kCastNamespace_ = 'urn:x-cast:com.chromoting.cast.all'; 37 38 /** @type {string} @private */ 39 this.kApplicationId_ = "8A1211E3"; 40 41 /** @type {Array.<Object>} @private */ 42 this.messageQueue_ = []; 43 44 this.start_(); 45}; 46 47/** 48 * The id of the script node. 49 * @type {string} 50 * @private 51 */ 52remoting.CastExtensionHandler.prototype.SCRIPT_NODE_ID_ = 'cast-script-node'; 53 54/** 55 * Attempts to load the Google Cast Chrome Sender API libary. 56 * @private 57 */ 58remoting.CastExtensionHandler.prototype.start_ = function() { 59 var node = document.getElementById(this.SCRIPT_NODE_ID_); 60 if (node) { 61 console.error( 62 'Multiple calls to CastExtensionHandler.start_ not expected.'); 63 return; 64 } 65 66 // Create a script node to load the Cast Sender API. 67 node = document.createElement('script'); 68 node.id = this.SCRIPT_NODE_ID_; 69 node.src = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js"; 70 node.type = 'text/javascript'; 71 document.body.insertBefore(node, document.body.firstChild); 72 73 /** @type {remoting.CastExtensionHandler} */ 74 var that = this; 75 var onLoad = function() { 76 window['__onGCastApiAvailable'] = that.onGCastApiAvailable.bind(that); 77 78 }; 79 var onLoadError = function(event) { 80 console.error("Failed to load Chrome Cast Sender API."); 81 } 82 node.addEventListener('load', onLoad, false); 83 node.addEventListener('error', onLoadError, false); 84 85}; 86 87/** 88 * Process Cast Extension Messages from the Chromoting host. 89 * @param {string} msgString The extension message's data. 90 */ 91remoting.CastExtensionHandler.prototype.onMessage = function(msgString) { 92 var message = getJsonObjectFromString(msgString); 93 94 // Save messages to send after a session is established. 95 this.messageQueue_.push(message); 96 // Trigger the sending of pending messages, followed by the one just 97 // received. 98 if (this.session_) { 99 this.sendPendingMessages_(); 100 } 101}; 102 103/** 104 * Send cast-extension messages through the client session. 105 * @param {Object} response The JSON response to be sent to the host. The 106 * response object must contain the appropriate keys. 107 * @private 108 */ 109remoting.CastExtensionHandler.prototype.sendMessageToHost_ = 110 function(response) { 111 this.clientSession_.sendCastExtensionMessage(response); 112}; 113 114/** 115 * Send pending messages from the host to the receiver app. 116 * @private 117 */ 118remoting.CastExtensionHandler.prototype.sendPendingMessages_ = function() { 119 var len = this.messageQueue_.length; 120 for(var i = 0; i<len; i++) { 121 this.session_.sendMessage(this.kCastNamespace_, 122 this.messageQueue_[i], 123 this.sendMessageSuccess.bind(this), 124 this.sendMessageFailure.bind(this)); 125 } 126 this.messageQueue_ = []; 127}; 128 129/** 130 * Event handler for '__onGCastApiAvailable' window event. This event is 131 * triggered if the Google Cast Chrome Sender API is available. We attempt to 132 * load this API in this.start(). If the API loaded successfully, we can proceed 133 * to initialize it and configure it to launch our Cast Receiver Application. 134 * 135 * @param {boolean} loaded True if the API loaded succesfully. 136 * @param {Object} errorInfo Info if the API load failed. 137 */ 138remoting.CastExtensionHandler.prototype.onGCastApiAvailable = 139 function(loaded, errorInfo) { 140 if (loaded) { 141 this.initializeCastApi(); 142 } else { 143 console.log(errorInfo); 144 } 145}; 146 147/** 148 * Initialize the Cast API. 149 * @private 150 */ 151remoting.CastExtensionHandler.prototype.initializeCastApi = function() { 152 var sessionRequest = new chrome.cast.SessionRequest(this.kApplicationId_); 153 var apiConfig = 154 new chrome.cast.ApiConfig(sessionRequest, 155 this.sessionListener.bind(this), 156 this.receiverListener.bind(this), 157 chrome.cast.AutoJoinPolicy.PAGE_SCOPED, 158 chrome.cast.DefaultActionPolicy.CREATE_SESSION); 159 chrome.cast.initialize( 160 apiConfig, this.onInitSuccess.bind(this), this.onInitError.bind(this)); 161}; 162 163/** 164 * Callback for successful initialization of the Cast API. 165 */ 166remoting.CastExtensionHandler.prototype.onInitSuccess = function() { 167 console.log("Initialization Successful."); 168}; 169 170/** 171 * Callback for failed initialization of the Cast API. 172 */ 173remoting.CastExtensionHandler.prototype.onInitError = function() { 174 console.error("Initialization Failed."); 175}; 176 177/** 178 * Listener invoked when a session is created or connected by the SDK. 179 * Note: The requestSession method would not cause this callback to be invoked 180 * since it is passed its own listener. 181 * @param {chrome.cast.Session} session The resulting session. 182 */ 183remoting.CastExtensionHandler.prototype.sessionListener = function(session) { 184 console.log('New Session:' + /** @type {string} */ (session.sessionId)); 185 this.session_ = session; 186 if (this.session_.media.length != 0) { 187 188 // There should be no media associated with the session, since we never 189 // directly load media from the Sender application. 190 this.onMediaDiscovered('sessionListener', this.session_.media[0]); 191 } 192 this.session_.addMediaListener( 193 this.onMediaDiscovered.bind(this, 'addMediaListener')); 194 this.session_.addUpdateListener(this.sessionUpdateListener.bind(this)); 195 this.session_.addMessageListener(this.kCastNamespace_, 196 this.chromotingMessageListener.bind(this)); 197 this.session_.sendMessage(this.kCastNamespace_, 198 {subject : 'test', chromoting_data : 'Hello, Cast.'}, 199 this.sendMessageSuccess.bind(this), 200 this.sendMessageFailure.bind(this)); 201 this.sendPendingMessages_(); 202}; 203 204/** 205 * Listener invoked when a media session is created by another sender. 206 * @param {string} how How this callback was triggered. 207 * @param {chrome.cast.media.Media} media The media item discovered. 208 * @private 209 */ 210remoting.CastExtensionHandler.prototype.onMediaDiscovered = 211 function(how, media) { 212 console.error("Unexpected media session discovered."); 213}; 214 215/** 216 * Listener invoked when a cast extension message was sent to the cast device 217 * successfully. 218 * @private 219 */ 220remoting.CastExtensionHandler.prototype.sendMessageSuccess = function() { 221}; 222 223/** 224 * Listener invoked when a cast extension message failed to be sent to the cast 225 * device. 226 * @param {Object} error The error. 227 * @private 228 */ 229remoting.CastExtensionHandler.prototype.sendMessageFailure = function(error) { 230 console.error('Failed to Send Message.', error); 231}; 232 233/** 234 * Listener invoked when a cast extension message is received from the Cast 235 * device. 236 * @param {string} ns The namespace of the message received. 237 * @param {string} message The stringified JSON message received. 238 */ 239remoting.CastExtensionHandler.prototype.chromotingMessageListener = 240 function(ns, message) { 241 if (ns === this.kCastNamespace_) { 242 try { 243 var messageObj = getJsonObjectFromString(message); 244 this.sendMessageToHost_(messageObj); 245 } catch (err) { 246 console.error('Failed to process message from Cast device.'); 247 } 248 } else { 249 console.error("Unexpected message from Cast device."); 250 } 251}; 252 253/** 254 * Listener invoked when there updates to the current session. 255 * 256 * @param {boolean} isAlive True if the session is still alive. 257 */ 258remoting.CastExtensionHandler.prototype.sessionUpdateListener = 259 function(isAlive) { 260 var message = isAlive ? 'Session Updated' : 'Session Removed'; 261 message += ': ' + this.session_.sessionId +'.'; 262 console.log(message); 263}; 264 265/** 266 * Listener invoked when the availability of a Cast receiver that supports 267 * the application in sessionRequest is known or changes. 268 * 269 * @param {chrome.cast.ReceiverAvailability} availability Receiver availability. 270 */ 271remoting.CastExtensionHandler.prototype.receiverListener = 272 function(availability) { 273 if (availability === chrome.cast.ReceiverAvailability.AVAILABLE) { 274 console.log("Receiver(s) Found."); 275 } else { 276 console.error("No Receivers Available."); 277 } 278}; 279 280/** 281 * Launches the associated receiver application by requesting that it be created 282 * on the Cast device. It uses the SessionRequest passed during initialization 283 * to determine what application to launch on the Cast device. 284 * 285 * Note: This method is intended to be used as a click listener for a custom 286 * cast button on the webpage. We currently use the default cast button in 287 * Chrome, so this method is unused. 288 */ 289remoting.CastExtensionHandler.prototype.launchApp = function() { 290 chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this), 291 this.onLaunchError.bind(this)); 292}; 293 294/** 295 * Listener invoked when chrome.cast.requestSession completes successfully. 296 * 297 * @param {chrome.cast.Session} session The requested session. 298 */ 299remoting.CastExtensionHandler.prototype.onRequestSessionSuccess = 300 function (session) { 301 this.session_ = session; 302 this.session_.addUpdateListener(this.sessionUpdateListener.bind(this)); 303 if (this.session_.media.length != 0) { 304 this.onMediaDiscovered('onRequestSession', this.session_.media[0]); 305 } 306 this.session_.addMediaListener( 307 this.onMediaDiscovered.bind(this, 'addMediaListener')); 308 this.session_.addMessageListener(this.kCastNamespace_, 309 this.chromotingMessageListener.bind(this)); 310}; 311 312/** 313 * Listener invoked when chrome.cast.requestSession fails. 314 * @param {chrome.cast.Error} error The error code. 315 */ 316remoting.CastExtensionHandler.prototype.onLaunchError = function(error) { 317 console.error("Error Casting to Receiver.", error); 318}; 319 320/** 321 * Stops the running receiver application associated with the session. 322 * TODO(aiguha): When the user disconnects using the blue drop down bar, 323 * the client session should notify the CastExtensionHandler, which should 324 * call this method to close the session with the Cast device. 325 */ 326remoting.CastExtensionHandler.prototype.stopApp = function() { 327 this.session_.stop(this.onStopAppSuccess.bind(this), 328 this.onStopAppError.bind(this)); 329}; 330 331/** 332 * Listener invoked when the receiver application is stopped successfully. 333 */ 334remoting.CastExtensionHandler.prototype.onStopAppSuccess = function() { 335}; 336 337/** 338 * Listener invoked when we fail to stop the receiver application. 339 * 340 * @param {chrome.cast.Error} error The error code. 341 */ 342remoting.CastExtensionHandler.prototype.onStopAppError = function(error) { 343 console.error('Error Stopping App: ', error); 344}; 345