Skip to content

Commit

Permalink
Avoid requesting expired playlist segments when resuming live playback (
Browse files Browse the repository at this point in the history
#6855)

* Avoid requesting expired playlist segments when resuming live playback
Resolves #6854

* Enhance playlist loading logs and ignore non-blocking playlist requests (without directives) when one with is active

* Reduce the expected availability to playlist duration plus target duration
  • Loading branch information
robwalch authored Nov 21, 2024
1 parent 793bab2 commit 42ef766
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 23 deletions.
4 changes: 4 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,8 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
protected unregisterListeners(): void;
// (undocumented)
protected waitForCdnTuneIn(details: LevelDetails): boolean | 0;
// (undocumented)
protected waitForLive(levelInfo: Level): boolean | undefined;
}

// Warning: (ae-missing-release-tag) "BaseTrack" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -3132,6 +3134,8 @@ export class LevelDetails {
// (undocumented)
endSN: number;
// (undocumented)
get expired(): boolean;
// (undocumented)
get fragmentEnd(): number;
// (undocumented)
fragmentHint?: MediaFragment;
Expand Down
10 changes: 6 additions & 4 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
import { ChunkMetadata } from '../types/transmuxer';
import { alignMediaPlaylistByPDT } from '../utils/discontinuities';
import { mediaAttributesIdentical } from '../utils/media-option-attributes';
import type Hls from '../hls';
import type { FragmentTracker } from './fragment-tracker';
import type Hls from '../hls';
import type { Fragment, MediaFragment, Part } from '../loader/fragment';
import type KeyLoader from '../loader/key-loader';
import type { LevelDetails } from '../loader/level-details';
Expand Down Expand Up @@ -207,8 +207,9 @@ class AudioStreamController
break;
case State.WAITING_TRACK: {
const { levels, trackId } = this;
const details = levels?.[trackId]?.details;
if (details) {
const currenTrack = levels?.[trackId];
const details = currenTrack?.details;
if (details && !this.waitForLive(currenTrack)) {
if (this.waitForCdnTuneIn(details)) {
break;
}
Expand Down Expand Up @@ -318,10 +319,11 @@ class AudioStreamController
const trackDetails = levelInfo.details;
if (
!trackDetails ||
(trackDetails.live && this.levelLastLoaded !== levelInfo) ||
this.waitForLive(levelInfo) ||
this.waitForCdnTuneIn(trackDetails)
) {
this.state = State.WAITING_TRACK;
this.startFragRequested = false;
return;
}

Expand Down
11 changes: 10 additions & 1 deletion src/controller/audio-track-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,17 @@ class AudioTrackController extends BasePlaylistController {
}
}
// track not retrieved yet, or live playlist we need to (re)load it
const details = audioTrack.details;
const age = details?.age;
this.log(
`loading audio-track playlist ${id} "${audioTrack.name}" lang:${audioTrack.lang} group:${groupId}`,
`Loading audio-track ${id} "${audioTrack.name}" lang:${audioTrack.lang} group:${groupId}${
hlsUrlParameters?.msn !== undefined
? ' at sn ' +
hlsUrlParameters.msn +
' part ' +
hlsUrlParameters.part
: ''
}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`,
);
this.clearTimer();
this.hls.trigger(Events.AUDIO_TRACK_LOADING, {
Expand Down
27 changes: 21 additions & 6 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,15 @@ export default class BaseStreamController
}
}

protected waitForLive(levelInfo: Level) {
const details = levelInfo.details;
return (
details?.live &&
details.type !== 'EVENT' &&
(this.levelLastLoaded !== levelInfo || details.expired)
);
}

protected flushMainBuffer(
startOffset: number,
endOffset: number,
Expand Down Expand Up @@ -1077,7 +1086,7 @@ export default class BaseStreamController
const { level: levelIndex, sn, part: partIndex } = chunkMeta;
if (!levels?.[levelIndex]) {
this.warn(
`Levels object was unset while buffering fragment ${sn} of level ${levelIndex}. The current chunk will not be buffered.`,
`Levels object was unset while buffering fragment ${sn} of ${this.playlistLabel()} ${levelIndex}. The current chunk will not be buffered.`,
);
return null;
}
Expand Down Expand Up @@ -1643,6 +1652,7 @@ export default class BaseStreamController
// Leave this.startPosition at -1, so that we can use `getInitialLiveFragment` logic when startPosition has
// not been specified via the config or an as an argument to startLoad (#3736).
startPosition = this.hls.liveSyncPosition || sliding;
this.startPosition = -1;
} else {
this.log(`setting startPosition to 0 by default`);
this.startPosition = startPosition = 0;
Expand All @@ -1666,9 +1676,14 @@ export default class BaseStreamController
}

private handleFragLoadAborted(frag: Fragment, part: Part | undefined) {
if (this.transmuxer && isMediaFragment(frag) && frag.stats.aborted) {
if (
this.transmuxer &&
frag.type === this.playlistType &&
isMediaFragment(frag) &&
frag.stats.aborted
) {
this.warn(
`Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of level ${
`Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of ${this.playlistLabel()} ${
frag.level
} was aborted`,
);
Expand Down Expand Up @@ -1861,7 +1876,7 @@ export default class BaseStreamController

protected resetWhenMissingContext(chunkMeta: ChunkMetadata) {
this.warn(
`The loading context changed while buffering fragment ${chunkMeta.sn} of level ${chunkMeta.level}. This chunk will not be buffered.`,
`The loading context changed while buffering fragment ${chunkMeta.sn} of ${this.playlistLabel()} ${chunkMeta.level}. This chunk will not be buffered.`,
);
this.removeUnbufferedFrags();
this.resetStartWhenNotLoaded(this.levelLastLoaded);
Expand Down Expand Up @@ -1930,7 +1945,7 @@ export default class BaseStreamController
);
if (!parsed && this.transmuxer?.error === null) {
const error = new Error(
`Found no media in fragment ${frag.sn} of level ${frag.level} resetting transmuxer to fallback to playlist timing`,
`Found no media in fragment ${frag.sn} of ${this.playlistLabel()} ${frag.level} resetting transmuxer to fallback to playlist timing`,
);
if (level.fragmentError === 0) {
// Mark and track the odd empty segment as a gap to avoid reloading
Expand All @@ -1943,7 +1958,7 @@ export default class BaseStreamController
fatal: false,
error,
frag,
reason: `Found no media in msn ${frag.sn} of level "${level.url}"`,
reason: `Found no media in msn ${frag.sn} of ${this.playlistLabel()} "${level.url}"`,
});
if (!this.hls) {
return;
Expand Down
4 changes: 3 additions & 1 deletion src/controller/level-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,8 @@ export default class LevelController extends BasePlaylistController {
}

const pathwayId = currentLevel.attrs['PATHWAY-ID'];
const details = currentLevel.details;
const age = details?.age;
this.log(
`Loading level index ${currentLevelIndex}${
hlsUrlParameters?.msn !== undefined
Expand All @@ -656,7 +658,7 @@ export default class LevelController extends BasePlaylistController {
' part ' +
hlsUrlParameters.part
: ''
} with${pathwayId ? ' Pathway ' + pathwayId : ''} ${url}`,
}${pathwayId ? ' Pathway ' + pathwayId : ''}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`,
);

// console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId);
Expand Down
16 changes: 12 additions & 4 deletions src/controller/stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,11 @@ export default class StreamController
this._hasEnoughToStart = false;
}
// if startPosition undefined but lastCurrentTime set, set startPosition to last currentTime
if (lastCurrentTime > 0 && startPosition === -1) {
if (
lastCurrentTime > 0 &&
startPosition === -1 &&
!skipSeekToStartPosition
) {
this.log(
`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(
3,
Expand Down Expand Up @@ -186,7 +190,9 @@ export default class StreamController
const details = currentLevel?.details;
if (
details &&
(!details.live || this.levelLastLoaded === currentLevel)
(!details.live ||
(this.levelLastLoaded === currentLevel &&
!this.waitForLive(currentLevel)))
) {
if (this.waitForCdnTuneIn(details)) {
break;
Expand Down Expand Up @@ -287,10 +293,11 @@ export default class StreamController
if (
!levelDetails ||
this.state === State.WAITING_LEVEL ||
(levelDetails.live && this.levelLastLoaded !== levelInfo)
this.waitForLive(levelInfo)
) {
this.level = level;
this.state = State.WAITING_LEVEL;
this.startFragRequested = false;
return;
}

Expand Down Expand Up @@ -649,7 +656,8 @@ export default class StreamController
const level = data.levelInfo;
if (
!level.details ||
(level.details.live && this.levelLastLoaded !== level) ||
(level.details.live &&
(this.levelLastLoaded !== level || level.details.expired)) ||
this.waitForCdnTuneIn(level.details)
) {
this.state = State.WAITING_LEVEL;
Expand Down
5 changes: 4 additions & 1 deletion src/controller/subtitle-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {
} from '../utils/encryption-methods-util';
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 Hls from '../hls';
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 @@ -425,6 +425,9 @@ export class SubtitleStreamController
if (!track || !levels.length || !track.details) {
return;
}
if (this.waitForLive(track)) {
return;
}
const { config } = this;
const currentTime = this.getLoadPosition();
const bufferedInfo = BufferHelper.bufferedInfo(
Expand Down
13 changes: 12 additions & 1 deletion src/controller/subtitle-track-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,18 @@ class SubtitleTrackController extends BasePlaylistController {
);
}
}
this.log(`Loading subtitle playlist for id ${id}`);
const details = currentTrack.details;
const age = details?.age;
this.log(
`Loading subtitle ${id} "${currentTrack.name}" lang:${currentTrack.lang} group:${groupId}${
hlsUrlParameters?.msn !== undefined
? ' at sn ' +
hlsUrlParameters.msn +
' part ' +
hlsUrlParameters.part
: ''
}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`,
);
this.hls.trigger(Events.SUBTITLE_TRACK_LOADING, {
url,
id,
Expand Down
12 changes: 12 additions & 0 deletions src/loader/level-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,16 @@ export class LevelDetails {
}
return this.endSN;
}

get expired(): boolean {
if (this.live && this.age) {
const playlistWindowDuration = this.partEnd - this.fragmentStart;
return (
this.age >
Math.max(playlistWindowDuration, this.totalduration) +
this.levelTargetDuration
);
}
return false;
}
}
18 changes: 13 additions & 5 deletions src/loader/playlist-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,17 +240,25 @@ class PlaylistLoader implements NetworkComponentAPI {
// Check if a loader for this context already exists
let loader = this.getInternalLoader(context);
if (loader) {
const logger = this.hls.logger;
const loaderContext = loader.context as PlaylistLoaderContext;
if (
loaderContext &&
loaderContext.url === context.url &&
loaderContext.levelOrTrack === context.levelOrTrack
loaderContext.levelOrTrack === context.levelOrTrack &&
(loaderContext.url === context.url ||
(loaderContext.deliveryDirectives && !context.deliveryDirectives))
) {
// same URL can't overlap
this.hls.logger.trace('[playlist-loader]: playlist request ongoing');
// same URL can't overlap, or wait for blocking request
if (loaderContext.url === context.url) {
logger.log(`[playlist-loader]: playlist request ongoing`);
} else {
logger.log(
`[playlist-loader]: ignore ${context.url} in favor of ${loaderContext.url}`,
);
}
return;
}
this.hls.logger.log(
logger.log(
`[playlist-loader]: aborting previous loader for type: ${context.type}`,
);
loader.abort();
Expand Down

0 comments on commit 42ef766

Please sign in to comment.