d7019d06 by David LaPalomento

Merge pull request #534 from BrandonOCasey/browserify-p2

browserify-p2: m3u8, stream, and stub
2 parents b0caca88 c2ce93db
......@@ -34,5 +34,5 @@ dist-test/
docs/api/
es5/
tmp
test/data/manifests.js
test/data/expected.js
test/test-manifests.js
test/test-expected.js
......
......@@ -40,14 +40,17 @@
</label>
<button type=submit>Load</button>
</form>
<ul>
<li><a href="/test/">Run unit tests in browser.</a></li>
<li><a href="/docs/api/">Read generated docs.</a></li>
</ul>
<script src="/node_modules/video.js/dist/video.js"></script>
<script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
<script src="/node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="/src/videojs-hls.js"></script>
<script src="/src/videojs-contrib-hls.js"></script>
<script src="/src/xhr.js"></script>
<script src="/src/stream.js"></script>
<script src="/src/m3u8/m3u8-parser.js"></script>
<script src="/dist/videojs-contrib-hls.js"></script>
<script src="/src/playlist.js"></script>
<script src="/src/playlist-loader.js"></script>
<script src="/src/decrypter.js"></script>
......
......@@ -2,7 +2,7 @@
"name": "videojs-contrib-hls",
"version": "1.3.5",
"description": "Play back HLS with video.js, even where it's not natively supported",
"main": "es5/videojs-hls.js",
"main": "es5/stub.js",
"engines": {
"node": ">= 0.10.12"
},
......@@ -13,16 +13,17 @@
"scripts": {
"prebuild": "npm run clean",
"build": "npm-run-all -p build:*",
"build:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.build();\"",
"build:js": "npm-run-all build:js:babel build:js:browserify build:js:bannerize build:js:uglify",
"build:js:babel": "babel src -d es5",
"build:js:bannerize": "bannerize dist/videojs-contrib-hls.js --banner=scripts/banner.ejs",
"build:js:browserify": "browserify . -s src/videojs-hls.js -o dist/videojs-contrib-hls.js",
"build:js:browserify": "browserify . -s videojs-contrib-hls -o dist/videojs-contrib-hls.js",
"build:js:uglify": "uglifyjs dist/videojs-contrib-hls.js --comments --mangle --compress -o dist/videojs-contrib-hls.min.js",
"build:test": "node scripts/build-test.js",
"clean": "npm-run-all clean:*",
"build:test": "npm-run-all build:test:manifest build:test:js",
"build:test:js": "node scripts/build-test.js",
"build:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.build();\"",
"clean": "npm-run-all -p clean:*",
"clean:build": "node -e \"var s=require('shelljs'),d=['dist','dist-test','es5'];s.rm('-rf',d);s.mkdir('-p',d);\"",
"clean:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.clean();\"",
"clean:test": "node -e \"var b=require('./scripts/manifest-data.js'); b.clean();\"",
"docs": "npm-run-all docs:*",
"docs:api": "jsdoc src -r -d docs/api",
"docs:toc": "doctoc README.md",
......@@ -39,9 +40,10 @@
"preversion": "npm test",
"version": "npm run build",
"watch": "npm-run-all -p watch:*",
"watch:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
"watch:js": "watchify src/videojs-hls.js -t babelify -v -o dist/videojs-contrib-hls.js",
"watch:test": "node scripts/watch-test.js",
"watch:js": "watchify src/stub.js -t babelify -v -o dist/videojs-contrib-hls.js",
"watch:test": "npm-run-all -p watch:test:*",
"watch:test:js": "node scripts/watch-test.js",
"watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
"prepublish": "npm run build"
},
"keywords": [
......@@ -69,7 +71,8 @@
"test/karma",
"scripts",
"utils",
"test/data"
"test/test-manifests.js",
"test/test-expected.js"
]
},
"files": [
......
......@@ -2,7 +2,7 @@ var browserify = require('browserify');
var fs = require('fs');
var glob = require('glob');
glob('test/**/*.test.js', function(err, files) {
glob('test/{m3u8,stub}.test.js', function(err, files) {
browserify(files)
.transform('babelify')
.bundle()
......
var fs = require('fs');
var path = require('path');
var basePath = path.resolve(__dirname + '/..');
var testDataDir = basePath + '/test/data';
var manifestDir = basePath + '/utils/manifest';
var manifestFilepath = testDataDir + '/manifests.js';
var expectedFilepath = testDataDir + '/expected.js';
var basePath = path.resolve(__dirname, '..');
var testDataDir = path.join(basePath,'test');
var manifestDir = path.join(basePath, 'utils', 'manifest');
var manifestFilepath = path.join(testDataDir, 'test-manifests.js');
var expectedFilepath = path.join(testDataDir, 'test-expected.js');
var build = function() {
var manifests = 'window.manifests = {\n';
var expected = 'window.expected = {\n';
var manifests = 'export default {\n';
var expected = 'export default {\n';
var files = fs.readdirSync(manifestDir);
while (files.length > 0) {
......
......@@ -3,7 +3,7 @@ var fs = require('fs');
var glob = require('glob');
var watchify = require('watchify');
glob('test/**/*.test.js', function(err, files) {
glob('test/{m3u8,stub}.test.js', function(err, files) {
var b = browserify(files, {
cache: {},
packageCache: {},
......
{
"curly": true,
"eqeqeq": true,
"globals": {
"console": true
},
"immed": true,
"latedef": true,
"newcap": true,
"noarg": true,
"sub": true,
"undef": true,
"unused": true,
"boss": true,
"eqnull": true,
"browser": true
}
......@@ -5,30 +5,55 @@
* that do not assume the entirety of the manifest is ready and expose a
* ReadableStream-like interface.
*/
(function(videojs, parseInt, isFinite, mergeOptions, undefined) {
var
noop = function() {},
// "forgiving" attribute list psuedo-grammar:
// attributes -> keyvalue (',' keyvalue)*
// keyvalue -> key '=' value
// key -> [^=]*
// value -> '"' [^"]* '"' | [^,]*
attributeSeparator = (function() {
var
key = '[^=]*',
value = '"[^"]*"|[^,]*',
keyvalue = '(?:' + key + ')=(?:' + value + ')';
import Stream from '../stream';
import {mergeOptions} from 'video.js';
/**
* A stream that buffers string input and generates a `data` event for each
* line.
*/
export class LineStream extends Stream {
constructor() {
super();
this.buffer = '';
}
/**
* Add new data to be parsed.
* @param data {string} the text to process
*/
push(data) {
let nextNewline;
this.buffer += data;
nextNewline = this.buffer.indexOf('\n');
for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) {
this.trigger('data', this.buffer.substring(0, nextNewline));
this.buffer = this.buffer.substring(nextNewline + 1);
}
}
}
// "forgiving" attribute list psuedo-grammar:
// attributes -> keyvalue (',' keyvalue)*
// keyvalue -> key '=' value
// key -> [^=]*
// value -> '"' [^"]* '"' | [^,]*
const attributeSeparator = function() {
let key = '[^=]*';
let value = '"[^"]*"|[^,]*';
let keyvalue = '(?:' + key + ')=(?:' + value + ')';
return new RegExp('(?:^|,)(' + keyvalue + ')');
})(),
parseAttributes = function(attributes) {
var
};
const parseAttributes = function(attributes) {
// split the string using attributes as the separator
attrs = attributes.split(attributeSeparator),
i = attrs.length,
result = {},
attr;
let attrs = attributes.split(attributeSeparator());
let i = attrs.length;
let result = {};
let attr;
while (i--) {
// filter out unmatched portions of the string
......@@ -37,7 +62,7 @@
}
// split the key and value
attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1);
attr = (/([^=]*)=(.*)/).exec(attrs[i]).slice(1);
// trim whitespace and remove optional quotes around the value
attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
......@@ -45,39 +70,9 @@
result[attr[0]] = attr[1];
}
return result;
},
Stream = videojs.Hls.Stream,
LineStream,
ParseStream,
Parser;
/**
* A stream that buffers string input and generates a `data` event for each
* line.
*/
LineStream = function() {
var buffer = '';
LineStream.prototype.init.call(this);
/**
* Add new data to be parsed.
* @param data {string} the text to process
*/
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();
};
/**
/**
* A line-level M3U8 parser event stream. It expects to receive input one
* line at a time and performs a context-free parse of its contents. A stream
* interpretation of a manifest can be useful if the manifest is expected to
......@@ -98,18 +93,20 @@
* tags are given the tag type `unknown` and a single additional property
* `data` with the remainder of the input.
*/
ParseStream = function() {
ParseStream.prototype.init.call(this);
};
ParseStream.prototype = new Stream();
export class ParseStream extends Stream {
constructor() {
super();
}
/**
* Parses an additional line of input.
* @param line {string} a single line of an M3U8 file to parse
*/
ParseStream.prototype.push = function(line) {
var match, event;
push(line) {
let match;
let event;
//strip whitespace
// strip whitespace
line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, '');
if (line.length === 0) {
// ignore empty lines
......@@ -134,12 +131,12 @@
return;
}
//strip off any carriage returns here so the regex matching
//doesn't have to account for them.
line = line.replace('\r','');
// strip off any carriage returns here so the regex matching
// doesn't have to account for them.
line = line.replace('\r', '');
// Tags
match = /^#EXTM3U/.exec(line);
match = (/^#EXTM3U/).exec(line);
if (match) {
this.trigger('data', {
type: 'tag',
......@@ -271,10 +268,9 @@
event.attributes = parseAttributes(match[1]);
if (event.attributes.RESOLUTION) {
(function() {
var
split = event.attributes.RESOLUTION.split('x'),
resolution = {};
let split = event.attributes.RESOLUTION.split('x');
let resolution = {};
if (split[0]) {
resolution.width = parseInt(split[0], 10);
}
......@@ -282,7 +278,6 @@
resolution.height = parseInt(split[1], 10);
}
event.attributes.RESOLUTION = resolution;
})();
}
if (event.attributes.BANDWIDTH) {
event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
......@@ -320,7 +315,7 @@
event.attributes = parseAttributes(match[1]);
// parse the IV string into a Uint32Array
if (event.attributes.IV) {
if (event.attributes.IV.substring(0,2) === '0x') {
if (event.attributes.IV.substring(0, 2) === '0x') {
event.attributes.IV = event.attributes.IV.substring(2);
}
......@@ -341,7 +336,8 @@
type: 'tag',
data: line.slice(4, line.length)
});
};
}
}
/**
* A parser for M3U8 files. The current interpretation of the input is
......@@ -361,17 +357,19 @@
* events during the parse if it encounters input that seems invalid or
* requires some property of the manifest object to be defaulted.
*/
Parser = function() {
var
self = this,
uris = [],
currentUri = {},
key;
Parser.prototype.init.call(this);
export class Parser extends Stream {
constructor() {
super();
this.lineStream = new LineStream();
this.parseStream = new ParseStream();
this.lineStream.pipe(this.parseStream);
/* eslint-disable consistent-this */
let self = this;
/* eslint-enable consistent-this */
let uris = [];
let currentUri = {};
let key;
let noop = function() {};
// the manifest is empty until the parse stream begins delivering data
this.manifest = {
......@@ -382,10 +380,10 @@
// update the manifest with the m3u8 entry from the parse stream
this.parseStream.on('data', function(entry) {
({
tag: function() {
tag() {
// switch based on the tag type
(({
'allow-cache': function() {
'allow-cache'() {
this.manifest.allowCache = entry.allowed;
if (!('allowed' in entry)) {
this.trigger('info', {
......@@ -394,8 +392,9 @@
this.manifest.allowCache = true;
}
},
'byterange': function() {
var byterange = {};
byterange() {
let byterange = {};
if ('length' in entry) {
currentUri.byterange = byterange;
byterange.length = entry.length;
......@@ -412,10 +411,10 @@
byterange.offset = entry.offset;
}
},
'endlist': function() {
endlist() {
this.manifest.endList = true;
},
'inf': function() {
inf() {
if (!('mediaSequence' in this.manifest)) {
this.manifest.mediaSequence = 0;
this.trigger('info', {
......@@ -435,7 +434,7 @@
this.manifest.segments = uris;
},
'key': function() {
key() {
if (!entry.attributes) {
this.trigger('warn', {
message: 'ignoring key declaration without attribute list'
......@@ -465,11 +464,11 @@
uri: entry.attributes.URI
};
if (entry.attributes.IV !== undefined) {
if (typeof entry.attributes.IV !== 'undefined') {
key.iv = entry.attributes.IV;
}
},
'media-sequence': function() {
'media-sequence'() {
if (!isFinite(entry.number)) {
this.trigger('warn', {
message: 'ignoring invalid media sequence: ' + entry.number
......@@ -478,7 +477,7 @@
}
this.manifest.mediaSequence = entry.number;
},
'discontinuity-sequence': function() {
'discontinuity-sequence'() {
if (!isFinite(entry.number)) {
this.trigger('warn', {
message: 'ignoring invalid discontinuity sequence: ' + entry.number
......@@ -487,7 +486,7 @@
}
this.manifest.discontinuitySequence = entry.number;
},
'playlist-type': function() {
'playlist-type'() {
if (!(/VOD|EVENT/).test(entry.playlistType)) {
this.trigger('warn', {
message: 'ignoring unknown playlist type: ' + entry.playlist
......@@ -496,7 +495,7 @@
}
this.manifest.playlistType = entry.playlistType;
},
'stream-inf': function() {
'stream-inf'() {
this.manifest.playlists = uris;
if (!entry.attributes) {
......@@ -512,11 +511,11 @@
currentUri.attributes = mergeOptions(currentUri.attributes,
entry.attributes);
},
'discontinuity': function() {
discontinuity() {
currentUri.discontinuity = true;
this.manifest.discontinuityStarts.push(uris.length);
},
'targetduration': function() {
targetduration() {
if (!isFinite(entry.duration) || entry.duration < 0) {
this.trigger('warn', {
message: 'ignoring invalid target duration: ' + entry.duration
......@@ -525,7 +524,7 @@
}
this.manifest.targetDuration = entry.duration;
},
'totalduration': function() {
totalduration() {
if (!isFinite(entry.duration) || entry.duration < 0) {
this.trigger('warn', {
message: 'ignoring invalid total duration: ' + entry.duration
......@@ -536,7 +535,7 @@
}
})[entry.tagType] || noop).call(self);
},
uri: function() {
uri() {
currentUri.uri = entry.uri;
uris.push(currentUri);
......@@ -556,33 +555,36 @@
// prepare for the next URI
currentUri = {};
},
comment: function() {
comment() {
// comments are not important for playback
}
})[entry.type].call(self);
});
};
Parser.prototype = new Stream();
}
/**
* Parse the input string and update the manifest object.
* @param chunk {string} a potentially incomplete portion of the manifest
*/
Parser.prototype.push = function(chunk) {
push(chunk) {
this.lineStream.push(chunk);
};
}
/**
* Flush any remaining input. This can be handy if the last line of an M3U8
* manifest did not contain a trailing newline but the file has been
* completely received.
*/
Parser.prototype.end = function() {
end() {
// flush any buffered input
this.lineStream.push('\n');
};
}
window.videojs.m3u8 = {
LineStream: LineStream,
ParseStream: ParseStream,
Parser: Parser
};
})(window.videojs, window.parseInt, window.isFinite, window.videojs.mergeOptions);
}
export default {
LineStream,
ParseStream,
Parser
};
......
......@@ -2,45 +2,57 @@
* A lightweight readable stream implemention that handles event dispatching.
* Objects that inherit from streams should call init in their constructors.
*/
(function(videojs, undefined) {
var Stream = function() {
this.init = function() {
var listeners = {};
export default class Stream {
constructor() {
this.init();
}
init() {
this.listeners = {};
}
/**
* Add a listener for a specified event type.
* @param type {string} the event name
* @param listener {function} the callback to be invoked when an event of
* the specified type occurs
*/
this.on = function(type, listener) {
if (!listeners[type]) {
listeners[type] = [];
on(type, listener) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
listeners[type].push(listener);
};
this.listeners[type].push(listener);
}
/**
* Remove a listener for a specified event type.
* @param type {string} the event name
* @param listener {function} a function previously registered for this
* type of event through `on`
*/
this.off = function(type, listener) {
var index;
if (!listeners[type]) {
off(type, listener) {
let index;
if (!this.listeners[type]) {
return false;
}
index = listeners[type].indexOf(listener);
listeners[type].splice(index, 1);
index = this.listeners[type].indexOf(listener);
this.listeners[type].splice(index, 1);
return index > -1;
};
}
/**
* Trigger an event of the specified type on this stream. Any additional
* arguments to this function are passed as parameters to event listeners.
* @param type {string} the event name
*/
this.trigger = function(type) {
var callbacks, i, length, args;
callbacks = listeners[type];
trigger(type) {
let callbacks;
let i;
let length;
let args;
callbacks = this.listeners[type];
if (!callbacks) {
return;
}
......@@ -60,15 +72,14 @@
callbacks[i].apply(this, args);
}
}
};
}
/**
* Destroys the stream and cleans up.
*/
this.dispose = function() {
listeners = {};
};
};
};
dispose() {
this.listeners = {};
}
/**
* Forwards all `data` events on this stream to the destination stream. The
* destination stream should provide a method `push` to receive the data
......@@ -76,11 +87,9 @@
* @param destination {stream} the stream that will receive all `data` events
* @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
*/
Stream.prototype.pipe = function(destination) {
pipe(destination) {
this.on('data', function(data) {
destination.push(data);
});
};
videojs.Hls.Stream = Stream;
})(window.videojs);
}
}
......
import m3u8 from './m3u8';
import Stream from './stream';
import videojs from 'video.js';
if(typeof window.videojs.Hls === 'undefined') {
videojs.Hls = {};
}
videojs.Hls.Stream = Stream;
videojs.m3u8 = m3u8;
......@@ -16,22 +16,16 @@
<script src="/node_modules/video.js/dist/video.js"></script>
<script src="/node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<script src="/src/videojs-hls.js"></script>
<script src="/src/videojs-contrib-hls.js"></script>
<script src="/src/xhr.js"></script>
<script src="/src/stream.js"></script>
<script src="/src/m3u8/m3u8-parser.js"></script>
<script src="/dist/videojs-contrib-hls.js"></script>
<script src="/src/playlist.js"></script>
<script src="/src/playlist-loader.js"></script>
<script src="/src/decrypter.js"></script>
<script src="/src/bin-utils.js"></script>
<script src="/test/data/manifests.js"></script>
<script src="/test/data/expected.js"></script>
<script src="/test/data/ts-segment-bc.js"></script>
<script src="/test/videojs-hls.test.js"></script>
<script src="/test/m3u8.test.js"></script>
<script src="/test/videojs-contrib-hls.test.js"></script>
<script src="/dist-test/videojs-contrib-hls.js"></script>
<script src="/test/playlist.test.js"></script>
<script src="/test/playlist-loader.test.js"></script>
<script src="/test/decrypter.test.js"></script>
......
......@@ -2,8 +2,7 @@ var merge = require('lodash-compat/object/merge');
var DEFAULTS = {
basePath: '../..',
//frameworks: ['browserify', 'qunit'],
frameworks: ['qunit'],
frameworks: ['browserify', 'qunit'],
files: [
......@@ -16,20 +15,19 @@ var DEFAULTS = {
'node_modules/pkcs7/dist/pkcs7.unpad.js',
'node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'src/videojs-hls.js',
// these two stub old functionality
'src/videojs-contrib-hls.js',
'src/xhr.js',
'src/stream.js',
'src/m3u8/m3u8-parser.js',
'dist/videojs-contrib-hls.js',
'src/playlist.js',
'src/playlist-loader.js',
'src/decrypter.js',
'src/bin-utils.js',
'test/data/manifests.js',
'test/data/expected.js',
'test/data/ts-segment-bc.js',
'test/stub.test.js',
'test/videojs-hls.test.js',
'test/videojs-contrib-hls.test.js',
'test/m3u8.test.js',
'test/playlist.test.js',
'test/playlist-loader.test.js',
......@@ -44,12 +42,12 @@ var DEFAULTS = {
],
plugins: [
// 'karma-browserify',
'karma-browserify',
'karma-qunit'
],
preprocessors: {
// 'test/**/*.js': ['browserify']
'test/{stub,m3u8}.test.js': ['browserify']
},
reporters: ['dots'],
......@@ -59,18 +57,16 @@ var DEFAULTS = {
singleRun: true,
concurrency: Infinity,
/*
browserify: {
debug: true,
transform: [
'babelify',
'browserify-shim'
],
noparse: [
noParse: [
'test/data/**',
]
}
*/
};
/**
......
......@@ -29,6 +29,7 @@ module.exports = function(config) {
postDetection: function(availableBrowsers) {
var safariIndex = availableBrowsers.indexOf('Safari');
if(safariIndex !== -1) {
console.log("Not running safari it is/was broken");
availableBrowsers.splice(safariIndex, 1);
}
return availableBrowsers;
......
(function(window, undefined) {
var
//manifestController = this.manifestController,
m3u8 = window.videojs.m3u8,
ParseStream = m3u8.ParseStream,
parseStream,
LineStream = m3u8.LineStream,
lineStream,
Parser = m3u8.Parser,
parser;
/*
M3U8 Test Suite
*/
QUnit.module('LineStream', {
setup: function() {
lineStream = new LineStream();
import {ParseStream, LineStream, Parser} from '../src/m3u8';
import QUnit from 'qunit';
import testDataExpected from './test-expected.js';
import testDataManifests from './test-manifests.js';
QUnit.module('LineStream', {
beforeEach() {
this.lineStream = new LineStream();
}
});
test('empty inputs produce no tokens', function() {
var data = false;
lineStream.on('data', function() {
});
QUnit.test('empty inputs produce no tokens', function() {
let data = false;
this.lineStream.on('data', function() {
data = true;
});
lineStream.push('');
ok(!data, 'no tokens were produced');
});
test('splits on newlines', function() {
var lines = [];
lineStream.on('data', function(line) {
lines.push(line);
});
lineStream.push('#EXTM3U\nmovie.ts\n');
this.lineStream.push('');
QUnit.ok(!data, 'no tokens were produced');
});
QUnit.test('splits on newlines', function() {
let lines = [];
strictEqual(2, lines.length, 'two lines are ready');
strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
});
test('empty lines become empty strings', function() {
var lines = [];
lineStream.on('data', function(line) {
this.lineStream.on('data', function(line) {
lines.push(line);
});
lineStream.push('\n\n');
this.lineStream.push('#EXTM3U\nmovie.ts\n');
strictEqual(2, lines.length, 'two lines are ready');
strictEqual('', lines.shift(), 'the first line is empty');
strictEqual('', lines.shift(), 'the second line is empty');
});
test('handles lines broken across appends', function() {
var lines = [];
lineStream.on('data', function(line) {
QUnit.strictEqual(2, lines.length, 'two lines are ready');
QUnit.strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
QUnit.strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
});
QUnit.test('empty lines become empty strings', function() {
let lines = [];
this.lineStream.on('data', function(line) {
lines.push(line);
});
lineStream.push('#EXTM');
strictEqual(0, lines.length, 'no lines are ready');
this.lineStream.push('\n\n');
QUnit.strictEqual(2, lines.length, 'two lines are ready');
QUnit.strictEqual('', lines.shift(), 'the first line is empty');
QUnit.strictEqual('', lines.shift(), 'the second line is empty');
});
QUnit.test('handles lines broken across appends', function() {
let lines = [];
lineStream.push('3U\nmovie.ts\n');
strictEqual(2, lines.length, 'two lines are ready');
strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
this.lineStream.on('data', function(line) {
lines.push(line);
});
test('stops sending events after deregistering', function() {
var
temporaryLines = [],
temporary = function(line) {
this.lineStream.push('#EXTM');
QUnit.strictEqual(0, lines.length, 'no lines are ready');
this.lineStream.push('3U\nmovie.ts\n');
QUnit.strictEqual(2, lines.length, 'two lines are ready');
QUnit.strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
QUnit.strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
});
QUnit.test('stops sending events after deregistering', function() {
let temporaryLines = [];
let temporary = function(line) {
temporaryLines.push(line);
},
permanentLines = [],
permanent = function(line) {
};
let permanentLines = [];
let permanent = function(line) {
permanentLines.push(line);
};
lineStream.on('data', temporary);
lineStream.on('data', permanent);
lineStream.push('line one\n');
strictEqual(temporaryLines.length, permanentLines.length, 'both callbacks receive the event');
ok(lineStream.off('data', temporary), 'a listener was removed');
lineStream.push('line two\n');
strictEqual(1, temporaryLines.length, 'no new events are received');
strictEqual(2, permanentLines.length, 'new events are still received');
});
QUnit.module('ParseStream', {
setup: function() {
lineStream = new LineStream();
parseStream = new ParseStream();
lineStream.pipe(parseStream);
this.lineStream.on('data', temporary);
this.lineStream.on('data', permanent);
this.lineStream.push('line one\n');
QUnit.strictEqual(temporaryLines.length,
permanentLines.length,
'both callbacks receive the event');
QUnit.ok(this.lineStream.off('data', temporary), 'a listener was removed');
this.lineStream.push('line two\n');
QUnit.strictEqual(1, temporaryLines.length, 'no new events are received');
QUnit.strictEqual(2, permanentLines.length, 'new events are still received');
});
QUnit.module('ParseStream', {
beforeEach() {
this.lineStream = new LineStream();
this.parseStream = new ParseStream();
this.lineStream.pipe(this.parseStream);
}
});
test('parses comment lines', function() {
var
manifest = '# a line that starts with a hash mark without "EXT" is a comment\n',
element;
parseStream.on('data', function(elem) {
});
QUnit.test('parses comment lines', function() {
let manifest = '# a line that starts with a hash mark without "EXT" is a comment\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'comment', 'the type is comment');
strictEqual(element.text,
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'comment', 'the type is comment');
QUnit.strictEqual(element.text,
manifest.slice(1, manifest.length - 1),
'the comment text is parsed');
});
test('parses uri lines', function() {
var
manifest = 'any non-blank line that does not start with a hash-mark is a URI\n',
element;
parseStream.on('data', function(elem) {
});
QUnit.test('parses uri lines', function() {
let manifest = 'any non-blank line that does not start with a hash-mark is a URI\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'uri', 'the type is uri');
strictEqual(element.uri,
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'uri', 'the type is uri');
QUnit.strictEqual(element.uri,
manifest.substring(0, manifest.length - 1),
'the uri text is parsed');
});
test('parses unknown tag types', function() {
var
manifest = '#EXT-X-EXAMPLE-TAG:some,additional,stuff\n',
element;
parseStream.on('data', function(elem) {
});
QUnit.test('parses unknown tag types', function() {
let manifest = '#EXT-X-EXAMPLE-TAG:some,additional,stuff\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the type is tag');
strictEqual(element.data,
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the type is tag');
QUnit.strictEqual(element.data,
manifest.slice(4, manifest.length - 1),
'unknown tag data is preserved');
});
});
// #EXTM3U
test('parses #EXTM3U tags', function() {
var
manifest = '#EXTM3U\n',
element;
parseStream.on('data', function(elem) {
// #EXTM3U
QUnit.test('parses #EXTM3U tags', function() {
let manifest = '#EXTM3U\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'm3u', 'the tag type is m3u');
});
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'm3u', 'the tag type is m3u');
});
// #EXTINF
test('parses minimal #EXTINF tags', function() {
var
manifest = '#EXTINF\n',
element;
parseStream.on('data', function(elem) {
// #EXTINF
QUnit.test('parses minimal #EXTINF tags', function() {
let manifest = '#EXTINF\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'inf', 'the tag type is inf');
});
test('parses #EXTINF tags with durations', function() {
var
manifest = '#EXTINF:15\n',
element;
parseStream.on('data', function(elem) {
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
});
QUnit.test('parses #EXTINF tags with durations', function() {
let manifest = '#EXTINF:15\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'inf', 'the tag type is inf');
strictEqual(element.duration, 15, 'the duration is parsed');
ok(!('title' in element), 'no title is parsed');
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
QUnit.strictEqual(element.duration, 15, 'the duration is parsed');
QUnit.ok(!('title' in element), 'no title is parsed');
manifest = '#EXTINF:21,\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'inf', 'the tag type is inf');
strictEqual(element.duration, 21, 'the duration is parsed');
ok(!('title' in element), 'no title is parsed');
});
test('parses #EXTINF tags with a duration and title', function() {
var
manifest = '#EXTINF:13,Does anyone really use the title attribute?\n',
element;
parseStream.on('data', function(elem) {
this.lineStream.push(manifest);
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
QUnit.strictEqual(element.duration, 21, 'the duration is parsed');
QUnit.ok(!('title' in element), 'no title is parsed');
});
QUnit.test('parses #EXTINF tags with a duration and title', function() {
let manifest = '#EXTINF:13,Does anyone really use the title attribute?\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'inf', 'the tag type is inf');
strictEqual(element.duration, 13, 'the duration is parsed');
strictEqual(element.title,
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
QUnit.strictEqual(element.duration, 13, 'the duration is parsed');
QUnit.strictEqual(element.title,
manifest.substring(manifest.indexOf(',') + 1, manifest.length - 1),
'the title is parsed');
});
test('parses #EXTINF tags with carriage returns', function() {
var
manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n',
element;
parseStream.on('data', function(elem) {
});
QUnit.test('parses #EXTINF tags with carriage returns', function() {
let manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'inf', 'the tag type is inf');
strictEqual(element.duration, 13, 'the duration is parsed');
strictEqual(element.title,
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
QUnit.strictEqual(element.duration, 13, 'the duration is parsed');
QUnit.strictEqual(element.title,
manifest.substring(manifest.indexOf(',') + 1, manifest.length - 2),
'the title is parsed');
});
});
// #EXT-X-TARGETDURATION
test('parses minimal #EXT-X-TARGETDURATION tags', function() {
var
manifest = '#EXT-X-TARGETDURATION\n',
element;
parseStream.on('data', function(elem) {
// #EXT-X-TARGETDURATION
QUnit.test('parses minimal #EXT-X-TARGETDURATION tags', function() {
let manifest = '#EXT-X-TARGETDURATION\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
ok(!('duration' in element), 'no duration is parsed');
});
test('parses #EXT-X-TARGETDURATION with duration', function() {
var
manifest = '#EXT-X-TARGETDURATION:47\n',
element;
parseStream.on('data', function(elem) {
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
QUnit.ok(!('duration' in element), 'no duration is parsed');
});
QUnit.test('parses #EXT-X-TARGETDURATION with duration', function() {
let manifest = '#EXT-X-TARGETDURATION:47\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
strictEqual(element.duration, 47, 'the duration is parsed');
});
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
QUnit.strictEqual(element.duration, 47, 'the duration is parsed');
});
// #EXT-X-VERSION
test('parses minimal #EXT-X-VERSION tags', function() {
var
manifest = '#EXT-X-VERSION:\n',
element;
parseStream.on('data', function(elem) {
// #EXT-X-VERSION
QUnit.test('parses minimal #EXT-X-VERSION tags', function() {
let manifest = '#EXT-X-VERSION:\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'version', 'the tag type is version');
ok(!('version' in element), 'no version is present');
});
test('parses #EXT-X-VERSION with a version', function() {
var
manifest = '#EXT-X-VERSION:99\n',
element;
parseStream.on('data', function(elem) {
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'version', 'the tag type is version');
QUnit.ok(!('version' in element), 'no version is present');
});
QUnit.test('parses #EXT-X-VERSION with a version', function() {
let manifest = '#EXT-X-VERSION:99\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'version', 'the tag type is version');
strictEqual(element.version, 99, 'the version is parsed');
});
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'version', 'the tag type is version');
QUnit.strictEqual(element.version, 99, 'the version is parsed');
});
// #EXT-X-MEDIA-SEQUENCE
QUnit.test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function() {
let manifest = '#EXT-X-MEDIA-SEQUENCE\n';
let element;
// #EXT-X-MEDIA-SEQUENCE
test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function() {
var
manifest = '#EXT-X-MEDIA-SEQUENCE\n',
element;
parseStream.on('data', function(elem) {
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
ok(!('number' in element), 'no number is present');
});
test('parses #EXT-X-MEDIA-SEQUENCE with sequence numbers', function() {
var
manifest = '#EXT-X-MEDIA-SEQUENCE:109\n',
element;
parseStream.on('data', function(elem) {
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
QUnit.ok(!('number' in element), 'no number is present');
});
QUnit.test('parses #EXT-X-MEDIA-SEQUENCE with sequence numbers', function() {
let manifest = '#EXT-X-MEDIA-SEQUENCE:109\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
ok(element.number, 109, 'the number is parsed');
});
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
QUnit.ok(element.number, 109, 'the number is parsed');
});
// #EXT-X-PLAYLIST-TYPE
QUnit.test('parses minimal #EXT-X-PLAYLIST-TYPE tags', function() {
let manifest = '#EXT-X-PLAYLIST-TYPE:\n';
let element;
// #EXT-X-PLAYLIST-TYPE
test('parses minimal #EXT-X-PLAYLIST-TYPE tags', function() {
var
manifest = '#EXT-X-PLAYLIST-TYPE:\n',
element;
parseStream.on('data', function(elem) {
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
ok(!('playlistType' in element), 'no playlist type is present');
});
test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function() {
var
manifest = '#EXT-X-PLAYLIST-TYPE:EVENT\n',
element;
parseStream.on('data', function(elem) {
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
QUnit.ok(!('playlistType' in element), 'no playlist type is present');
});
QUnit.test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function() {
let manifest = '#EXT-X-PLAYLIST-TYPE:EVENT\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
strictEqual(element.playlistType, 'EVENT', 'the playlist type is EVENT');
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
QUnit.strictEqual(element.playlistType, 'EVENT', 'the playlist type is EVENT');
manifest = '#EXT-X-PLAYLIST-TYPE:VOD\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
strictEqual(element.playlistType, 'VOD', 'the playlist type is VOD');
this.lineStream.push(manifest);
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
QUnit.strictEqual(element.playlistType, 'VOD', 'the playlist type is VOD');
manifest = '#EXT-X-PLAYLIST-TYPE:nonsense\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
strictEqual(element.playlistType, 'nonsense', 'the playlist type is parsed');
});
// #EXT-X-BYTERANGE
test('parses minimal #EXT-X-BYTERANGE tags', function() {
var
manifest = '#EXT-X-BYTERANGE\n',
element;
parseStream.on('data', function(elem) {
this.lineStream.push(manifest);
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
QUnit.strictEqual(element.playlistType, 'nonsense', 'the playlist type is parsed');
});
// #EXT-X-BYTERANGE
QUnit.test('parses minimal #EXT-X-BYTERANGE tags', function() {
let manifest = '#EXT-X-BYTERANGE\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
ok(!('length' in element), 'no length is present');
ok(!('offset' in element), 'no offset is present');
});
test('parses #EXT-X-BYTERANGE with length and offset', function() {
var
manifest = '#EXT-X-BYTERANGE:45\n',
element;
parseStream.on('data', function(elem) {
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
QUnit.ok(!('length' in element), 'no length is present');
QUnit.ok(!('offset' in element), 'no offset is present');
});
QUnit.test('parses #EXT-X-BYTERANGE with length and offset', function() {
let manifest = '#EXT-X-BYTERANGE:45\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
strictEqual(element.length, 45, 'length is parsed');
ok(!('offset' in element), 'no offset is present');
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
QUnit.strictEqual(element.length, 45, 'length is parsed');
QUnit.ok(!('offset' in element), 'no offset is present');
manifest = '#EXT-X-BYTERANGE:108@16\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
strictEqual(element.length, 108, 'length is parsed');
strictEqual(element.offset, 16, 'offset is parsed');
});
// #EXT-X-ALLOW-CACHE
test('parses minimal #EXT-X-ALLOW-CACHE tags', function() {
var
manifest = '#EXT-X-ALLOW-CACHE:\n',
element;
parseStream.on('data', function(elem) {
this.lineStream.push(manifest);
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
QUnit.strictEqual(element.length, 108, 'length is parsed');
QUnit.strictEqual(element.offset, 16, 'offset is parsed');
});
// #EXT-X-ALLOW-CACHE
QUnit.test('parses minimal #EXT-X-ALLOW-CACHE tags', function() {
let manifest = '#EXT-X-ALLOW-CACHE:\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
ok(!('allowed' in element), 'no allowed is present');
});
test('parses valid #EXT-X-ALLOW-CACHE tags', function() {
var
manifest = '#EXT-X-ALLOW-CACHE:YES\n',
element;
parseStream.on('data', function(elem) {
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
QUnit.ok(!('allowed' in element), 'no allowed is present');
});
QUnit.test('parses valid #EXT-X-ALLOW-CACHE tags', function() {
let manifest = '#EXT-X-ALLOW-CACHE:YES\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
ok(element.allowed, 'allowed is parsed');
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
QUnit.ok(element.allowed, 'allowed is parsed');
manifest = '#EXT-X-ALLOW-CACHE:NO\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
ok(!element.allowed, 'allowed is parsed');
});
// #EXT-X-STREAM-INF
test('parses minimal #EXT-X-STREAM-INF tags', function() {
var
manifest = '#EXT-X-STREAM-INF\n',
element;
parseStream.on('data', function(elem) {
this.lineStream.push(manifest);
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
QUnit.ok(!element.allowed, 'allowed is parsed');
});
// #EXT-X-STREAM-INF
QUnit.test('parses minimal #EXT-X-STREAM-INF tags', function() {
let manifest = '#EXT-X-STREAM-INF\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
ok(!('attributes' in element), 'no attributes are present');
});
test('parses #EXT-X-STREAM-INF with common attributes', function() {
var
manifest = '#EXT-X-STREAM-INF:BANDWIDTH=14400\n',
element;
parseStream.on('data', function(elem) {
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
QUnit.ok(!('attributes' in element), 'no attributes are present');
});
QUnit.test('parses #EXT-X-STREAM-INF with common attributes', function() {
let manifest = '#EXT-X-STREAM-INF:BANDWIDTH=14400\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
strictEqual(element.attributes.BANDWIDTH, 14400, 'bandwidth is parsed');
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
QUnit.strictEqual(element.attributes.BANDWIDTH, 14400, 'bandwidth is parsed');
manifest = '#EXT-X-STREAM-INF:PROGRAM-ID=7\n';
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
strictEqual(element.attributes['PROGRAM-ID'], 7, 'program-id is parsed');
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
QUnit.strictEqual(element.attributes['PROGRAM-ID'], 7, 'program-id is parsed');
manifest = '#EXT-X-STREAM-INF:RESOLUTION=396x224\n';
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed');
strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed');
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
QUnit.strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed');
QUnit.strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed');
manifest = '#EXT-X-STREAM-INF:CODECS="avc1.4d400d, mp4a.40.2"\n';
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
strictEqual(element.attributes.CODECS,
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
QUnit.strictEqual(element.attributes.CODECS,
'avc1.4d400d, mp4a.40.2',
'codecs are parsed');
});
test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() {
var
manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n',
element;
parseStream.on('data', function(elem) {
});
QUnit.test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() {
let manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
strictEqual(element.attributes.NUMERIC, '24', 'numeric attributes are parsed');
strictEqual(element.attributes.ALPHA, 'Value', 'alphabetic attributes are parsed');
strictEqual(element.attributes.MIXED, '123abc', 'mixed attributes are parsed');
});
// #EXT-X-ENDLIST
test('parses #EXT-X-ENDLIST tags', function() {
var
manifest = '#EXT-X-ENDLIST\n',
element;
parseStream.on('data', function(elem) {
this.lineStream.push(manifest);
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
QUnit.strictEqual(element.attributes.NUMERIC, '24', 'numeric attributes are parsed');
QUnit.strictEqual(element.attributes.ALPHA,
'Value',
'alphabetic attributes are parsed');
QUnit.strictEqual(element.attributes.MIXED, '123abc', 'mixed attributes are parsed');
});
// #EXT-X-ENDLIST
QUnit.test('parses #EXT-X-ENDLIST tags', function() {
let manifest = '#EXT-X-ENDLIST\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf');
});
QUnit.ok(element, 'an event was triggered');
QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
QUnit.strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf');
});
// #EXT-X-KEY
QUnit.test('parses valid #EXT-X-KEY tags', function() {
let manifest =
'#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n';
let element;
// #EXT-X-KEY
test('parses valid #EXT-X-KEY tags', function() {
var
manifest = '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n',
element;
parseStream.on('data', function(elem) {
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
deepEqual(element, {
QUnit.ok(element, 'an event was triggered');
QUnit.deepEqual(element, {
type: 'tag',
tagType: 'key',
attributes: {
......@@ -543,9 +541,9 @@
}, 'parsed a valid key');
manifest = '#EXT-X-KEY:URI="https://example.com/key#1",METHOD=FutureType-1024\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
deepEqual(element, {
this.lineStream.push(manifest);
QUnit.ok(element, 'an event was triggered');
QUnit.deepEqual(element, {
type: 'tag',
tagType: 'key',
attributes: {
......@@ -555,92 +553,93 @@
}, 'parsed the attribute list independent of order');
manifest = '#EXT-X-KEY:IV=1234567890abcdef1234567890abcdef\n';
lineStream.push(manifest);
ok(element.attributes.IV, 'detected an IV attribute');
deepEqual(element.attributes.IV, new Uint32Array([
this.lineStream.push(manifest);
QUnit.ok(element.attributes.IV, 'detected an IV attribute');
QUnit.deepEqual(element.attributes.IV, new Uint32Array([
0x12345678,
0x90abcdef,
0x12345678,
0x90abcdef
]), 'parsed an IV value');
});
});
QUnit.test('parses minimal #EXT-X-KEY tags', function() {
let manifest = '#EXT-X-KEY:\n';
let element;
test('parses minimal #EXT-X-KEY tags', function() {
var
manifest = '#EXT-X-KEY:\n',
element;
parseStream.on('data', function(elem) {
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(element, 'an event was triggered');
deepEqual(element, {
QUnit.ok(element, 'an event was triggered');
QUnit.deepEqual(element, {
type: 'tag',
tagType: 'key'
}, 'parsed a minimal key tag');
});
});
QUnit.test('parses lightly-broken #EXT-X-KEY tags', function() {
let manifest = '#EXT-X-KEY:URI=\'https://example.com/single-quote\',METHOD=AES-128\n';
let element;
test('parses lightly-broken #EXT-X-KEY tags', function() {
var
manifest = '#EXT-X-KEY:URI=\'https://example.com/single-quote\',METHOD=AES-128\n',
element;
parseStream.on('data', function(elem) {
this.parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
strictEqual(element.attributes.URI,
QUnit.strictEqual(element.attributes.URI,
'https://example.com/single-quote',
'parsed a single-quoted uri');
element = null;
manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n';
lineStream.push(manifest);
strictEqual(element.tagType, 'key', 'parsed the tag type');
strictEqual(element.attributes.URI,
this.lineStream.push(manifest);
QUnit.strictEqual(element.tagType, 'key', 'parsed the tag type');
QUnit.strictEqual(element.attributes.URI,
'https://example.com/key',
'inferred a colon after the tag type');
element = null;
manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n';
lineStream.push(manifest);
strictEqual(element.attributes.URI,
this.lineStream.push(manifest);
QUnit.strictEqual(element.attributes.URI,
'https://example.com/key',
'trims and removes quotes around the URI');
});
});
QUnit.test('ignores empty lines', function() {
let manifest = '\n';
let event = false;
test('ignores empty lines', function() {
var
manifest = '\n',
event = false;
parseStream.on('data', function() {
this.parseStream.on('data', function() {
event = true;
});
lineStream.push(manifest);
this.lineStream.push(manifest);
ok(!event, 'no event is triggered');
});
QUnit.ok(!event, 'no event is triggered');
});
QUnit.module('m3u8 parser');
QUnit.module('m3u8 parser');
test('can be constructed', function() {
notStrictEqual(new Parser(), undefined, 'parser is defined');
});
QUnit.test('can be constructed', function() {
QUnit.notStrictEqual(typeof new Parser(), 'undefined', 'parser is defined');
});
QUnit.module('m3u8s');
QUnit.module('m3u8s');
test('parses static manifests as expected', function() {
var key;
for (key in window.manifests) {
if (window.expected[key]) {
parser = new Parser();
parser.push(window.manifests[key]);
deepEqual(parser.manifest,
window.expected[key],
key + '.m3u8 was parsed correctly');
QUnit.test('parses static manifests as expected', function() {
let key;
for (key in testDataManifests) {
if (testDataExpected[key]) {
let parser = new Parser();
parser.push(testDataManifests[key]);
QUnit.deepEqual(parser.manifest,
testDataExpected[key],
key + '.m3u8 was parsed correctly'
);
}
}
});
})(window, window.console);
});
......
import manifests from './test-manifests';
import expected from './test-expected';
window.manifests = manifests;
window.expected = expected;