Source: lib/queue/queue_manager.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2025 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.queue.QueueManager');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.Player');
  9. goog.require('shaka.config.RepeatMode');
  10. goog.require('shaka.util.Error');
  11. goog.require('shaka.util.EventManager');
  12. goog.require('shaka.util.FakeEvent');
  13. goog.require('shaka.util.FakeEventTarget');
  14. goog.require('shaka.util.IDestroyable');
  15. goog.requireType('shaka.media.PreloadManager');
  16. /**
  17. * @implements {shaka.extern.IQueueManager}
  18. * @implements {shaka.util.IDestroyable}
  19. * @export
  20. */
  21. shaka.queue.QueueManager = class extends shaka.util.FakeEventTarget {
  22. /**
  23. * @param {shaka.Player} player
  24. */
  25. constructor(player) {
  26. super();
  27. /** @private {?shaka.Player} */
  28. this.player_ = player;
  29. /** @private {?shaka.extern.QueueConfiguration} */
  30. this.config_ = null;
  31. /** @private {!Array<shaka.extern.QueueItem>} */
  32. this.items_ = [];
  33. /** @private {number} */
  34. this.currentItemIndex_ = -1;
  35. /**
  36. * @private {?{item: shaka.extern.QueueItem,
  37. * preloadManager: ?shaka.media.PreloadManager}}
  38. */
  39. this.preloadNext_ = null;
  40. /** @private {shaka.util.EventManager} */
  41. this.eventManager_ = new shaka.util.EventManager();
  42. }
  43. /**
  44. * @override
  45. * @export
  46. */
  47. async destroy() {
  48. await this.removeAllItems();
  49. this.player_ = null;
  50. if (this.eventManager_) {
  51. this.eventManager_.release();
  52. this.eventManager_ = null;
  53. }
  54. // FakeEventTarget implements IReleasable
  55. super.release();
  56. }
  57. /**
  58. * @override
  59. * @export
  60. */
  61. configure(config) {
  62. this.config_ = config;
  63. }
  64. /**
  65. * @override
  66. * @export
  67. */
  68. getConfiguration() {
  69. return this.config_;
  70. }
  71. /**
  72. * @override
  73. * @export
  74. */
  75. getCurrentItem() {
  76. if (this.items_.length && this.currentItemIndex_ >= 0 &&
  77. this.currentItemIndex_ < this.items_.length) {
  78. return this.items_[this.currentItemIndex_];
  79. }
  80. return null;
  81. }
  82. /**
  83. * @override
  84. * @export
  85. */
  86. getCurrentItemIndex() {
  87. return this.currentItemIndex_;
  88. }
  89. /**
  90. * @override
  91. * @export
  92. */
  93. getItems() {
  94. return this.items_.slice();
  95. }
  96. /**
  97. * @override
  98. * @export
  99. */
  100. insertItems(items) {
  101. this.items_.push(...items);
  102. this.dispatchEvent(new shaka.util.FakeEvent(
  103. shaka.util.FakeEvent.EventName.ItemsInserted));
  104. }
  105. /**
  106. * @override
  107. * @export
  108. */
  109. async removeAllItems() {
  110. this.eventManager_.removeAll();
  111. if (this.items_.length && this.currentItemIndex_ >= 0) {
  112. await this.player_.unload();
  113. }
  114. if (this.preloadNext_) {
  115. if (!this.preloadNext_.preloadManager.isDestroyed()) {
  116. await this.preloadNext_.preloadManager.destroy();
  117. }
  118. this.preloadNext_ = null;
  119. }
  120. this.items_ = [];
  121. this.currentItemIndex_ = -1;
  122. this.dispatchEvent(new shaka.util.FakeEvent(
  123. shaka.util.FakeEvent.EventName.ItemsRemoved));
  124. }
  125. /**
  126. * @override
  127. * @export
  128. */
  129. async playItem(itemIndex) {
  130. goog.asserts.assert(this.player_, 'We should have player');
  131. this.eventManager_.removeAll();
  132. if (!this.items_.length || itemIndex >= this.items_.length) {
  133. throw new shaka.util.Error(
  134. shaka.util.Error.Severity.CRITICAL,
  135. shaka.util.Error.Category.PLAYER,
  136. shaka.util.Error.Code.QUEUE_INDEX_OUT_OF_BOUNDS);
  137. }
  138. const item = this.items_[itemIndex];
  139. if (this.currentItemIndex_ != itemIndex) {
  140. this.currentItemIndex_ = itemIndex;
  141. this.dispatchEvent(new shaka.util.FakeEvent(
  142. shaka.util.FakeEvent.EventName.CurrentItemChanged));
  143. }
  144. const mediaElement = this.player_.getMediaElement();
  145. const preloadNextUrlWindow =
  146. this.config_ ? this.config_.preloadNextUrlWindow : 0;
  147. if (preloadNextUrlWindow > 0) {
  148. let preloadInProcess = false;
  149. this.eventManager_.listen(mediaElement, 'timeupdate', async () => {
  150. if (this.preloadNext_ || this.items_.length <= 1 || preloadInProcess ||
  151. this.player_.isLive() || !mediaElement.duration) {
  152. return;
  153. }
  154. const timeToEnd =
  155. this.player_.seekRange().end - mediaElement.currentTime;
  156. if (!isNaN(timeToEnd)) {
  157. if (timeToEnd <= preloadNextUrlWindow) {
  158. const repeatMode = this.config_ && this.config_.repeatMode;
  159. let nextItem = null;
  160. if ((this.currentItemIndex_ + 1) < this.items_.length) {
  161. nextItem = this.items_[this.currentItemIndex_ + 1];
  162. } else if (repeatMode == shaka.config.RepeatMode.ALL) {
  163. nextItem = this.items_[0];
  164. }
  165. if (nextItem) {
  166. preloadInProcess = true;
  167. let preloadManager = null;
  168. try {
  169. preloadManager = await this.player_.preload(
  170. nextItem.manifestUri,
  171. nextItem.startTime,
  172. nextItem.mimeType,
  173. nextItem.config);
  174. } catch (error) {
  175. preloadManager = null;
  176. // Ignore errors.
  177. }
  178. this.preloadNext_ = {
  179. item: nextItem,
  180. preloadManager,
  181. };
  182. preloadInProcess = false;
  183. }
  184. }
  185. }
  186. });
  187. }
  188. this.eventManager_.listen(this.player_, 'complete', () => {
  189. const repeatMode = this.config_ && this.config_.repeatMode;
  190. let playAgain = false;
  191. if (repeatMode == shaka.config.RepeatMode.SINGLE) {
  192. playAgain = true;
  193. } else {
  194. const nextItemIndex = this.currentItemIndex_ + 1;
  195. if (nextItemIndex < this.items_.length) {
  196. this.playItem(nextItemIndex);
  197. } else if (repeatMode == shaka.config.RepeatMode.ALL) {
  198. if (this.items_.length == 1) {
  199. playAgain = true;
  200. } else {
  201. this.playItem(0);
  202. }
  203. }
  204. }
  205. if (playAgain) {
  206. if (mediaElement.paused) {
  207. mediaElement.currentTime = this.player_.seekRange().start;
  208. mediaElement.play();
  209. } else {
  210. this.eventManager_.listen(mediaElement, 'paused', () => {
  211. mediaElement.currentTime = this.player_.seekRange().start;
  212. mediaElement.play();
  213. });
  214. }
  215. }
  216. });
  217. if (item.config) {
  218. this.player_.resetConfiguration();
  219. this.player_.configure(item.config);
  220. }
  221. let assetUriOrPreloader = item.manifestUri;
  222. if (this.preloadNext_ && this.preloadNext_.item == item &&
  223. this.preloadNext_.preloadManager) {
  224. assetUriOrPreloader = this.preloadNext_.preloadManager;
  225. }
  226. await this.player_.load(assetUriOrPreloader, item.startTime, item.mimeType);
  227. this.preloadNext_ = null;
  228. if (item.extraText) {
  229. for (const extraText of item.extraText) {
  230. if (extraText.mime) {
  231. this.player_.addTextTrackAsync(extraText.uri, extraText.language,
  232. extraText.kind, extraText.mime, extraText.codecs);
  233. } else {
  234. this.player_.addTextTrackAsync(extraText.uri, extraText.language,
  235. extraText.kind);
  236. }
  237. }
  238. }
  239. if (item.extraThumbnail) {
  240. for (const extraThumbnail of item.extraThumbnail) {
  241. this.player_.addThumbnailsTrack(extraThumbnail);
  242. }
  243. }
  244. if (item.extraChapter) {
  245. for (const extraChapter of item.extraChapter) {
  246. this.player_.addChaptersTrack(
  247. extraChapter.uri, extraChapter.language, extraChapter.mime);
  248. }
  249. }
  250. }
  251. };
  252. shaka.Player.setQueueManagerFactory((player) => {
  253. return new shaka.queue.QueueManager(player);
  254. });