Source: lib/cast/cast_sender.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.cast.CastSender');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.cast.CastUtils');
  9. goog.require('shaka.log');
  10. goog.require('shaka.util.Error');
  11. goog.require('shaka.util.FakeEvent');
  12. goog.require('shaka.util.IDestroyable');
  13. goog.require('shaka.util.PublicPromise');
  14. goog.require('shaka.util.Timer');
  15. /**
  16. * @implements {shaka.util.IDestroyable}
  17. */
  18. shaka.cast.CastSender = class {
  19. /**
  20. * @param {string} receiverAppId The ID of the cast receiver application.
  21. * @param {function()} onStatusChanged A callback invoked when the cast status
  22. * changes.
  23. * @param {function()} onFirstCastStateUpdate A callback invoked when an
  24. * "update" event has been received for the first time.
  25. * @param {function(string, !shaka.util.FakeEvent)} onRemoteEvent A callback
  26. * invoked with target name and event when a remote event is received.
  27. * @param {function()} onResumeLocal A callback invoked when the local player
  28. * should resume playback. Called before the cached remote state is wiped.
  29. * @param {function()} onInitStateRequired A callback to get local player's.
  30. * state. Invoked when casting is initiated from Chrome's cast button.
  31. * @param {boolean} androidReceiverCompatible Indicates if the app is
  32. * compatible with an Android Receiver.
  33. */
  34. constructor(receiverAppId, onStatusChanged, onFirstCastStateUpdate,
  35. onRemoteEvent, onResumeLocal, onInitStateRequired,
  36. androidReceiverCompatible) {
  37. /** @private {string} */
  38. this.receiverAppId_ = receiverAppId;
  39. /** @private {boolean} */
  40. this.androidReceiverCompatible_ = androidReceiverCompatible;
  41. /** @private {shaka.util.Timer} */
  42. this.statusChangeTimer_ = new shaka.util.Timer(onStatusChanged);
  43. /** @private {?function()} */
  44. this.onFirstCastStateUpdate_ = onFirstCastStateUpdate;
  45. /** @private {boolean} */
  46. this.hasJoinedExistingSession_ = false;
  47. /** @private {?function(string, !shaka.util.FakeEvent)} */
  48. this.onRemoteEvent_ = onRemoteEvent;
  49. /** @private {?function()} */
  50. this.onResumeLocal_ = onResumeLocal;
  51. /** @private {?function()} */
  52. this.onInitStateRequired_ = onInitStateRequired;
  53. /** @private {boolean} */
  54. this.apiReady_ = false;
  55. /** @private {boolean} */
  56. this.isCasting_ = false;
  57. /** @private {string} */
  58. this.receiverName_ = '';
  59. /** @private {Object} */
  60. this.appData_ = null;
  61. /** @private {?function()} */
  62. this.onConnectionStatusChangedBound_ =
  63. () => this.onConnectionStatusChanged_();
  64. /** @private {?function(string, string)} */
  65. this.onMessageReceivedBound_ = (namespace, serialized) =>
  66. this.onMessageReceived_(namespace, serialized);
  67. /** @private {Object} */
  68. this.cachedProperties_ = {
  69. 'video': {},
  70. 'player': {},
  71. };
  72. /** @private {number} */
  73. this.nextAsyncCallId_ = 0;
  74. /** @private {Map<string, !shaka.util.PublicPromise>} */
  75. this.asyncCallPromises_ = new Map();
  76. /** @private {shaka.util.PublicPromise} */
  77. this.castPromise_ = null;
  78. shaka.cast.CastSender.instances_.add(this);
  79. }
  80. /** @override */
  81. destroy() {
  82. shaka.cast.CastSender.instances_.delete(this);
  83. this.rejectAllPromises_();
  84. if (shaka.cast.CastSender.session_) {
  85. this.removeListeners_();
  86. // Don't leave the session, so that this session can be re-used later if
  87. // necessary.
  88. }
  89. if (this.statusChangeTimer_) {
  90. this.statusChangeTimer_.stop();
  91. this.statusChangeTimer_ = null;
  92. }
  93. this.onRemoteEvent_ = null;
  94. this.onResumeLocal_ = null;
  95. this.apiReady_ = false;
  96. this.isCasting_ = false;
  97. this.appData_ = null;
  98. this.cachedProperties_ = null;
  99. this.asyncCallPromises_ = null;
  100. this.castPromise_ = null;
  101. this.onConnectionStatusChangedBound_ = null;
  102. this.onMessageReceivedBound_ = null;
  103. return Promise.resolve();
  104. }
  105. /**
  106. * @return {boolean} True if the cast API is available.
  107. */
  108. apiReady() {
  109. return this.apiReady_;
  110. }
  111. /**
  112. * @return {boolean} True if there are receivers.
  113. */
  114. hasReceivers() {
  115. return shaka.cast.CastSender.hasReceivers_;
  116. }
  117. /**
  118. * @return {boolean} True if we are currently casting.
  119. */
  120. isCasting() {
  121. return this.isCasting_;
  122. }
  123. /**
  124. * @return {string} The name of the Cast receiver device, if isCasting().
  125. */
  126. receiverName() {
  127. return this.receiverName_;
  128. }
  129. /**
  130. * @return {boolean} True if we have a cache of remote properties from the
  131. * receiver.
  132. */
  133. hasRemoteProperties() {
  134. return Object.keys(this.cachedProperties_['video']).length != 0;
  135. }
  136. /** Initialize the Cast API. */
  137. init() {
  138. const CastSender = shaka.cast.CastSender;
  139. if (!this.receiverAppId_.length) {
  140. // Return if no cast receiver id has been provided.
  141. // Nothing will be initialized, no global hooks will be installed.
  142. // If the receiver ID changes before this instance dies, init will be
  143. // called again.
  144. return;
  145. }
  146. // Check for the cast API.
  147. if (!window.chrome || !chrome.cast || !chrome.cast.isAvailable) {
  148. // If the API is not available on this platform or is not ready yet,
  149. // install a hook to be notified when it becomes available.
  150. // If the API becomes available before this instance dies, init will be
  151. // called again.
  152. // Check if our callback is already installed.
  153. if (window.__onGCastApiAvailable !== CastSender.onGCastApiAvailable_) {
  154. // Save pre-existing __onGCastApiAvailable in order to restore later.
  155. CastSender.__onGCastApiAvailable_ =
  156. window.__onGCastApiAvailable || null;
  157. window.__onGCastApiAvailable = CastSender.onGCastApiAvailable_;
  158. }
  159. return;
  160. }
  161. // The API is now available.
  162. this.apiReady_ = true;
  163. this.statusChangeTimer_.tickNow();
  164. // Use static versions of the API callbacks, since the ChromeCast API is
  165. // static. If we used local versions, we might end up retaining references
  166. // to destroyed players here.
  167. const sessionRequest = new chrome.cast.SessionRequest(this.receiverAppId_,
  168. /* capabilities= */ [],
  169. /* timeout= */ null,
  170. this.androidReceiverCompatible_,
  171. /* credentialsData= */null);
  172. const apiConfig = new chrome.cast.ApiConfig(sessionRequest,
  173. (session) => CastSender.onExistingSessionJoined_(session),
  174. (availability) => CastSender.onReceiverStatusChanged_(availability),
  175. 'origin_scoped');
  176. // TODO: Have never seen this fail. When would it and how should we react?
  177. chrome.cast.initialize(apiConfig,
  178. () => { shaka.log.debug('CastSender: init'); },
  179. (error) => { shaka.log.error('CastSender: init error', error); });
  180. if (shaka.cast.CastSender.hasReceivers_) {
  181. // Fire a fake cast status change, to simulate the update that
  182. // would be fired normally.
  183. // This is after a brief delay, to give users a chance to add event
  184. // listeners.
  185. this.statusChangeTimer_.tickAfter(shaka.cast.CastSender.STATUS_DELAY);
  186. }
  187. const oldSession = shaka.cast.CastSender.session_;
  188. if (oldSession && oldSession.status != chrome.cast.SessionStatus.STOPPED) {
  189. // The old session still exists, so re-use it.
  190. shaka.log.debug('CastSender: re-using existing connection');
  191. this.onExistingSessionJoined_(oldSession);
  192. } else {
  193. // The session has been canceled in the meantime, so ignore it.
  194. shaka.cast.CastSender.session_ = null;
  195. }
  196. }
  197. /**
  198. * Set application-specific data.
  199. *
  200. * @param {Object} appData Application-specific data to relay to the receiver.
  201. */
  202. setAppData(appData) {
  203. this.appData_ = appData;
  204. if (this.isCasting_) {
  205. this.sendMessage_({
  206. 'type': 'appData',
  207. 'appData': this.appData_,
  208. });
  209. }
  210. }
  211. /**
  212. * @return {!Promise} Resolved when connected to a receiver. Rejected if the
  213. * connection fails or is canceled by the user.
  214. */
  215. async cast() {
  216. if (!this.apiReady_) {
  217. throw new shaka.util.Error(
  218. shaka.util.Error.Severity.RECOVERABLE,
  219. shaka.util.Error.Category.CAST,
  220. shaka.util.Error.Code.CAST_API_UNAVAILABLE);
  221. }
  222. if (!shaka.cast.CastSender.hasReceivers_) {
  223. throw new shaka.util.Error(
  224. shaka.util.Error.Severity.RECOVERABLE,
  225. shaka.util.Error.Category.CAST,
  226. shaka.util.Error.Code.NO_CAST_RECEIVERS);
  227. }
  228. if (this.isCasting_) {
  229. throw new shaka.util.Error(
  230. shaka.util.Error.Severity.RECOVERABLE,
  231. shaka.util.Error.Category.CAST,
  232. shaka.util.Error.Code.ALREADY_CASTING);
  233. }
  234. this.castPromise_ = new shaka.util.PublicPromise();
  235. chrome.cast.requestSession(
  236. (session) => this.onSessionInitiated_(session),
  237. (error) => this.onConnectionError_(error));
  238. await this.castPromise_;
  239. }
  240. /**
  241. * Shows user a cast dialog where they can choose to stop
  242. * casting. Relies on Chrome to perform disconnect if they do.
  243. * Doesn't do anything if not connected.
  244. */
  245. showDisconnectDialog() {
  246. if (!this.isCasting_) {
  247. return;
  248. }
  249. chrome.cast.requestSession(
  250. (session) => this.onSessionInitiated_(session),
  251. (error) => this.onConnectionError_(error));
  252. }
  253. /**
  254. * Forces the receiver app to shut down by disconnecting. Does nothing if not
  255. * connected.
  256. */
  257. forceDisconnect() {
  258. if (!this.isCasting_) {
  259. return;
  260. }
  261. this.rejectAllPromises_();
  262. if (shaka.cast.CastSender.session_) {
  263. this.removeListeners_();
  264. // This can throw if we've already been disconnected somehow.
  265. try {
  266. shaka.cast.CastSender.session_.stop(() => {}, () => {});
  267. } catch (error) {}
  268. shaka.cast.CastSender.session_ = null;
  269. }
  270. // Update casting status.
  271. this.onConnectionStatusChanged_();
  272. }
  273. /**
  274. * Getter for properties of remote objects.
  275. * @param {string} targetName
  276. * @param {string} property
  277. * @return {?}
  278. */
  279. get(targetName, property) {
  280. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  281. 'Unexpected target name');
  282. const CastUtils = shaka.cast.CastUtils;
  283. if (targetName == 'video') {
  284. if (CastUtils.VideoVoidMethods.includes(property)) {
  285. return (...args) => this.remoteCall_(targetName, property, ...args);
  286. }
  287. } else if (targetName == 'player') {
  288. if (CastUtils.PlayerGetterMethodsThatRequireLive.has(property)) {
  289. const isLive = this.get('player', 'isLive')();
  290. goog.asserts.assert(isLive,
  291. property + ' should be called on a live stream!');
  292. // If the property shouldn't exist, return a fake function so that the
  293. // user doesn't call an undefined function and get a second error.
  294. if (!isLive) {
  295. return () => undefined;
  296. }
  297. }
  298. if (CastUtils.PlayerVoidMethods.includes(property)) {
  299. return (...args) => this.remoteCall_(targetName, property, ...args);
  300. }
  301. if (CastUtils.PlayerPromiseMethods.includes(property)) {
  302. return (...args) =>
  303. this.remoteAsyncCall_(targetName, property, ...args);
  304. }
  305. if (CastUtils.PlayerGetterMethods.has(property) ||
  306. CastUtils.LargePlayerGetterMethods.has(property)) {
  307. return () => this.propertyGetter_(targetName, property);
  308. }
  309. }
  310. return this.propertyGetter_(targetName, property);
  311. }
  312. /**
  313. * Setter for properties of remote objects.
  314. * @param {string} targetName
  315. * @param {string} property
  316. * @param {?} value
  317. */
  318. set(targetName, property, value) {
  319. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  320. 'Unexpected target name');
  321. this.cachedProperties_[targetName][property] = value;
  322. this.sendMessage_({
  323. 'type': 'set',
  324. 'targetName': targetName,
  325. 'property': property,
  326. 'value': value,
  327. });
  328. }
  329. /**
  330. * @param {chrome.cast.Session} session
  331. * @private
  332. */
  333. onSessionInitiated_(session) {
  334. shaka.log.debug('CastSender: onSessionInitiated');
  335. const initState = this.onInitStateRequired_();
  336. this.onSessionCreated_(session);
  337. this.sendMessage_({
  338. 'type': 'init',
  339. 'initState': initState,
  340. 'appData': this.appData_,
  341. });
  342. this.castPromise_.resolve();
  343. }
  344. /**
  345. * @param {chrome.cast.Error} error
  346. * @private
  347. */
  348. onConnectionError_(error) {
  349. // Default error code:
  350. let code = shaka.util.Error.Code.UNEXPECTED_CAST_ERROR;
  351. switch (error.code) {
  352. case 'cancel':
  353. code = shaka.util.Error.Code.CAST_CANCELED_BY_USER;
  354. break;
  355. case 'timeout':
  356. code = shaka.util.Error.Code.CAST_CONNECTION_TIMED_OUT;
  357. break;
  358. case 'receiver_unavailable':
  359. code = shaka.util.Error.Code.CAST_RECEIVER_APP_UNAVAILABLE;
  360. break;
  361. }
  362. this.castPromise_.reject(new shaka.util.Error(
  363. shaka.util.Error.Severity.CRITICAL,
  364. shaka.util.Error.Category.CAST,
  365. code,
  366. error));
  367. }
  368. /**
  369. * @param {string} targetName
  370. * @param {string} property
  371. * @return {?}
  372. * @private
  373. */
  374. propertyGetter_(targetName, property) {
  375. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  376. 'Unexpected target name');
  377. return this.cachedProperties_[targetName][property];
  378. }
  379. /**
  380. * @param {string} targetName
  381. * @param {string} methodName
  382. * @param {...*} varArgs
  383. * @private
  384. */
  385. remoteCall_(targetName, methodName, ...varArgs) {
  386. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  387. 'Unexpected target name');
  388. this.sendMessage_({
  389. 'type': 'call',
  390. 'targetName': targetName,
  391. 'methodName': methodName,
  392. 'args': varArgs,
  393. });
  394. }
  395. /**
  396. * @param {string} targetName
  397. * @param {string} methodName
  398. * @param {...*} varArgs
  399. * @return {!Promise}
  400. * @private
  401. */
  402. remoteAsyncCall_(targetName, methodName, ...varArgs) {
  403. goog.asserts.assert(targetName == 'video' || targetName == 'player',
  404. 'Unexpected target name');
  405. const p = new shaka.util.PublicPromise();
  406. const id = this.nextAsyncCallId_.toString();
  407. this.nextAsyncCallId_++;
  408. this.asyncCallPromises_.set(id, p);
  409. try {
  410. this.sendMessage_({
  411. 'type': 'asyncCall',
  412. 'targetName': targetName,
  413. 'methodName': methodName,
  414. 'args': varArgs,
  415. 'id': id,
  416. });
  417. } catch (error) {
  418. p.reject(error);
  419. }
  420. return p;
  421. }
  422. /**
  423. * A static version of onExistingSessionJoined_, that calls that method for
  424. * each known instance.
  425. * @param {chrome.cast.Session} session
  426. * @private
  427. */
  428. static onExistingSessionJoined_(session) {
  429. for (const instance of shaka.cast.CastSender.instances_) {
  430. instance.onExistingSessionJoined_(session);
  431. }
  432. }
  433. /**
  434. * @param {chrome.cast.Session} session
  435. * @private
  436. */
  437. onExistingSessionJoined_(session) {
  438. shaka.log.debug('CastSender: onExistingSessionJoined');
  439. this.castPromise_ = new shaka.util.PublicPromise();
  440. this.hasJoinedExistingSession_ = true;
  441. this.onSessionInitiated_(session);
  442. }
  443. /**
  444. * A static version of onReceiverStatusChanged_, that calls that method for
  445. * each known instance.
  446. * @param {string} availability
  447. * @private
  448. */
  449. static onReceiverStatusChanged_(availability) {
  450. for (const instance of shaka.cast.CastSender.instances_) {
  451. instance.onReceiverStatusChanged_(availability);
  452. }
  453. }
  454. /**
  455. * @param {string} availability
  456. * @private
  457. */
  458. onReceiverStatusChanged_(availability) {
  459. // The cast API is telling us whether there are any cast receiver devices
  460. // available.
  461. shaka.log.debug('CastSender: receiver status', availability);
  462. shaka.cast.CastSender.hasReceivers_ = availability == 'available';
  463. this.statusChangeTimer_.tickNow();
  464. }
  465. /**
  466. * @param {chrome.cast.Session} session
  467. * @private
  468. */
  469. onSessionCreated_(session) {
  470. shaka.cast.CastSender.session_ = session;
  471. session.addUpdateListener(this.onConnectionStatusChangedBound_);
  472. session.addMessageListener(shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE,
  473. this.onMessageReceivedBound_);
  474. this.onConnectionStatusChanged_();
  475. }
  476. /**
  477. * @private
  478. */
  479. removeListeners_() {
  480. const session = shaka.cast.CastSender.session_;
  481. session.removeUpdateListener(this.onConnectionStatusChangedBound_);
  482. session.removeMessageListener(shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE,
  483. this.onMessageReceivedBound_);
  484. }
  485. /**
  486. * @private
  487. */
  488. onConnectionStatusChanged_() {
  489. const connected = shaka.cast.CastSender.session_ ?
  490. shaka.cast.CastSender.session_.status == 'connected' :
  491. false;
  492. shaka.log.debug('CastSender: connection status', connected);
  493. if (this.isCasting_ && !connected) {
  494. // Tell CastProxy to transfer state back to local player.
  495. this.onResumeLocal_();
  496. // Clear whatever we have cached.
  497. for (const targetName in this.cachedProperties_) {
  498. this.cachedProperties_[targetName] = {};
  499. }
  500. this.rejectAllPromises_();
  501. }
  502. this.isCasting_ = connected;
  503. this.receiverName_ = connected ?
  504. shaka.cast.CastSender.session_.receiver.friendlyName :
  505. '';
  506. this.statusChangeTimer_.tickNow();
  507. }
  508. /**
  509. * Reject any async call promises that are still pending.
  510. * @private
  511. */
  512. rejectAllPromises_() {
  513. if (!this.asyncCallPromises_) {
  514. return;
  515. }
  516. for (const id of this.asyncCallPromises_.keys()) {
  517. const p = this.asyncCallPromises_.get(id);
  518. this.asyncCallPromises_.delete(id);
  519. // Reject pending async operations as if they were interrupted.
  520. // At the moment, load() is the only async operation we are worried about.
  521. p.reject(new shaka.util.Error(
  522. shaka.util.Error.Severity.RECOVERABLE,
  523. shaka.util.Error.Category.PLAYER,
  524. shaka.util.Error.Code.LOAD_INTERRUPTED));
  525. }
  526. }
  527. /**
  528. * @param {string} namespace
  529. * @param {string} serialized
  530. * @private
  531. */
  532. onMessageReceived_(namespace, serialized) {
  533. // Since this method is in the compiled library, make sure all messages
  534. // passed in here were created with quoted property names.
  535. const message = shaka.cast.CastUtils.deserialize(serialized);
  536. shaka.log.v2('CastSender: message', message);
  537. switch (message['type']) {
  538. case 'event': {
  539. const targetName = message['targetName'];
  540. const event = message['event'];
  541. const fakeEvent = shaka.util.FakeEvent.fromRealEvent(event);
  542. this.onRemoteEvent_(targetName, fakeEvent);
  543. break;
  544. }
  545. case 'update': {
  546. const update = message['update'];
  547. for (const targetName in update) {
  548. const target = this.cachedProperties_[targetName] || {};
  549. for (const property in update[targetName]) {
  550. target[property] = update[targetName][property];
  551. }
  552. }
  553. if (this.hasJoinedExistingSession_) {
  554. this.onFirstCastStateUpdate_();
  555. this.hasJoinedExistingSession_ = false;
  556. }
  557. break;
  558. }
  559. case 'asyncComplete': {
  560. const id = message['id'];
  561. const error = message['error'];
  562. const p = this.asyncCallPromises_.get(id);
  563. this.asyncCallPromises_.delete(id);
  564. goog.asserts.assert(p, 'Unexpected async id');
  565. if (!p) {
  566. break;
  567. }
  568. if (error) {
  569. // This is a hacky way to reconstruct the serialized error.
  570. const reconstructedError = new shaka.util.Error(
  571. error.severity, error.category, error.code);
  572. for (const k in error) {
  573. (/** @type {Object} */(reconstructedError))[k] = error[k];
  574. }
  575. p.reject(reconstructedError);
  576. } else {
  577. p.resolve();
  578. }
  579. break;
  580. }
  581. }
  582. }
  583. /**
  584. * @param {!Object} message
  585. * @private
  586. */
  587. sendMessage_(message) {
  588. // Since this method is in the compiled library, make sure all messages
  589. // passed in here were created with quoted property names.
  590. const serialized = shaka.cast.CastUtils.serialize(message);
  591. const session = shaka.cast.CastSender.session_;
  592. // NOTE: This takes an error callback that we have not seen fire. We don't
  593. // know if it would fire synchronously or asynchronously. Until we know how
  594. // it works, we just log from that callback. But we _have_ seen
  595. // sendMessage() throw synchronously, so we handle that.
  596. try {
  597. session.sendMessage(shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE,
  598. serialized,
  599. () => {}, // success callback
  600. shaka.log.error); // error callback
  601. } catch (error) {
  602. shaka.log.error('Cast session sendMessage threw', error);
  603. // Translate the error
  604. const shakaError = new shaka.util.Error(
  605. shaka.util.Error.Severity.CRITICAL,
  606. shaka.util.Error.Category.CAST,
  607. shaka.util.Error.Code.CAST_CONNECTION_TIMED_OUT,
  608. error);
  609. // Dispatch it through the Player proxy
  610. const fakeEvent = new shaka.util.FakeEvent(
  611. 'error', (new Map()).set('detail', shakaError));
  612. this.onRemoteEvent_('player', fakeEvent);
  613. // Force this session to disconnect and transfer playback to the local
  614. // device
  615. this.forceDisconnect();
  616. // Throw the translated error from this getter/setter/method to the UI/app
  617. throw shakaError;
  618. }
  619. }
  620. };
  621. /** @type {number} */
  622. shaka.cast.CastSender.STATUS_DELAY = 0.02;
  623. /** @private {boolean} */
  624. shaka.cast.CastSender.hasReceivers_ = false;
  625. /** @private {chrome.cast.Session} */
  626. shaka.cast.CastSender.session_ = null;
  627. /** @private {?function(boolean)} */
  628. shaka.cast.CastSender.__onGCastApiAvailable_ = null;
  629. /**
  630. * A set of all living CastSender instances. The constructor and destroy
  631. * methods will add and remove instances from this set.
  632. *
  633. * This is used to deal with delayed initialization of the Cast SDK. When the
  634. * SDK becomes available, instances will be reinitialized.
  635. *
  636. * @private {!Set<shaka.cast.CastSender>}
  637. */
  638. shaka.cast.CastSender.instances_ = new Set();
  639. /**
  640. * If the cast SDK is not available yet, it will invoke this callback once it
  641. * becomes available.
  642. *
  643. * @param {boolean} loaded
  644. * @private
  645. */
  646. shaka.cast.CastSender.onSdkLoaded_ = (loaded) => {
  647. if (loaded) {
  648. // Any living instances of CastSender should have their init methods called
  649. // again now that the API is available.
  650. for (const sender of shaka.cast.CastSender.instances_) {
  651. sender.init();
  652. }
  653. }
  654. };
  655. /**
  656. * @param {boolean} available
  657. * @private
  658. */
  659. shaka.cast.CastSender.onGCastApiAvailable_ = (available) => {
  660. // Restore callback from saved.
  661. if (shaka.cast.CastSender.__onGCastApiAvailable_) {
  662. window.__onGCastApiAvailable =
  663. shaka.cast.CastSender.__onGCastApiAvailable_;
  664. } else {
  665. delete window.__onGCastApiAvailable;
  666. }
  667. shaka.cast.CastSender.__onGCastApiAvailable_ = null;
  668. // A note about this value: In our testing environment, we load both
  669. // uncompiled and compiled code. This global callback in uncompiled mode
  670. // can be overwritten by the same in compiled mode. The two versions will
  671. // each have their own instances_ map. Therefore the callback must have a
  672. // name, as opposed to being anonymous. This way, the CastSender tests
  673. // can invoke the named static method instead of using a global that could
  674. // be overwritten.
  675. shaka.cast.CastSender.onSdkLoaded_(available);
  676. // call restored callback (if any)
  677. if (typeof window.__onGCastApiAvailable === 'function') {
  678. window.__onGCastApiAvailable(available);
  679. }
  680. };