UNPKG

shaka-player

Version:
448 lines (386 loc) 11.9 kB
/*! @license * Shaka Player * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.queue.QueueManager'); goog.require('goog.asserts'); goog.require('shaka.Player'); goog.require('shaka.config.RepeatMode'); goog.require('shaka.util.Error'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.Timer'); goog.requireType('shaka.media.PreloadManager'); /** * @implements {shaka.extern.IQueueManager} * @implements {shaka.util.IDestroyable} * @export */ shaka.queue.QueueManager = class extends shaka.util.FakeEventTarget { /** * @param {shaka.Player} player */ constructor(player) { super(); /** @private {?shaka.Player} */ this.player_ = player; /** @private {?shaka.extern.QueueConfiguration} */ this.config_ = null; /** @private {!Array<shaka.extern.QueueItem>} */ this.items_ = []; /** @private {number} */ this.currentItemIndex_ = -1; /** * @private {?{ * item: shaka.extern.QueueItem, * preloadManager: ?shaka.media.PreloadManager, * }} */ this.preloadNext_ = null; /** * @private {?{ * item: shaka.extern.QueueItem, * preloadManager: ?shaka.media.PreloadManager, * }} */ this.preloadPrev_ = null; /** @private {shaka.util.EventManager} */ this.eventManager_ = new shaka.util.EventManager(); /** @private {?shaka.util.Timer} */ this.repeatTimer_ = null; } /** * @override * @export */ async destroy() { await this.removeAllItems(); this.player_ = null; if (this.eventManager_) { this.eventManager_.release(); this.eventManager_ = null; } if (this.repeatTimer_) { this.repeatTimer_.stop(); this.repeatTimer_ = null; } // FakeEventTarget implements IReleasable super.release(); } /** * @override * @export */ configure(config) { this.config_ = config; } /** * @override * @export */ getConfiguration() { return this.config_; } /** * @override * @export */ setCustomPlayer(player) { this.player_ = player; } /** * @override * @export */ getCurrentItem() { if (this.items_.length && this.currentItemIndex_ >= 0 && this.currentItemIndex_ < this.items_.length) { return this.items_[this.currentItemIndex_]; } return null; } /** * @override * @export */ getCurrentItemIndex() { return this.currentItemIndex_; } /** * @override * @export */ getItems() { return this.items_.slice(); } /** * @override * @export */ insertItems(items) { this.items_.push(...items); this.dispatchEvent(new shaka.util.FakeEvent( shaka.util.FakeEvent.EventName.ItemsInserted)); } /** * @override * @export */ async removeAllItems() { this.eventManager_.removeAll(); if (this.player_ && this.items_.length && this.currentItemIndex_ >= 0) { try { await this.player_.unload(); } catch (e) { // Ignore errors during unload } } const promises = []; if (this.preloadPrev_?.preloadManager && !this.preloadPrev_.preloadManager.isDestroyed()) { promises.push(this.preloadPrev_.preloadManager.destroy()); } this.preloadPrev_ = null; if (this.preloadNext_?.preloadManager && !this.preloadNext_.preloadManager.isDestroyed()) { promises.push(this.preloadNext_.preloadManager.destroy()); } this.preloadNext_ = null; for (const item of this.items_) { if (item.preloadManager && !item.preloadManager.isDestroyed()) { promises.push(item.preloadManager.destroy()); } } if (promises.length) { await Promise.all(promises); } this.items_ = []; this.currentItemIndex_ = -1; this.dispatchEvent(new shaka.util.FakeEvent( shaka.util.FakeEvent.EventName.ItemsRemoved)); } /** * @override * @export */ async playItem(itemIndex) { goog.asserts.assert(this.player_, 'We should have player'); this.eventManager_.removeAll(); if (this.repeatTimer_) { this.repeatTimer_.stop(); this.repeatTimer_ = null; } if (!this.items_.length || itemIndex < 0 || itemIndex >= this.items_.length) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.PLAYER, shaka.util.Error.Code.QUEUE_INDEX_OUT_OF_BOUNDS); } const currentItem = this.getCurrentItem(); const item = this.items_[itemIndex]; if (this.currentItemIndex_ !== itemIndex) { this.currentItemIndex_ = itemIndex; this.dispatchEvent(new shaka.util.FakeEvent( shaka.util.FakeEvent.EventName.CurrentItemChanged)); } const mediaElement = this.player_.getMediaElement(); this.setupPreloadNext_(mediaElement); this.setupRepeatOnComplete_(mediaElement); const assetUriOrPreloader = this.getAssetOrPreloader_(item); await this.cleanupPreloadPrev_(item, currentItem); if (item.config) { this.player_.resetConfiguration(); this.player_.configure(item.config); } await this.player_.load(assetUriOrPreloader, item.startTime, item.mimeType); this.preloadNext_ = null; await this.addExtraTracks_(item); } /** * Sets up preloading of the next item if applicable * * @param {HTMLMediaElement} mediaElement * @private */ setupPreloadNext_(mediaElement) { if (!this.config_ || this.config_.preloadNextUrlWindow <= 0) { return; } let preloadInProcess = false; const listener = async () => { if (this.preloadNext_ || this.items_.length <= 1 || preloadInProcess || this.player_.isDynamic() || !mediaElement.duration) { return; } const timeToEnd = this.player_.seekRange().end - mediaElement.currentTime; if (isNaN(timeToEnd) || timeToEnd > this.config_.preloadNextUrlWindow) { return; } preloadInProcess = true; let nextItem = null; const repeatMode = this.config_.repeatMode; const nextIndex = this.currentItemIndex_ + 1; if (nextIndex < this.items_.length) { nextItem = this.items_[nextIndex]; } else if (repeatMode === shaka.config.RepeatMode.ALL) { nextItem = this.items_[0]; } if (nextItem && (!nextItem.preloadManager || nextItem.preloadManager.isDestroyed())) { try { const preloadManager = await this.player_.preload( nextItem.manifestUri, nextItem.startTime, nextItem.mimeType, nextItem.config); this.preloadNext_ = {item: nextItem, preloadManager}; } catch (e) { // Ignore errors during preload this.preloadNext_ = {item: nextItem, preloadManager: null}; } // Remove listener once next item is preloaded this.eventManager_.unlisten(mediaElement, 'timeupdate', listener); } preloadInProcess = false; }; this.eventManager_.listen(mediaElement, 'timeupdate', listener); } /** * Handles repeating the current item when paused * * @param {HTMLMediaElement} mediaElement * @private */ playCurrentItemAfterPause_(mediaElement) { if (mediaElement.paused) { mediaElement.currentTime = this.player_.seekRange().start; mediaElement.play(); } else { this.eventManager_.listenOnce(mediaElement, 'paused', () => { mediaElement.currentTime = this.player_.seekRange().start; mediaElement.play(); }); } } /** * Sets up repeat behavior on playback completion * * @param {HTMLMediaElement} mediaElement * @private */ setupRepeatOnComplete_(mediaElement) { this.eventManager_.listen(this.player_, 'complete', () => { const repeatMode = this.config_?.repeatMode; if (repeatMode === shaka.config.RepeatMode.OFF) { return; } if (repeatMode === shaka.config.RepeatMode.SINGLE) { this.playCurrentItemAfterPause_(mediaElement); return; } const nextIndex = this.currentItemIndex_ + 1; let targetIndex = null; if (nextIndex < this.items_.length) { targetIndex = nextIndex; } else if (repeatMode === shaka.config.RepeatMode.ALL) { targetIndex = (this.items_.length > 1) ? 0 : this.currentItemIndex_; } if (targetIndex !== null) { if (targetIndex === this.currentItemIndex_) { this.playCurrentItemAfterPause_(mediaElement); } else { if (this.repeatTimer_) { this.repeatTimer_.stop(); this.repeatTimer_ = null; } this.repeatTimer_ = new shaka.util.Timer(() => { goog.asserts.assert(targetIndex != null, 'targetIndex should not be null'); this.playItem(targetIndex).catch(() => {}); }).tickAfter(0); } } }); } /** * Determines which asset to use: preloadPrev_, preloadNext_ or manifestUri * * @param {!shaka.extern.QueueItem} item * @return {string|shaka.media.PreloadManager} * @private */ getAssetOrPreloader_(item) { let asset = item.manifestUri; if (item.preloadManager && !item.preloadManager.isDestroyed()) { asset = item.preloadManager; } else if (this.preloadNext_?.item === item && this.preloadNext_.preloadManager) { asset = this.preloadNext_.preloadManager; } else if (this.preloadPrev_?.item === item && this.preloadPrev_.preloadManager) { asset = this.preloadPrev_.preloadManager; } return asset; } /** * Cleans up preloadPrev_ if no longer needed and saves preload of the * previous item * * @param {!shaka.extern.QueueItem} currentItem * @param {?shaka.extern.QueueItem} previousItem * @private */ async cleanupPreloadPrev_(currentItem, previousItem) { const usingPrev = this.preloadPrev_?.item === currentItem; if (this.preloadPrev_ && !usingPrev && this.preloadPrev_.preloadManager && !this.preloadPrev_.preloadManager.isDestroyed()) { await this.preloadPrev_.preloadManager.destroy(); } this.preloadPrev_ = null; if (this.config_?.preloadPrevItem && previousItem && this.player_.getLoadMode() === shaka.Player.LoadMode.MEDIA_SOURCE) { try { const preloadManager = await this.player_.unloadAndSavePreload(); this.preloadPrev_ = {item: previousItem, preloadManager}; } catch (e) { this.preloadPrev_ = {item: previousItem, preloadManager: null}; } } } /** * Adds extra tracks (text, thumbnails, chapters) in parallel * * @param {!shaka.extern.QueueItem} item * @return {!Promise} * @private */ async addExtraTracks_(item) { const textPromises = item.extraText?.map(async (extraText) => { if (extraText.mime) { await this.player_.addTextTrackAsync( extraText.uri, extraText.language, extraText.kind, extraText.mime, extraText.codecs); } else { await this.player_.addTextTrackAsync( extraText.uri, extraText.language, extraText.kind); } }) || []; const thumbnailPromises = item.extraThumbnail?.map(async (thumb) => { await this.player_.addThumbnailsTrack(thumb); }) || []; const chapterPromises = item.extraChapter?.map(async (chapter) => { await this.player_.addChaptersTrack( chapter.uri, chapter.language, chapter.mime); }) || []; await Promise.all([ ...textPromises, ...thumbnailPromises, ...chapterPromises, ]); } }; shaka.Player.setQueueManagerFactory((player) => { return new shaka.queue.QueueManager(player); });