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 * @param {!remoting.ClientSession} clientSession The client session to send
26 * cast extension messages to.
28 remoting
.CastExtensionHandler = function(clientSession
) {
30 this.clientSession_
= clientSession
;
32 /** @type {chrome.cast.Session} @private */
35 /** @type {string} @private */
36 this.kCastNamespace_
= 'urn:x-cast:com.chromoting.cast.all';
38 /** @type {string} @private */
39 this.kApplicationId_
= "8A1211E3";
41 /** @type {Array.<Object>} @private */
42 this.messageQueue_
= [];
48 * The id of the script node.
52 remoting
.CastExtensionHandler
.prototype.SCRIPT_NODE_ID_
= 'cast-script-node';
55 * Attempts to load the Google Cast Chrome Sender API libary.
58 remoting
.CastExtensionHandler
.prototype.start_ = function() {
59 var node
= document
.getElementById(this.SCRIPT_NODE_ID_
);
62 'Multiple calls to CastExtensionHandler.start_ not expected.');
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
);
73 /** @type {remoting.CastExtensionHandler} */
75 var onLoad = function() {
76 window
['__onGCastApiAvailable'] = that
.onGCastApiAvailable
.bind(that
);
79 var onLoadError = function(event
) {
80 console
.error("Failed to load Chrome Cast Sender API.");
82 node
.addEventListener('load', onLoad
, false);
83 node
.addEventListener('error', onLoadError
, false);
88 * Process Cast Extension Messages from the Chromoting host.
89 * @param {string} msgString The extension message's data.
91 remoting
.CastExtensionHandler
.prototype.onMessage = function(msgString
) {
92 var message
= getJsonObjectFromString(msgString
);
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
99 this.sendPendingMessages_();
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.
109 remoting
.CastExtensionHandler
.prototype.sendMessageToHost_
=
111 this.clientSession_
.sendCastExtensionMessage(response
);
115 * Send pending messages from the host to the receiver app.
118 remoting
.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));
126 this.messageQueue_
= [];
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.
135 * @param {boolean} loaded True if the API loaded succesfully.
136 * @param {Object} errorInfo Info if the API load failed.
138 remoting
.CastExtensionHandler
.prototype.onGCastApiAvailable
=
139 function(loaded
, errorInfo
) {
141 this.initializeCastApi();
143 console
.log(errorInfo
);
148 * Initialize the Cast API.
151 remoting
.CastExtensionHandler
.prototype.initializeCastApi = function() {
152 var sessionRequest
= new chrome
.cast
.SessionRequest(this.kApplicationId_
);
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));
164 * Callback for successful initialization of the Cast API.
166 remoting
.CastExtensionHandler
.prototype.onInitSuccess = function() {
167 console
.log("Initialization Successful.");
171 * Callback for failed initialization of the Cast API.
173 remoting
.CastExtensionHandler
.prototype.onInitError = function() {
174 console
.error("Initialization Failed.");
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.
183 remoting
.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) {
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]);
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_();
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.
210 remoting
.CastExtensionHandler
.prototype.onMediaDiscovered
=
211 function(how
, media
) {
212 console
.error("Unexpected media session discovered.");
216 * Listener invoked when a cast extension message was sent to the cast device
220 remoting
.CastExtensionHandler
.prototype.sendMessageSuccess = function() {
224 * Listener invoked when a cast extension message failed to be sent to the cast
226 * @param {Object} error The error.
229 remoting
.CastExtensionHandler
.prototype.sendMessageFailure = function(error
) {
230 console
.error('Failed to Send Message.', error
);
234 * Listener invoked when a cast extension message is received from the Cast
236 * @param {string} ns The namespace of the message received.
237 * @param {string} message The stringified JSON message received.
239 remoting
.CastExtensionHandler
.prototype.chromotingMessageListener
=
240 function(ns
, message
) {
241 if (ns
=== this.kCastNamespace_
) {
243 var messageObj
= getJsonObjectFromString(message
);
244 this.sendMessageToHost_(messageObj
);
246 console
.error('Failed to process message from Cast device.');
249 console
.error("Unexpected message from Cast device.");
254 * Listener invoked when there updates to the current session.
256 * @param {boolean} isAlive True if the session is still alive.
258 remoting
.CastExtensionHandler
.prototype.sessionUpdateListener
=
260 var message
= isAlive
? 'Session Updated' : 'Session Removed';
261 message
+= ': ' + this.session_
.sessionId
+'.';
262 console
.log(message
);
266 * Listener invoked when the availability of a Cast receiver that supports
267 * the application in sessionRequest is known or changes.
269 * @param {chrome.cast.ReceiverAvailability} availability Receiver availability.
271 remoting
.CastExtensionHandler
.prototype.receiverListener
=
272 function(availability
) {
273 if (availability
=== chrome
.cast
.ReceiverAvailability
.AVAILABLE
) {
274 console
.log("Receiver(s) Found.");
276 console
.error("No Receivers Available.");
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.
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.
289 remoting
.CastExtensionHandler
.prototype.launchApp = function() {
290 chrome
.cast
.requestSession(this.onRequestSessionSuccess
.bind(this),
291 this.onLaunchError
.bind(this));
295 * Listener invoked when chrome.cast.requestSession completes successfully.
297 * @param {chrome.cast.Session} session The requested session.
299 remoting
.CastExtensionHandler
.prototype.onRequestSessionSuccess
=
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]);
306 this.session_
.addMediaListener(
307 this.onMediaDiscovered
.bind(this, 'addMediaListener'));
308 this.session_
.addMessageListener(this.kCastNamespace_
,
309 this.chromotingMessageListener
.bind(this));
313 * Listener invoked when chrome.cast.requestSession fails.
314 * @param {chrome.cast.Error} error The error code.
316 remoting
.CastExtensionHandler
.prototype.onLaunchError = function(error
) {
317 console
.error("Error Casting to Receiver.", error
);
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.
326 remoting
.CastExtensionHandler
.prototype.stopApp = function() {
327 this.session_
.stop(this.onStopAppSuccess
.bind(this),
328 this.onStopAppError
.bind(this));
332 * Listener invoked when the receiver application is stopped successfully.
334 remoting
.CastExtensionHandler
.prototype.onStopAppSuccess = function() {
338 * Listener invoked when we fail to stop the receiver application.
340 * @param {chrome.cast.Error} error The error code.
342 remoting
.CastExtensionHandler
.prototype.onStopAppError = function(error
) {
343 console
.error('Error Stopping App: ', error
);