Browse Source

Collect parsing events into a manifest object

Create a parser that interprets parsing events and produces a manifest object. Get all the tests working. Comment a few manifest controller tests out because the interface of that object needs to be updated to use the new parser.
pull/80/head
David LaPalomento 12 years ago
parent
commit
15b36142ac
  1. 2
      package.json
  2. 385
      src/m3u8/m3u8-parser.js
  3. 253
      src/m3u8/m3u8-tokenizer.js
  4. 795
      test/m3u8_test.js
  5. 2
      test/manifest/playlistM3U8data.js
  6. 2
      test/manifest/playlist_media_sequence_template.js
  7. 5
      test/video-js-hls.html

2
package.json

@ -18,7 +18,7 @@
"grunt": "~0.4.1" "grunt": "~0.4.1"
}, },
"dependencies": { "dependencies": {
"video.js": "~4.2.2",
"video.js": "git+https://github.com/dmlap/video-js.git#v4.3.0-3",
"videojs-contrib-media-sources": "0.0.0" "videojs-contrib-media-sources": "0.0.0"
} }
} }

385
src/m3u8/m3u8-parser.js

@ -0,0 +1,385 @@
(function(parseInt, isFinite, mergeOptions, undefined) {
var
noop = function() {},
parseAttributes = function(attributes) {
var
attrs = attributes.split(','),
i = attrs.length,
result = {},
attr;
while (i--) {
attr = attrs[i].split('=');
result[attr[0]] = attr[1];
}
return result;
},
Stream,
LineStream,
ParseStream,
Parser;
Stream = function() {
this.init = function() {
var listeners = {};
this.on = function(type, listener) {
if (!listeners[type]) {
listeners[type] = [];
}
listeners[type].push(listener);
};
this.off = function(type, listener) {
var index;
if (!listeners[type]) {
return false;
}
index = listeners[type].indexOf(listener);
listeners[type].splice(index, 1);
return index > -1;
};
this.trigger = function(type) {
var callbacks, i, length, args;
callbacks = listeners[type];
if (!callbacks) {
return;
}
args = Array.prototype.slice.call(arguments, 1);
length = callbacks.length;
for (i = 0; i < length; ++i) {
callbacks[i].apply(this, args);
}
};
};
};
Stream.prototype.pipe = function(destination) {
this.on('data', function(data) {
destination.push(data);
});
};
LineStream = function() {
var buffer = '';
LineStream.prototype.init.call(this);
this.push = function(data) {
var nextNewline;
buffer += data;
nextNewline = buffer.indexOf('\n');
for (; nextNewline > -1; nextNewline = buffer.indexOf('\n')) {
this.trigger('data', buffer.substring(0, nextNewline));
buffer = buffer.substring(nextNewline + 1);
}
};
};
LineStream.prototype = new Stream();
ParseStream = function() {
ParseStream.prototype.init.call(this);
};
ParseStream.prototype = new Stream();
ParseStream.prototype.push = function(line) {
var match, event;
if (line.length === 0) {
// ignore empty lines
return;
}
// URIs
if (line[0] !== '#') {
this.trigger('data', {
type: 'uri',
uri: line
});
return;
}
// Comments
if (line.indexOf('#EXT') !== 0) {
this.trigger('data', {
type: 'comment',
text: line.slice(1)
});
return;
}
// Tags
match = /^#EXTM3U/.exec(line);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'm3u'
});
return;
}
match = (/^#EXTINF:?([0-9\.]*)?,?(.*)?$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'inf'
};
if (match[1]) {
event.duration = parseFloat(match[1], 10);
}
if (match[2]) {
event.title = match[2];
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-TARGETDURATION:?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'targetduration'
};
if (match[1]) {
event.duration = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-VERSION:?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'version'
};
if (match[1]) {
event.version = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'media-sequence'
};
if (match[1]) {
event.number = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'playlist-type'
};
if (match[1]) {
event.playlistType = match[1];
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'byterange'
};
if (match[1]) {
event.length = parseInt(match[1], 10);
}
if (match[2]) {
event.offset = parseInt(match[2], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-ALLOW-CACHE:?(YES|NO)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'allow-cache'
};
if (match[1]) {
event.allowed = !(/NO/).test(match[1]);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-STREAM-INF:?(.*)$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'stream-inf'
};
if (match[1]) {
event.attributes = parseAttributes(match[1]);
if (event.attributes.RESOLUTION) {
(function() {
var
split = event.attributes.RESOLUTION.split('x'),
resolution = {};
if (split[0]) {
resolution.width = parseInt(split[0], 10);
}
if (split[1]) {
resolution.height = parseInt(split[1], 10);
}
event.attributes.RESOLUTION = resolution;
})();
}
if (event.attributes.BANDWIDTH) {
event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
}
if (event.attributes['PROGRAM-ID']) {
event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
}
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-ENDLIST/).exec(line);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'endlist'
});
return;
}
// unknown tag type
this.trigger('data', {
type: 'tag',
data: line.slice(4, line.length)
});
};
Parser = function() {
var
self = this,
uris = [],
currentUri = {};
Parser.prototype.init.call(this);
this.lineStream = new LineStream();
this.parseStream = new ParseStream();
this.lineStream.pipe(this.parseStream);
// the manifest is empty until the parse stream begins delivering data
this.manifest = {
allowCache: true
};
// update the manifest with the m3u8 entry from the parse stream
this.parseStream.on('data', function(entry) {
({
tag: function() {
// switch based on the tag type
(({
'allow-cache': function() {
this.manifest.allowCache = entry.allowed;
if (!('allowed' in entry)) {
this.trigger('info', {
message: 'defaulting allowCache to YES'
});
this.manifest.allowCache = true;
}
},
'byterange': function() {
var byterange = {};
if ('length' in entry) {
currentUri.byterange = byterange;
byterange.length = entry.length;
if (!('offset' in entry)) {
this.trigger('info', {
message: 'defaulting offset to zero'
});
entry.offset = 0;
}
}
if ('offset' in entry) {
currentUri.byterange = byterange;
byterange.offset = entry.offset;
}
},
'inf': function() {
if (!this.manifest.playlistType) {
this.manifest.playlistType = 'VOD';
this.trigger('info', {
message: 'defaulting playlist type to VOD'
});
}
if (!('mediaSequence' in this.manifest)) {
this.manifest.mediaSequence = 0;
this.trigger('info', {
message: 'defaulting media sequence to zero'
});
}
if (entry.duration >= 0) {
currentUri.duration = entry.duration;
}
this.manifest.segments = uris;
},
'media-sequence': function() {
if (!isFinite(entry.number)) {
this.trigger('warn', {
message: 'ignoring invalid media sequence: ' + entry.number
});
return;
}
this.manifest.mediaSequence = entry.number;
},
'playlist-type': function() {
if (!(/VOD|EVENT/).test(entry.playlistType)) {
this.trigger('warn', {
message: 'ignoring unknown playlist type: ' + entry.playlist
});
return;
}
this.manifest.playlistType = entry.playlistType;
},
'stream-inf': function() {
if (!currentUri.attributes) {
currentUri.attributes = {};
}
currentUri.attributes = mergeOptions(currentUri.attributes,
entry.attributes);
this.manifest.playlists = uris;
},
'targetduration': function() {
if (!isFinite(entry.duration) || entry.duration < 0) {
this.trigger('warn', {
message: 'ignoring invalid target duration: ' + entry.duration
});
return;
}
this.manifest.targetDuration = entry.duration;
}
})[entry.tagType] || noop).call(self);
},
uri: function() {
currentUri.uri = entry.uri;
uris.push(currentUri);
// prepare for the next URI
currentUri = {};
},
comment: function() {
// comments are not important for playback
}
})[entry.type].call(self);
});
};
Parser.prototype = new Stream();
Parser.prototype.push = function(chunk) {
this.lineStream.push(chunk);
};
Parser.prototype.end = function() {
// flush any buffered input
this.lineStream.push('\n');
};
window.videojs.m3u8 = {
LineStream: LineStream,
ParseStream: ParseStream,
Parser: Parser
};
})(window.parseInt, window.isFinite, window.videojs.util.mergeOptions);

253
src/m3u8/m3u8-tokenizer.js

@ -1,253 +0,0 @@
(function(parseInt, undefined) {
var
parseAttributes = function(attributes) {
var
attrs = attributes.split(','),
i = attrs.length,
result = {},
attr;
while (i--) {
attr = attrs[i].split('=');
result[attr[0]] = attr[1];
}
return result;
},
Stream,
Tokenizer,
Parser;
Stream = function() {
var listeners = {};
this.on = function(type, listener) {
if (!listeners[type]) {
listeners[type] = [];
}
listeners[type].push(listener);
};
this.off = function(type, listener) {
var index;
if (!listeners[type]) {
return false;
}
index = listeners[type].indexOf(listener);
listeners[type].splice(index, 1);
return index > -1;
};
this.trigger = function(type) {
var callbacks, i, length, args;
callbacks = listeners[type];
if (!callbacks) {
return;
}
args = Array.prototype.slice.call(arguments, 1);
length = callbacks.length;
for (i = 0; i < length; ++i) {
callbacks[i].apply(this, args);
}
};
};
Stream.prototype.pipe = function(destination) {
this.on('data', function(data) {
destination.push(data);
});
};
Tokenizer = function() {
var
buffer = '',
tokenizer;
this.push = function(data) {
var nextNewline;
buffer += data;
nextNewline = buffer.indexOf('\n');
for (; nextNewline > -1; nextNewline = buffer.indexOf('\n')) {
this.trigger('data', buffer.substring(0, nextNewline));
buffer = buffer.substring(nextNewline + 1);
}
};
};
Tokenizer.prototype = new Stream();
Parser = function() {};
Parser.prototype = new Stream();
Parser.prototype.push = function(line) {
var match, event;
if (line.length === 0) {
// ignore empty lines
return;
}
// URIs
if (line[0] !== '#') {
this.trigger('data', {
type: 'uri',
uri: line
});
return;
}
// Comments
if (line.indexOf('#EXT') !== 0) {
this.trigger('data', {
type: 'comment',
text: line.slice(1)
});
return;
}
// Tags
match = /^#EXTM3U/.exec(line);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'm3u'
});
return;
}
match = (/^#EXTINF:?([0-9\.]*)?,?(.*)?$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'inf'
};
if (match[1]) {
event.duration = parseInt(match[1], 10);
}
if (match[2]) {
event.title = match[2];
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-TARGETDURATION:?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'targetduration'
};
if (match[1]) {
event.duration = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-VERSION:?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'version'
};
if (match[1]) {
event.version = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-MEDIA-SEQUENCE:?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'media-sequence'
};
if (match[1]) {
event.number = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'playlist-type'
};
if (match[1]) {
event.playlistType = match[1];
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'byterange'
};
if (match[1]) {
event.length = parseInt(match[1], 10);
}
if (match[2]) {
event.offset = parseInt(match[2], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-ALLOW-CACHE:?(YES|NO)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'allow-cache'
};
if (match[1]) {
event.allowed = !(/NO/).test(match[1]);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-STREAM-INF:?(.*)$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'stream-inf'
};
if (match[1]) {
event.attributes = parseAttributes(match[1]);
if (event.attributes.RESOLUTION) {
(function() {
var
split = event.attributes.RESOLUTION.split('x'),
resolution = {};
if (split[0]) {
resolution.width = parseInt(split[0], 10);
}
if (split[1]) {
resolution.height = parseInt(split[1], 10);
}
event.attributes.RESOLUTION = resolution;
})();
}
if (event.attributes.BANDWIDTH) {
event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
}
if (event.attributes['PROGRAM-ID']) {
event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
}
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-ENDLIST/).exec(line);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'endlist'
});
return;
}
// unknown tag type
this.trigger('data', {
type: 'tag',
data: line.slice(4, line.length)
});
};
window.videojs.m3u8 = {
Tokenizer: Tokenizer,
Parser: Parser
};
})(window.parseInt);

795
test/m3u8_test.js
File diff suppressed because it is too large
View File

2
test/manifest/playlistM3U8data.js

@ -1,4 +1,4 @@
window.playlistData = '#EXTM3U\n'+
window.playlistM3U8data = '#EXTM3U\n'+
'#EXT-X-TARGETDURATION:10\n' + '#EXT-X-TARGETDURATION:10\n' +
'#EXT-X-VERSION:4\n' + '#EXT-X-VERSION:4\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' + '#EXT-X-MEDIA-SEQUENCE:0\n' +

2
test/manifest/playlist_media_sequence_template.js

@ -1,7 +1,7 @@
window.playlist_media_sequence_template = '#EXTM3U\n'+ window.playlist_media_sequence_template = '#EXTM3U\n'+
'#EXT-X-PLAYLIST-TYPE:VOD\n'+ '#EXT-X-PLAYLIST-TYPE:VOD\n'+
'{{#if mediaSequence}}#EXT-X-MEDIA-SEQUENCE:{{{mediaSequence}}}{{/if}}\n'+ '{{#if mediaSequence}}#EXT-X-MEDIA-SEQUENCE:{{{mediaSequence}}}{{/if}}\n'+
'{{#if mediaSequence1}}#EXT-X-MEDIA-SEQUENCE:{{{mediaSequence2}}}{{/if}}\n'+
'{{#if mediaSequence1}}#EXT-X-MEDIA-SEQUENCE:{{{mediaSequence1}}}{{/if}}\n'+
'#EXT-X-ALLOW-CACHE:YES\n'+ '#EXT-X-ALLOW-CACHE:YES\n'+
'#EXT-X-TARGETDURATION:8\n'+ '#EXT-X-TARGETDURATION:8\n'+
'#EXTINF:6.640,{}\n'+ '#EXTINF:6.640,{}\n'+

5
test/video-js-hls.html

@ -11,7 +11,7 @@
<script src="../libs/handlebars/handlebars-v1.1.2.js"></script> <script src="../libs/handlebars/handlebars-v1.1.2.js"></script>
<!-- video.js --> <!-- video.js -->
<script src="../node_modules/video.js/video.dev.js"></script>
<script src="../node_modules/video.js/dist/video-js/video.js"></script>
<!-- HLS plugin --> <!-- HLS plugin -->
<script src="../src/video-js-hls.js"></script> <script src="../src/video-js-hls.js"></script>
@ -22,10 +22,9 @@
<script src="../src/segment-parser.js"></script> <script src="../src/segment-parser.js"></script>
<!-- M3U8 --> <!-- M3U8 -->
<script src="../src/m3u8/m3u8-tokenizer.js"></script>
<script src="../src/m3u8/m3u8-parser.js"></script>
<script src="../src/m3u8/m3u8.js"></script> <script src="../src/m3u8/m3u8.js"></script>
<script src="../src/m3u8/m3u8-tag-types.js"></script> <script src="../src/m3u8/m3u8-tag-types.js"></script>
<script src="../build/m3u8-parser.js"></script>
<script src="../src/manifest-controller.js"></script> <script src="../src/manifest-controller.js"></script>
<!-- M3U8 TEST DATA --> <!-- M3U8 TEST DATA -->
<script src="manifest/playlistM3U8data.js"></script> <script src="manifest/playlistM3U8data.js"></script>

Loading…
Cancel
Save