From 2ad1eff39ef063e026393a0e26ebc48ca3452b06 Mon Sep 17 00:00:00 2001 From: theodab Date: Tue, 5 Nov 2024 04:13:02 -0800 Subject: [PATCH] feat(preload): Wait for prefetches when preloading (#7533) Previously, the PreloadManager would consider a preload "finished" after a few major files like the manifest had been preloaded. It would start prefetching some segments, but wouldn't wait on it to notify the developer. This PR changes the PreloadManager so that PreloadManager.waitForFinish won't return until the prefetched segments have finished loading. Because of that, this also better surfaces errors thrown during segment prefetching, when preloading. Issue #7520 --- lib/media/preload_manager.js | 16 +++++---- lib/media/segment_prefetch.js | 45 ++++++++++++++++++------- test/test/util/fake_segment_prefetch.js | 1 + 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/lib/media/preload_manager.js b/lib/media/preload_manager.js index 9c4e214798..08cdc4accd 100644 --- a/lib/media/preload_manager.js +++ b/lib/media/preload_manager.js @@ -607,9 +607,10 @@ shaka.media.PreloadManager = class extends shaka.util.FakeEventTarget { * Performs a final filtering of the manifest, and chooses the initial * variant. * + * @return {!Promise} * @private */ - chooseInitialVariantInner_() { + async chooseInitialVariantInner_() { goog.asserts.assert( this.manifest_, 'The manifest should already be parsed.'); @@ -655,13 +656,15 @@ shaka.media.PreloadManager = class extends shaka.util.FakeEventTarget { this.abrManager_.setVariants(Array.from(adaptationSet.values())); const variant = this.abrManager_.chooseVariant(); if (variant) { + const promises = []; this.prefetchedVariant_ = variant; if (variant.video) { - this.makePrefetchForStream_(variant.video, isLive); + promises.push(this.prefetchStream_(variant.video, isLive)); } if (variant.audio) { - this.makePrefetchForStream_(variant.audio, isLive); + promises.push(this.prefetchStream_(variant.audio, isLive)); } + await Promise.all(promises); } } } @@ -672,7 +675,7 @@ shaka.media.PreloadManager = class extends shaka.util.FakeEventTarget { * @return {!Promise} * @private */ - async makePrefetchForStream_(stream, isLive) { + async prefetchStream_(stream, isLive) { // Use the prefetch limit from the config if this is set, otherwise use 2. const prefetchLimit = this.config_.streaming.segmentPrefetchLimit || 2; const prefetch = new shaka.media.SegmentPrefetch( @@ -700,13 +703,14 @@ shaka.media.PreloadManager = class extends shaka.util.FakeEventTarget { if (isLive) { // Preload only the init segment for Live if (prefetchSegment.initSegmentReference) { - prefetch.prefetchInitSegment(prefetchSegment.initSegmentReference); + await prefetch.prefetchInitSegment( + prefetchSegment.initSegmentReference); } } else { // Preload a segment, too... either the first segment, or the segment // that corresponds with this.startTime_, as appropriate. // Note: this method also preload the init segment - prefetch.prefetchSegmentsByTime(prefetchSegment.startTime); + await prefetch.prefetchSegmentsByTime(prefetchSegment.startTime); } } } diff --git a/lib/media/segment_prefetch.js b/lib/media/segment_prefetch.js index 03ec5a8f33..b7ea6ceadc 100644 --- a/lib/media/segment_prefetch.js +++ b/lib/media/segment_prefetch.js @@ -12,6 +12,7 @@ goog.require('shaka.media.InitSegmentReference'); goog.require('shaka.media.SegmentIterator'); goog.require('shaka.media.SegmentReference'); goog.require('shaka.net.NetworkingEngine'); +goog.require('shaka.util.Error'); goog.require('shaka.util.Uint8ArrayUtils'); @@ -74,6 +75,7 @@ shaka.media.SegmentPrefetch = class { * * @param {number} currTime * @param {boolean=} skipFirst + * @return {!Promise} * @public */ prefetchSegmentsByTime(currTime, skipFirst = false) { @@ -83,7 +85,7 @@ shaka.media.SegmentPrefetch = class { const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_); if (!this.stream_.segmentIndex) { shaka.log.debug(logPrefix, 'missing segmentIndex'); - return; + return Promise.resolve(); } if (!this.iterator_) { this.iterator_ = this.stream_.segmentIndex.getIteratorForTime( @@ -91,11 +93,12 @@ shaka.media.SegmentPrefetch = class { } if (!this.iterator_) { shaka.log.debug(logPrefix, 'missing iterator'); - return; + return Promise.resolve(); } if (skipFirst) { this.iterator_.next(); } + const promises = []; while (this.segmentPrefetchMap_.size < this.prefetchLimit_) { const reference = this.iterator_.next().value; if (!reference) { @@ -115,22 +118,26 @@ shaka.media.SegmentPrefetch = class { prefetchAllowed = false; } if (prefetchAllowed && reference.initSegmentReference) { - this.prefetchInitSegment(reference.initSegmentReference); + promises.push(this.prefetchInitSegment( + reference.initSegmentReference)); } if (prefetchAllowed && !this.segmentPrefetchMap_.has(reference)) { const segmentPrefetchOperation = new shaka.media.SegmentPrefetchOperation(this.fetchDispatcher_); - segmentPrefetchOperation.dispatchFetch(reference, this.stream_); + promises.push(segmentPrefetchOperation.dispatchFetch( + reference, this.stream_)); this.segmentPrefetchMap_.set(reference, segmentPrefetchOperation); } } this.clearInitSegments_(); + return Promise.all(promises); } /** * Fetch init segment. * * @param {!shaka.media.InitSegmentReference} initSegmentReference + * @return {!Promise} */ prefetchInitSegment(initSegmentReference) { goog.asserts.assert(this.prefetchLimit_ > 0, @@ -139,11 +146,11 @@ shaka.media.SegmentPrefetch = class { const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_); if (!this.stream_.segmentIndex) { shaka.log.debug(logPrefix, 'missing segmentIndex'); - return; + return Promise.resolve(); } if (initSegmentReference.getSegmentData()) { - return; + return Promise.resolve(); } // init segments are ignored from the prefetch limit @@ -152,14 +159,16 @@ shaka.media.SegmentPrefetch = class { return shaka.media.InitSegmentReference.equal( reference, initSegmentReference); }); - if (!someReference) { - const segmentPrefetchOperation = - new shaka.media.SegmentPrefetchOperation(this.fetchDispatcher_); - segmentPrefetchOperation.dispatchFetch( - initSegmentReference, this.stream_); - this.initSegmentPrefetchMap_.set( - initSegmentReference, segmentPrefetchOperation); + if (someReference) { + return Promise.resolve(); } + const segmentPrefetchOperation = new shaka.media.SegmentPrefetchOperation( + this.fetchDispatcher_); + const promise = segmentPrefetchOperation.dispatchFetch( + initSegmentReference, this.stream_); + this.initSegmentPrefetchMap_.set( + initSegmentReference, segmentPrefetchOperation); + return promise; } /** @@ -415,6 +424,7 @@ shaka.media.SegmentPrefetchOperation = class { * @param {!(shaka.media.SegmentReference|shaka.media.InitSegmentReference)} * reference * @param {!shaka.extern.Stream} stream + * @return {!Promise} * @public */ dispatchFetch(reference, stream) { @@ -433,6 +443,15 @@ shaka.media.SegmentPrefetchOperation = class { buffered = new Uint8Array(0); } }); + return this.operation_.promise.catch((e) => { + // Ignore OPERATION_ABORTED errors. + if (e instanceof shaka.util.Error && + e.code == shaka.util.Error.Code.OPERATION_ABORTED) { + return Promise.resolve(); + } + // Continue to surface other errors. + return Promise.reject(e); + }); } /** diff --git a/test/test/util/fake_segment_prefetch.js b/test/test/util/fake_segment_prefetch.js index cd057f6aa3..115284c307 100644 --- a/test/test/util/fake_segment_prefetch.js +++ b/test/test/util/fake_segment_prefetch.js @@ -63,6 +63,7 @@ shaka.test.FakeSegmentPrefetch = class { this.prefetchPosTime_ = reference.startTime; reference = iterator.next().value; } + return Promise.resolve(); } /** @override */