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