Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schedule live reload based on start of last update request #4961

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
32 changes: 16 additions & 16 deletions src/controller/level-helper.ts
Expand Up @@ -434,29 +434,29 @@ 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;
if (fragments.length && reloadInterval * 3 > 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 three target durations', function () {
const newPlaylist = generatePlaylist([3, 4], 0, 2);
newPlaylist.targetduration = 5;
newPlaylist.updated = true;
const actual = computeReloadInterval(newPlaylist, 15000);
expect(actual).to.equal(5000);
const actualLow = computeReloadInterval(newPlaylist, 14000);
expect(actualLow).to.equal(2000);
});
});
});