Skip to content

Commit

Permalink
Eliminate JavaScript execution and setTimeout latency from playlist r…
Browse files Browse the repository at this point in the history
…eload interval
  • Loading branch information
robwalch committed Oct 19, 2022
1 parent a9d1fb3 commit 4859ed1
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 63 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
57 changes: 48 additions & 9 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,22 +211,53 @@ export default class BasePlaylistController implements NetworkComponentAPI {
part
);
}
const position = this.hls.mainForwardBufferInfo?.start || 0;
const bufferInfo = this.hls.mainForwardBufferInfo;
const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0;
const distanceToLiveEdgeMs = (details.edge - position) * 1000;
let reloadInterval = computeReloadInterval(
const reloadInterval = computeReloadInterval(
details,
stats,
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 @@ -245,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
41 changes: 12 additions & 29 deletions src/controller/level-helper.ts
Expand Up @@ -434,46 +434,29 @@ export function addSliding(details: LevelDetails, start: number) {

export function computeReloadInterval(
newDetails: LevelDetails,
stats: LoaderStats,
distanceToLiveEdgeMs: number = Infinity
): number {
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;
if (newDetails.updated) {
// Use last segment duration when shorter than target duration and near live edge
const fragments = newDetails.fragments;
if (fragments.length && reloadInterval * 3 > distanceToLiveEdgeMs) {
const lastSegmentDuration =
fragments[fragments.length - 1].duration * 1000;
if (lastSegmentDuration < reloadInterval) {
reloadInterval = lastSegmentDuration;
}
}
}

let estimatedTimeUntilUpdate;
if (!newDetails.updated) {
} 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 {
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
);
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
32 changes: 7 additions & 25 deletions tests/unit/controller/level-helper.ts
Expand Up @@ -279,59 +279,41 @@ expect: ${JSON.stringify(merged.fragments[i])}`
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], 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 two target durations', function () {
it('returns the last fragment duration when distance to live edge is less than three target durations', function () {
const newPlaylist = generatePlaylist([3, 4], 0, 2);
newPlaylist.targetduration = 5;
newPlaylist.updated = true;
const actual = computeReloadInterval(newPlaylist, new LoadStats(), 11000);
const actual = computeReloadInterval(newPlaylist, 15000);
expect(actual).to.equal(5000);
const actualLow = computeReloadInterval(
newPlaylist,
new LoadStats(),
9000
);
const actualLow = computeReloadInterval(newPlaylist, 14000);
expect(actualLow).to.equal(2000);
});
});
Expand Down

0 comments on commit 4859ed1

Please sign in to comment.