Skip to content

Commit

Permalink
Skip errored fragments with no alternate and fix fragment retry delay…
Browse files Browse the repository at this point in the history
… when there are no alternates

Resolves #6741
Resolves #5647
Resolves #5153
Closes #6171 (replaces)
  • Loading branch information
robwalch committed Nov 18, 2024
1 parent 0ecc0fa commit 7e65049
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 38 deletions.
14 changes: 7 additions & 7 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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!))
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
55 changes: 35 additions & 20 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NetworkErrorAction } from './error-controller';
import { ErrorActionFlags, NetworkErrorAction } from './error-controller';
import {
findFragmentByPDT,
findFragmentByPTS,
Expand All @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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])) {
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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();
}
Expand Down
10 changes: 5 additions & 5 deletions src/controller/stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
Expand Down
14 changes: 9 additions & 5 deletions src/controller/subtitle-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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];
Expand All @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion src/loader/fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*/
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 7e65049

Please sign in to comment.