@eyevinn/m3u8
Version:
streaming m3u8 parser for Apple's HTTP Live Streaming protocol
352 lines (306 loc) • 15 kB
JavaScript
var m3u8 = require('../parser'),
sinon = require('sinon'),
should = require('should');
describe('parser', function() {
it('should error if not valid M3U first line', function(done) {
var parser = getParser();
parser.on('error', function(error) {
error.message.should.containEql('Non-valid M3U file. First line: ');
done();
});
parser.write('NOT VALID\n');
});
describe('#parseLine', function() {
it('should call known tags', function() {
var parser = getParser();
var mock = sinon.mock(parser);
mock.expects('EXT-X-MEDIA').once().returns(45);
parser.parseLine('#EXT-X-MEDIA:GROUP-ID="600k", LANGUAGE="eng"');
mock.verify();
});
it('should set data on m3u on unknown tags', function() {
var parser = getParser();
parser.parseLine('#THIS-IS-A-TAG:some value');
parser.m3u.get('THIS-IS-A-TAG').should.eql('some value');
});
it('should split on first colon only', function() {
var parser = getParser();
parser.parseLine('#THIS-IS-A-TAG:http://www.ted.com');
parser.m3u.get('THIS-IS-A-TAG').should.eql('http://www.ted.com');
});
});
describe('#addItem', function() {
it('should make currentItem the added item', function() {
var parser = getParser();
var item = new m3u8.M3U.PlaylistItem;
parser.addItem(item);
parser.currentItem.should.eql(item);
});
});
describe('#EXTINF', function() {
it('should create a new Playlist item', function() {
var parser = getParser();
parser.EXTINF('4.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('duration').should.eql(4.5);
parser.currentItem.get('title').should.eql('some title');
});
});
describe('#EXT-X-BYTERANGE', function() {
it('should set byteRange on currentItem', function() {
var parser = getParser();
parser.EXTINF('4.5,');
parser['EXT-X-BYTERANGE']('45@90');
parser.currentItem.get('byteRange').should.eql('45@90');
});
});
describe('#EXT-X-DISCONTINUITY', function() {
it('should indicate discontinuation on subsequent playlist item', function() {
var parser = getParser();
parser['EXT-X-DISCONTINUITY']();
parser.EXTINF('4.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('duration').should.eql(4.5);
parser.currentItem.get('title').should.eql('some title');
parser.currentItem.get('discontinuity').should.eql(true);
});
});
describe('#EXT-X-CUE-OUT', function() {
it('should indicate cue out with a duration', function() {
var parser = getParser();
parser['EXT-X-CUE-OUT']('DURATION=30');
parser.EXTINF('4.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('cueout').should.eql(30);
parser.EXTINF('3.0,another title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
should.not.exist(parser.currentItem.get('cueout'));
});
it('should indicate cue out with duration 0 ', function() {
var parser = getParser();
parser['EXT-X-CUE-OUT']('');
parser.EXTINF('4.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('cueout').should.eql(0);
parser.EXTINF('4.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
should.not.exist(parser.currentItem.get('cueout'));
});
it('should indicate cue out duration without duration attr', function() {
var parser = getParser();
parser['EXT-X-CUE-OUT']('30');
parser.EXTINF('4.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('cueout').should.eql(30);
});
});
describe('#EXT-X-CUE-OUT-CONT', function() {
it('should indicate cue out cont with progress if present', function() {
var parser = getParser();
parser['EXT-X-CUE-OUT']('30');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('cueout').should.eql(30);
parser['EXT-X-CUE-OUT-CONT']('10/30');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('cont-offset').should.eql(10);
parser.currentItem.get('cont-dur').should.eql(30);
parser['EXT-X-CUE-OUT-CONT']('14.5/30');
parser.EXTINF('4.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('cont-offset').should.eql(14.5);
parser.currentItem.get('cont-dur').should.eql(30);
});
});
describe('#EXT-X-CUE-IN', function() {
it('should indicate cue in is true if present', function() {
var parser = getParser();
parser.EXTINF('4.5,some title');
parser['EXT-X-CUE-IN']();
parser.currentItem.constructor.name.should.eql('PlaylistItem');
should.not.exist(parser.currentItem.get('cuein'));
parser.EXTINF('3.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('cuein').should.eql(true);
});
});
describe('#EXT-X-DATERANGE', function() {
it('should handle a set of attributes / value pairs for a range of time', function() {
var parser = getParser();
parser['EXT-X-DATERANGE']('START-DATE="2020-11-21T10:00:00.000Z",X-CUSTOM="MY CUSTOM TAG"');
parser.EXTINF('4.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('daterange')['START-DATE'].should.eql('2020-11-21T10:00:00.000Z');
parser.currentItem.toString().should.eql('#EXT-X-DATERANGE:START-DATE="2020-11-21T10:00:00.000Z",X-CUSTOM="MY CUSTOM TAG"\n#EXTINF:4.5000,some title');
});
});
describe('#EXT-X-KEY', function() {
it('should indicate key with method, and other attributes if present', function() {
var parser = getParser();
parser['EXT-X-KEY']('METHOD=AES-128,URI="https://mock.mock.com/key",IV=0x2b4420d1f5d6dff30dde825ae7012450,KEYFORMAT="identity",KEYFORMATVERSIONS="5"');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('key-method').should.eql('AES-128');
parser.currentItem.get('key-uri').should.eql('https://mock.mock.com/key');
parser.currentItem.get('key-iv').should.eql('0x2b4420d1f5d6dff30dde825ae7012450');
parser.currentItem.get('key-keyformat').should.eql('identity');
parser.currentItem.get('key-keyformatversions').should.eql('5');
});
it('should indicate key with no attributes if method is NONE', function() {
var parser = getParser();
parser['EXT-X-KEY']('METHOD=NONE,URI="https://mock.mock.com/key",IV=0x2b4420d1f5d6dff30dde825ae7012450,KEYFORMAT="identity",KEYFORMATVERSIONS="5"');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('key-method').should.eql('NONE');
(parser.currentItem.get('key-uri') === undefined).should.be.true();
(parser.currentItem.get('key-iv') === undefined).should.be.true();
(parser.currentItem.get('key-keyformat') === undefined).should.be.true();
(parser.currentItem.get('key-keyformatversions') === undefined).should.be.true();
});
it('should support two or more EXT-X-KEY tags with different KEYFORMAT attributes', function() {
var parser = getParser();
parser['EXT-X-KEY']('METHOD=SAMPLE-AES,URI="https://mock.mock.com/key",IV=0x2b4420d1f5d6dff30dde825ae7012450,KEYFORMAT="identity1",KEYFORMATVERSIONS="5"');
parser['EXT-X-KEY']('METHOD=SAMPLE-AES-CTR,URI="https://mock.mock.com/key",IV=0x2b4420d1f5d6dff30dde825ae7012450,KEYFORMAT="identity2",KEYFORMATVERSIONS="5"');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
const keys = parser.currentItem.get('keys');
keys['identity1']['method'].should.eql('SAMPLE-AES');
keys['identity1']['uri'].should.eql('"https://mock.mock.com/key"');
keys['identity1']['iv'].should.eql('0x2b4420d1f5d6dff30dde825ae7012450');
keys['identity1']['keyFormat'].should.eql('"identity1"');
keys['identity1']['keyFormatVersions'].should.eql('"5"');
keys['identity2']['method'].should.eql('SAMPLE-AES-CTR');
keys['identity2']['uri'].should.eql('"https://mock.mock.com/key"');
keys['identity2']['iv'].should.eql('0x2b4420d1f5d6dff30dde825ae7012450');
keys['identity2']['keyFormat'].should.eql('"identity2"');
keys['identity2']['keyFormatVersions'].should.eql('"5"');
});
});
describe('#EXT-X-MAP', function() {
it('should indicate map with attributes if present', function() {
var parser = getParser();
parser['EXT-X-MAP']('URI="https://mock.mock.com/map/init.mp4",BYTERANGE="500@0"');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('map-uri').should.eql('https://mock.mock.com/map/init.mp4');
parser.currentItem.get('map-byterange').should.eql('500@0');
});
it('should indicate map with only uri attribute if present', function() {
var parser = getParser();
parser['EXT-X-MAP']('URI="https://mock.mock.com/map/init.mp4"');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('map-uri').should.eql('https://mock.mock.com/map/init.mp4');
(parser.currentItem.get('map-byterange') === undefined).should.be.true();
});
});
describe('#EXT-X-START', function () {
it('should indicate start with attributes if present', function () {
var parser = getParser();
parser['EXT-X-START']('TIME-OFFSET=15,PRECISE=YES');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('start-timeoffset').should.eql(15);
parser.currentItem.get('start-precise').should.eql(true);
});
it('should indicate start with only timeoffset attribute if present', function () {
var parser = getParser();
parser['EXT-X-START']('TIME-OFFSET=15');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('start-timeoffset').should.eql(15);
(parser.currentItem.get('start-precise') === undefined).should.be.true();
});
it('should indicate start with only timeoffset attribute (decimal) if present', function () {
var parser = getParser();
parser['EXT-X-START']('TIME-OFFSET=15.625');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('start-timeoffset').should.eql(15.625);
(parser.currentItem.get('start-precise') === undefined).should.be.true();
});
it('should indicate start with only timeoffset attribute (negative decimal) if present', function () {
var parser = getParser();
parser['EXT-X-START']('TIME-OFFSET=-15.500');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('start-timeoffset').should.eql(-15.5);
(parser.currentItem.get('start-precise') === undefined).should.be.true();
});
it('should indicate start with only timeoffset attribute (negative integer) if present', function () {
var parser = getParser();
parser['EXT-X-START']('TIME-OFFSET=-15');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('start-timeoffset').should.eql(-15);
(parser.currentItem.get('start-precise') === undefined).should.be.true();
});
it('should indicate start with attributes (PRECISE=NO) if present', function () {
var parser = getParser();
parser['EXT-X-START']('TIME-OFFSET=15,PRECISE=NO');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('start-timeoffset').should.eql(15);
parser.currentItem.get('start-precise').should.eql(false);
});
});
describe('#EXT-X-PROGRAM-DATE-TIME', function() {
it('should indicate program-date-time with date if present', function() {
var parser = getParser();
parser['EXT-X-PROGRAM-DATE-TIME']('2021-11-17T23:27:29.000+00:00');
parser.EXTINF('10,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('date').should.eql('2021-11-17T23:27:29.000+00:00');
});
});
describe('#EXT-X-STREAM-INF', function() {
it('should create a new Stream item', function() {
var parser = getParser();
parser['EXT-X-STREAM-INF']('NAME="I am a stream!"');
parser.currentItem.constructor.name.should.eql('StreamItem');
parser.currentItem.get('name').should.eql('I am a stream!');
});
});
describe('#EXT-X-I-FRAME-STREAM-INF', function() {
it('should create a new Iframe Stream item', function() {
var parser = getParser();
parser['EXT-X-I-FRAME-STREAM-INF']('NAME="I am an iframe stream!"');
parser.currentItem.constructor.name.should.eql('IframeStreamItem');
parser.currentItem.get('name').should.eql('I am an iframe stream!');
});
});
describe('#EXT-X-MEDIA-INF', function() {
it('should create a new Media item', function() {
var parser = getParser();
parser['EXT-X-MEDIA']('NAME="I am a media item!"');
parser.currentItem.constructor.name.should.eql('MediaItem');
parser.currentItem.get('name').should.eql('I am a media item!');
});
});
describe('#parseAttributes', function() {
it('should return an array of key-values', function() {
var parser = getParser();
var keyValues = parser.parseAttributes(
'KEY="I, am a value",RESOLUTION=640x360,FORCED=NO'
);
keyValues[0].key.should.eql('KEY');
keyValues[0].value.should.eql('"I, am a value"');
keyValues[2].value.should.eql('NO');
});
});
describe('#EXT-X-ASSET', function() {
it('should return asset metadata', function() {
var parser = getParser();
parser['EXT-X-CUE-OUT']('30');
parser['EXT-X-ASSET']('CAID=0x0000000020FB6501');
parser.EXTINF('4.5,some title');
parser.currentItem.constructor.name.should.eql('PlaylistItem');
parser.currentItem.get('assetdata').should.eql('CAID=0x0000000020FB6501');
});
});
});
function getParser() {
var parser = m3u8.createStream();
return parser;
}