@warren-bank/hls-proxy 3.1.2 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -2
- package/hls-proxy/cookies.js +17 -0
- package/hls-proxy/manifest_parser.js +489 -489
- package/hls-proxy/proxy.js +5 -1
- package/hls-proxy/segment_cache.js +2 -1
- package/hls-proxy/timers.js +44 -0
- package/hls-proxy/utils.js +157 -157
- package/package.json +3 -2
|
@@ -1,489 +1,489 @@
|
|
|
1
|
-
const {URL} = require('@warren-bank/url')
|
|
2
|
-
const utils = require('./utils')
|
|
3
|
-
|
|
4
|
-
const regexs = {
|
|
5
|
-
vod_start_at: /#vod_start(?:_prefetch_at)?=((?:\d+:)?(?:\d+:)?\d+)$/i,
|
|
6
|
-
m3u8_line_separator: /\s*[\r\n]+\s*/,
|
|
7
|
-
m3u8_line_landmark: /^(#[^:]+[:]?)/,
|
|
8
|
-
m3u8_line_url: /URI=["']([^"']+)["']/id
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const url_location_landmarks = {
|
|
12
|
-
m3u8: {
|
|
13
|
-
same_line: [
|
|
14
|
-
'#EXT-X-MEDIA:',
|
|
15
|
-
'#EXT-X-I-FRAME-STREAM-INF:',
|
|
16
|
-
'#EXT-X-RENDITION-REPORT:',
|
|
17
|
-
'#EXT-X-DATERANGE:',
|
|
18
|
-
'#EXT-X-CONTENT-STEERING:'
|
|
19
|
-
],
|
|
20
|
-
next_line: [
|
|
21
|
-
'#EXT-X-STREAM-INF:'
|
|
22
|
-
]
|
|
23
|
-
},
|
|
24
|
-
ts: {
|
|
25
|
-
same_line: [
|
|
26
|
-
'#EXT-X-MAP:',
|
|
27
|
-
'#EXT-X-PART:',
|
|
28
|
-
'#EXT-X-PRELOAD-HINT:'
|
|
29
|
-
],
|
|
30
|
-
next_line: [
|
|
31
|
-
'#EXTINF:'
|
|
32
|
-
]
|
|
33
|
-
},
|
|
34
|
-
json: {
|
|
35
|
-
same_line: [
|
|
36
|
-
'#EXT-X-SESSION-DATA:'
|
|
37
|
-
],
|
|
38
|
-
next_line: []
|
|
39
|
-
},
|
|
40
|
-
key: {
|
|
41
|
-
same_line: [
|
|
42
|
-
'#EXT-X-KEY:'
|
|
43
|
-
],
|
|
44
|
-
next_line: []
|
|
45
|
-
},
|
|
46
|
-
other: {
|
|
47
|
-
same_line: [],
|
|
48
|
-
next_line: []
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const meta_data_location_landmarks = {
|
|
53
|
-
is_vod: {
|
|
54
|
-
same_line: [
|
|
55
|
-
'#EXT-X-PLAYLIST-TYPE:',
|
|
56
|
-
'#EXT-X-ENDLIST'
|
|
57
|
-
],
|
|
58
|
-
resolve_value: {
|
|
59
|
-
'#EXT-X-PLAYLIST-TYPE:': (m3u8_line, landmark) => {
|
|
60
|
-
const value = m3u8_line.substring(landmark.length, landmark.length + 3)
|
|
61
|
-
return (value.toUpperCase() === 'VOD')
|
|
62
|
-
},
|
|
63
|
-
'#EXT-X-ENDLIST': () => true
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
seg_duration_ms: {
|
|
67
|
-
same_line: [
|
|
68
|
-
'#EXT-X-TARGETDURATION:'
|
|
69
|
-
],
|
|
70
|
-
resolve_value: {
|
|
71
|
-
'#EXT-X-TARGETDURATION:': (m3u8_line, landmark) => {
|
|
72
|
-
m3u8_line = m3u8_line.substring(landmark.length)
|
|
73
|
-
const value = parseInt(m3u8_line, 10)
|
|
74
|
-
return isNaN(value)
|
|
75
|
-
? null
|
|
76
|
-
: (value * 1000) // convert seconds to ms
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const get_vod_start_at_ms = function(m3u8_url) {
|
|
83
|
-
try {
|
|
84
|
-
const matches = regexs.vod_start_at.exec(m3u8_url)
|
|
85
|
-
|
|
86
|
-
if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
|
|
87
|
-
throw ''
|
|
88
|
-
|
|
89
|
-
let offset
|
|
90
|
-
offset = matches[1]
|
|
91
|
-
offset = parse_HHMMSS_to_seconds(offset)
|
|
92
|
-
offset = offset * 1000 // convert seconds to ms
|
|
93
|
-
|
|
94
|
-
return offset
|
|
95
|
-
}
|
|
96
|
-
catch(e) {
|
|
97
|
-
const def_offset = null
|
|
98
|
-
|
|
99
|
-
return def_offset
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const parse_HHMMSS_to_seconds = function(str) {
|
|
104
|
-
const parts = str.split(':')
|
|
105
|
-
let seconds = 0
|
|
106
|
-
let multiplier = 1
|
|
107
|
-
|
|
108
|
-
while (parts.length > 0) {
|
|
109
|
-
seconds += multiplier * parseInt(parts.pop(), 10)
|
|
110
|
-
multiplier *= 60
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return seconds
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// returns: {
|
|
117
|
-
// meta_data: {is_vod, seg_duration_ms},
|
|
118
|
-
// embedded_urls: [{line_index, url_indices, url_type, original_match_url, resolved_match_url, redirected_url, unencoded_url, encoded_url, referer_url}],
|
|
119
|
-
// prefetch_urls: [],
|
|
120
|
-
// modified_m3u8: ''
|
|
121
|
-
// }
|
|
122
|
-
const parse_manifest = function(m3u8_content, m3u8_url, referer_url, hooks, cache_segments, debug, vod_start_at_ms, redirected_base_url, should_prefetch_url) {
|
|
123
|
-
const m3u8_lines = m3u8_content.split(regexs.m3u8_line_separator)
|
|
124
|
-
m3u8_content = null
|
|
125
|
-
|
|
126
|
-
const meta_data = {}
|
|
127
|
-
const embedded_urls = extract_embedded_urls(m3u8_lines, m3u8_url, referer_url, (cache_segments ? meta_data : null))
|
|
128
|
-
const prefetch_urls = []
|
|
129
|
-
|
|
130
|
-
if (embedded_urls && Array.isArray(embedded_urls) && embedded_urls.length) {
|
|
131
|
-
embedded_urls.forEach(embedded_url => {
|
|
132
|
-
redirect_embedded_url(embedded_url, hooks, m3u8_url, debug)
|
|
133
|
-
finalize_embedded_url(embedded_url, vod_start_at_ms, debug)
|
|
134
|
-
encode_embedded_url(embedded_url, redirected_base_url, debug)
|
|
135
|
-
get_prefetch_url(embedded_url, should_prefetch_url, prefetch_urls)
|
|
136
|
-
modify_m3u8_line(embedded_url, m3u8_lines)
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
meta_data,
|
|
142
|
-
embedded_urls,
|
|
143
|
-
prefetch_urls,
|
|
144
|
-
modified_m3u8: m3u8_lines.filter(line => !!line).join("\n")
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const extract_embedded_urls = function(m3u8_lines, m3u8_url, referer_url, meta_data) {
|
|
149
|
-
const embedded_urls = []
|
|
150
|
-
|
|
151
|
-
let m3u8_line, has_next_m3u8_line, next_m3u8_line, matches, matching_landmark, matching_url
|
|
152
|
-
|
|
153
|
-
for (let i=0; i < m3u8_lines.length; i++) {
|
|
154
|
-
m3u8_line = m3u8_lines[i]
|
|
155
|
-
has_next_m3u8_line = ((i+1) < m3u8_lines.length)
|
|
156
|
-
|
|
157
|
-
matches = regexs.m3u8_line_landmark.exec(m3u8_line)
|
|
158
|
-
if (!matches) continue
|
|
159
|
-
matching_landmark = matches[1]
|
|
160
|
-
|
|
161
|
-
matches = regexs.m3u8_line_url.exec(m3u8_line)
|
|
162
|
-
matching_url = matches
|
|
163
|
-
? matches[1]
|
|
164
|
-
: null
|
|
165
|
-
|
|
166
|
-
if (meta_data !== null)
|
|
167
|
-
extract_meta_data(meta_data, m3u8_line, matching_landmark)
|
|
168
|
-
|
|
169
|
-
for (let url_type in url_location_landmarks) {
|
|
170
|
-
if (matching_url && (url_location_landmarks[url_type]['same_line'].indexOf(matching_landmark) >= 0)) {
|
|
171
|
-
embedded_urls.push({
|
|
172
|
-
line_index: i,
|
|
173
|
-
url_indices: matches.indices[1],
|
|
174
|
-
url_type: url_type,
|
|
175
|
-
original_match_url: matching_url,
|
|
176
|
-
resolved_match_url: (new URL(matching_url, m3u8_url)).href,
|
|
177
|
-
redirected_url: null,
|
|
178
|
-
referer_url: referer_url,
|
|
179
|
-
encoded_url: null
|
|
180
|
-
})
|
|
181
|
-
break
|
|
182
|
-
}
|
|
183
|
-
if (has_next_m3u8_line && (url_location_landmarks[url_type]['next_line'].indexOf(matching_landmark) >= 0)) {
|
|
184
|
-
next_m3u8_line = m3u8_lines[i+1].trim()
|
|
185
|
-
|
|
186
|
-
if (next_m3u8_line && (next_m3u8_line[0] !== '#')) {
|
|
187
|
-
i++
|
|
188
|
-
|
|
189
|
-
embedded_urls.push({
|
|
190
|
-
line_index: i,
|
|
191
|
-
url_indices: null,
|
|
192
|
-
url_type: url_type,
|
|
193
|
-
original_match_url: next_m3u8_line,
|
|
194
|
-
resolved_match_url: (new URL(next_m3u8_line, m3u8_url)).href,
|
|
195
|
-
redirected_url: null,
|
|
196
|
-
referer_url: referer_url,
|
|
197
|
-
encoded_url: null
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
break
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return embedded_urls
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const extract_meta_data = function(meta_data, m3u8_line, matching_landmark) {
|
|
209
|
-
for (let meta_data_key in meta_data_location_landmarks) {
|
|
210
|
-
if (meta_data_location_landmarks[meta_data_key]['same_line'].indexOf(matching_landmark) >= 0) {
|
|
211
|
-
const func = meta_data_location_landmarks[meta_data_key]['resolve_value'][matching_landmark]
|
|
212
|
-
if (typeof func === 'function') {
|
|
213
|
-
const meta_data_value = func(m3u8_line, matching_landmark)
|
|
214
|
-
if ((meta_data_value !== undefined) && (meta_data_value !== null)) {
|
|
215
|
-
meta_data[meta_data_key] = meta_data_value
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const redirect_embedded_url = function(embedded_url, hooks, m3u8_url, debug) {
|
|
223
|
-
if (hooks && (hooks instanceof Object) && hooks.redirect && (typeof hooks.redirect === 'function')) {
|
|
224
|
-
let url, url_type, referer_url, result
|
|
225
|
-
|
|
226
|
-
url = embedded_url.resolved_match_url
|
|
227
|
-
url_type = null
|
|
228
|
-
referer_url = null
|
|
229
|
-
|
|
230
|
-
debug(3, 'redirecting (pre-hook):', url)
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
result = hooks.redirect(url, embedded_url.referer_url)
|
|
234
|
-
|
|
235
|
-
if (result) {
|
|
236
|
-
if (typeof result === 'string') {
|
|
237
|
-
url = result
|
|
238
|
-
}
|
|
239
|
-
else if (result instanceof Object) {
|
|
240
|
-
url = result.url
|
|
241
|
-
url_type = result.url_type
|
|
242
|
-
referer_url = result.referer_url
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (typeof url !== 'string') throw new Error('bad return value')
|
|
247
|
-
|
|
248
|
-
url = url.trim()
|
|
249
|
-
|
|
250
|
-
if (url.length && (url.toLowerCase().indexOf('http') !== 0))
|
|
251
|
-
url = (new URL(url, m3u8_url)).href
|
|
252
|
-
}
|
|
253
|
-
catch(e) {
|
|
254
|
-
url = ''
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (url) {
|
|
258
|
-
embedded_url.redirected_url = url
|
|
259
|
-
|
|
260
|
-
if (typeof url_type === 'string') {
|
|
261
|
-
url_type = url_type.toLowerCase().trim()
|
|
262
|
-
|
|
263
|
-
if (url_type.length)
|
|
264
|
-
embedded_url.url_type = url_type
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (typeof referer_url === 'string') {
|
|
268
|
-
referer_url = referer_url.trim()
|
|
269
|
-
|
|
270
|
-
if (referer_url.length && (referer_url.toLowerCase().indexOf('http') === 0))
|
|
271
|
-
embedded_url.referer_url = referer_url
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
debug(3, 'redirecting (post-hook):', url)
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
embedded_url.redirected_url = ''
|
|
278
|
-
|
|
279
|
-
debug(3, 'redirecting (post-hook):', 'URL filtered, removed from manifest')
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const finalize_embedded_url = function(embedded_url, vod_start_at_ms, debug) {
|
|
285
|
-
if (embedded_url.redirected_url === '') {
|
|
286
|
-
embedded_url.unencoded_url = ''
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (embedded_url.url_type)
|
|
292
|
-
debug(3, 'url type:', embedded_url.url_type)
|
|
293
|
-
|
|
294
|
-
debug(2, 'redirecting:', url)
|
|
295
|
-
|
|
296
|
-
if (vod_start_at_ms && (embedded_url.url_type === 'm3u8'))
|
|
297
|
-
url += `#vod_start=${Math.floor(vod_start_at_ms/1000)}`
|
|
298
|
-
|
|
299
|
-
if (embedded_url.referer_url)
|
|
300
|
-
url += `|${embedded_url.referer_url}`
|
|
301
|
-
|
|
302
|
-
embedded_url.unencoded_url = url
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const encode_embedded_url = function(embedded_url, redirected_base_url, debug) {
|
|
307
|
-
embedded_url.encoded_url = (embedded_url.unencoded_url)
|
|
308
|
-
? `${redirected_base_url}/${ utils.base64_encode(embedded_url.unencoded_url) }.${embedded_url.url_type || 'other'}`
|
|
309
|
-
: ''
|
|
310
|
-
|
|
311
|
-
if (embedded_url.encoded_url)
|
|
312
|
-
debug(3, 'redirecting (proxied):', embedded_url.encoded_url)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const get_prefetch_url = function(embedded_url, should_prefetch_url, prefetch_urls = []) {
|
|
316
|
-
if (embedded_url.redirected_url !== '') {
|
|
317
|
-
const url = embedded_url.redirected_url || embedded_url.resolved_match_url
|
|
318
|
-
|
|
319
|
-
if (should_prefetch_url(url, embedded_url.url_type))
|
|
320
|
-
prefetch_urls.push(url)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const modify_m3u8_line = function(embedded_url, m3u8_lines) {
|
|
325
|
-
const {line_index, url_indices, encoded_url} = embedded_url
|
|
326
|
-
|
|
327
|
-
if (url_indices && Array.isArray(url_indices) && (url_indices.length === 2)) {
|
|
328
|
-
const m3u8_line = m3u8_lines[line_index]
|
|
329
|
-
|
|
330
|
-
m3u8_lines[line_index] = m3u8_line.substring(0, url_indices[0]) + encoded_url + m3u8_line.substring(url_indices[1])
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
m3u8_lines[line_index] = encoded_url
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_url, referer_url, redirected_base_url) {
|
|
338
|
-
const {hooks, cache_segments, max_segments, debug_level} = params
|
|
339
|
-
|
|
340
|
-
const {has_cache, get_time_since_last_access, is_expired, prefetch_segment} = segment_cache
|
|
341
|
-
|
|
342
|
-
const debug = utils.debug.bind(null, params)
|
|
343
|
-
const should_prefetch_url = utils.should_prefetch_url.bind(null, params)
|
|
344
|
-
|
|
345
|
-
if (hooks && (hooks instanceof Object) && hooks.modify_m3u8_content && (typeof hooks.modify_m3u8_content === 'function')) {
|
|
346
|
-
m3u8_content = hooks.modify_m3u8_content(m3u8_content, m3u8_url) || m3u8_content
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const debug_divider = (debug_level >= 4)
|
|
350
|
-
? ('-').repeat(40)
|
|
351
|
-
: ''
|
|
352
|
-
|
|
353
|
-
if (debug_level >= 4) {
|
|
354
|
-
debug(4, 'proxied response (original m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
let is_vod, seg_duration_ms, prefetch_urls
|
|
358
|
-
|
|
359
|
-
// only used with prefetch
|
|
360
|
-
const vod_start_at_ms = (cache_segments)
|
|
361
|
-
? get_vod_start_at_ms(m3u8_url)
|
|
362
|
-
: null
|
|
363
|
-
|
|
364
|
-
// only used with prefetch
|
|
365
|
-
const perform_prefetch = (cache_segments)
|
|
366
|
-
? (urls, dont_touch_access) => {
|
|
367
|
-
if (!urls || !Array.isArray(urls) || !urls.length)
|
|
368
|
-
return
|
|
369
|
-
|
|
370
|
-
let promise
|
|
371
|
-
|
|
372
|
-
if (is_vod || has_cache(m3u8_url)) {
|
|
373
|
-
promise = Promise.resolve()
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
const matching_url = urls[0]
|
|
377
|
-
urls[0] = undefined
|
|
378
|
-
|
|
379
|
-
promise = prefetch_segment(m3u8_url, matching_url, referer_url, dont_touch_access)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
promise.then(() => {
|
|
383
|
-
urls.forEach((matching_url, index) => {
|
|
384
|
-
if (matching_url) {
|
|
385
|
-
prefetch_segment(m3u8_url, matching_url, referer_url, dont_touch_access)
|
|
386
|
-
|
|
387
|
-
urls[index] = undefined
|
|
388
|
-
}
|
|
389
|
-
})
|
|
390
|
-
})
|
|
391
|
-
}
|
|
392
|
-
: null
|
|
393
|
-
|
|
394
|
-
{
|
|
395
|
-
const parsed_manifest = parse_manifest(m3u8_content, m3u8_url, referer_url, hooks, cache_segments, debug, vod_start_at_ms, redirected_base_url, should_prefetch_url)
|
|
396
|
-
is_vod = !!parsed_manifest.meta_data.is_vod // default: false => hls live stream
|
|
397
|
-
seg_duration_ms = parsed_manifest.meta_data.seg_duration_ms || 10000 // default: 10 seconds in ms
|
|
398
|
-
prefetch_urls = parsed_manifest.prefetch_urls
|
|
399
|
-
m3u8_content = parsed_manifest.modified_m3u8
|
|
400
|
-
|
|
401
|
-
if (debug_level >= 4) {
|
|
402
|
-
debug(4, 'parsed manifest:', `\n${debug_divider}\n${JSON.stringify(parsed_manifest, null, 2)}\n${debug_divider}`)
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (prefetch_urls.length) {
|
|
407
|
-
if (is_vod && vod_start_at_ms) {
|
|
408
|
-
// full video: prevent prefetch of URLs for skipped video segments
|
|
409
|
-
|
|
410
|
-
const skip_segment_count = Math.floor(vod_start_at_ms / seg_duration_ms)
|
|
411
|
-
|
|
412
|
-
prefetch_urls.splice(0, skip_segment_count)
|
|
413
|
-
debug(3, 'prefetch (ignored):', `${skip_segment_count} URLs in m3u8 skipped to initialize vod prefetch timer from start position obtained from HLS manifest URL #hash`)
|
|
414
|
-
}
|
|
415
|
-
if (prefetch_urls.length > max_segments) {
|
|
416
|
-
if (hooks && (hooks instanceof Object) && hooks.prefetch_segments && (typeof hooks.prefetch_segments === 'function')) {
|
|
417
|
-
prefetch_urls = hooks.prefetch_segments(prefetch_urls, max_segments, is_vod, seg_duration_ms, perform_prefetch)
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
if (!is_vod) {
|
|
421
|
-
// live stream: cache from the end
|
|
422
|
-
|
|
423
|
-
const overflow = prefetch_urls.length - max_segments
|
|
424
|
-
|
|
425
|
-
prefetch_urls.splice(0, overflow)
|
|
426
|
-
debug(3, 'prefetch (ignored):', `${overflow} URLs in m3u8 skipped to prevent cache overflow`)
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
// full video: cache from the beginning w/ timer to update cache at rate of playback (assuming no pausing or seeking)
|
|
430
|
-
|
|
431
|
-
const $prefetch_urls = [...prefetch_urls]
|
|
432
|
-
const batch_size = Math.ceil(max_segments / 2)
|
|
433
|
-
const batch_time = seg_duration_ms * batch_size
|
|
434
|
-
|
|
435
|
-
const is_client_paused = () => {
|
|
436
|
-
const time_since_last_access = get_time_since_last_access(m3u8_url)
|
|
437
|
-
|
|
438
|
-
let inactivity_timeout
|
|
439
|
-
inactivity_timeout = seg_duration_ms * 2
|
|
440
|
-
inactivity_timeout = Math.floor(inactivity_timeout / 1000) // convert to seconds
|
|
441
|
-
|
|
442
|
-
return (time_since_last_access < 0)
|
|
443
|
-
? false
|
|
444
|
-
: (time_since_last_access >= inactivity_timeout)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const prefetch_next_batch = (is_cache_empty) => {
|
|
448
|
-
is_cache_empty = (is_cache_empty === true)
|
|
449
|
-
|
|
450
|
-
if (!is_cache_empty && is_expired(m3u8_url)) {
|
|
451
|
-
debug(3, 'prefetch (stopped):', 'vod stream removed from cache due to inactivity longer than timeout; prefetch has stopped')
|
|
452
|
-
return
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (!is_cache_empty && is_client_paused()) {
|
|
456
|
-
debug(3, 'prefetch (skipped):', 'vod stream is paused; prefetch will continue after client playback resumes')
|
|
457
|
-
setTimeout(prefetch_next_batch, batch_time)
|
|
458
|
-
return
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if ($prefetch_urls.length > batch_size) {
|
|
462
|
-
const batch_urls = $prefetch_urls.splice(0, batch_size)
|
|
463
|
-
|
|
464
|
-
perform_prefetch(batch_urls, !is_cache_empty)
|
|
465
|
-
setTimeout(prefetch_next_batch, is_cache_empty ? 0 : batch_time)
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
perform_prefetch($prefetch_urls, !is_cache_empty)
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
prefetch_urls = []
|
|
473
|
-
prefetch_next_batch(true)
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
perform_prefetch(prefetch_urls)
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (debug_level >= 4) {
|
|
481
|
-
debug(4, 'proxied response (modified m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
return m3u8_content
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
module.exports = {
|
|
488
|
-
modify_m3u8_content
|
|
489
|
-
}
|
|
1
|
+
const {URL} = require('@warren-bank/url')
|
|
2
|
+
const utils = require('./utils')
|
|
3
|
+
|
|
4
|
+
const regexs = {
|
|
5
|
+
vod_start_at: /#vod_start(?:_prefetch_at)?=((?:\d+:)?(?:\d+:)?\d+)$/i,
|
|
6
|
+
m3u8_line_separator: /\s*[\r\n]+\s*/,
|
|
7
|
+
m3u8_line_landmark: /^(#[^:]+[:]?)/,
|
|
8
|
+
m3u8_line_url: /URI=["']([^"']+)["']/id
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const url_location_landmarks = {
|
|
12
|
+
m3u8: {
|
|
13
|
+
same_line: [
|
|
14
|
+
'#EXT-X-MEDIA:',
|
|
15
|
+
'#EXT-X-I-FRAME-STREAM-INF:',
|
|
16
|
+
'#EXT-X-RENDITION-REPORT:',
|
|
17
|
+
'#EXT-X-DATERANGE:',
|
|
18
|
+
'#EXT-X-CONTENT-STEERING:'
|
|
19
|
+
],
|
|
20
|
+
next_line: [
|
|
21
|
+
'#EXT-X-STREAM-INF:'
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
ts: {
|
|
25
|
+
same_line: [
|
|
26
|
+
'#EXT-X-MAP:',
|
|
27
|
+
'#EXT-X-PART:',
|
|
28
|
+
'#EXT-X-PRELOAD-HINT:'
|
|
29
|
+
],
|
|
30
|
+
next_line: [
|
|
31
|
+
'#EXTINF:'
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
json: {
|
|
35
|
+
same_line: [
|
|
36
|
+
'#EXT-X-SESSION-DATA:'
|
|
37
|
+
],
|
|
38
|
+
next_line: []
|
|
39
|
+
},
|
|
40
|
+
key: {
|
|
41
|
+
same_line: [
|
|
42
|
+
'#EXT-X-KEY:'
|
|
43
|
+
],
|
|
44
|
+
next_line: []
|
|
45
|
+
},
|
|
46
|
+
other: {
|
|
47
|
+
same_line: [],
|
|
48
|
+
next_line: []
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const meta_data_location_landmarks = {
|
|
53
|
+
is_vod: {
|
|
54
|
+
same_line: [
|
|
55
|
+
'#EXT-X-PLAYLIST-TYPE:',
|
|
56
|
+
'#EXT-X-ENDLIST'
|
|
57
|
+
],
|
|
58
|
+
resolve_value: {
|
|
59
|
+
'#EXT-X-PLAYLIST-TYPE:': (m3u8_line, landmark) => {
|
|
60
|
+
const value = m3u8_line.substring(landmark.length, landmark.length + 3)
|
|
61
|
+
return (value.toUpperCase() === 'VOD')
|
|
62
|
+
},
|
|
63
|
+
'#EXT-X-ENDLIST': () => true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
seg_duration_ms: {
|
|
67
|
+
same_line: [
|
|
68
|
+
'#EXT-X-TARGETDURATION:'
|
|
69
|
+
],
|
|
70
|
+
resolve_value: {
|
|
71
|
+
'#EXT-X-TARGETDURATION:': (m3u8_line, landmark) => {
|
|
72
|
+
m3u8_line = m3u8_line.substring(landmark.length)
|
|
73
|
+
const value = parseInt(m3u8_line, 10)
|
|
74
|
+
return isNaN(value)
|
|
75
|
+
? null
|
|
76
|
+
: (value * 1000) // convert seconds to ms
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const get_vod_start_at_ms = function(m3u8_url) {
|
|
83
|
+
try {
|
|
84
|
+
const matches = regexs.vod_start_at.exec(m3u8_url)
|
|
85
|
+
|
|
86
|
+
if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
|
|
87
|
+
throw ''
|
|
88
|
+
|
|
89
|
+
let offset
|
|
90
|
+
offset = matches[1]
|
|
91
|
+
offset = parse_HHMMSS_to_seconds(offset)
|
|
92
|
+
offset = offset * 1000 // convert seconds to ms
|
|
93
|
+
|
|
94
|
+
return offset
|
|
95
|
+
}
|
|
96
|
+
catch(e) {
|
|
97
|
+
const def_offset = null
|
|
98
|
+
|
|
99
|
+
return def_offset
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parse_HHMMSS_to_seconds = function(str) {
|
|
104
|
+
const parts = str.split(':')
|
|
105
|
+
let seconds = 0
|
|
106
|
+
let multiplier = 1
|
|
107
|
+
|
|
108
|
+
while (parts.length > 0) {
|
|
109
|
+
seconds += multiplier * parseInt(parts.pop(), 10)
|
|
110
|
+
multiplier *= 60
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return seconds
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// returns: {
|
|
117
|
+
// meta_data: {is_vod, seg_duration_ms},
|
|
118
|
+
// embedded_urls: [{line_index, url_indices, url_type, original_match_url, resolved_match_url, redirected_url, unencoded_url, encoded_url, referer_url}],
|
|
119
|
+
// prefetch_urls: [],
|
|
120
|
+
// modified_m3u8: ''
|
|
121
|
+
// }
|
|
122
|
+
const parse_manifest = function(m3u8_content, m3u8_url, referer_url, hooks, cache_segments, debug, vod_start_at_ms, redirected_base_url, should_prefetch_url) {
|
|
123
|
+
const m3u8_lines = m3u8_content.split(regexs.m3u8_line_separator)
|
|
124
|
+
m3u8_content = null
|
|
125
|
+
|
|
126
|
+
const meta_data = {}
|
|
127
|
+
const embedded_urls = extract_embedded_urls(m3u8_lines, m3u8_url, referer_url, (cache_segments ? meta_data : null))
|
|
128
|
+
const prefetch_urls = []
|
|
129
|
+
|
|
130
|
+
if (embedded_urls && Array.isArray(embedded_urls) && embedded_urls.length) {
|
|
131
|
+
embedded_urls.forEach(embedded_url => {
|
|
132
|
+
redirect_embedded_url(embedded_url, hooks, m3u8_url, debug)
|
|
133
|
+
finalize_embedded_url(embedded_url, vod_start_at_ms, debug)
|
|
134
|
+
encode_embedded_url(embedded_url, redirected_base_url, debug)
|
|
135
|
+
get_prefetch_url(embedded_url, should_prefetch_url, prefetch_urls)
|
|
136
|
+
modify_m3u8_line(embedded_url, m3u8_lines)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
meta_data,
|
|
142
|
+
embedded_urls,
|
|
143
|
+
prefetch_urls,
|
|
144
|
+
modified_m3u8: m3u8_lines.filter(line => !!line).join("\n")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const extract_embedded_urls = function(m3u8_lines, m3u8_url, referer_url, meta_data) {
|
|
149
|
+
const embedded_urls = []
|
|
150
|
+
|
|
151
|
+
let m3u8_line, has_next_m3u8_line, next_m3u8_line, matches, matching_landmark, matching_url
|
|
152
|
+
|
|
153
|
+
for (let i=0; i < m3u8_lines.length; i++) {
|
|
154
|
+
m3u8_line = m3u8_lines[i]
|
|
155
|
+
has_next_m3u8_line = ((i+1) < m3u8_lines.length)
|
|
156
|
+
|
|
157
|
+
matches = regexs.m3u8_line_landmark.exec(m3u8_line)
|
|
158
|
+
if (!matches) continue
|
|
159
|
+
matching_landmark = matches[1]
|
|
160
|
+
|
|
161
|
+
matches = regexs.m3u8_line_url.exec(m3u8_line)
|
|
162
|
+
matching_url = matches
|
|
163
|
+
? matches[1]
|
|
164
|
+
: null
|
|
165
|
+
|
|
166
|
+
if (meta_data !== null)
|
|
167
|
+
extract_meta_data(meta_data, m3u8_line, matching_landmark)
|
|
168
|
+
|
|
169
|
+
for (let url_type in url_location_landmarks) {
|
|
170
|
+
if (matching_url && (url_location_landmarks[url_type]['same_line'].indexOf(matching_landmark) >= 0)) {
|
|
171
|
+
embedded_urls.push({
|
|
172
|
+
line_index: i,
|
|
173
|
+
url_indices: matches.indices[1],
|
|
174
|
+
url_type: url_type,
|
|
175
|
+
original_match_url: matching_url,
|
|
176
|
+
resolved_match_url: (new URL(matching_url, m3u8_url)).href,
|
|
177
|
+
redirected_url: null,
|
|
178
|
+
referer_url: referer_url,
|
|
179
|
+
encoded_url: null
|
|
180
|
+
})
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
if (has_next_m3u8_line && (url_location_landmarks[url_type]['next_line'].indexOf(matching_landmark) >= 0)) {
|
|
184
|
+
next_m3u8_line = m3u8_lines[i+1].trim()
|
|
185
|
+
|
|
186
|
+
if (next_m3u8_line && (next_m3u8_line[0] !== '#')) {
|
|
187
|
+
i++
|
|
188
|
+
|
|
189
|
+
embedded_urls.push({
|
|
190
|
+
line_index: i,
|
|
191
|
+
url_indices: null,
|
|
192
|
+
url_type: url_type,
|
|
193
|
+
original_match_url: next_m3u8_line,
|
|
194
|
+
resolved_match_url: (new URL(next_m3u8_line, m3u8_url)).href,
|
|
195
|
+
redirected_url: null,
|
|
196
|
+
referer_url: referer_url,
|
|
197
|
+
encoded_url: null
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return embedded_urls
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const extract_meta_data = function(meta_data, m3u8_line, matching_landmark) {
|
|
209
|
+
for (let meta_data_key in meta_data_location_landmarks) {
|
|
210
|
+
if (meta_data_location_landmarks[meta_data_key]['same_line'].indexOf(matching_landmark) >= 0) {
|
|
211
|
+
const func = meta_data_location_landmarks[meta_data_key]['resolve_value'][matching_landmark]
|
|
212
|
+
if (typeof func === 'function') {
|
|
213
|
+
const meta_data_value = func(m3u8_line, matching_landmark)
|
|
214
|
+
if ((meta_data_value !== undefined) && (meta_data_value !== null)) {
|
|
215
|
+
meta_data[meta_data_key] = meta_data_value
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const redirect_embedded_url = function(embedded_url, hooks, m3u8_url, debug) {
|
|
223
|
+
if (hooks && (hooks instanceof Object) && hooks.redirect && (typeof hooks.redirect === 'function')) {
|
|
224
|
+
let url, url_type, referer_url, result
|
|
225
|
+
|
|
226
|
+
url = embedded_url.resolved_match_url
|
|
227
|
+
url_type = null
|
|
228
|
+
referer_url = null
|
|
229
|
+
|
|
230
|
+
debug(3, 'redirecting (pre-hook):', url)
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
result = hooks.redirect(url, embedded_url.referer_url)
|
|
234
|
+
|
|
235
|
+
if (result) {
|
|
236
|
+
if (typeof result === 'string') {
|
|
237
|
+
url = result
|
|
238
|
+
}
|
|
239
|
+
else if (result instanceof Object) {
|
|
240
|
+
url = result.url
|
|
241
|
+
url_type = result.url_type
|
|
242
|
+
referer_url = result.referer_url
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (typeof url !== 'string') throw new Error('bad return value')
|
|
247
|
+
|
|
248
|
+
url = url.trim()
|
|
249
|
+
|
|
250
|
+
if (url.length && (url.toLowerCase().indexOf('http') !== 0))
|
|
251
|
+
url = (new URL(url, m3u8_url)).href
|
|
252
|
+
}
|
|
253
|
+
catch(e) {
|
|
254
|
+
url = ''
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (url) {
|
|
258
|
+
embedded_url.redirected_url = url
|
|
259
|
+
|
|
260
|
+
if (typeof url_type === 'string') {
|
|
261
|
+
url_type = url_type.toLowerCase().trim()
|
|
262
|
+
|
|
263
|
+
if (url_type.length)
|
|
264
|
+
embedded_url.url_type = url_type
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (typeof referer_url === 'string') {
|
|
268
|
+
referer_url = referer_url.trim()
|
|
269
|
+
|
|
270
|
+
if (referer_url.length && (referer_url.toLowerCase().indexOf('http') === 0))
|
|
271
|
+
embedded_url.referer_url = referer_url
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
debug(3, 'redirecting (post-hook):', url)
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
embedded_url.redirected_url = ''
|
|
278
|
+
|
|
279
|
+
debug(3, 'redirecting (post-hook):', 'URL filtered, removed from manifest')
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const finalize_embedded_url = function(embedded_url, vod_start_at_ms, debug) {
|
|
285
|
+
if (embedded_url.redirected_url === '') {
|
|
286
|
+
embedded_url.unencoded_url = ''
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
let url = embedded_url.redirected_url || embedded_url.resolved_match_url
|
|
290
|
+
|
|
291
|
+
if (embedded_url.url_type)
|
|
292
|
+
debug(3, 'url type:', embedded_url.url_type)
|
|
293
|
+
|
|
294
|
+
debug(2, 'redirecting:', url)
|
|
295
|
+
|
|
296
|
+
if (vod_start_at_ms && (embedded_url.url_type === 'm3u8'))
|
|
297
|
+
url += `#vod_start=${Math.floor(vod_start_at_ms/1000)}`
|
|
298
|
+
|
|
299
|
+
if (embedded_url.referer_url)
|
|
300
|
+
url += `|${embedded_url.referer_url}`
|
|
301
|
+
|
|
302
|
+
embedded_url.unencoded_url = url
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const encode_embedded_url = function(embedded_url, redirected_base_url, debug) {
|
|
307
|
+
embedded_url.encoded_url = (embedded_url.unencoded_url)
|
|
308
|
+
? `${redirected_base_url}/${ utils.base64_encode(embedded_url.unencoded_url) }.${embedded_url.url_type || 'other'}`
|
|
309
|
+
: ''
|
|
310
|
+
|
|
311
|
+
if (embedded_url.encoded_url)
|
|
312
|
+
debug(3, 'redirecting (proxied):', embedded_url.encoded_url)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const get_prefetch_url = function(embedded_url, should_prefetch_url, prefetch_urls = []) {
|
|
316
|
+
if (embedded_url.redirected_url !== '') {
|
|
317
|
+
const url = embedded_url.redirected_url || embedded_url.resolved_match_url
|
|
318
|
+
|
|
319
|
+
if (should_prefetch_url(url, embedded_url.url_type))
|
|
320
|
+
prefetch_urls.push(url)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const modify_m3u8_line = function(embedded_url, m3u8_lines) {
|
|
325
|
+
const {line_index, url_indices, encoded_url} = embedded_url
|
|
326
|
+
|
|
327
|
+
if (url_indices && Array.isArray(url_indices) && (url_indices.length === 2)) {
|
|
328
|
+
const m3u8_line = m3u8_lines[line_index]
|
|
329
|
+
|
|
330
|
+
m3u8_lines[line_index] = m3u8_line.substring(0, url_indices[0]) + encoded_url + m3u8_line.substring(url_indices[1])
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
m3u8_lines[line_index] = encoded_url
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_url, referer_url, redirected_base_url) {
|
|
338
|
+
const {hooks, cache_segments, max_segments, debug_level} = params
|
|
339
|
+
|
|
340
|
+
const {has_cache, get_time_since_last_access, is_expired, prefetch_segment} = segment_cache
|
|
341
|
+
|
|
342
|
+
const debug = utils.debug.bind(null, params)
|
|
343
|
+
const should_prefetch_url = utils.should_prefetch_url.bind(null, params)
|
|
344
|
+
|
|
345
|
+
if (hooks && (hooks instanceof Object) && hooks.modify_m3u8_content && (typeof hooks.modify_m3u8_content === 'function')) {
|
|
346
|
+
m3u8_content = hooks.modify_m3u8_content(m3u8_content, m3u8_url) || m3u8_content
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const debug_divider = (debug_level >= 4)
|
|
350
|
+
? ('-').repeat(40)
|
|
351
|
+
: ''
|
|
352
|
+
|
|
353
|
+
if (debug_level >= 4) {
|
|
354
|
+
debug(4, 'proxied response (original m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let is_vod, seg_duration_ms, prefetch_urls
|
|
358
|
+
|
|
359
|
+
// only used with prefetch
|
|
360
|
+
const vod_start_at_ms = (cache_segments)
|
|
361
|
+
? get_vod_start_at_ms(m3u8_url)
|
|
362
|
+
: null
|
|
363
|
+
|
|
364
|
+
// only used with prefetch
|
|
365
|
+
const perform_prefetch = (cache_segments)
|
|
366
|
+
? (urls, dont_touch_access) => {
|
|
367
|
+
if (!urls || !Array.isArray(urls) || !urls.length)
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
let promise
|
|
371
|
+
|
|
372
|
+
if (is_vod || has_cache(m3u8_url)) {
|
|
373
|
+
promise = Promise.resolve()
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const matching_url = urls[0]
|
|
377
|
+
urls[0] = undefined
|
|
378
|
+
|
|
379
|
+
promise = prefetch_segment(m3u8_url, matching_url, referer_url, dont_touch_access)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
promise.then(() => {
|
|
383
|
+
urls.forEach((matching_url, index) => {
|
|
384
|
+
if (matching_url) {
|
|
385
|
+
prefetch_segment(m3u8_url, matching_url, referer_url, dont_touch_access)
|
|
386
|
+
|
|
387
|
+
urls[index] = undefined
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
: null
|
|
393
|
+
|
|
394
|
+
{
|
|
395
|
+
const parsed_manifest = parse_manifest(m3u8_content, m3u8_url, referer_url, hooks, cache_segments, debug, vod_start_at_ms, redirected_base_url, should_prefetch_url)
|
|
396
|
+
is_vod = !!parsed_manifest.meta_data.is_vod // default: false => hls live stream
|
|
397
|
+
seg_duration_ms = parsed_manifest.meta_data.seg_duration_ms || 10000 // default: 10 seconds in ms
|
|
398
|
+
prefetch_urls = parsed_manifest.prefetch_urls
|
|
399
|
+
m3u8_content = parsed_manifest.modified_m3u8
|
|
400
|
+
|
|
401
|
+
if (debug_level >= 4) {
|
|
402
|
+
debug(4, 'parsed manifest:', `\n${debug_divider}\n${JSON.stringify(parsed_manifest, null, 2)}\n${debug_divider}`)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (prefetch_urls.length) {
|
|
407
|
+
if (is_vod && vod_start_at_ms) {
|
|
408
|
+
// full video: prevent prefetch of URLs for skipped video segments
|
|
409
|
+
|
|
410
|
+
const skip_segment_count = Math.floor(vod_start_at_ms / seg_duration_ms)
|
|
411
|
+
|
|
412
|
+
prefetch_urls.splice(0, skip_segment_count)
|
|
413
|
+
debug(3, 'prefetch (ignored):', `${skip_segment_count} URLs in m3u8 skipped to initialize vod prefetch timer from start position obtained from HLS manifest URL #hash`)
|
|
414
|
+
}
|
|
415
|
+
if (prefetch_urls.length > max_segments) {
|
|
416
|
+
if (hooks && (hooks instanceof Object) && hooks.prefetch_segments && (typeof hooks.prefetch_segments === 'function')) {
|
|
417
|
+
prefetch_urls = hooks.prefetch_segments(prefetch_urls, max_segments, is_vod, seg_duration_ms, perform_prefetch)
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
if (!is_vod) {
|
|
421
|
+
// live stream: cache from the end
|
|
422
|
+
|
|
423
|
+
const overflow = prefetch_urls.length - max_segments
|
|
424
|
+
|
|
425
|
+
prefetch_urls.splice(0, overflow)
|
|
426
|
+
debug(3, 'prefetch (ignored):', `${overflow} URLs in m3u8 skipped to prevent cache overflow`)
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
// full video: cache from the beginning w/ timer to update cache at rate of playback (assuming no pausing or seeking)
|
|
430
|
+
|
|
431
|
+
const $prefetch_urls = [...prefetch_urls]
|
|
432
|
+
const batch_size = Math.ceil(max_segments / 2)
|
|
433
|
+
const batch_time = seg_duration_ms * batch_size
|
|
434
|
+
|
|
435
|
+
const is_client_paused = () => {
|
|
436
|
+
const time_since_last_access = get_time_since_last_access(m3u8_url)
|
|
437
|
+
|
|
438
|
+
let inactivity_timeout
|
|
439
|
+
inactivity_timeout = seg_duration_ms * 2
|
|
440
|
+
inactivity_timeout = Math.floor(inactivity_timeout / 1000) // convert to seconds
|
|
441
|
+
|
|
442
|
+
return (time_since_last_access < 0)
|
|
443
|
+
? false
|
|
444
|
+
: (time_since_last_access >= inactivity_timeout)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const prefetch_next_batch = (is_cache_empty) => {
|
|
448
|
+
is_cache_empty = (is_cache_empty === true)
|
|
449
|
+
|
|
450
|
+
if (!is_cache_empty && is_expired(m3u8_url)) {
|
|
451
|
+
debug(3, 'prefetch (stopped):', 'vod stream removed from cache due to inactivity longer than timeout; prefetch has stopped')
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!is_cache_empty && is_client_paused()) {
|
|
456
|
+
debug(3, 'prefetch (skipped):', 'vod stream is paused; prefetch will continue after client playback resumes')
|
|
457
|
+
setTimeout(prefetch_next_batch, batch_time)
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if ($prefetch_urls.length > batch_size) {
|
|
462
|
+
const batch_urls = $prefetch_urls.splice(0, batch_size)
|
|
463
|
+
|
|
464
|
+
perform_prefetch(batch_urls, !is_cache_empty)
|
|
465
|
+
setTimeout(prefetch_next_batch, is_cache_empty ? 0 : batch_time)
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
perform_prefetch($prefetch_urls, !is_cache_empty)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
prefetch_urls = []
|
|
473
|
+
prefetch_next_batch(true)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
perform_prefetch(prefetch_urls)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (debug_level >= 4) {
|
|
481
|
+
debug(4, 'proxied response (modified m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return m3u8_content
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
module.exports = {
|
|
488
|
+
modify_m3u8_content
|
|
489
|
+
}
|