Skip to content

Commit

Permalink
Allow parsing custom tags from m3u8 manifest (videojs#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
keisans authored and gesinger committed Jan 19, 2018
1 parent 9e63313 commit c28b80f
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 6 deletions.
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Manifest {
mediaSequence: number,
discontinuitySequence: number,
playlistType: string,
custom: {},
playlists: [
{
attributes: {},
Expand Down Expand Up @@ -131,7 +132,8 @@ Manifest {
},
'cue-out': string,
'cue-out-cont': string,
'cue-in': string
'cue-in': string,
custom: {}
}
]
}
Expand Down Expand Up @@ -237,6 +239,75 @@ Example media playlist using `EXT-X-CUE-` tags.
* [EXT-X-SESSION-KEY](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.5)
* [EXT-X-INDEPENDENT-SEGMENTS](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.5.1)

### Custom Parsers

To add a parser for a non-standard tag the parser object allows for the specification of custom tags using regular expressions. If a custom parser is specified, a `custom` object is appended to the manifest object.

```js
const manifest = [
'#EXTM3U',
'#EXT-X-VERSION:3',
'#VOD-FRAMERATE:29.97',
''
].join('\n');

const parser = new m3u8Parser.Parser();
parser.addParser({
expression: /^#VOD-FRAMERATE/,
customType: 'framerate'
});

parser.push(manifest);
parser.end();
parser.manifest.custom.framerate // "#VOD-FRAMERATE:29.97"
```

Custom parsers may additionally be provided a data parsing function that take a line and return a value.

```js
const manifest = [
'#EXTM3U',
'#EXT-X-VERSION:3',
'#VOD-FRAMERATE:29.97',
''
].join('\n');

const parser = new m3u8Parser.Parser();
parser.addParser({
expression: /^#VOD-FRAMERATE/,
customType: 'framerate',
dataParser: function(line) {
return parseFloat(line.split(':')[1]);
}
});

parser.push(manifest);
parser.end();
parser.manifest.custom.framerate // 29.97
```

Custom parsers may also extract data at a segment level by passing `segment: true` to the options object. Having a segment level custom parser will add a `custom` object to the segment data.

```js
const manifest = [
'#EXTM3U',
'#VOD-TIMING:1511816599485',
'#EXTINF:8.0,',
'ex1.ts',
''
].join('\n');

const parser = new m3u8Parser.Parser();
parser.addParser({
expression: /#VOD-TIMING/,
customType: 'vodTiming',
segment: true
});

parser.push(manifest);
parser.end();
parser.manifest.segments[0].custom.vodTiming // #VOD-TIMING:1511816599485
```
## Including the Parser

To include m3u8-parser on your website or web application, use any of the following methods.
Expand Down
35 changes: 35 additions & 0 deletions src/parse-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const parseAttributes = function(attributes) {
export default class ParseStream extends Stream {
constructor() {
super();
this.customParsers = [];
}

/**
Expand Down Expand Up @@ -101,6 +102,12 @@ export default class ParseStream extends Stream {
return;
}

for (let i = 0; i < this.customParsers.length; i++) {
if (this.customParsers[i].call(this, line)) {
return;
}
}

// Comments
if (line.indexOf('#EXT') !== 0) {
this.trigger('data', {
Expand Down Expand Up @@ -427,4 +434,32 @@ export default class ParseStream extends Stream {
data: line.slice(4)
});
}

/**
* Add a parser for custom headers
*
* @param {Object} options a map of options for the added parser
* @param {RegExp} options.expression a regular expression to match the custom header
* @param {string} options.customType the custom type to register to the output
* @param {Function} [options.dataParser] function to parse the line into an object
* @param {boolean} [options.segment] should tag data be attached to the segment object
*/
addParser({expression, customType, dataParser, segment}) {
if (typeof dataParser !== 'function') {
dataParser = (line) => line;
}
this.customParsers.push(line => {
const match = expression.exec(line);

if (match) {
this.trigger('data', {
type: 'custom',
data: dataParser(line),
customType,
segment
});
return true;
}
});
}
}
31 changes: 26 additions & 5 deletions src/parser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @file m3u8/parser.js
*/
import Stream from './stream' ;
import Stream from './stream';
import LineStream from './line-stream';
import ParseStream from './parse-stream';

Expand Down Expand Up @@ -32,6 +32,7 @@ export default class Parser extends Stream {
this.lineStream = new LineStream();
this.parseStream = new ParseStream();
this.lineStream.pipe(this.parseStream);

/* eslint-disable consistent-this */
const self = this;
/* eslint-enable consistent-this */
Expand Down Expand Up @@ -316,8 +317,7 @@ export default class Parser extends Stream {
uris.push(currentUri);

// if no explicit duration was declared, use the target duration
if (this.manifest.targetDuration &&
!('duration' in currentUri)) {
if (this.manifest.targetDuration && !('duration' in currentUri)) {
this.trigger('warn', {
message: 'defaulting segment duration to the target duration'
});
Expand All @@ -338,10 +338,20 @@ export default class Parser extends Stream {
},
comment() {
// comments are not important for playback
},
custom() {
// if this is segment-level data attach the output to the segment
if (entry.segment) {
currentUri.custom = currentUri.custom || {};
currentUri.custom[entry.customType] = entry.data;
// if this is manifest-level data attach to the top level manifest object
} else {
this.manifest.custom = this.manifest.custom || {};
this.manifest.custom[entry.customType] = entry.data;
}
}
})[entry.type].call(self);
});

}

/**
Expand All @@ -362,5 +372,16 @@ export default class Parser extends Stream {
// flush any buffered input
this.lineStream.push('\n');
}

/**
* Add an additional parser for non-standard tags
*
* @param {Object} options a map of options for the added parser
* @param {RegExp} options.expression a regular expression to match the custom header
* @param {string} options.type the type to register to the output
* @param {Function} [options.dataParser] function to parse the line into an object
* @param {boolean} [options.segment] should tag data be attached to the segment object
*/
addParser(options) {
this.parseStream.addParser(options);
}
}
108 changes: 108 additions & 0 deletions test/m3u8.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,29 @@ QUnit.module('ParseStream', {
this.lineStream.pipe(this.parseStream);
}
});
QUnit.test('parses custom tags', function(assert) {
const manifest = '#VOD-STARTTIMESTAMP:1501533337573\n';
let element;

this.parseStream.addParser({
expression: /^#VOD-STARTTIMESTAMP/,
customType: 'startTimestamp'
});

this.parseStream.on('data', function(elem) {
element = elem;
});

this.lineStream.push(manifest);
assert.ok(element, 'element');
assert.strictEqual(element.type, 'custom', 'the type of the data is custom');
assert.strictEqual(
element.customType,
'startTimestamp',
'the customType is startTimestamp'
);
});

QUnit.test('parses comment lines', function(assert) {
const manifest = '# a line that starts with a hash mark without "EXT" is a comment\n';
let element;
Expand Down Expand Up @@ -803,6 +826,91 @@ QUnit.test('can be constructed', function(assert) {
assert.notStrictEqual(typeof new Parser(), 'undefined', 'parser is defined');
});

QUnit.test('can set custom parsers', function(assert) {
const parser = new Parser();
const manifest = [
'#EXTM3U',
'#EXT-X-VERSION:3',
'#EXT-X-TARGETDURATION:10',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#VOD-STARTTIMESTAMP:1501533337573',
'#VOD-TOTALDELETEDDURATION:0.0',
'#VOD-FRAMERATE:29.97',
''
].join('\n');

parser.addParser({
expression: /^#VOD-STARTTIMESTAMP/,
customType: 'startTimestamp'
});
parser.addParser({
expression: /^#VOD-TOTALDELETEDDURATION/,
customType: 'totalDeleteDuration'
});
parser.addParser({
expression: /^#VOD-FRAMERATE/,
customType: 'framerate',
dataParser: (line) => (line.split(':')[1])
});

parser.push(manifest);
assert.strictEqual(
parser.manifest.custom.startTimestamp,
'#VOD-STARTTIMESTAMP:1501533337573',
'sets custom timestamp line'
);

assert.strictEqual(
parser.manifest.custom.totalDeleteDuration,
'#VOD-TOTALDELETEDDURATION:0.0',
'sets custom delete duration'
);

assert.strictEqual(parser.manifest.custom.framerate, '29.97', 'sets framerate');
});

QUnit.test('segment level custom data', function(assert) {
const parser = new Parser();

const manifest = [
'#EXTM3U',
'#VOD-TIMING:1511816599485',
'#COMMENT',
'#EXTINF:8.0,',
'ex1.ts',
'#VOD-TIMING',
'#EXTINF:8.0,',
'ex2.ts',
'#VOD-TIMING:1511816615485',
'#EXT-UNKNOWN',
'#EXTINF:8.0,',
'ex3.ts',
'#VOD-TIMING:1511816623485',
'#EXTINF:8.0,',
'ex3.ts',
'#EXT-X-ENDLIST'
].join('\n');

parser.addParser({
expression: /^#VOD-TIMING/,
customType: 'vodTiming',
segment: true
});

parser.push(manifest);
assert.equal(
parser.manifest.segments[0].custom.vodTiming,
'#VOD-TIMING:1511816599485',
'parser attached segment level custom data'
);
assert.equal(
parser.manifest.segments[1].custom.vodTiming,
'#VOD-TIMING',
'parser got segment level custom data without :'
);
});

QUnit.test('attaches cue-out data to segment', function(assert) {
const parser = new Parser();

Expand Down

0 comments on commit c28b80f

Please sign in to comment.