From 7e6504967e7ad31d8e3df528f82a99f7a3bcf5ce Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Mon, 18 Nov 2024 14:26:35 -0800 Subject: [PATCH] Skip errored fragments with no alternate and fix fragment retry delay when there are no alternates Resolves #6741 Resolves #5647 Resolves #5153 Closes #6171 (replaces) --- src/controller/audio-stream-controller.ts | 14 ++--- src/controller/base-stream-controller.ts | 55 +++++++++++++------- src/controller/stream-controller.ts | 10 ++-- src/controller/subtitle-stream-controller.ts | 14 +++-- src/loader/fragment.ts | 6 ++- 5 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/controller/audio-stream-controller.ts b/src/controller/audio-stream-controller.ts index 32a579a3bc0..b19d4a89d88 100644 --- a/src/controller/audio-stream-controller.ts +++ b/src/controller/audio-stream-controller.ts @@ -5,7 +5,7 @@ import ChunkCache from '../demux/chunk-cache'; import TransmuxerInterface from '../demux/transmuxer-interface'; import { ErrorDetails } from '../errors'; import { Events } from '../events'; -import { ElementaryStreamTypes } from '../loader/fragment'; +import { ElementaryStreamTypes, isMediaFragment } from '../loader/fragment'; import { Level } from '../types/level'; import { PlaylistContextType, PlaylistLevelType } from '../types/loader'; import { ChunkMetadata } from '../types/transmuxer'; @@ -409,8 +409,8 @@ class AudioStreamController if ( this.startFragRequested && mainFragLoading && - mainFragLoading.sn !== 'initSegment' && - frag.sn !== 'initSegment' && + isMediaFragment(mainFragLoading) && + isMediaFragment(frag) && !frag.endList && (!trackDetails.live || (!this.loadingParts && targetBufferTime < this.hls.liveSyncPosition!)) @@ -694,7 +694,7 @@ class AudioStreamController private onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) { if ( data.frag.type === PlaylistLevelType.MAIN && - data.frag.sn !== 'initSegment' + isMediaFragment(data.frag) ) { this.mainFragLoading = data; if (this.state === State.IDLE) { @@ -722,8 +722,8 @@ class AudioStreamController ); return; } - if (frag.sn !== 'initSegment') { - this.fragPrevious = frag as MediaFragment; + if (isMediaFragment(frag)) { + this.fragPrevious = frag; const track = this.switchingTrack; if (track) { this.bufferedTrack = track; @@ -962,7 +962,7 @@ class AudioStreamController fragState === FragmentState.NOT_LOADED || fragState === FragmentState.PARTIAL ) { - if (frag.sn === 'initSegment') { + if (!isMediaFragment(frag)) { this._loadInitSegment(frag, track); } else if (track.details?.live && !this.initPTS[frag.cc]) { this.log( diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index c8bc0938a6a..345fb252f4c 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -1,4 +1,4 @@ -import { NetworkErrorAction } from './error-controller'; +import { ErrorActionFlags, NetworkErrorAction } from './error-controller'; import { findFragmentByPDT, findFragmentByPTS, @@ -8,6 +8,12 @@ import { FragmentState } from './fragment-tracker'; import Decrypter from '../crypt/decrypter'; import { ErrorDetails, ErrorTypes } from '../errors'; import { Events } from '../events'; +import { + type Fragment, + isMediaFragment, + type MediaFragment, + type Part, +} from '../loader/fragment'; import FragmentLoader from '../loader/fragment-loader'; import TaskLoop from '../task-loop'; import { PlaylistLevelType } from '../types/loader'; @@ -31,7 +37,6 @@ import type { FragmentTracker } from './fragment-tracker'; import type { HlsConfig } from '../config'; import type TransmuxerInterface from '../demux/transmuxer-interface'; import type Hls from '../hls'; -import type { Fragment, MediaFragment, Part } from '../loader/fragment'; import type { FragmentLoadProgressCallback, LoadError, @@ -714,7 +719,7 @@ export default class BaseStreamController : '(detached)' })`, ); - if (frag.sn !== 'initSegment') { + if (isMediaFragment(frag)) { if (frag.type !== PlaylistLevelType.SUBTITLE) { const el = frag.elementaryStreams; if (!Object.keys(el).some((type) => !!el[type])) { @@ -803,7 +808,7 @@ export default class BaseStreamController const fragPrevious = this.fragPrevious; if ( - frag.sn !== 'initSegment' && + isMediaFragment(frag) && (!fragPrevious || frag.sn !== fragPrevious.sn) ) { const shouldLoadParts = this.shouldLoadParts(level.details, frag.end); @@ -817,7 +822,7 @@ export default class BaseStreamController } } targetBufferTime = Math.max(frag.start, targetBufferTime || 0); - if (this.loadingParts && frag.sn !== 'initSegment') { + if (this.loadingParts && isMediaFragment(frag)) { const partList = details.partList; if (partList && progressCallback) { if (targetBufferTime > frag.end && details.fragmentHint) { @@ -885,7 +890,7 @@ export default class BaseStreamController } } - if (frag.sn !== 'initSegment' && this.loadingParts) { + if (isMediaFragment(frag) && this.loadingParts) { this.log( `LL-Part loading OFF after next part miss @${targetBufferTime.toFixed( 2, @@ -1661,7 +1666,7 @@ export default class BaseStreamController } private handleFragLoadAborted(frag: Fragment, part: Part | undefined) { - if (this.transmuxer && frag.sn !== 'initSegment' && frag.stats.aborted) { + if (this.transmuxer && isMediaFragment(frag) && frag.stats.aborted) { this.warn( `Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of level ${ frag.level @@ -1708,12 +1713,18 @@ export default class BaseStreamController } // keep retrying until the limit will be reached const errorAction = data.errorAction; - const { action, retryCount = 0, retryConfig } = errorAction || {}; - if ( - errorAction && - action === NetworkErrorAction.RetryRequest && - retryConfig - ) { + const { action, flags, retryCount = 0, retryConfig } = errorAction || {}; + const couldRetry = !!errorAction && !!retryConfig; + const retry = couldRetry && action === NetworkErrorAction.RetryRequest; + const noAlternate = + couldRetry && + !errorAction.resolved && + flags === ErrorActionFlags.MoveAllAlternatesMatchingHost; + if (!retry && noAlternate && isMediaFragment(frag) && !frag.endList) { + this.resetFragmentErrors(filterType); + this.treatAsGap(frag); + errorAction.resolved = true; + } else if ((retry || noAlternate) && retryCount < retryConfig.maxNumRetry) { this.resetStartWhenNotLoaded(this.levelLastLoaded); const delay = getRetryDelay(retryConfig, retryCount); this.warn( @@ -1742,9 +1753,7 @@ export default class BaseStreamController ); return; } - } else if ( - errorAction?.action === NetworkErrorAction.SendAlternateToPenaltyBox - ) { + } else if (action === NetworkErrorAction.SendAlternateToPenaltyBox) { this.state = State.WAITING_LEVEL; } else { this.state = State.ERROR; @@ -1925,10 +1934,7 @@ export default class BaseStreamController ); if (level.fragmentError === 0) { // Mark and track the odd empty segment as a gap to avoid reloading - level.fragmentError++; - frag.gap = true; - this.fragmentTracker.removeFragment(frag); - this.fragmentTracker.fragBuffered(frag, true); + this.treatAsGap(frag, level); } this.warn(error.message); this.hls.trigger(Events.ERROR, { @@ -1970,6 +1976,15 @@ export default class BaseStreamController )}]${part && frag.type === 'main' ? 'INDEPENDENT=' + (part.independent ? 'YES' : 'NO') : ''}`; } + private treatAsGap(frag: MediaFragment, level?: Level) { + if (level) { + level.fragmentError++; + } + frag.gap = true; + this.fragmentTracker.removeFragment(frag); + this.fragmentTracker.fragBuffered(frag, true); + } + protected resetTransmuxer() { this.transmuxer?.reset(); } diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 389700b09f5..fea4e261bc5 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -6,7 +6,7 @@ import TransmuxerInterface from '../demux/transmuxer-interface'; import { ErrorDetails } from '../errors'; import { Events } from '../events'; import { changeTypeSupported } from '../is-supported'; -import { ElementaryStreamTypes } from '../loader/fragment'; +import { ElementaryStreamTypes, isMediaFragment } from '../loader/fragment'; import { PlaylistContextType, PlaylistLevelType } from '../types/loader'; import { ChunkMetadata } from '../types/transmuxer'; import { BufferHelper } from '../utils/buffer-helper'; @@ -319,7 +319,7 @@ export default class StreamController this.couldBacktrack && !this.fragPrevious && frag && - frag.sn !== 'initSegment' && + isMediaFragment(frag) && this.fragmentTracker.getState(frag) !== FragmentState.OK ) { const backtrackSn = (this.backtrackFragment ?? frag).sn as number; @@ -378,7 +378,7 @@ export default class StreamController fragState === FragmentState.NOT_LOADED || fragState === FragmentState.PARTIAL ) { - if (frag.sn === 'initSegment') { + if (!isMediaFragment(frag)) { this._loadInitSegment(frag, level); } else if (this.bitrateTest) { this.log( @@ -963,8 +963,8 @@ export default class StreamController this.fragLastKbps = Math.round( (8 * stats.total) / (stats.buffering.end - stats.loading.first), ); - if (frag.sn !== 'initSegment') { - this.fragPrevious = frag as MediaFragment; + if (isMediaFragment(frag)) { + this.fragPrevious = frag; } this.fragBufferedComplete(frag, part); } diff --git a/src/controller/subtitle-stream-controller.ts b/src/controller/subtitle-stream-controller.ts index c0f97e63de3..43a7b7d3373 100644 --- a/src/controller/subtitle-stream-controller.ts +++ b/src/controller/subtitle-stream-controller.ts @@ -3,6 +3,11 @@ import { findFragmentByPTS } from './fragment-finders'; import { FragmentState } from './fragment-tracker'; import { ErrorDetails, ErrorTypes } from '../errors'; import { Events } from '../events'; +import { + type Fragment, + isMediaFragment, + type MediaFragment, +} from '../loader/fragment'; import { Level } from '../types/level'; import { PlaylistLevelType } from '../types/loader'; import { BufferHelper } from '../utils/buffer-helper'; @@ -15,7 +20,6 @@ import { addSliding } from '../utils/level-helper'; import { subtitleOptionsIdentical } from '../utils/media-option-attributes'; import type Hls from '../hls'; import type { FragmentTracker } from './fragment-tracker'; -import type { Fragment, MediaFragment } from '../loader/fragment'; import type KeyLoader from '../loader/key-loader'; import type { LevelDetails } from '../loader/level-details'; import type { NetworkComponentAPI } from '../types/component-api'; @@ -126,8 +130,8 @@ export class SubtitleStreamController data: SubtitleFragProcessed, ) { const { frag, success } = data; - if (frag.sn !== 'initSegment') { - this.fragPrevious = frag as MediaFragment; + if (isMediaFragment(frag)) { + this.fragPrevious = frag; } this.state = State.IDLE; if (!success) { @@ -466,7 +470,7 @@ export class SubtitleStreamController return; } foundFrag = this.mapToInitFragWhenRequired(foundFrag) as Fragment; - if (foundFrag.sn !== 'initSegment') { + if (isMediaFragment(foundFrag)) { // Load earlier fragment in same discontinuity to make up for misaligned playlists and cues that extend beyond end of segment const curSNIdx = foundFrag.sn - trackDetails.startSN; const prevFrag = fragments[curSNIdx - 1]; @@ -492,7 +496,7 @@ export class SubtitleStreamController level: Level, targetBufferTime: number, ) { - if (frag.sn === 'initSegment') { + if (!isMediaFragment(frag)) { this._loadInitSegment(frag, level); } else { super.loadFragment(frag, level, targetBufferTime); diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 2d7f8e8cbde..92fa6cc8748 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -152,6 +152,10 @@ export type MediaFragmentRef = { programDateTime: number | null; }; +export function isMediaFragment(frag: Fragment): frag is MediaFragment { + return frag.sn !== 'initSegment'; +} + /** * Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}. */ @@ -323,7 +327,7 @@ export class Fragment extends BaseSegment { } get ref(): MediaFragmentRef | null { - if (this.sn === 'initSegment') { + if (!isMediaFragment(this)) { return null; } if (!this._ref) {