From a9d1fb3e2da0361d246f00c738d3132e9d12210d Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Tue, 11 Oct 2022 17:01:54 -0700 Subject: [PATCH] Schedule live reload time base on start of last update request Use last fragment duration rather than target duration when playhead is less than two target durations from the live edge --- src/controller/base-playlist-controller.ts | 8 +++++- src/controller/level-helper.ts | 25 ++++++++++++++--- tests/unit/controller/level-helper.ts | 32 +++++++++++++++++++--- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index e14d4db04f2..59c77a4df06 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -204,7 +204,13 @@ 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; } 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); + }); }); });