diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 7d09876d9f7..915b76cd0aa 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -803,6 +803,12 @@ export interface BufferEOSData { // // @public (undocumented) export interface BufferFlushedData { + // (undocumented) + end: number; + // (undocumented) + error?: Error; + // (undocumented) + start: number; // (undocumented) type: SourceBufferName; } diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 8f0e90948b5..af5317e80f1 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -1342,7 +1342,7 @@ export default class BaseStreamController const config = this.config; const minLength = Math.max( Math.min(threshold - fragDuration, config.maxBufferLength), - fragDuration, + fragDuration / 2, ); const reducedLength = Math.max( threshold - fragDuration * 3, @@ -2038,6 +2038,16 @@ export default class BaseStreamController this.warn( `Buffer full error while media.currentTime (${this.getLoadPosition()}) is not buffered, flush ${playlistType} buffer`, ); + } else if ( + bufferedInfo.nextStart && + frag && + bufferedInfo.nextStart > frag.start + ) { + this.flushMainBuffer( + bufferedInfo.nextStart, + Number.POSITIVE_INFINITY, + playlistType === PlaylistLevelType.AUDIO ? playlistType : undefined, + ); } if (frag) { this.fragmentTracker.removeFragment(frag); diff --git a/src/controller/buffer-controller.ts b/src/controller/buffer-controller.ts index bddf826c332..02e04efa9d8 100755 --- a/src/controller/buffer-controller.ts +++ b/src/controller/buffer-controller.ts @@ -118,10 +118,7 @@ export default class BufferController extends Logger implements ComponentAPI { audiovideo: 0, }; private appendError?: ErrorData; - // Tracks whether a QuotaExceededError back-buffer eviction is in progress - // for a given SourceBuffer type. While true, subsequent QuotaExceededErrors - // on the same type piggyback on the pending eviction instead of triggering - // their own or emitting BUFFER_FULL_ERROR. + // Tracks whether a QuotaExceededError back-buffer eviction is in progress for a given SourceBuffer type. private _quotaEvictionPending: Partial> = {}; // Record of required or created buffers by type. SourceBuffer is stored in Track.buffer once created. @@ -859,7 +856,6 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe } const fragStart = (part || frag).start; - let quotaEvictionAttempted = false; const operation: BufferOperation = { label: `append-${type}`, execute: () => { @@ -914,31 +910,22 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe `quota` in error; if (isQuotaError) { - // An eviction is already in flight for this type — piggyback on it - // by queuing a retry after the pending remove, no new eviction needed. - if (this._quotaEvictionPending[type]) { - this.log( - `QuotaExceededError on "${type}" sn: ${sn} — eviction already pending, queuing retry`, - ); - this.insertNext([operation], type); - return; - } - // First QuotaExceededError for this type: evict minimum back buffer - if (!quotaEvictionAttempted) { + if (!this._quotaEvictionPending[type]) { const evictEnd = this.getBackBufferEvictionTarget( type, data.byteLength, frag.type, ); if (evictEnd > 0) { - quotaEvictionAttempted = true; this._quotaEvictionPending[type] = true; this.log( `QuotaExceededError on "${type}" append sn: ${sn} — evicting back buffer to ${evictEnd.toFixed(3)}s and retrying`, ); - const removeOp = this.getQuotaEvictionFlushOp(type, 0, evictEnd); - this.insertNext([removeOp, operation], type); + const removeOp = this.getFlushOp(type, 0, evictEnd); + const clearOp = this.getClearEvictionPendingOp(type); + + this.insertNext([removeOp, operation, clearOp], type); return; } this.warn( @@ -1014,30 +1001,15 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe this.append(operation, type, this.isPending(this.tracks[type])); } - // Like getFlushOp but clears _quotaEvictionPending on complete/error - private getQuotaEvictionFlushOp( - type: SourceBufferName, - start: number, - end: number, - ): BufferOperation { - this.log(`queuing quota-eviction "${type}" remove ${start}-${end}`); + private getClearEvictionPendingOp(type: string): BufferOperation { return { - label: 'remove', + label: 'clear', execute: () => { - this.removeExecutor(type, start, end); - }, - onStart: () => {}, - onComplete: () => { this._quotaEvictionPending[type] = false; - this.hls.trigger(Events.BUFFER_FLUSHED, { type }); - }, - onError: (error: Error) => { - this._quotaEvictionPending[type] = false; - this.warn( - `Failed to remove ${start}-${end} from "${type}" SourceBuffer (quota eviction)`, - error, - ); }, + onStart: () => {}, + onComplete: () => {}, + onError: () => {}, }; } @@ -1057,13 +1029,19 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe }, onComplete: () => { // logger.debug(`[buffer-controller]: Finished flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`); - this.hls.trigger(Events.BUFFER_FLUSHED, { type }); + this.hls.trigger(Events.BUFFER_FLUSHED, { type, start, end }); }, onError: (error: Error) => { this.warn( `Failed to remove ${start}-${end} from "${type}" SourceBuffer`, error, ); + this.hls.trigger(Events.BUFFER_FLUSHED, { + type, + start: 0, + end: 0, + error, + }); }, }; } @@ -1677,6 +1655,8 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe if (removedRanges?.length) { this.hls.trigger(Events.BUFFER_FLUSHED, { type: type, + start: removedRanges.start(0), + end: removedRanges.end(removedRanges.length - 1), }); } }, @@ -1837,11 +1817,9 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe const track = this.tracks[type]; const sb = track?.buffer; if (!media || !mediaSource || !sb) { - this.warn( + throw new Error( `Attempting to remove from the ${type} SourceBuffer, but it does not exist`, ); - this.shiftAndExecuteNext(type); - return; } const mediaDuration = Number.isFinite(media.duration) ? media.duration @@ -1858,8 +1836,9 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe ); sb.remove(removeStart, removeEnd); } else { - // Cycle the queue - this.shiftAndExecuteNext(type); + throw new Error( + `Cannot remove ${removeEnd <= removeStart ? `invalid range (${removeStart} >= ${removeEnd}) ` : ''}from the ${type} SourceBuffer${track.ending ? ' while track ending' : ''}`, + ); } } diff --git a/src/types/events.ts b/src/types/events.ts index 9abaa24f561..56ffa048960 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -117,6 +117,9 @@ export interface BufferFlushingData { export interface BufferFlushedData { type: SourceBufferName; + start: number; + end: number; + error?: Error; } export interface ManifestLoadingData { diff --git a/tests/unit/controller/buffer-controller-operations.ts b/tests/unit/controller/buffer-controller-operations.ts index 9d6e210369a..8d0f4c36f12 100644 --- a/tests/unit/controller/buffer-controller-operations.ts +++ b/tests/unit/controller/buffer-controller-operations.ts @@ -780,8 +780,7 @@ describe('BufferController with attached media', function () { ).to.have.been.calledOnce; }); - it('dequeues the remove operation if the requested remove range is not valid', function () { - // Does not flush if start greater than end + it('Errors and signals flushed with error when the requested remove range is not valid', function () { hls.trigger(Events.BUFFER_FLUSHING, { startOffset: 9001, endOffset: 9000, @@ -811,7 +810,27 @@ describe('BufferController with attached media', function () { expect( triggerSpy, 'Only Events.BUFFER_FLUSHING should have been triggered', - ).to.have.been.calledOnce; + ).to.have.been.calledThrice; + const err1 = triggerSpy.getCall(1).lastArg.error; + const err2 = triggerSpy.getCall(2).lastArg.error; + expect(triggerSpy).to.have.been.calledWith(Events.BUFFER_FLUSHED, { + start: 0, + end: 0, + type: 'video', + error: err1, + }); + expect(err1.message).to.eq( + 'Cannot remove invalid range (9001 >= 9000) from the video SourceBuffer', + ); + expect(triggerSpy).to.have.been.calledWith(Events.BUFFER_FLUSHED, { + start: 0, + end: 0, + type: 'audio', + error: err2, + }); + expect(err2.message).to.eq( + 'Cannot remove invalid range (9001 >= 9000) from the audio SourceBuffer', + ); }); });