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
);