Skip to content

Commit

Permalink
add fragment tracker support to alternate audio
Browse files Browse the repository at this point in the history
  • Loading branch information
David Kim authored and David Kim committed Dec 8, 2017
1 parent d0e690b commit bc045f3
Show file tree
Hide file tree
Showing 10 changed files with 61 additions and 91 deletions.
2 changes: 0 additions & 2 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions doc/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 14 additions & 50 deletions src/controller/audio-stream-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -31,7 +32,7 @@ const State = {

class AudioStreamController extends EventHandler {

constructor(hls) {
constructor(hls, fragmentTracker) {
super(hls,
Event.MEDIA_ATTACHED,
Event.MEDIA_DETACHING,
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -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);
Expand All @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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`);
Expand All @@ -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:
Expand All @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion src/controller/level-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
27 changes: 3 additions & 24 deletions src/controller/stream-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
});
}
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 0 additions & 2 deletions src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
46 changes: 41 additions & 5 deletions src/helper/fragment-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const FragmentTrackerState = {
NONE: 'NONE',
LOADING_BUFFER: 'LOADING_BUFFER',
PARTIAL: 'PARTIAL',
GOOD: 'GOOD',
};

export class FragmentTracker extends EventHandler {
Expand All @@ -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 = {};
Expand All @@ -32,6 +35,7 @@ export class FragmentTracker extends EventHandler {
}

destroy() {
this.goodFragments = {};
this.loadingFragments = {};
this.partialFragments = {};
this.partialFragmentTimes = {};
Expand All @@ -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];
Expand All @@ -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];
}
}
}
}

/**
Expand All @@ -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];

Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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]) {
Expand Down
2 changes: 1 addition & 1 deletion src/hls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions tests/unit/helper/fragment-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});

0 comments on commit bc045f3

Please sign in to comment.