separate m3u8 from HLS, and depend m3u8-parser package(#734)
Showing
8 changed files
with
3 additions
and
387 deletions
... | @@ -83,6 +83,7 @@ | ... | @@ -83,6 +83,7 @@ |
83 | "test/" | 83 | "test/" |
84 | ], | 84 | ], |
85 | "dependencies": { | 85 | "dependencies": { |
86 | "m3u8-parser": "^1.0.2", | ||
86 | "pkcs7": "^0.2.2", | 87 | "pkcs7": "^0.2.2", |
87 | "video.js": "^5.10.1", | 88 | "video.js": "^5.10.1", |
88 | "videojs-contrib-media-sources": "^3.1.0", | 89 | "videojs-contrib-media-sources": "^3.1.0", | ... | ... |
src/m3u8/index.js
deleted
100644 → 0
1 | /** | ||
2 | * @file m3u8/index.js | ||
3 | * | ||
4 | * Utilities for parsing M3U8 files. If the entire manifest is available, | ||
5 | * `Parser` will create an object representation with enough detail for managing | ||
6 | * playback. `ParseStream` and `LineStream` are lower-level parsing primitives | ||
7 | * that do not assume the entirety of the manifest is ready and expose a | ||
8 | * ReadableStream-like interface. | ||
9 | */ | ||
10 | |||
11 | import LineStream from './line-stream'; | ||
12 | import ParseStream from './parse-stream'; | ||
13 | import Parser from './parser'; | ||
14 | |||
15 | export default { | ||
16 | LineStream, | ||
17 | ParseStream, | ||
18 | Parser | ||
19 | }; |
src/m3u8/line-stream.js
deleted
100644 → 0
1 | /** | ||
2 | * @file m3u8/line-stream.js | ||
3 | */ | ||
4 | import Stream from '../stream'; | ||
5 | |||
6 | /** | ||
7 | * A stream that buffers string input and generates a `data` event for each | ||
8 | * line. | ||
9 | * | ||
10 | * @class LineStream | ||
11 | * @extends Stream | ||
12 | */ | ||
13 | export default class LineStream extends Stream { | ||
14 | constructor() { | ||
15 | super(); | ||
16 | this.buffer = ''; | ||
17 | } | ||
18 | |||
19 | /** | ||
20 | * Add new data to be parsed. | ||
21 | * | ||
22 | * @param {String} data the text to process | ||
23 | */ | ||
24 | push(data) { | ||
25 | let nextNewline; | ||
26 | |||
27 | this.buffer += data; | ||
28 | nextNewline = this.buffer.indexOf('\n'); | ||
29 | |||
30 | for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) { | ||
31 | this.trigger('data', this.buffer.substring(0, nextNewline)); | ||
32 | this.buffer = this.buffer.substring(nextNewline + 1); | ||
33 | } | ||
34 | } | ||
35 | } |
src/m3u8/parse-stream.js
deleted
100644 → 0
1 | /** | ||
2 | * @file m3u8/parse-stream.js | ||
3 | */ | ||
4 | import Stream from '../stream'; | ||
5 | |||
6 | /** | ||
7 | * "forgiving" attribute list psuedo-grammar: | ||
8 | * attributes -> keyvalue (',' keyvalue)* | ||
9 | * keyvalue -> key '=' value | ||
10 | * key -> [^=]* | ||
11 | * value -> '"' [^"]* '"' | [^,]* | ||
12 | */ | ||
13 | const attributeSeparator = function() { | ||
14 | let key = '[^=]*'; | ||
15 | let value = '"[^"]*"|[^,]*'; | ||
16 | let keyvalue = '(?:' + key + ')=(?:' + value + ')'; | ||
17 | |||
18 | return new RegExp('(?:^|,)(' + keyvalue + ')'); | ||
19 | }; | ||
20 | |||
21 | /** | ||
22 | * Parse attributes from a line given the seperator | ||
23 | * | ||
24 | * @param {String} attributes the attibute line to parse | ||
25 | */ | ||
26 | const parseAttributes = function(attributes) { | ||
27 | // split the string using attributes as the separator | ||
28 | let attrs = attributes.split(attributeSeparator()); | ||
29 | let i = attrs.length; | ||
30 | let result = {}; | ||
31 | let attr; | ||
32 | |||
33 | while (i--) { | ||
34 | // filter out unmatched portions of the string | ||
35 | if (attrs[i] === '') { | ||
36 | continue; | ||
37 | } | ||
38 | |||
39 | // split the key and value | ||
40 | attr = (/([^=]*)=(.*)/).exec(attrs[i]).slice(1); | ||
41 | // trim whitespace and remove optional quotes around the value | ||
42 | attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); | ||
43 | attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); | ||
44 | attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); | ||
45 | result[attr[0]] = attr[1]; | ||
46 | } | ||
47 | return result; | ||
48 | }; | ||
49 | |||
50 | /** | ||
51 | * A line-level M3U8 parser event stream. It expects to receive input one | ||
52 | * line at a time and performs a context-free parse of its contents. A stream | ||
53 | * interpretation of a manifest can be useful if the manifest is expected to | ||
54 | * be too large to fit comfortably into memory or the entirety of the input | ||
55 | * is not immediately available. Otherwise, it's probably much easier to work | ||
56 | * with a regular `Parser` object. | ||
57 | * | ||
58 | * Produces `data` events with an object that captures the parser's | ||
59 | * interpretation of the input. That object has a property `tag` that is one | ||
60 | * of `uri`, `comment`, or `tag`. URIs only have a single additional | ||
61 | * property, `line`, which captures the entirety of the input without | ||
62 | * interpretation. Comments similarly have a single additional property | ||
63 | * `text` which is the input without the leading `#`. | ||
64 | * | ||
65 | * Tags always have a property `tagType` which is the lower-cased version of | ||
66 | * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance, | ||
67 | * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized | ||
68 | * tags are given the tag type `unknown` and a single additional property | ||
69 | * `data` with the remainder of the input. | ||
70 | * | ||
71 | * @class ParseStream | ||
72 | * @extends Stream | ||
73 | */ | ||
74 | export default class ParseStream extends Stream { | ||
75 | constructor() { | ||
76 | super(); | ||
77 | } | ||
78 | |||
79 | /** | ||
80 | * Parses an additional line of input. | ||
81 | * | ||
82 | * @param {String} line a single line of an M3U8 file to parse | ||
83 | */ | ||
84 | push(line) { | ||
85 | let match; | ||
86 | let event; | ||
87 | |||
88 | // strip whitespace | ||
89 | line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, ''); | ||
90 | if (line.length === 0) { | ||
91 | // ignore empty lines | ||
92 | return; | ||
93 | } | ||
94 | |||
95 | // URIs | ||
96 | if (line[0] !== '#') { | ||
97 | this.trigger('data', { | ||
98 | type: 'uri', | ||
99 | uri: line | ||
100 | }); | ||
101 | return; | ||
102 | } | ||
103 | |||
104 | // Comments | ||
105 | if (line.indexOf('#EXT') !== 0) { | ||
106 | this.trigger('data', { | ||
107 | type: 'comment', | ||
108 | text: line.slice(1) | ||
109 | }); | ||
110 | return; | ||
111 | } | ||
112 | |||
113 | // strip off any carriage returns here so the regex matching | ||
114 | // doesn't have to account for them. | ||
115 | line = line.replace('\r', ''); | ||
116 | |||
117 | // Tags | ||
118 | match = (/^#EXTM3U/).exec(line); | ||
119 | if (match) { | ||
120 | this.trigger('data', { | ||
121 | type: 'tag', | ||
122 | tagType: 'm3u' | ||
123 | }); | ||
124 | return; | ||
125 | } | ||
126 | match = (/^#EXTINF:?([0-9\.]*)?,?(.*)?$/).exec(line); | ||
127 | if (match) { | ||
128 | event = { | ||
129 | type: 'tag', | ||
130 | tagType: 'inf' | ||
131 | }; | ||
132 | if (match[1]) { | ||
133 | event.duration = parseFloat(match[1]); | ||
134 | } | ||
135 | if (match[2]) { | ||
136 | event.title = match[2]; | ||
137 | } | ||
138 | this.trigger('data', event); | ||
139 | return; | ||
140 | } | ||
141 | match = (/^#EXT-X-TARGETDURATION:?([0-9.]*)?/).exec(line); | ||
142 | if (match) { | ||
143 | event = { | ||
144 | type: 'tag', | ||
145 | tagType: 'targetduration' | ||
146 | }; | ||
147 | if (match[1]) { | ||
148 | event.duration = parseInt(match[1], 10); | ||
149 | } | ||
150 | this.trigger('data', event); | ||
151 | return; | ||
152 | } | ||
153 | match = (/^#ZEN-TOTAL-DURATION:?([0-9.]*)?/).exec(line); | ||
154 | if (match) { | ||
155 | event = { | ||
156 | type: 'tag', | ||
157 | tagType: 'totalduration' | ||
158 | }; | ||
159 | if (match[1]) { | ||
160 | event.duration = parseInt(match[1], 10); | ||
161 | } | ||
162 | this.trigger('data', event); | ||
163 | return; | ||
164 | } | ||
165 | match = (/^#EXT-X-VERSION:?([0-9.]*)?/).exec(line); | ||
166 | if (match) { | ||
167 | event = { | ||
168 | type: 'tag', | ||
169 | tagType: 'version' | ||
170 | }; | ||
171 | if (match[1]) { | ||
172 | event.version = parseInt(match[1], 10); | ||
173 | } | ||
174 | this.trigger('data', event); | ||
175 | return; | ||
176 | } | ||
177 | match = (/^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/).exec(line); | ||
178 | if (match) { | ||
179 | event = { | ||
180 | type: 'tag', | ||
181 | tagType: 'media-sequence' | ||
182 | }; | ||
183 | if (match[1]) { | ||
184 | event.number = parseInt(match[1], 10); | ||
185 | } | ||
186 | this.trigger('data', event); | ||
187 | return; | ||
188 | } | ||
189 | match = (/^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/).exec(line); | ||
190 | if (match) { | ||
191 | event = { | ||
192 | type: 'tag', | ||
193 | tagType: 'discontinuity-sequence' | ||
194 | }; | ||
195 | if (match[1]) { | ||
196 | event.number = parseInt(match[1], 10); | ||
197 | } | ||
198 | this.trigger('data', event); | ||
199 | return; | ||
200 | } | ||
201 | match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line); | ||
202 | if (match) { | ||
203 | event = { | ||
204 | type: 'tag', | ||
205 | tagType: 'playlist-type' | ||
206 | }; | ||
207 | if (match[1]) { | ||
208 | event.playlistType = match[1]; | ||
209 | } | ||
210 | this.trigger('data', event); | ||
211 | return; | ||
212 | } | ||
213 | match = (/^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/).exec(line); | ||
214 | if (match) { | ||
215 | event = { | ||
216 | type: 'tag', | ||
217 | tagType: 'byterange' | ||
218 | }; | ||
219 | if (match[1]) { | ||
220 | event.length = parseInt(match[1], 10); | ||
221 | } | ||
222 | if (match[2]) { | ||
223 | event.offset = parseInt(match[2], 10); | ||
224 | } | ||
225 | this.trigger('data', event); | ||
226 | return; | ||
227 | } | ||
228 | match = (/^#EXT-X-ALLOW-CACHE:?(YES|NO)?/).exec(line); | ||
229 | if (match) { | ||
230 | event = { | ||
231 | type: 'tag', | ||
232 | tagType: 'allow-cache' | ||
233 | }; | ||
234 | if (match[1]) { | ||
235 | event.allowed = !(/NO/).test(match[1]); | ||
236 | } | ||
237 | this.trigger('data', event); | ||
238 | return; | ||
239 | } | ||
240 | match = (/^#EXT-X-STREAM-INF:?(.*)$/).exec(line); | ||
241 | if (match) { | ||
242 | event = { | ||
243 | type: 'tag', | ||
244 | tagType: 'stream-inf' | ||
245 | }; | ||
246 | if (match[1]) { | ||
247 | event.attributes = parseAttributes(match[1]); | ||
248 | |||
249 | if (event.attributes.RESOLUTION) { | ||
250 | let split = event.attributes.RESOLUTION.split('x'); | ||
251 | let resolution = {}; | ||
252 | |||
253 | if (split[0]) { | ||
254 | resolution.width = parseInt(split[0], 10); | ||
255 | } | ||
256 | if (split[1]) { | ||
257 | resolution.height = parseInt(split[1], 10); | ||
258 | } | ||
259 | event.attributes.RESOLUTION = resolution; | ||
260 | } | ||
261 | if (event.attributes.BANDWIDTH) { | ||
262 | event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); | ||
263 | } | ||
264 | if (event.attributes['PROGRAM-ID']) { | ||
265 | event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10); | ||
266 | } | ||
267 | } | ||
268 | this.trigger('data', event); | ||
269 | return; | ||
270 | } | ||
271 | match = (/^#EXT-X-MEDIA:?(.*)$/).exec(line); | ||
272 | if (match) { | ||
273 | event = { | ||
274 | type: 'tag', | ||
275 | tagType: 'media' | ||
276 | }; | ||
277 | if (match[1]) { | ||
278 | event.attributes = parseAttributes(match[1]); | ||
279 | } | ||
280 | this.trigger('data', event); | ||
281 | return; | ||
282 | } | ||
283 | match = (/^#EXT-X-ENDLIST/).exec(line); | ||
284 | if (match) { | ||
285 | this.trigger('data', { | ||
286 | type: 'tag', | ||
287 | tagType: 'endlist' | ||
288 | }); | ||
289 | return; | ||
290 | } | ||
291 | match = (/^#EXT-X-DISCONTINUITY/).exec(line); | ||
292 | if (match) { | ||
293 | this.trigger('data', { | ||
294 | type: 'tag', | ||
295 | tagType: 'discontinuity' | ||
296 | }); | ||
297 | return; | ||
298 | } | ||
299 | match = (/^#EXT-X-KEY:?(.*)$/).exec(line); | ||
300 | if (match) { | ||
301 | event = { | ||
302 | type: 'tag', | ||
303 | tagType: 'key' | ||
304 | }; | ||
305 | if (match[1]) { | ||
306 | event.attributes = parseAttributes(match[1]); | ||
307 | // parse the IV string into a Uint32Array | ||
308 | if (event.attributes.IV) { | ||
309 | if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') { | ||
310 | event.attributes.IV = event.attributes.IV.substring(2); | ||
311 | } | ||
312 | |||
313 | event.attributes.IV = event.attributes.IV.match(/.{8}/g); | ||
314 | event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16); | ||
315 | event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16); | ||
316 | event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16); | ||
317 | event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16); | ||
318 | event.attributes.IV = new Uint32Array(event.attributes.IV); | ||
319 | } | ||
320 | } | ||
321 | this.trigger('data', event); | ||
322 | return; | ||
323 | } | ||
324 | |||
325 | // unknown tag type | ||
326 | this.trigger('data', { | ||
327 | type: 'tag', | ||
328 | data: line.slice(4, line.length) | ||
329 | }); | ||
330 | } | ||
331 | } |
src/m3u8/parser.js
deleted
100644 → 0
This diff is collapsed.
Click to expand it.
... | @@ -8,7 +8,7 @@ | ... | @@ -8,7 +8,7 @@ |
8 | import resolveUrl from './resolve-url'; | 8 | import resolveUrl from './resolve-url'; |
9 | import {mergeOptions} from 'video.js'; | 9 | import {mergeOptions} from 'video.js'; |
10 | import Stream from './stream'; | 10 | import Stream from './stream'; |
11 | import m3u8 from './m3u8'; | 11 | import m3u8 from 'm3u8-parser'; |
12 | 12 | ||
13 | /** | 13 | /** |
14 | * Returns a new array of segments that is the result of merging | 14 | * Returns a new array of segments that is the result of merging | ... | ... |
... | @@ -11,7 +11,7 @@ import xhrFactory from './xhr'; | ... | @@ -11,7 +11,7 @@ import xhrFactory from './xhr'; |
11 | import {Decrypter, AsyncStream, decrypt} from './decrypter'; | 11 | import {Decrypter, AsyncStream, decrypt} from './decrypter'; |
12 | import utils from './bin-utils'; | 12 | import utils from './bin-utils'; |
13 | import {MediaSource, URL} from 'videojs-contrib-media-sources'; | 13 | import {MediaSource, URL} from 'videojs-contrib-media-sources'; |
14 | import m3u8 from './m3u8'; | 14 | import m3u8 from 'm3u8-parser'; |
15 | import videojs from 'video.js'; | 15 | import videojs from 'video.js'; |
16 | import MasterPlaylistController from './master-playlist-controller'; | 16 | import MasterPlaylistController from './master-playlist-controller'; |
17 | import Config from './config'; | 17 | import Config from './config'; | ... | ... |
test/m3u8.test.js
deleted
100644 → 0
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment