2015-12-16 00:30:14 -05:00
/ *
* Level Controller
* /
import Event from '../events' ;
2016-01-18 14:07:26 -05:00
import EventHandler from '../event-handler' ;
2015-12-16 00:30:14 -05:00
import { logger } from '../utils/logger' ;
import { ErrorTypes , ErrorDetails } from '../errors' ;
2016-01-18 14:07:26 -05:00
class LevelController extends EventHandler {
2015-12-16 00:30:14 -05:00
constructor ( hls ) {
2016-01-18 14:07:26 -05:00
super ( hls ,
Event . MANIFEST _LOADED ,
Event . LEVEL _LOADED ,
Event . ERROR ) ;
2015-12-16 00:30:14 -05:00
this . ontick = this . tick . bind ( this ) ;
this . _manualLevel = this . _autoLevelCapping = - 1 ;
}
destroy ( ) {
if ( this . timer ) {
2016-05-09 11:46:47 -04:00
clearTimeout ( this . timer ) ;
2016-04-15 15:20:04 -04:00
this . timer = null ;
2015-12-16 00:30:14 -05:00
}
this . _manualLevel = - 1 ;
}
2016-03-17 01:06:37 -04:00
startLoad ( ) {
this . canload = true ;
// speed up live playlist refresh if timer exists
if ( this . timer ) {
this . tick ( ) ;
}
}
stopLoad ( ) {
this . canload = false ;
}
2016-01-18 14:07:26 -05:00
onManifestLoaded ( data ) {
2015-12-23 12:46:01 -05:00
var levels0 = [ ] , levels = [ ] , bitrateStart , i , bitrateSet = { } , videoCodecFound = false , audioCodecFound = false , hls = this . hls ;
2015-12-16 00:30:14 -05:00
// regroup redundant level together
data . levels . forEach ( level => {
if ( level . videoCodec ) {
videoCodecFound = true ;
}
if ( level . audioCodec ) {
audioCodecFound = true ;
}
var redundantLevelId = bitrateSet [ level . bitrate ] ;
if ( redundantLevelId === undefined ) {
bitrateSet [ level . bitrate ] = levels0 . length ;
level . url = [ level . url ] ;
level . urlId = 0 ;
levels0 . push ( level ) ;
} else {
levels0 [ redundantLevelId ] . 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 ;
}
// only keep level with supported audio/video codecs
2015-12-23 12:46:01 -05:00
levels = levels . filter ( function ( level ) {
2016-02-01 12:02:17 -05:00
var checkSupportedAudio = function ( codec ) { return MediaSource . isTypeSupported ( ` audio/mp4;codecs= ${ codec } ` ) ; } ;
var checkSupportedVideo = function ( codec ) { return MediaSource . isTypeSupported ( ` video/mp4;codecs= ${ codec } ` ) ; } ;
2015-12-16 00:30:14 -05:00
var audioCodec = level . audioCodec , videoCodec = level . videoCodec ;
2016-02-01 12:02:17 -05:00
return ( ! audioCodec || checkSupportedAudio ( audioCodec ) ) &&
( ! videoCodec || checkSupportedVideo ( videoCodec ) ) ;
2015-12-16 00:30:14 -05:00
} ) ;
2015-12-23 12:46:01 -05:00
if ( levels . length ) {
// start bitrate is the first bitrate of the manifest
bitrateStart = levels [ 0 ] . bitrate ;
// sort level on bitrate
levels . sort ( function ( a , b ) {
return a . bitrate - b . bitrate ;
} ) ;
this . _levels = levels ;
// find index of first level in sorted levels
for ( i = 0 ; i < levels . length ; i ++ ) {
if ( levels [ i ] . bitrate === bitrateStart ) {
this . _firstLevel = i ;
logger . log ( ` manifest loaded, ${ levels . length } level(s) found, first bitrate: ${ bitrateStart } ` ) ;
break ;
}
2015-12-16 00:30:14 -05:00
}
2015-12-23 12:46:01 -05:00
hls . trigger ( Event . MANIFEST _PARSED , { levels : this . _levels , firstLevel : this . _firstLevel , stats : data . stats } ) ;
} else {
2016-03-14 13:06:02 -04:00
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' } ) ;
2015-12-16 00:30:14 -05:00
}
return ;
}
get levels ( ) {
return this . _levels ;
}
get level ( ) {
return this . _level ;
}
set level ( newLevel ) {
2016-05-09 11:46:47 -04:00
let levels = this . _levels ;
if ( levels && levels . length > newLevel ) {
if ( this . _level !== newLevel || levels [ newLevel ] . details === undefined ) {
this . setLevelInternal ( newLevel ) ;
}
2015-12-16 00:30:14 -05:00
}
}
setLevelInternal ( newLevel ) {
2016-05-09 11:46:47 -04:00
let levels = this . _levels ;
2015-12-16 00:30:14 -05:00
// check if level idx is valid
2016-05-09 11:46:47 -04:00
if ( newLevel >= 0 && newLevel < levels . length ) {
2015-12-16 00:30:14 -05:00
// stopping live reloading timer if any
if ( this . timer ) {
2016-05-09 11:46:47 -04:00
clearTimeout ( this . timer ) ;
2015-12-16 00:30:14 -05:00
this . timer = null ;
}
this . _level = newLevel ;
logger . log ( ` switching to level ${ newLevel } ` ) ;
this . hls . trigger ( Event . LEVEL _SWITCH , { level : newLevel } ) ;
2016-05-09 11:46:47 -04:00
var level = levels [ newLevel ] ;
2015-12-16 00:30:14 -05:00
// check if we need to load playlist for this level
if ( level . details === undefined || level . details . live === true ) {
// level not retrieved yet, or live playlist we need to (re)load it
logger . log ( ` (re)loading playlist for level ${ newLevel } ` ) ;
var urlId = level . urlId ;
this . hls . trigger ( Event . LEVEL _LOADING , { url : level . url [ urlId ] , level : newLevel , id : urlId } ) ;
}
} else {
// invalid level id given, trigger error
this . hls . trigger ( Event . ERROR , { type : ErrorTypes . OTHER _ERROR , details : ErrorDetails . LEVEL _SWITCH _ERROR , level : newLevel , fatal : false , reason : 'invalid level idx' } ) ;
}
}
get manualLevel ( ) {
return this . _manualLevel ;
}
set manualLevel ( newLevel ) {
this . _manualLevel = newLevel ;
2016-05-09 11:46:47 -04:00
if ( this . _startLevel === undefined ) {
this . _startLevel = newLevel ;
}
2015-12-16 00:30:14 -05:00
if ( newLevel !== - 1 ) {
this . level = newLevel ;
}
}
get firstLevel ( ) {
return this . _firstLevel ;
}
set firstLevel ( newLevel ) {
this . _firstLevel = newLevel ;
}
get startLevel ( ) {
if ( this . _startLevel === undefined ) {
return this . _firstLevel ;
} else {
return this . _startLevel ;
}
}
set startLevel ( newLevel ) {
this . _startLevel = newLevel ;
}
2016-01-18 14:07:26 -05:00
onError ( data ) {
2015-12-16 00:30:14 -05:00
if ( data . fatal ) {
return ;
}
2016-05-20 15:45:04 -04:00
let details = data . details , hls = this . hls , levelId , level , levelError = false ;
2015-12-16 00:30:14 -05:00
// try to recover not fatal errors
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 ;
case ErrorDetails . LEVEL _LOAD _ERROR :
case ErrorDetails . LEVEL _LOAD _TIMEOUT :
levelId = data . level ;
2016-05-20 15:45:04 -04:00
levelError = true ;
2015-12-16 00:30:14 -05:00
break ;
default :
break ;
}
/ * t r y t o s w i t c h t o a r e d u n d a n t s t r e a m i f a n y a v a i l a b l e .
* if no redundant stream available , emergency switch down ( if in auto mode and current level not 0 )
2015-12-23 12:46:01 -05:00
* otherwise , we cannot recover this network error ...
* don ' t raise FRAG _LOAD _ERROR and FRAG _LOAD _TIMEOUT as fatal , as it is handled by mediaController
2015-12-16 00:30:14 -05:00
* /
if ( levelId !== undefined ) {
level = this . _levels [ levelId ] ;
if ( level . urlId < ( level . url . length - 1 ) ) {
level . urlId ++ ;
level . details = undefined ;
logger . warn ( ` level controller, ${ details } for level ${ levelId } : 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 ) {
logger . warn ( ` level controller, ${ details } : emergency switch-down for next fragment ` ) ;
hls . abrController . nextAutoLevel = 0 ;
} else if ( level && level . details && level . details . live ) {
logger . warn ( ` level controller, ${ details } on live stream, discard ` ) ;
2016-05-20 15:45:04 -04:00
if ( levelError ) {
// reset this._level so that another call to set level() will retrigger a frag load
this . _level = undefined ;
}
2015-12-23 12:46:01 -05:00
// FRAG_LOAD_ERROR and FRAG_LOAD_TIMEOUT are handled by mediaController
} else if ( details !== ErrorDetails . FRAG _LOAD _ERROR && details !== ErrorDetails . FRAG _LOAD _TIMEOUT ) {
2015-12-16 00:30:14 -05:00
logger . error ( ` cannot recover ${ details } error ` ) ;
this . _level = undefined ;
// stopping live reloading timer if any
if ( this . timer ) {
2016-05-09 11:46:47 -04:00
clearTimeout ( this . timer ) ;
2015-12-16 00:30:14 -05:00
this . timer = null ;
}
// redispatch same error but with fatal set to true
data . fatal = true ;
2016-04-08 12:49:34 -04:00
hls . trigger ( Event . ERROR , data ) ;
2015-12-16 00:30:14 -05:00
}
}
}
}
2016-01-18 14:07:26 -05:00
onLevelLoaded ( data ) {
2016-05-09 11:46:47 -04:00
// only process level loaded events matching with expected level
if ( data . level === this . _level ) {
let newDetails = data . details ;
// if current playlist is a live playlist, arm a timer to reload it
if ( newDetails . live ) {
let reloadInterval = 1000 * newDetails . targetduration ,
curLevel = this . _levels [ data . level ] ,
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 ;
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 ) ) ;
logger . log ( ` live playlist, reload in ${ reloadInterval } ms ` ) ;
this . timer = setTimeout ( this . ontick , reloadInterval ) ;
} else {
this . timer = null ;
}
2015-12-16 00:30:14 -05:00
}
}
tick ( ) {
var levelId = this . _level ;
2016-03-17 01:06:37 -04:00
if ( levelId !== undefined && this . canload ) {
2015-12-16 00:30:14 -05:00
var level = this . _levels [ levelId ] , urlId = level . urlId ;
this . hls . trigger ( Event . LEVEL _LOADING , { url : level . url [ urlId ] , level : levelId , id : urlId } ) ;
}
}
2016-03-16 13:43:01 -04:00
get nextLoadLevel ( ) {
2015-12-16 00:30:14 -05:00
if ( this . _manualLevel !== - 1 ) {
return this . _manualLevel ;
} else {
return this . hls . abrController . nextAutoLevel ;
}
}
2016-03-16 13:43:01 -04:00
set nextLoadLevel ( nextLevel ) {
this . level = nextLevel ;
if ( this . _manualLevel === - 1 ) {
this . hls . abrController . nextAutoLevel = nextLevel ;
}
}
2015-12-16 00:30:14 -05:00
}
export default LevelController ;