Skip to content

Commit

Permalink
Merge pull request video-dev#1320 from NicolasSiver/add-fragment-erro…
Browse files Browse the repository at this point in the history
…rs-for-level-loading

Add fragment errors for level loading
  • Loading branch information
mangui authored Sep 13, 2017
2 parents 87ee875 + 0bca151 commit 0e10c3e
Showing 1 changed file with 96 additions and 90 deletions.
186 changes: 96 additions & 90 deletions src/controller/level-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class LevelController extends EventHandler {
Event.LEVEL_LOADED,
Event.FRAG_LOADED,
Event.ERROR);
this.ontick = this.tick.bind(this);
this._manualLevel = -1;
this.timer = null;
}

destroy() {
Expand All @@ -27,7 +27,7 @@ class LevelController extends EventHandler {
}

cleanTimer() {
if (this.timer) {
if (this.timer !== null) {
clearTimeout(this.timer);
this.timer = null;
}
Expand Down Expand Up @@ -57,56 +57,51 @@ class LevelController extends EventHandler {
}

onManifestLoaded(data) {
var levels0 = [],
levels = [],
let levels = [],
bitrateStart,
bitrateSet = {},
levelSet = {},
levelFromSet = null,
videoCodecFound = false,
audioCodecFound = false,
hls = this.hls,
brokenmp4inmp3 = /chrome|firefox/.test(navigator.userAgent.toLowerCase());
chromeOrFirefox = /chrome|firefox/.test(navigator.userAgent.toLowerCase());

// regroup redundant level together
// regroup redundant levels together
data.levels.forEach(level => {
if(level.videoCodec) {
videoCodecFound = true;
}
// erase audio codec info if browser does not support mp4a.40.34. demuxer will autodetect codec and fallback to mpeg/audio
if(brokenmp4inmp3 && level.audioCodec && level.audioCodec.indexOf('mp4a.40.34') !== -1) {
level.loadError = 0;
level.fragmentError = false;

videoCodecFound = videoCodecFound || !!level.videoCodec;
audioCodecFound = audioCodecFound || !!level.audioCodec || !!(level.attrs && level.attrs.AUDIO);

// erase audio codec info if browser does not support mp4a.40.34.
// demuxer will autodetect codec and fallback to mpeg/audio
if (chromeOrFirefox === true && level.audioCodec && level.audioCodec.indexOf('mp4a.40.34') !== -1) {
level.audioCodec = undefined;
}
if(level.audioCodec || (level.attrs && level.attrs.AUDIO)) {
audioCodecFound = true;
}
let redundantLevelId = bitrateSet[level.bitrate];
if (redundantLevelId === undefined) {
bitrateSet[level.bitrate] = levels0.length;

levelFromSet = levelSet[level.bitrate];

if (levelFromSet === undefined) {
level.url = [level.url];
level.urlId = 0;
levels0.push(level);
levelSet[level.bitrate] = level;
levels.push(level);
} else {
levels0[redundantLevelId].url.push(level.url);
levelFromSet.url.push(level.url);
}
});

// remove audio-only level if we also have levels with audio+video codecs signalled
if(videoCodecFound && audioCodecFound) {
levels0.forEach(level => {
if(level.videoCodec) {
levels.push(level);
}
});
} else {
levels = levels0;
if (videoCodecFound === true && audioCodecFound === true) {
levels = levels.filter(({videoCodec}) => !!videoCodec);
}
// only keep level with supported audio/video codecs
levels = levels.filter(function(level) {
let audioCodec = level.audioCodec, videoCodec = level.videoCodec;
return (!audioCodec || isCodecSupportedInMp4(audioCodec)) &&
(!videoCodec || isCodecSupportedInMp4(videoCodec));

// only keep levels with supported audio/video codecs
levels = levels.filter(({audioCodec, videoCodec}) => {
return (!audioCodec || isCodecSupportedInMp4(audioCodec)) && (!videoCodec || isCodecSupportedInMp4(videoCodec));
});

if(levels.length) {
if (levels.length > 0) {
// start bitrate is the first bitrate of the manifest
bitrateStart = levels[0].bitrate;
// sort level on bitrate
Expand All @@ -122,11 +117,23 @@ class LevelController extends EventHandler {
break;
}
}
hls.trigger(Event.MANIFEST_PARSED, {levels: levels, firstLevel: this._firstLevel, stats: data.stats, audio : audioCodecFound, video : videoCodecFound, altAudio : data.audioTracks.length > 0});
this.hls.trigger(Event.MANIFEST_PARSED, {
levels : levels,
firstLevel: this._firstLevel,
stats : data.stats,
audio : audioCodecFound,
video : videoCodecFound,
altAudio : data.audioTracks.length > 0
});
} else {
hls.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, fatal: true, url: hls.url, reason: 'no level with compatible codecs found in manifest'});
this.hls.trigger(Event.ERROR, {
type : ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
fatal : true,
url : this.hls.url,
reason : 'no level with compatible codecs found in manifest'
});
}
return;
}

get levels() {
Expand Down Expand Up @@ -217,74 +224,72 @@ class LevelController extends EventHandler {
}

onError(data) {
if(data.fatal) {
if (data.fatal === true) {
if (data.type === ErrorTypes.NETWORK_ERROR) {
this.cleanTimer();
}
return;
}

let details = data.details, hls = this.hls, levelId, level, levelError = false;
let details = data.details, levelError = false, fragmentError = false;
let levelIndex, level;
let {config, media} = this.hls;

// try to recover not fatal errors
switch(details) {
switch (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:
levelId = data.frag.level;
break;
levelIndex = data.frag.level;
fragmentError = true;
break;
case ErrorDetails.LEVEL_LOAD_ERROR:
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
levelId = data.context.level;
levelIndex = data.context.level;
levelError = true;
break;
case ErrorDetails.REMUX_ALLOC_ERROR:
levelId = data.level;
break;
default:
levelIndex = data.level;
break;
}
/* try to switch to a redundant stream if any available.
* if no redundant stream available, emergency switch down (if in auto mode and current level not 0)
* otherwise, we cannot recover this network error ...
*/
if (levelId !== undefined) {
level = this._levels[levelId];
if(!level.loadError) {
level.loadError = 1;
} else {
level.loadError++;
}
if (levelIndex !== undefined) {
level = this._levels[levelIndex];
level.loadError++;
level.fragmentError = fragmentError;

// if any redundant streams available and if we haven't try them all (level.loadError is reseted on successful frag/level load.
// if level.loadError reaches nbRedundantLevel it means that we tried them all, no hope => let's switch down
const nbRedundantLevel = level.url.length;
if (nbRedundantLevel > 1 && level.loadError < nbRedundantLevel) {
level.urlId = (level.urlId + 1) % nbRedundantLevel;
// if level.loadError reaches redundantLevels it means that we tried them all, no hope => let's switch down
const redundantLevels = level.url.length;

if (redundantLevels > 1 && level.loadError < redundantLevels) {
level.urlId = (level.urlId + 1) % redundantLevels;
level.details = undefined;
logger.warn(`level controller,${details} for level ${levelId}: switching to redundant stream id ${level.urlId}`);
logger.warn(`level controller,${details} for level ${levelIndex}: switching to redundant stream id ${level.urlId}`);
} else {
// we could try to recover if in auto mode and current level not lowest level (0)
let recoverable = ((this._manualLevel === -1) && levelId);
if (recoverable) {
if ((this._manualLevel === -1) && levelIndex !== 0) {
logger.warn(`level controller,${details}: switch-down for next fragment`);
hls.nextAutoLevel = Math.max(0,levelId-1);
} else if(level && level.details && level.details.live) {
this.hls.nextAutoLevel = Math.max(0, levelIndex - 1);
} else if (level && level.details && level.details.live) {
logger.warn(`level controller,${details} on live stream, discard`);
if (levelError) {
// reset this._level so that another call to set level() will retrigger a frag load
if (levelError === true) {
// reset this._level so that another call to set level() will trigger again a frag load
this._level = undefined;
}
// other errors are handled by stream controller
} else if (details === ErrorDetails.LEVEL_LOAD_ERROR ||
details === ErrorDetails.LEVEL_LOAD_TIMEOUT) {
let media = hls.media,
// 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end
mediaBuffered = media && BufferHelper.isBuffered(media,media.currentTime) && BufferHelper.isBuffered(media,media.currentTime+0.5);
if (mediaBuffered) {
let retryDelay = hls.config.levelLoadingRetryDelay;
logger.warn(`level controller,${details}, but media buffered, retry in ${retryDelay}ms`);
this.timer = setTimeout(this.ontick,retryDelay);
} else if (levelError === true) {
// 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end
let mediaBuffered = !!media && BufferHelper.isBuffered(media, media.currentTime) && BufferHelper.isBuffered(media, media.currentTime + 0.5);
// FIXME Rely on Level Retry parameters, now it's possible to retry as long as media is buffered
if (mediaBuffered === true) {
logger.warn(`level controller,${details}, but media buffered, retry in ${config.levelLoadingRetryDelay}ms`);
this.timer = setTimeout(() => this.tick(), config.levelLoadingRetryDelay);
// boolean used to inform stream controller not to switch back to IDLE on non fatal error
data.levelRetry = true;
} else {
Expand All @@ -300,44 +305,46 @@ class LevelController extends EventHandler {
}
}

// reset level load error counter on successful frag loaded
onFragLoaded(data) {
const fragLoaded = data.frag;
if (fragLoaded && fragLoaded.type === 'main') {
const level = this._levels[fragLoaded.level];
if (level) {
// reset errors on the successful load of a fragment
onFragLoaded({frag}) {
if (frag !== undefined && frag.type === 'main') {
const level = this._levels[frag.level];
if (level !== undefined) {
level.fragmentError = false;
level.loadError = 0;
}
}
}

onLevelLoaded(data) {
const levelId = data.level;
// only process level loaded events matching with expected level
// only process level loaded events matching with expected level
if (levelId === this._level) {
let curLevel = this._levels[levelId];
// reset level load error counter on successful level loaded
curLevel.loadError = 0;
// reset level load error counter on successful level loaded only if there is no issues with fragments
if(curLevel.fragmentError === false){
curLevel.loadError = 0;
}
let newDetails = data.details;
// if current playlist is a live playlist, arm a timer to reload it
if (newDetails.live) {
let reloadInterval = 1000*( newDetails.averagetargetduration ? newDetails.averagetargetduration : newDetails.targetduration),
curDetails = curLevel.details;
let reloadInterval = 1000 * ( newDetails.averagetargetduration ? newDetails.averagetargetduration : newDetails.targetduration),
curDetails = curLevel.details;
if (curDetails && newDetails.endSN === curDetails.endSN) {
// 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.
reloadInterval /=2;
reloadInterval /= 2;
logger.log(`same live playlist, reload twice faster`);
}
// decrement reloadInterval with level loading delay
reloadInterval -= performance.now() - data.stats.trequest;
// in any case, don't reload more than every second
reloadInterval = Math.max(1000,Math.round(reloadInterval));
reloadInterval = Math.max(1000, Math.round(reloadInterval));
logger.log(`live playlist, reload in ${reloadInterval} ms`);
this.timer = setTimeout(this.ontick,reloadInterval);
this.timer = setTimeout(() => this.tick(), reloadInterval);
} else {
this.timer = null;
this.cleanTimer();
}
}
}
Expand Down Expand Up @@ -370,4 +377,3 @@ class LevelController extends EventHandler {
}

export default LevelController;

0 comments on commit 0e10c3e

Please sign in to comment.