parser.test.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. import QUnit from 'qunit';
  2. import testDataExpected from 'data-files!expecteds';
  3. import testDataManifests from 'data-files!manifests';
  4. import {Parser} from '../src';
  5. QUnit.module('m3u8s', function(hooks) {
  6. hooks.beforeEach(function() {
  7. this.parser = new Parser();
  8. QUnit.dump.maxDepth = 8;
  9. });
  10. QUnit.module('general');
  11. QUnit.test('can be constructed', function(assert) {
  12. assert.notStrictEqual(this.parser, 'undefined', 'parser is defined');
  13. });
  14. QUnit.test('can set custom parsers', function(assert) {
  15. const manifest = [
  16. '#EXTM3U',
  17. '#EXT-X-VERSION:3',
  18. '#EXT-X-TARGETDURATION:10',
  19. '#EXT-X-MEDIA-SEQUENCE:0',
  20. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  21. '#VOD-STARTTIMESTAMP:1501533337573',
  22. '#VOD-TOTALDELETEDDURATION:0.0',
  23. '#VOD-FRAMERATE:29.97',
  24. ''
  25. ].join('\n');
  26. this.parser.addParser({
  27. expression: /^#VOD-STARTTIMESTAMP/,
  28. customType: 'startTimestamp'
  29. });
  30. this.parser.addParser({
  31. expression: /^#VOD-TOTALDELETEDDURATION/,
  32. customType: 'totalDeleteDuration'
  33. });
  34. this.parser.addParser({
  35. expression: /^#VOD-FRAMERATE/,
  36. customType: 'framerate',
  37. dataParser: (line) => (line.split(':')[1])
  38. });
  39. this.parser.push(manifest);
  40. this.parser.end();
  41. assert.strictEqual(
  42. this.parser.manifest.custom.startTimestamp,
  43. '#VOD-STARTTIMESTAMP:1501533337573',
  44. 'sets custom timestamp line'
  45. );
  46. assert.strictEqual(
  47. this.parser.manifest.custom.totalDeleteDuration,
  48. '#VOD-TOTALDELETEDDURATION:0.0',
  49. 'sets custom delete duration'
  50. );
  51. assert.strictEqual(this.parser.manifest.custom.framerate, '29.97', 'sets framerate');
  52. });
  53. QUnit.test('segment level custom data', function(assert) {
  54. const manifest = [
  55. '#EXTM3U',
  56. '#VOD-TIMING:1511816599485',
  57. '#COMMENT',
  58. '#EXTINF:8.0,',
  59. 'ex1.ts',
  60. '#VOD-TIMING',
  61. '#EXTINF:8.0,',
  62. 'ex2.ts',
  63. '#VOD-TIMING:1511816615485',
  64. '#EXT-UNKNOWN',
  65. '#EXTINF:8.0,',
  66. 'ex3.ts',
  67. '#VOD-TIMING:1511816623485',
  68. '#EXTINF:8.0,',
  69. 'ex3.ts',
  70. '#EXT-X-ENDLIST'
  71. ].join('\n');
  72. this.parser.addParser({
  73. expression: /^#VOD-TIMING/,
  74. customType: 'vodTiming',
  75. segment: true
  76. });
  77. this.parser.push(manifest);
  78. this.parser.end();
  79. assert.equal(
  80. this.parser.manifest.segments[0].custom.vodTiming,
  81. '#VOD-TIMING:1511816599485',
  82. 'parser attached segment level custom data'
  83. );
  84. assert.equal(
  85. this.parser.manifest.segments[1].custom.vodTiming,
  86. '#VOD-TIMING',
  87. 'parser got segment level custom data without :'
  88. );
  89. });
  90. QUnit.test('attaches cue-out data to segment', function(assert) {
  91. const manifest = [
  92. '#EXTM3U',
  93. '#EXTINF:5,',
  94. '#COMMENT',
  95. 'ex1.ts',
  96. '#EXT-X-CUE-OUT:10',
  97. '#EXTINF:5,',
  98. 'ex2.ts',
  99. '#EXT-UKNOWN-TAG',
  100. '#EXTINF:5,',
  101. 'ex3.ts',
  102. '#EXT-X-CUE-OUT:',
  103. '#EXTINF:5,',
  104. 'ex3.ts',
  105. '#EXT-X-ENDLIST'
  106. ].join('\n');
  107. this.parser.push(manifest);
  108. this.parser.end();
  109. assert.equal(this.parser.manifest.segments[1].cueOut, '10', 'parser attached cue out tag');
  110. assert.equal(this.parser.manifest.segments[3].cueOut, '', 'cue out without data');
  111. });
  112. QUnit.test('attaches cue-out-cont data to segment', function(assert) {
  113. const manifest = [
  114. '#EXTM3U',
  115. '#EXTINF:5,',
  116. '#COMMENT',
  117. 'ex1.ts',
  118. '#EXT-X-CUE-OUT-CONT:10/60',
  119. '#EXTINF:5,',
  120. 'ex2.ts',
  121. '#EXT-UKNOWN-TAG',
  122. '#EXTINF:5,',
  123. 'ex3.ts',
  124. '#EXT-X-CUE-OUT-CONT:',
  125. '#EXTINF:5,',
  126. 'ex3.ts',
  127. '#EXT-X-ENDLIST'
  128. ].join('\n');
  129. this.parser.push(manifest);
  130. this.parser.end();
  131. assert.equal(
  132. this.parser.manifest.segments[1].cueOutCont, '10/60',
  133. 'parser attached cue out cont tag'
  134. );
  135. assert.equal(this.parser.manifest.segments[3].cueOutCont, '', 'cue out cont without data');
  136. });
  137. QUnit.test('attaches cue-in data to segment', function(assert) {
  138. const manifest = [
  139. '#EXTM3U',
  140. '#EXTINF:5,',
  141. '#COMMENT',
  142. 'ex1.ts',
  143. '#EXT-X-CUE-IN:',
  144. '#EXTINF:5,',
  145. 'ex2.ts',
  146. '#EXT-X-CUE-IN:15',
  147. '#EXT-UKNOWN-TAG',
  148. '#EXTINF:5,',
  149. 'ex3.ts',
  150. '#EXTINF:5,',
  151. 'ex3.ts',
  152. '#EXT-X-ENDLIST'
  153. ].join('\n');
  154. this.parser.push(manifest);
  155. this.parser.end();
  156. assert.equal(this.parser.manifest.segments[1].cueIn, '', 'parser attached cue in tag');
  157. assert.equal(this.parser.manifest.segments[2].cueIn, '15', 'cue in with data');
  158. });
  159. QUnit.test('parses characteristics attribute', function(assert) {
  160. const manifest = [
  161. '#EXTM3U',
  162. '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",CHARACTERISTICS="char",NAME="test"',
  163. '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2, avc1.4d400d",SUBTITLES="subs"',
  164. 'index.m3u8'
  165. ].join('\n');
  166. this.parser.push(manifest);
  167. this.parser.end();
  168. assert.equal(
  169. this.parser.manifest.mediaGroups.SUBTITLES.subs.test.characteristics,
  170. 'char',
  171. 'parsed CHARACTERISTICS attribute'
  172. );
  173. });
  174. QUnit.test('parses FORCED attribute', function(assert) {
  175. const manifest = [
  176. '#EXTM3U',
  177. '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",CHARACTERISTICS="char",NAME="test",FORCED=YES',
  178. '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2, avc1.4d400d",SUBTITLES="subs"',
  179. 'index.m3u8'
  180. ].join('\n');
  181. this.parser.push(manifest);
  182. this.parser.end();
  183. assert.ok(
  184. this.parser.manifest.mediaGroups.SUBTITLES.subs.test.forced,
  185. 'parsed FORCED attribute'
  186. );
  187. });
  188. QUnit.test('parses Widevine #EXT-X-KEY attributes and attaches to manifest', function(assert) {
  189. const manifest = [
  190. '#EXTM3U',
  191. '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,' +
  192. 'URI="data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
  193. 'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=0x800AACAA522958AE888062B5695DB6BF,' +
  194. 'KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"',
  195. '#EXTINF:5,',
  196. 'ex1.ts',
  197. '#EXT-X-ENDLIST'
  198. ].join('\n');
  199. this.parser.push(manifest);
  200. this.parser.end();
  201. assert.ok(this.parser.manifest.contentProtection, 'contentProtection property added');
  202. assert.equal(
  203. this.parser.manifest.contentProtection['com.widevine.alpha'].attributes.schemeIdUri,
  204. 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
  205. 'schemeIdUri set correctly'
  206. );
  207. assert.equal(
  208. this.parser.manifest.contentProtection['com.widevine.alpha'].attributes.keyId,
  209. '800AACAA522958AE888062B5695DB6BF',
  210. 'keyId set correctly'
  211. );
  212. assert.equal(
  213. this.parser.manifest.contentProtection['com.widevine.alpha'].pssh.byteLength,
  214. 62,
  215. 'base64 URI decoded to TypedArray'
  216. );
  217. });
  218. QUnit.test('Widevine #EXT-X-KEY attributes not attached to manifest if METHOD is invalid', function(assert) {
  219. const manifest = [
  220. '#EXTM3U',
  221. '#EXT-X-KEY:METHOD=NONE,' +
  222. 'URI="data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
  223. 'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=0x800AACAA522958AE888062B5695DB6BF,' +
  224. 'KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"',
  225. '#EXTINF:5,',
  226. 'ex1.ts',
  227. '#EXT-X-ENDLIST'
  228. ].join('\n');
  229. this.parser.push(manifest);
  230. this.parser.end();
  231. assert.notOk(this.parser.manifest.contentProtection, 'contentProtection not added');
  232. });
  233. QUnit.test('Widevine #EXT-X-KEY attributes not attached to manifest if URI is invalid', function(assert) {
  234. const manifest = [
  235. '#EXTM3U',
  236. '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,' +
  237. 'URI="AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
  238. 'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=0x800AACAA522958AE888062B5695DB6BF,' +
  239. 'KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"',
  240. '#EXTINF:5,',
  241. 'ex1.ts',
  242. '#EXT-X-ENDLIST'
  243. ].join('\n');
  244. this.parser.push(manifest);
  245. this.parser.end();
  246. assert.notOk(this.parser.manifest.contentProtection, 'contentProtection not added');
  247. });
  248. QUnit.test('Widevine #EXT-X-KEY attributes not attached to manifest if KEYID is invalid', function(assert) {
  249. const manifest = [
  250. '#EXTM3U',
  251. '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,' +
  252. 'URI="data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
  253. 'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=800AACAA522958AE888062B5695DB6BF,' +
  254. 'KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"',
  255. '#EXTINF:5,',
  256. 'ex1.ts',
  257. '#EXT-X-ENDLIST'
  258. ].join('\n');
  259. this.parser.push(manifest);
  260. this.parser.end();
  261. assert.notOk(this.parser.manifest.contentProtection, 'contentProtection not added');
  262. });
  263. QUnit.test('Widevine #EXT-X-KEY attributes not attached to manifest if KEYFORMAT is not Widevine UUID', function(assert) {
  264. const manifest = [
  265. '#EXTM3U',
  266. '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,' +
  267. 'URI="data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnN' +
  268. 'oYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=",KEYID=0x800AACAA522958AE888062B5695DB6BF,' +
  269. 'KEYFORMATVERSIONS="1",KEYFORMAT="invalid-keyformat"',
  270. '#EXTINF:5,',
  271. 'ex1.ts',
  272. '#EXT-X-ENDLIST'
  273. ].join('\n');
  274. this.parser.push(manifest);
  275. this.parser.end();
  276. assert.notOk(this.parser.manifest.contentProtection, 'contentProtection not added');
  277. });
  278. QUnit.test('byterange offset defaults to next byte', function(assert) {
  279. const manifest = [
  280. '#EXTM3U',
  281. '#EXTINF:5,',
  282. '#EXT-X-BYTERANGE:10@5',
  283. 'segment.ts',
  284. '#EXTINF:5,',
  285. '#EXT-X-BYTERANGE:20',
  286. 'segment.ts',
  287. '#EXTINF:5,',
  288. '#EXT-X-BYTERANGE:30',
  289. 'segment.ts',
  290. '#EXTINF:5,',
  291. 'segment2.ts',
  292. '#EXT-X-BYTERANGE:15@100',
  293. 'segment.ts',
  294. '#EXT-X-BYTERANGE:17',
  295. 'segment.ts',
  296. '#EXT-X-ENDLIST'
  297. ].join('\n');
  298. this.parser.push(manifest);
  299. this.parser.end();
  300. assert.deepEqual(
  301. this.parser.manifest.segments[0].byterange,
  302. { length: 10, offset: 5 },
  303. 'first segment has correct byterange'
  304. );
  305. assert.deepEqual(
  306. this.parser.manifest.segments[1].byterange,
  307. { length: 20, offset: 15 },
  308. 'second segment has correct byterange'
  309. );
  310. assert.deepEqual(
  311. this.parser.manifest.segments[2].byterange,
  312. { length: 30, offset: 35 },
  313. 'third segment has correct byterange'
  314. );
  315. assert.notOk(this.parser.manifest.segments[3].byterange, 'fourth segment has no byterange');
  316. assert.deepEqual(
  317. this.parser.manifest.segments[4].byterange,
  318. { length: 15, offset: 100 },
  319. 'fifth segment has correct byterange'
  320. );
  321. // not tested is a segment with no offset coming after a segment that isn't a sub range,
  322. // as the spec requires that a byterange without an offset must follow a segment that
  323. // is a sub range of the same media resource
  324. assert.deepEqual(
  325. this.parser.manifest.segments[5].byterange,
  326. { length: 17, offset: 115 },
  327. 'sixth segment has correct byterange'
  328. );
  329. });
  330. QUnit.module('warn/info', {
  331. beforeEach() {
  332. this.warnings = [];
  333. this.infos = [];
  334. this.parser.on('warn', (warn) => this.warnings.push(warn.message));
  335. this.parser.on('info', (info) => this.infos.push(info.message));
  336. }
  337. });
  338. QUnit.test('warn when #EXT-X-TARGETDURATION is invalid', function(assert) {
  339. this.parser.push([
  340. '#EXT-X-VERSION:3',
  341. '#EXT-X-MEDIA-SEQUENCE:0',
  342. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  343. '#EXT-X-TARGETDURATION:foo',
  344. '#EXTINF:10,',
  345. 'media-00001.ts',
  346. '#EXT-X-ENDLIST'
  347. ].join('\n'));
  348. this.parser.end();
  349. const warnings = [
  350. 'ignoring invalid target duration: undefined'
  351. ];
  352. assert.deepEqual(
  353. this.warnings,
  354. warnings,
  355. 'warnings as expected'
  356. );
  357. assert.deepEqual(
  358. this.infos,
  359. [],
  360. 'info as expected'
  361. );
  362. });
  363. QUnit.test('warns when #EXT-X-START missing TIME-OFFSET attribute', function(assert) {
  364. this.parser.push([
  365. '#EXT-X-VERSION:3',
  366. '#EXT-X-MEDIA-SEQUENCE:0',
  367. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  368. '#EXT-X-TARGETDURATION:10',
  369. '#EXT-X-START:PRECISE=YES',
  370. '#EXTINF:10,',
  371. 'media-00001.ts',
  372. '#EXT-X-ENDLIST'
  373. ].join('\n'));
  374. this.parser.end();
  375. assert.deepEqual(
  376. this.warnings,
  377. ['ignoring start declaration without appropriate attribute list'],
  378. 'warnings as expected'
  379. );
  380. assert.deepEqual(
  381. this.infos,
  382. [],
  383. 'info as expected'
  384. );
  385. assert.strictEqual(typeof this.parser.manifest.start, 'undefined', 'does not parse start');
  386. });
  387. QUnit.test('warning when #EXT-X-SKIP missing SKIPPED-SEGMENTS attribute', function(assert) {
  388. this.parser.push([
  389. '#EXT-X-VERSION:3',
  390. '#EXT-X-MEDIA-SEQUENCE:0',
  391. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  392. '#EXT-X-TARGETDURATION:10',
  393. '#EXT-X-SKIP:foo=bar',
  394. '#EXTINF:10,',
  395. 'media-00001.ts',
  396. '#EXT-X-ENDLIST'
  397. ].join('\n'));
  398. this.parser.end();
  399. assert.deepEqual(
  400. this.warnings,
  401. ['#EXT-X-SKIP lacks required attribute(s): SKIPPED-SEGMENTS'],
  402. 'warnings as expected'
  403. );
  404. assert.deepEqual(
  405. this.infos,
  406. [],
  407. 'info as expected'
  408. );
  409. });
  410. QUnit.test('warns when #EXT-X-PART missing URI/DURATION attributes', function(assert) {
  411. this.parser.push([
  412. '#EXT-X-VERSION:3',
  413. '#EXT-X-MEDIA-SEQUENCE:0',
  414. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  415. '#EXT-X-TARGETDURATION:10',
  416. '#EXT-X-PART:DURATION=1',
  417. '#EXT-X-PART:URI=2',
  418. '#EXT-X-PART:foo=bar',
  419. '#EXTINF:10,',
  420. 'media-00001.ts',
  421. '#EXT-X-ENDLIST'
  422. ].join('\n'));
  423. this.parser.end();
  424. const warnings = [
  425. '#EXT-X-PART #0 for segment #0 lacks required attribute(s): URI',
  426. '#EXT-X-PART #1 for segment #0 lacks required attribute(s): DURATION',
  427. '#EXT-X-PART #2 for segment #0 lacks required attribute(s): URI, DURATION'
  428. ];
  429. assert.deepEqual(
  430. this.warnings,
  431. warnings,
  432. 'warnings as expected'
  433. );
  434. assert.deepEqual(
  435. this.infos,
  436. [],
  437. 'info as expected'
  438. );
  439. });
  440. QUnit.test('warns when #EXT-X-PRELOAD-HINT missing TYPE/URI attribute', function(assert) {
  441. this.parser.push([
  442. '#EXT-X-VERSION:3',
  443. '#EXT-X-MEDIA-SEQUENCE:0',
  444. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  445. '#EXT-X-TARGETDURATION:10',
  446. '#EXT-X-PRELOAD-HINT:TYPE=foo',
  447. '#EXT-X-PRELOAD-HINT:URI=foo',
  448. '#EXT-X-PRELOAD-HINT:foo=bar',
  449. '#EXTINF:10,',
  450. 'media-00001.ts',
  451. '#EXT-X-ENDLIST'
  452. ].join('\n'));
  453. this.parser.end();
  454. const warnings = [
  455. '#EXT-X-PRELOAD-HINT #0 for segment #0 lacks required attribute(s): URI',
  456. '#EXT-X-PRELOAD-HINT #1 for segment #0 lacks required attribute(s): TYPE',
  457. '#EXT-X-PRELOAD-HINT #2 for segment #0 lacks required attribute(s): TYPE, URI'
  458. ];
  459. assert.deepEqual(
  460. this.warnings,
  461. warnings,
  462. 'warnings as expected'
  463. );
  464. assert.deepEqual(
  465. this.infos,
  466. [],
  467. 'info as expected'
  468. );
  469. });
  470. QUnit.test('warns when we get #EXT-X-PRELOAD-HINT with the same TYPE', function(assert) {
  471. this.parser.push([
  472. '#EXT-X-VERSION:3',
  473. '#EXT-X-MEDIA-SEQUENCE:0',
  474. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  475. '#EXT-X-TARGETDURATION:10',
  476. '#EXT-X-PRELOAD-HINT:TYPE=foo,URI=foo1',
  477. '#EXT-X-PRELOAD-HINT:TYPE=foo,URI=foo2',
  478. '#EXTINF:10,',
  479. 'media-00001.ts',
  480. '#EXT-X-ENDLIST'
  481. ].join('\n'));
  482. this.parser.end();
  483. const warnings = [
  484. '#EXT-X-PRELOAD-HINT #1 for segment #0 has the same TYPE foo as preload hint #0'
  485. ];
  486. assert.deepEqual(
  487. this.warnings,
  488. warnings,
  489. 'warnings as expected'
  490. );
  491. assert.deepEqual(
  492. this.infos,
  493. [],
  494. 'info as expected'
  495. );
  496. });
  497. QUnit.test('warn when #EXT-X-RENDITION-REPORT missing LAST-MSN/URI attribute', function(assert) {
  498. this.parser.push([
  499. '#EXT-X-VERSION:3',
  500. '#EXT-X-MEDIA-SEQUENCE:0',
  501. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  502. '#EXT-X-TARGETDURATION:10',
  503. '#EXT-X-RENDITION-REPORT:URI=foo',
  504. '#EXT-X-RENDITION-REPORT:LAST-MSN=2',
  505. '#EXT-X-RENDITION-REPORT:foo=bar',
  506. '#EXTINF:10,',
  507. 'media-00001.ts',
  508. '#EXT-X-ENDLIST'
  509. ].join('\n'));
  510. this.parser.end();
  511. const warnings = [
  512. '#EXT-X-RENDITION-REPORT #0 lacks required attribute(s): LAST-MSN',
  513. '#EXT-X-RENDITION-REPORT #1 lacks required attribute(s): URI',
  514. '#EXT-X-RENDITION-REPORT #2 lacks required attribute(s): LAST-MSN, URI'
  515. ];
  516. assert.deepEqual(
  517. this.warnings,
  518. warnings,
  519. 'warnings as expected'
  520. );
  521. assert.deepEqual(
  522. this.infos,
  523. [],
  524. 'info as expected'
  525. );
  526. });
  527. QUnit.test('warns when #EXT-X-RENDITION-REPORT missing LAST-PART attribute with parts', function(assert) {
  528. this.parser.push([
  529. '#EXT-X-VERSION:3',
  530. '#EXT-X-MEDIA-SEQUENCE:0',
  531. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  532. '#EXT-X-TARGETDURATION:10',
  533. '#EXT-X-RENDITION-REPORT:URI=foo,LAST-MSN=4',
  534. '#EXT-X-PART:URI=foo,DURATION=10',
  535. '#EXT-X-RENDITION-REPORT:URI=foo,LAST-MSN=4',
  536. '#EXTINF:10,',
  537. 'media-00001.ts',
  538. '#EXT-X-ENDLIST'
  539. ].join('\n'));
  540. this.parser.end();
  541. const warnings = [
  542. '#EXT-X-RENDITION-REPORT #0 lacks required attribute(s): LAST-PART',
  543. '#EXT-X-RENDITION-REPORT #1 lacks required attribute(s): LAST-PART'
  544. ];
  545. assert.deepEqual(
  546. this.warnings,
  547. warnings,
  548. 'warnings as expected'
  549. );
  550. assert.deepEqual(
  551. this.infos,
  552. [],
  553. 'info as expected'
  554. );
  555. });
  556. QUnit.test('warns when #EXT-X-PART-INF missing PART-TARGET attribute', function(assert) {
  557. this.parser.push([
  558. '#EXT-X-VERSION:3',
  559. '#EXT-X-MEDIA-SEQUENCE:0',
  560. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  561. '#EXT-X-TARGETDURATION:10',
  562. '#EXT-X-PART-INF:URI=foo',
  563. '#EXTINF:10,',
  564. 'media-00001.ts',
  565. '#EXT-X-ENDLIST'
  566. ].join('\n'));
  567. this.parser.end();
  568. const warnings = [
  569. '#EXT-X-PART-INF lacks required attribute(s): PART-TARGET'
  570. ];
  571. assert.deepEqual(
  572. this.warnings,
  573. warnings,
  574. 'warnings as expected'
  575. );
  576. assert.deepEqual(
  577. this.infos,
  578. [],
  579. 'info as expected'
  580. );
  581. });
  582. QUnit.test('warns when #EXT-X-SERVER-CONTROL missing CAN-SKIP-UNTIL with CAN-SKIP-DATERANGES attribute', function(assert) {
  583. this.parser.push([
  584. '#EXT-X-VERSION:3',
  585. '#EXT-X-MEDIA-SEQUENCE:0',
  586. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  587. '#EXT-X-TARGETDURATION:10',
  588. '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=NO,HOLD-BACK=30,CAN-SKIP-DATERANGES=YES',
  589. '#EXTINF:10,',
  590. 'media-00001.ts',
  591. '#EXT-X-ENDLIST'
  592. ].join('\n'));
  593. this.parser.end();
  594. const warnings = [
  595. '#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set'
  596. ];
  597. assert.deepEqual(
  598. this.warnings,
  599. warnings,
  600. 'warnings as expected'
  601. );
  602. assert.deepEqual(
  603. this.infos,
  604. [],
  605. 'info as expected'
  606. );
  607. });
  608. QUnit.test('warn when #EXT-X-SERVER-CONTROL HOLD-BACK and PART-HOLD-BACK too low', function(assert) {
  609. this.parser.push([
  610. '#EXT-X-VERSION:3',
  611. '#EXT-X-MEDIA-SEQUENCE:0',
  612. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  613. '#EXT-X-TARGETDURATION:10',
  614. '#EXT-X-PART-INF:PART-TARGET=1',
  615. '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=1,PART-HOLD-BACK=0.5',
  616. '#EXTINF:10,',
  617. 'media-00001.ts',
  618. '#EXT-X-ENDLIST'
  619. ].join('\n'));
  620. this.parser.end();
  621. const warnings = [
  622. '#EXT-X-SERVER-CONTROL clamping HOLD-BACK (1) to targetDuration * 3 (30)',
  623. '#EXT-X-SERVER-CONTROL clamping PART-HOLD-BACK (0.5) to partTargetDuration * 2 (2).'
  624. ];
  625. assert.deepEqual(
  626. this.warnings,
  627. warnings,
  628. 'warnings as expected'
  629. );
  630. assert.deepEqual(
  631. this.infos,
  632. [],
  633. 'info as expected'
  634. );
  635. });
  636. QUnit.test('warn when #EXT-X-SERVER-CONTROL before target durations HOLD-BACK/PART-HOLD-BACK too low', function(assert) {
  637. this.parser.push([
  638. '#EXT-X-VERSION:3',
  639. '#EXT-X-MEDIA-SEQUENCE:0',
  640. '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=1,PART-HOLD-BACK=0.5',
  641. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  642. '#EXT-X-TARGETDURATION:10',
  643. '#EXT-X-PART-INF:PART-TARGET=1',
  644. '#EXTINF:10,',
  645. 'media-00001.ts',
  646. '#EXT-X-ENDLIST'
  647. ].join('\n'));
  648. this.parser.end();
  649. const warnings = [
  650. '#EXT-X-SERVER-CONTROL clamping HOLD-BACK (1) to targetDuration * 3 (30)',
  651. '#EXT-X-SERVER-CONTROL clamping PART-HOLD-BACK (0.5) to partTargetDuration * 2 (2).'
  652. ];
  653. assert.deepEqual(
  654. this.warnings,
  655. warnings,
  656. 'warnings as expected'
  657. );
  658. assert.deepEqual(
  659. this.infos,
  660. [],
  661. 'info as expected'
  662. );
  663. });
  664. QUnit.test('info when #EXT-X-SERVER-CONTROL sets defaults', function(assert) {
  665. this.parser.push([
  666. '#EXT-X-VERSION:3',
  667. '#EXT-X-MEDIA-SEQUENCE:0',
  668. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  669. '#EXT-X-TARGETDURATION:10',
  670. '#EXT-X-PART-INF:PART-TARGET=1',
  671. '#EXT-X-SERVER-CONTROL:foo=bar',
  672. '#EXTINF:10,',
  673. 'media-00001.ts',
  674. '#EXT-X-ENDLIST'
  675. ].join('\n'));
  676. this.parser.end();
  677. const infos = [
  678. '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false',
  679. '#EXT-X-SERVER-CONTROL defaulting HOLD-BACK to targetDuration * 3 (30).',
  680. '#EXT-X-SERVER-CONTROL defaulting PART-HOLD-BACK to partTargetDuration * 3 (3).'
  681. ];
  682. assert.deepEqual(
  683. this.warnings,
  684. [],
  685. 'warnings as expected'
  686. );
  687. assert.deepEqual(
  688. this.infos,
  689. infos,
  690. 'info as expected'
  691. );
  692. });
  693. QUnit.test('info when #EXT-X-SERVER-CONTROL before target durations and sets defaults', function(assert) {
  694. this.parser.push([
  695. '#EXT-X-VERSION:3',
  696. '#EXT-X-MEDIA-SEQUENCE:0',
  697. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  698. '#EXT-X-SERVER-CONTROL:foo=bar',
  699. '#EXT-X-TARGETDURATION:10',
  700. '#EXT-X-PART-INF:PART-TARGET=1',
  701. '#EXTINF:10,',
  702. 'media-00001.ts',
  703. '#EXT-X-ENDLIST'
  704. ].join('\n'));
  705. this.parser.end();
  706. const infos = [
  707. '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false',
  708. '#EXT-X-SERVER-CONTROL defaulting HOLD-BACK to targetDuration * 3 (30).',
  709. '#EXT-X-SERVER-CONTROL defaulting PART-HOLD-BACK to partTargetDuration * 3 (3).'
  710. ];
  711. assert.deepEqual(
  712. this.warnings,
  713. [],
  714. 'warnings as expected'
  715. );
  716. assert.deepEqual(
  717. this.infos,
  718. infos,
  719. 'info as expected'
  720. );
  721. });
  722. QUnit.test('Can understand widevine/fairplay/playready drm ext-x-key', function(assert) {
  723. this.parser.push([
  724. '#EXT-X-VERSION:3',
  725. '#EXT-X-MEDIA-SEQUENCE:0',
  726. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  727. '#EXT-X-TARGETDURATION:10',
  728. '#EXT-X-PART-INF:PART-TARGET=1',
  729. '#EXT-X-SERVER-CONTROL:foo=bar',
  730. '#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,foo",KEYID=0x555777,IV=1234567890abcdef1234567890abcdef,KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"',
  731. '#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://foo",KEYFORMATVERSIONS="1",KEYFORMAT="com.apple.streamingkeydelivery"',
  732. '#EXT-X-KEY:METHOD=SAMPLE-AES,URI="http://example.com",KEYFORMATVERSIONS="1",KEYFORMAT="com.microsoft.playready"',
  733. '#EXTINF:10,',
  734. 'media-00001.ts',
  735. '#EXT-X-ENDLIST'
  736. ].join('\n'));
  737. this.parser.end();
  738. assert.deepEqual(
  739. Object.keys(this.parser.manifest.contentProtection),
  740. ['com.widevine.alpha', 'com.apple.fps.1_0', 'com.microsoft.playready'],
  741. 'info as expected'
  742. );
  743. });
  744. QUnit.module('integration');
  745. for (const key in testDataExpected) {
  746. if (!testDataManifests[key]) {
  747. throw new Error(`${key}.js does not have an equivelent m3u8 manifest to test against`);
  748. }
  749. }
  750. for (const key in testDataManifests) {
  751. if (!testDataExpected[key]) {
  752. throw new Error(`${key}.m3u8 does not have an equivelent expected js file to test against`);
  753. }
  754. QUnit.test(`parses ${key}.m3u8 as expected in ${key}.js`, function(assert) {
  755. this.parser.push(testDataManifests[key]());
  756. this.parser.end();
  757. assert.deepEqual(
  758. this.parser.manifest,
  759. testDataExpected[key](),
  760. key + '.m3u8 was parsed correctly'
  761. );
  762. });
  763. }
  764. });