@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.
@@ -1,50 +1,102 @@
1
+ const {URL} = require('@warren-bank/url')
1
2
  const utils = require('./utils')
2
3
 
3
4
  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')
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 ts_regexs = {
13
- "file_ext": /^\.ts/i,
14
- "sequence_number": /[^\d](\d+)$/i
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 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}`
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 get_seg_duration_ms = function(m3u8_content) {
30
- try {
31
- const matches = regexs.ts_duration.exec(m3u8_content)
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
- if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
34
- throw ''
85
+ if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
86
+ throw ''
35
87
 
36
- let duration
37
- duration = matches[1]
38
- duration = parseInt(duration, 10)
39
- duration = duration * 1000 // convert seconds to ms
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
- return duration
42
- }
43
- catch(e) {
44
- const def_duration = 10000 // 10 seconds in ms
93
+ return offset
94
+ }
95
+ catch(e) {
96
+ const def_offset = null
45
97
 
46
- return def_duration
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
- const get_vod_start_at_ms = function(m3u8_url) {
64
- try {
65
- const matches = regexs.vod_start_at.exec(m3u8_url)
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
- if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
68
- throw ''
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
- let offset
71
- offset = matches[1]
72
- offset = parse_HHMMSS_to_seconds(offset)
73
- offset = offset * 1000 // convert seconds to ms
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
- return offset
76
- }
77
- catch(e) {
78
- const def_offset = null
204
+ return embedded_urls
205
+ }
79
206
 
80
- return def_offset
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
- 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
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
- 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})
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
- let matching_url
180
- if (!abs_path) {
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
- 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
- })
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
  }
@@ -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 {is_secure, host, cache_segments} = params
12
- let {acl_whitelist} = params
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 [url, referer_url] = (() => {
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 = regexs.m3u8.test(url)
49
+ const is_m3u8 = (url_type === 'm3u8')
76
50
 
77
- const send_ts = function(segment) {
78
- res.writeHead(200, { "Content-Type": "video/MP2T" })
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) // Buffer (cached segment data), false (prefetch is pending: add callback), undefined (no prefetch is pending)
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) { // Buffer (cached segment data)
86
- send_ts(segment)
59
+ if (segment && segment.length) { // Buffer (cached segment data)
60
+ send_cache_segment(segment)
87
61
  return
88
62
  }
89
- else if (segment === false) { // false (prefetch is pending: add callback)
90
- add_listener(url, send_ts)
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
 
@@ -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
- origin: new RegExp('^(https?://[^/]+)(?:/.*)?$', 'i'),
5
- ts: new RegExp('\\.ts(?:[\\?#]|$)', 'i')
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 = regexs.ts.test(url)
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": "2.0.1",
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": {