02b31a5e by Brandon Casey

separate m3u8 from HLS, and depend m3u8-parser package(#734)

1 parent b6ad6cc9
...@@ -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",
......
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 };
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 }
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 }
...@@ -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';
......