@@ -12,7 +12,7 @@ export default class HLSHandler extends PassThrough {
1212 this . headers = options . headers || { }
1313 this . localAddress = options . localAddress || null
1414 this . onResolveUrl = options . onResolveUrl || null
15- this . strategy = options . type ?. includes ( 'fmp4' ) ? 'segmented' : 'streaming'
15+ this . strategy = options . strategy || ( options . type ?. includes ( 'fmp4' ) ? 'segmented' : 'streaming' )
1616
1717 this . fetcher = new SegmentFetcher ( {
1818 headers : this . headers ,
@@ -24,12 +24,13 @@ export default class HLSHandler extends PassThrough {
2424 this . processedOrder = [ ]
2525 this . MAX_HISTORY = 200
2626 this . segmentQueue = [ ]
27+ this . MAX_PARALLEL_FETCHES = this . strategy === 'segmented' ? 3 : ( this . strategy === 'streaming' ? 2 : 1 )
2728 this . isFetching = false
2829 this . stop = false
2930 this . lastMapUri = null
3031 this . isLive = false
3132 this . playlistTimer = null
32- this . activeSegmentStream = null
33+ this . activeSegmentStreams = new Map ( )
3334 this . lastMediaSequence = - 1
3435 this . highestSequence = - 1
3536 this . maxGap = 30
@@ -54,10 +55,10 @@ export default class HLSHandler extends PassThrough {
5455 clearTimeout ( this . playlistTimer )
5556 this . playlistTimer = null
5657 }
57- if ( this . activeSegmentStream ) {
58- this . activeSegmentStream . destroy ( )
59- this . activeSegmentStream = null
58+ for ( const stream of this . activeSegmentStreams . values ( ) ) {
59+ stream . destroy ( )
6060 }
61+ this . activeSegmentStreams . clear ( )
6162 this . segmentQueue = [ ]
6263 this . processedSegments . clear ( )
6364 this . processedOrder = [ ]
@@ -107,31 +108,31 @@ export default class HLSHandler extends PassThrough {
107108 }
108109
109110 if ( parsed . isMaster ) {
110- const bestVariant = parsed . variants . reduce ( ( prev , current ) => {
111- const hasAudio = ( v ) => v . codecs ?. includes ( 'mp4a' ) || v . codecs ?. includes ( 'opus' )
112- if ( ! hasAudio ( prev ) ) return current
113- if ( ! hasAudio ( current ) ) return prev
114- return ( current . bandwidth > prev . bandwidth ) ? current : prev
115- } , parsed . variants [ 0 ] )
116-
117- const itag = bestVariant . url . match ( / [ \/ & ] i t a g [ \/ ] ( \d + ) / ) ?. [ 1 ] ||
118- bestVariant . url . match ( / [ ? & ] i t a g = ( \d + ) / ) ?. [ 1 ] || 'unknown'
119-
120- logger ( 'debug' , 'HLSHandler' , `Selected variant itag: ${ itag } , bandwidth: ${ bestVariant . bandwidth } , codecs: ${ bestVariant . codecs } ` )
111+ const sortedVariants = parsed . variants . sort ( ( a , b ) => b . bandwidth - a . bandwidth )
112+ const bestVariant = sortedVariants . find ( v =>
113+ ( v . codecs ?. includes ( 'mp4a' ) || v . codecs ?. includes ( 'opus' ) ) &&
114+ ! v . codecs ?. includes ( 'avc1' )
115+ ) || sortedVariants . find ( v =>
116+ v . codecs ?. includes ( 'mp4a' ) || v . codecs ?. includes ( 'opus' )
117+ ) || sortedVariants [ 0 ]
118+
119+ logger ( 'debug' , 'HLSHandler' , `Selected variant bandwidth: ${ bestVariant . bandwidth } , codecs: ${ bestVariant . codecs } ` )
121120 this . currentUrl = bestVariant . url
122121 return setImmediate ( ( ) => this . _playlistLoop ( ) )
123122 }
124123
125124 this . isLive = parsed . isLive
126125
127126 if ( this . lastMediaSequence !== - 1 && ( parsed . mediaSequence < this . lastMediaSequence || parsed . mediaSequence > this . lastMediaSequence + this . maxGap ) ) {
128- logger ( 'warn' , 'HLSHandler' , `Playlist sequence discontinuity (${ this . lastMediaSequence } -> ${ parsed . mediaSequence } ). Resetting to live edge.` )
129- this . segmentQueue = [ ]
130- this . processedSegments . clear ( )
131- this . processedOrder = [ ]
132- this . highestSequence = - 1
133- this . preRolled = false
134- this . justResynced = true
127+ if ( this . isLive ) {
128+ logger ( 'warn' , 'HLSHandler' , `Playlist sequence discontinuity (${ this . lastMediaSequence } -> ${ parsed . mediaSequence } ). Resetting to live edge.` )
129+ this . segmentQueue = [ ]
130+ this . processedSegments . clear ( )
131+ this . processedOrder = [ ]
132+ this . highestSequence = - 1
133+ this . preRolled = false
134+ this . justResynced = true
135+ }
135136 }
136137 this . lastMediaSequence = parsed . mediaSequence
137138
@@ -142,7 +143,7 @@ export default class HLSHandler extends PassThrough {
142143 }
143144
144145 const isFirstLoad = this . processedSegments . size === 0
145- if ( ( isFirstLoad || this . justResynced ) && this . isLive ) {
146+ if ( this . isLive && ( isFirstLoad || this . justResynced ) ) {
146147 if ( this . justResynced ) {
147148 this . processedSegments . clear ( )
148149 this . processedOrder = [ ]
@@ -159,6 +160,8 @@ export default class HLSHandler extends PassThrough {
159160 if ( seg . sequence !== - 1 && seg . sequence > this . highestSequence ) this . highestSequence = seg . sequence
160161 }
161162 this . justResynced = false
163+ } else {
164+ this . justResynced = false
162165 }
163166
164167 const newSegments = parsed . segments . filter ( s => {
@@ -209,45 +212,64 @@ export default class HLSHandler extends PassThrough {
209212 }
210213 }
211214
215+ async _fetchWithRetry ( segment , attempt = 1 ) {
216+ try {
217+ if ( this . strategy === 'segmented' ) {
218+ const data = await this . fetcher . fetchSegment ( segment , { stream : false } )
219+ return { segment, data }
220+ }
221+ const stream = await this . fetcher . fetchSegment ( segment , { stream : true } )
222+ return { segment, stream }
223+ } catch ( err ) {
224+ if ( this . stop ) return null
225+ const isRecoverable = err . message === 'aborted' || err . code === 'ECONNRESET' || err . code === 'ETIMEDOUT'
226+ if ( isRecoverable && attempt <= 3 ) {
227+ const delay = Math . pow ( 2 , attempt ) * 500
228+ logger ( 'warn' , 'HLSHandler' , `Segment fetch failed (attempt ${ attempt } /3): ${ err . message } . Retrying in ${ delay } ms...` )
229+ await new Promise ( r => setTimeout ( r , delay ) )
230+ return this . _fetchWithRetry ( segment , attempt + 1 )
231+ }
232+ logger ( 'error' , 'HLSHandler' , `Segment fetch permanently failed ${ segment . sequence } : ${ err . message } ` )
233+ return null
234+ }
235+ }
236+
212237 async _fetchSegments ( ) {
213238 if ( this . isFetching || this . stop ) return
214239 this . isFetching = true
215240
216- let nextSegmentPromise = null
241+ const fetchPool = new Map ( )
217242
218- const getSegment = async ( seg ) => {
219- try {
220- if ( this . strategy === 'segmented' ) {
221- return { segment : seg , data : await this . fetcher . fetchSegment ( seg , { stream : false } ) }
222- }
223- return { segment : seg , stream : await this . fetcher . fetchSegment ( seg , { stream : true } ) }
224- } catch ( err ) {
225- logger ( 'error' , 'HLSHandler' , `Segment fetch error ${ seg . sequence } : ${ err . message } ` )
226- return null
243+ const fillPool = ( ) => {
244+ while ( fetchPool . size < this . MAX_PARALLEL_FETCHES && this . segmentQueue . length > 0 ) {
245+ const seg = this . segmentQueue . shift ( )
246+ const key = seg . sequence !== - 1 ? seg . sequence : seg . url
247+ fetchPool . set ( key , this . _fetchWithRetry ( seg ) )
227248 }
228249 }
229250
230- while ( ( this . segmentQueue . length > 0 || nextSegmentPromise ) && ! this . stop ) {
231- if ( this . isLive && this . segmentQueue . length < 3 && ! nextSegmentPromise ) {
232- await new Promise ( r => setTimeout ( r , 500 ) )
233- if ( this . segmentQueue . length === 0 ) break
234- }
251+ while ( ( this . segmentQueue . length > 0 || fetchPool . size > 0 ) && ! this . stop ) {
252+ fillPool ( )
235253
236- let current
237- if ( nextSegmentPromise ) {
238- current = await nextSegmentPromise
239- nextSegmentPromise = null
240- } else {
241- const seg = this . segmentQueue . shift ( )
242- current = await getSegment ( seg )
254+ if ( this . isLive && fetchPool . size === 0 && this . segmentQueue . length === 0 && ! this . preRolled ) {
255+ await new Promise ( r => setTimeout ( r , 500 ) )
256+ if ( this . segmentQueue . length === 0 && fetchPool . size === 0 ) break
257+ continue
243258 }
244259
245- if ( ! current ) continue
260+ if ( fetchPool . size === 0 ) break
246261
247- if ( this . segmentQueue . length > 0 && ! this . stop ) {
248- nextSegmentPromise = getSegment ( this . segmentQueue . shift ( ) )
262+ const [ key , promise ] = fetchPool . entries ( ) . next ( ) . value
263+ fetchPool . delete ( key )
264+
265+ const current = await promise
266+ if ( ! current ) {
267+ logger ( 'warn' , 'HLSHandler' , `Skipping failed segment: ${ key } ` )
268+ continue
249269 }
250270
271+ this . preRolled = true
272+
251273 try {
252274 const { segment, data, stream } = current
253275 if ( segment . map && segment . map . uri !== this . lastMapUri ) {
@@ -263,20 +285,20 @@ export default class HLSHandler extends PassThrough {
263285 if ( ! this . write ( data ) ) await new Promise ( r => this . once ( 'drain' , r ) )
264286 }
265287 } else if ( stream ) {
266- this . activeSegmentStream = stream
288+ this . activeSegmentStreams . set ( key , stream )
267289 for await ( const chunk of stream ) {
268290 if ( this . stop ) break
269291 if ( ! this . write ( chunk ) ) await new Promise ( r => this . once ( 'drain' , r ) )
270292 }
271- this . activeSegmentStream = null
293+ this . activeSegmentStreams . delete ( key )
272294 }
273295 } catch ( err ) {
274296 logger ( 'error' , 'HLSHandler' , `Segment processing error: ${ err . message } ` )
275297 }
276298 }
277299
278300 this . isFetching = false
279- if ( ! this . isLive && this . segmentQueue . length === 0 && ! this . stop ) {
301+ if ( ! this . isLive && this . segmentQueue . length === 0 && fetchPool . size === 0 && ! this . stop ) {
280302 this . emit ( 'finishBuffering' )
281303 this . end ( )
282304 }
0 commit comments