@warren-bank/hls-proxy 3.3.0 → 3.4.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/README.md +11 -0
- package/hls-proxy/bin/hlsd.js +14 -12
- package/hls-proxy/bin/lib/help.js +2 -0
- package/hls-proxy/bin/lib/process_argv.js +2 -0
- package/hls-proxy/manifest_parser.js +21 -4
- package/hls-proxy/proxy.js +4 -4
- package/hls-proxy/segment_cache.js +49 -40
- package/hls-proxy/segment_cache_storage/filesystem/index.js +56 -0
- package/hls-proxy/segment_cache_storage/index.js +14 -0
- package/hls-proxy/segment_cache_storage/memory/index.js +17 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -128,6 +128,8 @@ options:
|
|
|
128
128
|
--max-segments <number>
|
|
129
129
|
--cache-timeout <number>
|
|
130
130
|
--cache-key <number>
|
|
131
|
+
--cache-storage <adapter>
|
|
132
|
+
--cache-storage-fs-dirpath <dirpath>
|
|
131
133
|
-v <number>
|
|
132
134
|
--acl-whitelist <ip_address_list>
|
|
133
135
|
--http-proxy <http[s]://[user:pass@]hostname:port>
|
|
@@ -328,6 +330,15 @@ options:
|
|
|
328
330
|
* full filename of .ts file
|
|
329
331
|
* `2`:
|
|
330
332
|
* full URL of .ts file
|
|
333
|
+
* _--cache-storage_ selects a storage adapter that is used to hold the cache of prefetched video segments
|
|
334
|
+
* this option is only meaningful when _--prefetch_ is enabled
|
|
335
|
+
* `memory` (default):
|
|
336
|
+
* uses RAM
|
|
337
|
+
* `filesystem`:
|
|
338
|
+
* each video segment is written to a new file within a specified directory
|
|
339
|
+
* filenames are random and unique
|
|
340
|
+
* _--cache-storage-fs-dirpath_ specifies the directory in which to save video segments when using a filesystem-based cache storage adapter
|
|
341
|
+
* this option is only meaningful when _--prefetch_ is enabled and _--cache-storage_ is `filesystem`
|
|
331
342
|
* _-v_ sets logging verbosity level:
|
|
332
343
|
* `-1`:
|
|
333
344
|
* silent
|
package/hls-proxy/bin/hlsd.js
CHANGED
|
@@ -32,18 +32,20 @@ const server = (use_tls)
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
const middleware = require('../proxy')({
|
|
35
|
-
is_secure:
|
|
36
|
-
host:
|
|
37
|
-
req_headers:
|
|
38
|
-
req_options:
|
|
39
|
-
hooks:
|
|
40
|
-
cache_segments:
|
|
41
|
-
max_segments:
|
|
42
|
-
cache_timeout:
|
|
43
|
-
cache_key:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
is_secure: use_tls,
|
|
36
|
+
host: normalize_host(argv_vals["--host"], argv_vals["--port"]),
|
|
37
|
+
req_headers: argv_vals["--req-headers"],
|
|
38
|
+
req_options: argv_vals["--req-options"],
|
|
39
|
+
hooks: argv_vals["--hooks"],
|
|
40
|
+
cache_segments: argv_vals["--prefetch"],
|
|
41
|
+
max_segments: argv_vals["--max-segments"],
|
|
42
|
+
cache_timeout: argv_vals["--cache-timeout"],
|
|
43
|
+
cache_key: argv_vals["--cache-key"],
|
|
44
|
+
cache_storage: argv_vals["--cache-storage"],
|
|
45
|
+
cache_storage_fs_dirpath: argv_vals["--cache-storage-fs-dirpath"],
|
|
46
|
+
debug_level: argv_vals["-v"],
|
|
47
|
+
acl_whitelist: argv_vals["--acl-whitelist"],
|
|
48
|
+
http_proxy: argv_vals["--http-proxy"]
|
|
47
49
|
})
|
|
48
50
|
|
|
49
51
|
if (middleware.connection)
|
|
@@ -24,6 +24,8 @@ options:
|
|
|
24
24
|
--max-segments <number>
|
|
25
25
|
--cache-timeout <number>
|
|
26
26
|
--cache-key <number>
|
|
27
|
+
--cache-storage <adapter>
|
|
28
|
+
--cache-storage-fs-dirpath <dirpath>
|
|
27
29
|
-v <number>
|
|
28
30
|
--acl-whitelist <ip_address_list>
|
|
29
31
|
--http-proxy <http[s]://[user:pass@]hostname:port>
|
|
@@ -29,6 +29,8 @@ const argv_flags = {
|
|
|
29
29
|
"--max-segments": {num: "int"},
|
|
30
30
|
"--cache-timeout": {num: "int"},
|
|
31
31
|
"--cache-key": {num: "int"},
|
|
32
|
+
"--cache-storage": {enum: ["memory", "filesystem"]},
|
|
33
|
+
"--cache-storage-fs-dirpath": {file: "path-exists"},
|
|
32
34
|
|
|
33
35
|
"-v": {num: "int"},
|
|
34
36
|
"--acl-whitelist": {},
|
|
@@ -121,10 +121,12 @@ const parse_manifest = function(m3u8_content, m3u8_url, referer_url, hooks, cach
|
|
|
121
121
|
if (embedded_urls && Array.isArray(embedded_urls) && embedded_urls.length) {
|
|
122
122
|
embedded_urls.forEach(embedded_url => {
|
|
123
123
|
redirect_embedded_url(embedded_url, hooks, m3u8_url, debug)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
if (validate_embedded_url(embedded_url)) {
|
|
125
|
+
finalize_embedded_url(embedded_url, vod_start_at_ms, debug)
|
|
126
|
+
encode_embedded_url(embedded_url, redirected_base_url, debug)
|
|
127
|
+
get_prefetch_url(embedded_url, should_prefetch_url, prefetch_urls)
|
|
128
|
+
modify_m3u8_line(embedded_url, m3u8_lines)
|
|
129
|
+
}
|
|
128
130
|
})
|
|
129
131
|
}
|
|
130
132
|
|
|
@@ -291,6 +293,21 @@ const redirect_embedded_url = function(embedded_url, hooks, m3u8_url, debug) {
|
|
|
291
293
|
}
|
|
292
294
|
}
|
|
293
295
|
|
|
296
|
+
const validate_embedded_url = function(embedded_url) {
|
|
297
|
+
if (embedded_url.redirected_url === '') {
|
|
298
|
+
return true
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
const url = new URL(
|
|
302
|
+
embedded_url.redirected_url || embedded_url.resolved_match_url
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
const supported_protocols = ['http:','https:']
|
|
306
|
+
|
|
307
|
+
return (url.protocol && (supported_protocols.indexOf(url.protocol.toLowerCase()) >= 0))
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
294
311
|
const finalize_embedded_url = function(embedded_url, vod_start_at_ms, debug) {
|
|
295
312
|
if (embedded_url.redirected_url === '') {
|
|
296
313
|
embedded_url.unencoded_url = ''
|
package/hls-proxy/proxy.js
CHANGED
|
@@ -35,7 +35,7 @@ const get_middleware = function(params) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// Create an HTTP tunneling proxy
|
|
38
|
-
middleware.request = (req, res) => {
|
|
38
|
+
middleware.request = async (req, res) => {
|
|
39
39
|
debug(3, 'proxying (raw):', req.url)
|
|
40
40
|
|
|
41
41
|
utils.add_CORS_headers(res)
|
|
@@ -56,13 +56,13 @@ const get_middleware = function(params) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
if (cache_segments && !is_m3u8) {
|
|
59
|
-
let segment = get_segment(url, url_type) // Buffer (cached segment data), false (prefetch is pending: add callback), undefined (no prefetch is pending)
|
|
59
|
+
let segment = await get_segment(url, url_type) // Buffer (cached segment data), false (prefetch is pending: add callback), undefined (no prefetch is pending)
|
|
60
60
|
|
|
61
|
-
if (segment && segment.length) {
|
|
61
|
+
if (segment && segment.length) { // Buffer (cached segment data)
|
|
62
62
|
send_cache_segment(segment)
|
|
63
63
|
return
|
|
64
64
|
}
|
|
65
|
-
else if (segment === false) {
|
|
65
|
+
else if (segment === false) { // false (prefetch is pending: add callback)
|
|
66
66
|
add_listener(url, url_type, send_cache_segment)
|
|
67
67
|
return
|
|
68
68
|
}
|
|
@@ -7,9 +7,10 @@ module.exports = function(params) {
|
|
|
7
7
|
|
|
8
8
|
if (!cache_segments) return {}
|
|
9
9
|
|
|
10
|
-
const debug
|
|
11
|
-
const get_request_options
|
|
12
|
-
const should_prefetch_url
|
|
10
|
+
const debug = utils.debug.bind(null, params)
|
|
11
|
+
const get_request_options = utils.get_request_options.bind(null, params)
|
|
12
|
+
const should_prefetch_url = utils.should_prefetch_url.bind(null, params)
|
|
13
|
+
const cache_storage_adapter = require('./segment_cache_storage')(params)
|
|
13
14
|
|
|
14
15
|
// maps: "m3u8_url" => {access: timestamp, ts: []}
|
|
15
16
|
const cache = {}
|
|
@@ -35,10 +36,10 @@ module.exports = function(params) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const clear_ts = function(m3u8_url) {
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
39
|
+
const ts = get_ts(m3u8_url)
|
|
40
|
+
if (!ts || !ts.length) return
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
ts_garbage_collect(m3u8_url, 0, ts.length - 1)
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const get_timestamp = function() {
|
|
@@ -78,13 +79,20 @@ module.exports = function(params) {
|
|
|
78
79
|
|
|
79
80
|
const ts_garbage_collect = function(m3u8_url, start, count) {
|
|
80
81
|
const ts = get_ts(m3u8_url)
|
|
81
|
-
if (!ts) return
|
|
82
|
+
if (!ts || !ts.length || (start >= ts.length)) return
|
|
82
83
|
|
|
83
84
|
for (let i=start; i < (start + count); i++) {
|
|
84
85
|
if (i >= ts.length) break
|
|
85
86
|
|
|
86
|
-
ts[i]
|
|
87
|
+
const segment = ts[i]
|
|
88
|
+
|
|
89
|
+
if (segment.has)
|
|
90
|
+
cache_storage_adapter.remove(segment.state)
|
|
91
|
+
|
|
92
|
+
segment.has = false
|
|
93
|
+
segment.cb = []
|
|
87
94
|
}
|
|
95
|
+
|
|
88
96
|
ts.splice(start, count)
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -132,7 +140,7 @@ module.exports = function(params) {
|
|
|
132
140
|
let i, segment
|
|
133
141
|
|
|
134
142
|
for (i=(ts.length - 1); i>=0; i--) {
|
|
135
|
-
segment = ts[i] // {key,
|
|
143
|
+
segment = ts[i] // {key, has, cb, state}
|
|
136
144
|
if (segment && (segment.key === key)) {
|
|
137
145
|
index = i
|
|
138
146
|
break
|
|
@@ -173,7 +181,7 @@ module.exports = function(params) {
|
|
|
173
181
|
|
|
174
182
|
// placeholder to prevent multiple download requests
|
|
175
183
|
index = ts.length
|
|
176
|
-
ts[index] = {key: get_privatekey_from_url(url),
|
|
184
|
+
ts[index] = {key: get_privatekey_from_url(url), has: false, cb: [], state: {}}
|
|
177
185
|
|
|
178
186
|
let options = get_request_options(url, /* is_m3u8= */ false, referer_url)
|
|
179
187
|
promise = request(options, '', {binary: true, stream: false, cookieJar: cookies.getCookieJar()})
|
|
@@ -188,15 +196,17 @@ module.exports = function(params) {
|
|
|
188
196
|
if (!dont_touch_access)
|
|
189
197
|
touch_access(m3u8_url)
|
|
190
198
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
199
|
+
const segment = ts[index]
|
|
200
|
+
segment.has = true
|
|
201
|
+
cache_storage_adapter.set(segment.state, response)
|
|
202
|
+
if (segment.cb.length) {
|
|
203
|
+
segment.cb.forEach((cb) => {
|
|
194
204
|
cb(response)
|
|
195
205
|
|
|
196
206
|
debug(1, 'cache (callback complete):', debug_url)
|
|
197
207
|
})
|
|
208
|
+
segment.cb = []
|
|
198
209
|
}
|
|
199
|
-
ts[index].databuffer = response
|
|
200
210
|
|
|
201
211
|
// cleanup: prune length of ts[] so it contains no more than "max_segments"
|
|
202
212
|
if (ts.length > max_segments) {
|
|
@@ -217,34 +227,38 @@ module.exports = function(params) {
|
|
|
217
227
|
return promise
|
|
218
228
|
}
|
|
219
229
|
|
|
220
|
-
const get_segment = function(url, url_type) {
|
|
230
|
+
const get_segment = async function(url, url_type) {
|
|
221
231
|
if (! should_prefetch_url(url, url_type)) return undefined
|
|
222
232
|
|
|
223
233
|
let debug_url = (debug_level >= 3) ? url : get_publickey_from_url(url)
|
|
234
|
+
let segment = find_segment(url)
|
|
224
235
|
|
|
225
|
-
let segment = find_segment(url)
|
|
226
236
|
if (segment !== undefined) {
|
|
227
237
|
const {m3u8_url, index} = segment
|
|
228
238
|
const ts = get_ts(m3u8_url)
|
|
229
239
|
touch_access(m3u8_url)
|
|
230
240
|
|
|
231
|
-
segment = ts[index]
|
|
241
|
+
segment = ts[index]
|
|
242
|
+
|
|
243
|
+
if (segment.has) {
|
|
244
|
+
debug(1, 'cache (hit):', debug_url)
|
|
245
|
+
|
|
246
|
+
segment = await cache_storage_adapter.get(segment.state)
|
|
232
247
|
|
|
233
|
-
|
|
248
|
+
// cleanup: remove all previous segments
|
|
249
|
+
// =====================================
|
|
250
|
+
// todo:
|
|
251
|
+
// - why does this sometimes cause the video player to get stuck.. repeatedly request the .m3u8 file, but stop requesting any .ts segments?
|
|
252
|
+
// - is it a coincidence that commenting this line appears to stop such behavior?
|
|
253
|
+
// - could it possibly be a race condition? cleanup also occurs asynchronously when prefetch responses are received, but javascript (node) is single threaded.. and this code doesn't yield or use a timer.
|
|
254
|
+
// =====================================
|
|
255
|
+
// ts_garbage_collect(m3u8_url, 0, (index + 1))
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
234
258
|
debug(1, 'cache (pending prefetch):', debug_url)
|
|
235
259
|
|
|
236
|
-
|
|
260
|
+
segment = false
|
|
237
261
|
}
|
|
238
|
-
debug(1, 'cache (hit):', debug_url)
|
|
239
|
-
|
|
240
|
-
// cleanup: remove all previous segments
|
|
241
|
-
// =====================================
|
|
242
|
-
// todo:
|
|
243
|
-
// - why does this sometimes cause the video player to get stuck.. repeatedly request the .m3u8 file, but stop requesting any .ts segments?
|
|
244
|
-
// - is it a coincidence that commenting this line appears to stop such behavior?
|
|
245
|
-
// - could it possibly be a race condition? cleanup also occurs asynchronously when prefetch responses are received, but javascript (node) is single threaded.. and this code doesn't yield or use a timer.
|
|
246
|
-
// =====================================
|
|
247
|
-
// ts_garbage_collect(m3u8_url, 0, (index + 1))
|
|
248
262
|
}
|
|
249
263
|
else {
|
|
250
264
|
debug(1, 'cache (miss):', debug_url)
|
|
@@ -263,22 +277,17 @@ module.exports = function(params) {
|
|
|
263
277
|
const ts = get_ts(m3u8_url)
|
|
264
278
|
touch_access(m3u8_url)
|
|
265
279
|
|
|
266
|
-
segment = ts[index]
|
|
267
|
-
|
|
268
|
-
if (segment === false) {
|
|
269
|
-
ts[index].databuffer = [cb]
|
|
280
|
+
segment = ts[index]
|
|
270
281
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
else if (segment instanceof Array) {
|
|
274
|
-
ts[index].databuffer.push(cb)
|
|
282
|
+
if (segment.has) {
|
|
283
|
+
cb(segment)
|
|
275
284
|
|
|
276
|
-
debug(1, 'cache (callback
|
|
285
|
+
debug(1, 'cache (callback complete):', debug_url)
|
|
277
286
|
}
|
|
278
287
|
else {
|
|
279
|
-
cb(
|
|
288
|
+
segment.cb.push(cb)
|
|
280
289
|
|
|
281
|
-
debug(1, 'cache (callback
|
|
290
|
+
debug(1, 'cache (callback added):', debug_url)
|
|
282
291
|
}
|
|
283
292
|
}
|
|
284
293
|
return true
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
|
|
5
|
+
const {denodeify} = require('@warren-bank/node-request')
|
|
6
|
+
|
|
7
|
+
const $fs = {
|
|
8
|
+
writeFile: denodeify(fs.writeFile),
|
|
9
|
+
readFile: denodeify(fs.readFile),
|
|
10
|
+
rm: denodeify(fs.rm)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = function(dirpath) {
|
|
14
|
+
|
|
15
|
+
// synchronous (private)
|
|
16
|
+
const get_random_filename = (state) => {
|
|
17
|
+
let random_bytes, fname, fpath
|
|
18
|
+
|
|
19
|
+
while (true) {
|
|
20
|
+
random_bytes = get_random_bytes()
|
|
21
|
+
fname = convert_random_bytes_to_filename(random_bytes)
|
|
22
|
+
fpath = path.join(dirpath, fname)
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(fpath)) {
|
|
25
|
+
state.fpath = fpath
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// synchronous (private)
|
|
32
|
+
const get_random_bytes = () => crypto.randomBytes(30)
|
|
33
|
+
|
|
34
|
+
// synchronous (private)
|
|
35
|
+
const convert_random_bytes_to_filename = (buffer) => buffer.toString('base64').replaceAll('/', '_')
|
|
36
|
+
|
|
37
|
+
// async
|
|
38
|
+
const set = async (state, blob) => {
|
|
39
|
+
get_random_filename(state)
|
|
40
|
+
|
|
41
|
+
await $fs.writeFile(state.fpath, blob)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// async
|
|
45
|
+
const get = async (state) => {
|
|
46
|
+
return await $fs.readFile(state.fpath, {encoding: null})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// async
|
|
50
|
+
const remove = async (state) => {
|
|
51
|
+
await $fs.rm(state.fpath, {force: true})
|
|
52
|
+
delete state.fpath
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {set, get, remove}
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = function(params) {
|
|
2
|
+
const {cache_storage, cache_storage_fs_dirpath} = params
|
|
3
|
+
|
|
4
|
+
if (cache_storage) {
|
|
5
|
+
if (cache_storage === 'memory')
|
|
6
|
+
return require('./memory')()
|
|
7
|
+
|
|
8
|
+
if ((cache_storage === 'filesystem') && cache_storage_fs_dirpath)
|
|
9
|
+
return require('./filesystem')(cache_storage_fs_dirpath)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// default
|
|
13
|
+
return require('./memory')()
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = function() {
|
|
2
|
+
|
|
3
|
+
// async
|
|
4
|
+
const set = async (state, blob) => {
|
|
5
|
+
state.databuffer = blob
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// async
|
|
9
|
+
const get = async (state) => state.databuffer
|
|
10
|
+
|
|
11
|
+
// async
|
|
12
|
+
const remove = async (state) => {
|
|
13
|
+
delete state.databuffer
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {set, get, remove}
|
|
17
|
+
}
|
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": "3.
|
|
4
|
+
"version": "3.4.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"start": "node hls-proxy/bin/hlsd.js",
|
|
7
7
|
"sudo": "sudo node hls-proxy/bin/hlsd.js"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@warren-bank/node-process-argv": "^1.2.1",
|
|
14
14
|
"@warren-bank/node-request": "^2.0.12",
|
|
15
|
-
"@warren-bank/url": "^3.1.
|
|
15
|
+
"@warren-bank/url": "^3.1.2",
|
|
16
16
|
"hpagent": "^1.2.0",
|
|
17
17
|
"tough-cookie": "^3.0.1"
|
|
18
18
|
},
|