@warren-bank/hls-proxy 2.0.0 → 2.0.2
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 +35 -0
- package/hls-proxy/manifest_parser.js +335 -335
- package/hls-proxy/proxy.js +4 -3
- package/hls-proxy/utils.js +100 -100
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -528,6 +528,41 @@ curl --silent --insecure "$URL"
|
|
|
528
528
|
|
|
529
529
|
- - - -
|
|
530
530
|
|
|
531
|
+
#### Major Versions:
|
|
532
|
+
|
|
533
|
+
* `v1.x`
|
|
534
|
+
- commit history is in branch: [`v01`](https://github.com/warren-bank/HLS-Proxy/commits/v01)
|
|
535
|
+
- summary:
|
|
536
|
+
* m3u8 manifest parser uses regex patterns to identify all URL patterns without any special knowledge of the m3u8 manifest specification
|
|
537
|
+
* internal `proxy` module exports a function that accepts an instance of [`http.Server`](https://nodejs.org/api/http.html#class-httpserver) and adds event listeners to process requests
|
|
538
|
+
- system requirements:
|
|
539
|
+
* Node.js v6.4.0 and higher
|
|
540
|
+
- required features: [`Proxy` constructor](https://node.green/#ES2015-built-ins-Proxy-constructor-requires-new), [`Proxy` 'apply' handler](https://node.green/#ES2015-built-ins-Proxy--apply--handler), [`Reflect.apply`](https://node.green/#ES2015-built-ins-Reflect-Reflect-apply)
|
|
541
|
+
* `v2.x`
|
|
542
|
+
- commit history is in branch: [`v02`](https://github.com/warren-bank/HLS-Proxy/commits/v02)
|
|
543
|
+
- summary:
|
|
544
|
+
* m3u8 manifest parser uses regex patterns to identify all URL patterns without any special knowledge of the m3u8 manifest specification
|
|
545
|
+
* internal `proxy` module exports an Object containing event listeners to process requests that can be either:
|
|
546
|
+
- added to an instance of [`http.Server`](https://nodejs.org/api/http.html#class-httpserver)
|
|
547
|
+
- added to an [`Express.js`](https://github.com/expressjs/express) application as middleware to handle a custom route
|
|
548
|
+
* important limitation: since `/` is a valid character in a base64 encoded URL, the path for a custom route needs to end with a character that is not allowed in base64 encoding (ex: `'/proxy_/*'`)
|
|
549
|
+
- system requirements:
|
|
550
|
+
* Node.js v6.4.0 and higher
|
|
551
|
+
- required features: [`Proxy` constructor](https://node.green/#ES2015-built-ins-Proxy-constructor-requires-new), [`Proxy` 'apply' handler](https://node.green/#ES2015-built-ins-Proxy--apply--handler), [`Reflect.apply`](https://node.green/#ES2015-built-ins-Reflect-Reflect-apply)
|
|
552
|
+
* `v3.x`
|
|
553
|
+
- commit history is in branch: [`v03`](https://github.com/warren-bank/HLS-Proxy/commits/v03)
|
|
554
|
+
- summary:
|
|
555
|
+
* m3u8 manifest parser uses special knowledge of the m3u8 manifest specification to contextually identify URLs
|
|
556
|
+
* internal `proxy` module exports an Object containing event listeners to process requests that can be either:
|
|
557
|
+
- added to an instance of [`http.Server`](https://nodejs.org/api/http.html#class-httpserver)
|
|
558
|
+
- added to an [`Express.js`](https://github.com/expressjs/express) application as middleware to handle a custom route
|
|
559
|
+
* important limitation: since `/` is a valid character in a base64 encoded URL, the path for a custom route needs to end with a character that is not allowed in base64 encoding (ex: `'/proxy_/*'`)
|
|
560
|
+
- system requirements:
|
|
561
|
+
* Node.js v16.0.0 and higher
|
|
562
|
+
- required features: [`Proxy` constructor](https://node.green/#ES2015-built-ins-Proxy-constructor-requires-new), [`Proxy` 'apply' handler](https://node.green/#ES2015-built-ins-Proxy--apply--handler), [`Reflect.apply`](https://node.green/#ES2015-built-ins-Reflect-Reflect-apply), [`RegExp` 'd' flag](https://node.green/#ES2022-features-RegExp-Match-Indices---hasIndices-----d--flag-)
|
|
563
|
+
|
|
564
|
+
- - - -
|
|
565
|
+
|
|
531
566
|
#### Legal:
|
|
532
567
|
|
|
533
568
|
* copyright: [Warren Bank](https://github.com/warren-bank)
|
|
@@ -1,335 +1,335 @@
|
|
|
1
|
-
const utils = require('./utils')
|
|
2
|
-
|
|
3
|
-
const regexs = {
|
|
4
|
-
m3u8: new RegExp('\\.m3u8(?:[\\?#]|$)', 'i'),
|
|
5
|
-
ts_duration: new RegExp('^#EXT-X-TARGETDURATION:(\\d+)(?:\\.\\d+)?$', 'im'),
|
|
6
|
-
vod: new RegExp('^(?:#EXT-X-PLAYLIST-TYPE:VOD|#EXT-X-ENDLIST)$', 'im'),
|
|
7
|
-
vod_start_at: new RegExp('#vod_start(?:_prefetch_at)?=((?:\\d+:)?(?:\\d+:)?\\d+)$', 'i'),
|
|
8
|
-
urls: new RegExp('(^|(?<!(?:KEYFORMAT=))[\\s\'"])((?:https?:/)?/)?((?:[^\\?#/\\s\'"]*/)+?)?([^\\?#,/\\s\'"]+?)(\\.[^\\?#,/\\.\\s\'"]+(?:[\\?#][^\\s\'"]*)?)?([\\s\'"]|$)', 'img'),
|
|
9
|
-
keys: new RegExp('(^#EXT-X-KEY:(?:.+,)?URI=")([^"]+)(".*$)', 'img')
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const ts_regexs = {
|
|
13
|
-
"file_ext": /^\.ts/i,
|
|
14
|
-
"sequence_number": /[^\d](\d+)$/i
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const get_ts_file_ext = function(file_name, file_ext) {
|
|
18
|
-
let ts_file_ext, matches
|
|
19
|
-
|
|
20
|
-
if (ts_regexs["file_ext"].test(file_ext)) {
|
|
21
|
-
matches = ts_regexs["sequence_number"].exec(file_name)
|
|
22
|
-
if (matches && matches.length) {
|
|
23
|
-
ts_file_ext = `_${matches[1]}${file_ext}`
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return ts_file_ext
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const get_seg_duration_ms = function(m3u8_content) {
|
|
30
|
-
try {
|
|
31
|
-
const matches = regexs.ts_duration.exec(m3u8_content)
|
|
32
|
-
|
|
33
|
-
if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
|
|
34
|
-
throw ''
|
|
35
|
-
|
|
36
|
-
let duration
|
|
37
|
-
duration = matches[1]
|
|
38
|
-
duration = parseInt(duration, 10)
|
|
39
|
-
duration = duration * 1000 // convert seconds to ms
|
|
40
|
-
|
|
41
|
-
return duration
|
|
42
|
-
}
|
|
43
|
-
catch(e) {
|
|
44
|
-
const def_duration = 10000 // 10 seconds in ms
|
|
45
|
-
|
|
46
|
-
return def_duration
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const parse_HHMMSS_to_seconds = function(str) {
|
|
51
|
-
const parts = str.split(':')
|
|
52
|
-
let seconds = 0
|
|
53
|
-
let multiplier = 1
|
|
54
|
-
|
|
55
|
-
while (parts.length > 0) {
|
|
56
|
-
seconds += multiplier * parseInt(parts.pop(), 10)
|
|
57
|
-
multiplier *= 60
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return seconds
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const get_vod_start_at_ms = function(m3u8_url) {
|
|
64
|
-
try {
|
|
65
|
-
const matches = regexs.vod_start_at.exec(m3u8_url)
|
|
66
|
-
|
|
67
|
-
if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
|
|
68
|
-
throw ''
|
|
69
|
-
|
|
70
|
-
let offset
|
|
71
|
-
offset = matches[1]
|
|
72
|
-
offset = parse_HHMMSS_to_seconds(offset)
|
|
73
|
-
offset = offset * 1000 // convert seconds to ms
|
|
74
|
-
|
|
75
|
-
return offset
|
|
76
|
-
}
|
|
77
|
-
catch(e) {
|
|
78
|
-
const def_offset = null
|
|
79
|
-
|
|
80
|
-
return def_offset
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_url, referer_url, redirected_base_url) {
|
|
85
|
-
const {hooks, cache_segments, max_segments, debug_level} = params
|
|
86
|
-
|
|
87
|
-
const {has_cache, get_time_since_last_access, is_expired, prefetch_segment} = segment_cache
|
|
88
|
-
|
|
89
|
-
const debug = utils.debug.bind(null, params)
|
|
90
|
-
const should_prefetch_url = utils.should_prefetch_url.bind(null, params)
|
|
91
|
-
|
|
92
|
-
if (hooks && (hooks instanceof Object) && hooks.modify_m3u8_content && (typeof hooks.modify_m3u8_content === 'function')) {
|
|
93
|
-
m3u8_content = hooks.modify_m3u8_content(m3u8_content, m3u8_url) || m3u8_content
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const base_urls = {
|
|
97
|
-
"relative": m3u8_url.replace(/[\?#].*$/, '').replace(/[^\/]+$/, ''),
|
|
98
|
-
"absolute": m3u8_url.replace(/(:\/\/[^\/]+).*$/, '$1')
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const debug_divider = (debug_level >= 4)
|
|
102
|
-
? ('-').repeat(40)
|
|
103
|
-
: ''
|
|
104
|
-
|
|
105
|
-
if (debug_level >= 4) {
|
|
106
|
-
debug(4, 'proxied response (original m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (debug_level >= 2) {
|
|
110
|
-
m3u8_content = m3u8_content.replace(regexs.keys, function(match, head, key_url, tail) {
|
|
111
|
-
debug(2, 'key:', key_url)
|
|
112
|
-
return match
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// only used with prefetch
|
|
117
|
-
const seg_duration_ms = (cache_segments)
|
|
118
|
-
? get_seg_duration_ms(m3u8_content)
|
|
119
|
-
: null
|
|
120
|
-
|
|
121
|
-
// only used with prefetch
|
|
122
|
-
const vod_start_at_ms = (cache_segments)
|
|
123
|
-
? get_vod_start_at_ms(m3u8_url)
|
|
124
|
-
: null
|
|
125
|
-
|
|
126
|
-
// only used with prefetch
|
|
127
|
-
const is_vod = (cache_segments)
|
|
128
|
-
? ((typeof vod_start_at_ms === 'number') || (!has_cache(m3u8_url) && regexs.vod.test(m3u8_content)))
|
|
129
|
-
: null
|
|
130
|
-
|
|
131
|
-
// only used with prefetch
|
|
132
|
-
const perform_prefetch = (cache_segments)
|
|
133
|
-
? (urls, dont_touch_access) => {
|
|
134
|
-
if (!urls || !Array.isArray(urls) || !urls.length)
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
let promise
|
|
138
|
-
|
|
139
|
-
if (is_vod || has_cache(m3u8_url)) {
|
|
140
|
-
promise = Promise.resolve()
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
const matching_url = urls[0]
|
|
144
|
-
urls[0] = undefined
|
|
145
|
-
|
|
146
|
-
promise = prefetch_segment(m3u8_url, matching_url, referer_url, dont_touch_access)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
promise.then(() => {
|
|
150
|
-
urls.forEach((matching_url, index) => {
|
|
151
|
-
if (matching_url) {
|
|
152
|
-
prefetch_segment(m3u8_url, matching_url, referer_url, dont_touch_access)
|
|
153
|
-
|
|
154
|
-
urls[index] = undefined
|
|
155
|
-
}
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
: null
|
|
160
|
-
|
|
161
|
-
let prefetch_urls = []
|
|
162
|
-
|
|
163
|
-
m3u8_content = m3u8_content.replace(regexs.urls, function(match, head, abs_path, rel_path, file_name, file_ext, tail) {
|
|
164
|
-
if (
|
|
165
|
-
((head === `"`) || (head === `'`) || (tail === `"`) || (tail === `'`)) &&
|
|
166
|
-
(head !== tail)
|
|
167
|
-
) return match
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
!abs_path && (
|
|
171
|
-
(!file_ext)
|
|
172
|
-
|| ( rel_path && ( rel_path.indexOf('#EXT') === 0))
|
|
173
|
-
|| (!rel_path && (file_name.indexOf('#EXT') === 0))
|
|
174
|
-
)
|
|
175
|
-
) return match
|
|
176
|
-
|
|
177
|
-
debug(3, 'modify (raw):', {match, head, abs_path, rel_path, file_name, file_ext, tail})
|
|
178
|
-
|
|
179
|
-
let matching_url
|
|
180
|
-
if (!abs_path) {
|
|
181
|
-
matching_url = `${base_urls.relative}${rel_path || ''}${file_name}${file_ext || ''}`
|
|
182
|
-
}
|
|
183
|
-
else if (abs_path[0] === '/') {
|
|
184
|
-
matching_url = `${base_urls.absolute}${abs_path}${rel_path || ''}${file_name}${file_ext || ''}`
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
matching_url = `${abs_path}${rel_path || ''}${file_name}${file_ext || ''}`
|
|
188
|
-
}
|
|
189
|
-
matching_url = matching_url.trim()
|
|
190
|
-
|
|
191
|
-
if (hooks && (hooks instanceof Object) && hooks.redirect && (typeof hooks.redirect === 'function')) {
|
|
192
|
-
debug(3, 'redirecting (pre-hook):', matching_url)
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
let result = hooks.redirect(matching_url, referer_url)
|
|
196
|
-
|
|
197
|
-
if (result) {
|
|
198
|
-
if (typeof result === 'string') {
|
|
199
|
-
matching_url = result
|
|
200
|
-
}
|
|
201
|
-
else if (result instanceof Object) {
|
|
202
|
-
if (result.matching_url) matching_url = result.matching_url
|
|
203
|
-
if (result.file_name) file_name = result.file_name
|
|
204
|
-
if (result.file_ext) file_ext = result.file_ext
|
|
205
|
-
if (result.referer_url) referer_url = result.referer_url
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (typeof matching_url !== 'string') throw new Error('bad return value')
|
|
210
|
-
|
|
211
|
-
if (matching_url.length && matching_url.toLowerCase().indexOf('http') !== 0) {
|
|
212
|
-
matching_url = ( (matching_url[0] === '/') ? base_urls.absolute : base_urls.relative ) + matching_url
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
catch(e) {
|
|
216
|
-
matching_url = ''
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (!matching_url) {
|
|
220
|
-
debug(3, 'redirecting (post-hook):', 'URL filtered, removed from manifest')
|
|
221
|
-
return `${head}${tail}`
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
debug(2, 'redirecting:', matching_url)
|
|
225
|
-
|
|
226
|
-
// aggregate prefetch URLs into an array while iterating.
|
|
227
|
-
// after the loop is complete, check the count.
|
|
228
|
-
// if it exceeds the size of the cache, remove overflow elements from the beginning.
|
|
229
|
-
if (should_prefetch_url(matching_url))
|
|
230
|
-
prefetch_urls.push(matching_url)
|
|
231
|
-
|
|
232
|
-
if (vod_start_at_ms && regexs.m3u8.test(matching_url))
|
|
233
|
-
matching_url += `#vod_start=${Math.floor(vod_start_at_ms/1000)}`
|
|
234
|
-
|
|
235
|
-
if (referer_url)
|
|
236
|
-
matching_url += `|${referer_url}`
|
|
237
|
-
|
|
238
|
-
let ts_file_ext = get_ts_file_ext(file_name, file_ext)
|
|
239
|
-
let redirected_url = `${redirected_base_url}/${ utils.base64_encode(matching_url) }${ts_file_ext || file_ext || ''}`
|
|
240
|
-
debug(3, 'redirecting (proxied):', redirected_url)
|
|
241
|
-
|
|
242
|
-
return `${head}${redirected_url}${tail}`
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
if (prefetch_urls.length) {
|
|
246
|
-
if (is_vod && vod_start_at_ms) {
|
|
247
|
-
// full video: prevent prefetch of URLs for skipped video segments
|
|
248
|
-
|
|
249
|
-
const skip_segment_count = Math.floor(vod_start_at_ms / seg_duration_ms)
|
|
250
|
-
|
|
251
|
-
prefetch_urls.splice(0, skip_segment_count)
|
|
252
|
-
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`)
|
|
253
|
-
}
|
|
254
|
-
if (prefetch_urls.length > max_segments) {
|
|
255
|
-
if (hooks && (hooks instanceof Object) && hooks.prefetch_segments && (typeof hooks.prefetch_segments === 'function')) {
|
|
256
|
-
prefetch_urls = hooks.prefetch_segments(prefetch_urls, max_segments, is_vod, seg_duration_ms, perform_prefetch)
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
if (!is_vod) {
|
|
260
|
-
// live stream: cache from the end
|
|
261
|
-
|
|
262
|
-
const overflow = prefetch_urls.length - max_segments
|
|
263
|
-
|
|
264
|
-
prefetch_urls.splice(0, overflow)
|
|
265
|
-
debug(3, 'prefetch (ignored):', `${overflow} URLs in m3u8 skipped to prevent cache overflow`)
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
// full video: cache from the beginning w/ timer to update cache at rate of playback (assuming no pausing or seeking)
|
|
269
|
-
|
|
270
|
-
const $prefetch_urls = [...prefetch_urls]
|
|
271
|
-
const batch_size = Math.ceil(max_segments / 2)
|
|
272
|
-
const batch_time = seg_duration_ms * batch_size
|
|
273
|
-
|
|
274
|
-
const is_client_paused = () => {
|
|
275
|
-
const time_since_last_access = get_time_since_last_access(m3u8_url)
|
|
276
|
-
|
|
277
|
-
let inactivity_timeout
|
|
278
|
-
inactivity_timeout = seg_duration_ms * 2
|
|
279
|
-
inactivity_timeout = Math.floor(inactivity_timeout / 1000) // convert to seconds
|
|
280
|
-
|
|
281
|
-
return (time_since_last_access < 0)
|
|
282
|
-
? false
|
|
283
|
-
: (time_since_last_access >= inactivity_timeout)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const prefetch_next_batch = (is_cache_empty) => {
|
|
287
|
-
is_cache_empty = (is_cache_empty === true)
|
|
288
|
-
|
|
289
|
-
if (!is_cache_empty && is_expired(m3u8_url)) {
|
|
290
|
-
debug(3, 'prefetch (stopped):', 'vod stream removed from cache due to inactivity longer than timeout; prefetch has stopped')
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (!is_cache_empty && is_client_paused()) {
|
|
295
|
-
debug(3, 'prefetch (skipped):', 'vod stream is paused; prefetch will continue after client playback resumes')
|
|
296
|
-
setTimeout(prefetch_next_batch, batch_time)
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if ($prefetch_urls.length > batch_size) {
|
|
301
|
-
const batch_urls = $prefetch_urls.splice(0, batch_size)
|
|
302
|
-
|
|
303
|
-
perform_prefetch(batch_urls, !is_cache_empty)
|
|
304
|
-
setTimeout(prefetch_next_batch, is_cache_empty ? 0 : batch_time)
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
perform_prefetch($prefetch_urls, !is_cache_empty)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
prefetch_urls = []
|
|
312
|
-
prefetch_next_batch(true)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
perform_prefetch(prefetch_urls)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (debug_level >= 3) {
|
|
320
|
-
m3u8_content = m3u8_content.replace(regexs.keys, function(match, head, key_url, tail) {
|
|
321
|
-
debug(3, 'key (proxied):', key_url)
|
|
322
|
-
return match
|
|
323
|
-
})
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (debug_level >= 4) {
|
|
327
|
-
debug(4, 'proxied response (modified m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return m3u8_content
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
module.exports = {
|
|
334
|
-
modify_m3u8_content
|
|
335
|
-
}
|
|
1
|
+
const utils = require('./utils')
|
|
2
|
+
|
|
3
|
+
const regexs = {
|
|
4
|
+
m3u8: new RegExp('\\.m3u8(?:[\\?#]|$)', 'i'),
|
|
5
|
+
ts_duration: new RegExp('^#EXT-X-TARGETDURATION:(\\d+)(?:\\.\\d+)?$', 'im'),
|
|
6
|
+
vod: new RegExp('^(?:#EXT-X-PLAYLIST-TYPE:VOD|#EXT-X-ENDLIST)$', 'im'),
|
|
7
|
+
vod_start_at: new RegExp('#vod_start(?:_prefetch_at)?=((?:\\d+:)?(?:\\d+:)?\\d+)$', 'i'),
|
|
8
|
+
urls: new RegExp('(^|(?<!(?:KEYFORMAT=))[\\s\'"])((?:https?:/)?/)?((?:[^\\?#/\\s\'"]*/)+?)?([^\\?#,/\\s\'"]+?)(\\.[^\\?#,/\\.\\s\'"]+(?:[\\?#][^\\s\'"]*)?)?([\\s\'"]|$)', 'img'),
|
|
9
|
+
keys: new RegExp('(^#EXT-X-KEY:(?:.+,)?URI=")([^"]+)(".*$)', 'img')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ts_regexs = {
|
|
13
|
+
"file_ext": /^\.ts/i,
|
|
14
|
+
"sequence_number": /[^\d](\d+)$/i
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const get_ts_file_ext = function(file_name, file_ext) {
|
|
18
|
+
let ts_file_ext, matches
|
|
19
|
+
|
|
20
|
+
if (ts_regexs["file_ext"].test(file_ext)) {
|
|
21
|
+
matches = ts_regexs["sequence_number"].exec(file_name)
|
|
22
|
+
if (matches && matches.length) {
|
|
23
|
+
ts_file_ext = `_${matches[1]}${file_ext}`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return ts_file_ext
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const get_seg_duration_ms = function(m3u8_content) {
|
|
30
|
+
try {
|
|
31
|
+
const matches = regexs.ts_duration.exec(m3u8_content)
|
|
32
|
+
|
|
33
|
+
if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
|
|
34
|
+
throw ''
|
|
35
|
+
|
|
36
|
+
let duration
|
|
37
|
+
duration = matches[1]
|
|
38
|
+
duration = parseInt(duration, 10)
|
|
39
|
+
duration = duration * 1000 // convert seconds to ms
|
|
40
|
+
|
|
41
|
+
return duration
|
|
42
|
+
}
|
|
43
|
+
catch(e) {
|
|
44
|
+
const def_duration = 10000 // 10 seconds in ms
|
|
45
|
+
|
|
46
|
+
return def_duration
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parse_HHMMSS_to_seconds = function(str) {
|
|
51
|
+
const parts = str.split(':')
|
|
52
|
+
let seconds = 0
|
|
53
|
+
let multiplier = 1
|
|
54
|
+
|
|
55
|
+
while (parts.length > 0) {
|
|
56
|
+
seconds += multiplier * parseInt(parts.pop(), 10)
|
|
57
|
+
multiplier *= 60
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return seconds
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const get_vod_start_at_ms = function(m3u8_url) {
|
|
64
|
+
try {
|
|
65
|
+
const matches = regexs.vod_start_at.exec(m3u8_url)
|
|
66
|
+
|
|
67
|
+
if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
|
|
68
|
+
throw ''
|
|
69
|
+
|
|
70
|
+
let offset
|
|
71
|
+
offset = matches[1]
|
|
72
|
+
offset = parse_HHMMSS_to_seconds(offset)
|
|
73
|
+
offset = offset * 1000 // convert seconds to ms
|
|
74
|
+
|
|
75
|
+
return offset
|
|
76
|
+
}
|
|
77
|
+
catch(e) {
|
|
78
|
+
const def_offset = null
|
|
79
|
+
|
|
80
|
+
return def_offset
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_url, referer_url, redirected_base_url) {
|
|
85
|
+
const {hooks, cache_segments, max_segments, debug_level} = params
|
|
86
|
+
|
|
87
|
+
const {has_cache, get_time_since_last_access, is_expired, prefetch_segment} = segment_cache
|
|
88
|
+
|
|
89
|
+
const debug = utils.debug.bind(null, params)
|
|
90
|
+
const should_prefetch_url = utils.should_prefetch_url.bind(null, params)
|
|
91
|
+
|
|
92
|
+
if (hooks && (hooks instanceof Object) && hooks.modify_m3u8_content && (typeof hooks.modify_m3u8_content === 'function')) {
|
|
93
|
+
m3u8_content = hooks.modify_m3u8_content(m3u8_content, m3u8_url) || m3u8_content
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const base_urls = {
|
|
97
|
+
"relative": m3u8_url.replace(/[\?#].*$/, '').replace(/[^\/]+$/, ''),
|
|
98
|
+
"absolute": m3u8_url.replace(/(:\/\/[^\/]+).*$/, '$1')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const debug_divider = (debug_level >= 4)
|
|
102
|
+
? ('-').repeat(40)
|
|
103
|
+
: ''
|
|
104
|
+
|
|
105
|
+
if (debug_level >= 4) {
|
|
106
|
+
debug(4, 'proxied response (original m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (debug_level >= 2) {
|
|
110
|
+
m3u8_content = m3u8_content.replace(regexs.keys, function(match, head, key_url, tail) {
|
|
111
|
+
debug(2, 'key:', key_url)
|
|
112
|
+
return match
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// only used with prefetch
|
|
117
|
+
const seg_duration_ms = (cache_segments)
|
|
118
|
+
? get_seg_duration_ms(m3u8_content)
|
|
119
|
+
: null
|
|
120
|
+
|
|
121
|
+
// only used with prefetch
|
|
122
|
+
const vod_start_at_ms = (cache_segments)
|
|
123
|
+
? get_vod_start_at_ms(m3u8_url)
|
|
124
|
+
: null
|
|
125
|
+
|
|
126
|
+
// only used with prefetch
|
|
127
|
+
const is_vod = (cache_segments)
|
|
128
|
+
? ((typeof vod_start_at_ms === 'number') || (!has_cache(m3u8_url) && regexs.vod.test(m3u8_content)))
|
|
129
|
+
: null
|
|
130
|
+
|
|
131
|
+
// only used with prefetch
|
|
132
|
+
const perform_prefetch = (cache_segments)
|
|
133
|
+
? (urls, dont_touch_access) => {
|
|
134
|
+
if (!urls || !Array.isArray(urls) || !urls.length)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
let promise
|
|
138
|
+
|
|
139
|
+
if (is_vod || has_cache(m3u8_url)) {
|
|
140
|
+
promise = Promise.resolve()
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const matching_url = urls[0]
|
|
144
|
+
urls[0] = undefined
|
|
145
|
+
|
|
146
|
+
promise = prefetch_segment(m3u8_url, matching_url, referer_url, dont_touch_access)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
promise.then(() => {
|
|
150
|
+
urls.forEach((matching_url, index) => {
|
|
151
|
+
if (matching_url) {
|
|
152
|
+
prefetch_segment(m3u8_url, matching_url, referer_url, dont_touch_access)
|
|
153
|
+
|
|
154
|
+
urls[index] = undefined
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
: null
|
|
160
|
+
|
|
161
|
+
let prefetch_urls = []
|
|
162
|
+
|
|
163
|
+
m3u8_content = m3u8_content.replace(regexs.urls, function(match, head, abs_path, rel_path, file_name, file_ext, tail) {
|
|
164
|
+
if (
|
|
165
|
+
((head === `"`) || (head === `'`) || (tail === `"`) || (tail === `'`)) &&
|
|
166
|
+
(head !== tail)
|
|
167
|
+
) return match
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
!abs_path && (
|
|
171
|
+
(!file_ext)
|
|
172
|
+
|| ( rel_path && ( rel_path.indexOf('#EXT') === 0))
|
|
173
|
+
|| (!rel_path && (file_name.indexOf('#EXT') === 0))
|
|
174
|
+
)
|
|
175
|
+
) return match
|
|
176
|
+
|
|
177
|
+
debug(3, 'modify (raw):', {match, head, abs_path, rel_path, file_name, file_ext, tail})
|
|
178
|
+
|
|
179
|
+
let matching_url
|
|
180
|
+
if (!abs_path) {
|
|
181
|
+
matching_url = `${base_urls.relative}${rel_path || ''}${file_name}${file_ext || ''}`
|
|
182
|
+
}
|
|
183
|
+
else if (abs_path[0] === '/') {
|
|
184
|
+
matching_url = `${base_urls.absolute}${abs_path}${rel_path || ''}${file_name}${file_ext || ''}`
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
matching_url = `${abs_path}${rel_path || ''}${file_name}${file_ext || ''}`
|
|
188
|
+
}
|
|
189
|
+
matching_url = matching_url.trim()
|
|
190
|
+
|
|
191
|
+
if (hooks && (hooks instanceof Object) && hooks.redirect && (typeof hooks.redirect === 'function')) {
|
|
192
|
+
debug(3, 'redirecting (pre-hook):', matching_url)
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
let result = hooks.redirect(matching_url, referer_url)
|
|
196
|
+
|
|
197
|
+
if (result) {
|
|
198
|
+
if (typeof result === 'string') {
|
|
199
|
+
matching_url = result
|
|
200
|
+
}
|
|
201
|
+
else if (result instanceof Object) {
|
|
202
|
+
if (result.matching_url) matching_url = result.matching_url
|
|
203
|
+
if (result.file_name) file_name = result.file_name
|
|
204
|
+
if (result.file_ext) file_ext = result.file_ext
|
|
205
|
+
if (result.referer_url) referer_url = result.referer_url
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (typeof matching_url !== 'string') throw new Error('bad return value')
|
|
210
|
+
|
|
211
|
+
if (matching_url.length && matching_url.toLowerCase().indexOf('http') !== 0) {
|
|
212
|
+
matching_url = ( (matching_url[0] === '/') ? base_urls.absolute : base_urls.relative ) + matching_url
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch(e) {
|
|
216
|
+
matching_url = ''
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!matching_url) {
|
|
220
|
+
debug(3, 'redirecting (post-hook):', 'URL filtered, removed from manifest')
|
|
221
|
+
return `${head}${tail}`
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
debug(2, 'redirecting:', matching_url)
|
|
225
|
+
|
|
226
|
+
// aggregate prefetch URLs into an array while iterating.
|
|
227
|
+
// after the loop is complete, check the count.
|
|
228
|
+
// if it exceeds the size of the cache, remove overflow elements from the beginning.
|
|
229
|
+
if (should_prefetch_url(matching_url))
|
|
230
|
+
prefetch_urls.push(matching_url)
|
|
231
|
+
|
|
232
|
+
if (vod_start_at_ms && regexs.m3u8.test(matching_url))
|
|
233
|
+
matching_url += `#vod_start=${Math.floor(vod_start_at_ms/1000)}`
|
|
234
|
+
|
|
235
|
+
if (referer_url)
|
|
236
|
+
matching_url += `|${referer_url}`
|
|
237
|
+
|
|
238
|
+
let ts_file_ext = get_ts_file_ext(file_name, file_ext)
|
|
239
|
+
let redirected_url = `${redirected_base_url}/${ utils.base64_encode(matching_url) }${ts_file_ext || file_ext || ''}`
|
|
240
|
+
debug(3, 'redirecting (proxied):', redirected_url)
|
|
241
|
+
|
|
242
|
+
return `${head}${redirected_url}${tail}`
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
if (prefetch_urls.length) {
|
|
246
|
+
if (is_vod && vod_start_at_ms) {
|
|
247
|
+
// full video: prevent prefetch of URLs for skipped video segments
|
|
248
|
+
|
|
249
|
+
const skip_segment_count = Math.floor(vod_start_at_ms / seg_duration_ms)
|
|
250
|
+
|
|
251
|
+
prefetch_urls.splice(0, skip_segment_count)
|
|
252
|
+
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`)
|
|
253
|
+
}
|
|
254
|
+
if (prefetch_urls.length > max_segments) {
|
|
255
|
+
if (hooks && (hooks instanceof Object) && hooks.prefetch_segments && (typeof hooks.prefetch_segments === 'function')) {
|
|
256
|
+
prefetch_urls = hooks.prefetch_segments(prefetch_urls, max_segments, is_vod, seg_duration_ms, perform_prefetch)
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
if (!is_vod) {
|
|
260
|
+
// live stream: cache from the end
|
|
261
|
+
|
|
262
|
+
const overflow = prefetch_urls.length - max_segments
|
|
263
|
+
|
|
264
|
+
prefetch_urls.splice(0, overflow)
|
|
265
|
+
debug(3, 'prefetch (ignored):', `${overflow} URLs in m3u8 skipped to prevent cache overflow`)
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// full video: cache from the beginning w/ timer to update cache at rate of playback (assuming no pausing or seeking)
|
|
269
|
+
|
|
270
|
+
const $prefetch_urls = [...prefetch_urls]
|
|
271
|
+
const batch_size = Math.ceil(max_segments / 2)
|
|
272
|
+
const batch_time = seg_duration_ms * batch_size
|
|
273
|
+
|
|
274
|
+
const is_client_paused = () => {
|
|
275
|
+
const time_since_last_access = get_time_since_last_access(m3u8_url)
|
|
276
|
+
|
|
277
|
+
let inactivity_timeout
|
|
278
|
+
inactivity_timeout = seg_duration_ms * 2
|
|
279
|
+
inactivity_timeout = Math.floor(inactivity_timeout / 1000) // convert to seconds
|
|
280
|
+
|
|
281
|
+
return (time_since_last_access < 0)
|
|
282
|
+
? false
|
|
283
|
+
: (time_since_last_access >= inactivity_timeout)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const prefetch_next_batch = (is_cache_empty) => {
|
|
287
|
+
is_cache_empty = (is_cache_empty === true)
|
|
288
|
+
|
|
289
|
+
if (!is_cache_empty && is_expired(m3u8_url)) {
|
|
290
|
+
debug(3, 'prefetch (stopped):', 'vod stream removed from cache due to inactivity longer than timeout; prefetch has stopped')
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!is_cache_empty && is_client_paused()) {
|
|
295
|
+
debug(3, 'prefetch (skipped):', 'vod stream is paused; prefetch will continue after client playback resumes')
|
|
296
|
+
setTimeout(prefetch_next_batch, batch_time)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if ($prefetch_urls.length > batch_size) {
|
|
301
|
+
const batch_urls = $prefetch_urls.splice(0, batch_size)
|
|
302
|
+
|
|
303
|
+
perform_prefetch(batch_urls, !is_cache_empty)
|
|
304
|
+
setTimeout(prefetch_next_batch, is_cache_empty ? 0 : batch_time)
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
perform_prefetch($prefetch_urls, !is_cache_empty)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
prefetch_urls = []
|
|
312
|
+
prefetch_next_batch(true)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
perform_prefetch(prefetch_urls)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (debug_level >= 3) {
|
|
320
|
+
m3u8_content = m3u8_content.replace(regexs.keys, function(match, head, key_url, tail) {
|
|
321
|
+
debug(3, 'key (proxied):', key_url)
|
|
322
|
+
return match
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (debug_level >= 4) {
|
|
327
|
+
debug(4, 'proxied response (modified m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return m3u8_content
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = {
|
|
334
|
+
modify_m3u8_content
|
|
335
|
+
}
|
package/hls-proxy/proxy.js
CHANGED
|
@@ -3,12 +3,13 @@ const parser = require('./manifest_parser')
|
|
|
3
3
|
const utils = require('./utils')
|
|
4
4
|
|
|
5
5
|
const regexs = {
|
|
6
|
-
wrap: new RegExp('^(
|
|
6
|
+
wrap: new RegExp('^(.*?)/([a-zA-Z0-9\\+/=%]+)(?:[\\._][^/\\?#]*)?(?:[\\?#].*)?$'),
|
|
7
7
|
m3u8: new RegExp('\\.m3u8(?:[\\?#]|$)', 'i')
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const get_middleware = function(params) {
|
|
11
|
-
const {is_secure, host, cache_segments
|
|
11
|
+
const {is_secure, host, cache_segments} = params
|
|
12
|
+
let {acl_whitelist} = params
|
|
12
13
|
|
|
13
14
|
const segment_cache = require('./segment_cache')(params)
|
|
14
15
|
const {get_segment, add_listener} = segment_cache
|
|
@@ -47,7 +48,7 @@ const get_middleware = function(params) {
|
|
|
47
48
|
|
|
48
49
|
let url, url_lc, index
|
|
49
50
|
|
|
50
|
-
url = utils.base64_decode( req.url.replace(regexs.wrap, '$2') ).trim()
|
|
51
|
+
url = utils.base64_decode( decodeURIComponent( req.url.replace(regexs.wrap, '$2') ) ).trim()
|
|
51
52
|
url_lc = url.toLowerCase()
|
|
52
53
|
|
|
53
54
|
index = url_lc.indexOf('http')
|
package/hls-proxy/utils.js
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
const parse_url = require('url').parse
|
|
2
|
-
|
|
3
|
-
const regexs = {
|
|
4
|
-
origin: new RegExp('^(https?://[^/]+)(?:/.*)?$', 'i'),
|
|
5
|
-
ts: new RegExp('\\.ts(?:[\\?#]|$)', 'i')
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// btoa
|
|
9
|
-
const base64_encode = function(str) {
|
|
10
|
-
return Buffer.from(str, 'binary').toString('base64')
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// atob
|
|
14
|
-
const base64_decode = function(str) {
|
|
15
|
-
return Buffer.from(str, 'base64').toString('binary')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const add_CORS_headers = function(res) {
|
|
19
|
-
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
20
|
-
res.setHeader('Access-Control-Allow-Methods', '*')
|
|
21
|
-
res.setHeader('Access-Control-Allow-Headers', '*')
|
|
22
|
-
res.setHeader('Access-Control-Allow-Credentials', 'true')
|
|
23
|
-
res.setHeader('Access-Control-Max-Age', '86400')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const debug = function() {
|
|
27
|
-
const args = [...arguments]
|
|
28
|
-
const params = args.shift()
|
|
29
|
-
const verbosity = args.shift()
|
|
30
|
-
const append_LF = true
|
|
31
|
-
|
|
32
|
-
const {debug_level} = params
|
|
33
|
-
|
|
34
|
-
if (append_LF) args.push("\n")
|
|
35
|
-
|
|
36
|
-
if (debug_level >= verbosity) {
|
|
37
|
-
console.log.apply(console.log, args)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const get_request_options = function(params, url, is_m3u8, referer_url) {
|
|
42
|
-
const {req_headers, req_options, hooks} = params
|
|
43
|
-
|
|
44
|
-
const additional_req_options = (hooks && (hooks instanceof Object) && hooks.add_request_options && (typeof hooks.add_request_options === 'function'))
|
|
45
|
-
? hooks.add_request_options(url, is_m3u8)
|
|
46
|
-
: null
|
|
47
|
-
|
|
48
|
-
const additional_req_headers = (hooks && (hooks instanceof Object) && hooks.add_request_headers && (typeof hooks.add_request_headers === 'function'))
|
|
49
|
-
? hooks.add_request_headers(url, is_m3u8)
|
|
50
|
-
: null
|
|
51
|
-
|
|
52
|
-
if (!req_options && !additional_req_options && !req_headers && !additional_req_headers && !referer_url) return url
|
|
53
|
-
|
|
54
|
-
const request_options = Object.assign(
|
|
55
|
-
{},
|
|
56
|
-
parse_url(url),
|
|
57
|
-
(req_options || {}),
|
|
58
|
-
(additional_req_options || {})
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
request_options.headers = Object.assign(
|
|
62
|
-
{},
|
|
63
|
-
(( req_options && req_options.headers) ? req_options.headers : {}),
|
|
64
|
-
((additional_req_options && additional_req_options.headers) ? additional_req_options.headers : {}),
|
|
65
|
-
(req_headers || {}),
|
|
66
|
-
(additional_req_headers || {}),
|
|
67
|
-
(referer_url ? {"referer": referer_url, "origin": referer_url.replace(regexs.origin, '$1')} : {})
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
return request_options
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const should_prefetch_url = function(params, url) {
|
|
74
|
-
const {hooks, cache_segments} = params
|
|
75
|
-
|
|
76
|
-
let do_prefetch = !!cache_segments
|
|
77
|
-
|
|
78
|
-
if (do_prefetch) {
|
|
79
|
-
do_prefetch = regexs.ts.test(url)
|
|
80
|
-
|
|
81
|
-
if (hooks && (hooks instanceof Object) && hooks.prefetch && (typeof hooks.prefetch === 'function')) {
|
|
82
|
-
const override_prefetch = hooks.prefetch(url)
|
|
83
|
-
|
|
84
|
-
if ((typeof override_prefetch === 'boolean') && (override_prefetch !== do_prefetch)) {
|
|
85
|
-
debug(params, 3, 'prefetch override:', (override_prefetch ? 'allow' : 'deny'), url)
|
|
86
|
-
do_prefetch = override_prefetch
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return do_prefetch
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
module.exports = {
|
|
94
|
-
base64_encode,
|
|
95
|
-
base64_decode,
|
|
96
|
-
add_CORS_headers,
|
|
97
|
-
debug,
|
|
98
|
-
get_request_options,
|
|
99
|
-
should_prefetch_url
|
|
100
|
-
}
|
|
1
|
+
const parse_url = require('url').parse
|
|
2
|
+
|
|
3
|
+
const regexs = {
|
|
4
|
+
origin: new RegExp('^(https?://[^/]+)(?:/.*)?$', 'i'),
|
|
5
|
+
ts: new RegExp('\\.ts(?:[\\?#]|$)', 'i')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// btoa
|
|
9
|
+
const base64_encode = function(str) {
|
|
10
|
+
return Buffer.from(str, 'binary').toString('base64')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// atob
|
|
14
|
+
const base64_decode = function(str) {
|
|
15
|
+
return Buffer.from(str, 'base64').toString('binary')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const add_CORS_headers = function(res) {
|
|
19
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
20
|
+
res.setHeader('Access-Control-Allow-Methods', '*')
|
|
21
|
+
res.setHeader('Access-Control-Allow-Headers', '*')
|
|
22
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true')
|
|
23
|
+
res.setHeader('Access-Control-Max-Age', '86400')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const debug = function() {
|
|
27
|
+
const args = [...arguments]
|
|
28
|
+
const params = args.shift()
|
|
29
|
+
const verbosity = args.shift()
|
|
30
|
+
const append_LF = true
|
|
31
|
+
|
|
32
|
+
const {debug_level} = params
|
|
33
|
+
|
|
34
|
+
if (append_LF) args.push("\n")
|
|
35
|
+
|
|
36
|
+
if (debug_level >= verbosity) {
|
|
37
|
+
console.log.apply(console.log, args)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const get_request_options = function(params, url, is_m3u8, referer_url) {
|
|
42
|
+
const {req_headers, req_options, hooks} = params
|
|
43
|
+
|
|
44
|
+
const additional_req_options = (hooks && (hooks instanceof Object) && hooks.add_request_options && (typeof hooks.add_request_options === 'function'))
|
|
45
|
+
? hooks.add_request_options(url, is_m3u8)
|
|
46
|
+
: null
|
|
47
|
+
|
|
48
|
+
const additional_req_headers = (hooks && (hooks instanceof Object) && hooks.add_request_headers && (typeof hooks.add_request_headers === 'function'))
|
|
49
|
+
? hooks.add_request_headers(url, is_m3u8)
|
|
50
|
+
: null
|
|
51
|
+
|
|
52
|
+
if (!req_options && !additional_req_options && !req_headers && !additional_req_headers && !referer_url) return url
|
|
53
|
+
|
|
54
|
+
const request_options = Object.assign(
|
|
55
|
+
{},
|
|
56
|
+
parse_url(url),
|
|
57
|
+
(req_options || {}),
|
|
58
|
+
(additional_req_options || {})
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
request_options.headers = Object.assign(
|
|
62
|
+
{},
|
|
63
|
+
(( req_options && req_options.headers) ? req_options.headers : {}),
|
|
64
|
+
((additional_req_options && additional_req_options.headers) ? additional_req_options.headers : {}),
|
|
65
|
+
(req_headers || {}),
|
|
66
|
+
(additional_req_headers || {}),
|
|
67
|
+
(referer_url ? {"referer": referer_url, "origin": referer_url.replace(regexs.origin, '$1')} : {})
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return request_options
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const should_prefetch_url = function(params, url) {
|
|
74
|
+
const {hooks, cache_segments} = params
|
|
75
|
+
|
|
76
|
+
let do_prefetch = !!cache_segments
|
|
77
|
+
|
|
78
|
+
if (do_prefetch) {
|
|
79
|
+
do_prefetch = regexs.ts.test(url)
|
|
80
|
+
|
|
81
|
+
if (hooks && (hooks instanceof Object) && hooks.prefetch && (typeof hooks.prefetch === 'function')) {
|
|
82
|
+
const override_prefetch = hooks.prefetch(url)
|
|
83
|
+
|
|
84
|
+
if ((typeof override_prefetch === 'boolean') && (override_prefetch !== do_prefetch)) {
|
|
85
|
+
debug(params, 3, 'prefetch override:', (override_prefetch ? 'allow' : 'deny'), url)
|
|
86
|
+
do_prefetch = override_prefetch
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return do_prefetch
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
base64_encode,
|
|
95
|
+
base64_decode,
|
|
96
|
+
add_CORS_headers,
|
|
97
|
+
debug,
|
|
98
|
+
get_request_options,
|
|
99
|
+
should_prefetch_url
|
|
100
|
+
}
|
package/package.json
CHANGED