diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index a6690567ab5..993ea572a0f 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -240,6 +240,7 @@ class AudioTrackController extends BasePlaylistController { } protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { + super.loadPlaylist(); const audioTrack = this.tracksInGroup[this.trackId]; if (this.shouldLoadTrack(audioTrack)) { const id = audioTrack.id; diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index e14d4db04f2..00c09e443c3 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -17,6 +17,7 @@ import { ErrorTypes } from '../errors'; export default class BasePlaylistController implements NetworkComponentAPI { protected hls: Hls; protected timer: number = -1; + protected requestScheduled: number = -1; protected canLoad: boolean = false; protected retryCount: number = 0; protected log: (msg: any) => void; @@ -48,6 +49,7 @@ export default class BasePlaylistController implements NetworkComponentAPI { public startLoad(): void { this.canLoad = true; this.retryCount = 0; + this.requestScheduled = -1; this.loadPlaylist(); } @@ -89,7 +91,11 @@ export default class BasePlaylistController implements NetworkComponentAPI { } } - protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {} + protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { + if (this.requestScheduled === -1) { + this.requestScheduled = self.performance.now(); + } + } protected shouldLoadTrack(track: MediaPlaylist): boolean { return ( @@ -108,8 +114,9 @@ export default class BasePlaylistController implements NetworkComponentAPI { const { details, stats } = data; // Set last updated date-time - const elapsed = stats.loading.end - ? Math.max(0, self.performance.now() - stats.loading.end) + const now = self.performance.now(); + const elapsed = stats.loading.first + ? Math.max(0, now - stats.loading.first) : 0; details.advancedDateTime = Date.now() - elapsed; @@ -204,16 +211,53 @@ export default class BasePlaylistController implements NetworkComponentAPI { part ); } - let reloadInterval = computeReloadInterval(details, stats); + const bufferInfo = this.hls.mainForwardBufferInfo; + const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0; + const distanceToLiveEdgeMs = (details.edge - position) * 1000; + const reloadInterval = computeReloadInterval( + details, + distanceToLiveEdgeMs + ); + if (!details.updated) { + this.requestScheduled = -1; + } else if (now > this.requestScheduled + reloadInterval) { + this.requestScheduled = stats.loading.start; + } + if (msn !== undefined && details.canBlockReload) { - reloadInterval -= details.partTarget || 1; + this.requestScheduled = + stats.loading.first + + reloadInterval - + (details.partTarget * 1000 || 1000); + } else { + this.requestScheduled = + (this.requestScheduled === -1 ? now : this.requestScheduled) + + reloadInterval; } + let estimatedTimeUntilUpdate = this.requestScheduled - now; + estimatedTimeUntilUpdate = Math.max(0, estimatedTimeUntilUpdate); this.log( - `reload live playlist ${index} in ${Math.round(reloadInterval)} ms` + `reload live playlist ${index} in ${Math.round( + estimatedTimeUntilUpdate + )} ms` ); + // this.log( + // `live reload ${details.updated ? 'REFRESHED' : 'MISSED'} + // reload in ${estimatedTimeUntilUpdate / 1000} + // round trip ${(stats.loading.end - stats.loading.start) / 1000} + // diff ${ + // (reloadInterval - + // (estimatedTimeUntilUpdate + stats.loading.end - stats.loading.start)) / + // 1000 + // } + // reload interval ${reloadInterval / 1000} + // target duration ${details.targetduration} + // distance to edge ${distanceToLiveEdgeMs / 1000}` + // ); + this.timer = self.setTimeout( () => this.loadPlaylist(deliveryDirectives), - reloadInterval + estimatedTimeUntilUpdate ); } else { this.clearTimer(); @@ -239,6 +283,7 @@ export default class BasePlaylistController implements NetworkComponentAPI { const { config } = this.hls; const retry = this.retryCount < config.levelLoadingMaxRetry; if (retry) { + this.requestScheduled = -1; this.retryCount++; if ( errorEvent.details.indexOf('LoadTimeOut') > -1 && diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index fe0e67192dd..f392696f443 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -518,6 +518,7 @@ export default class LevelController extends BasePlaylistController { } protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) { + super.loadPlaylist(); const level = this.currentLevelIndex; const currentLevel = this._levels[level]; diff --git a/src/controller/level-helper.ts b/src/controller/level-helper.ts index 6b07004f21b..18ce16264a1 100644 --- a/src/controller/level-helper.ts +++ b/src/controller/level-helper.ts @@ -7,7 +7,6 @@ import { logger } from '../utils/logger'; import { Fragment, Part } from '../loader/fragment'; import { LevelDetails } from '../loader/level-details'; import type { Level } from '../types/level'; -import type { LoaderStats } from '../types/loader'; import type { MediaPlaylist } from '../types/media-playlist'; import { DateRange } from '../loader/date-range'; @@ -434,29 +433,33 @@ export function addSliding(details: LevelDetails, start: number) { export function computeReloadInterval( newDetails: LevelDetails, - stats: LoaderStats + distanceToLiveEdgeMs: number = Infinity ): number { - const reloadInterval = 1000 * newDetails.targetduration; - const roundTrip = stats.loading.end - stats.loading.start; - - let estimatedTimeUntilUpdate; - if (!newDetails.updated) { + let reloadInterval = 1000 * newDetails.targetduration; + + if (newDetails.updated) { + // Use last segment duration when shorter than target duration and near live edge + const fragments = newDetails.fragments; + const liveEdgeMaxTargetDurations = 4; + if ( + fragments.length && + reloadInterval * liveEdgeMaxTargetDurations > distanceToLiveEdgeMs + ) { + const lastSegmentDuration = + fragments[fragments.length - 1].duration * 1000; + if (lastSegmentDuration < reloadInterval) { + reloadInterval = lastSegmentDuration; + } + } + } else { // estimate = 'miss half average'; // follow HLS Spec, If the client reloads a Playlist file and finds that it has not // changed then it MUST wait for a period of one-half the target // duration before retrying. - estimatedTimeUntilUpdate = reloadInterval / 2; - } else { - estimatedTimeUntilUpdate = reloadInterval - roundTrip; + reloadInterval /= 2; } - // console.log(`[computeReloadInterval] live reload ${newDetails.updated ? 'REFRESHED' : 'MISSED'}`, - // '\n method', estimate, - // '\n estimated time until update =>', estimatedTimeUntilUpdate, - // '\n average target duration', reloadInterval, - // '\n time round trip', roundTrip); - - return Math.round(estimatedTimeUntilUpdate); + return Math.round(reloadInterval); } export function getFragmentWithSN( diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index c16622f032d..30b273defc5 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -275,6 +275,7 @@ class SubtitleTrackController extends BasePlaylistController { } protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { + super.loadPlaylist(); const currentTrack = this.tracksInGroup[this.trackId]; if (this.shouldLoadTrack(currentTrack)) { const id = currentTrack.id; diff --git a/tests/unit/controller/level-helper.ts b/tests/unit/controller/level-helper.ts index ada8a230015..5c70a739b30 100644 --- a/tests/unit/controller/level-helper.ts +++ b/tests/unit/controller/level-helper.ts @@ -17,7 +17,7 @@ import { AttrList } from '../../../src/utils/attr-list'; chai.use(sinonChai); const expect = chai.expect; -const generatePlaylist = (sequenceNumbers, offset = 0) => { +const generatePlaylist = (sequenceNumbers, offset = 0, duration = 5) => { const playlist = new LevelDetails(''); playlist.startSN = sequenceNumbers[0]; playlist.endSN = sequenceNumbers[sequenceNumbers.length - 1]; @@ -25,7 +25,7 @@ const generatePlaylist = (sequenceNumbers, offset = 0) => { const frag = new Fragment(PlaylistLevelType.MAIN, ''); frag.sn = n; frag.start = i * 5 + offset; - frag.duration = 5; + frag.duration = duration; return frag; }); return playlist; @@ -265,11 +265,21 @@ expect: ${JSON.stringify(merged.fragments[i])}` }); describe('computeReloadInterval', function () { + let sandbox; + beforeEach(function () { + sandbox = sinon.createSandbox(); + sandbox.stub(performance, 'now').returns(0); + }); + + afterEach(function () { + sandbox.restore(); + }); + it('returns the targetduration of the new level if available', function () { - const newPlaylist = generatePlaylist([3, 4]); + const newPlaylist = generatePlaylist([3, 4], 0, 6); newPlaylist.targetduration = 5; newPlaylist.updated = true; - const actual = computeReloadInterval(newPlaylist, new LoadStats()); + const actual = computeReloadInterval(newPlaylist); expect(actual).to.equal(5000); }); @@ -277,38 +287,34 @@ expect: ${JSON.stringify(merged.fragments[i])}` const newPlaylist = generatePlaylist([1, 2]); newPlaylist.updated = false; newPlaylist.targetduration = 5; - const actual = computeReloadInterval(newPlaylist, new LoadStats()); + const actual = computeReloadInterval(newPlaylist); expect(actual).to.equal(2500); }); it('rounds the reload interval', function () { - const newPlaylist = generatePlaylist([3, 4]); + const newPlaylist = generatePlaylist([3, 4], 0, 10); newPlaylist.targetduration = 5.9999; newPlaylist.updated = true; - const actual = computeReloadInterval(newPlaylist, new LoadStats()); + const actual = computeReloadInterval(newPlaylist); expect(actual).to.equal(6000); }); - it('subtracts the request time of the last level load from the reload interval', function () { - const newPlaylist = generatePlaylist([3, 4]); - newPlaylist.targetduration = 5; - newPlaylist.updated = true; - const stats = new LoadStats(); - stats.loading.start = 0; - stats.loading.end = 1000; - const actual = computeReloadInterval(newPlaylist, stats); - expect(actual).to.equal(4000); - }); - it('returns a minimum of half the target duration', function () { const newPlaylist = generatePlaylist([3, 4]); newPlaylist.targetduration = 5; newPlaylist.updated = false; - const stats = new LoadStats(); - stats.loading.start = 0; - stats.loading.end = 1000; - const actual = computeReloadInterval(newPlaylist, stats); + const actual = computeReloadInterval(newPlaylist); expect(actual).to.equal(2500); }); + + it('returns the last fragment duration when distance to live edge is less than or equal to four target durations', function () { + const newPlaylist = generatePlaylist([3, 4], 0, 2); + newPlaylist.targetduration = 5; + newPlaylist.updated = true; + const actual = computeReloadInterval(newPlaylist, 20000); + expect(actual).to.equal(5000); + const actualLow = computeReloadInterval(newPlaylist, 14000); + expect(actualLow).to.equal(2000); + }); }); });