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.
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.
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.
20 /** @suppress {duplicate} */
21 var remoting = remoting || {};
25 * @implements {remoting.ProtocolExtension}
27 remoting.CastExtensionHandler = function() {
28 /** @private {chrome.cast.Session} */
31 /** @private {string} */
32 this.kCastNamespace_ = 'urn:x-cast:com.chromoting.cast.all';
34 /** @private {string} */
35 this.kApplicationId_ = "8A1211E3";
37 /** @private {Array<Object>} */
38 this.messageQueue_ = [];
40 /** @private {?function(string,string)} */
41 this.sendMessageToHostCallback_ = null;
44 /** @private {string} */
45 remoting.CastExtensionHandler.EXTENSION_TYPE = 'cast_message';
47 /** @return {Array<string>} */
48 remoting.CastExtensionHandler.prototype.getExtensionTypes = function() {
49 return [remoting.CastExtensionHandler.EXTENSION_TYPE];
53 * The id of the script node.
56 remoting.CastExtensionHandler.prototype.SCRIPT_NODE_ID_ = 'cast-script-node';
59 * Attempts to load the Google Cast Chrome Sender API libary.
61 * @param {function(string,string)} sendMessageToHost Callback to send a message
64 remoting.CastExtensionHandler.prototype.startExtension =
65 function(sendMessageToHost) {
66 this.sendMessageToHostCallback_ = sendMessageToHost;
68 var node = document.getElementById(this.SCRIPT_NODE_ID_);
71 'Multiple calls to CastExtensionHandler.start_ not expected.');
75 // Create a script node to load the Cast Sender API.
76 node = document.createElement('script');
77 node.id = this.SCRIPT_NODE_ID_;
78 node.src = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";
79 node.type = 'text/javascript';
80 document.body.insertBefore(node, document.body.firstChild);
82 /** @type {remoting.CastExtensionHandler} */
84 var onLoad = function() {
85 window['__onGCastApiAvailable'] = that.onGCastApiAvailable.bind(that);
88 var onLoadError = function(event) {
89 console.error("Failed to load Chrome Cast Sender API.");
91 node.addEventListener('load', onLoad, false);
92 node.addEventListener('error', onLoadError, false);
96 * Process Cast Extension Messages from the Chromoting host.
98 * @param {string} type The message type.
99 * @param {Object} message The parsed extension message data.
100 * @return {boolean} True if the extension message was handled.
102 remoting.CastExtensionHandler.prototype.onExtensionMessage =
103 function(type, message) {
104 // Save messages to send after a session is established.
105 this.messageQueue_.push(message);
106 // Trigger the sending of pending messages, followed by the one just
109 this.sendPendingMessages_();
115 * Send cast-extension messages through the host via the plugin.
116 * @param {Object} data The JSON response to be sent to the host. The
117 * response object must contain the appropriate keys.
120 remoting.CastExtensionHandler.prototype.sendMessageToHost_ = function(data) {
121 this.sendMessageToHostCallback_(remoting.CastExtensionHandler.EXTENSION_TYPE,
122 JSON.stringify(data));
126 * Send pending messages from the host to the receiver app.
129 remoting.CastExtensionHandler.prototype.sendPendingMessages_ = function() {
130 var len = this.messageQueue_.length;
131 for(var i = 0; i<len; i++) {
132 this.session_.sendMessage(this.kCastNamespace_,
133 this.messageQueue_[i],
134 this.sendMessageSuccess.bind(this),
135 this.sendMessageFailure.bind(this));
137 this.messageQueue_ = [];
141 * Event handler for '__onGCastApiAvailable' window event. This event is
142 * triggered if the Google Cast Chrome Sender API is available. We attempt to
143 * load this API in this.start(). If the API loaded successfully, we can proceed
144 * to initialize it and configure it to launch our Cast Receiver Application.
146 * @param {boolean} loaded True if the API loaded succesfully.
147 * @param {Object} errorInfo Info if the API load failed.
149 remoting.CastExtensionHandler.prototype.onGCastApiAvailable =
150 function(loaded, errorInfo) {
152 this.initializeCastApi();
154 console.log(errorInfo);
159 * Initialize the Cast API.
162 remoting.CastExtensionHandler.prototype.initializeCastApi = function() {
163 var sessionRequest = new chrome.cast.SessionRequest(this.kApplicationId_);
165 new chrome.cast.ApiConfig(sessionRequest,
166 this.sessionListener.bind(this),
167 this.receiverListener.bind(this),
168 chrome.cast.AutoJoinPolicy.PAGE_SCOPED,
169 chrome.cast.DefaultActionPolicy.CREATE_SESSION);
170 chrome.cast.initialize(
171 apiConfig, this.onInitSuccess.bind(this), this.onInitError.bind(this));
175 * Callback for successful initialization of the Cast API.
177 remoting.CastExtensionHandler.prototype.onInitSuccess = function() {
178 console.log("Initialization Successful.");
182 * Callback for failed initialization of the Cast API.
184 remoting.CastExtensionHandler.prototype.onInitError = function() {
185 console.error("Initialization Failed.");
189 * Listener invoked when a session is created or connected by the SDK.
190 * Note: The requestSession method would not cause this callback to be invoked
191 * since it is passed its own listener.
192 * @param {chrome.cast.Session} session The resulting session.
194 remoting.CastExtensionHandler.prototype.sessionListener = function(session) {
195 console.log('New Session:' + /** @type {string} */ (session.sessionId));
196 this.session_ = session;
197 if (this.session_.media.length != 0) {
199 // There should be no media associated with the session, since we never
200 // directly load media from the Sender application.
201 this.onMediaDiscovered('sessionListener', this.session_.media[0]);
203 this.session_.addMediaListener(
204 this.onMediaDiscovered.bind(this, 'addMediaListener'));
205 this.session_.addUpdateListener(this.sessionUpdateListener.bind(this));
206 this.session_.addMessageListener(this.kCastNamespace_,
207 this.chromotingMessageListener.bind(this));
208 this.session_.sendMessage(this.kCastNamespace_,
209 {subject : 'test', chromoting_data : 'Hello, Cast.'},
210 this.sendMessageSuccess.bind(this),
211 this.sendMessageFailure.bind(this));
212 this.sendPendingMessages_();
216 * Listener invoked when a media session is created by another sender.
217 * @param {string} how How this callback was triggered.
218 * @param {chrome.cast.media.Media} media The media item discovered.
221 remoting.CastExtensionHandler.prototype.onMediaDiscovered =
222 function(how, media) {
223 console.error("Unexpected media session discovered.");
227 * Listener invoked when a cast extension message was sent to the cast device
231 remoting.CastExtensionHandler.prototype.sendMessageSuccess = function() {
235 * Listener invoked when a cast extension message failed to be sent to the cast
237 * @param {Object} error The error.
240 remoting.CastExtensionHandler.prototype.sendMessageFailure = function(error) {
241 console.error('Failed to Send Message.', error);
245 * Listener invoked when a cast extension message is received from the Cast
247 * @param {string} ns The namespace of the message received.
248 * @param {string} message The stringified JSON message received.
250 remoting.CastExtensionHandler.prototype.chromotingMessageListener =
251 function(ns, message) {
252 if (ns === this.kCastNamespace_) {
254 var messageObj = base.getJsonObjectFromString(message);
255 this.sendMessageToHost_(messageObj);
257 console.error('Failed to process message from Cast device.');
260 console.error("Unexpected message from Cast device.");
265 * Listener invoked when there updates to the current session.
267 * @param {boolean} isAlive True if the session is still alive.
269 remoting.CastExtensionHandler.prototype.sessionUpdateListener =
271 var message = isAlive ? 'Session Updated' : 'Session Removed';
272 message += ': ' + this.session_.sessionId +'.';
273 console.log(message);
277 * Listener invoked when the availability of a Cast receiver that supports
278 * the application in sessionRequest is known or changes.
280 * @param {chrome.cast.ReceiverAvailability} availability Receiver availability.
282 remoting.CastExtensionHandler.prototype.receiverListener =
283 function(availability) {
284 if (availability === chrome.cast.ReceiverAvailability.AVAILABLE) {
285 console.log("Receiver(s) Found.");
287 console.error("No Receivers Available.");
292 * Launches the associated receiver application by requesting that it be created
293 * on the Cast device. It uses the SessionRequest passed during initialization
294 * to determine what application to launch on the Cast device.
296 * Note: This method is intended to be used as a click listener for a custom
297 * cast button on the webpage. We currently use the default cast button in
298 * Chrome, so this method is unused.
300 remoting.CastExtensionHandler.prototype.launchApp = function() {
301 chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this),
302 this.onLaunchError.bind(this));
306 * Listener invoked when chrome.cast.requestSession completes successfully.
308 * @param {chrome.cast.Session} session The requested session.
310 remoting.CastExtensionHandler.prototype.onRequestSessionSuccess =
312 this.session_ = session;
313 this.session_.addUpdateListener(this.sessionUpdateListener.bind(this));
314 if (this.session_.media.length != 0) {
315 this.onMediaDiscovered('onRequestSession', this.session_.media[0]);
317 this.session_.addMediaListener(
318 this.onMediaDiscovered.bind(this, 'addMediaListener'));
319 this.session_.addMessageListener(this.kCastNamespace_,
320 this.chromotingMessageListener.bind(this));
324 * Listener invoked when chrome.cast.requestSession fails.
325 * @param {chrome.cast.Error} error The error code.
327 remoting.CastExtensionHandler.prototype.onLaunchError = function(error) {
328 console.error("Error Casting to Receiver.", error);
332 * Stops the running receiver application associated with the session.
333 * TODO(aiguha): When the user disconnects using the blue drop down bar,
334 * the client session should notify the CastExtensionHandler, which should
335 * call this method to close the session with the Cast device.
337 remoting.CastExtensionHandler.prototype.stopApp = function() {
338 this.session_.stop(this.onStopAppSuccess.bind(this),
339 this.onStopAppError.bind(this));
343 * Listener invoked when the receiver application is stopped successfully.
345 remoting.CastExtensionHandler.prototype.onStopAppSuccess = function() {
349 * Listener invoked when we fail to stop the receiver application.
351 * @param {chrome.cast.Error} error The error code.
353 remoting.CastExtensionHandler.prototype.onStopAppError = function(error) {
354 console.error('Error Stopping App: ', error);