m3u8.test.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  1. import {ParseStream, LineStream, Parser} from '../src';
  2. import QUnit from 'qunit';
  3. import testDataExpected from './test-expected.js';
  4. import testDataManifests from './test-manifests.js';
  5. QUnit.module('LineStream', {
  6. beforeEach() {
  7. this.lineStream = new LineStream();
  8. }
  9. });
  10. QUnit.test('empty inputs produce no tokens', function() {
  11. let data = false;
  12. this.lineStream.on('data', function() {
  13. data = true;
  14. });
  15. this.lineStream.push('');
  16. QUnit.ok(!data, 'no tokens were produced');
  17. });
  18. QUnit.test('splits on newlines', function() {
  19. const lines = [];
  20. this.lineStream.on('data', function(line) {
  21. lines.push(line);
  22. });
  23. this.lineStream.push('#EXTM3U\nmovie.ts\n');
  24. QUnit.strictEqual(2, lines.length, 'two lines are ready');
  25. QUnit.strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
  26. QUnit.strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
  27. });
  28. QUnit.test('empty lines become empty strings', function() {
  29. const lines = [];
  30. this.lineStream.on('data', function(line) {
  31. lines.push(line);
  32. });
  33. this.lineStream.push('\n\n');
  34. QUnit.strictEqual(2, lines.length, 'two lines are ready');
  35. QUnit.strictEqual('', lines.shift(), 'the first line is empty');
  36. QUnit.strictEqual('', lines.shift(), 'the second line is empty');
  37. });
  38. QUnit.test('handles lines broken across appends', function() {
  39. const lines = [];
  40. this.lineStream.on('data', function(line) {
  41. lines.push(line);
  42. });
  43. this.lineStream.push('#EXTM');
  44. QUnit.strictEqual(0, lines.length, 'no lines are ready');
  45. this.lineStream.push('3U\nmovie.ts\n');
  46. QUnit.strictEqual(2, lines.length, 'two lines are ready');
  47. QUnit.strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
  48. QUnit.strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
  49. });
  50. QUnit.test('stops sending events after deregistering', function() {
  51. const temporaryLines = [];
  52. const temporary = function(line) {
  53. temporaryLines.push(line);
  54. };
  55. const permanentLines = [];
  56. const permanent = function(line) {
  57. permanentLines.push(line);
  58. };
  59. this.lineStream.on('data', temporary);
  60. this.lineStream.on('data', permanent);
  61. this.lineStream.push('line one\n');
  62. QUnit.strictEqual(temporaryLines.length,
  63. permanentLines.length,
  64. 'both callbacks receive the event');
  65. QUnit.ok(this.lineStream.off('data', temporary), 'a listener was removed');
  66. this.lineStream.push('line two\n');
  67. QUnit.strictEqual(1, temporaryLines.length, 'no new events are received');
  68. QUnit.strictEqual(2, permanentLines.length, 'new events are still received');
  69. });
  70. QUnit.module('ParseStream', {
  71. beforeEach() {
  72. this.lineStream = new LineStream();
  73. this.parseStream = new ParseStream();
  74. this.lineStream.pipe(this.parseStream);
  75. }
  76. });
  77. QUnit.test('parses comment lines', function() {
  78. const manifest = '# a line that starts with a hash mark without "EXT" is a comment\n';
  79. let element;
  80. this.parseStream.on('data', function(elem) {
  81. element = elem;
  82. });
  83. this.lineStream.push(manifest);
  84. QUnit.ok(element, 'an event was triggered');
  85. QUnit.strictEqual(element.type, 'comment', 'the type is comment');
  86. QUnit.strictEqual(element.text,
  87. manifest.slice(1, manifest.length - 1),
  88. 'the comment text is parsed');
  89. });
  90. QUnit.test('parses uri lines', function() {
  91. const manifest = 'any non-blank line that does not start with a hash-mark is a URI\n';
  92. let element;
  93. this.parseStream.on('data', function(elem) {
  94. element = elem;
  95. });
  96. this.lineStream.push(manifest);
  97. QUnit.ok(element, 'an event was triggered');
  98. QUnit.strictEqual(element.type, 'uri', 'the type is uri');
  99. QUnit.strictEqual(element.uri,
  100. manifest.substring(0, manifest.length - 1),
  101. 'the uri text is parsed');
  102. });
  103. QUnit.test('parses unknown tag types', function() {
  104. const manifest = '#EXT-X-EXAMPLE-TAG:some,additional,stuff\n';
  105. let element;
  106. this.parseStream.on('data', function(elem) {
  107. element = elem;
  108. });
  109. this.lineStream.push(manifest);
  110. QUnit.ok(element, 'an event was triggered');
  111. QUnit.strictEqual(element.type, 'tag', 'the type is tag');
  112. QUnit.strictEqual(element.data,
  113. manifest.slice(4, manifest.length - 1),
  114. 'unknown tag data is preserved');
  115. });
  116. // #EXTM3U
  117. QUnit.test('parses #EXTM3U tags', function() {
  118. const manifest = '#EXTM3U\n';
  119. let element;
  120. this.parseStream.on('data', function(elem) {
  121. element = elem;
  122. });
  123. this.lineStream.push(manifest);
  124. QUnit.ok(element, 'an event was triggered');
  125. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  126. QUnit.strictEqual(element.tagType, 'm3u', 'the tag type is m3u');
  127. });
  128. // #EXTINF
  129. QUnit.test('parses minimal #EXTINF tags', function() {
  130. const manifest = '#EXTINF\n';
  131. let element;
  132. this.parseStream.on('data', function(elem) {
  133. element = elem;
  134. });
  135. this.lineStream.push(manifest);
  136. QUnit.ok(element, 'an event was triggered');
  137. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  138. QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
  139. });
  140. QUnit.test('parses #EXTINF tags with durations', function() {
  141. let manifest = '#EXTINF:15\n';
  142. let element;
  143. this.parseStream.on('data', function(elem) {
  144. element = elem;
  145. });
  146. this.lineStream.push(manifest);
  147. QUnit.ok(element, 'an event was triggered');
  148. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  149. QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
  150. QUnit.strictEqual(element.duration, 15, 'the duration is parsed');
  151. QUnit.ok(!('title' in element), 'no title is parsed');
  152. manifest = '#EXTINF:21,\n';
  153. this.lineStream.push(manifest);
  154. QUnit.ok(element, 'an event was triggered');
  155. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  156. QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
  157. QUnit.strictEqual(element.duration, 21, 'the duration is parsed');
  158. QUnit.ok(!('title' in element), 'no title is parsed');
  159. });
  160. QUnit.test('parses #EXTINF tags with a duration and title', function() {
  161. const manifest = '#EXTINF:13,Does anyone really use the title attribute?\n';
  162. let element;
  163. this.parseStream.on('data', function(elem) {
  164. element = elem;
  165. });
  166. this.lineStream.push(manifest);
  167. QUnit.ok(element, 'an event was triggered');
  168. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  169. QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
  170. QUnit.strictEqual(element.duration, 13, 'the duration is parsed');
  171. QUnit.strictEqual(element.title,
  172. manifest.substring(manifest.indexOf(',') + 1, manifest.length - 1),
  173. 'the title is parsed');
  174. });
  175. QUnit.test('parses #EXTINF tags with carriage returns', function() {
  176. const manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n';
  177. let element;
  178. this.parseStream.on('data', function(elem) {
  179. element = elem;
  180. });
  181. this.lineStream.push(manifest);
  182. QUnit.ok(element, 'an event was triggered');
  183. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  184. QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
  185. QUnit.strictEqual(element.duration, 13, 'the duration is parsed');
  186. QUnit.strictEqual(element.title,
  187. manifest.substring(manifest.indexOf(',') + 1, manifest.length - 2),
  188. 'the title is parsed');
  189. });
  190. // #EXT-X-TARGETDURATION
  191. QUnit.test('parses minimal #EXT-X-TARGETDURATION tags', function() {
  192. const manifest = '#EXT-X-TARGETDURATION\n';
  193. let element;
  194. this.parseStream.on('data', function(elem) {
  195. element = elem;
  196. });
  197. this.lineStream.push(manifest);
  198. QUnit.ok(element, 'an event was triggered');
  199. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  200. QUnit.strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
  201. QUnit.ok(!('duration' in element), 'no duration is parsed');
  202. });
  203. QUnit.test('parses #EXT-X-TARGETDURATION with duration', function() {
  204. const manifest = '#EXT-X-TARGETDURATION:47\n';
  205. let element;
  206. this.parseStream.on('data', function(elem) {
  207. element = elem;
  208. });
  209. this.lineStream.push(manifest);
  210. QUnit.ok(element, 'an event was triggered');
  211. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  212. QUnit.strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
  213. QUnit.strictEqual(element.duration, 47, 'the duration is parsed');
  214. });
  215. // #EXT-X-VERSION
  216. QUnit.test('parses minimal #EXT-X-VERSION tags', function() {
  217. const manifest = '#EXT-X-VERSION:\n';
  218. let element;
  219. this.parseStream.on('data', function(elem) {
  220. element = elem;
  221. });
  222. this.lineStream.push(manifest);
  223. QUnit.ok(element, 'an event was triggered');
  224. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  225. QUnit.strictEqual(element.tagType, 'version', 'the tag type is version');
  226. QUnit.ok(!('version' in element), 'no version is present');
  227. });
  228. QUnit.test('parses #EXT-X-VERSION with a version', function() {
  229. const manifest = '#EXT-X-VERSION:99\n';
  230. let element;
  231. this.parseStream.on('data', function(elem) {
  232. element = elem;
  233. });
  234. this.lineStream.push(manifest);
  235. QUnit.ok(element, 'an event was triggered');
  236. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  237. QUnit.strictEqual(element.tagType, 'version', 'the tag type is version');
  238. QUnit.strictEqual(element.version, 99, 'the version is parsed');
  239. });
  240. // #EXT-X-MEDIA-SEQUENCE
  241. QUnit.test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function() {
  242. const manifest = '#EXT-X-MEDIA-SEQUENCE\n';
  243. let element;
  244. this.parseStream.on('data', function(elem) {
  245. element = elem;
  246. });
  247. this.lineStream.push(manifest);
  248. QUnit.ok(element, 'an event was triggered');
  249. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  250. QUnit.strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
  251. QUnit.ok(!('number' in element), 'no number is present');
  252. });
  253. QUnit.test('parses #EXT-X-MEDIA-SEQUENCE with sequence numbers', function() {
  254. const manifest = '#EXT-X-MEDIA-SEQUENCE:109\n';
  255. let element;
  256. this.parseStream.on('data', function(elem) {
  257. element = elem;
  258. });
  259. this.lineStream.push(manifest);
  260. QUnit.ok(element, 'an event was triggered');
  261. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  262. QUnit.strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
  263. QUnit.ok(element.number, 109, 'the number is parsed');
  264. });
  265. // #EXT-X-PLAYLIST-TYPE
  266. QUnit.test('parses minimal #EXT-X-PLAYLIST-TYPE tags', function() {
  267. const manifest = '#EXT-X-PLAYLIST-TYPE:\n';
  268. let element;
  269. this.parseStream.on('data', function(elem) {
  270. element = elem;
  271. });
  272. this.lineStream.push(manifest);
  273. QUnit.ok(element, 'an event was triggered');
  274. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  275. QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
  276. QUnit.ok(!('playlistType' in element), 'no playlist type is present');
  277. });
  278. QUnit.test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function() {
  279. let manifest = '#EXT-X-PLAYLIST-TYPE:EVENT\n';
  280. let element;
  281. this.parseStream.on('data', function(elem) {
  282. element = elem;
  283. });
  284. this.lineStream.push(manifest);
  285. QUnit.ok(element, 'an event was triggered');
  286. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  287. QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
  288. QUnit.strictEqual(element.playlistType, 'EVENT', 'the playlist type is EVENT');
  289. manifest = '#EXT-X-PLAYLIST-TYPE:VOD\n';
  290. this.lineStream.push(manifest);
  291. QUnit.ok(element, 'an event was triggered');
  292. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  293. QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
  294. QUnit.strictEqual(element.playlistType, 'VOD', 'the playlist type is VOD');
  295. manifest = '#EXT-X-PLAYLIST-TYPE:nonsense\n';
  296. this.lineStream.push(manifest);
  297. QUnit.ok(element, 'an event was triggered');
  298. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  299. QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
  300. QUnit.strictEqual(element.playlistType, 'nonsense', 'the playlist type is parsed');
  301. });
  302. // #EXT-X-BYTERANGE
  303. QUnit.test('parses minimal #EXT-X-BYTERANGE tags', function() {
  304. const manifest = '#EXT-X-BYTERANGE\n';
  305. let element;
  306. this.parseStream.on('data', function(elem) {
  307. element = elem;
  308. });
  309. this.lineStream.push(manifest);
  310. QUnit.ok(element, 'an event was triggered');
  311. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  312. QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
  313. QUnit.ok(!('length' in element), 'no length is present');
  314. QUnit.ok(!('offset' in element), 'no offset is present');
  315. });
  316. QUnit.test('parses #EXT-X-BYTERANGE with length and offset', function() {
  317. let manifest = '#EXT-X-BYTERANGE:45\n';
  318. let element;
  319. this.parseStream.on('data', function(elem) {
  320. element = elem;
  321. });
  322. this.lineStream.push(manifest);
  323. QUnit.ok(element, 'an event was triggered');
  324. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  325. QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
  326. QUnit.strictEqual(element.length, 45, 'length is parsed');
  327. QUnit.ok(!('offset' in element), 'no offset is present');
  328. manifest = '#EXT-X-BYTERANGE:108@16\n';
  329. this.lineStream.push(manifest);
  330. QUnit.ok(element, 'an event was triggered');
  331. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  332. QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
  333. QUnit.strictEqual(element.length, 108, 'length is parsed');
  334. QUnit.strictEqual(element.offset, 16, 'offset is parsed');
  335. });
  336. // #EXT-X-ALLOW-CACHE
  337. QUnit.test('parses minimal #EXT-X-ALLOW-CACHE tags', function() {
  338. const manifest = '#EXT-X-ALLOW-CACHE:\n';
  339. let element;
  340. this.parseStream.on('data', function(elem) {
  341. element = elem;
  342. });
  343. this.lineStream.push(manifest);
  344. QUnit.ok(element, 'an event was triggered');
  345. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  346. QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
  347. QUnit.ok(!('allowed' in element), 'no allowed is present');
  348. });
  349. QUnit.test('parses valid #EXT-X-ALLOW-CACHE tags', function() {
  350. let manifest = '#EXT-X-ALLOW-CACHE:YES\n';
  351. let element;
  352. this.parseStream.on('data', function(elem) {
  353. element = elem;
  354. });
  355. this.lineStream.push(manifest);
  356. QUnit.ok(element, 'an event was triggered');
  357. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  358. QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
  359. QUnit.ok(element.allowed, 'allowed is parsed');
  360. manifest = '#EXT-X-ALLOW-CACHE:NO\n';
  361. this.lineStream.push(manifest);
  362. QUnit.ok(element, 'an event was triggered');
  363. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  364. QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
  365. QUnit.ok(!element.allowed, 'allowed is parsed');
  366. });
  367. // #EXT-X-MAP
  368. QUnit.test('parses minimal #EXT-X-MAP tags', function() {
  369. const manifest = '#EXT-X-MAP:URI="init.m4s"\n';
  370. let element;
  371. this.parseStream.on('data', function(elem) {
  372. element = elem;
  373. });
  374. this.lineStream.push(manifest);
  375. QUnit.ok(element, 'an event was triggered');
  376. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  377. QUnit.strictEqual(element.tagType, 'map', 'the tag type is map');
  378. QUnit.strictEqual(element.uri, 'init.m4s', 'parsed the uri');
  379. });
  380. QUnit.test('parses #EXT-X-MAP tags with a byterange', function() {
  381. const manifest = '#EXT-X-MAP:URI="0.m4s", BYTERANGE="1000@23"\n';
  382. let element;
  383. this.parseStream.on('data', function(elem) {
  384. element = elem;
  385. });
  386. this.lineStream.push(manifest);
  387. QUnit.ok(element, 'an event was triggered');
  388. QUnit.strictEqual(element.uri, '0.m4s', 'parsed the uri');
  389. QUnit.strictEqual(element.byterange.length,
  390. 1000,
  391. 'parsed the byterange length');
  392. QUnit.strictEqual(element.byterange.offset,
  393. 23,
  394. 'parsed the byterange offset');
  395. });
  396. QUnit.test('parses #EXT-X-MAP tags with arbitrary attributes', function() {
  397. const manifest = '#EXT-X-MAP:URI="init.mp4", SOMETHING=YES,BYTERANGE="720@0"\n';
  398. let element;
  399. this.parseStream.on('data', function(elem) {
  400. element = elem;
  401. });
  402. this.lineStream.push(manifest);
  403. QUnit.ok(element, 'an event was triggered');
  404. QUnit.strictEqual(element.uri, 'init.mp4', 'parsed the uri');
  405. QUnit.strictEqual(element.byterange.length,
  406. 720,
  407. 'parsed the byterange length');
  408. QUnit.strictEqual(element.byterange.offset,
  409. 0,
  410. 'parsed the byterange offset');
  411. });
  412. // #EXT-X-STREAM-INF
  413. QUnit.test('parses minimal #EXT-X-STREAM-INF tags', function() {
  414. const manifest = '#EXT-X-STREAM-INF\n';
  415. let element;
  416. this.parseStream.on('data', function(elem) {
  417. element = elem;
  418. });
  419. this.lineStream.push(manifest);
  420. QUnit.ok(element, 'an event was triggered');
  421. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  422. QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
  423. QUnit.ok(!('attributes' in element), 'no attributes are present');
  424. });
  425. // #EXT-X-PROGRAM-DATE-TIME
  426. QUnit.test('parses minimal EXT-X-PROGRAM-DATE-TIME tags', function() {
  427. const manifest = '#EXT-X-PROGRAM-DATE-TIME\n';
  428. let element;
  429. this.parseStream.on('data', function(elem) {
  430. element = elem;
  431. });
  432. this.lineStream.push(manifest);
  433. QUnit.ok(element, 'an event was triggered');
  434. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  435. QUnit.strictEqual(element.tagType, 'program-date-time', 'the tag type is date-time');
  436. QUnit.ok(!('dateTimeString' in element), 'no dateTime is present');
  437. });
  438. QUnit.test('parses EXT-X-PROGRAM-DATE-TIME tags with valid date-time formats',
  439. function() {
  440. let manifest = '#EXT-X-PROGRAM-DATE-TIME:2016-06-22T09:20:16.166-04:00\n';
  441. let element;
  442. this.parseStream.on('data', function(elem) {
  443. element = elem;
  444. });
  445. this.lineStream.push(manifest);
  446. QUnit.ok(element, 'an event was triggered');
  447. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  448. QUnit.strictEqual(element.tagType, 'program-date-time', 'the tag type is date-time');
  449. QUnit.strictEqual(element.dateTimeString, '2016-06-22T09:20:16.166-04:00',
  450. 'dateTimeString is parsed');
  451. QUnit.deepEqual(element.dateTimeObject, new Date('2016-06-22T09:20:16.166-04:00'),
  452. 'dateTimeObject is parsed');
  453. manifest = '#EXT-X-PROGRAM-DATE-TIME:2016-06-22T09:20:16.16389Z\n';
  454. this.lineStream.push(manifest);
  455. QUnit.ok(element, 'an event was triggered');
  456. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  457. QUnit.strictEqual(element.tagType, 'program-date-time', 'the tag type is date-time');
  458. QUnit.strictEqual(element.dateTimeString, '2016-06-22T09:20:16.16389Z',
  459. 'dateTimeString is parsed');
  460. QUnit.deepEqual(element.dateTimeObject, new Date('2016-06-22T09:20:16.16389Z'),
  461. 'dateTimeObject is parsed');
  462. });
  463. QUnit.test('parses #EXT-X-STREAM-INF with common attributes', function() {
  464. let manifest = '#EXT-X-STREAM-INF:BANDWIDTH=14400\n';
  465. let element;
  466. this.parseStream.on('data', function(elem) {
  467. element = elem;
  468. });
  469. this.lineStream.push(manifest);
  470. QUnit.ok(element, 'an event was triggered');
  471. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  472. QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
  473. QUnit.strictEqual(element.attributes.BANDWIDTH, 14400, 'bandwidth is parsed');
  474. manifest = '#EXT-X-STREAM-INF:PROGRAM-ID=7\n';
  475. this.lineStream.push(manifest);
  476. QUnit.ok(element, 'an event was triggered');
  477. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  478. QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
  479. QUnit.strictEqual(element.attributes['PROGRAM-ID'], 7, 'program-id is parsed');
  480. manifest = '#EXT-X-STREAM-INF:RESOLUTION=396x224\n';
  481. this.lineStream.push(manifest);
  482. QUnit.ok(element, 'an event was triggered');
  483. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  484. QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
  485. QUnit.strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed');
  486. QUnit.strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed');
  487. manifest = '#EXT-X-STREAM-INF:CODECS="avc1.4d400d, mp4a.40.2"\n';
  488. this.lineStream.push(manifest);
  489. QUnit.ok(element, 'an event was triggered');
  490. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  491. QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
  492. QUnit.strictEqual(element.attributes.CODECS,
  493. 'avc1.4d400d, mp4a.40.2',
  494. 'codecs are parsed');
  495. });
  496. QUnit.test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() {
  497. const manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n';
  498. let element;
  499. this.parseStream.on('data', function(elem) {
  500. element = elem;
  501. });
  502. this.lineStream.push(manifest);
  503. QUnit.ok(element, 'an event was triggered');
  504. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  505. QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
  506. QUnit.strictEqual(element.attributes.NUMERIC, '24', 'numeric attributes are parsed');
  507. QUnit.strictEqual(element.attributes.ALPHA,
  508. 'Value',
  509. 'alphabetic attributes are parsed');
  510. QUnit.strictEqual(element.attributes.MIXED, '123abc', 'mixed attributes are parsed');
  511. });
  512. // #EXT-X-ENDLIST
  513. QUnit.test('parses #EXT-X-ENDLIST tags', function() {
  514. const manifest = '#EXT-X-ENDLIST\n';
  515. let element;
  516. this.parseStream.on('data', function(elem) {
  517. element = elem;
  518. });
  519. this.lineStream.push(manifest);
  520. QUnit.ok(element, 'an event was triggered');
  521. QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
  522. QUnit.strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf');
  523. });
  524. // #EXT-X-KEY
  525. QUnit.test('parses valid #EXT-X-KEY tags', function() {
  526. let manifest =
  527. '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n';
  528. let element;
  529. this.parseStream.on('data', function(elem) {
  530. element = elem;
  531. });
  532. this.lineStream.push(manifest);
  533. QUnit.ok(element, 'an event was triggered');
  534. QUnit.deepEqual(element, {
  535. type: 'tag',
  536. tagType: 'key',
  537. attributes: {
  538. METHOD: 'AES-128',
  539. URI: 'https://priv.example.com/key.php?r=52'
  540. }
  541. }, 'parsed a valid key');
  542. manifest = '#EXT-X-KEY:URI="https://example.com/key#1",METHOD=FutureType-1024\n';
  543. this.lineStream.push(manifest);
  544. QUnit.ok(element, 'an event was triggered');
  545. QUnit.deepEqual(element, {
  546. type: 'tag',
  547. tagType: 'key',
  548. attributes: {
  549. METHOD: 'FutureType-1024',
  550. URI: 'https://example.com/key#1'
  551. }
  552. }, 'parsed the attribute list independent of order');
  553. manifest = '#EXT-X-KEY:IV=1234567890abcdef1234567890abcdef\n';
  554. this.lineStream.push(manifest);
  555. QUnit.ok(element.attributes.IV, 'detected an IV attribute');
  556. QUnit.deepEqual(element.attributes.IV, new Uint32Array([
  557. 0x12345678,
  558. 0x90abcdef,
  559. 0x12345678,
  560. 0x90abcdef
  561. ]), 'parsed an IV value');
  562. });
  563. QUnit.test('parses minimal #EXT-X-KEY tags', function() {
  564. const manifest = '#EXT-X-KEY:\n';
  565. let element;
  566. this.parseStream.on('data', function(elem) {
  567. element = elem;
  568. });
  569. this.lineStream.push(manifest);
  570. QUnit.ok(element, 'an event was triggered');
  571. QUnit.deepEqual(element, {
  572. type: 'tag',
  573. tagType: 'key'
  574. }, 'parsed a minimal key tag');
  575. });
  576. QUnit.test('parses lightly-broken #EXT-X-KEY tags', function() {
  577. let manifest = '#EXT-X-KEY:URI=\'https://example.com/single-quote\',METHOD=AES-128\n';
  578. let element;
  579. this.parseStream.on('data', function(elem) {
  580. element = elem;
  581. });
  582. this.lineStream.push(manifest);
  583. QUnit.strictEqual(element.attributes.URI,
  584. 'https://example.com/single-quote',
  585. 'parsed a single-quoted uri');
  586. element = null;
  587. manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n';
  588. this.lineStream.push(manifest);
  589. QUnit.strictEqual(element.tagType, 'key', 'parsed the tag type');
  590. QUnit.strictEqual(element.attributes.URI,
  591. 'https://example.com/key',
  592. 'inferred a colon after the tag type');
  593. element = null;
  594. manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n';
  595. this.lineStream.push(manifest);
  596. QUnit.strictEqual(element.attributes.URI,
  597. 'https://example.com/key',
  598. 'trims and removes quotes around the URI');
  599. });
  600. QUnit.test('parses prefixed with 0x or 0X #EXT-X-KEY:IV tags', function() {
  601. let manifest;
  602. let element;
  603. this.parseStream.on('data', function(elem) {
  604. element = elem;
  605. });
  606. manifest = '#EXT-X-KEY:IV=0x1234567890abcdef1234567890abcdef\n';
  607. this.lineStream.push(manifest);
  608. QUnit.ok(element.attributes.IV, 'detected an IV attribute');
  609. QUnit.deepEqual(element.attributes.IV, new Uint32Array([
  610. 0x12345678,
  611. 0x90abcdef,
  612. 0x12345678,
  613. 0x90abcdef
  614. ]), 'parsed an IV value with 0x');
  615. manifest = '#EXT-X-KEY:IV=0X1234567890abcdef1234567890abcdef\n';
  616. this.lineStream.push(manifest);
  617. QUnit.ok(element.attributes.IV, 'detected an IV attribute');
  618. QUnit.deepEqual(element.attributes.IV, new Uint32Array([
  619. 0x12345678,
  620. 0x90abcdef,
  621. 0x12345678,
  622. 0x90abcdef
  623. ]), 'parsed an IV value with 0X');
  624. });
  625. QUnit.test('ignores empty lines', function() {
  626. const manifest = '\n';
  627. let event = false;
  628. this.parseStream.on('data', function() {
  629. event = true;
  630. });
  631. this.lineStream.push(manifest);
  632. QUnit.ok(!event, 'no event is triggered');
  633. });
  634. QUnit.module('m3u8 parser');
  635. QUnit.test('can be constructed', function() {
  636. QUnit.notStrictEqual(typeof new Parser(), 'undefined', 'parser is defined');
  637. });
  638. QUnit.test('attaches cue-out data to segment', function() {
  639. const parser = new Parser();
  640. const manifest = [
  641. '#EXTM3U',
  642. '#EXTINF:5,',
  643. '#COMMENT',
  644. 'ex1.ts',
  645. '#EXT-X-CUE-OUT:10',
  646. '#EXTINF:5,',
  647. 'ex2.ts',
  648. '#EXT-X-CUE-OUT15',
  649. '#EXT-UKNOWN-TAG',
  650. '#EXTINF:5,',
  651. 'ex3.ts',
  652. '#EXT-X-CUE-OUT',
  653. '#EXTINF:5,',
  654. 'ex3.ts',
  655. '#EXT-X-ENDLIST'
  656. ].join('\n');
  657. parser.push(manifest);
  658. QUnit.equal(parser.manifest.segments[1].cueOut, '10', 'parser attached cue out tag');
  659. QUnit.equal(parser.manifest.segments[2].cueOut, '15', 'cue out without : seperator');
  660. QUnit.equal(parser.manifest.segments[3].cueOut, '', 'cue out without data');
  661. });
  662. QUnit.test('attaches cue-out-cont data to segment', function() {
  663. const parser = new Parser();
  664. const manifest = [
  665. '#EXTM3U',
  666. '#EXTINF:5,',
  667. '#COMMENT',
  668. 'ex1.ts',
  669. '#EXT-X-CUE-OUT-CONT:10/60',
  670. '#EXTINF:5,',
  671. 'ex2.ts',
  672. '#EXT-X-CUE-OUT-CONT15/30',
  673. '#EXT-UKNOWN-TAG',
  674. '#EXTINF:5,',
  675. 'ex3.ts',
  676. '#EXT-X-CUE-OUT-CONT',
  677. '#EXTINF:5,',
  678. 'ex3.ts',
  679. '#EXT-X-ENDLIST'
  680. ].join('\n');
  681. parser.push(manifest);
  682. QUnit.equal(parser.manifest.segments[1].cueOutCont, '10/60',
  683. 'parser attached cue out cont tag');
  684. QUnit.equal(parser.manifest.segments[2].cueOutCont, '15/30',
  685. 'cue out cont without : seperator');
  686. QUnit.equal(parser.manifest.segments[3].cueOutCont, '', 'cue out cont without data');
  687. });
  688. QUnit.test('attaches cue-in data to segment', function() {
  689. const parser = new Parser();
  690. const manifest = [
  691. '#EXTM3U',
  692. '#EXTINF:5,',
  693. '#COMMENT',
  694. 'ex1.ts',
  695. '#EXT-X-CUE-IN',
  696. '#EXTINF:5,',
  697. 'ex2.ts',
  698. '#EXT-X-CUE-IN:15',
  699. '#EXT-UKNOWN-TAG',
  700. '#EXTINF:5,',
  701. 'ex3.ts',
  702. '#EXT-X-CUE-IN=abc',
  703. '#EXTINF:5,',
  704. 'ex3.ts',
  705. '#EXT-X-ENDLIST'
  706. ].join('\n');
  707. parser.push(manifest);
  708. QUnit.equal(parser.manifest.segments[1].cueIn, '', 'parser attached cue in tag');
  709. QUnit.equal(parser.manifest.segments[2].cueIn, '15', 'cue in with data');
  710. QUnit.equal(parser.manifest.segments[3].cueIn, '=abc',
  711. 'cue in without colon seperator');
  712. });
  713. QUnit.test('parses characteristics attribute', function() {
  714. const parser = new Parser();
  715. const manifest = [
  716. '#EXTM3U',
  717. '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",CHARACTERISTICS="char",NAME="test"',
  718. '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2, avc1.4d400d",SUBTITLES="subs"',
  719. 'index.m3u8'
  720. ].join('\n');
  721. parser.push(manifest);
  722. QUnit.equal(parser.manifest.mediaGroups.SUBTITLES.subs.test.characteristics,
  723. 'char',
  724. 'parsed CHARACTERISTICS attribute');
  725. });
  726. QUnit.test('parses FORCED attribute', function() {
  727. const parser = new Parser();
  728. const manifest = [
  729. '#EXTM3U',
  730. '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",CHARACTERISTICS="char",NAME="test",FORCED=YES',
  731. '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2, avc1.4d400d",SUBTITLES="subs"',
  732. 'index.m3u8'
  733. ].join('\n');
  734. parser.push(manifest);
  735. QUnit.ok(parser.manifest.mediaGroups.SUBTITLES.subs.test.forced,
  736. 'parsed FORCED attribute');
  737. });
  738. QUnit.module('m3u8s');
  739. QUnit.test('parses static manifests as expected', function() {
  740. let key;
  741. for (key in testDataManifests) {
  742. if (testDataExpected[key]) {
  743. const parser = new Parser();
  744. parser.push(testDataManifests[key]);
  745. QUnit.deepEqual(parser.manifest,
  746. testDataExpected[key],
  747. key + '.m3u8 was parsed correctly'
  748. );
  749. }
  750. }
  751. });