@warren-bank/hls-proxy 3.0.0 → 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.
@@ -2,13 +2,10 @@ const {URL} = require('@warren-bank/url')
2
2
  const utils = require('./utils')
3
3
 
4
4
  const regexs = {
5
+ vod_start_at: /#vod_start(?:_prefetch_at)?=((?:\d+:)?(?:\d+:)?\d+)$/i,
5
6
  m3u8_line_separator: /\s*[\r\n]+\s*/,
6
- m3u8_line_landmark: /^(#[^:]+:)/,
7
- m3u8_line_url: /URI=["']([^"']+)["']/id,
8
-
9
- vod_start_at: new RegExp('#vod_start(?:_prefetch_at)?=((?:\\d+:)?(?:\\d+:)?\\d+)$', 'i'),
10
- vod: new RegExp('^(?:#EXT-X-PLAYLIST-TYPE:VOD|#EXT-X-ENDLIST)$', 'im'),
11
- ts_duration: new RegExp('^#EXT-X-TARGETDURATION:(\\d+)(?:\\.\\d+)?$', 'im')
7
+ m3u8_line_landmark: /^(#[^:]+[:]?)/,
8
+ m3u8_line_url: /URI=["']([^"']+)["']/id
12
9
  }
13
10
 
14
11
  const url_location_landmarks = {
@@ -51,16 +48,82 @@ const url_location_landmarks = {
51
48
  }
52
49
  }
53
50
 
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
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ const get_vod_start_at_ms = function(m3u8_url) {
82
+ try {
83
+ const matches = regexs.vod_start_at.exec(m3u8_url)
84
+
85
+ if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
86
+ throw ''
87
+
88
+ let offset
89
+ offset = matches[1]
90
+ offset = parse_HHMMSS_to_seconds(offset)
91
+ offset = offset * 1000 // convert seconds to ms
92
+
93
+ return offset
94
+ }
95
+ catch(e) {
96
+ const def_offset = null
97
+
98
+ return def_offset
99
+ }
100
+ }
101
+
102
+ const parse_HHMMSS_to_seconds = function(str) {
103
+ const parts = str.split(':')
104
+ let seconds = 0
105
+ let multiplier = 1
106
+
107
+ while (parts.length > 0) {
108
+ seconds += multiplier * parseInt(parts.pop(), 10)
109
+ multiplier *= 60
110
+ }
111
+
112
+ return seconds
113
+ }
114
+
54
115
  // returns: {
116
+ // meta_data: {is_vod, seg_duration_ms},
55
117
  // embedded_urls: [{line_index, url_indices, url_type, original_match_url, resolved_match_url, redirected_url, unencoded_url, encoded_url, referer_url}],
56
118
  // prefetch_urls: [],
57
119
  // modified_m3u8: ''
58
120
  // }
59
- const parse_manifest = function(m3u8_content, m3u8_url, referer_url, hooks, debug, vod_start_at_ms, redirected_base_url, should_prefetch_url) {
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) {
60
122
  const m3u8_lines = m3u8_content.split(regexs.m3u8_line_separator)
61
123
  m3u8_content = null
62
124
 
63
- const embedded_urls = extract_embedded_urls(m3u8_lines, m3u8_url, referer_url)
125
+ const meta_data = {}
126
+ const embedded_urls = extract_embedded_urls(m3u8_lines, m3u8_url, referer_url, (cache_segments ? meta_data : null))
64
127
  const prefetch_urls = []
65
128
 
66
129
  if (embedded_urls && Array.isArray(embedded_urls) && embedded_urls.length) {
@@ -74,13 +137,14 @@ const parse_manifest = function(m3u8_content, m3u8_url, referer_url, hooks, debu
74
137
  }
75
138
 
76
139
  return {
140
+ meta_data,
77
141
  embedded_urls,
78
142
  prefetch_urls,
79
143
  modified_m3u8: m3u8_lines.filter(line => !!line).join("\n")
80
144
  }
81
145
  }
82
146
 
83
- const extract_embedded_urls = function(m3u8_lines, m3u8_url, referer_url) {
147
+ const extract_embedded_urls = function(m3u8_lines, m3u8_url, referer_url, meta_data) {
84
148
  const embedded_urls = []
85
149
 
86
150
  let m3u8_line, has_next_m3u8_line, next_m3u8_line, matches, matching_landmark, matching_url
@@ -98,6 +162,9 @@ const extract_embedded_urls = function(m3u8_lines, m3u8_url, referer_url) {
98
162
  ? matches[1]
99
163
  : null
100
164
 
165
+ if (meta_data !== null)
166
+ extract_meta_data(meta_data, m3u8_line, matching_landmark)
167
+
101
168
  for (let url_type in url_location_landmarks) {
102
169
  if (matching_url && (url_location_landmarks[url_type]['same_line'].indexOf(matching_landmark) >= 0)) {
103
170
  embedded_urls.push({
@@ -137,6 +204,20 @@ const extract_embedded_urls = function(m3u8_lines, m3u8_url, referer_url) {
137
204
  return embedded_urls
138
205
  }
139
206
 
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
+
140
221
  const redirect_embedded_url = function(embedded_url, hooks, m3u8_url, debug) {
141
222
  if (hooks && (hooks instanceof Object) && hooks.redirect && (typeof hooks.redirect === 'function')) {
142
223
  let url, url_type, referer_url, result
@@ -252,61 +333,6 @@ const modify_m3u8_line = function(embedded_url, m3u8_lines) {
252
333
  }
253
334
  }
254
335
 
255
- const get_seg_duration_ms = function(m3u8_content) {
256
- try {
257
- const matches = regexs.ts_duration.exec(m3u8_content)
258
-
259
- if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
260
- throw ''
261
-
262
- let duration
263
- duration = matches[1]
264
- duration = parseInt(duration, 10)
265
- duration = duration * 1000 // convert seconds to ms
266
-
267
- return duration
268
- }
269
- catch(e) {
270
- const def_duration = 10000 // 10 seconds in ms
271
-
272
- return def_duration
273
- }
274
- }
275
-
276
- const parse_HHMMSS_to_seconds = function(str) {
277
- const parts = str.split(':')
278
- let seconds = 0
279
- let multiplier = 1
280
-
281
- while (parts.length > 0) {
282
- seconds += multiplier * parseInt(parts.pop(), 10)
283
- multiplier *= 60
284
- }
285
-
286
- return seconds
287
- }
288
-
289
- const get_vod_start_at_ms = function(m3u8_url) {
290
- try {
291
- const matches = regexs.vod_start_at.exec(m3u8_url)
292
-
293
- if ((matches == null) || !Array.isArray(matches) || (matches.length < 2))
294
- throw ''
295
-
296
- let offset
297
- offset = matches[1]
298
- offset = parse_HHMMSS_to_seconds(offset)
299
- offset = offset * 1000 // convert seconds to ms
300
-
301
- return offset
302
- }
303
- catch(e) {
304
- const def_offset = null
305
-
306
- return def_offset
307
- }
308
- }
309
-
310
336
  const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_url, referer_url, redirected_base_url) {
311
337
  const {hooks, cache_segments, max_segments, debug_level} = params
312
338
 
@@ -327,21 +353,13 @@ const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_u
327
353
  debug(4, 'proxied response (original m3u8):', `\n${debug_divider}\n${m3u8_content}\n${debug_divider}`)
328
354
  }
329
355
 
330
- // only used with prefetch
331
- const seg_duration_ms = (cache_segments)
332
- ? get_seg_duration_ms(m3u8_content)
333
- : null
356
+ let is_vod, seg_duration_ms, prefetch_urls
334
357
 
335
358
  // only used with prefetch
336
359
  const vod_start_at_ms = (cache_segments)
337
360
  ? get_vod_start_at_ms(m3u8_url)
338
361
  : null
339
362
 
340
- // only used with prefetch
341
- const is_vod = (cache_segments)
342
- ? ((typeof vod_start_at_ms === 'number') || (!has_cache(m3u8_url) && regexs.vod.test(m3u8_content)))
343
- : null
344
-
345
363
  // only used with prefetch
346
364
  const perform_prefetch = (cache_segments)
347
365
  ? (urls, dont_touch_access) => {
@@ -372,11 +390,16 @@ const modify_m3u8_content = function(params, segment_cache, m3u8_content, m3u8_u
372
390
  }
373
391
  : null
374
392
 
375
- let prefetch_urls
376
393
  {
377
- const parsed_manifest = parse_manifest(m3u8_content, m3u8_url, referer_url, hooks, debug, vod_start_at_ms, redirected_base_url, should_prefetch_url)
378
- prefetch_urls = parsed_manifest.prefetch_urls
379
- m3u8_content = parsed_manifest.modified_m3u8
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
399
+
400
+ if (debug_level >= 4) {
401
+ debug(4, 'parsed manifest:', `\n${debug_divider}\n${JSON.stringify(parsed_manifest, null, 2)}\n${debug_divider}`)
402
+ }
380
403
  }
381
404
 
382
405
  if (prefetch_urls.length) {
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.0.0",
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"