@@ -7,26 +7,37 @@ export default class HLSHandler extends PassThrough {
77 constructor ( url , options = { } ) {
88 super ( { highWaterMark : options . highWaterMark || 1024 * 1024 * 5 } )
99
10+ this . masterUrl = url
1011 this . currentUrl = url
1112 this . headers = options . headers || { }
1213 this . localAddress = options . localAddress || null
14+ this . onResolveUrl = options . onResolveUrl || null
1315 this . strategy = options . type ?. includes ( 'fmp4' ) ? 'segmented' : 'streaming'
1416
1517 this . fetcher = new SegmentFetcher ( {
1618 headers : this . headers ,
17- localAddress : this . localAddress
19+ localAddress : this . localAddress ,
20+ onResolveUrl : this . onResolveUrl
1821 } )
1922
2023 this . processedSegments = new Set ( )
2124 this . processedOrder = [ ]
22- this . MAX_HISTORY = 100
25+ this . MAX_HISTORY = 200
2326 this . segmentQueue = [ ]
2427 this . isFetching = false
2528 this . stop = false
2629 this . lastMapUri = null
2730 this . isLive = false
2831 this . playlistTimer = null
2932 this . activeSegmentStream = null
33+ this . lastMediaSequence = - 1
34+ this . highestSequence = - 1
35+ this . maxGap = 30
36+ this . stuckCount = 0
37+ this . preRolled = false
38+ this . justResynced = false
39+ this . masterRefreshCounter = 0
40+ this . MASTER_REFRESH_INTERVAL = 3
3041
3142 this . on ( 'error' , ( err ) => { this . destroy ( err ) } )
3243 this . _start ( )
@@ -50,13 +61,15 @@ export default class HLSHandler extends PassThrough {
5061 this . segmentQueue = [ ]
5162 this . processedSegments . clear ( )
5263 this . processedOrder = [ ]
64+ this . lastMediaSequence = - 1
65+ this . highestSequence = - 1
5366 if ( ! this . destroyed ) super . destroy ( err )
5467 }
5568
56- _rememberSegment ( url ) {
57- if ( this . processedSegments . has ( url ) ) return false
58- this . processedSegments . add ( url )
59- this . processedOrder . push ( url )
69+ _rememberSegment ( key ) {
70+ if ( this . processedSegments . has ( key ) ) return false
71+ this . processedSegments . add ( key )
72+ this . processedOrder . push ( key )
6073 if ( this . processedOrder . length > this . MAX_HISTORY ) {
6174 this . processedSegments . delete ( this . processedOrder . shift ( ) )
6275 }
@@ -69,50 +82,174 @@ export default class HLSHandler extends PassThrough {
6982 const { body : playlistContent , error, statusCode } = await http1makeRequest ( this . currentUrl , {
7083 headers : this . headers , method : 'GET' , localAddress : this . localAddress
7184 } )
72- if ( error || statusCode !== 200 ) throw new Error ( `Playlist fetch failed: ${ statusCode } ` )
7385
74- const parsed = PlaylistParser . parse ( playlistContent , this . currentUrl )
86+ if ( error || statusCode !== 200 ) {
87+ if ( statusCode === 403 || statusCode === 410 ) {
88+ if ( this . currentUrl !== this . masterUrl ) {
89+ this . currentUrl = this . masterUrl
90+ this . justResynced = true
91+ return setImmediate ( ( ) => this . _playlistLoop ( ) )
92+ }
93+ }
94+ throw new Error ( `Playlist fetch failed: ${ statusCode } ` )
95+ }
96+
97+ let parsed
98+ try {
99+ parsed = PlaylistParser . parse ( playlistContent , this . currentUrl )
100+ } catch ( e ) {
101+ if ( this . currentUrl !== this . masterUrl ) {
102+ this . currentUrl = this . masterUrl
103+ this . justResynced = true
104+ return setImmediate ( ( ) => this . _playlistLoop ( ) )
105+ }
106+ throw e
107+ }
108+
75109 if ( parsed . isMaster ) {
76- this . currentUrl = parsed . variants [ 0 ] . url
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 } ` )
121+ this . currentUrl = bestVariant . url
77122 return setImmediate ( ( ) => this . _playlistLoop ( ) )
78123 }
79124
80- const isFirstLoad = ! this . isLive && this . processedSegments . size === 0
81125 this . isLive = parsed . isLive
82126
83- if ( isFirstLoad && this . isLive ) {
84- const toSkip = Math . max ( 0 , parsed . segments . length - 3 )
85- for ( let i = 0 ; i < toSkip ; i ++ ) {
86- this . processedSegments . add ( parsed . segments [ i ] . url )
87- this . processedOrder . push ( parsed . segments [ i ] . url )
127+ 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
135+ }
136+ this . lastMediaSequence = parsed . mediaSequence
137+
138+ if ( this . isLive && ++ this . masterRefreshCounter >= this . MASTER_REFRESH_INTERVAL ) {
139+ this . masterRefreshCounter = 0
140+ this . currentUrl = this . masterUrl
141+ return setImmediate ( ( ) => this . _playlistLoop ( ) )
142+ }
143+
144+ const isFirstLoad = this . processedSegments . size === 0
145+ if ( ( isFirstLoad || this . justResynced ) && this . isLive ) {
146+ if ( this . justResynced ) {
147+ this . processedSegments . clear ( )
148+ this . processedOrder = [ ]
149+ this . highestSequence = - 1
150+ }
151+
152+ const toTake = 12
153+ const startIdx = Math . max ( 0 , parsed . segments . length - toTake )
154+ for ( let i = 0 ; i < startIdx ; i ++ ) {
155+ const seg = parsed . segments [ i ]
156+ const key = seg . sequence !== - 1 ? seg . sequence : seg . url
157+ this . processedSegments . add ( key )
158+ this . processedOrder . push ( key )
159+ if ( seg . sequence !== - 1 && seg . sequence > this . highestSequence ) this . highestSequence = seg . sequence
88160 }
161+ this . justResynced = false
89162 }
90163
91- const newSegments = parsed . segments . filter ( s => ! this . processedSegments . has ( s . url ) )
92- for ( const segment of newSegments ) {
93- this . _rememberSegment ( segment . url )
94- this . segmentQueue . push ( segment )
164+ const newSegments = parsed . segments . filter ( s => {
165+ if ( s . sequence !== - 1 && s . sequence <= this . highestSequence ) return false
166+ const key = s . sequence !== - 1 ? s . sequence : s . url
167+ return ! this . processedSegments . has ( key )
168+ } )
169+
170+ if ( newSegments . length > 0 ) {
171+ this . stuckCount = 0
172+ for ( const segment of newSegments ) {
173+ if ( segment . discontinuity && this . isLive ) {
174+ logger ( 'debug' , 'HLSHandler' , 'Discontinuity detected in segment. Clearing queue and re-syncing.' )
175+ this . segmentQueue = [ ]
176+ this . processedSegments . clear ( )
177+ this . processedOrder = [ ]
178+ this . highestSequence = - 1
179+ this . preRolled = false
180+ this . justResynced = true
181+ return setImmediate ( ( ) => this . _playlistLoop ( ) )
182+ }
183+
184+ const key = segment . sequence !== - 1 ? segment . sequence : segment . url
185+ this . _rememberSegment ( key )
186+ this . segmentQueue . push ( segment )
187+ if ( segment . sequence !== - 1 && segment . sequence > this . highestSequence ) this . highestSequence = segment . sequence
188+ }
189+ } else if ( this . isLive ) {
190+ if ( ++ this . stuckCount >= 10 ) {
191+ logger ( 'warn' , 'HLSHandler' , 'No new segments for 10 reloads. Refreshing master playlist.' )
192+ this . stuckCount = 0
193+ this . currentUrl = this . masterUrl
194+ this . justResynced = true
195+ return setImmediate ( ( ) => this . _playlistLoop ( ) )
196+ }
95197 }
96198
97- if ( this . segmentQueue . length > 0 && ! this . isFetching ) this . _fetchSegments ( )
199+ if ( this . segmentQueue . length > 0 ) {
200+ if ( ! this . isFetching ) this . _fetchSegments ( )
201+ }
98202
99203 if ( this . isLive && ! playlistContent . includes ( '#EXT-X-ENDLIST' ) ) {
100- const delay = Math . max ( 1 , parsed . targetDuration ) * 1000
101- this . playlistTimer = setTimeout ( ( ) => this . _playlistLoop ( ) , delay )
204+ this . _scheduleNextTick ( parsed . targetDuration )
102205 }
103206 } catch ( err ) {
104207 if ( ! this . isLive ) return this . destroy ( err )
105- this . playlistTimer = setTimeout ( ( ) => this . _playlistLoop ( ) , 5000 )
208+ this . playlistTimer = setTimeout ( ( ) => this . _playlistLoop ( ) , 3000 )
106209 }
107210 }
108211
109212 async _fetchSegments ( ) {
110213 if ( this . isFetching || this . stop ) return
111214 this . isFetching = true
112215
113- while ( this . segmentQueue . length > 0 && ! this . stop ) {
114- const segment = this . segmentQueue . shift ( )
216+ let nextSegmentPromise = null
217+
218+ const getSegment = async ( seg ) => {
115219 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
227+ }
228+ }
229+
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+ }
235+
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 )
243+ }
244+
245+ if ( ! current ) continue
246+
247+ if ( this . segmentQueue . length > 0 && ! this . stop ) {
248+ nextSegmentPromise = getSegment ( this . segmentQueue . shift ( ) )
249+ }
250+
251+ try {
252+ const { segment, data, stream } = current
116253 if ( segment . map && segment . map . uri !== this . lastMapUri ) {
117254 const mapData = await this . fetcher . fetchMap ( segment . map , segment . key )
118255 if ( mapData && ! this . stop ) {
@@ -122,22 +259,19 @@ export default class HLSHandler extends PassThrough {
122259 }
123260
124261 if ( this . strategy === 'segmented' ) {
125- const data = await this . fetcher . fetchSegment ( segment , { stream : false } )
126- if ( ! this . stop ) {
262+ if ( ! this . stop && data ) {
127263 if ( ! this . write ( data ) ) await new Promise ( r => this . once ( 'drain' , r ) )
128264 }
129- } else {
130- const segmentStream = await this . fetcher . fetchSegment ( segment , { stream : true } )
131- this . activeSegmentStream = segmentStream
132-
133- for await ( const chunk of segmentStream ) {
265+ } else if ( stream ) {
266+ this . activeSegmentStream = stream
267+ for await ( const chunk of stream ) {
134268 if ( this . stop ) break
135269 if ( ! this . write ( chunk ) ) await new Promise ( r => this . once ( 'drain' , r ) )
136270 }
137271 this . activeSegmentStream = null
138272 }
139273 } catch ( err ) {
140- logger ( 'error' , 'HLSHandler' , `Segment error ${ segment . sequence } : ${ err . message } ` )
274+ logger ( 'error' , 'HLSHandler' , `Segment processing error : ${ err . message } ` )
141275 }
142276 }
143277
@@ -147,4 +281,10 @@ export default class HLSHandler extends PassThrough {
147281 this . end ( )
148282 }
149283 }
284+
285+ _scheduleNextTick ( targetDuration ) {
286+ if ( this . stop ) return
287+ const delay = Math . max ( 0.5 , targetDuration / 2 ) * 1000
288+ this . playlistTimer = setTimeout ( ( ) => this . _playlistLoop ( ) , delay )
289+ }
150290}
0 commit comments