Source: lib/cast/cast_sender.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.cast.CastSender');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.cast.CastUtils');
  20. goog.require('shaka.log');
  21. goog.require('shaka.util.Error');
  22. goog.require('shaka.util.FakeEvent');
  23. goog.require('shaka.util.IDestroyable');
  24. goog.require('shaka.util.PublicPromise');
  25. /**
  26. * @constructor
  27. * @struct
  28. * @param {string} receiverAppId The ID of the cast receiver application.
  29. * @param {function()} onStatusChanged A callback invoked when the cast status
  30. * changes.
  31. * @param {function()} onFirstCastStateUpdate A callback invoked when an
  32. * "update" event has been received for the first time.
  33. * @param {function(string, !shaka.util.FakeEvent)} onRemoteEvent A callback
  34. * invoked with target name and event when a remote event is received.
  35. * @param {function()} onResumeLocal A callback invoked when the local player
  36. * should resume playback. Called before the cached remote state is wiped.
  37. * @param {function()} onInitStateRequired A callback to get local player's.
  38. * state. Invoked when casting is initiated from Chrome's cast button.
  39. * @implements {shaka.util.IDestroyable}
  40. */
  41. shaka.cast.CastSender =
  42. function(receiverAppId, onStatusChanged, onFirstCastStateUpdate,
  43. onRemoteEvent, onResumeLocal, onInitStateRequired) {
  44. /** @private {string} */
  45. this.receiverAppId_ = receiverAppId;
  46. /** @private {?function()} */
  47. this.onStatusChanged_ = onStatusChanged;
  48. /** @private {?function()} */
  49. this.onFirstCastStateUpdate_ = onFirstCastStateUpdate;
  50. /** @private {boolean} */
  51. this.hasJoinedExistingSession_ = false;
  52. /** @private {?function(string, !shaka.util.FakeEvent)} */
  53. this.onRemoteEvent_ = onRemoteEvent;
  54. /** @private {?function()} */
  55. this.onResumeLocal_ = onResumeLocal;
  56. /** @private {?function()} */
  57. this.onInitStateRequired_ = onInitStateRequired;
  58. /** @private {boolean} */
  59. this.apiReady_ = false;
  60. /** @private {boolean} */
  61. this.isCasting_ = false;
  62. /** @private {string} */
  63. this.receiverName_ = '';
  64. /** @private {Object} */
  65. this.appData_ = null;
  66. /** @private {?function()} */
  67. this.onConnectionStatusChangedBound_ =
  68. this.onConnectionStatusChanged_.bind(this);
  69. /** @private {?function(string, string)} */
  70. this.onMessageReceivedBound_ = this.onMessageReceived_.bind(this);
  71. /** @private {Object} */
  72. this.cachedProperties_ = {
  73. 'video': {},
  74. 'player': {}
  75. };
  76. /** @private {number} */
  77. this.nextAsyncCallId_ = 0;
  78. /** @private {Object.<string, !shaka.util.PublicPromise>} */
  79. this.asyncCallPromises_ = {};
  80. /** @private {shaka.util.PublicPromise} */
  81. this.castPromise_ = null;
  82. };
  83. /** @private {boolean} */
  84. shaka.cast.CastSender.hasReceivers_ = false;
  85. /** @private {chrome.cast.Session} */
  86. shaka.cast.CastSender.session_ = null;
  87. /** @override */
  88. shaka.cast.CastSender.prototype.destroy = function() {
  89. this.rejectAllPromises_();
  90. if (shaka.cast.CastSender.session_) {
  91. this.removeListeners_();
  92. // Don't leave the session, so that this session can be re-used later if
  93. // necessary.
  94. }
  95. this.onStatusChanged_ = null;
  96. this.onRemoteEvent_ = null;
  97. this.onResumeLocal_ = null;
  98. this.apiReady_ = false;
  99. this.isCasting_ = false;
  100. this.appData_ = null;
  101. this.cachedProperties_ = null;
  102. this.asyncCallPromises_ = null;
  103. this.castPromise_ = null;
  104. this.onConnectionStatusChangedBound_ = null;
  105. this.onMessageReceivedBound_ = null;
  106. return Promise.resolve();
  107. };
  108. /**
  109. * @return {boolean} True if the cast API is available.
  110. */
  111. shaka.cast.CastSender.prototype.apiReady = function() {
  112. return this.apiReady_;
  113. };
  114. /**
  115. * @return {boolean} True if there are receivers.
  116. */
  117. shaka.cast.CastSender.prototype.hasReceivers = function() {
  118. return shaka.cast.CastSender.hasReceivers_;
  119. };
  120. /**
  121. * @return {boolean} True if we are currently casting.
  122. */
  123. shaka.cast.CastSender.prototype.isCasting = function() {
  124. return this.isCasting_;
  125. };
  126. /**
  127. * @return {string} The name of the Cast receiver device, if isCasting().
  128. */
  129. shaka.cast.CastSender.prototype.receiverName = function() {
  130. return this.receiverName_;
  131. };
  132. /**
  133. * @return {boolean} True if we have a cache of remote properties from the
  134. * receiver.
  135. */
  136. shaka.cast.CastSender.prototype.hasRemoteProperties = function() {
  137. return Object.keys(this.cachedProperties_['video']).length != 0;
  138. };
  139. /** Initialize the Cast API. */
  140. shaka.cast.CastSender.prototype.init = function() {
  141. // Check for the cast extension.
  142. if (!window.chrome || !chrome.cast || !chrome.cast.isAvailable) {
  143. // Not available yet, so wait to be notified if/when it is available.
  144. window.__onGCastApiAvailable = (function(loaded) {
  145. if (loaded) {
  146. this.init();
  147. }
  148. }).bind(this);
  149. return;
  150. }
  151. // The API is now available.
  152. delete window.__onGCastApiAvailable;
  153. this.apiReady_ = true;
  154. this.onStatusChanged_();
  155. let sessionRequest = new chrome.cast.SessionRequest(this.receiverAppId_);
  156. let apiConfig = new chrome.cast.ApiConfig(sessionRequest,
  157. this.onExistingSessionJoined_.bind(this),
  158. this.onReceiverStatusChanged_.bind(this),
  159. 'origin_scoped');
  160. // TODO: Have never seen this fail. When would it and how should we react?
  161. chrome.cast.initialize(apiConfig,
  162. function() { shaka.log.debug('CastSender: init'); },
  163. function(error) { shaka.log.error('CastSender: init error', error); });
  164. if (shaka.cast.CastSender.hasReceivers_) {
  165. // Fire a fake cast status change, to simulate the update that
  166. // would be fired normally.
  167. // This is after a brief delay, to give users a chance to add event
  168. // listeners.
  169. setTimeout(this.onStatusChanged_.bind(this), 20);
  170. }
  171. let oldSession = shaka.cast.CastSender.session_;
  172. if (oldSession && oldSession.status != chrome.cast.SessionStatus.STOPPED) {
  173. // The old session still exists, so re-use it.
  174. shaka.log.debug('CastSender: re-using existing connection');
  175. this.onExistingSessionJoined_(oldSession);
  176. } else {
  177. // The session has been canceled in the meantime, so ignore it.
  178. shaka.cast.CastSender.session_ = null;
  179. }
  180. };
  181. /**
  182. * Set application-specific data.
  183. *
  184. * @param {Object} appData Application-specific data to relay to the receiver.
  185. */
  186. shaka.cast.CastSender.prototype.setAppData = function(appData) {
  187. this.appData_ = appData;
  188. if (this.isCasting_) {
  189. this.sendMessage_({
  190. 'type': 'appData',
  191. 'appData': this.appData_
  192. });
  193. }
  194. };
  195. /**
  196. * @param {shaka.cast.CastUtils.InitStateType} initState Video and player state
  197. * to be sent to the receiver.
  198. * @return {!Promise} Resolved when connected to a receiver. Rejected if the
  199. * connection fails or is canceled by the user.
  200. */
  201. shaka.cast.CastSender.prototype.cast = function(initState) {
  202. if (!this.apiReady_) {
  203. return Promise.reject(new shaka.util.Error(
  204. shaka.util.Error.Severity.RECOVERABLE,
  205. shaka.util.Error.Category.CAST,
  206. shaka.util.Error.Code.CAST_API_UNAVAILABLE));
  207. }
  208. if (!shaka.cast.CastSender.hasReceivers_) {
  209. return Promise.reject(new shaka.util.Error(
  210. shaka.util.Error.Severity.RECOVERABLE,
  211. shaka.util.Error.Category.CAST,
  212. shaka.util.Error.Code.NO_CAST_RECEIVERS));
  213. }
  214. if (this.isCasting_) {
  215. return Promise.reject(new shaka.util.Error(
  216. shaka.util.Error.Severity.RECOVERABLE,
  217. shaka.util.Error.Category.CAST,
  218. shaka.util.Error.Code.ALREADY_CASTING));
  219. }
  220. this.castPromise_ = new shaka.util.PublicPromise();
  221. chrome.cast.requestSession(
  222. this.onSessionInitiated_.bind(this, initState),
  223. this.onConnectionError_.bind(this));
  224. return this.castPromise_;
  225. };
  226. /**
  227. * Shows user a cast dialog where they can choose to stop
  228. * casting. Relies on Chrome to perform disconnect if they do.
  229. * Doesn't do anything if not connected.
  230. */
  231. shaka.cast.CastSender.prototype.showDisconnectDialog = function() {
  232. if (!this.isCasting_) {
  233. return;
  234. }
  235. let initState = this.onInitStateRequired_();
  236. chrome.cast.requestSession(
  237. this.onSessionInitiated_.bind(this, initState),
  238. this.onConnectionError_.bind(this));
  239. };
  240. /**
  241. * Forces the receiver app to shut down by disconnecting. Does nothing if not
  242. * connected.
  243. */
  244. shaka.cast.CastSender.prototype.forceDisconnect = function() {
  245. if (!this.isCasting_) {
  246. return;
  247. }
  248. this.rejectAllPromises_();
  249. if (shaka.cast.CastSender.session_) {
  250. this.removeListeners_();
  251. shaka.cast.CastSender.session_.stop(function() {}, function() {});
  252. shaka.cast.CastSender.session_ = null;
  253. }
  254. };
  255. /**
  256. * Getter for properties of remote objects.
  257. * @param {string} targetName
  258. * @param {string} property
  259. * @return {?}
  260. */
  261. shaka.cast.CastSender.prototype.get = function(targetName, property) {
  262. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  263. 'Unexpected target name');
  264. const CastUtils = shaka.cast.CastUtils;
  265. if (targetName == 'video') {
  266. if (CastUtils.VideoVoidMethods.indexOf(property) >= 0) {
  267. return this.remoteCall_.bind(this, targetName, property);
  268. }
  269. } else if (targetName == 'player') {
  270. if (CastUtils.PlayerGetterMethodsThatRequireLive[property]) {
  271. let isLive = this.get('player', 'isLive')();
  272. goog.asserts.assert(isLive,
  273. property + ' should be called on a live stream!');
  274. // If the property shouldn't exist, return a fake function so that the
  275. // user doesn't call an undefined function and get a second error.
  276. if (!isLive) {
  277. return () => undefined;
  278. }
  279. }
  280. if (CastUtils.PlayerVoidMethods.indexOf(property) >= 0) {
  281. return this.remoteCall_.bind(this, targetName, property);
  282. }
  283. if (CastUtils.PlayerPromiseMethods.indexOf(property) >= 0) {
  284. return this.remoteAsyncCall_.bind(this, targetName, property);
  285. }
  286. if (CastUtils.PlayerGetterMethods[property]) {
  287. return this.propertyGetter_.bind(this, targetName, property);
  288. }
  289. }
  290. return this.propertyGetter_(targetName, property);
  291. };
  292. /**
  293. * Setter for properties of remote objects.
  294. * @param {string} targetName
  295. * @param {string} property
  296. * @param {?} value
  297. */
  298. shaka.cast.CastSender.prototype.set = function(targetName, property, value) {
  299. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  300. 'Unexpected target name');
  301. this.cachedProperties_[targetName][property] = value;
  302. this.sendMessage_({
  303. 'type': 'set',
  304. 'targetName': targetName,
  305. 'property': property,
  306. 'value': value
  307. });
  308. };
  309. /**
  310. * @param {shaka.cast.CastUtils.InitStateType} initState
  311. * @param {chrome.cast.Session} session
  312. * @private
  313. */
  314. shaka.cast.CastSender.prototype.onSessionInitiated_ =
  315. function(initState, session) {
  316. shaka.log.debug('CastSender: onSessionInitiated');
  317. this.onSessionCreated_(session);
  318. this.sendMessage_({
  319. 'type': 'init',
  320. 'initState': initState,
  321. 'appData': this.appData_
  322. });
  323. this.castPromise_.resolve();
  324. };
  325. /**
  326. * @param {chrome.cast.Error} error
  327. * @private
  328. */
  329. shaka.cast.CastSender.prototype.onConnectionError_ = function(error) {
  330. // Default error code:
  331. let code = shaka.util.Error.Code.UNEXPECTED_CAST_ERROR;
  332. switch (error.code) {
  333. case 'cancel':
  334. code = shaka.util.Error.Code.CAST_CANCELED_BY_USER;
  335. break;
  336. case 'timeout':
  337. code = shaka.util.Error.Code.CAST_CONNECTION_TIMED_OUT;
  338. break;
  339. case 'receiver_unavailable':
  340. code = shaka.util.Error.Code.CAST_RECEIVER_APP_UNAVAILABLE;
  341. break;
  342. }
  343. this.castPromise_.reject(new shaka.util.Error(
  344. shaka.util.Error.Severity.CRITICAL,
  345. shaka.util.Error.Category.CAST,
  346. code,
  347. error));
  348. };
  349. /**
  350. * @param {string} targetName
  351. * @param {string} property
  352. * @return {?}
  353. * @private
  354. */
  355. shaka.cast.CastSender.prototype.propertyGetter_ =
  356. function(targetName, property) {
  357. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  358. 'Unexpected target name');
  359. return this.cachedProperties_[targetName][property];
  360. };
  361. /**
  362. * @param {string} targetName
  363. * @param {string} methodName
  364. * @private
  365. */
  366. shaka.cast.CastSender.prototype.remoteCall_ =
  367. function(targetName, methodName) {
  368. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  369. 'Unexpected target name');
  370. let args = Array.prototype.slice.call(arguments, 2);
  371. this.sendMessage_({
  372. 'type': 'call',
  373. 'targetName': targetName,
  374. 'methodName': methodName,
  375. 'args': args
  376. });
  377. };
  378. /**
  379. * @param {string} targetName
  380. * @param {string} methodName
  381. * @return {!Promise}
  382. * @private
  383. */
  384. shaka.cast.CastSender.prototype.remoteAsyncCall_ =
  385. function(targetName, methodName) {
  386. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  387. 'Unexpected target name');
  388. let args = Array.prototype.slice.call(arguments, 2);
  389. let p = new shaka.util.PublicPromise();
  390. let id = this.nextAsyncCallId_.toString();
  391. this.nextAsyncCallId_++;
  392. this.asyncCallPromises_[id] = p;
  393. this.sendMessage_({
  394. 'type': 'asyncCall',
  395. 'targetName': targetName,
  396. 'methodName': methodName,
  397. 'args': args,
  398. 'id': id
  399. });
  400. return p;
  401. };
  402. /**
  403. * @param {chrome.cast.Session} session
  404. * @private
  405. */
  406. shaka.cast.CastSender.prototype.onExistingSessionJoined_ = function(session) {
  407. shaka.log.debug('CastSender: onExistingSessionJoined');
  408. let initState = this.onInitStateRequired_();
  409. this.castPromise_ = new shaka.util.PublicPromise();
  410. this.hasJoinedExistingSession_ = true;
  411. this.onSessionInitiated_(initState, session);
  412. };
  413. /**
  414. * @param {string} availability
  415. * @private
  416. */
  417. shaka.cast.CastSender.prototype.onReceiverStatusChanged_ =
  418. function(availability) {
  419. // The cast extension is telling us whether there are any cast receiver
  420. // devices available.
  421. shaka.log.debug('CastSender: receiver status', availability);
  422. shaka.cast.CastSender.hasReceivers_ = availability == 'available';
  423. this.onStatusChanged_();
  424. };
  425. /**
  426. * @param {chrome.cast.Session} session
  427. * @private
  428. */
  429. shaka.cast.CastSender.prototype.onSessionCreated_ = function(session) {
  430. shaka.cast.CastSender.session_ = session;
  431. session.addUpdateListener(this.onConnectionStatusChangedBound_);
  432. session.addMessageListener(shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE,
  433. this.onMessageReceivedBound_);
  434. this.onConnectionStatusChanged_();
  435. };
  436. /**
  437. * @private
  438. */
  439. shaka.cast.CastSender.prototype.removeListeners_ = function() {
  440. let session = shaka.cast.CastSender.session_;
  441. session.removeUpdateListener(this.onConnectionStatusChangedBound_);
  442. session.removeMessageListener(shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE,
  443. this.onMessageReceivedBound_);
  444. };
  445. /**
  446. * @private
  447. */
  448. shaka.cast.CastSender.prototype.onConnectionStatusChanged_ = function() {
  449. let connected = shaka.cast.CastSender.session_ ?
  450. shaka.cast.CastSender.session_.status == 'connected' :
  451. false;
  452. shaka.log.debug('CastSender: connection status', connected);
  453. if (this.isCasting_ && !connected) {
  454. // Tell CastProxy to transfer state back to local player.
  455. this.onResumeLocal_();
  456. // Clear whatever we have cached.
  457. for (let targetName in this.cachedProperties_) {
  458. this.cachedProperties_[targetName] = {};
  459. }
  460. this.rejectAllPromises_();
  461. }
  462. this.isCasting_ = connected;
  463. this.receiverName_ = connected ?
  464. shaka.cast.CastSender.session_.receiver.friendlyName :
  465. '';
  466. this.onStatusChanged_();
  467. };
  468. /**
  469. * Reject any async call promises that are still pending.
  470. * @private
  471. */
  472. shaka.cast.CastSender.prototype.rejectAllPromises_ = function() {
  473. for (let id in this.asyncCallPromises_) {
  474. let p = this.asyncCallPromises_[id];
  475. delete this.asyncCallPromises_[id];
  476. // Reject pending async operations as if they were interrupted.
  477. // At the moment, load() is the only async operation we are worried about.
  478. p.reject(new shaka.util.Error(
  479. shaka.util.Error.Severity.RECOVERABLE,
  480. shaka.util.Error.Category.PLAYER,
  481. shaka.util.Error.Code.LOAD_INTERRUPTED));
  482. }
  483. };
  484. /**
  485. * @param {string} namespace
  486. * @param {string} serialized
  487. * @private
  488. */
  489. shaka.cast.CastSender.prototype.onMessageReceived_ =
  490. function(namespace, serialized) {
  491. // Since this method is in the compiled library, make sure all messages passed
  492. // in here were created with quoted property names.
  493. let message = shaka.cast.CastUtils.deserialize(serialized);
  494. shaka.log.v2('CastSender: message', message);
  495. switch (message['type']) {
  496. case 'event': {
  497. let targetName = message['targetName'];
  498. let event = message['event'];
  499. let fakeEvent = new shaka.util.FakeEvent(event['type'], event);
  500. this.onRemoteEvent_(targetName, fakeEvent);
  501. break;
  502. }
  503. case 'update': {
  504. let update = message['update'];
  505. for (let targetName in update) {
  506. let target = this.cachedProperties_[targetName] || {};
  507. for (let property in update[targetName]) {
  508. target[property] = update[targetName][property];
  509. }
  510. }
  511. if (this.hasJoinedExistingSession_) {
  512. this.onFirstCastStateUpdate_();
  513. this.hasJoinedExistingSession_ = false;
  514. }
  515. break;
  516. }
  517. case 'asyncComplete': {
  518. let id = message['id'];
  519. let error = message['error'];
  520. let p = this.asyncCallPromises_[id];
  521. delete this.asyncCallPromises_[id];
  522. goog.asserts.assert(p, 'Unexpected async id');
  523. if (!p) break;
  524. if (error) {
  525. // This is a hacky way to reconstruct the serialized error.
  526. let reconstructedError = new shaka.util.Error(
  527. error.severity, error.category, error.code);
  528. for (let k in error) {
  529. (/** @type {Object} */(reconstructedError))[k] = error[k];
  530. }
  531. p.reject(reconstructedError);
  532. } else {
  533. p.resolve();
  534. }
  535. break;
  536. }
  537. }
  538. };
  539. /**
  540. * @param {!Object} message
  541. * @private
  542. */
  543. shaka.cast.CastSender.prototype.sendMessage_ = function(message) {
  544. // Since this method is in the compiled library, make sure all messages passed
  545. // in here were created with quoted property names.
  546. let serialized = shaka.cast.CastUtils.serialize(message);
  547. // TODO: have never seen this fail. When would it and how should we react?
  548. let session = shaka.cast.CastSender.session_;
  549. session.sendMessage(shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE,
  550. serialized,
  551. function() {}, // success callback
  552. shaka.log.error); // error callback
  553. };