parser.test.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214
  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.test('PDT value is assigned to segments with explicit #EXT-X-PROGRAM-DATE-TIME tags', function(assert) {
  745. this.parser.push([
  746. '#EXTM3U',
  747. '#EXT-X-VERSION:6',
  748. '#EXT-X-TARGETDURATION:8',
  749. '#EXT-X-MEDIA-SEQUENCE:0',
  750. '#EXTINF:8.0',
  751. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  752. 'https://example.com/playlist1.m3u8',
  753. '#EXTINF:8.0,',
  754. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T22:14:10.053+00:00',
  755. 'https://example.com/playlist2.m3u8',
  756. '#EXT-X-ENDLIST'
  757. ].join('\n'));
  758. this.parser.end();
  759. assert.equal(this.parser.manifest.segments[0].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
  760. assert.equal(this.parser.manifest.segments[1].programDateTime, new Date('2017-07-31T22:14:10.053+00:00').getTime());
  761. });
  762. QUnit.test('backfill PDT values when the first EXT-X-PROGRAM-DATE-TIME tag appears after one or more Media Segment URIs', function(assert) {
  763. this.parser.push([
  764. '#EXTM3U',
  765. '#EXT-X-VERSION:6',
  766. '#EXT-X-TARGETDURATION:8',
  767. '#EXT-X-MEDIA-SEQUENCE:0',
  768. '#EXTINF:8.0',
  769. 'https://example.com/playlist1.m3u8',
  770. '#EXTINF:8.0,',
  771. 'https://example.com/playlist2.m3u8',
  772. '#EXTINF:8.0',
  773. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  774. 'https://example.com/playlist3.m3u8',
  775. '#EXT-X-ENDLIST'
  776. ].join('\n'));
  777. this.parser.end();
  778. const segments = this.parser.manifest.segments;
  779. assert.equal(segments[2].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
  780. assert.equal(segments[1].programDateTime, segments[2].programDateTime - (segments[1].duration * 1000));
  781. assert.equal(segments[0].programDateTime, segments[1].programDateTime - (segments[0].duration * 1000));
  782. });
  783. QUnit.test('extrapolates forward when subsequent fragments do not have explicit PDT tags', function(assert) {
  784. this.parser.push([
  785. '#EXTM3U',
  786. '#EXT-X-VERSION:6',
  787. '#EXT-X-TARGETDURATION:8',
  788. '#EXT-X-MEDIA-SEQUENCE:0',
  789. '#EXTINF:8.0',
  790. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  791. 'https://example.com/playlist1.m3u8',
  792. '#EXTINF:8.0,',
  793. 'https://example.com/playlist2.m3u8',
  794. '#EXTINF:8.0',
  795. 'https://example.com/playlist3.m3u8',
  796. '#EXT-X-ENDLIST'
  797. ].join('\n'));
  798. this.parser.end();
  799. const segments = this.parser.manifest.segments;
  800. assert.equal(segments[0].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
  801. assert.equal(segments[1].programDateTime, segments[0].programDateTime + segments[1].duration * 1000);
  802. assert.equal(segments[2].programDateTime, segments[1].programDateTime + segments[2].duration * 1000);
  803. });
  804. QUnit.test('warns when #EXT-X-DATERANGE missing attribute', function(assert) {
  805. this.parser.push([
  806. '#EXT-X-VERSION:3',
  807. '#EXT-X-MEDIA-SEQUENCE:0',
  808. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  809. '#EXTINF:10,',
  810. 'media-00001.ts',
  811. '#EXT-X-ENDLIST',
  812. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  813. '#EXT-X-DATERANGE:ID="12345"'
  814. ].join('\n'));
  815. this.parser.end();
  816. const warnings = [
  817. '#EXT-X-DATERANGE #0 lacks required attribute(s): START-DATE'
  818. ];
  819. assert.deepEqual(
  820. this.warnings,
  821. warnings,
  822. 'warnings as expected'
  823. );
  824. });
  825. QUnit.test('warns when #EXT-X-DATERANGE end date attribute is less than start date', function(assert) {
  826. this.parser.push([
  827. '#EXT-X-VERSION:3',
  828. '#EXT-X-MEDIA-SEQUENCE:0',
  829. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  830. '#EXTINF:10,',
  831. 'media-00001.ts',
  832. '#EXT-X-ENDLIST',
  833. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  834. '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-DATE="2023-04-13T15:15:15.840000Z"'
  835. ].join('\n'));
  836. this.parser.end();
  837. const warnings = [
  838. 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE'
  839. ];
  840. assert.deepEqual(
  841. this.warnings,
  842. warnings,
  843. 'warnings as expected'
  844. );
  845. });
  846. QUnit.test('warns when #EXT-X-DATERANGE duration or planned duration attribute is negative', function(assert) {
  847. this.parser.push([
  848. '#EXT-X-VERSION:3',
  849. '#EXT-X-MEDIA-SEQUENCE:0',
  850. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  851. '#EXTINF:10,',
  852. 'media-00001.ts',
  853. '#EXT-X-ENDLIST',
  854. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  855. '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",PLANNED-DURATION=-38.4,DURATION=-15.5'
  856. ].join('\n'));
  857. this.parser.end();
  858. const warnings = [
  859. 'EXT-X-DATERANGE DURATION must not be negative',
  860. 'EXT-X-DATERANGE PLANNED-DURATION must not be negative'
  861. ];
  862. assert.deepEqual(
  863. this.warnings,
  864. warnings,
  865. 'warnings as expected'
  866. );
  867. });
  868. QUnit.test('warns when #EXT-X-DATERANGE has a END-ON-NEXT=YES attribute and a DURATION or END-DATE attribute', function(assert) {
  869. this.parser.push([
  870. '#EXT-X-VERSION:3',
  871. '#EXT-X-MEDIA-SEQUENCE:0',
  872. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  873. '#EXTINF:10,',
  874. 'media-00001.ts',
  875. '#EXT-X-ENDLIST',
  876. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  877. '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",END-ON-NEXT=YES, END-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE"'
  878. ].join('\n'));
  879. this.parser.end();
  880. const warnings = [
  881. 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes'
  882. ];
  883. assert.deepEqual(
  884. this.warnings,
  885. warnings,
  886. 'warnings as expected'
  887. );
  888. });
  889. QUnit.test('warns when #EXT-X-DATERANGE has a END-ON-NEXT=YES attribute but not a CLASS attribute', function(assert) {
  890. this.parser.push([
  891. '#EXT-X-VERSION:3',
  892. '#EXT-X-MEDIA-SEQUENCE:0',
  893. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  894. '#EXTINF:10,',
  895. 'media-00001.ts',
  896. '#EXT-X-ENDLIST',
  897. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  898. '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES'
  899. ].join('\n'));
  900. this.parser.end();
  901. const warnings = [
  902. 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute'
  903. ];
  904. assert.deepEqual(
  905. this.warnings,
  906. warnings,
  907. 'warnings as expected'
  908. );
  909. });
  910. QUnit.test('warns when playlist has multiple #EXT-X-DATERANGE tag same ID but different attribute values', function(assert) {
  911. this.parser.push([
  912. '#EXT-X-VERSION:3',
  913. '#EXT-X-MEDIA-SEQUENCE:0',
  914. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  915. '#EXTINF:10,',
  916. 'media-00001.ts',
  917. '#EXT-X-ENDLIST',
  918. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  919. '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES,CLASS="CLASSATTRIBUTE"',
  920. '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE1"'
  921. ].join('\n'));
  922. this.parser.end();
  923. const warnings = [
  924. 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values'
  925. ];
  926. assert.deepEqual(
  927. this.warnings,
  928. warnings,
  929. 'warnings as expected'
  930. );
  931. });
  932. QUnit.test('when #EXT-X-DATERANGE has both DURATION and END-DATE attributes, value of the END-DATE attribute must be START-DATE + DURATION', function(assert) {
  933. this.parser.push([
  934. '#EXT-X-VERSION:3',
  935. '#EXT-X-MEDIA-SEQUENCE:0',
  936. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  937. '#EXTINF:10,',
  938. 'media-00001.ts',
  939. '#EXT-X-ENDLIST',
  940. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  941. '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:16:15.840000Z",DURATION=14.0,END-DATE="2023-04-13T18:15:15.840000Z"'
  942. ].join('\n'));
  943. this.parser.end();
  944. assert.deepEqual(this.parser.manifest.dateRanges[0].endDate, new Date('2023-04-13T15:16:29.840000Z'));
  945. });
  946. QUnit.test('warns when playlist contains #EXT-X-DATERANGE tag but no #EXT-X-PROGRAM-DATE-TIME', function(assert) {
  947. this.parser.push([
  948. '#EXT-X-VERSION:3',
  949. '#EXT-X-MEDIA-SEQUENCE:0',
  950. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  951. '#EXTINF:10,',
  952. 'media-00001.ts',
  953. '#EXT-X-ENDLIST',
  954. '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES,CLASS="sampleClassAttrib"'
  955. ].join('\n'));
  956. this.parser.end();
  957. const warnings = [
  958. 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
  959. ];
  960. assert.deepEqual(
  961. this.warnings,
  962. warnings,
  963. 'warnings as expected'
  964. );
  965. });
  966. QUnit.test('playlist with multiple ext-x-daterange with same ID but no conflicting attributes', function(assert) {
  967. const expectedDateRange = {
  968. id: '12345',
  969. scte35In: '0xFC30200FFF2',
  970. scte35Out: '0xFC30200FFF2',
  971. startDate: new Date('2023-04-13T18:16:15.840000Z'),
  972. class: 'CLASSATTRIBUTE'
  973. };
  974. this.parser.push([
  975. '#EXT-X-VERSION:3',
  976. '#EXT-X-MEDIA-SEQUENCE:0',
  977. '#EXT-X-DISCONTINUITY-SEQUENCE:0',
  978. '#EXTINF:10,',
  979. 'media-00001.ts',
  980. '#EXT-X-ENDLIST',
  981. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  982. '#EXT-X-DATERANGE:ID="12345",SCTE35-IN=0xFC30200FFF2,START-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE"',
  983. '#EXT-X-DATERANGE:ID="12345",SCTE35-OUT=0xFC30200FFF2,START-DATE="2023-04-13T18:16:15.840000Z"'
  984. ].join('\n'));
  985. this.parser.end();
  986. assert.equal(this.parser.manifest.dateRanges.length, 1, 'two dateranges with same ID are merged');
  987. assert.deepEqual(this.parser.manifest.dateRanges[0], expectedDateRange);
  988. });
  989. QUnit.test('playlist with multiple ext-x-daterange ', function(assert) {
  990. this.parser.push([
  991. ' #EXTM3U',
  992. '#EXT-X-VERSION:6',
  993. '#EXT-X-TARGETDURATION:8',
  994. '#EXT-X-MEDIA-SEQUENCE:0',
  995. '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
  996. '#EXT-X-DATERANGE:ID="event1",START-DATE="2023-04-20T10:00:00Z",DURATION=30.0,END-DATE="2023-04-20T10:00:30Z",X-CUSTOM-KEY="value"',
  997. '#EXTINF:8.0',
  998. 'https://example.com/playlist1.m3u8',
  999. '#EXT-SCTE35-IN:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
  1000. '#EXT-X-DATERANGE:ID="event2",START-DATE="2023-04-20T11:00:00Z",DURATION=60.0,END-DATE="2023-04-20T11:01:00Z",X-CUSTOM-KEY="value"',
  1001. '#EXTINF:8.0,',
  1002. 'https://example.com/playlist2.m3u8',
  1003. '#EXT-SCTE35-OUT:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
  1004. '#EXT-X-DATERANGE:ID="event3",START-DATE="2023-04-20T12:00:00Z",DURATION=120.0,END-DATE="2023-04-20T12:02:00Z",X-CUSTOM-KEY="value"',
  1005. '#EXTINF:8.0',
  1006. 'https://example.com/playlist3.m3u8',
  1007. '#EXT-SCTE35-IN:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
  1008. '#EXT-SCTE35-OUT:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
  1009. '#EXT-X-ENDLIST'
  1010. ].join('\n'));
  1011. this.parser.end();
  1012. assert.equal(this.parser.manifest.dateRanges.length, 3);
  1013. });
  1014. QUnit.test('parses #EXT-X-INDEPENDENT-SEGMENTS', function(assert) {
  1015. this.parser.push([
  1016. '#EXTM3U',
  1017. '#EXT-X-VERSION:6',
  1018. '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=3.252,CAN-SKIP-UNTIL=42.0',
  1019. '#EXT-X-INDEPENDENT-SEGMENTS'
  1020. ].join('\n'));
  1021. this.parser.end();
  1022. assert.equal(this.parser.manifest.independentSegments, true);
  1023. });
  1024. QUnit.test('parses #EXT-X-CONTENT-STEERING', function(assert) {
  1025. const expectedContentSteeringObject = {
  1026. serverUri: '/foo?bar=00012',
  1027. pathwayId: 'CDN-A'
  1028. };
  1029. this.parser.push('#EXT-X-CONTENT-STEERING:SERVER-URI="/foo?bar=00012",PATHWAY-ID="CDN-A"');
  1030. this.parser.end();
  1031. assert.deepEqual(this.parser.manifest.contentSteering, expectedContentSteeringObject);
  1032. });
  1033. QUnit.test('parses #EXT-X-CONTENT-STEERING without PATHWAY-ID', function(assert) {
  1034. const expectedContentSteeringObject = {
  1035. serverUri: '/bar?foo=00012'
  1036. };
  1037. this.parser.push('#EXT-X-CONTENT-STEERING:SERVER-URI="/bar?foo=00012"');
  1038. this.parser.end();
  1039. assert.deepEqual(this.parser.manifest.contentSteering, expectedContentSteeringObject);
  1040. });
  1041. QUnit.test('warns on #EXT-X-CONTENT-STEERING missing SERVER-URI', function(assert) {
  1042. const warning = ['#EXT-X-CONTENT-STEERING lacks required attribute(s): SERVER-URI'];
  1043. this.parser.push('#EXT-X-CONTENT-STEERING:PATHWAY-ID="CDN-A"');
  1044. this.parser.end();
  1045. assert.deepEqual(this.warnings, warning, 'warnings as expected');
  1046. });
  1047. QUnit.module('integration');
  1048. for (const key in testDataExpected) {
  1049. if (!testDataManifests[key]) {
  1050. throw new Error(`${key}.js does not have an equivelent m3u8 manifest to test against`);
  1051. }
  1052. }
  1053. for (const key in testDataManifests) {
  1054. if (!testDataExpected[key]) {
  1055. throw new Error(`${key}.m3u8 does not have an equivelent expected js file to test against`);
  1056. }
  1057. QUnit.test(`parses ${key}.m3u8 as expected in ${key}.js`, function(assert) {
  1058. this.parser.push(testDataManifests[key]());
  1059. this.parser.end();
  1060. assert.deepEqual(
  1061. this.parser.manifest,
  1062. testDataExpected[key](),
  1063. key + '.m3u8 was parsed correctly'
  1064. );
  1065. });
  1066. }
  1067. });