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