diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index e14d4db04f2..0220525cc4e 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -204,9 +204,15 @@ export default class BasePlaylistController implements NetworkComponentAPI { part ); } - let reloadInterval = computeReloadInterval(details, stats); + const position = this.hls.mainForwardBufferInfo?.start || 0; + const distanceToLiveEdgeMs = (details.edge - position) * 1000; + let reloadInterval = computeReloadInterval( + details, + stats, + distanceToLiveEdgeMs + ); if (msn !== undefined && details.canBlockReload) { - reloadInterval -= details.partTarget || 1; + reloadInterval -= details.partTarget || 1; } this.log( `reload live playlist ${index} in ${Math.round(reloadInterval)} ms` diff --git a/src/controller/level-helper.ts b/src/controller/level-helper.ts index 6b07004f21b..2a7e9f8829d 100644 --- a/src/controller/level-helper.ts +++ b/src/controller/level-helper.ts @@ -434,10 +434,20 @@ export function addSliding(details: LevelDetails, start: number) { export function computeReloadInterval( newDetails: LevelDetails, - stats: LoaderStats + stats: LoaderStats, + distanceToLiveEdgeMs: number = Infinity ): number { - const reloadInterval = 1000 * newDetails.targetduration; - const roundTrip = stats.loading.end - stats.loading.start; + let reloadInterval = 1000 * newDetails.targetduration; + + // Use last segment duration when shorter than target duration and near live edge + const fragments = newDetails.fragments; + if (fragments.length && reloadInterval * 2 > distanceToLiveEdgeMs) { + const lastSegmentDuration = fragments[fragments.length - 1].duration * 1000; + if (lastSegmentDuration < reloadInterval) { + const now = performance.now(); + reloadInterval = stats.loading.start + lastSegmentDuration - now; + } + } let estimatedTimeUntilUpdate; if (!newDetails.updated) { @@ -447,7 +457,14 @@ export function computeReloadInterval( // duration before retrying. estimatedTimeUntilUpdate = reloadInterval / 2; } else { - estimatedTimeUntilUpdate = reloadInterval - roundTrip; + const roundTrip = stats.loading.end - stats.loading.start; + const now = performance.now(); + const estimatedRefreshFromLastRequest = + stats.loading.start + reloadInterval - now; + estimatedTimeUntilUpdate = Math.min( + reloadInterval - roundTrip, + estimatedRefreshFromLastRequest + ); } // console.log(`[computeReloadInterval] live reload ${newDetails.updated ? 'REFRESHED' : 'MISSED'}`, diff --git a/tests/unit/controller/level-helper.ts b/tests/unit/controller/level-helper.ts index ada8a230015..106d0bf5a0b 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,8 +265,18 @@ 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()); @@ -282,7 +292,7 @@ expect: ${JSON.stringify(merged.fragments[i])}` }); 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()); @@ -310,5 +320,19 @@ expect: ${JSON.stringify(merged.fragments[i])}` const actual = computeReloadInterval(newPlaylist, stats); expect(actual).to.equal(2500); }); + + it('returns the last fragment duration when distance to live edge is less than two target durations', function () { + const newPlaylist = generatePlaylist([3, 4], 0, 2); + newPlaylist.targetduration = 5; + newPlaylist.updated = true; + const actual = computeReloadInterval(newPlaylist, new LoadStats(), 11000); + expect(actual).to.equal(5000); + const actualLow = computeReloadInterval( + newPlaylist, + new LoadStats(), + 9000 + ); + expect(actualLow).to.equal(2000); + }); }); });