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