UNPKG

@eyevinn/m3u8

Version:

streaming m3u8 parser for Apple's HTTP Live Streaming protocol

352 lines (306 loc) 15 kB
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; }