Source: lib/cast/cast_proxy.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.cast.CastProxy');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.Player');
  9. goog.require('shaka.cast.CastSender');
  10. goog.require('shaka.cast.CastUtils');
  11. goog.require('shaka.log');
  12. goog.require('shaka.util.Error');
  13. goog.require('shaka.util.EventManager');
  14. goog.require('shaka.util.FakeEvent');
  15. goog.require('shaka.util.FakeEventTarget');
  16. goog.require('shaka.util.IDestroyable');
  17. /**
  18. * @event shaka.cast.CastProxy.CastStatusChangedEvent
  19. * @description Fired when cast status changes. The status change will be
  20. * reflected in canCast() and isCasting().
  21. * @property {string} type
  22. * 'caststatuschanged'
  23. * @exportDoc
  24. */
  25. /**
  26. * @summary A proxy to switch between local and remote playback for Chromecast
  27. * in a way that is transparent to the app's controls.
  28. *
  29. * @implements {shaka.util.IDestroyable}
  30. * @export
  31. */
  32. shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
  33. /**
  34. * @param {!HTMLMediaElement} video The local video element associated with
  35. * the local Player instance.
  36. * @param {!shaka.Player} player A local Player instance.
  37. * @param {string} receiverAppId The ID of the cast receiver application.
  38. * If blank, casting will not be available, but the proxy will still
  39. * function otherwise.
  40. * @param {boolean} androidReceiverCompatible Indicates if the app is
  41. * compatible with an Android Receiver.
  42. */
  43. constructor(video, player, receiverAppId,
  44. androidReceiverCompatible = false) {
  45. super();
  46. /** @private {HTMLMediaElement} */
  47. this.localVideo_ = video;
  48. /** @private {shaka.Player} */
  49. this.localPlayer_ = player;
  50. /** @private {Object} */
  51. this.videoProxy_ = null;
  52. /** @private {Object} */
  53. this.playerProxy_ = null;
  54. /** @private {shaka.util.FakeEventTarget} */
  55. this.videoEventTarget_ = null;
  56. /** @private {shaka.util.FakeEventTarget} */
  57. this.playerEventTarget_ = null;
  58. /** @private {shaka.util.EventManager} */
  59. this.eventManager_ = null;
  60. /** @private {string} */
  61. this.receiverAppId_ = receiverAppId;
  62. /** @private {boolean} */
  63. this.androidReceiverCompatible_ = androidReceiverCompatible;
  64. /** @private {!Array<?>} */
  65. this.addThumbnailsTrackCalls_ = [];
  66. /** @private {!Array<?>} */
  67. this.addTextTrackAsyncCalls_ = [];
  68. /** @private {!Array<?>} */
  69. this.addChaptersTrackCalls_ = [];
  70. /** @private {!Map} */
  71. this.compiledToExternNames_ = new Map();
  72. /** @private {shaka.cast.CastSender} */
  73. this.sender_ = null;
  74. if (window.chrome) {
  75. this.sender_ = new shaka.cast.CastSender(
  76. receiverAppId,
  77. () => this.onCastStatusChanged_(),
  78. () => this.onFirstCastStateUpdate_(),
  79. (targetName, event) => this.onRemoteEvent_(targetName, event),
  80. () => this.onResumeLocal_(),
  81. () => this.getInitState_(),
  82. androidReceiverCompatible);
  83. this.init_();
  84. } else {
  85. this.initWithoutSender_();
  86. }
  87. }
  88. /**
  89. * Destroys the proxy and the underlying local Player.
  90. *
  91. * @param {boolean=} forceDisconnect If true, force the receiver app to shut
  92. * down by disconnecting. Does nothing if not connected.
  93. * @override
  94. * @export
  95. */
  96. destroy(forceDisconnect = false) {
  97. if (this.sender_ && forceDisconnect) {
  98. this.sender_.forceDisconnect();
  99. }
  100. if (this.eventManager_) {
  101. this.eventManager_.release();
  102. this.eventManager_ = null;
  103. }
  104. const waitFor = [];
  105. if (this.localPlayer_) {
  106. waitFor.push(this.localPlayer_.destroy());
  107. this.localPlayer_ = null;
  108. }
  109. if (this.sender_) {
  110. waitFor.push(this.sender_.destroy());
  111. this.sender_ = null;
  112. }
  113. this.localVideo_ = null;
  114. this.videoProxy_ = null;
  115. this.playerProxy_ = null;
  116. // FakeEventTarget implements IReleasable
  117. super.release();
  118. return Promise.all(waitFor);
  119. }
  120. /**
  121. * Get a proxy for the video element that delegates to local and remote video
  122. * elements as appropriate.
  123. *
  124. * @suppress {invalidCasts} to cast proxy Objects to unrelated types
  125. * @return {!HTMLMediaElement}
  126. * @export
  127. */
  128. getVideo() {
  129. return /** @type {!HTMLMediaElement} */(this.videoProxy_);
  130. }
  131. /**
  132. * Get a proxy for the Player that delegates to local and remote Player
  133. * objects as appropriate.
  134. *
  135. * @suppress {invalidCasts} to cast proxy Objects to unrelated types
  136. * @return {!shaka.Player}
  137. * @export
  138. */
  139. getPlayer() {
  140. return /** @type {!shaka.Player} */(this.playerProxy_);
  141. }
  142. /**
  143. * @return {boolean} True if the cast API is available and there are
  144. * receivers.
  145. * @export
  146. */
  147. canCast() {
  148. if (!this.sender_) {
  149. return false;
  150. }
  151. return this.sender_.apiReady() && this.sender_.hasReceivers();
  152. }
  153. /**
  154. * @return {boolean} True if we are currently casting.
  155. * @export
  156. */
  157. isCasting() {
  158. if (!this.sender_) {
  159. return false;
  160. }
  161. return this.sender_.isCasting();
  162. }
  163. /**
  164. * @return {string} The name of the Cast receiver device, if isCasting().
  165. * @export
  166. */
  167. receiverName() {
  168. if (!this.sender_) {
  169. return '';
  170. }
  171. return this.sender_.receiverName();
  172. }
  173. /**
  174. * @return {!Promise} Resolved when connected to a receiver. Rejected if the
  175. * connection fails or is canceled by the user.
  176. * @export
  177. */
  178. async cast() {
  179. if (!this.sender_) {
  180. return;
  181. }
  182. // TODO: transfer manually-selected tracks?
  183. await this.sender_.cast();
  184. if (!this.localPlayer_) {
  185. // We've already been destroyed.
  186. return;
  187. }
  188. // Unload the local manifest when casting succeeds.
  189. await this.localPlayer_.unload();
  190. }
  191. /**
  192. * Set application-specific data.
  193. *
  194. * @param {Object} appData Application-specific data to relay to the receiver.
  195. * @export
  196. */
  197. setAppData(appData) {
  198. if (!this.sender_) {
  199. return;
  200. }
  201. this.sender_.setAppData(appData);
  202. }
  203. /**
  204. * Show a dialog where user can choose to disconnect from the cast connection.
  205. * @export
  206. */
  207. suggestDisconnect() {
  208. if (!this.sender_) {
  209. return;
  210. }
  211. this.sender_.showDisconnectDialog();
  212. }
  213. /**
  214. * Force the receiver app to shut down by disconnecting.
  215. * @export
  216. */
  217. forceDisconnect() {
  218. if (!this.sender_) {
  219. return;
  220. }
  221. this.sender_.forceDisconnect();
  222. }
  223. /**
  224. * @param {string} newAppId
  225. * @param {boolean=} newCastAndroidReceiver
  226. * @export
  227. */
  228. async changeReceiverId(newAppId, newCastAndroidReceiver = false) {
  229. if (newAppId == this.receiverAppId_ &&
  230. newCastAndroidReceiver == this.androidReceiverCompatible_) {
  231. // Nothing to change
  232. return;
  233. }
  234. this.receiverAppId_ = newAppId;
  235. this.androidReceiverCompatible_ = newCastAndroidReceiver;
  236. if (!this.sender_) {
  237. return;
  238. }
  239. // Destroy the old sender
  240. this.sender_.forceDisconnect();
  241. await this.sender_.destroy();
  242. this.sender_ = null;
  243. // Create the new one
  244. this.sender_ = new shaka.cast.CastSender(
  245. newAppId,
  246. () => this.onCastStatusChanged_(),
  247. () => this.onFirstCastStateUpdate_(),
  248. (targetName, event) => this.onRemoteEvent_(targetName, event),
  249. () => this.onResumeLocal_(),
  250. () => this.getInitState_(),
  251. newCastAndroidReceiver);
  252. this.sender_.init();
  253. }
  254. /**
  255. * Initialize the Proxies without Cast sender.
  256. * @private
  257. */
  258. initWithoutSender_() {
  259. this.videoProxy_ = /** @type {Object} */(this.localVideo_);
  260. this.playerProxy_ = /** @type {Object} */(this.localPlayer_);
  261. }
  262. /**
  263. * Initialize the Proxies and the Cast sender.
  264. * @private
  265. */
  266. init_() {
  267. this.sender_.init();
  268. this.eventManager_ = new shaka.util.EventManager();
  269. for (const name of shaka.cast.CastUtils.VideoEvents) {
  270. this.eventManager_.listen(this.localVideo_, name,
  271. (event) => this.videoProxyLocalEvent_(event));
  272. }
  273. for (const key in shaka.util.FakeEvent.EventName) {
  274. const name = shaka.util.FakeEvent.EventName[key];
  275. this.eventManager_.listen(this.localPlayer_, name,
  276. (event) => this.playerProxyLocalEvent_(event));
  277. }
  278. // We would like to use Proxy here, but it is not supported on Safari.
  279. this.videoProxy_ = {};
  280. for (const k in this.localVideo_) {
  281. Object.defineProperty(this.videoProxy_, k, {
  282. configurable: false,
  283. enumerable: true,
  284. get: () => this.videoProxyGet_(k),
  285. set: (value) => { this.videoProxySet_(k, value); },
  286. });
  287. }
  288. this.playerProxy_ = {};
  289. this.iterateOverPlayerMethods_((name, method) => {
  290. goog.asserts.assert(this.playerProxy_, 'Must have player proxy!');
  291. Object.defineProperty(this.playerProxy_, name, {
  292. configurable: false,
  293. enumerable: true,
  294. get: () => this.playerProxyGet_(name),
  295. });
  296. });
  297. if (COMPILED) {
  298. this.mapCompiledToUncompiledPlayerMethodNames_();
  299. }
  300. this.videoEventTarget_ = new shaka.util.FakeEventTarget();
  301. this.videoEventTarget_.dispatchTarget =
  302. /** @type {EventTarget} */(this.videoProxy_);
  303. this.playerEventTarget_ = new shaka.util.FakeEventTarget();
  304. this.playerEventTarget_.dispatchTarget =
  305. /** @type {EventTarget} */(this.playerProxy_);
  306. this.eventManager_.listen(this.localPlayer_,
  307. shaka.util.FakeEvent.EventName.Unloading, () => {
  308. if (this.sender_ && this.sender_.isCasting()) {
  309. return;
  310. }
  311. this.resetExternalTracks();
  312. });
  313. }
  314. /**
  315. * Maps compiled to uncompiled player names so we can figure out
  316. * which method to call in compiled build, while casting.
  317. * @private
  318. */
  319. mapCompiledToUncompiledPlayerMethodNames_() {
  320. // In compiled mode, UI tries to access player methods by their internal
  321. // renamed names, but the proxy object doesn't know about those. See
  322. // https://github.com/shaka-project/shaka-player/issues/2130 for details.
  323. const methodsToNames = new Map();
  324. this.iterateOverPlayerMethods_((name, method) => {
  325. if (methodsToNames.has(method)) {
  326. // If two method names, point to the same method, add them to the
  327. // map as aliases of each other.
  328. const name2 = methodsToNames.get(method);
  329. // Assumes that the compiled name is shorter
  330. if (name.length < name2.length) {
  331. this.compiledToExternNames_.set(name, name2);
  332. } else {
  333. this.compiledToExternNames_.set(name2, name);
  334. }
  335. } else {
  336. methodsToNames.set(method, name);
  337. }
  338. });
  339. }
  340. /**
  341. * Iterates over all of the methods of the player, including inherited methods
  342. * from FakeEventTarget.
  343. * @param {function(string, function())} operation
  344. * @private
  345. */
  346. iterateOverPlayerMethods_(operation) {
  347. goog.asserts.assert(this.localPlayer_, 'Must have player!');
  348. const player = /** @type {!Object} */ (this.localPlayer_);
  349. // Avoid accessing any over-written methods in the prototype chain.
  350. const seenNames = new Set();
  351. /**
  352. * @param {string} name
  353. * @return {boolean}
  354. */
  355. function shouldAddToTheMap(name) {
  356. if (name == 'constructor') {
  357. // Don't proxy the constructor.
  358. return false;
  359. }
  360. const method = /** @type {Object} */(player)[name];
  361. if (typeof method != 'function') {
  362. // Don't proxy non-methods.
  363. return false;
  364. }
  365. // Add if the map does not already have it
  366. return !seenNames.has(name);
  367. }
  368. // First, look at the methods on the object itself, so this can properly
  369. // proxy any methods not on the prototype (for example, in the mock player).
  370. for (const key in player) {
  371. if (shouldAddToTheMap(key)) {
  372. seenNames.add(key);
  373. operation(key, player[key]);
  374. }
  375. }
  376. // The exact length of the prototype chain might vary; for resiliency, this
  377. // will just look at the entire chain, rather than assuming a set length.
  378. let proto = /** @type {!Object} */ (Object.getPrototypeOf(player));
  379. const objProto = /** @type {!Object} */ (Object.getPrototypeOf({}));
  380. while (proto && proto != objProto) { // Don't proxy Object methods.
  381. for (const name of Object.getOwnPropertyNames(proto)) {
  382. if (shouldAddToTheMap(name)) {
  383. seenNames.add(name);
  384. operation(name, (player)[name]);
  385. }
  386. }
  387. proto = /** @type {!Object} */ (Object.getPrototypeOf(proto));
  388. }
  389. }
  390. /**
  391. * @return {shaka.cast.CastUtils.InitStateType} initState Video and player
  392. * state to be sent to the receiver.
  393. * @private
  394. */
  395. getInitState_() {
  396. const initState = {
  397. 'video': {},
  398. 'player': {},
  399. 'playerAfterLoad': {},
  400. 'manifest': this.localPlayer_.getAssetUri(),
  401. 'startTime': null,
  402. 'addThumbnailsTrackCalls': this.addThumbnailsTrackCalls_,
  403. 'addTextTrackAsyncCalls': this.addTextTrackAsyncCalls_,
  404. 'addChaptersTrackCalls': this.addChaptersTrackCalls_,
  405. };
  406. // Pause local playback before capturing state.
  407. this.localVideo_.pause();
  408. for (const name of shaka.cast.CastUtils.VideoInitStateAttributes) {
  409. initState['video'][name] = this.localVideo_[name];
  410. }
  411. // If the video is still playing, set the startTime.
  412. // Has no effect if nothing is loaded.
  413. if (!this.localVideo_.ended) {
  414. initState['startTime'] = this.localVideo_.currentTime;
  415. }
  416. for (const pair of shaka.cast.CastUtils.PlayerInitState) {
  417. const getter = pair[0];
  418. const setter = pair[1];
  419. const value = /** @type {Object} */(this.localPlayer_)[getter]();
  420. initState['player'][setter] = value;
  421. }
  422. for (const pair of shaka.cast.CastUtils.PlayerInitAfterLoadState) {
  423. const getter = pair[0];
  424. const setter = pair[1];
  425. const value = /** @type {Object} */(this.localPlayer_)[getter]();
  426. initState['playerAfterLoad'][setter] = value;
  427. }
  428. return initState;
  429. }
  430. /**
  431. * Dispatch an event to notify the app that the status has changed.
  432. * @private
  433. */
  434. onCastStatusChanged_() {
  435. const event = new shaka.util.FakeEvent('caststatuschanged');
  436. this.dispatchEvent(event);
  437. }
  438. /**
  439. * Dispatch a synthetic play or pause event to ensure that the app correctly
  440. * knows that the player is playing, if joining an existing receiver.
  441. * @private
  442. */
  443. onFirstCastStateUpdate_() {
  444. const type = this.videoProxy_['paused'] ? 'pause' : 'play';
  445. const fakeEvent = new shaka.util.FakeEvent(type);
  446. this.videoEventTarget_.dispatchEvent(fakeEvent);
  447. }
  448. /**
  449. * Transfer remote state back and resume local playback.
  450. * @private
  451. */
  452. onResumeLocal_() {
  453. // Transfer back the player state.
  454. for (const pair of shaka.cast.CastUtils.PlayerInitState) {
  455. const getter = pair[0];
  456. const setter = pair[1];
  457. const value = this.sender_.get('player', getter)();
  458. /** @type {Object} */(this.localPlayer_)[setter](value);
  459. }
  460. const addThumbnailsTrackCalls = this.addThumbnailsTrackCalls_;
  461. const addTextTrackAsyncCalls = this.addTextTrackAsyncCalls_;
  462. const addChaptersTrackCalls = this.addChaptersTrackCalls_;
  463. this.resetExternalTracks();
  464. // Get the most recent manifest URI and ended state.
  465. const assetUri = this.sender_.get('player', 'getAssetUri')();
  466. const ended = this.sender_.get('video', 'ended');
  467. let manifestReady = Promise.resolve();
  468. const autoplay = this.localVideo_.autoplay;
  469. let startTime = null;
  470. // If the video is still playing, set the startTime.
  471. // Has no effect if nothing is loaded.
  472. if (!ended) {
  473. startTime = this.sender_.get('video', 'currentTime');
  474. }
  475. let activeTextTrack;
  476. const textTracks = this.sender_.get('player', 'getTextTracks')();
  477. if (textTracks && textTracks.length) {
  478. activeTextTrack = textTracks.find((t) => t.active);
  479. }
  480. const isTextTrackVisible =
  481. this.sender_.get('player', 'isTextTrackVisible')();
  482. // Now load the manifest, if present.
  483. if (assetUri) {
  484. // Don't autoplay the content until we finish setting up initial state.
  485. this.localVideo_.autoplay = false;
  486. manifestReady = this.localPlayer_.load(assetUri, startTime);
  487. }
  488. // Get the video state into a temp variable since we will apply it async.
  489. const videoState = {};
  490. for (const name of shaka.cast.CastUtils.VideoInitStateAttributes) {
  491. videoState[name] = this.sender_.get('video', name);
  492. }
  493. // Finally, take on video state and player's "after load" state.
  494. manifestReady.then(() => {
  495. if (!this.localVideo_) {
  496. // We've already been destroyed.
  497. return;
  498. }
  499. for (const args of addThumbnailsTrackCalls) {
  500. this.getPlayer().addThumbnailsTrack(...args);
  501. }
  502. for (const args of addTextTrackAsyncCalls) {
  503. this.getPlayer().addTextTrackAsync(...args);
  504. }
  505. for (const args of addChaptersTrackCalls) {
  506. this.getPlayer().addChaptersTrack(...args);
  507. }
  508. for (const name of shaka.cast.CastUtils.VideoInitStateAttributes) {
  509. this.localVideo_[name] = videoState[name];
  510. }
  511. for (const pair of shaka.cast.CastUtils.PlayerInitAfterLoadState) {
  512. const getter = pair[0];
  513. const setter = pair[1];
  514. const value = this.sender_.get('player', getter)();
  515. /** @type {Object} */(this.localPlayer_)[setter](value);
  516. }
  517. this.localPlayer_.setTextTrackVisibility(isTextTrackVisible);
  518. if (activeTextTrack) {
  519. this.localPlayer_.selectTextLanguage(
  520. activeTextTrack.language,
  521. activeTextTrack.roles,
  522. activeTextTrack.forced);
  523. }
  524. // Restore the original autoplay setting.
  525. this.localVideo_.autoplay = autoplay;
  526. if (assetUri) {
  527. // Resume playback with transferred state.
  528. this.localVideo_.play();
  529. }
  530. }, (error) => {
  531. // Pass any errors through to the app.
  532. goog.asserts.assert(error instanceof shaka.util.Error,
  533. 'Wrong error type!');
  534. const eventType = shaka.util.FakeEvent.EventName.Error;
  535. const data = (new Map()).set('detail', error);
  536. const event = new shaka.util.FakeEvent(eventType, data);
  537. this.localPlayer_.dispatchEvent(event);
  538. });
  539. }
  540. /**
  541. * @param {string} name
  542. * @return {?}
  543. * @private
  544. */
  545. videoProxyGet_(name) {
  546. if (name == 'addEventListener') {
  547. return (type, listener, options) => {
  548. return this.videoEventTarget_.addEventListener(type, listener, options);
  549. };
  550. }
  551. if (name == 'removeEventListener') {
  552. return (type, listener, options) => {
  553. return this.videoEventTarget_.removeEventListener(
  554. type, listener, options);
  555. };
  556. }
  557. // If we are casting, but the first update has not come in yet, use local
  558. // values, but not local methods.
  559. if (this.sender_.isCasting() && !this.sender_.hasRemoteProperties()) {
  560. const value = this.localVideo_[name];
  561. if (typeof value != 'function') {
  562. return value;
  563. }
  564. }
  565. // Use local values and methods if we are not casting.
  566. if (!this.sender_.isCasting()) {
  567. let value = this.localVideo_[name];
  568. if (typeof value == 'function') {
  569. // eslint-disable-next-line no-restricted-syntax
  570. value = value.bind(this.localVideo_);
  571. }
  572. return value;
  573. }
  574. return this.sender_.get('video', name);
  575. }
  576. /**
  577. * @param {string} name
  578. * @param {?} value
  579. * @private
  580. */
  581. videoProxySet_(name, value) {
  582. if (!this.sender_.isCasting()) {
  583. this.localVideo_[name] = value;
  584. return;
  585. }
  586. this.sender_.set('video', name, value);
  587. }
  588. /**
  589. * @param {!Event} event
  590. * @private
  591. */
  592. videoProxyLocalEvent_(event) {
  593. if (this.sender_.isCasting()) {
  594. // Ignore any unexpected local events while casting. Events can still be
  595. // fired by the local video and Player when we unload() after the Cast
  596. // connection is complete.
  597. return;
  598. }
  599. // Convert this real Event into a FakeEvent for dispatch from our
  600. // FakeEventListener.
  601. const fakeEvent = shaka.util.FakeEvent.fromRealEvent(event);
  602. this.videoEventTarget_.dispatchEvent(fakeEvent);
  603. }
  604. /**
  605. * @param {string} name
  606. * @param {boolean} dontRecordCalls
  607. * @return {?}
  608. * @private
  609. */
  610. playerProxyGet_(name, dontRecordCalls = false) {
  611. // If name is a shortened compiled name, get the original version
  612. // from our map.
  613. if (this.compiledToExternNames_.has(name)) {
  614. name = this.compiledToExternNames_.get(name);
  615. }
  616. if (name == 'addEventListener') {
  617. return (type, listener, options) => {
  618. return this.playerEventTarget_.addEventListener(
  619. type, listener, options);
  620. };
  621. }
  622. if (name == 'removeEventListener') {
  623. return (type, listener, options) => {
  624. return this.playerEventTarget_.removeEventListener(
  625. type, listener, options);
  626. };
  627. }
  628. if (name == 'getMediaElement') {
  629. return () => this.videoProxy_;
  630. }
  631. if (name == 'getSharedConfiguration') {
  632. shaka.log.warning(
  633. 'Can\'t share configuration across a network. Returning copy.');
  634. return this.sender_.get('player', 'getConfiguration');
  635. }
  636. if (name == 'getNetworkingEngine') {
  637. // Always returns a local instance, in case you need to make a request.
  638. // Issues a warning, in case you think you are making a remote request
  639. // or affecting remote filters.
  640. if (this.sender_.isCasting()) {
  641. shaka.log.warning('NOTE: getNetworkingEngine() is always local!');
  642. }
  643. return () => this.localPlayer_.getNetworkingEngine();
  644. }
  645. if (name == 'getDrmEngine') {
  646. // Always returns a local instance.
  647. if (this.sender_.isCasting()) {
  648. shaka.log.warning('NOTE: getDrmEngine() is always local!');
  649. }
  650. return () => this.localPlayer_.getDrmEngine();
  651. }
  652. if (name == 'getAdManager') {
  653. // Always returns a local instance.
  654. if (this.sender_.isCasting()) {
  655. shaka.log.warning('NOTE: getAdManager() is always local!');
  656. }
  657. return () => this.localPlayer_.getAdManager();
  658. }
  659. if (name == 'getQueueManager') {
  660. // Always returns a local instance.
  661. return () => this.localPlayer_.getQueueManager();
  662. }
  663. if (name == 'setVideoContainer') {
  664. // Always returns a local instance.
  665. if (this.sender_.isCasting()) {
  666. shaka.log.warning('NOTE: setVideoContainer() is always local!');
  667. }
  668. return (container) => this.localPlayer_.setVideoContainer(container);
  669. }
  670. if (!dontRecordCalls) {
  671. if (name == 'addThumbnailsTrack') {
  672. return (...args) => {
  673. this.addThumbnailsTrackCalls_.push(args);
  674. return this.playerProxyGet_(
  675. name, /* dontRecordCalls= */ true)(...args);
  676. };
  677. }
  678. if (name == 'addTextTrackAsync') {
  679. return (...args) => {
  680. this.addTextTrackAsyncCalls_.push(args);
  681. return this.playerProxyGet_(
  682. name, /* dontRecordCalls= */ true)(...args);
  683. };
  684. }
  685. if (name == 'addChaptersTrack') {
  686. return (...args) => {
  687. this.addChaptersTrackCalls_.push(args);
  688. return this.playerProxyGet_(
  689. name, /* dontRecordCalls= */ true)(...args);
  690. };
  691. }
  692. }
  693. if (this.sender_.isCasting()) {
  694. // These methods are unavailable or otherwise stubbed during casting.
  695. if (name == 'getManifest' || name == 'drmInfo') {
  696. return () => {
  697. shaka.log.alwaysWarn(name + '() does not work while casting!');
  698. return null;
  699. };
  700. }
  701. if (name == 'attach' || name == 'detach') {
  702. return () => {
  703. shaka.log.alwaysWarn(name + '() does not work while casting!');
  704. return Promise.resolve();
  705. };
  706. }
  707. if (name == 'getChapters') {
  708. // This does not follow our standard pattern (takes an arguments), and
  709. // therefore can't be proactively proxied and cached the way other
  710. // synchronous getters can.
  711. return () => {
  712. shaka.log.alwaysWarn(name + '() does not work while casting!');
  713. return [];
  714. };
  715. }
  716. } // if (this.sender_.isCasting())
  717. // If we are casting, but the first update has not come in yet, use local
  718. // getters, but not local methods.
  719. if (this.sender_.isCasting() && !this.sender_.hasRemoteProperties()) {
  720. if (shaka.cast.CastUtils.PlayerGetterMethods.has(name) ||
  721. shaka.cast.CastUtils.LargePlayerGetterMethods.has(name)) {
  722. const value = /** @type {Object} */(this.localPlayer_)[name];
  723. goog.asserts.assert(typeof value == 'function',
  724. 'only methods on Player');
  725. // eslint-disable-next-line no-restricted-syntax
  726. return value.bind(this.localPlayer_);
  727. }
  728. }
  729. // Use local getters and methods if we are not casting.
  730. if (!this.sender_.isCasting()) {
  731. const value = /** @type {Object} */(this.localPlayer_)[name];
  732. goog.asserts.assert(typeof value == 'function',
  733. 'only methods on Player');
  734. // eslint-disable-next-line no-restricted-syntax
  735. return value.bind(this.localPlayer_);
  736. }
  737. return this.sender_.get('player', name);
  738. }
  739. /**
  740. * @param {!Event} event
  741. * @private
  742. */
  743. playerProxyLocalEvent_(event) {
  744. if (this.sender_.isCasting()) {
  745. // Ignore any unexpected local events while casting.
  746. return;
  747. }
  748. this.playerEventTarget_.dispatchEvent(event);
  749. }
  750. /**
  751. * @param {string} targetName
  752. * @param {!shaka.util.FakeEvent} event
  753. * @private
  754. */
  755. onRemoteEvent_(targetName, event) {
  756. goog.asserts.assert(this.sender_.isCasting(),
  757. 'Should only receive remote events while casting');
  758. if (!this.sender_.isCasting()) {
  759. // Ignore any unexpected remote events.
  760. return;
  761. }
  762. if (targetName == 'video') {
  763. this.videoEventTarget_.dispatchEvent(event);
  764. } else if (targetName == 'player') {
  765. if (event.type == shaka.util.FakeEvent.EventName.Unloading) {
  766. this.resetExternalTracks();
  767. }
  768. this.playerEventTarget_.dispatchEvent(event);
  769. }
  770. }
  771. /**
  772. * Reset external tracks
  773. */
  774. resetExternalTracks() {
  775. this.addThumbnailsTrackCalls_ = [];
  776. this.addTextTrackAsyncCalls_ = [];
  777. this.addChaptersTrackCalls_ = [];
  778. }
  779. };