diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 42b581f285d..1cb3265a042 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -330,6 +330,8 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP // (undocumented) protected getFwdBufferInfo(bufferable: Bufferable | null, type: PlaylistLevelType): BufferInfo | null; // (undocumented) + protected getFwdBufferInfoAtPos(bufferable: Bufferable | null, pos: number, type: PlaylistLevelType): BufferInfo | null; + // (undocumented) protected getInitialLiveFragment(levelDetails: LevelDetails, fragments: Array): Fragment | null; // (undocumented) protected getLevelDetails(): LevelDetails | undefined; @@ -404,7 +406,9 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP // (undocumented) protected onvseeking: EventListener | null; // (undocumented) - protected reduceMaxBufferLength(threshold?: number): boolean; + protected reduceLengthAndFlushBuffer(data: ErrorData): boolean; + // (undocumented) + protected reduceMaxBufferLength(threshold: number): boolean; // (undocumented) protected resetFragmentErrors(filterType: PlaylistLevelType): void; // (undocumented) diff --git a/src/controller/audio-stream-controller.ts b/src/controller/audio-stream-controller.ts index 9d946b022d8..9232477432a 100644 --- a/src/controller/audio-stream-controller.ts +++ b/src/controller/audio-stream-controller.ts @@ -671,33 +671,12 @@ class AudioStreamController } break; case ErrorDetails.BUFFER_FULL_ERROR: - // if in appending state - if ( - data.parent === 'audio' && - (this.state === State.PARSING || this.state === State.PARSED) - ) { - let flushBuffer = true; - const bufferedInfo = this.getFwdBufferInfo( - this.mediaBuffer, - PlaylistLevelType.AUDIO - ); - // 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end - // reduce max buf len if current position is buffered - if (bufferedInfo && bufferedInfo.len > 0.5) { - flushBuffer = !this.reduceMaxBufferLength(bufferedInfo.len); - } - if (flushBuffer) { - // current position is not buffered, but browser is still complaining about buffer full error - // this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708 - // in that case flush the whole audio buffer to recover - this.warn( - 'Buffer full error also media.currentTime is not buffered, flush audio buffer' - ); - this.fragCurrent = null; - this.bufferedTrack = null; - super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio'); - } - this.resetLoadingState(); + if (!data.parent || data.parent !== 'audio') { + return; + } + if (this.reduceLengthAndFlushBuffer(data)) { + this.bufferedTrack = null; + super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio'); } break; default: diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index e9b2b5db983..4ff03d3e00e 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -899,16 +899,22 @@ export default class BaseStreamController bufferable: Bufferable | null, type: PlaylistLevelType ): BufferInfo | null { - const { config } = this; const pos = this.getLoadPosition(); if (!Number.isFinite(pos)) { return null; } - const bufferInfo = BufferHelper.bufferInfo( - bufferable, - pos, - config.maxBufferHole - ); + return this.getFwdBufferInfoAtPos(bufferable, pos, type); + } + + protected getFwdBufferInfoAtPos( + bufferable: Bufferable | null, + pos: number, + type: PlaylistLevelType + ): BufferInfo | null { + const { + config: { maxBufferHole }, + } = this; + const bufferInfo = BufferHelper.bufferInfo(bufferable, pos, maxBufferHole); // Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) { const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type); @@ -916,7 +922,7 @@ export default class BaseStreamController return BufferHelper.bufferInfo( bufferable, pos, - Math.max(bufferInfo.nextStart, config.maxBufferHole) + Math.max(bufferInfo.nextStart, maxBufferHole) ); } } @@ -937,7 +943,7 @@ export default class BaseStreamController return Math.min(maxBufLen, config.maxMaxBufferLength); } - protected reduceMaxBufferLength(threshold?: number) { + protected reduceMaxBufferLength(threshold: number) { const config = this.config; const minLength = threshold || config.maxBufferLength; if (config.maxMaxBufferLength >= minLength) { @@ -1177,7 +1183,7 @@ export default class BaseStreamController this.fragmentTracker.getState(nextFrag) !== FragmentState.OK ) { this.log( - `SN ${frag.sn} just loaded, load next one: ${nextFrag.sn}` + `Skipping loaded ${frag.type} SN ${frag.sn} at buffer end` ); frag = nextFrag; } else { @@ -1413,6 +1419,39 @@ export default class BaseStreamController } else { this.state = State.ERROR; } + // Perform next async tick sooner to speed up error action resolution + this.tickImmediate(); + } + + protected reduceLengthAndFlushBuffer(data: ErrorData): boolean { + // if in appending state + if (this.state === State.PARSING || this.state === State.PARSED) { + const playlistType = data.parent as PlaylistLevelType; + const bufferedInfo = this.getFwdBufferInfo( + this.mediaBuffer, + playlistType + ); + // 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end + // reduce max buf len if current position is buffered + let flushBuffer = true; + if (bufferedInfo && bufferedInfo.len > 0.5) { + flushBuffer = !this.reduceMaxBufferLength(bufferedInfo.len); + } + if (flushBuffer) { + // current position is not buffered, but browser is still complaining about buffer full error + // this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708 + // in that case flush the whole audio buffer to recover + this.warn( + `Buffer full error while media.currentTime is not buffered, flush ${playlistType} buffer` + ); + } + if (data.frag) { + this.nextLoadPosition = data.frag.start; + } + this.resetLoadingState(); + return flushBuffer; + } + return false; } protected resetFragmentErrors(filterType: PlaylistLevelType) { diff --git a/src/controller/error-controller.ts b/src/controller/error-controller.ts index 51c644c7a44..b88adfd109a 100644 --- a/src/controller/error-controller.ts +++ b/src/controller/error-controller.ts @@ -12,6 +12,7 @@ import { isTimeoutError, shouldRetry, } from '../utils/error-helper'; +import { findFragmentByPTS } from './fragment-finders'; import { HdcpLevels } from '../types/level'; import { logger } from '../utils/logger'; import type Hls from '../hls'; @@ -284,7 +285,10 @@ export default class ErrorController implements NetworkComponentAPI { } const level = this.hls.levels[levelIndex]; if (level) { - level.loadError++; + // No penalty for GAP tags so that player can switch back when GAPs are found in other levels + if (data.details !== ErrorDetails.FRAG_GAP) { + level.loadError++; + } const redundantLevels = level.url.length; // Try redundant fail-over until level.loadError reaches redundantLevels if (redundantLevels > 1 && level.loadError < redundantLevels) { @@ -299,6 +303,20 @@ export default class ErrorController implements NetworkComponentAPI { candidate !== hls.loadLevel && levels[candidate].loadError === 0 ) { + // Skip level switch if GAP tag is found in next level + if (data.details === ErrorDetails.FRAG_GAP && data.frag) { + const levelDetails = hls.levels[candidate].details; + if (levelDetails) { + const fragCandidate = findFragmentByPTS( + data.frag, + levelDetails.fragments, + data.frag.start + ); + if (fragCandidate?.gap) { + continue; + } + } + } nextLevel = candidate; break; } @@ -384,7 +402,7 @@ export default class ErrorController implements NetworkComponentAPI { private switchLevel(data: ErrorData, levelIndex: number | undefined) { if (levelIndex !== undefined && data.errorAction) { - this.warn(`${data.details}: switching to level ${levelIndex}`); + this.warn(`switching to level ${levelIndex} after ${data.details}`); this.hls.nextAutoLevel = levelIndex; data.errorAction.resolved = true; // Stream controller is responsible for this but won't switch on false start diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 00b1b4476c7..701b6eedec8 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -302,25 +302,42 @@ export default class StreamController } else if (this.backtrackFragment && bufferInfo.len) { this.backtrackFragment = null; } - // Avoid loop loading by using nextLoadPosition set for backtracking - if ( - frag && - this.fragmentTracker.getState(frag) === FragmentState.OK && - this.nextLoadPosition > targetBufferTime - ) { - // Cleanup the fragment tracker before trying to find the next unbuffered fragment - const type = - this.audioOnly && !this.altAudio - ? ElementaryStreamTypes.AUDIO - : ElementaryStreamTypes.VIDEO; - const mediaBuffer = - (type === ElementaryStreamTypes.VIDEO - ? this.videoBuffer - : this.mediaBuffer) || this.media; - if (mediaBuffer) { - this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN); + if (frag) { + // Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags + const trackerState = this.fragmentTracker.getState(frag); + if ( + (trackerState === FragmentState.OK || + (trackerState === FragmentState.PARTIAL && frag.gap)) && + this.nextLoadPosition > targetBufferTime + ) { + const gapStart = frag.gap; + if (!gapStart) { + // Cleanup the fragment tracker before trying to find the next unbuffered fragment + const type = + this.audioOnly && !this.altAudio + ? ElementaryStreamTypes.AUDIO + : ElementaryStreamTypes.VIDEO; + const mediaBuffer = + (type === ElementaryStreamTypes.VIDEO + ? this.videoBuffer + : this.mediaBuffer) || this.media; + if (mediaBuffer) { + this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN); + } + } + frag = this.getNextFragment(this.nextLoadPosition, levelDetails); + if (gapStart && frag && !frag.gap && bufferInfo.nextStart) { + // Make sure this doesn't make the next buffer timerange exceed forward buffer length after a gap + const nextbufferInfo = this.getFwdBufferInfoAtPos( + this.mediaBuffer ? this.mediaBuffer : this.media, + bufferInfo.nextStart, + PlaylistLevelType.MAIN + ); + if (nextbufferInfo !== null && nextbufferInfo.len > maxBufLen) { + return; + } + } } - frag = this.getNextFragment(this.nextLoadPosition, levelDetails); } if (!frag) { return; @@ -877,32 +894,11 @@ export default class StreamController } break; case ErrorDetails.BUFFER_FULL_ERROR: - // if in appending state - if ( - data.parent === 'main' && - (this.state === State.PARSING || this.state === State.PARSED) - ) { - let flushBuffer = true; - const bufferedInfo = this.getFwdBufferInfo( - this.media, - PlaylistLevelType.MAIN - ); - // 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end - // reduce max buf len if current position is buffered - if (bufferedInfo && bufferedInfo.len > 0.5) { - flushBuffer = !this.reduceMaxBufferLength(bufferedInfo.len); - } - if (flushBuffer) { - // current position is not buffered, but browser is still complaining about buffer full error - // this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708 - // in that case flush the whole buffer to recover - this.warn( - 'buffer full error also media.currentTime is not buffered, flush main' - ); - // flush main buffer - this.immediateLevelSwitch(); - } - this.resetLoadingState(); + if (!data.parent || data.parent !== 'main') { + return; + } + if (this.reduceLengthAndFlushBuffer(data)) { + this.flushMainBuffer(0, Number.POSITIVE_INFINITY); } break; default: