Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make stream controllers wait for playlist updates before requesting segments from expired playlists #6855

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading