@warren-bank/hls-proxy 2.0.1 → 3.1.0
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/hls-proxy/manifest_parser.js +309 -156
- package/hls-proxy/proxy.js +12 -40
- package/hls-proxy/segment_cache.js +4 -6
- package/hls-proxy/utils.js +64 -7
- package/package.json +3 -2
|
@@ -1,50 +1,102 @@
|
|
|
1
|
+
const {URL} = require('@warren-bank/url')
|
|
1
2
|
const utils = require('./utils')
|
|
2
3
|
|
|
3
4
|
const regexs = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
urls: new RegExp('(^|(?<!(?:KEYFORMAT=))[\\s\'"])((?:https?:/)?/)?((?:[^\\?#/\\s\'"]*/)+?)?([^\\?#,/\\s\'"]+?)(\\.[^\\?#,/\\.\\s\'"]+(?:[\\?#][^\\s\'"]*)?)?([\\s\'"]|$)', 'img'),
|
|
9
|
-
keys: new RegExp('(^#EXT-X-KEY:(?:.+,)?URI=")([^"]+)(".*$)', 'img')
|
|
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
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
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-PART:',
|
|
27
|
+
'#EXT-X-PRELOAD-HINT:'
|
|
28
|
+
],
|
|
29
|
+
next_line: [
|
|
30
|
+
'#EXTINF:'
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
json: {
|
|
34
|
+
same_line: [
|
|
35
|
+
'#EXT-X-SESSION-DATA:'
|
|
36
|
+
],
|
|
37
|
+
next_line: []
|
|
38
|
+
},
|
|
39
|
+
key: {
|
|
40
|
+
same_line: [
|
|
41
|
+
'#EXT-X-KEY:'
|
|
42
|
+
],
|
|
43
|
+
next_line: []
|
|
44
|
+
},
|
|
45
|
+
other: {
|
|
46
|
+
same_line: [],
|
|
47
|
+
next_line: []
|
|
48
|
+
}
|
|
15
49
|
}
|
|
16
50
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
51
|
+
const meta_data_location_landmarks = {
|
|
52
|
+
is_vod: {
|
|
53
|
+
same_line: [
|
|
54
|
+
'#EXT-X-PLAYLIST-TYPE:',
|
|
55
|
+
'#EXT-X-ENDLIST'
|
|
56
|
+
],
|
|
57
|
+
resolve_value: {
|
|
58
|
+
'#EXT-X-PLAYLIST-TYPE:': (m3u8_line, landmark) => {
|
|
59
|
+
const value = m3u8_line.substring(landmark.length, landmark.length + 3)
|
|
60
|
+
return (value.toUpperCase() === 'VOD')
|
|
61
|
+
},
|
|
62
|
+
'#EXT-X-ENDLIST': () => true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
seg_duration_ms: {
|
|
66
|
+
same_line: [
|
|
67
|
+
'#EXT-X-TARGETDURATION:'
|
|
68
|
+
],
|
|
69
|
+
resolve_value: {
|
|
70
|
+
'#EXT-X-TARGETDURATION:': (m3u8_line, landmark) => {
|
|
71
|
+
m3u8_line = m3u8_line.substring(landmark.length)
|
|
72
|
+
const value = parseInt(m3u8_line, 10)
|
|
73
|
+
return isNaN(value)
|
|
74
|
+
? null
|
|
75
|
+
: (value * 1000) // convert seconds to ms
|
|
76
|
+
}
|
|
24
77
|
}
|
|
25
78
|
}
|
|
26
|
-
return ts_file_ext
|
|
27
79
|
}
|
|
28
80
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
81
|
+
const get_vod_start_at_ms = function(m3u8_url) {
|
|
82
|
+
try {
|
|
83
|
+
const matches = regexs.vod_start_at.exec(m3u8_url)
|
|
32
84
|
|
|
33
|
-
|
|
34
|
-
|
|
85
|
+
if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
|
|
86
|
+
throw ''
|
|
35
87
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
let offset
|
|
89
|
+
offset = matches[1]
|
|
90
|
+
offset = parse_HHMMSS_to_seconds(offset)
|
|
91
|
+
offset = offset * 1000 // convert seconds to ms
|
|
40
92
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
93
|
+
return offset
|
|
94
|
+
}
|
|
95
|
+
catch(e) {
|
|
96
|
+
const def_offset = null
|
|
45
97
|
|
|
46
|
-
|
|
47
|
-
|
|
98
|
+
return def_offset
|
|
99
|
+
}
|
|
48
100
|
}
|
|
49
101
|
|
|
50
102
|
const parse_HHMMSS_to_seconds = function(str) {
|
|
@@ -60,25 +112,225 @@ const parse_HHMMSS_to_seconds = function(str) {
|
|
|
60
112
|
return seconds
|
|
61
113
|
}
|
|
62
114
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
115
|
+
// returns: {
|
|
116
|
+
// meta_data: {is_vod, seg_duration_ms},
|
|
117
|
+
// embedded_urls: [{line_index, url_indices, url_type, original_match_url, resolved_match_url, redirected_url, unencoded_url, encoded_url, referer_url}],
|
|
118
|
+
// prefetch_urls: [],
|
|
119
|
+
// modified_m3u8: ''
|
|
120
|
+
// }
|
|
121
|
+
const parse_manifest = function(m3u8_content, m3u8_url, referer_url, hooks, cache_segments, debug, vod_start_at_ms, redirected_base_url, should_prefetch_url) {
|
|
122
|
+
const m3u8_lines = m3u8_content.split(regexs.m3u8_line_separator)
|
|
123
|
+
m3u8_content = null
|
|
124
|
+
|
|
125
|
+
const meta_data = {}
|
|
126
|
+
const embedded_urls = extract_embedded_urls(m3u8_lines, m3u8_url, referer_url, (cache_segments ? meta_data : null))
|
|
127
|
+
const prefetch_urls = []
|
|
128
|
+
|
|
129
|
+
if (embedded_urls && Array.isArray(embedded_urls) && embedded_urls.length) {
|
|
130
|
+
embedded_urls.forEach(embedded_url => {
|
|
131
|
+
redirect_embedded_url(embedded_url, hooks, m3u8_url, debug)
|
|
132
|
+
finalize_embedded_url(embedded_url, vod_start_at_ms, debug)
|
|
133
|
+
encode_embedded_url(embedded_url, redirected_base_url, debug)
|
|
134
|
+
get_prefetch_url(embedded_url, should_prefetch_url, prefetch_urls)
|
|
135
|
+
modify_m3u8_line(embedded_url, m3u8_lines)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
66
138
|
|
|
67
|
-
|
|
68
|
-
|
|
139
|
+
return {
|
|
140
|
+
meta_data,
|
|
141
|
+
embedded_urls,
|
|
142
|
+
prefetch_urls,
|
|
143
|
+
modified_m3u8: m3u8_lines.filter(line => !!line).join("\n")
|
|
144
|
+
}
|
|
145
|
+
}
|
|
69
146
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
147
|
+
const extract_embedded_urls = function(m3u8_lines, m3u8_url, referer_url, meta_data) {
|
|
148
|
+
const embedded_urls = []
|
|
149
|
+
|
|
150
|
+
let m3u8_line, has_next_m3u8_line, next_m3u8_line, matches, matching_landmark, matching_url
|
|
151
|
+
|
|
152
|
+
for (let i=0; i < m3u8_lines.length; i++) {
|
|
153
|
+
m3u8_line = m3u8_lines[i]
|
|
154
|
+
has_next_m3u8_line = ((i+1) < m3u8_lines.length)
|
|
155
|
+
|
|
156
|
+
matches = regexs.m3u8_line_landmark.exec(m3u8_line)
|
|
157
|
+
if (!matches) continue
|
|
158
|
+
matching_landmark = matches[1]
|
|
159
|
+
|
|
160
|
+
matches = regexs.m3u8_line_url.exec(m3u8_line)
|
|
161
|
+
matching_url = matches
|
|
162
|
+
? matches[1]
|
|
163
|
+
: null
|
|
164
|
+
|
|
165
|
+
if (meta_data !== null)
|
|
166
|
+
extract_meta_data(meta_data, m3u8_line, matching_landmark)
|
|
167
|
+
|
|
168
|
+
for (let url_type in url_location_landmarks) {
|
|
169
|
+
if (matching_url && (url_location_landmarks[url_type]['same_line'].indexOf(matching_landmark) >= 0)) {
|
|
170
|
+
embedded_urls.push({
|
|
171
|
+
line_index: i,
|
|
172
|
+
url_indices: matches.indices[1],
|
|
173
|
+
url_type: url_type,
|
|
174
|
+
original_match_url: matching_url,
|
|
175
|
+
resolved_match_url: (new URL(matching_url, m3u8_url)).href,
|
|
176
|
+
redirected_url: null,
|
|
177
|
+
referer_url: referer_url,
|
|
178
|
+
encoded_url: null
|
|
179
|
+
})
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
if (has_next_m3u8_line && (url_location_landmarks[url_type]['next_line'].indexOf(matching_landmark) >= 0)) {
|
|
183
|
+
next_m3u8_line = m3u8_lines[i+1].trim()
|
|
184
|
+
|
|
185
|
+
if (next_m3u8_line && (next_m3u8_line[0] !== '#')) {
|
|
186
|
+
i++
|
|
187
|
+
|
|
188
|
+
embedded_urls.push({
|
|
189
|
+
line_index: i,
|
|
190
|
+
url_indices: null,
|
|
191
|
+
url_type: url_type,
|
|
192
|
+
original_match_url: next_m3u8_line,
|
|
193
|
+
resolved_match_url: (new URL(next_m3u8_line, m3u8_url)).href,
|
|
194
|
+
redirected_url: null,
|
|
195
|
+
referer_url: referer_url,
|
|
196
|
+
encoded_url: null
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
74
203
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
catch(e) {
|
|
78
|
-
const def_offset = null
|
|
204
|
+
return embedded_urls
|
|
205
|
+
}
|
|
79
206
|
|
|
80
|
-
|
|
81
|
-
|
|
207
|
+
const extract_meta_data = function(meta_data, m3u8_line, matching_landmark) {
|
|
208
|
+
for (let meta_data_key in meta_data_location_landmarks) {
|
|
209
|
+
if (meta_data_location_landmarks[meta_data_key]['same_line'].indexOf(matching_landmark) >= 0) {
|
|
210
|
+
const func = meta_data_location_landmarks[meta_data_key]['resolve_value'][matching_landmark]
|
|
211
|
+
if (typeof func === 'function') {
|
|
212
|
+
const meta_data_value = func(m3u8_line, matching_landmark)
|
|
213
|
+
if ((meta_data_value !== undefined) && (meta_data_value !== null)) {
|
|
214
|
+
meta_data[meta_data_key] = meta_data_value
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const redirect_embedded_url = function(embedded_url, hooks, m3u8_url, debug) {
|
|
222
|
+
if (hooks && (hooks instanceof Object) && hooks.redirect && (typeof hooks.redirect === 'function')) {
|
|
223
|
+
let url, url_type, referer_url, result
|
|
224
|
+
|
|
225
|
+
url = embedded_url.resolved_match_url
|
|
226
|
+
url_type = null
|
|
227
|
+
referer_url = null
|
|
228
|
+
|
|
229
|
+
debug(3, 'redirecting (pre-hook):', url)
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
result = hooks.redirect(url, embedded_url.referer_url)
|
|
233
|
+
|
|
234
|
+
if (result) {
|
|
235
|
+
if (typeof result === 'string') {
|
|
236
|
+
url = result
|
|
237
|
+
}
|
|
238
|
+
else if (result instanceof Object) {
|
|
239
|
+
url = result.url
|
|
240
|
+
url_type = result.url_type
|
|
241
|
+
referer_url = result.referer_url
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (typeof url !== 'string') throw new Error('bad return value')
|
|
246
|
+
|
|
247
|
+
url = url.trim()
|
|
248
|
+
|
|
249
|
+
if (url.length && (url.toLowerCase().indexOf('http') !== 0))
|
|
250
|
+
url = (new URL(url, m3u8_url)).href
|
|
251
|
+
}
|
|
252
|
+
catch(e) {
|
|
253
|
+
url = ''
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (url) {
|
|
257
|
+
embedded_url.redirected_url = url
|
|
258
|
+
|
|
259
|
+
if (typeof url_type === 'string') {
|
|
260
|
+
url_type = url_type.toLowerCase().trim()
|
|
261
|
+
|
|
262
|
+
if (url_type.length)
|
|
263
|
+
embedded_url.url_type = url_type
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (typeof referer_url === 'string') {
|
|
267
|
+
referer_url = referer_url.trim()
|
|
268
|
+
|
|
269
|
+
if (referer_url.length && (referer_url.toLowerCase().indexOf('http') === 0))
|
|
270
|
+
embedded_url.referer_url = referer_url
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
debug(3, 'redirecting (post-hook):', url)
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
embedded_url.redirected_url = ''
|
|
277
|
+
|
|
278
|
+
debug(3, 'redirecting (post-hook):', 'URL filtered, removed from manifest')
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const finalize_embedded_url = function(embedded_url, vod_start_at_ms, debug) {
|
|
284
|
+
if (embedded_url.redirected_url === '') {
|
|
285
|
+
embedded_url.unencoded_url = ''
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const url = embedded_url.redirected_url || embedded_url.resolved_match_url
|
|
289
|
+
|
|
290
|
+
if (embedded_url.url_type)
|
|
291
|
+
debug(3, 'url type:', embedded_url.url_type)
|
|
292
|
+
|
|
293
|
+
debug(2, 'redirecting:', url)
|
|
294
|
+
|
|
295
|
+
if (vod_start_at_ms && (embedded_url.url_type === 'm3u8'))
|
|
296
|
+
url += `#vod_start=${Math.floor(vod_start_at_ms/1000)}`
|
|
297
|
+
|
|
298
|
+
if (embedded_url.referer_url)
|
|
299
|
+
url += `|${embedded_url.referer_url}`
|
|
300
|
+
|
|
301
|
+
embedded_url.unencoded_url = url
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const encode_embedded_url = function(embedded_url, redirected_base_url, debug) {
|
|
306
|
+
embedded_url.encoded_url = (embedded_url.unencoded_url)
|
|
307
|
+
? `${redirected_base_url}/${ utils.base64_encode(embedded_url.unencoded_url) }.${embedded_url.url_type || 'other'}`
|
|
308
|
+
: ''
|
|
309
|
+
|
|
310
|
+
if (embedded_url.encoded_url)
|
|
311
|
+
debug(3, 'redirecting (proxied):', embedded_url.encoded_url)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const get_prefetch_url = function(embedded_url, should_prefetch_url, prefetch_urls = []) {
|
|
315
|
+
if (embedded_url.redirected_url !== '') {
|
|
316
|
+
const url = embedded_url.redirected_url || embedded_url.resolved_match_url
|
|
317
|
+
|
|
318
|
+
if (should_prefetch_url(url, embedded_url.url_type))
|
|
319
|
+
prefetch_urls.push(url)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const modify_m3u8_line = function(embedded_url, m3u8_lines) {
|
|
324
|
+
const {line_index, url_indices, encoded_url} = embedded_url
|
|
325
|
+
|
|
326
|
+
if (url_indices && Array.isArray(url_indices) && (url_indices.length === 2)) {
|
|
327
|
+
const m3u8_line = m3u8_lines[line_index]
|
|
328
|
+
|
|
329
|
+
m3u8_lines[line_index] = m3u8_line.substring(0, url_indices[0]) + encoded_url + m3u8_line.substring(url_indices[1])
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
m3u8_lines[line_index] = encoded_url
|
|
333
|
+
}
|
|
82
334
|
}
|
|
83
335
|
|
|
84
336
|
const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_url, referer_url, redirected_base_url) {
|
|
@@ -93,11 +345,6 @@ const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_u
|
|
|
93
345
|
m3u8_content = hooks.modify_m3u8_content(m3u8_content, m3u8_url) || m3u8_content
|
|
94
346
|
}
|
|
95
347
|
|
|
96
|
-
const base_urls = {
|
|
97
|
-
"relative": m3u8_url.replace(/[\?#].*$/, '').replace(/[^\/]+$/, ''),
|
|
98
|
-
"absolute": m3u8_url.replace(/(:\/\/[^\/]+).*$/, '$1')
|
|
99
|
-
}
|
|
100
|
-
|
|
101
348
|
const debug_divider = (debug_level >= 4)
|
|
102
349
|
? ('-').repeat(40)
|
|
103
350
|
: ''
|
|
@@ -106,28 +353,13 @@ const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_u
|
|
|
106
353
|
debug(4, 'proxied response (original m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
107
354
|
}
|
|
108
355
|
|
|
109
|
-
|
|
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
|
|
356
|
+
let is_vod, seg_duration_ms, prefetch_urls
|
|
120
357
|
|
|
121
358
|
// only used with prefetch
|
|
122
359
|
const vod_start_at_ms = (cache_segments)
|
|
123
360
|
? get_vod_start_at_ms(m3u8_url)
|
|
124
361
|
: null
|
|
125
362
|
|
|
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
363
|
// only used with prefetch
|
|
132
364
|
const perform_prefetch = (cache_segments)
|
|
133
365
|
? (urls, dont_touch_access) => {
|
|
@@ -158,89 +390,17 @@ const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_u
|
|
|
158
390
|
}
|
|
159
391
|
: null
|
|
160
392
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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})
|
|
393
|
+
{
|
|
394
|
+
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)
|
|
395
|
+
is_vod = !!parsed_manifest.meta_data.is_vod // default: false => hls live stream
|
|
396
|
+
seg_duration_ms = parsed_manifest.meta_data.seg_duration_ms || 10000 // default: 10 seconds in ms
|
|
397
|
+
prefetch_urls = parsed_manifest.prefetch_urls
|
|
398
|
+
m3u8_content = parsed_manifest.modified_m3u8
|
|
178
399
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
matching_url = `${base_urls.relative}${rel_path || ''}${file_name}${file_ext || ''}`
|
|
400
|
+
if (debug_level >= 4) {
|
|
401
|
+
debug(4, 'parsed manifest:', `\n${debug_divider}\n${JSON.stringify(parsed_manifest, null, 2)}\n${debug_divider}`)
|
|
182
402
|
}
|
|
183
|
-
|
|
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
|
-
})
|
|
403
|
+
}
|
|
244
404
|
|
|
245
405
|
if (prefetch_urls.length) {
|
|
246
406
|
if (is_vod && vod_start_at_ms) {
|
|
@@ -316,13 +476,6 @@ const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_u
|
|
|
316
476
|
perform_prefetch(prefetch_urls)
|
|
317
477
|
}
|
|
318
478
|
|
|
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
479
|
if (debug_level >= 4) {
|
|
327
480
|
debug(4, 'proxied response (modified m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
|
|
328
481
|
}
|
package/hls-proxy/proxy.js
CHANGED
|
@@ -2,19 +2,15 @@ const request = require('@warren-bank/node-request').request
|
|
|
2
2
|
const parser = require('./manifest_parser')
|
|
3
3
|
const utils = require('./utils')
|
|
4
4
|
|
|
5
|
-
const regexs = {
|
|
6
|
-
wrap: new RegExp('^(.*)/([^\\._/\\?#]+)(?:[\\._][^/\\?#]*)?(?:[\\?#].*)?$', 'i'),
|
|
7
|
-
m3u8: new RegExp('\\.m3u8(?:[\\?#]|$)', 'i')
|
|
8
|
-
}
|
|
9
|
-
|
|
10
5
|
const get_middleware = function(params) {
|
|
11
|
-
const {
|
|
12
|
-
let
|
|
6
|
+
const {cache_segments} = params
|
|
7
|
+
let {acl_whitelist} = params
|
|
13
8
|
|
|
14
9
|
const segment_cache = require('./segment_cache')(params)
|
|
15
10
|
const {get_segment, add_listener} = segment_cache
|
|
16
11
|
|
|
17
12
|
const debug = utils.debug.bind(null, params)
|
|
13
|
+
const parse_req_url = utils.parse_req_url.bind(null, params)
|
|
18
14
|
const get_request_options = utils.get_request_options.bind(null, params)
|
|
19
15
|
const modify_m3u8_content = parser.modify_m3u8_content.bind(null, params, segment_cache)
|
|
20
16
|
|
|
@@ -42,29 +38,7 @@ const get_middleware = function(params) {
|
|
|
42
38
|
|
|
43
39
|
utils.add_CORS_headers(res)
|
|
44
40
|
|
|
45
|
-
const
|
|
46
|
-
if (!regexs.wrap.test(req.url))
|
|
47
|
-
return ['', '']
|
|
48
|
-
|
|
49
|
-
let url, url_lc, index
|
|
50
|
-
|
|
51
|
-
url = utils.base64_decode( req.url.replace(regexs.wrap, '$2') ).trim()
|
|
52
|
-
url_lc = url.toLowerCase()
|
|
53
|
-
|
|
54
|
-
index = url_lc.indexOf('http')
|
|
55
|
-
if (index !== 0)
|
|
56
|
-
return ['', '']
|
|
57
|
-
|
|
58
|
-
index = url_lc.indexOf('|http')
|
|
59
|
-
if (index >=0) {
|
|
60
|
-
const referer_url = url.substring(index + 1, url.length)
|
|
61
|
-
url = url.substring(0, index).trim()
|
|
62
|
-
return [url, referer_url]
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
return [url, '']
|
|
66
|
-
}
|
|
67
|
-
})()
|
|
41
|
+
const {redirected_base_url, url_type, url, referer_url} = parse_req_url(req)
|
|
68
42
|
|
|
69
43
|
if (!url) {
|
|
70
44
|
res.writeHead(400)
|
|
@@ -72,22 +46,22 @@ const get_middleware = function(params) {
|
|
|
72
46
|
return
|
|
73
47
|
}
|
|
74
48
|
|
|
75
|
-
const is_m3u8 =
|
|
49
|
+
const is_m3u8 = (url_type === 'm3u8')
|
|
76
50
|
|
|
77
|
-
const
|
|
78
|
-
res.writeHead(200, { "Content-Type":
|
|
51
|
+
const send_cache_segment = function(segment) {
|
|
52
|
+
res.writeHead(200, { "Content-Type": utils.get_content_type(url_type) })
|
|
79
53
|
res.end(segment)
|
|
80
54
|
}
|
|
81
55
|
|
|
82
56
|
if (cache_segments && !is_m3u8) {
|
|
83
|
-
let segment = get_segment(url)
|
|
57
|
+
let segment = get_segment(url, url_type) // Buffer (cached segment data), false (prefetch is pending: add callback), undefined (no prefetch is pending)
|
|
84
58
|
|
|
85
|
-
if (segment && segment.length) {
|
|
86
|
-
|
|
59
|
+
if (segment && segment.length) { // Buffer (cached segment data)
|
|
60
|
+
send_cache_segment(segment)
|
|
87
61
|
return
|
|
88
62
|
}
|
|
89
|
-
else if (segment === false) {
|
|
90
|
-
add_listener(url,
|
|
63
|
+
else if (segment === false) { // false (prefetch is pending: add callback)
|
|
64
|
+
add_listener(url, url_type, send_cache_segment)
|
|
91
65
|
return
|
|
92
66
|
}
|
|
93
67
|
}
|
|
@@ -108,8 +82,6 @@ const get_middleware = function(params) {
|
|
|
108
82
|
? redirects[(redirects.length - 1)]
|
|
109
83
|
: url
|
|
110
84
|
|
|
111
|
-
const redirected_base_url = `${ is_secure ? 'https' : 'http' }://${host || req.headers.host}${req.url.replace(regexs.wrap, '$1')}`
|
|
112
|
-
|
|
113
85
|
res.writeHead(200, { "Content-Type": "application/x-mpegURL" })
|
|
114
86
|
res.end( modify_m3u8_content(response.toString().trim(), m3u8_url, referer_url, redirected_base_url) )
|
|
115
87
|
}
|
|
@@ -154,8 +154,6 @@ module.exports = function(params) {
|
|
|
154
154
|
const prefetch_segment = function(m3u8_url, url, referer_url, dont_touch_access) {
|
|
155
155
|
let promise = Promise.resolve()
|
|
156
156
|
|
|
157
|
-
if (! should_prefetch_url(url)) return promise
|
|
158
|
-
|
|
159
157
|
if (cache[m3u8_url] === undefined) {
|
|
160
158
|
// initialize a new data structure
|
|
161
159
|
cache[m3u8_url] = {access: 0, ts: []}
|
|
@@ -218,8 +216,8 @@ module.exports = function(params) {
|
|
|
218
216
|
return promise
|
|
219
217
|
}
|
|
220
218
|
|
|
221
|
-
const get_segment = function(url) {
|
|
222
|
-
if (! should_prefetch_url(url)) return undefined
|
|
219
|
+
const get_segment = function(url, url_type) {
|
|
220
|
+
if (! should_prefetch_url(url, url_type)) return undefined
|
|
223
221
|
|
|
224
222
|
let debug_url = (debug_level >= 3) ? url : get_publickey_from_url(url)
|
|
225
223
|
|
|
@@ -253,8 +251,8 @@ module.exports = function(params) {
|
|
|
253
251
|
return segment
|
|
254
252
|
}
|
|
255
253
|
|
|
256
|
-
const add_listener = function(url, cb) {
|
|
257
|
-
if (! should_prefetch_url(url)) return false
|
|
254
|
+
const add_listener = function(url, url_type, cb) {
|
|
255
|
+
if (! should_prefetch_url(url, url_type)) return false
|
|
258
256
|
|
|
259
257
|
let debug_url = (debug_level >= 3) ? url : get_publickey_from_url(url)
|
|
260
258
|
|
package/hls-proxy/utils.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const parse_url = require('url').parse
|
|
1
|
+
const parse_url = require('@warren-bank/url').parse
|
|
2
2
|
|
|
3
3
|
const regexs = {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
req_url: new RegExp('^(.*)/([^\\._/\\?#]+)(?:[\\._]([^/\\?#]*))?(?:[\\?#].*)?$', 'i'),
|
|
5
|
+
origin: new RegExp('^(https?://[^/]+)(?:/.*)?$', 'i')
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
// btoa
|
|
@@ -15,6 +15,61 @@ const base64_decode = function(str) {
|
|
|
15
15
|
return Buffer.from(str, 'base64').toString('binary')
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const parse_req_url = function(params, req) {
|
|
19
|
+
const {is_secure, host} = params
|
|
20
|
+
|
|
21
|
+
const result = {redirected_base_url: '', url_type: '', url: '', referer_url: ''}
|
|
22
|
+
|
|
23
|
+
const matches = regexs.req_url.exec(req.url)
|
|
24
|
+
|
|
25
|
+
if (matches) {
|
|
26
|
+
result.redirected_base_url = `${ is_secure ? 'https' : 'http' }://${host || req.headers.host}${matches[1] || ''}`
|
|
27
|
+
|
|
28
|
+
if (matches[3])
|
|
29
|
+
result.url_type = matches[3].toLowerCase().trim()
|
|
30
|
+
|
|
31
|
+
let url, url_lc, index
|
|
32
|
+
|
|
33
|
+
url = base64_decode( matches[2] ).trim()
|
|
34
|
+
url_lc = url.toLowerCase()
|
|
35
|
+
index = url_lc.indexOf('http')
|
|
36
|
+
|
|
37
|
+
if (index === 0) {
|
|
38
|
+
index = url_lc.indexOf('|http')
|
|
39
|
+
|
|
40
|
+
if (index > 0) {
|
|
41
|
+
url = url.substring(0, index).trim()
|
|
42
|
+
|
|
43
|
+
result.referer_url = url.substring(index + 1, url.length)
|
|
44
|
+
}
|
|
45
|
+
result.url = url
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const get_content_type = function(url_type) {
|
|
53
|
+
let content_type
|
|
54
|
+
switch(url_type) {
|
|
55
|
+
case 'm3u8':
|
|
56
|
+
content_type = 'application/x-mpegurl'
|
|
57
|
+
break
|
|
58
|
+
case 'ts':
|
|
59
|
+
content_type = 'video/MP2T'
|
|
60
|
+
break
|
|
61
|
+
case 'json':
|
|
62
|
+
content_type = 'application/json'
|
|
63
|
+
break
|
|
64
|
+
case 'key':
|
|
65
|
+
case 'other':
|
|
66
|
+
default:
|
|
67
|
+
content_type = 'application/octet-stream'
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
return content_type
|
|
71
|
+
}
|
|
72
|
+
|
|
18
73
|
const add_CORS_headers = function(res) {
|
|
19
74
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
20
75
|
res.setHeader('Access-Control-Allow-Methods', '*')
|
|
@@ -70,16 +125,16 @@ const get_request_options = function(params, url, is_m3u8, referer_url) {
|
|
|
70
125
|
return request_options
|
|
71
126
|
}
|
|
72
127
|
|
|
73
|
-
const should_prefetch_url = function(params, url) {
|
|
128
|
+
const should_prefetch_url = function(params, url, url_type) {
|
|
74
129
|
const {hooks, cache_segments} = params
|
|
75
130
|
|
|
76
|
-
let do_prefetch = !!cache_segments
|
|
131
|
+
let do_prefetch = !!url && !!cache_segments
|
|
77
132
|
|
|
78
133
|
if (do_prefetch) {
|
|
79
|
-
do_prefetch =
|
|
134
|
+
do_prefetch = (url_type === 'ts') || (url_type === 'key')
|
|
80
135
|
|
|
81
136
|
if (hooks && (hooks instanceof Object) && hooks.prefetch && (typeof hooks.prefetch === 'function')) {
|
|
82
|
-
const override_prefetch = hooks.prefetch(url)
|
|
137
|
+
const override_prefetch = hooks.prefetch(url, url_type)
|
|
83
138
|
|
|
84
139
|
if ((typeof override_prefetch === 'boolean') && (override_prefetch !== do_prefetch)) {
|
|
85
140
|
debug(params, 3, 'prefetch override:', (override_prefetch ? 'allow' : 'deny'), url)
|
|
@@ -93,6 +148,8 @@ const should_prefetch_url = function(params, url) {
|
|
|
93
148
|
module.exports = {
|
|
94
149
|
base64_encode,
|
|
95
150
|
base64_decode,
|
|
151
|
+
parse_req_url,
|
|
152
|
+
get_content_type,
|
|
96
153
|
add_CORS_headers,
|
|
97
154
|
debug,
|
|
98
155
|
get_request_options,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-bank/hls-proxy",
|
|
3
3
|
"description": "Node.js server to proxy HLS video streams",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"start": "node hls-proxy/bin/hlsd.js",
|
|
7
7
|
"sudo": "sudo node hls-proxy/bin/hlsd.js"
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@warren-bank/node-process-argv": "^1.2.1",
|
|
14
|
-
"@warren-bank/node-request": "^2.0.10"
|
|
14
|
+
"@warren-bank/node-request": "^2.0.10",
|
|
15
|
+
"@warren-bank/url": "^3.1.0"
|
|
15
16
|
},
|
|
16
17
|
"license": "GPL-2.0",
|
|
17
18
|
"author": {
|