1+ import { PassThrough } from 'node:stream'
2+ import { http1makeRequest , logger } from '../../utils.js'
3+ import PlaylistParser from './PlaylistParser.js'
4+ import SegmentFetcher from './SegmentFetcher.js'
5+
6+ export default class HLSHandler extends PassThrough {
7+ constructor ( url , options = { } ) {
8+ super ( { highWaterMark : options . highWaterMark || 1024 * 1024 * 2 } ) // 2MB default
9+
10+ this . currentUrl = url
11+ this . options = options
12+ this . headers = options . headers || { }
13+ this . localAddress = options . localAddress || null
14+ this . chunkReadahead = options . chunkReadahead || 3
15+ this . liveBuffer = options . liveBuffer || 20000
16+
17+ this . fetcher = new SegmentFetcher ( {
18+ headers : this . headers ,
19+ localAddress : this . localAddress
20+ } )
21+
22+ this . processedSegments = new Set ( )
23+ this . processedOrder = [ ]
24+ this . MAX_HISTORY = 100
25+
26+ this . segmentQueue = [ ]
27+ this . isFetching = false
28+ this . stop = false
29+ this . lastMapUri = null
30+ this . isLive = false
31+ this . playlistTimer = null
32+
33+ this . on ( 'close' , ( ) => { this . destroy ( ) } )
34+ this . on ( 'error' , ( ) => { this . destroy ( ) } )
35+
36+ this . _start ( )
37+ }
38+
39+ async _start ( ) {
40+ await this . _playlistLoop ( )
41+ }
42+
43+ destroy ( err ) {
44+ if ( this . stop ) return
45+ this . stop = true
46+
47+ if ( this . playlistTimer ) {
48+ clearTimeout ( this . playlistTimer )
49+ this . playlistTimer = null
50+ }
51+
52+ this . segmentQueue = [ ]
53+ this . processedSegments . clear ( )
54+ this . processedOrder = [ ]
55+
56+ super . destroy ( err )
57+ }
58+
59+ _rememberSegment ( url ) {
60+ if ( this . processedSegments . has ( url ) ) return false
61+ this . processedSegments . add ( url )
62+ this . processedOrder . push ( url )
63+ if ( this . processedOrder . length > this . MAX_HISTORY ) {
64+ this . processedSegments . delete ( this . processedOrder . shift ( ) )
65+ }
66+ return true
67+ }
68+
69+ async _playlistLoop ( ) {
70+ if ( this . stop ) return
71+
72+ try {
73+ const { body : playlistContent , error, statusCode } = await http1makeRequest ( this . currentUrl , {
74+ headers : this . headers ,
75+ method : 'GET' ,
76+ localAddress : this . localAddress
77+ } )
78+
79+ if ( error || statusCode !== 200 ) {
80+ throw new Error ( `Failed to fetch playlist: ${ statusCode } ` )
81+ }
82+
83+ const parsed = PlaylistParser . parse ( playlistContent , this . currentUrl )
84+
85+ if ( parsed . isMaster ) {
86+ this . currentUrl = parsed . variants [ 0 ] . url
87+ return setImmediate ( ( ) => this . _playlistLoop ( ) )
88+ }
89+
90+ const isFirstLoad = ! this . isLive && this . processedSegments . size === 0
91+ this . isLive = parsed . isLive
92+
93+ let segmentsToAdd = parsed . segments
94+
95+ if ( isFirstLoad && this . isLive ) {
96+ const segmentsToSkip = Math . max ( 0 , parsed . segments . length - this . chunkReadahead )
97+ segmentsToAdd = parsed . segments . slice ( segmentsToSkip )
98+
99+ for ( let i = 0 ; i < segmentsToSkip ; i ++ ) {
100+ this . processedSegments . add ( parsed . segments [ i ] . url )
101+ this . processedOrder . push ( parsed . segments [ i ] . url )
102+ }
103+ }
104+
105+ for ( const segment of segmentsToAdd ) {
106+ if ( this . _rememberSegment ( segment . url ) ) {
107+ this . segmentQueue . push ( segment )
108+ }
109+ }
110+
111+ if ( this . segmentQueue . length > 0 && ! this . isFetching ) {
112+ this . _fetchSegments ( )
113+ }
114+
115+ if ( this . isLive && ! playlistContent . includes ( '#EXT-X-ENDLIST' ) ) {
116+ this . playlistTimer = setTimeout ( ( ) => this . _playlistLoop ( ) , parsed . targetDuration * 1000 )
117+ }
118+ } catch ( err ) {
119+ logger ( 'error' , 'HLSHandler' , `Playlist error: ${ err . message } ` )
120+ if ( ! this . isLive ) return this . destroy ( err )
121+
122+ this . playlistTimer = setTimeout ( ( ) => this . _playlistLoop ( ) , 5000 )
123+ }
124+ }
125+
126+ async _fetchSegments ( ) {
127+ if ( this . isFetching || this . stop ) return
128+ this . isFetching = true
129+
130+ while ( this . segmentQueue . length > 0 && ! this . stop ) {
131+ if ( this . writableLength >= this . writableHighWaterMark ) {
132+ await new Promise ( ( resolve ) => this . once ( 'drain' , resolve ) )
133+ if ( this . stop ) break
134+ }
135+
136+ const segment = this . segmentQueue . shift ( )
137+
138+ try {
139+ if ( segment . map && segment . map . uri !== this . lastMapUri ) {
140+ const mapData = await this . fetcher . fetchMap ( segment . map )
141+ if ( mapData && ! this . stop ) {
142+ this . write ( mapData )
143+ this . lastMapUri = segment . map . uri
144+ }
145+ }
146+
147+ const data = await this . fetcher . fetchSegment ( segment )
148+
149+ if ( ! this . stop ) {
150+ this . write ( data )
151+ }
152+ } catch ( err ) {
153+ logger ( 'error' , 'HLSHandler' , `Segment error ${ segment . sequence } : ${ err . message } ` )
154+ }
155+ }
156+
157+ this . isFetching = false
158+
159+ if ( ! this . isLive && this . segmentQueue . length === 0 && ! this . stop ) {
160+ this . emit ( 'finishBuffering' )
161+ this . end ( )
162+ }
163+ }
164+ }
0 commit comments