Source: lib/cast/cast_receiver.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.cast.CastReceiver');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.Player');
  9. goog.require('shaka.cast.CastUtils');
  10. goog.require('shaka.log');
  11. goog.require('shaka.util.Error');
  12. goog.require('shaka.util.EventManager');
  13. goog.require('shaka.util.FakeEvent');
  14. goog.require('shaka.util.FakeEventTarget');
  15. goog.require('shaka.util.IDestroyable');
  16. goog.require('shaka.util.Platform');
  17. goog.require('shaka.util.Timer');
  18. /**
  19. * A receiver to communicate between the Chromecast-hosted player and the
  20. * sender application.
  21. *
  22. * @implements {shaka.util.IDestroyable}
  23. * @export
  24. */
  25. shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
  26. /**
  27. * @param {!HTMLMediaElement} video The local video element associated with
  28. * the local Player instance.
  29. * @param {!shaka.Player} player A local Player instance.
  30. * @param {function(Object)=} appDataCallback A callback to handle
  31. * application-specific data passed from the sender. This can come either
  32. * from a Shaka-based sender through CastProxy.setAppData, or from a
  33. * sender using the customData field of the LOAD message of the standard
  34. * Cast message namespace. It can also be null if no such data is sent.
  35. * @param {function(string):string=} contentIdCallback A callback to
  36. * retrieve manifest URI from the provided content id.
  37. */
  38. constructor(video, player, appDataCallback, contentIdCallback) {
  39. super();
  40. /** @private {HTMLMediaElement} */
  41. this.video_ = video;
  42. /** @private {shaka.Player} */
  43. this.player_ = player;
  44. /** @private {shaka.util.EventManager} */
  45. this.eventManager_ = new shaka.util.EventManager();
  46. /** @private {Object} */
  47. this.targets_ = {
  48. 'video': video,
  49. 'player': player,
  50. };
  51. /** @private {?function(Object)} */
  52. this.appDataCallback_ = appDataCallback || (() => {});
  53. /** @private {?function(string):string} */
  54. this.contentIdCallback_ = contentIdCallback ||
  55. /**
  56. * @param {string} contentId
  57. * @return {string}
  58. */
  59. ((contentId) => contentId);
  60. /**
  61. * A Cast metadata object, one of:
  62. * - https://developers.google.com/cast/docs/reference/messages#GenericMediaMetadata
  63. * - https://developers.google.com/cast/docs/reference/messages#MovieMediaMetadata
  64. * - https://developers.google.com/cast/docs/reference/messages#TvShowMediaMetadata
  65. * - https://developers.google.com/cast/docs/reference/messages#MusicTrackMediaMetadata
  66. * @private {Object}
  67. */
  68. this.metadata_ = null;
  69. /** @private {boolean} */
  70. this.isConnected_ = false;
  71. /** @private {boolean} */
  72. this.isIdle_ = true;
  73. /** @private {number} */
  74. this.updateNumber_ = 0;
  75. /** @private {boolean} */
  76. this.startUpdatingUpdateNumber_ = false;
  77. /** @private {boolean} */
  78. this.initialStatusUpdatePending_ = true;
  79. /** @private {cast.receiver.CastMessageBus} */
  80. this.shakaBus_ = null;
  81. /** @private {cast.receiver.CastMessageBus} */
  82. this.genericBus_ = null;
  83. /** @private {shaka.util.Timer} */
  84. this.pollTimer_ = new shaka.util.Timer(() => {
  85. this.pollAttributes_();
  86. });
  87. this.init_();
  88. }
  89. /**
  90. * @return {boolean} True if the cast API is available and there are
  91. * receivers.
  92. * @export
  93. */
  94. isConnected() {
  95. return this.isConnected_;
  96. }
  97. /**
  98. * @return {boolean} True if the receiver is not currently doing loading or
  99. * playing anything.
  100. * @export
  101. */
  102. isIdle() {
  103. return this.isIdle_;
  104. }
  105. /**
  106. * Set all Cast content metadata, as defined by the Cast SDK.
  107. * Should be called from an appDataCallback.
  108. *
  109. * For a simpler way to set basic metadata, see:
  110. * - setContentTitle()
  111. * - setContentImage()
  112. * - setContentArtist()
  113. *
  114. * @param {Object} metadata
  115. * A Cast metadata object, one of:
  116. * - https://developers.google.com/cast/docs/reference/messages#GenericMediaMetadata
  117. * - https://developers.google.com/cast/docs/reference/messages#MovieMediaMetadata
  118. * - https://developers.google.com/cast/docs/reference/messages#TvShowMediaMetadata
  119. * - https://developers.google.com/cast/docs/reference/messages#MusicTrackMediaMetadata
  120. * @export
  121. */
  122. setContentMetadata(metadata) {
  123. this.metadata_ = metadata;
  124. }
  125. /**
  126. * Clear all Cast content metadata.
  127. * Should be called from an appDataCallback.
  128. *
  129. * @export
  130. */
  131. clearContentMetadata() {
  132. this.metadata_ = null;
  133. }
  134. /**
  135. * Set the Cast content's title.
  136. * Should be called from an appDataCallback.
  137. *
  138. * @param {string} title
  139. * @export
  140. */
  141. setContentTitle(title) {
  142. if (!this.metadata_) {
  143. this.metadata_ = {
  144. 'metadataType': cast.receiver.media.MetadataType.GENERIC,
  145. };
  146. }
  147. this.metadata_['title'] = title;
  148. }
  149. /**
  150. * Set the Cast content's thumbnail image.
  151. * Should be called from an appDataCallback.
  152. *
  153. * @param {string} imageUrl
  154. * @export
  155. */
  156. setContentImage(imageUrl) {
  157. if (!this.metadata_) {
  158. this.metadata_ = {
  159. 'metadataType': cast.receiver.media.MetadataType.GENERIC,
  160. };
  161. }
  162. this.metadata_['images'] = [
  163. {
  164. 'url': imageUrl,
  165. },
  166. ];
  167. }
  168. /**
  169. * Set the Cast content's artist.
  170. * Also sets the metadata type to music.
  171. * Should be called from an appDataCallback.
  172. *
  173. * @param {string} artist
  174. * @export
  175. */
  176. setContentArtist(artist) {
  177. if (!this.metadata_) {
  178. this.metadata_ = {};
  179. }
  180. this.metadata_['artist'] = artist;
  181. this.metadata_['metadataType'] =
  182. cast.receiver.media.MetadataType.MUSIC_TRACK;
  183. }
  184. /**
  185. * Destroys the underlying Player, then terminates the cast receiver app.
  186. *
  187. * @override
  188. * @export
  189. */
  190. async destroy() {
  191. if (this.eventManager_) {
  192. this.eventManager_.release();
  193. this.eventManager_ = null;
  194. }
  195. const waitFor = [];
  196. if (this.player_) {
  197. waitFor.push(this.player_.destroy());
  198. this.player_ = null;
  199. }
  200. if (this.pollTimer_) {
  201. this.pollTimer_.stop();
  202. this.pollTimer_ = null;
  203. }
  204. this.video_ = null;
  205. this.targets_ = null;
  206. this.appDataCallback_ = null;
  207. this.isConnected_ = false;
  208. this.isIdle_ = true;
  209. this.shakaBus_ = null;
  210. this.genericBus_ = null;
  211. // FakeEventTarget implements IReleasable
  212. super.release();
  213. await Promise.all(waitFor);
  214. const manager = cast.receiver.CastReceiverManager.getInstance();
  215. manager.stop();
  216. }
  217. /** @private */
  218. init_() {
  219. const manager = cast.receiver.CastReceiverManager.getInstance();
  220. manager.onSenderConnected = () => this.onSendersChanged_();
  221. manager.onSenderDisconnected = () => this.onSendersChanged_();
  222. manager.onSystemVolumeChanged = () => this.fakeVolumeChangeEvent_();
  223. this.genericBus_ = manager.getCastMessageBus(
  224. shaka.cast.CastUtils.GENERIC_MESSAGE_NAMESPACE);
  225. this.genericBus_.onMessage = (event) => this.onGenericMessage_(event);
  226. this.shakaBus_ = manager.getCastMessageBus(
  227. shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE);
  228. this.shakaBus_.onMessage = (event) => this.onShakaMessage_(event);
  229. if (goog.DEBUG) {
  230. // Sometimes it is useful to load the receiver app in Chrome to work on
  231. // the UI. To avoid log spam caused by the SDK trying to connect to web
  232. // sockets that don't exist, in uncompiled mode we check if the hosting
  233. // browser is a Chromecast before starting the receiver manager. We
  234. // wouldn't do browser detection except for debugging, so only do this in
  235. // uncompiled mode.
  236. if (shaka.util.Platform.isChromecast()) {
  237. manager.start();
  238. }
  239. } else {
  240. manager.start();
  241. }
  242. for (const name of shaka.cast.CastUtils.VideoEvents) {
  243. this.eventManager_.listen(
  244. this.video_, name, (event) => this.proxyEvent_('video', event));
  245. }
  246. for (const key in shaka.util.FakeEvent.EventName) {
  247. const name = shaka.util.FakeEvent.EventName[key];
  248. this.eventManager_.listen(
  249. this.player_, name, (event) => this.proxyEvent_('player', event));
  250. }
  251. // Do not start excluding values from update messages until the video is
  252. // fully loaded.
  253. this.eventManager_.listen(this.video_, 'loadeddata', () => {
  254. this.startUpdatingUpdateNumber_ = true;
  255. });
  256. // Maintain idle state.
  257. this.eventManager_.listen(this.player_, 'loading', () => {
  258. // No longer idle once loading. This allows us to show the spinner during
  259. // the initial buffering phase.
  260. this.isIdle_ = false;
  261. this.onCastStatusChanged_();
  262. });
  263. this.eventManager_.listen(this.video_, 'playing', () => {
  264. // No longer idle once playing. This allows us to replay a video without
  265. // reloading.
  266. this.isIdle_ = false;
  267. this.onCastStatusChanged_();
  268. });
  269. this.eventManager_.listen(this.video_, 'pause', () => {
  270. this.onCastStatusChanged_();
  271. });
  272. this.eventManager_.listen(this.player_, 'unloading', () => {
  273. // Go idle when unloading content.
  274. this.isIdle_ = true;
  275. this.onCastStatusChanged_();
  276. });
  277. this.eventManager_.listen(this.video_, 'ended', () => {
  278. // Go idle 5 seconds after 'ended', assuming we haven't started again or
  279. // been destroyed.
  280. const timer = new shaka.util.Timer(() => {
  281. if (this.video_ && this.video_.ended) {
  282. this.isIdle_ = true;
  283. this.onCastStatusChanged_();
  284. }
  285. });
  286. timer.tickAfter(shaka.cast.CastReceiver.IDLE_INTERVAL);
  287. });
  288. // Do not start polling until after the sender's 'init' message is handled.
  289. }
  290. /** @private */
  291. onSendersChanged_() {
  292. // Reset update message frequency values, to make sure whomever joined
  293. // will get a full update message.
  294. this.updateNumber_ = 0;
  295. // Don't reset startUpdatingUpdateNumber_, because this operation does not
  296. // result in new data being loaded.
  297. this.initialStatusUpdatePending_ = true;
  298. const manager = cast.receiver.CastReceiverManager.getInstance();
  299. this.isConnected_ = manager.getSenders().length != 0;
  300. this.onCastStatusChanged_();
  301. }
  302. /**
  303. * Dispatch an event to notify the receiver app that the status has changed.
  304. * @private
  305. */
  306. async onCastStatusChanged_() {
  307. // Do this asynchronously so that synchronous changes to idle state (such as
  308. // Player calling unload() as part of load()) are coalesced before the event
  309. // goes out.
  310. await Promise.resolve();
  311. if (!this.player_) {
  312. // We've already been destroyed.
  313. return;
  314. }
  315. const event = new shaka.util.FakeEvent('caststatuschanged');
  316. this.dispatchEvent(event);
  317. // Send a media status message, with a media info message if appropriate.
  318. if (!this.maybeSendMediaInfoMessage_()) {
  319. this.sendMediaStatus_();
  320. }
  321. }
  322. /**
  323. * Take on initial state from the sender.
  324. * @param {shaka.cast.CastUtils.InitStateType} initState
  325. * @param {Object} appData
  326. * @private
  327. */
  328. async initState_(initState, appData) {
  329. // Take on player state first.
  330. for (const k in initState['player']) {
  331. const v = initState['player'][k];
  332. // All player state vars are setters to be called.
  333. /** @type {Object} */(this.player_)[k](v);
  334. }
  335. // Now process custom app data, which may add additional player configs:
  336. this.appDataCallback_(appData);
  337. const autoplay = this.video_.autoplay;
  338. // Now load the manifest, if present.
  339. if (initState['manifest']) {
  340. // Don't autoplay the content until we finish setting up initial state.
  341. this.video_.autoplay = false;
  342. try {
  343. await this.player_.load(initState['manifest'], initState['startTime']);
  344. } catch (error) {
  345. // Pass any errors through to the app.
  346. goog.asserts.assert(error instanceof shaka.util.Error,
  347. 'Wrong error type! Error: ' + error);
  348. const eventType = shaka.util.FakeEvent.EventName.Error;
  349. const data = (new Map()).set('detail', error);
  350. const event = new shaka.util.FakeEvent(eventType, data);
  351. // Only dispatch the event if the player still exists.
  352. if (this.player_) {
  353. this.player_.dispatchEvent(event);
  354. }
  355. return;
  356. }
  357. } else {
  358. // Ensure the below happens async.
  359. await Promise.resolve();
  360. }
  361. if (!this.player_) {
  362. // We've already been destroyed.
  363. return;
  364. }
  365. // Finally, take on video state and player's "after load" state.
  366. for (const k in initState['video']) {
  367. const v = initState['video'][k];
  368. this.video_[k] = v;
  369. }
  370. for (const k in initState['playerAfterLoad']) {
  371. const v = initState['playerAfterLoad'][k];
  372. // All player state vars are setters to be called.
  373. /** @type {Object} */(this.player_)[k](v);
  374. }
  375. // Restore original autoplay setting.
  376. this.video_.autoplay = autoplay;
  377. if (initState['manifest']) {
  378. // Resume playback with transferred state.
  379. this.video_.play();
  380. // Notify generic controllers of the state change.
  381. this.sendMediaStatus_();
  382. }
  383. }
  384. /**
  385. * @param {string} targetName
  386. * @param {!Event} event
  387. * @private
  388. */
  389. proxyEvent_(targetName, event) {
  390. if (!this.player_) {
  391. // The receiver is destroyed, so it should ignore further events.
  392. return;
  393. }
  394. // Poll and send an update right before we send the event. Some events
  395. // indicate an attribute change, so that change should be visible when the
  396. // event is handled.
  397. this.pollAttributes_();
  398. this.sendMessage_({
  399. 'type': 'event',
  400. 'targetName': targetName,
  401. 'event': event,
  402. }, this.shakaBus_);
  403. }
  404. /** @private */
  405. pollAttributes_() {
  406. // The poll timer may have been pre-empted by an event (e.g. timeupdate).
  407. // Calling |start| will cancel any pending calls and therefore will avoid us
  408. // polling too often.
  409. this.pollTimer_.tickAfter(shaka.cast.CastReceiver.POLL_INTERVAL);
  410. const update = {
  411. 'video': {},
  412. 'player': {},
  413. };
  414. for (const name of shaka.cast.CastUtils.VideoAttributes) {
  415. update['video'][name] = this.video_[name];
  416. }
  417. // TODO: Instead of this variable frequency update system, instead cache the
  418. // previous player state and only send over changed values, with complete
  419. // updates every ~20 updates to account for dropped messages.
  420. if (this.player_.isLive()) {
  421. const PlayerGetterMethodsThatRequireLive =
  422. shaka.cast.CastUtils.PlayerGetterMethodsThatRequireLive;
  423. PlayerGetterMethodsThatRequireLive.forEach((frequency, name) => {
  424. if (this.updateNumber_ % frequency == 0) {
  425. update['player'][name] = /** @type {Object} */ (this.player_)[name]();
  426. }
  427. });
  428. }
  429. shaka.cast.CastUtils.PlayerGetterMethods.forEach((frequency, name) => {
  430. if (this.updateNumber_ % frequency == 0) {
  431. update['player'][name] = /** @type {Object} */ (this.player_)[name]();
  432. }
  433. });
  434. // Volume attributes are tied to the system volume.
  435. const manager = cast.receiver.CastReceiverManager.getInstance();
  436. const systemVolume = manager.getSystemVolume();
  437. if (systemVolume) {
  438. update['video']['volume'] = systemVolume.level;
  439. update['video']['muted'] = systemVolume.muted;
  440. }
  441. this.sendMessage_({
  442. 'type': 'update',
  443. 'update': update,
  444. }, this.shakaBus_);
  445. // Getters with large outputs each get sent in their own update message.
  446. shaka.cast.CastUtils.LargePlayerGetterMethods.forEach((frequency, name) => {
  447. if (this.updateNumber_ % frequency == 0) {
  448. const update = {'player': {}};
  449. update['player'][name] = /** @type {Object} */ (this.player_)[name]();
  450. this.sendMessage_({
  451. 'type': 'update',
  452. 'update': update,
  453. }, this.shakaBus_);
  454. }
  455. });
  456. // Only start progressing the update number once data is loaded,
  457. // just in case any of the "rarely changing" properties with less frequent
  458. // update messages changes significantly during the loading process.
  459. if (this.startUpdatingUpdateNumber_) {
  460. this.updateNumber_ += 1;
  461. }
  462. this.maybeSendMediaInfoMessage_();
  463. }
  464. /**
  465. * Composes and sends a mediaStatus message if appropriate.
  466. * @return {boolean}
  467. * @private
  468. */
  469. maybeSendMediaInfoMessage_() {
  470. if (this.initialStatusUpdatePending_ &&
  471. (this.video_.duration || this.player_.isLive())) {
  472. // Send over a media status message to set the duration of the cast
  473. // dialogue.
  474. this.sendMediaInfoMessage_();
  475. this.initialStatusUpdatePending_ = false;
  476. return true;
  477. }
  478. return false;
  479. }
  480. /**
  481. * Composes and sends a mediaStatus message with a mediaInfo component.
  482. *
  483. * @param {number=} requestId
  484. * @private
  485. */
  486. sendMediaInfoMessage_(requestId = 0) {
  487. const media = {
  488. 'contentId': this.player_.getAssetUri(),
  489. 'streamType': this.player_.isLive() ? 'LIVE' : 'BUFFERED',
  490. // Sending an empty string for now since it's a mandatory field.
  491. // We don't have this info, and it doesn't seem to be useful, anyway.
  492. 'contentType': '',
  493. };
  494. if (!this.player_.isLive()) {
  495. // Optional, and only sent when the duration is known.
  496. media['duration'] = this.video_.duration;
  497. }
  498. if (this.metadata_) {
  499. media['metadata'] = this.metadata_;
  500. }
  501. this.sendMediaStatus_(requestId, media);
  502. }
  503. /**
  504. * Dispatch a fake 'volumechange' event to mimic the video element, since
  505. * volume changes are routed to the system volume on the receiver.
  506. * @private
  507. */
  508. fakeVolumeChangeEvent_() {
  509. // Volume attributes are tied to the system volume.
  510. const manager = cast.receiver.CastReceiverManager.getInstance();
  511. const systemVolume = manager.getSystemVolume();
  512. goog.asserts.assert(systemVolume, 'System volume should not be null!');
  513. if (systemVolume) {
  514. // Send an update message with just the latest volume level and muted
  515. // state.
  516. this.sendMessage_({
  517. 'type': 'update',
  518. 'update': {
  519. 'video': {
  520. 'volume': systemVolume.level,
  521. 'muted': systemVolume.muted,
  522. },
  523. },
  524. }, this.shakaBus_);
  525. }
  526. // Send another message with a 'volumechange' event to update the sender's
  527. // UI.
  528. this.sendMessage_({
  529. 'type': 'event',
  530. 'targetName': 'video',
  531. 'event': {'type': 'volumechange'},
  532. }, this.shakaBus_);
  533. }
  534. /**
  535. * Since this method is in the compiled library, make sure all messages are
  536. * read with quoted properties.
  537. * @param {!cast.receiver.CastMessageBus.Event} event
  538. * @private
  539. */
  540. onShakaMessage_(event) {
  541. const message = shaka.cast.CastUtils.deserialize(event.data);
  542. shaka.log.debug('CastReceiver: message', message);
  543. switch (message['type']) {
  544. case 'init':
  545. // Reset update message frequency values after initialization.
  546. this.updateNumber_ = 0;
  547. this.startUpdatingUpdateNumber_ = false;
  548. this.initialStatusUpdatePending_ = true;
  549. this.initState_(message['initState'], message['appData']);
  550. // The sender is supposed to reflect the cast system volume after
  551. // connecting. Using fakeVolumeChangeEvent_() would create a race on
  552. // the sender side, since it would have volume properties, but no
  553. // others.
  554. // This would lead to hasRemoteProperties() being true, even though a
  555. // complete set had never been sent.
  556. // Now that we have init state, this is a good time for the first update
  557. // message anyway.
  558. this.pollAttributes_();
  559. break;
  560. case 'appData':
  561. this.appDataCallback_(message['appData']);
  562. break;
  563. case 'set': {
  564. const targetName = message['targetName'];
  565. const property = message['property'];
  566. const value = message['value'];
  567. if (targetName == 'video') {
  568. // Volume attributes must be rerouted to the system.
  569. const manager = cast.receiver.CastReceiverManager.getInstance();
  570. if (property == 'volume') {
  571. manager.setSystemVolumeLevel(value);
  572. break;
  573. } else if (property == 'muted') {
  574. manager.setSystemVolumeMuted(value);
  575. break;
  576. }
  577. }
  578. this.targets_[targetName][property] = value;
  579. break;
  580. }
  581. case 'call': {
  582. const targetName = message['targetName'];
  583. const methodName = message['methodName'];
  584. const args = message['args'];
  585. const target = this.targets_[targetName];
  586. // eslint-disable-next-line prefer-spread
  587. target[methodName].apply(target, args);
  588. break;
  589. }
  590. case 'asyncCall': {
  591. const targetName = message['targetName'];
  592. const methodName = message['methodName'];
  593. if (targetName == 'player' && methodName == 'load') {
  594. // Reset update message frequency values after a load.
  595. this.updateNumber_ = 0;
  596. this.startUpdatingUpdateNumber_ = false;
  597. }
  598. const args = message['args'];
  599. const id = message['id'];
  600. const senderId = event.senderId;
  601. const target = this.targets_[targetName];
  602. // eslint-disable-next-line prefer-spread
  603. let p = target[methodName].apply(target, args);
  604. if (targetName == 'player' && methodName == 'load') {
  605. // Wait until the manifest has actually loaded to send another media
  606. // info message, so on a new load it doesn't send the old info over.
  607. p = p.then(() => {
  608. this.initialStatusUpdatePending_ = true;
  609. });
  610. }
  611. // Replies must go back to the specific sender who initiated, so that we
  612. // don't have to deal with conflicting IDs between senders.
  613. p.then(
  614. () => this.sendAsyncComplete_(senderId, id, /* error= */ null),
  615. (error) => this.sendAsyncComplete_(senderId, id, error));
  616. break;
  617. }
  618. }
  619. }
  620. /**
  621. * @param {!cast.receiver.CastMessageBus.Event} event
  622. * @private
  623. */
  624. onGenericMessage_(event) {
  625. const message = shaka.cast.CastUtils.deserialize(event.data);
  626. shaka.log.debug('CastReceiver: message', message);
  627. // TODO(ismena): error message on duplicate request id from the same sender
  628. switch (message['type']) {
  629. case 'PLAY':
  630. this.video_.play();
  631. // Notify generic controllers that the player state changed.
  632. // requestId=0 (the parameter) means that the message was not
  633. // triggered by a GET_STATUS request.
  634. this.sendMediaStatus_();
  635. break;
  636. case 'PAUSE':
  637. this.video_.pause();
  638. this.sendMediaStatus_();
  639. break;
  640. case 'SEEK': {
  641. const currentTime = message['currentTime'];
  642. const resumeState = message['resumeState'];
  643. if (currentTime != null) {
  644. this.video_.currentTime = Number(currentTime);
  645. }
  646. if (resumeState && resumeState == 'PLAYBACK_START') {
  647. this.video_.play();
  648. this.sendMediaStatus_();
  649. } else if (resumeState && resumeState == 'PLAYBACK_PAUSE') {
  650. this.video_.pause();
  651. this.sendMediaStatus_();
  652. }
  653. break;
  654. }
  655. case 'STOP':
  656. this.player_.unload().then(() => {
  657. if (!this.player_) {
  658. // We've already been destroyed.
  659. return;
  660. }
  661. this.sendMediaStatus_();
  662. });
  663. break;
  664. case 'GET_STATUS':
  665. // TODO(ismena): According to the SDK this is supposed to be a
  666. // unicast message to the sender that requested the status,
  667. // but it doesn't appear to be working.
  668. // Look into what's going on there and change this to be a
  669. // unicast.
  670. this.sendMediaInfoMessage_(Number(message['requestId']));
  671. break;
  672. case 'VOLUME': {
  673. const volumeObject = message['volume'];
  674. const level = volumeObject['level'];
  675. const muted = volumeObject['muted'];
  676. const oldVolumeLevel = this.video_.volume;
  677. const oldVolumeMuted = this.video_.muted;
  678. if (level != null) {
  679. this.video_.volume = Number(level);
  680. }
  681. if (muted != null) {
  682. this.video_.muted = muted;
  683. }
  684. // Notify generic controllers if the volume changed.
  685. if (oldVolumeLevel != this.video_.volume ||
  686. oldVolumeMuted != this.video_.muted) {
  687. this.sendMediaStatus_();
  688. }
  689. break;
  690. }
  691. case 'LOAD': {
  692. // Reset update message frequency values after a load.
  693. this.updateNumber_ = 0;
  694. this.startUpdatingUpdateNumber_ = false;
  695. // This already sends an update.
  696. this.initialStatusUpdatePending_ = false;
  697. const mediaInfo = message['media'];
  698. const contentId = mediaInfo['contentId'];
  699. const currentTime = message['currentTime'];
  700. const assetUri = this.contentIdCallback_(contentId);
  701. const autoplay = message['autoplay'] || true;
  702. const customData = mediaInfo['customData'];
  703. this.appDataCallback_(customData);
  704. if (autoplay) {
  705. this.video_.autoplay = true;
  706. }
  707. this.player_.load(assetUri, currentTime).then(() => {
  708. if (!this.player_) {
  709. // We've already been destroyed.
  710. return;
  711. }
  712. // Notify generic controllers that the media has changed.
  713. this.sendMediaInfoMessage_();
  714. }).catch((error) => {
  715. goog.asserts.assert(error instanceof shaka.util.Error,
  716. 'Wrong error type!');
  717. // Load failed. Dispatch the error message to the sender.
  718. let type = 'LOAD_FAILED';
  719. if (error.category == shaka.util.Error.Category.PLAYER &&
  720. error.code == shaka.util.Error.Code.LOAD_INTERRUPTED) {
  721. type = 'LOAD_CANCELLED';
  722. }
  723. this.sendMessage_({
  724. 'requestId': Number(message['requestId']),
  725. 'type': type,
  726. }, this.genericBus_);
  727. });
  728. break;
  729. }
  730. default:
  731. shaka.log.warning(
  732. 'Unrecognized message type from the generic Chromecast controller!',
  733. message['type']);
  734. // Dispatch an error to the sender.
  735. this.sendMessage_({
  736. 'requestId': Number(message['requestId']),
  737. 'type': 'INVALID_REQUEST',
  738. 'reason': 'INVALID_COMMAND',
  739. }, this.genericBus_);
  740. break;
  741. }
  742. }
  743. /**
  744. * Tell the sender that the async operation is complete.
  745. * @param {string} senderId
  746. * @param {string} id
  747. * @param {shaka.util.Error} error
  748. * @private
  749. */
  750. sendAsyncComplete_(senderId, id, error) {
  751. if (!this.player_) {
  752. // We've already been destroyed.
  753. return;
  754. }
  755. this.sendMessage_({
  756. 'type': 'asyncComplete',
  757. 'id': id,
  758. 'error': error,
  759. }, this.shakaBus_, senderId);
  760. }
  761. /**
  762. * Since this method is in the compiled library, make sure all messages passed
  763. * in here were created with quoted property names.
  764. * @param {!Object} message
  765. * @param {cast.receiver.CastMessageBus} bus
  766. * @param {string=} senderId
  767. * @private
  768. */
  769. sendMessage_(message, bus, senderId) {
  770. // Cuts log spam when debugging the receiver UI in Chrome.
  771. if (!this.isConnected_) {
  772. return;
  773. }
  774. const serialized = shaka.cast.CastUtils.serialize(message);
  775. if (senderId) {
  776. bus.getCastChannel(senderId).send(serialized);
  777. } else {
  778. bus.broadcast(serialized);
  779. }
  780. }
  781. /**
  782. * @return {string}
  783. * @private
  784. */
  785. getPlayState_() {
  786. const playState = shaka.cast.CastReceiver.PLAY_STATE;
  787. if (this.isIdle_) {
  788. return playState.IDLE;
  789. } else if (this.player_.isBuffering()) {
  790. return playState.BUFFERING;
  791. } else if (this.video_.paused) {
  792. return playState.PAUSED;
  793. } else {
  794. return playState.PLAYING;
  795. }
  796. }
  797. /**
  798. * @param {number=} requestId
  799. * @param {Object=} media
  800. * @private
  801. */
  802. sendMediaStatus_(requestId = 0, media = null) {
  803. const mediaStatus = {
  804. // mediaSessionId is a unique ID for the playback of this specific
  805. // session.
  806. // It's used to identify a specific instance of a playback.
  807. // We don't support multiple playbacks, so just return 0.
  808. 'mediaSessionId': 0,
  809. 'playbackRate': this.video_.playbackRate,
  810. 'playerState': this.getPlayState_(),
  811. 'currentTime': this.video_.currentTime,
  812. // supportedMediaCommands is a sum of all the flags of commands that the
  813. // player supports.
  814. // The list of commands with respective flags is:
  815. // 1 - Pause
  816. // 2 - Seek
  817. // 4 - Stream volume
  818. // 8 - Stream mute
  819. // 16 - Skip forward
  820. // 32 - Skip backward
  821. // We support all of them, and their sum is 63.
  822. 'supportedMediaCommands': 63,
  823. 'volume': {
  824. 'level': this.video_.volume,
  825. 'muted': this.video_.muted,
  826. },
  827. };
  828. if (media) {
  829. mediaStatus['media'] = media;
  830. }
  831. const ret = {
  832. 'requestId': requestId,
  833. 'type': 'MEDIA_STATUS',
  834. 'status': [mediaStatus],
  835. };
  836. this.sendMessage_(ret, this.genericBus_);
  837. }
  838. };
  839. /** @type {number} The interval, in seconds, to poll for changes. */
  840. shaka.cast.CastReceiver.POLL_INTERVAL = 0.5;
  841. /** @type {number} The interval, in seconds, to go "idle". */
  842. shaka.cast.CastReceiver.IDLE_INTERVAL = 5;
  843. /**
  844. * @enum {string}
  845. */
  846. shaka.cast.CastReceiver.PLAY_STATE = {
  847. IDLE: 'IDLE',
  848. PLAYING: 'PLAYING',
  849. BUFFERING: 'BUFFERING',
  850. PAUSED: 'PAUSED',
  851. };