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