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