diff --git a/doc/API.md b/doc/API.md index 17f36f98215..bc5ed7acbcd 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1280,8 +1280,6 @@ Full list of errors is described below: - `Hls.ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR` - raised when manifest only contains quality level with codecs incompatible with MediaSource Engine. - data: { type : `MEDIA_ERROR`, details : `Hls.ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR`, fatal : `true`, url : manifest URL } - - `Hls.ErrorDetails.FRAG_LOOP_LOADING_ERROR` - raised upon detection of same audio fragment being requested in loop - - data: { type : `MEDIA_ERROR`, details : `Hls.ErrorDetails.FRAG_LOOP_LOADING_ERROR`, fatal : `true` or `false`, frag : fragment object } - `Hls.ErrorDetails.FRAG_DECRYPT_ERROR` - raised when fragment decryption fails - data: { type : `MEDIA_ERROR`, details : `Hls.ErrorDetails.FRAG_DECRYPT_ERROR`, fatal : `true`, reason : failure reason } - `Hls.ErrorDetails.FRAG_PARSING_ERROR` - raised when fragment parsing fails diff --git a/doc/design.md b/doc/design.md index 057965d6899..637a7c6a93c 100644 --- a/doc/design.md +++ b/doc/design.md @@ -279,9 +279,6 @@ design idea is pretty simple : - ```FRAG_LOAD_ERROR``` is raised by [src/loader/fragment-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][]. - if auto level switch is enabled and loaded frag level is greater than 0, or if media.currentTime is buffered, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0. - if frag level is 0 or auto level switch is disabled and media.currentTime is not buffered, this error is marked as fatal and a call to ```hls.startLoad()``` could help recover it. - - ```FRAG_LOOP_LOADING_ERROR``` is raised by [src/controller/audio-stream-controller.js][] upon detection of same fragment being requested in loop. this could happen with badly formatted fragments. - - if auto level switch is enabled and loaded frag level is greater than 0, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0. - - if frag level is 0 or auto level switch is disabled, this error is marked as fatal and a call to ```hls.startLoad()``` could help recover it. - ```FRAG_LOAD_TIMEOUT``` is raised by [src/loader/fragment-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][]. - if auto level switch is enabled and loaded frag level is greater than 0, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0. - if frag level is 0 or auto level switch is disabled, this error is marked as fatal and a call to ```hls.startLoad()``` could help recover it. diff --git a/src/config.js b/src/config.js index 12da5ff9922..e7501b965cd 100644 --- a/src/config.js +++ b/src/config.js @@ -54,7 +54,6 @@ export var hlsDefaultConfig = { fragLoadingMaxRetry: 6, // used by fragment-loader fragLoadingRetryDelay: 1000, // used by fragment-loader fragLoadingMaxRetryTimeout: 64000, // used by fragment-loader - fragLoadingLoopThreshold: 3, // used by stream-controller startFragPrefetch: false, // used by stream-controller fpsDroppedMonitoringPeriod: 5000, // used by fps-controller fpsDroppedMonitoringThreshold: 0.2, // used by fps-controller diff --git a/src/controller/audio-stream-controller.js b/src/controller/audio-stream-controller.js index b6ecadac902..c7b81529f57 100644 --- a/src/controller/audio-stream-controller.js +++ b/src/controller/audio-stream-controller.js @@ -8,9 +8,10 @@ import Demuxer from '../demux/demuxer'; import Event from '../events'; import EventHandler from '../event-handler'; import * as LevelHelper from '../helper/level-helper';import TimeRanges from '../utils/timeRanges'; -import {ErrorTypes, ErrorDetails} from '../errors'; +import {ErrorDetails} from '../errors'; import {logger} from '../utils/logger'; import { findFragWithCC } from '../utils/discontinuities'; +import {FragmentTrackerState} from '../helper/fragment-tracker'; const State = { STOPPED : 'STOPPED', @@ -31,7 +32,7 @@ const State = { class AudioStreamController extends EventHandler { - constructor(hls) { + constructor(hls, fragmentTracker) { super(hls, Event.MEDIA_ATTACHED, Event.MEDIA_DETACHING, @@ -49,7 +50,7 @@ class AudioStreamController extends EventHandler { Event.BUFFER_APPENDED, Event.BUFFER_FLUSHED, Event.INIT_PTS_FOUND); - + this.fragmentTracker = fragmentTracker; this.config = hls.config; this.audioCodecSwap = false; this.ticks = 0; @@ -336,31 +337,19 @@ class AudioStreamController extends EventHandler { hls.trigger(Event.KEY_LOADING, {frag: frag}); } else { logger.log(`Loading ${frag.sn}, cc: ${frag.cc} of [${trackDetails.startSN} ,${trackDetails.endSN}],track ${trackId}, currentTime:${pos},bufferEnd:${bufferEnd.toFixed(3)}`); - // ensure that we are not reloading the same fragments in loop ... - if (this.fragLoadIdx !== undefined) { - this.fragLoadIdx++; - } else { - this.fragLoadIdx = 0; - } - if (frag.loadCounter) { - frag.loadCounter++; - let maxThreshold = config.fragLoadingLoopThreshold; - // if this frag has already been loaded 3 times, and if it has been reloaded recently - if (frag.loadCounter > maxThreshold && (Math.abs(this.fragLoadIdx - frag.loadIdx) < maxThreshold)) { - hls.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.FRAG_LOOP_LOADING_ERROR, fatal: false, frag: frag}); - return; + let fragmentState = this.fragmentTracker.getState(frag); + // Check if fragment is not loaded + let ftState = this.fragmentTracker.getState(frag); + if(ftState === FragmentTrackerState.NONE) { + this.fragCurrent = frag; + this.startFragRequested = true; + if (!isNaN(frag.sn)) { + this.nextLoadPosition = frag.start + frag.duration; } + hls.trigger(Event.FRAG_LOADING, {frag: frag}); + this.state = State.FRAG_LOADING; } else { - frag.loadCounter = 1; - } - frag.loadIdx = this.fragLoadIdx; - this.fragCurrent = frag; - this.startFragRequested = true; - if (!isNaN(frag.sn)) { - this.nextLoadPosition = frag.start + frag.duration; } - hls.trigger(Event.FRAG_LOADING, {frag: frag}); - this.state = State.FRAG_LOADING; } } } @@ -440,18 +429,6 @@ class AudioStreamController extends EventHandler { this.startPosition = this.lastCurrentTime = 0; } - // reset fragment loading counter on MSE detaching to avoid reporting FRAG_LOOP_LOADING_ERROR after error recovery - var tracks = this.tracks; - if (tracks) { - // reset fragment load counter - tracks.forEach(track => { - if(track.details) { - track.details.fragments.forEach(fragment => { - fragment.loadCounter = undefined; - }); - } - }); - } // remove video listeners if (media) { media.removeEventListener('seeking', this.onvseeking); @@ -471,10 +448,6 @@ class AudioStreamController extends EventHandler { if (this.media) { this.lastCurrentTime = this.media.currentTime; } - // avoid reporting fragment loop loading error in case user is seeking several times on same position - if (this.fragLoadIdx !== undefined) { - this.fragLoadIdx += 2 * this.config.fragLoadingLoopThreshold; - } // tick to speed up processing this.tick(); } @@ -516,10 +489,6 @@ class AudioStreamController extends EventHandler { this.audioSwitch = true; //main audio track are handled by stream-controller, just do something if switching to alt audio track this.state=State.IDLE; - // increase fragment load Index to avoid frag loop loading error after buffer flush - if (this.fragLoadIdx !== undefined) { - this.fragLoadIdx += 2 * this.config.fragLoadingLoopThreshold; - } } this.tick(); } @@ -826,8 +795,6 @@ class AudioStreamController extends EventHandler { let config = this.config; if (loadError <= config.fragLoadingMaxRetry) { this.fragLoadError = loadError; - // reset load counter to avoid frag loop loading error - frag.loadCounter = 0; // exponential backoff capped to config.fragLoadingMaxRetryTimeout var delay = Math.min(Math.pow(2,loadError-1)*config.fragLoadingRetryDelay,config.fragLoadingMaxRetryTimeout); logger.warn(`audioStreamController: frag loading failed, retry in ${delay} ms`); @@ -842,7 +809,6 @@ class AudioStreamController extends EventHandler { } } break; - case ErrorDetails.FRAG_LOOP_LOADING_ERROR: case ErrorDetails.AUDIO_TRACK_LOAD_ERROR: case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: case ErrorDetails.KEY_LOAD_ERROR: @@ -867,8 +833,6 @@ class AudioStreamController extends EventHandler { // reduce max buffer length as it might be too high. we do this to avoid loop flushing ... config.maxMaxBufferLength/=2; logger.warn(`audio:reduce max buffer length to ${config.maxMaxBufferLength}s`); - // increase fragment load Index to avoid frag loop loading error after buffer flush - this.fragLoadIdx += 2 * config.fragLoadingLoopThreshold; } this.state = State.IDLE; } else { diff --git a/src/controller/level-controller.js b/src/controller/level-controller.js index 6a4cb99085e..83102fa8e59 100644 --- a/src/controller/level-controller.js +++ b/src/controller/level-controller.js @@ -255,7 +255,6 @@ export default class LevelController extends EventHandler { switch (data.details) { case ErrorDetails.FRAG_LOAD_ERROR: case ErrorDetails.FRAG_LOAD_TIMEOUT: - case ErrorDetails.FRAG_LOOP_LOADING_ERROR: case ErrorDetails.KEY_LOAD_ERROR: case ErrorDetails.KEY_LOAD_TIMEOUT: levelIndex = data.frag.level; diff --git a/src/controller/stream-controller.js b/src/controller/stream-controller.js index 1467d4e7ee0..f0e8cb06762 100644 --- a/src/controller/stream-controller.js +++ b/src/controller/stream-controller.js @@ -501,9 +501,9 @@ class StreamController extends EventHandler { this.hls.trigger(Event.KEY_LOADING, {frag: frag}); } else { logger.log(`Loading ${frag.sn} of [${levelDetails.startSN} ,${levelDetails.endSN}],level ${level}, currentTime:${pos.toFixed(3)},bufferEnd:${bufferEnd.toFixed(3)}`); - // Check if fragment is attempting to load or already loaded with bad PTS + // Check if fragment is not loaded let ftState = this.fragmentTracker.getState(frag); - if(ftState !== FragmentTrackerState.LOADING_BUFFER && ftState !== FragmentTrackerState.PARTIAL) { + if(ftState === FragmentTrackerState.NONE) { frag.autoLevel = this.hls.autoLevelEnabled; frag.bitrateTest = this.bitrateTest; @@ -750,14 +750,12 @@ class StreamController extends EventHandler { this.startPosition = this.lastCurrentTime = 0; } - // reset fragment loading counter on MSE detaching to avoid reporting FRAG_LOOP_LOADING_ERROR after error recovery + // reset fragment backtracked flag var levels = this.levels; if (levels) { - // reset fragment load counter levels.forEach(level => { if(level.details) { level.details.fragments.forEach(fragment => { - fragment.loadCounter = undefined; fragment.backtracked = undefined; }); } @@ -1319,25 +1317,6 @@ class StreamController extends EventHandler { } } break; - case ErrorDetails.FRAG_LOOP_LOADING_ERROR: - if(!data.fatal) { - // if buffer is not empty - if (mediaBuffered) { - // try to reduce max buffer length : rationale is that we could get - // frag loop loading error because of buffer eviction - this._reduceMaxBufferLength(frag.duration); - this.state = State.IDLE; - } else { - // buffer empty. report as fatal if in manual mode or if lowest level. - // level controller takes care of emergency switch down logic - if (!frag.autoLevel || frag.level === 0) { - // switch error to fatal - data.fatal = true; - this.state = State.ERROR; - } - } - } - break; case ErrorDetails.LEVEL_LOAD_ERROR: case ErrorDetails.LEVEL_LOAD_TIMEOUT: if(this.state !== State.ERROR) { diff --git a/src/errors.js b/src/errors.js index cad8bff81e9..0ae9ad1fdd2 100644 --- a/src/errors.js +++ b/src/errors.js @@ -30,8 +30,6 @@ export const ErrorDetails = { AUDIO_TRACK_LOAD_TIMEOUT: 'audioTrackLoadTimeOut', // Identifier for fragment load error - data: { frag : fragment object, response : { code: error code, text: error text }} FRAG_LOAD_ERROR: 'fragLoadError', - // Identifier for fragment loop loading error - data: { frag : fragment object} - FRAG_LOOP_LOADING_ERROR: 'fragLoopLoadingError', // Identifier for fragment load timeout error - data: { frag : fragment object} FRAG_LOAD_TIMEOUT: 'fragLoadTimeOut', // Identifier for a fragment decryption error event - data: {id : demuxer Id,frag: fragment object, reason : parsing error description } diff --git a/src/helper/fragment-tracker.js b/src/helper/fragment-tracker.js index c81819028fa..1c9c0e983b9 100644 --- a/src/helper/fragment-tracker.js +++ b/src/helper/fragment-tracker.js @@ -11,6 +11,7 @@ export const FragmentTrackerState = { NONE: 'NONE', LOADING_BUFFER: 'LOADING_BUFFER', PARTIAL: 'PARTIAL', + GOOD: 'GOOD', }; export class FragmentTracker extends EventHandler { @@ -24,6 +25,8 @@ export class FragmentTracker extends EventHandler { // This holds all the loading fragments until the buffer is populated this.loadingFragments = {}; + // This holds all the successfully loaded fragments until the buffer is evicted + this.goodFragments = {}; // This keeps track of all fragments that loaded differently into the buffer from the PTS this.partialFragments = {}; this.partialFragmentTimes = {}; @@ -32,6 +35,7 @@ export class FragmentTracker extends EventHandler { } destroy() { + this.goodFragments = {}; this.loadingFragments = {}; this.partialFragments = {}; this.partialFragmentTimes = {}; @@ -52,7 +56,7 @@ export class FragmentTracker extends EventHandler { for (let fragKey in this.partialFragments) { if (this.partialFragments.hasOwnProperty(fragKey)) { fragment = this.partialFragments[fragKey]; - if(this.partialFragmentTimes[type] && this.partialFragmentTimes[type][fragKey]){ + if(this.partialFragmentTimes[type] !== undefined && this.partialFragmentTimes[type][fragKey] !== undefined){ fragmentTimes = this.partialFragmentTimes[type][fragKey]; for (let i = 0; i < fragmentTimes.length; i++) { time = fragmentTimes[i]; @@ -79,6 +83,29 @@ export class FragmentTracker extends EventHandler { } } } + for (let fragKey in this.goodFragments) { + if (this.goodFragments.hasOwnProperty(fragKey)) { + fragment = this.goodFragments[fragKey]; + let found = false; + for (let i = 0; i < timeRange.length; i++) { + startTime = timeRange.start(i) - bufferPadding; + endTime = timeRange.end(i) + bufferPadding; + if (fragment.startPTS >= startTime && fragment.endPTS <= endTime) { + // Fragment is entirely contained in buffer + found = true; + // No need to check the other timeRange times since it's completely playable + break; + } + if(fragment.endPTS <= startTime) { + // No need to check the rest of the timeRange as it is in order + break; + } + } + if(!found) { + delete this.goodFragments[fragKey]; + } + } + } } /** @@ -89,9 +116,9 @@ export class FragmentTracker extends EventHandler { detectPartialFragments(fragment) { let fragmentGaps, startTime, endTime; let fragKey = getFragmentKey(fragment); + let goodFragment = true; for(let type in this.timeRanges) { if (this.timeRanges.hasOwnProperty(type)) { - if(fragment.type === 'main' || fragment.type === type) { let timeRange = this.timeRanges[type]; @@ -123,16 +150,22 @@ export class FragmentTracker extends EventHandler { } logger.warn(`fragment-tracker: fragment with malformed PTS detected(${type}), level: ${fragment.level} sn: ${fragment.sn} startPTS: ${fragment.startPTS} endPTS: ${fragment.endPTS} loadedPTS: ${fragmentGapString}`); } - - if(!this.partialFragmentTimes[type]) { + if(this.partialFragmentTimes[type] === undefined) { + // fragment type can be 'main' while buffer type can be 'video' so we need both this.partialFragmentTimes[type] = {}; } this.partialFragmentTimes[type][fragKey] = fragmentGaps; this.partialFragments[fragKey] = fragment; + + goodFragment = false; } } } } + // Fragments can be good in one buffer but not in all, so we need to check this outside the loop + if (goodFragment) { + this.goodFragments[fragKey] = fragment; + } } /** @@ -169,7 +202,10 @@ export class FragmentTracker extends EventHandler { */ getState(fragment) { let fragKey = getFragmentKey(fragment); - if (this.loadingFragments[fragKey]) { + if (this.goodFragments[fragKey]) { + // Fragment is still loaded + return FragmentTrackerState.GOOD; + }else if (this.loadingFragments[fragKey]) { // Fragment never loaded into buffer return FragmentTrackerState.LOADING_BUFFER; }else if (this.partialFragments[fragKey]) { diff --git a/src/hls.js b/src/hls.js index b2f221bba03..2df7125f6ac 100644 --- a/src/hls.js +++ b/src/hls.js @@ -105,7 +105,7 @@ export default class Hls { // optional audio stream controller let Controller = config.audioStreamController; if (Controller) { - networkControllers.push(new Controller(this)); + networkControllers.push(new Controller(this, fragmentTracker)); } this.networkControllers = networkControllers; diff --git a/tests/unit/helper/fragment-tracker.js b/tests/unit/helper/fragment-tracker.js index b8ef400e2b3..dc14d1ab79b 100644 --- a/tests/unit/helper/fragment-tracker.js +++ b/tests/unit/helper/fragment-tracker.js @@ -95,7 +95,7 @@ describe('FragmentTracker', () => { hls.trigger(Event.FRAG_BUFFERED, { frag: fragment }); - assert.strictEqual(fragmentTracker.getState(fragment), FragmentTrackerState.NONE); + assert.strictEqual(fragmentTracker.getState(fragment), FragmentTrackerState.GOOD); }); it('detects partial fragments', () => { @@ -245,7 +245,7 @@ describe('FragmentTracker', () => { }); hls.trigger(Event.FRAG_BUFFERED, { frag: fragment }); - assert.strictEqual(fragmentTracker.getState(fragment), FragmentTrackerState.NONE); + assert.strictEqual(fragmentTracker.getState(fragment), FragmentTrackerState.GOOD); }); }); });