2015-12-16 00:30:14 -05:00
/ * *
* Playlist Loader
* /
import Event from '../events' ;
import { ErrorTypes , ErrorDetails } from '../errors' ;
import URLHelper from '../utils/url' ;
2016-01-13 15:58:12 -05:00
import AttrList from '../utils/attr-list' ;
2015-12-16 00:30:14 -05:00
//import {logger} from '../utils/logger';
class PlaylistLoader {
constructor ( hls ) {
this . hls = hls ;
this . onml = this . onManifestLoading . bind ( this ) ;
this . onll = this . onLevelLoading . bind ( this ) ;
hls . on ( Event . MANIFEST _LOADING , this . onml ) ;
hls . on ( Event . LEVEL _LOADING , this . onll ) ;
}
destroy ( ) {
if ( this . loader ) {
this . loader . destroy ( ) ;
this . loader = null ;
}
this . url = this . id = null ;
this . hls . off ( Event . MANIFEST _LOADING , this . onml ) ;
this . hls . off ( Event . LEVEL _LOADING , this . onll ) ;
}
onManifestLoading ( event , data ) {
this . load ( data . url , null ) ;
}
onLevelLoading ( event , data ) {
this . load ( data . url , data . level , data . id ) ;
}
load ( url , id1 , id2 ) {
2016-01-13 15:58:12 -05:00
var config = this . hls . config ,
retry ,
timeout ,
retryDelay ;
2015-12-16 00:30:14 -05:00
this . url = url ;
this . id = id1 ;
this . id2 = id2 ;
2016-01-13 15:58:12 -05:00
if ( this . id === undefined ) {
retry = config . manifestLoadingMaxRetry ;
timeout = config . manifestLoadingTimeOut ;
retryDelay = config . manifestLoadingRetryDelay ;
} else {
retry = config . levelLoadingMaxRetry ;
timeout = config . levelLoadingTimeOut ;
retryDelay = config . levelLoadingRetryDelay ;
}
2015-12-16 00:30:14 -05:00
this . loader = typeof ( config . pLoader ) !== 'undefined' ? new config . pLoader ( config ) : new config . loader ( config ) ;
2016-01-13 15:58:12 -05:00
this . loader . load ( url , '' , this . loadsuccess . bind ( this ) , this . loaderror . bind ( this ) , this . loadtimeout . bind ( this ) , timeout , retry , retryDelay ) ;
2015-12-16 00:30:14 -05:00
}
resolve ( url , baseUrl ) {
return URLHelper . buildAbsoluteURL ( baseUrl , url ) ;
}
parseMasterPlaylist ( string , baseurl ) {
2016-01-13 15:58:12 -05:00
let levels = [ ] , result ;
2015-12-16 00:30:14 -05:00
// https://regex101.com is your friend
2016-01-13 15:58:12 -05:00
const re = /#EXT-X-STREAM-INF:([^\n\r]*)[\r\n]+([^\r\n]+)/g ;
2015-12-16 00:30:14 -05:00
while ( ( result = re . exec ( string ) ) != null ) {
2016-01-13 15:58:12 -05:00
const level = { } ;
var attrs = level . attrs = new AttrList ( result [ 1 ] ) ;
level . url = this . resolve ( result [ 2 ] , baseurl ) ;
var resolution = attrs . decimalResolution ( 'RESOLUTION' ) ;
if ( resolution ) {
level . width = resolution . width ;
level . height = resolution . height ;
}
level . bitrate = attrs . decimalInteger ( 'BANDWIDTH' ) ;
level . name = attrs . NAME ;
var codecs = attrs . CODECS ;
if ( codecs ) {
codecs = codecs . split ( ',' ) ;
for ( let i = 0 ; i < codecs . length ; i ++ ) {
const codec = codecs [ i ] ;
if ( codec . indexOf ( 'avc1' ) !== - 1 ) {
level . videoCodec = this . avc1toavcoti ( codec ) ;
} else {
level . audioCodec = codec ;
}
2015-12-16 00:30:14 -05:00
}
}
2016-01-13 15:58:12 -05:00
2015-12-16 00:30:14 -05:00
levels . push ( level ) ;
}
return levels ;
}
avc1toavcoti ( codec ) {
var result , avcdata = codec . split ( '.' ) ;
if ( avcdata . length > 2 ) {
result = avcdata . shift ( ) + '.' ;
result += parseInt ( avcdata . shift ( ) ) . toString ( 16 ) ;
2015-12-23 12:46:01 -05:00
result += ( '000' + parseInt ( avcdata . shift ( ) ) . toString ( 16 ) ) . substr ( - 4 ) ;
2015-12-16 00:30:14 -05:00
} else {
result = codec ;
}
return result ;
}
cloneObj ( obj ) {
return JSON . parse ( JSON . stringify ( obj ) ) ;
}
parseLevelPlaylist ( string , baseurl , id ) {
2016-01-13 15:58:12 -05:00
var currentSN = 0 ,
totalduration = 0 ,
level = { url : baseurl , fragments : [ ] , live : true , startSN : 0 } ,
levelkey = { method : null , key : null , iv : null , uri : null } ,
cc = 0 ,
programDateTime = null ,
frag = null ,
result ,
regexp ,
byteRangeEndOffset ,
byteRangeStartOffset ;
regexp = /(?:#EXT-X-(MEDIA-SEQUENCE):(\d+))|(?:#EXT-X-(TARGETDURATION):(\d+))|(?:#EXT-X-(KEY):(.*))|(?:#EXT(INF):([\d\.]+)[^\r\n]*([\r\n]+[^#|\r\n]+)?)|(?:#EXT-X-(BYTERANGE):([\d]+[@[\d]*)]*[\r\n]+([^#|\r\n]+)?|(?:#EXT-X-(ENDLIST))|(?:#EXT-X-(DIS)CONTINUITY))|(?:#EXT-X-(PROGRAM-DATE-TIME):(.*))/g ;
2015-12-16 00:30:14 -05:00
while ( ( result = regexp . exec ( string ) ) !== null ) {
result . shift ( ) ;
result = result . filter ( function ( n ) { return ( n !== undefined ) ; } ) ;
switch ( result [ 0 ] ) {
case 'MEDIA-SEQUENCE' :
currentSN = level . startSN = parseInt ( result [ 1 ] ) ;
break ;
case 'TARGETDURATION' :
level . targetduration = parseFloat ( result [ 1 ] ) ;
break ;
case 'ENDLIST' :
level . live = false ;
break ;
case 'DIS' :
cc ++ ;
break ;
case 'BYTERANGE' :
var params = result [ 1 ] . split ( '@' ) ;
if ( params . length === 1 ) {
byteRangeStartOffset = byteRangeEndOffset ;
} else {
byteRangeStartOffset = parseInt ( params [ 1 ] ) ;
}
byteRangeEndOffset = parseInt ( params [ 0 ] ) + byteRangeStartOffset ;
if ( frag && ! frag . url ) {
frag . byteRangeStartOffset = byteRangeStartOffset ;
frag . byteRangeEndOffset = byteRangeEndOffset ;
frag . url = this . resolve ( result [ 2 ] , baseurl ) ;
}
break ;
case 'INF' :
var duration = parseFloat ( result [ 1 ] ) ;
if ( ! isNaN ( duration ) ) {
var fragdecryptdata ,
sn = currentSN ++ ;
if ( levelkey . method && levelkey . uri && ! levelkey . iv ) {
fragdecryptdata = this . cloneObj ( levelkey ) ;
var uint8View = new Uint8Array ( 16 ) ;
for ( var i = 12 ; i < 16 ; i ++ ) {
uint8View [ i ] = ( sn >> 8 * ( 15 - i ) ) & 0xff ;
}
fragdecryptdata . iv = uint8View ;
} else {
fragdecryptdata = levelkey ;
}
2016-01-13 15:58:12 -05:00
var url = result [ 2 ] ? this . resolve ( result [ 2 ] , baseurl ) : null ;
frag = { url : url , duration : duration , start : totalduration , sn : sn , level : id , cc : cc , byteRangeStartOffset : byteRangeStartOffset , byteRangeEndOffset : byteRangeEndOffset , decryptdata : fragdecryptdata , programDateTime : programDateTime } ;
level . fragments . push ( frag ) ;
2015-12-16 00:30:14 -05:00
totalduration += duration ;
byteRangeStartOffset = null ;
2016-01-13 15:58:12 -05:00
programDateTime = null ;
2015-12-16 00:30:14 -05:00
}
break ;
case 'KEY' :
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.4
var decryptparams = result [ 1 ] ;
2016-01-13 15:58:12 -05:00
var keyAttrs = new AttrList ( decryptparams ) ;
var decryptmethod = keyAttrs . enumeratedString ( 'METHOD' ) ,
decrypturi = keyAttrs . URI ,
decryptiv = keyAttrs . hexadecimalInteger ( 'IV' ) ;
2015-12-16 00:30:14 -05:00
if ( decryptmethod ) {
levelkey = { method : null , key : null , iv : null , uri : null } ;
if ( ( decrypturi ) && ( decryptmethod === 'AES-128' ) ) {
levelkey . method = decryptmethod ;
// URI to get the key
levelkey . uri = this . resolve ( decrypturi , baseurl ) ;
levelkey . key = null ;
// Initialization Vector (IV)
2016-01-13 15:58:12 -05:00
levelkey . iv = decryptiv ;
2015-12-16 00:30:14 -05:00
}
}
break ;
2016-01-13 15:58:12 -05:00
case 'PROGRAM-DATE-TIME' :
programDateTime = new Date ( Date . parse ( result [ 1 ] ) ) ;
break ;
2015-12-16 00:30:14 -05:00
default :
break ;
}
}
//logger.log('found ' + level.fragments.length + ' fragments');
2016-01-13 15:58:12 -05:00
if ( frag && ! frag . url ) {
level . fragments . pop ( ) ;
totalduration -= frag . duration ;
}
2015-12-16 00:30:14 -05:00
level . totalduration = totalduration ;
level . endSN = currentSN - 1 ;
return level ;
}
loadsuccess ( event , stats ) {
2016-01-13 15:58:12 -05:00
var target = event . currentTarget ,
string = target . responseText ,
url = target . responseURL ,
id = this . id ,
id2 = this . id2 ,
hls = this . hls ,
levels ;
2015-12-16 00:30:14 -05:00
// responseURL not supported on some browsers (it is used to detect URL redirection)
if ( url === undefined ) {
// fallback to initial URL
url = this . url ;
}
stats . tload = performance . now ( ) ;
2016-01-13 15:58:12 -05:00
stats . mtime = new Date ( target . getResponseHeader ( 'Last-Modified' ) ) ;
2015-12-16 00:30:14 -05:00
if ( string . indexOf ( '#EXTM3U' ) === 0 ) {
if ( string . indexOf ( '#EXTINF:' ) > 0 ) {
// 1 level playlist
// if first request, fire manifest loaded event, level will be reloaded afterwards
// (this is to have a uniform logic for 1 level/multilevel playlists)
if ( this . id === null ) {
hls . trigger ( Event . MANIFEST _LOADED , { levels : [ { url : url } ] , url : url , stats : stats } ) ;
} else {
var levelDetails = this . parseLevelPlaylist ( string , url , id ) ;
stats . tparsed = performance . now ( ) ;
hls . trigger ( Event . LEVEL _LOADED , { details : levelDetails , level : id , id : id2 , stats : stats } ) ;
}
} else {
levels = this . parseMasterPlaylist ( string , url ) ;
// multi level playlist, parse level info
if ( levels . length ) {
hls . trigger ( Event . MANIFEST _LOADED , { levels : levels , url : url , stats : stats } ) ;
} else {
hls . trigger ( Event . ERROR , { type : ErrorTypes . NETWORK _ERROR , details : ErrorDetails . MANIFEST _PARSING _ERROR , fatal : true , url : url , reason : 'no level found in manifest' } ) ;
}
}
} else {
hls . trigger ( Event . ERROR , { type : ErrorTypes . NETWORK _ERROR , details : ErrorDetails . MANIFEST _PARSING _ERROR , fatal : true , url : url , reason : 'no EXTM3U delimiter' } ) ;
}
}
loaderror ( event ) {
var details , fatal ;
if ( this . id === null ) {
details = ErrorDetails . MANIFEST _LOAD _ERROR ;
fatal = true ;
} else {
details = ErrorDetails . LEVEL _LOAD _ERROR ;
fatal = false ;
}
this . loader . abort ( ) ;
this . hls . trigger ( Event . ERROR , { type : ErrorTypes . NETWORK _ERROR , details : details , fatal : fatal , url : this . url , loader : this . loader , response : event . currentTarget , level : this . id , id : this . id2 } ) ;
}
loadtimeout ( ) {
var details , fatal ;
if ( this . id === null ) {
details = ErrorDetails . MANIFEST _LOAD _TIMEOUT ;
fatal = true ;
} else {
details = ErrorDetails . LEVEL _LOAD _TIMEOUT ;
fatal = false ;
}
this . loader . abort ( ) ;
this . hls . trigger ( Event . ERROR , { type : ErrorTypes . NETWORK _ERROR , details : details , fatal : fatal , url : this . url , loader : this . loader , level : this . id , id : this . id2 } ) ;
}
}
export default PlaylistLoader ;