Skip to content

Commit

Permalink
Schedule live reload based on start of last update request (#4961)
Browse files Browse the repository at this point in the history
* 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

* Eliminate JavaScript execution and setTimeout latency from playlist reload interval

* Comment out debug logs

* Reload live stream using last segment length when distance to playlist end is less than or equal to four target durations
(Reload is scheduled on update. With a default live sync of 3 target durations, without interruption, a distance of 3x-4x is most likely.)
  • Loading branch information
robwalch committed Oct 24, 2022
1 parent d478272 commit e38d5b7
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 46 deletions.
1 change: 1 addition & 0 deletions src/controller/audio-track-controller.ts
Expand Up @@ -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;
Expand Down
59 changes: 52 additions & 7 deletions src/controller/base-playlist-controller.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -48,6 +49,7 @@ export default class BasePlaylistController implements NetworkComponentAPI {
public startLoad(): void {
this.canLoad = true;
this.retryCount = 0;
this.requestScheduled = -1;
this.loadPlaylist();
}

Expand Down Expand Up @@ -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 (
Expand All @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -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 &&
Expand Down
1 change: 1 addition & 0 deletions src/controller/level-controller.ts
Expand Up @@ -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];

Expand Down
37 changes: 20 additions & 17 deletions src/controller/level-helper.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/controller/subtitle-track-controller.ts
Expand Up @@ -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;
Expand Down
50 changes: 28 additions & 22 deletions tests/unit/controller/level-helper.ts
Expand Up @@ -17,15 +17,15 @@ 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];
playlist.fragments = sequenceNumbers.map((n, i) => {
const frag = new Fragment(PlaylistLevelType.MAIN, '');
frag.sn = n;
frag.start = i * 5 + offset;
frag.duration = 5;
frag.duration = duration;
return frag;
});
return playlist;
Expand Down Expand Up @@ -265,50 +265,56 @@ 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);
});

it('halves the reload interval if the playlist contains the same segments', function () {
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);
});
});
});

0 comments on commit e38d5b7

Please sign in to comment.