15b36142 by David LaPalomento

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.
1 parent 9628b848
......@@ -18,7 +18,7 @@
"grunt": "~0.4.1"
},
"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"
}
}
......
(function(parseInt, undefined) {
(function(parseInt, isFinite, mergeOptions, undefined) {
var
noop = function() {},
parseAttributes = function(attributes) {
var
attrs = attributes.split(','),
......@@ -13,37 +14,40 @@
return result;
},
Stream,
Tokenizer,
LineStream,
ParseStream,
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);
}
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) {
......@@ -52,10 +56,9 @@
});
};
Tokenizer = function() {
var
buffer = '',
tokenizer;
LineStream = function() {
var buffer = '';
LineStream.prototype.init.call(this);
this.push = function(data) {
var nextNewline;
......@@ -69,11 +72,13 @@
}
};
};
Tokenizer.prototype = new Stream();
LineStream.prototype = new Stream();
Parser = function() {};
Parser.prototype = new Stream();
Parser.prototype.push = function(line) {
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
......@@ -114,7 +119,7 @@
tagType: 'inf'
};
if (match[1]) {
event.duration = parseInt(match[1], 10);
event.duration = parseFloat(match[1], 10);
}
if (match[2]) {
event.title = match[2];
......@@ -146,7 +151,7 @@
this.trigger('data', event);
return;
}
match = (/^#EXT-X-MEDIA-SEQUENCE:?([0-9.]*)?/).exec(line);
match = (/^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
......@@ -246,8 +251,135 @@
});
};
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 = {
Tokenizer: Tokenizer,
LineStream: LineStream,
ParseStream: ParseStream,
Parser: Parser
};
})(window.parseInt);
})(window.parseInt, window.isFinite, window.videojs.util.mergeOptions);
......
window.playlistData = '#EXTM3U\n'+
window.playlistM3U8data = '#EXTM3U\n'+
'#EXT-X-TARGETDURATION:10\n' +
'#EXT-X-VERSION:4\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
......
window.playlist_media_sequence_template = '#EXTM3U\n'+
'#EXT-X-PLAYLIST-TYPE:VOD\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-TARGETDURATION:8\n'+
'#EXTINF:6.640,{}\n'+
......
......@@ -11,7 +11,7 @@
<script src="../libs/handlebars/handlebars-v1.1.2.js"></script>
<!-- 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 -->
<script src="../src/video-js-hls.js"></script>
......@@ -22,10 +22,9 @@
<script src="../src/segment-parser.js"></script>
<!-- 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-tag-types.js"></script>
<script src="../build/m3u8-parser.js"></script>
<script src="../src/manifest-controller.js"></script>
<!-- M3U8 TEST DATA -->
<script src="manifest/playlistM3U8data.js"></script>
......