diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 99f9e735878..d17b2d9a9c7 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -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) @@ -3132,6 +3134,8 @@ export class LevelDetails { // (undocumented) endSN: number; // (undocumented) + get expired(): boolean; + // (undocumented) get fragmentEnd(): number; // (undocumented) fragmentHint?: MediaFragment; diff --git a/src/controller/audio-stream-controller.ts b/src/controller/audio-stream-controller.ts index b19d4a89d88..e659eb9c000 100644 --- a/src/controller/audio-stream-controller.ts +++ b/src/controller/audio-stream-controller.ts @@ -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'; @@ -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; } @@ -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; } diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index 8475b111be8..d5767576ef6 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -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, { diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 345fb252f4c..56097724200 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -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, @@ -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; } @@ -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; @@ -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`, ); @@ -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); @@ -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 @@ -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; diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index 55b2695a001..2d7772eb1e8 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -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 @@ -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); diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 2dc320c0317..4140bcd6432 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -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, @@ -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; @@ -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; } @@ -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; diff --git a/src/controller/subtitle-stream-controller.ts b/src/controller/subtitle-stream-controller.ts index 43a7b7d3373..2ec2e4f49a7 100644 --- a/src/controller/subtitle-stream-controller.ts +++ b/src/controller/subtitle-stream-controller.ts @@ -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'; @@ -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( diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index d0e402f5936..f120d7a0d7e 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -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, diff --git a/src/loader/level-details.ts b/src/loader/level-details.ts index 6f396ba4c78..34c3e554fce 100644 --- a/src/loader/level-details.ts +++ b/src/loader/level-details.ts @@ -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; + } } diff --git a/src/loader/playlist-loader.ts b/src/loader/playlist-loader.ts index e62a5c38329..7f101079151 100644 --- a/src/loader/playlist-loader.ts +++ b/src/loader/playlist-loader.ts @@ -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();