diff --git a/demo/chart/timeline-chart.ts b/demo/chart/timeline-chart.ts index abb9dbb05d2..23c288ff875 100644 --- a/demo/chart/timeline-chart.ts +++ b/demo/chart/timeline-chart.ts @@ -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; } @@ -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; } diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 2bd4ec34d69..fea77cd1acd 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -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 @@ -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; } @@ -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!; @@ -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 { diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 30d053664f0..97e5257cbba 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -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'; @@ -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); @@ -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; diff --git a/tests/unit/controller/stream-controller.ts b/tests/unit/controller/stream-controller.ts index a879f3400eb..f954bd62a41 100644 --- a/tests/unit/controller/stream-controller.ts +++ b/tests/unit/controller/stream-controller.ts @@ -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; @@ -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; @@ -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; @@ -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); }); }); @@ -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); @@ -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;