Skip to content

Commit

Permalink
Honor startPosition from config/startLoad with live streams
Browse files Browse the repository at this point in the history
Do not sync with live edge when inside sliding window and liveMaxLatencyDuration
Fixes #3736
  • Loading branch information
Rob Walch committed Apr 7, 2021
1 parent e8cbd53 commit 9ff3ab8
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 27 deletions.
18 changes: 15 additions & 3 deletions demo/chart/timeline-chart.ts
Expand Up @@ -283,12 +283,18 @@ export class TimelineChart {
updateLevelOrTrack(details: LevelDetails) {
const { targetduration, totalduration, url } = details;
const { datasets } = this.chart.data;
const levelDataSet = arrayFind(
let levelDataSet = arrayFind(
datasets,
(dataset) =>
stripDeliveryDirectives(url) ===
stripDeliveryDirectives(dataset.url || '')
);
if (!levelDataSet) {
levelDataSet = arrayFind(
datasets,
(dataset) => details.fragments[0]?.level === dataset.level
);
}
if (!levelDataSet) {
return;
}
Expand Down Expand Up @@ -381,10 +387,16 @@ export class TimelineChart {
updateFragment(data: FragLoadedData | FragParsedData | FragChangedData) {
const { datasets } = this.chart.data;
const frag: Fragment = data.frag;
const levelDataSet = arrayFind(
let levelDataSet = arrayFind(
datasets,
(dataset) => dataset.url === frag.baseurl
(dataset) => frag.baseurl === dataset.url
);
if (!levelDataSet) {
levelDataSet = arrayFind(
datasets,
(dataset) => frag.level === dataset.level
);
}
if (!levelDataSet) {
return;
}
Expand Down
39 changes: 25 additions & 14 deletions src/controller/base-stream-controller.ts
Expand Up @@ -766,8 +766,15 @@ export default class BaseStreamController
// In order to discover the range, we load the best matching fragment for that level and demux it.
// Do not load using live logic if the starting frag is requested - we want to use getFragmentAtPosition() so that
// we get the fragment matching that start time
if (!levelDetails.PTSKnown && !this.startFragRequested) {
if (
!levelDetails.PTSKnown &&
!this.startFragRequested &&
this.startPosition === -1
) {
frag = this.getInitialLiveFragment(levelDetails, fragments);
this.startPosition = frag
? this.hls.liveSyncPosition || frag.start
: pos;
}
} else if (pos <= start) {
// VoD playlist: if loadPosition before start of playlist, load first fragment
Expand Down Expand Up @@ -981,20 +988,24 @@ export default class BaseStreamController
const currentTime = media.currentTime;
const start = levelDetails.fragments[0].start;
const end = levelDetails.edge;
const withinSlidingWindow =
currentTime >= start - config.maxFragLookUpTolerance &&
currentTime <= end;
// Continue if we can seek forward to sync position or if current time is outside of sliding window
if (
liveSyncPosition !== null &&
media.duration > liveSyncPosition &&
(currentTime < liveSyncPosition ||
currentTime < start - config.maxFragLookUpTolerance ||
currentTime > end)
(currentTime < liveSyncPosition || !withinSlidingWindow)
) {
// Continue if buffer is starving or if current time is behind max latency
const maxLatency =
config.liveMaxLatencyDuration !== undefined
? config.liveMaxLatencyDuration
: config.liveMaxLatencyDurationCount * levelDetails.targetduration;
if (media.readyState < 4 || currentTime < end - maxLatency) {
if (
(!withinSlidingWindow && media.readyState < 4) ||
currentTime < end - maxLatency
) {
if (!this.loadedmetadata) {
this.nextLoadPosition = liveSyncPosition;
}
Expand Down Expand Up @@ -1055,6 +1066,7 @@ export default class BaseStreamController

protected setStartPosition(details: LevelDetails, sliding: number) {
// compute start position if set to -1. use it straight away if value is defined
let startPosition = this.startPosition;
if (this.startPosition === -1 || this.lastCurrentTime === -1) {
// first, check if start time offset has been set in playlist, if yes, use this value
let startTimeOffset = details.startTimeOffset!;
Expand All @@ -1068,18 +1080,17 @@ export default class BaseStreamController
this.log(
`Start time offset found in playlist, adjust startPosition to ${startTimeOffset}`
);
this.startPosition = startTimeOffset;
this.startPosition = startPosition = startTimeOffset;
} else if (details.live) {
// Leave this.startPosition at -1, so that we can use `getInitialLiveFragment` logic when startPosition has
// not been specified via the config or an as an argument to startLoad (#3736).
startPosition = this.hls.liveSyncPosition || sliding;
} else {
if (details.live) {
this.startPosition = this.hls.liveSyncPosition || sliding;
this.log(`Configure startPosition to ${this.startPosition}`);
} else {
this.startPosition = 0;
}
this.startPosition = startPosition = 0;
}
this.lastCurrentTime = this.startPosition;
this.lastCurrentTime = startPosition;
}
this.nextLoadPosition = this.startPosition;
this.nextLoadPosition = startPosition;
}

protected getLoadPosition(): number {
Expand Down
5 changes: 2 additions & 3 deletions src/controller/stream-controller.ts
Expand Up @@ -8,7 +8,6 @@ import { FragmentState } from './fragment-tracker';
import type { Level } from '../types/level';
import { PlaylistLevelType } from '../types/loader';
import { Fragment, ElementaryStreamTypes } from '../loader/fragment';
import FragmentLoader from '../loader/fragment-loader';
import TransmuxerInterface from '../demux/transmuxer-interface';
import type { TransmuxerResult } from '../types/transmuxer';
import { ChunkMetadata } from '../types/transmuxer';
Expand Down Expand Up @@ -921,7 +920,7 @@ export default class StreamController

if (!this.loadedmetadata && buffered.length) {
this.loadedmetadata = true;
this._seekToStartPos();
this.seekToStartPos();
} else {
// Resolve gaps using the main buffer, whose ranges are the intersections of the A/V sourcebuffers
gapController.poll(this.lastCurrentTime);
Expand Down Expand Up @@ -969,7 +968,7 @@ export default class StreamController
* Seeks to the set startPosition if not equal to the mediaElement's current time.
* @private
*/
private _seekToStartPos() {
private seekToStartPos() {
const { media } = this;
const currentTime = media.currentTime;
let startPosition = this.startPosition;
Expand Down
31 changes: 24 additions & 7 deletions tests/unit/controller/stream-controller.ts
Expand Up @@ -114,6 +114,7 @@ describe('StreamController', function () {

beforeEach(function () {
streamController['fragPrevious'] = fragPrevious;
levelDetails.live = false;
levelDetails.startSN = mockFragments[0].sn;
levelDetails.endSN = mockFragments[mockFragments.length - 1].sn;
levelDetails.fragments = mockFragments;
Expand Down Expand Up @@ -375,7 +376,7 @@ describe('StreamController', function () {

it('should seek to start pos when metadata has not yet been loaded', function () {
// @ts-ignore
const seekStub = sandbox.stub(streamController, '_seekToStartPos');
const seekStub = sandbox.stub(streamController, 'seekToStartPos');
streamController['loadedmetadata'] = false;
streamController['checkBuffer']();
expect(seekStub).to.have.been.calledOnce;
Expand All @@ -384,7 +385,7 @@ describe('StreamController', function () {

it('should not seek to start pos when metadata has been loaded', function () {
// @ts-ignore
const seekStub = sandbox.stub(streamController, '_seekToStartPos');
const seekStub = sandbox.stub(streamController, 'seekToStartPos');
streamController['loadedmetadata'] = true;
streamController['checkBuffer']();
expect(seekStub).to.have.not.been.called;
Expand All @@ -393,24 +394,24 @@ describe('StreamController', function () {

it('should not seek to start pos when nothing has been buffered', function () {
// @ts-ignore
const seekStub = sandbox.stub(streamController, '_seekToStartPos');
const seekStub = sandbox.stub(streamController, 'seekToStartPos');
streamController['media'].buffered.length = 0;
streamController['checkBuffer']();
expect(seekStub).to.have.not.been.called;
expect(streamController['loadedmetadata']).to.be.false;
});

describe('_seekToStartPos', function () {
describe('seekToStartPos', function () {
it('should seek to startPosition when startPosition is not buffered & the media is not seeking', function () {
streamController['startPosition'] = 5;
streamController['_seekToStartPos']();
streamController['seekToStartPos']();
expect(streamController['media'].currentTime).to.equal(5);
});

it('should not seek to startPosition when it is buffered', function () {
streamController['startPosition'] = 5;
streamController['media'].currentTime = 5;
streamController['_seekToStartPos']();
streamController['seekToStartPos']();
expect(streamController['media'].currentTime).to.equal(5);
});
});
Expand All @@ -434,7 +435,7 @@ describe('StreamController', function () {
expect(streamController['lastCurrentTime']).to.equal(5);
});

it('should set startPosition to lastCurrentTime if unset', function () {
it('should set startPosition to lastCurrentTime if unset and lastCurrentTime > 0', function () {
streamController['lastCurrentTime'] = 5;
streamController.startLoad(-1);
assertStreamControllerStarted(streamController);
Expand All @@ -443,6 +444,22 @@ describe('StreamController', function () {
expect(streamController['lastCurrentTime']).to.equal(5);
});

it('should set startPosition when passed as an argument', function () {
streamController.startLoad(123);
assertStreamControllerStarted(streamController);
expect(streamController['nextLoadPosition']).to.equal(123);
expect(streamController['startPosition']).to.equal(123);
expect(streamController['lastCurrentTime']).to.equal(123);
});

it('should set startPosition to -1 when passed as an argument', function () {
streamController.startLoad(-1);
assertStreamControllerStarted(streamController);
expect(streamController['nextLoadPosition']).to.equal(-1);
expect(streamController['startPosition']).to.equal(-1);
expect(streamController['lastCurrentTime']).to.equal(-1);
});

it('sets up for a bandwidth test if starting at auto', function () {
streamController['startFragRequested'] = false;
hls.startLevel = -1;
Expand Down

0 comments on commit 9ff3ab8

Please sign in to comment.