playlist-loader.test.js 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808
  1. import QUnit from 'qunit';
  2. import {
  3. default as PlaylistLoader,
  4. updateSegments,
  5. updateMaster,
  6. setupMediaPlaylists,
  7. resolveMediaGroupUris,
  8. refreshDelay
  9. } from '../src/playlist-loader';
  10. import xhrFactory from '../src/xhr';
  11. import { useFakeEnvironment } from './test-helpers';
  12. import window from 'global/window';
  13. // Attempts to produce an absolute URL to a given relative path
  14. // based on window.location.href
  15. const urlTo = function(path) {
  16. return window.location.href
  17. .split('/')
  18. .slice(0, -1)
  19. .concat([path])
  20. .join('/');
  21. };
  22. QUnit.module('Playlist Loader', {
  23. beforeEach(assert) {
  24. this.env = useFakeEnvironment(assert);
  25. this.clock = this.env.clock;
  26. this.requests = this.env.requests;
  27. this.fakeHls = {
  28. xhr: xhrFactory()
  29. };
  30. },
  31. afterEach() {
  32. this.env.restore();
  33. }
  34. });
  35. QUnit.test('updateSegments copies over properties', function(assert) {
  36. assert.deepEqual(
  37. [
  38. { uri: 'test-uri-0', startTime: 0, endTime: 10 },
  39. {
  40. uri: 'test-uri-1',
  41. startTime: 10,
  42. endTime: 20,
  43. map: { someProp: 99, uri: '4' }
  44. }
  45. ],
  46. updateSegments(
  47. [
  48. { uri: 'test-uri-0', startTime: 0, endTime: 10 },
  49. { uri: 'test-uri-1', startTime: 10, endTime: 20, map: { someProp: 1 } }
  50. ],
  51. [
  52. { uri: 'test-uri-0' },
  53. { uri: 'test-uri-1', map: { someProp: 99, uri: '4' } }
  54. ],
  55. 0),
  56. 'retains properties from original segment');
  57. assert.deepEqual(
  58. [
  59. { uri: 'test-uri-0', map: { someProp: 100 } },
  60. { uri: 'test-uri-1', map: { someProp: 99, uri: '4' } }
  61. ],
  62. updateSegments(
  63. [
  64. { uri: 'test-uri-0' },
  65. { uri: 'test-uri-1', map: { someProp: 1 } }
  66. ],
  67. [
  68. { uri: 'test-uri-0', map: { someProp: 100 } },
  69. { uri: 'test-uri-1', map: { someProp: 99, uri: '4' } }
  70. ],
  71. 0),
  72. 'copies over/overwrites properties without offset');
  73. assert.deepEqual(
  74. [
  75. { uri: 'test-uri-1', map: { someProp: 1 } },
  76. { uri: 'test-uri-2', map: { someProp: 100, uri: '2' } }
  77. ],
  78. updateSegments(
  79. [
  80. { uri: 'test-uri-0' },
  81. { uri: 'test-uri-1', map: { someProp: 1 } }
  82. ],
  83. [
  84. { uri: 'test-uri-1' },
  85. { uri: 'test-uri-2', map: { someProp: 100, uri: '2' } }
  86. ],
  87. 1),
  88. 'copies over/overwrites properties with offset of 1');
  89. assert.deepEqual(
  90. [
  91. { uri: 'test-uri-2' },
  92. { uri: 'test-uri-3', map: { someProp: 100, uri: '2' } }
  93. ],
  94. updateSegments(
  95. [
  96. { uri: 'test-uri-0' },
  97. { uri: 'test-uri-1', map: { someProp: 1 } }
  98. ],
  99. [
  100. { uri: 'test-uri-2' },
  101. { uri: 'test-uri-3', map: { someProp: 100, uri: '2' } }
  102. ],
  103. 2),
  104. 'copies over/overwrites properties with offset of 2');
  105. });
  106. QUnit.test('updateMaster returns null when no playlists', function(assert) {
  107. const master = {
  108. playlists: []
  109. };
  110. const media = {};
  111. assert.deepEqual(updateMaster(master, media), null, 'returns null when no playlists');
  112. });
  113. QUnit.test('updateMaster returns null when no change', function(assert) {
  114. const master = {
  115. playlists: [{
  116. mediaSequence: 0,
  117. attributes: {
  118. BANDWIDTH: 9
  119. },
  120. uri: 'playlist-0-uri',
  121. resolvedUri: urlTo('playlist-0-uri'),
  122. segments: [{
  123. duration: 10,
  124. uri: 'segment-0-uri',
  125. resolvedUri: urlTo('segment-0-uri')
  126. }]
  127. }]
  128. };
  129. const media = {
  130. mediaSequence: 0,
  131. attributes: {
  132. BANDWIDTH: 9
  133. },
  134. uri: 'playlist-0-uri',
  135. segments: [{
  136. duration: 10,
  137. uri: 'segment-0-uri'
  138. }]
  139. };
  140. assert.deepEqual(updateMaster(master, media), null, 'returns null');
  141. });
  142. QUnit.test('updateMaster updates master when new media sequence', function(assert) {
  143. const master = {
  144. playlists: [{
  145. mediaSequence: 0,
  146. attributes: {
  147. BANDWIDTH: 9
  148. },
  149. uri: 'playlist-0-uri',
  150. resolvedUri: urlTo('playlist-0-uri'),
  151. segments: [{
  152. duration: 10,
  153. uri: 'segment-0-uri',
  154. resolvedUri: urlTo('segment-0-uri')
  155. }]
  156. }]
  157. };
  158. const media = {
  159. mediaSequence: 1,
  160. attributes: {
  161. BANDWIDTH: 9
  162. },
  163. uri: 'playlist-0-uri',
  164. segments: [{
  165. duration: 10,
  166. uri: 'segment-0-uri'
  167. }]
  168. };
  169. assert.deepEqual(
  170. updateMaster(master, media),
  171. {
  172. playlists: [{
  173. mediaSequence: 1,
  174. attributes: {
  175. BANDWIDTH: 9
  176. },
  177. uri: 'playlist-0-uri',
  178. resolvedUri: urlTo('playlist-0-uri'),
  179. segments: [{
  180. duration: 10,
  181. uri: 'segment-0-uri',
  182. resolvedUri: urlTo('segment-0-uri')
  183. }]
  184. }]
  185. },
  186. 'updates master when new media sequence');
  187. });
  188. QUnit.test('updateMaster retains top level values in master', function(assert) {
  189. const master = {
  190. mediaGroups: {
  191. AUDIO: {
  192. 'GROUP-ID': {
  193. default: true,
  194. uri: 'audio-uri'
  195. }
  196. }
  197. },
  198. playlists: [{
  199. mediaSequence: 0,
  200. attributes: {
  201. BANDWIDTH: 9
  202. },
  203. uri: 'playlist-0-uri',
  204. resolvedUri: urlTo('playlist-0-uri'),
  205. segments: [{
  206. duration: 10,
  207. uri: 'segment-0-uri',
  208. resolvedUri: urlTo('segment-0-uri')
  209. }]
  210. }]
  211. };
  212. const media = {
  213. mediaSequence: 1,
  214. attributes: {
  215. BANDWIDTH: 9
  216. },
  217. uri: 'playlist-0-uri',
  218. segments: [{
  219. duration: 10,
  220. uri: 'segment-0-uri'
  221. }]
  222. };
  223. assert.deepEqual(
  224. updateMaster(master, media),
  225. {
  226. mediaGroups: {
  227. AUDIO: {
  228. 'GROUP-ID': {
  229. default: true,
  230. uri: 'audio-uri'
  231. }
  232. }
  233. },
  234. playlists: [{
  235. mediaSequence: 1,
  236. attributes: {
  237. BANDWIDTH: 9
  238. },
  239. uri: 'playlist-0-uri',
  240. resolvedUri: urlTo('playlist-0-uri'),
  241. segments: [{
  242. duration: 10,
  243. uri: 'segment-0-uri',
  244. resolvedUri: urlTo('segment-0-uri')
  245. }]
  246. }]
  247. },
  248. 'retains top level values in master');
  249. });
  250. QUnit.test('updateMaster adds new segments to master', function(assert) {
  251. const master = {
  252. mediaGroups: {
  253. AUDIO: {
  254. 'GROUP-ID': {
  255. default: true,
  256. uri: 'audio-uri'
  257. }
  258. }
  259. },
  260. playlists: [{
  261. mediaSequence: 0,
  262. attributes: {
  263. BANDWIDTH: 9
  264. },
  265. uri: 'playlist-0-uri',
  266. resolvedUri: urlTo('playlist-0-uri'),
  267. segments: [{
  268. duration: 10,
  269. uri: 'segment-0-uri',
  270. resolvedUri: urlTo('segment-0-uri')
  271. }]
  272. }]
  273. };
  274. const media = {
  275. mediaSequence: 1,
  276. attributes: {
  277. BANDWIDTH: 9
  278. },
  279. uri: 'playlist-0-uri',
  280. segments: [{
  281. duration: 10,
  282. uri: 'segment-0-uri'
  283. }, {
  284. duration: 9,
  285. uri: 'segment-1-uri'
  286. }]
  287. };
  288. assert.deepEqual(
  289. updateMaster(master, media),
  290. {
  291. mediaGroups: {
  292. AUDIO: {
  293. 'GROUP-ID': {
  294. default: true,
  295. uri: 'audio-uri'
  296. }
  297. }
  298. },
  299. playlists: [{
  300. mediaSequence: 1,
  301. attributes: {
  302. BANDWIDTH: 9
  303. },
  304. uri: 'playlist-0-uri',
  305. resolvedUri: urlTo('playlist-0-uri'),
  306. segments: [{
  307. duration: 10,
  308. uri: 'segment-0-uri',
  309. resolvedUri: urlTo('segment-0-uri')
  310. }, {
  311. duration: 9,
  312. uri: 'segment-1-uri',
  313. resolvedUri: urlTo('segment-1-uri')
  314. }]
  315. }]
  316. },
  317. 'adds new segment to master');
  318. });
  319. QUnit.test('updateMaster changes old values', function(assert) {
  320. const master = {
  321. mediaGroups: {
  322. AUDIO: {
  323. 'GROUP-ID': {
  324. default: true,
  325. uri: 'audio-uri'
  326. }
  327. }
  328. },
  329. playlists: [{
  330. mediaSequence: 0,
  331. attributes: {
  332. BANDWIDTH: 9
  333. },
  334. uri: 'playlist-0-uri',
  335. resolvedUri: urlTo('playlist-0-uri'),
  336. segments: [{
  337. duration: 10,
  338. uri: 'segment-0-uri',
  339. resolvedUri: urlTo('segment-0-uri')
  340. }]
  341. }]
  342. };
  343. const media = {
  344. mediaSequence: 1,
  345. attributes: {
  346. BANDWIDTH: 8,
  347. newField: 1
  348. },
  349. uri: 'playlist-0-uri',
  350. segments: [{
  351. duration: 8,
  352. uri: 'segment-0-uri'
  353. }, {
  354. duration: 10,
  355. uri: 'segment-1-uri'
  356. }]
  357. };
  358. assert.deepEqual(
  359. updateMaster(master, media),
  360. {
  361. mediaGroups: {
  362. AUDIO: {
  363. 'GROUP-ID': {
  364. default: true,
  365. uri: 'audio-uri'
  366. }
  367. }
  368. },
  369. playlists: [{
  370. mediaSequence: 1,
  371. attributes: {
  372. BANDWIDTH: 8,
  373. newField: 1
  374. },
  375. uri: 'playlist-0-uri',
  376. resolvedUri: urlTo('playlist-0-uri'),
  377. segments: [{
  378. duration: 8,
  379. uri: 'segment-0-uri',
  380. resolvedUri: urlTo('segment-0-uri')
  381. }, {
  382. duration: 10,
  383. uri: 'segment-1-uri',
  384. resolvedUri: urlTo('segment-1-uri')
  385. }]
  386. }]
  387. },
  388. 'changes old values');
  389. });
  390. QUnit.test('updateMaster retains saved segment values', function(assert) {
  391. const master = {
  392. playlists: [{
  393. mediaSequence: 0,
  394. uri: 'playlist-0-uri',
  395. resolvedUri: urlTo('playlist-0-uri'),
  396. segments: [{
  397. duration: 10,
  398. uri: 'segment-0-uri',
  399. resolvedUri: urlTo('segment-0-uri'),
  400. startTime: 0,
  401. endTime: 10
  402. }]
  403. }]
  404. };
  405. const media = {
  406. mediaSequence: 0,
  407. uri: 'playlist-0-uri',
  408. segments: [{
  409. duration: 8,
  410. uri: 'segment-0-uri'
  411. }, {
  412. duration: 10,
  413. uri: 'segment-1-uri'
  414. }]
  415. };
  416. assert.deepEqual(
  417. updateMaster(master, media),
  418. {
  419. playlists: [{
  420. mediaSequence: 0,
  421. uri: 'playlist-0-uri',
  422. resolvedUri: urlTo('playlist-0-uri'),
  423. segments: [{
  424. duration: 8,
  425. uri: 'segment-0-uri',
  426. resolvedUri: urlTo('segment-0-uri'),
  427. startTime: 0,
  428. endTime: 10
  429. }, {
  430. duration: 10,
  431. uri: 'segment-1-uri',
  432. resolvedUri: urlTo('segment-1-uri')
  433. }]
  434. }]
  435. },
  436. 'retains saved segment values');
  437. });
  438. QUnit.test('updateMaster resolves key and map URIs', function(assert) {
  439. const master = {
  440. playlists: [{
  441. mediaSequence: 0,
  442. attributes: {
  443. BANDWIDTH: 9
  444. },
  445. uri: 'playlist-0-uri',
  446. resolvedUri: urlTo('playlist-0-uri'),
  447. segments: [{
  448. duration: 10,
  449. uri: 'segment-0-uri',
  450. resolvedUri: urlTo('segment-0-uri')
  451. }, {
  452. duration: 10,
  453. uri: 'segment-1-uri',
  454. resolvedUri: urlTo('segment-1-uri')
  455. }]
  456. }]
  457. };
  458. const media = {
  459. mediaSequence: 3,
  460. attributes: {
  461. BANDWIDTH: 9
  462. },
  463. uri: 'playlist-0-uri',
  464. segments: [{
  465. duration: 9,
  466. uri: 'segment-2-uri',
  467. key: {
  468. uri: 'key-2-uri'
  469. },
  470. map: {
  471. uri: 'map-2-uri'
  472. }
  473. }, {
  474. duration: 11,
  475. uri: 'segment-3-uri',
  476. key: {
  477. uri: 'key-3-uri'
  478. },
  479. map: {
  480. uri: 'map-3-uri'
  481. }
  482. }]
  483. };
  484. assert.deepEqual(
  485. updateMaster(master, media),
  486. {
  487. playlists: [{
  488. mediaSequence: 3,
  489. attributes: {
  490. BANDWIDTH: 9
  491. },
  492. uri: 'playlist-0-uri',
  493. resolvedUri: urlTo('playlist-0-uri'),
  494. segments: [{
  495. duration: 9,
  496. uri: 'segment-2-uri',
  497. resolvedUri: urlTo('segment-2-uri'),
  498. key: {
  499. uri: 'key-2-uri',
  500. resolvedUri: urlTo('key-2-uri')
  501. },
  502. map: {
  503. uri: 'map-2-uri',
  504. resolvedUri: urlTo('map-2-uri')
  505. }
  506. }, {
  507. duration: 11,
  508. uri: 'segment-3-uri',
  509. resolvedUri: urlTo('segment-3-uri'),
  510. key: {
  511. uri: 'key-3-uri',
  512. resolvedUri: urlTo('key-3-uri')
  513. },
  514. map: {
  515. uri: 'map-3-uri',
  516. resolvedUri: urlTo('map-3-uri')
  517. }
  518. }]
  519. }]
  520. },
  521. 'resolves key and map URIs');
  522. });
  523. QUnit.test('setupMediaPlaylists does nothing if no playlists', function(assert) {
  524. const master = {
  525. playlists: []
  526. };
  527. setupMediaPlaylists(master);
  528. assert.deepEqual(master, {
  529. playlists: []
  530. }, 'master remains unchanged');
  531. });
  532. QUnit.test('setupMediaPlaylists adds URI keys for each playlist', function(assert) {
  533. const master = {
  534. uri: 'master-uri',
  535. playlists: [{
  536. uri: 'uri-0'
  537. }, {
  538. uri: 'uri-1'
  539. }]
  540. };
  541. const expectedPlaylist0 = {
  542. attributes: {},
  543. resolvedUri: urlTo('uri-0'),
  544. uri: 'uri-0'
  545. };
  546. const expectedPlaylist1 = {
  547. attributes: {},
  548. resolvedUri: urlTo('uri-1'),
  549. uri: 'uri-1'
  550. };
  551. setupMediaPlaylists(master);
  552. assert.deepEqual(master.playlists[0], expectedPlaylist0, 'retained playlist indices');
  553. assert.deepEqual(master.playlists[1], expectedPlaylist1, 'retained playlist indices');
  554. assert.deepEqual(master.playlists['uri-0'], expectedPlaylist0, 'added playlist key');
  555. assert.deepEqual(master.playlists['uri-1'], expectedPlaylist1, 'added playlist key');
  556. assert.equal(this.env.log.warn.calls, 2, 'logged two warnings');
  557. assert.equal(this.env.log.warn.args[0],
  558. 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
  559. 'logged a warning');
  560. assert.equal(this.env.log.warn.args[1],
  561. 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
  562. 'logged a warning');
  563. });
  564. QUnit.test('setupMediaPlaylists adds attributes objects if missing', function(assert) {
  565. const master = {
  566. uri: 'master-uri',
  567. playlists: [{
  568. uri: 'uri-0'
  569. }, {
  570. uri: 'uri-1'
  571. }]
  572. };
  573. setupMediaPlaylists(master);
  574. assert.ok(master.playlists[0].attributes, 'added attributes object');
  575. assert.ok(master.playlists[1].attributes, 'added attributes object');
  576. assert.equal(this.env.log.warn.calls, 2, 'logged two warnings');
  577. assert.equal(this.env.log.warn.args[0],
  578. 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
  579. 'logged a warning');
  580. assert.equal(this.env.log.warn.args[1],
  581. 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
  582. 'logged a warning');
  583. });
  584. QUnit.test('setupMediaPlaylists resolves playlist URIs', function(assert) {
  585. const master = {
  586. uri: 'master-uri',
  587. playlists: [{
  588. attributes: { BANDWIDTH: 10 },
  589. uri: 'uri-0'
  590. }, {
  591. attributes: { BANDWIDTH: 100 },
  592. uri: 'uri-1'
  593. }]
  594. };
  595. setupMediaPlaylists(master);
  596. assert.equal(master.playlists[0].resolvedUri, urlTo('uri-0'), 'resolves URI');
  597. assert.equal(master.playlists[1].resolvedUri, urlTo('uri-1'), 'resolves URI');
  598. });
  599. QUnit.test('resolveMediaGroupUris does nothing when no media groups', function(assert) {
  600. const master = {
  601. uri: 'master-uri',
  602. playlists: [],
  603. mediaGroups: []
  604. };
  605. resolveMediaGroupUris(master);
  606. assert.deepEqual(master, {
  607. uri: 'master-uri',
  608. playlists: [],
  609. mediaGroups: []
  610. }, 'does nothing when no media groups');
  611. });
  612. QUnit.test('resolveMediaGroupUris resolves media group URIs', function(assert) {
  613. const master = {
  614. uri: 'master-uri',
  615. playlists: [{
  616. attributes: { BANDWIDTH: 10 },
  617. uri: 'playlist-0'
  618. }],
  619. mediaGroups: {
  620. // CLOSED-CAPTIONS will never have a URI
  621. 'CLOSED-CAPTIONS': {
  622. cc1: {
  623. English: {}
  624. }
  625. },
  626. 'AUDIO': {
  627. low: {
  628. // audio doesn't need a URI if it is a label for muxed
  629. main: {},
  630. commentary: {
  631. uri: 'audio-low-commentary-uri'
  632. }
  633. },
  634. high: {
  635. main: {},
  636. commentary: {
  637. uri: 'audio-high-commentary-uri'
  638. }
  639. }
  640. },
  641. 'SUBTITLES': {
  642. sub1: {
  643. english: {
  644. uri: 'subtitles-1-english-uri'
  645. },
  646. spanish: {
  647. uri: 'subtitles-1-spanish-uri'
  648. }
  649. },
  650. sub2: {
  651. english: {
  652. uri: 'subtitles-2-english-uri'
  653. },
  654. spanish: {
  655. uri: 'subtitles-2-spanish-uri'
  656. }
  657. },
  658. sub3: {
  659. english: {
  660. uri: 'subtitles-3-english-uri'
  661. },
  662. spanish: {
  663. uri: 'subtitles-3-spanish-uri'
  664. }
  665. }
  666. }
  667. }
  668. };
  669. resolveMediaGroupUris(master);
  670. assert.deepEqual(master, {
  671. uri: 'master-uri',
  672. playlists: [{
  673. attributes: { BANDWIDTH: 10 },
  674. uri: 'playlist-0'
  675. }],
  676. mediaGroups: {
  677. // CLOSED-CAPTIONS will never have a URI
  678. 'CLOSED-CAPTIONS': {
  679. cc1: {
  680. English: {}
  681. }
  682. },
  683. 'AUDIO': {
  684. low: {
  685. // audio doesn't need a URI if it is a label for muxed
  686. main: {},
  687. commentary: {
  688. uri: 'audio-low-commentary-uri',
  689. resolvedUri: urlTo('audio-low-commentary-uri')
  690. }
  691. },
  692. high: {
  693. main: {},
  694. commentary: {
  695. uri: 'audio-high-commentary-uri',
  696. resolvedUri: urlTo('audio-high-commentary-uri')
  697. }
  698. }
  699. },
  700. 'SUBTITLES': {
  701. sub1: {
  702. english: {
  703. uri: 'subtitles-1-english-uri',
  704. resolvedUri: urlTo('subtitles-1-english-uri')
  705. },
  706. spanish: {
  707. uri: 'subtitles-1-spanish-uri',
  708. resolvedUri: urlTo('subtitles-1-spanish-uri')
  709. }
  710. },
  711. sub2: {
  712. english: {
  713. uri: 'subtitles-2-english-uri',
  714. resolvedUri: urlTo('subtitles-2-english-uri')
  715. },
  716. spanish: {
  717. uri: 'subtitles-2-spanish-uri',
  718. resolvedUri: urlTo('subtitles-2-spanish-uri')
  719. }
  720. },
  721. sub3: {
  722. english: {
  723. uri: 'subtitles-3-english-uri',
  724. resolvedUri: urlTo('subtitles-3-english-uri')
  725. },
  726. spanish: {
  727. uri: 'subtitles-3-spanish-uri',
  728. resolvedUri: urlTo('subtitles-3-spanish-uri')
  729. }
  730. }
  731. }
  732. }
  733. }, 'resolved URIs of certain media groups');
  734. });
  735. QUnit.test('uses last segment duration for refresh delay', function(assert) {
  736. const media = { targetDuration: 7, segments: [] };
  737. assert.equal(refreshDelay(media, true), 3500,
  738. 'used half targetDuration when no segments');
  739. media.segments = [ { duration: 6}, { duration: 4 }, { } ];
  740. assert.equal(refreshDelay(media, true), 3500,
  741. 'used half targetDuration when last segment duration cannot be determined');
  742. media.segments = [ { duration: 6}, { duration: 4}, { duration: 5 } ];
  743. assert.equal(refreshDelay(media, true), 5000, 'used last segment duration for delay');
  744. assert.equal(refreshDelay(media, false), 3500,
  745. 'used half targetDuration when update is false');
  746. });
  747. QUnit.test('throws if the playlist url is empty or undefined', function(assert) {
  748. assert.throws(function() {
  749. PlaylistLoader();
  750. }, 'requires an argument');
  751. assert.throws(function() {
  752. PlaylistLoader('');
  753. }, 'does not accept the empty string');
  754. });
  755. QUnit.test('starts without any metadata', function(assert) {
  756. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  757. loader.load();
  758. assert.strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
  759. });
  760. QUnit.test('requests the initial playlist immediately', function(assert) {
  761. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  762. loader.load();
  763. assert.strictEqual(this.requests.length, 1, 'made a request');
  764. assert.strictEqual(this.requests[0].url,
  765. 'master.m3u8',
  766. 'requested the initial playlist');
  767. });
  768. QUnit.test('moves to HAVE_MASTER after loading a master playlist', function(assert) {
  769. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  770. let state;
  771. loader.load();
  772. loader.on('loadedplaylist', function() {
  773. state = loader.state;
  774. });
  775. this.requests.pop().respond(200, null,
  776. '#EXTM3U\n' +
  777. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  778. 'media.m3u8\n');
  779. assert.ok(loader.master, 'the master playlist is available');
  780. assert.strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct');
  781. });
  782. QUnit.test('logs warning for master playlist with invalid STREAM-INF', function(assert) {
  783. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  784. loader.load();
  785. this.requests.pop().respond(200, null,
  786. '#EXTM3U\n' +
  787. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  788. 'video1/media.m3u8\n' +
  789. '#EXT-X-STREAM-INF:\n' +
  790. 'video2/media.m3u8\n');
  791. assert.ok(loader.master, 'infers a master playlist');
  792. assert.equal(loader.master.playlists[1].uri, 'video2/media.m3u8',
  793. 'parsed invalid stream');
  794. assert.ok(loader.master.playlists[1].attributes, 'attached attributes property');
  795. assert.equal(this.env.log.warn.calls, 1, 'logged a warning');
  796. assert.equal(this.env.log.warn.args[0],
  797. 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
  798. 'logged a warning');
  799. });
  800. QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist',
  801. function(assert) {
  802. let loadedmetadatas = 0;
  803. let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
  804. loader.load();
  805. loader.on('loadedmetadata', function() {
  806. loadedmetadatas++;
  807. });
  808. this.requests.pop().respond(200, null,
  809. '#EXTM3U\n' +
  810. '#EXTINF:10,\n' +
  811. '0.ts\n' +
  812. '#EXT-X-ENDLIST\n');
  813. assert.ok(loader.master, 'infers a master playlist');
  814. assert.ok(loader.media(), 'sets the media playlist');
  815. assert.ok(loader.media().uri, 'sets the media playlist URI');
  816. assert.ok(loader.media().attributes, 'sets the media playlist attributes');
  817. assert.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
  818. assert.strictEqual(this.requests.length, 0, 'no more requests are made');
  819. assert.strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata');
  820. });
  821. QUnit.test('resolves relative media playlist URIs', function(assert) {
  822. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  823. loader.load();
  824. this.requests.shift().respond(200, null,
  825. '#EXTM3U\n' +
  826. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  827. 'video/media.m3u8\n');
  828. assert.equal(loader.master.playlists[0].resolvedUri, urlTo('video/media.m3u8'),
  829. 'resolved media URI');
  830. });
  831. QUnit.test('resolves media initialization segment URIs', function(assert) {
  832. let loader = new PlaylistLoader('video/fmp4.m3u8', this.fakeHls);
  833. loader.load();
  834. this.requests.shift().respond(200, null,
  835. '#EXTM3U\n' +
  836. '#EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"\n' +
  837. '#EXTINF:10,\n' +
  838. '0.ts\n' +
  839. '#EXT-X-ENDLIST\n');
  840. assert.equal(loader.media().segments[0].map.resolvedUri, urlTo('video/main.mp4'),
  841. 'resolved init segment URI');
  842. });
  843. QUnit.test('recognizes absolute URIs and requests them unmodified', function(assert) {
  844. let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls);
  845. loader.load();
  846. this.requests.shift().respond(200, null,
  847. '#EXTM3U\n' +
  848. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  849. 'http://example.com/video/media.m3u8\n');
  850. assert.equal(loader.master.playlists[0].resolvedUri,
  851. 'http://example.com/video/media.m3u8', 'resolved media URI');
  852. this.requests.shift().respond(200, null,
  853. '#EXTM3U\n' +
  854. '#EXTINF:10,\n' +
  855. 'http://example.com/00001.ts\n' +
  856. '#EXT-X-ENDLIST\n');
  857. assert.equal(loader.media().segments[0].resolvedUri,
  858. 'http://example.com/00001.ts', 'resolved segment URI');
  859. });
  860. QUnit.test('recognizes domain-relative URLs', function(assert) {
  861. let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls);
  862. loader.load();
  863. this.requests.shift().respond(200, null,
  864. '#EXTM3U\n' +
  865. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  866. '/media.m3u8\n');
  867. assert.equal(loader.master.playlists[0].resolvedUri,
  868. window.location.protocol + '//' +
  869. window.location.host + '/media.m3u8',
  870. 'resolved media URI');
  871. this.requests.shift().respond(200, null,
  872. '#EXTM3U\n' +
  873. '#EXTINF:10,\n' +
  874. '/00001.ts\n' +
  875. '#EXT-X-ENDLIST\n');
  876. assert.equal(loader.media().segments[0].resolvedUri,
  877. window.location.protocol + '//' +
  878. window.location.host + '/00001.ts',
  879. 'resolved segment URI');
  880. });
  881. QUnit.test('recognizes redirect, when manifest requested', function(assert) {
  882. let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls, {
  883. handleManifestRedirects: true
  884. });
  885. loader.load();
  886. const manifestRequest = this.requests.shift();
  887. manifestRequest.responseURL = window.location.protocol + '//' +
  888. 'foo-bar.com/manifest/media.m3u8';
  889. manifestRequest.respond(200, null,
  890. '#EXTM3U\n' +
  891. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  892. '/media.m3u8\n');
  893. assert.equal(loader.master.playlists[0].resolvedUri,
  894. window.location.protocol + '//' +
  895. 'foo-bar.com/media.m3u8',
  896. 'resolved media URI');
  897. this.requests.shift().respond(200, null,
  898. '#EXTM3U\n' +
  899. '#EXTINF:10,\n' +
  900. '/00001.ts\n' +
  901. '#EXT-X-ENDLIST\n');
  902. assert.equal(loader.media().segments[0].resolvedUri,
  903. window.location.protocol + '//' +
  904. 'foo-bar.com/00001.ts',
  905. 'resolved segment URI');
  906. });
  907. QUnit.test('recognizes redirect, when media requested', function(assert) {
  908. let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls, {
  909. handleManifestRedirects: true
  910. });
  911. loader.load();
  912. this.requests.shift().respond(200, null,
  913. '#EXTM3U\n' +
  914. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  915. '/media.m3u8\n');
  916. assert.equal(loader.master.playlists[0].resolvedUri,
  917. window.location.protocol + '//' +
  918. window.location.host + '/media.m3u8',
  919. 'resolved media URI');
  920. const mediaRequest = this.requests.shift();
  921. mediaRequest.responseURL = window.location.protocol + '//' +
  922. 'foo-bar.com/media.m3u8';
  923. mediaRequest.respond(200, null,
  924. '#EXTM3U\n' +
  925. '#EXTINF:10,\n' +
  926. '/00001.ts\n' +
  927. '#EXT-X-ENDLIST\n');
  928. assert.equal(loader.media().segments[0].resolvedUri,
  929. window.location.protocol + '//' +
  930. 'foo-bar.com/00001.ts',
  931. 'resolved segment URI');
  932. });
  933. QUnit.test('recognizes key URLs relative to master and playlist', function(assert) {
  934. let loader = new PlaylistLoader('/video/media-encrypted.m3u8', this.fakeHls);
  935. loader.load();
  936. this.requests.shift().respond(200, null,
  937. '#EXTM3U\n' +
  938. '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
  939. 'playlist/playlist.m3u8\n' +
  940. '#EXT-X-ENDLIST\n');
  941. assert.equal(loader.master.playlists[0].resolvedUri,
  942. window.location.protocol + '//' +
  943. window.location.host + '/video/playlist/playlist.m3u8',
  944. 'resolved media URI');
  945. this.requests.shift().respond(200, null,
  946. '#EXTM3U\n' +
  947. '#EXT-X-TARGETDURATION:15\n' +
  948. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
  949. '#EXTINF:2.833,\n' +
  950. 'http://example.com/000001.ts\n' +
  951. '#EXT-X-ENDLIST\n');
  952. assert.equal(loader.media().segments[0].key.resolvedUri,
  953. window.location.protocol + '//' +
  954. window.location.host + '/video/playlist/keys/key.php',
  955. 'resolved multiple relative paths for key URI');
  956. });
  957. QUnit.test('trigger an error event when a media playlist 404s', function(assert) {
  958. let count = 0;
  959. let loader = new PlaylistLoader('manifest/master.m3u8', this.fakeHls);
  960. loader.load();
  961. loader.on('error', function() {
  962. count += 1;
  963. });
  964. // master
  965. this.requests.shift().respond(200, null,
  966. '#EXTM3U\n' +
  967. '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
  968. 'playlist/playlist.m3u8\n' +
  969. '#EXT-X-STREAM-INF:PROGRAM-ID=2,BANDWIDTH=170\n' +
  970. 'playlist/playlist2.m3u8\n' +
  971. '#EXT-X-ENDLIST\n');
  972. assert.equal(count, 0,
  973. 'error not triggered before requesting playlist');
  974. // playlist
  975. this.requests.shift().respond(404);
  976. assert.equal(count, 1,
  977. 'error triggered after playlist 404');
  978. });
  979. QUnit.test('recognizes absolute key URLs', function(assert) {
  980. let loader = new PlaylistLoader('/video/media-encrypted.m3u8', this.fakeHls);
  981. loader.load();
  982. this.requests.shift().respond(200, null,
  983. '#EXTM3U\n' +
  984. '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
  985. 'playlist/playlist.m3u8\n' +
  986. '#EXT-X-ENDLIST\n');
  987. assert.equal(loader.master.playlists[0].resolvedUri,
  988. window.location.protocol + '//' +
  989. window.location.host + '/video/playlist/playlist.m3u8',
  990. 'resolved media URI');
  991. this.requests.shift().respond(
  992. 200,
  993. null,
  994. '#EXTM3U\n' +
  995. '#EXT-X-TARGETDURATION:15\n' +
  996. '#EXT-X-KEY:METHOD=AES-128,URI="http://example.com/keys/key.php"\n' +
  997. '#EXTINF:2.833,\n' +
  998. 'http://example.com/000001.ts\n' +
  999. '#EXT-X-ENDLIST\n'
  1000. );
  1001. assert.equal(loader.media().segments[0].key.resolvedUri,
  1002. 'http://example.com/keys/key.php', 'resolved absolute path for key URI');
  1003. });
  1004. QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist',
  1005. function(assert) {
  1006. let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
  1007. loader.load();
  1008. this.requests.pop().respond(200, null,
  1009. '#EXTM3U\n' +
  1010. '#EXTINF:10,\n' +
  1011. '0.ts\n');
  1012. assert.ok(loader.master, 'infers a master playlist');
  1013. assert.ok(loader.media(), 'sets the media playlist');
  1014. assert.ok(loader.media().attributes, 'sets the media playlist attributes');
  1015. assert.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
  1016. });
  1017. QUnit.test('moves to HAVE_METADATA after loading a media playlist', function(assert) {
  1018. let loadedPlaylist = 0;
  1019. let loadedMetadata = 0;
  1020. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1021. loader.load();
  1022. loader.on('loadedplaylist', function() {
  1023. loadedPlaylist++;
  1024. });
  1025. loader.on('loadedmetadata', function() {
  1026. loadedMetadata++;
  1027. });
  1028. this.requests.pop().respond(200, null,
  1029. '#EXTM3U\n' +
  1030. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1031. 'media.m3u8\n' +
  1032. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1033. 'alt.m3u8\n');
  1034. assert.strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once');
  1035. assert.strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata');
  1036. assert.strictEqual(this.requests.length, 1, 'requests the media playlist');
  1037. assert.strictEqual(this.requests[0].method, 'GET', 'GETs the media playlist');
  1038. assert.strictEqual(this.requests[0].url,
  1039. urlTo('media.m3u8'),
  1040. 'requests the first playlist');
  1041. this.requests.pop().respond(200, null,
  1042. '#EXTM3U\n' +
  1043. '#EXTINF:10,\n' +
  1044. '0.ts\n');
  1045. assert.ok(loader.master, 'sets the master playlist');
  1046. assert.ok(loader.media(), 'sets the media playlist');
  1047. assert.strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice');
  1048. assert.strictEqual(loadedMetadata, 1, 'fired loadedmetadata once');
  1049. assert.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
  1050. });
  1051. QUnit.test('defaults missing media groups for a media playlist', function(assert) {
  1052. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1053. loader.load();
  1054. this.requests.pop().respond(200, null,
  1055. '#EXTM3U\n' +
  1056. '#EXTINF:10,\n' +
  1057. '0.ts\n');
  1058. assert.ok(loader.master.mediaGroups.AUDIO, 'defaulted audio');
  1059. assert.ok(loader.master.mediaGroups.VIDEO, 'defaulted video');
  1060. assert.ok(loader.master.mediaGroups['CLOSED-CAPTIONS'], 'defaulted closed captions');
  1061. assert.ok(loader.master.mediaGroups.SUBTITLES, 'defaulted subtitles');
  1062. });
  1063. QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist',
  1064. function(assert) {
  1065. let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
  1066. loader.load();
  1067. this.requests.pop().respond(200, null,
  1068. '#EXTM3U\n' +
  1069. '#EXTINF:10,\n' +
  1070. '0.ts\n');
  1071. // 10s, one target duration
  1072. this.clock.tick(10 * 1000);
  1073. assert.strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct');
  1074. assert.strictEqual(this.requests.length, 1, 'requested playlist');
  1075. assert.strictEqual(this.requests[0].url,
  1076. urlTo('live.m3u8'),
  1077. 'refreshes the media playlist');
  1078. });
  1079. QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function(assert) {
  1080. let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
  1081. loader.load();
  1082. this.requests.pop().respond(200, null,
  1083. '#EXTM3U\n' +
  1084. '#EXTINF:10,\n' +
  1085. '0.ts\n');
  1086. // 10s, one target duration
  1087. this.clock.tick(10 * 1000);
  1088. this.requests.pop().respond(200, null,
  1089. '#EXTM3U\n' +
  1090. '#EXTINF:10,\n' +
  1091. '1.ts\n');
  1092. assert.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
  1093. });
  1094. QUnit.test('refreshes the playlist after last segment duration', function(assert) {
  1095. let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
  1096. let refreshes = 0;
  1097. loader.on('mediaupdatetimeout', () => refreshes++);
  1098. loader.load();
  1099. this.requests.pop().respond(200, null,
  1100. '#EXTM3U\n' +
  1101. '#EXT-X-TARGETDURATION:10\n' +
  1102. '#EXTINF:10,\n' +
  1103. '0.ts\n' +
  1104. '#EXTINF:4\n' +
  1105. '1.ts\n');
  1106. // 4s, last segment duration
  1107. this.clock.tick(4 * 1000);
  1108. assert.equal(refreshes, 1, 'refreshed playlist after last segment duration');
  1109. });
  1110. QUnit.test('emits an error when an initial playlist request fails', function(assert) {
  1111. let errors = [];
  1112. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1113. loader.load();
  1114. loader.on('error', function() {
  1115. errors.push(loader.error);
  1116. });
  1117. this.requests.pop().respond(500);
  1118. assert.strictEqual(errors.length, 1, 'emitted one error');
  1119. assert.strictEqual(errors[0].status, 500, 'http status is captured');
  1120. });
  1121. QUnit.test('errors when an initial media playlist request fails', function(assert) {
  1122. let errors = [];
  1123. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1124. loader.load();
  1125. loader.on('error', function() {
  1126. errors.push(loader.error);
  1127. });
  1128. this.requests.pop().respond(200, null,
  1129. '#EXTM3U\n' +
  1130. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1131. 'media.m3u8\n');
  1132. assert.strictEqual(errors.length, 0, 'emitted no errors');
  1133. this.requests.pop().respond(500);
  1134. assert.strictEqual(errors.length, 1, 'emitted one error');
  1135. assert.strictEqual(errors[0].status, 500, 'http status is captured');
  1136. });
  1137. // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
  1138. QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload',
  1139. function(assert) {
  1140. let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
  1141. loader.load();
  1142. this.requests.pop().respond(200, null,
  1143. '#EXTM3U\n' +
  1144. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1145. '#EXTINF:10,\n' +
  1146. '0.ts\n');
  1147. // trigger a refresh
  1148. this.clock.tick(10 * 1000);
  1149. this.requests.pop().respond(200, null,
  1150. '#EXTM3U\n' +
  1151. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1152. '#EXTINF:10,\n' +
  1153. '0.ts\n');
  1154. // half the default target-duration
  1155. this.clock.tick(5 * 1000);
  1156. assert.strictEqual(this.requests.length, 1, 'sent a request');
  1157. assert.strictEqual(this.requests[0].url,
  1158. urlTo('live.m3u8'),
  1159. 'requested the media playlist');
  1160. });
  1161. QUnit.test('preserves segment metadata across playlist refreshes', function(assert) {
  1162. let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
  1163. let segment;
  1164. loader.load();
  1165. this.requests.pop().respond(200, null,
  1166. '#EXTM3U\n' +
  1167. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1168. '#EXTINF:10,\n' +
  1169. '0.ts\n' +
  1170. '#EXTINF:10,\n' +
  1171. '1.ts\n' +
  1172. '#EXTINF:10,\n' +
  1173. '2.ts\n');
  1174. // add PTS info to 1.ts
  1175. segment = loader.media().segments[1];
  1176. segment.minVideoPts = 14;
  1177. segment.maxAudioPts = 27;
  1178. segment.preciseDuration = 10.045;
  1179. // trigger a refresh
  1180. this.clock.tick(10 * 1000);
  1181. this.requests.pop().respond(200, null,
  1182. '#EXTM3U\n' +
  1183. '#EXT-X-MEDIA-SEQUENCE:1\n' +
  1184. '#EXTINF:10,\n' +
  1185. '1.ts\n' +
  1186. '#EXTINF:10,\n' +
  1187. '2.ts\n');
  1188. assert.deepEqual(loader.media().segments[0], segment, 'preserved segment attributes');
  1189. });
  1190. QUnit.test('clears the update timeout when switching quality', function(assert) {
  1191. let loader = new PlaylistLoader('live-master.m3u8', this.fakeHls);
  1192. let refreshes = 0;
  1193. loader.load();
  1194. // track the number of playlist refreshes triggered
  1195. loader.on('mediaupdatetimeout', function() {
  1196. refreshes++;
  1197. });
  1198. // deliver the master
  1199. this.requests.pop().respond(200, null,
  1200. '#EXTM3U\n' +
  1201. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1202. 'live-low.m3u8\n' +
  1203. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1204. 'live-high.m3u8\n');
  1205. // deliver the low quality playlist
  1206. this.requests.pop().respond(200, null,
  1207. '#EXTM3U\n' +
  1208. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1209. '#EXTINF:10,\n' +
  1210. 'low-0.ts\n');
  1211. // change to a higher quality playlist
  1212. loader.media('live-high.m3u8');
  1213. this.requests.pop().respond(200, null,
  1214. '#EXTM3U\n' +
  1215. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1216. '#EXTINF:10,\n' +
  1217. 'high-0.ts\n');
  1218. // trigger a refresh
  1219. this.clock.tick(10 * 1000);
  1220. assert.equal(1, refreshes, 'only one refresh was triggered');
  1221. });
  1222. QUnit.test('media-sequence updates are considered a playlist change', function(assert) {
  1223. let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
  1224. loader.load();
  1225. this.requests.pop().respond(200, null,
  1226. '#EXTM3U\n' +
  1227. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1228. '#EXTINF:10,\n' +
  1229. '0.ts\n');
  1230. // trigger a refresh
  1231. this.clock.tick(10 * 1000);
  1232. this.requests.pop().respond(200, null,
  1233. '#EXTM3U\n' +
  1234. '#EXT-X-MEDIA-SEQUENCE:1\n' +
  1235. '#EXTINF:10,\n' +
  1236. '0.ts\n');
  1237. // half the default target-duration
  1238. this.clock.tick(5 * 1000);
  1239. assert.strictEqual(this.requests.length, 0, 'no request is sent');
  1240. });
  1241. QUnit.test('emits an error if a media refresh fails', function(assert) {
  1242. let errors = 0;
  1243. let errorResponseText = 'custom error message';
  1244. let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
  1245. loader.load();
  1246. loader.on('error', function() {
  1247. errors++;
  1248. });
  1249. this.requests.pop().respond(200, null,
  1250. '#EXTM3U\n' +
  1251. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1252. '#EXTINF:10,\n' +
  1253. '0.ts\n');
  1254. // trigger a refresh
  1255. this.clock.tick(10 * 1000);
  1256. this.requests.pop().respond(500, null, errorResponseText);
  1257. assert.strictEqual(errors, 1, 'emitted an error');
  1258. assert.strictEqual(loader.error.status, 500, 'captured the status code');
  1259. assert.strictEqual(loader.error.responseText,
  1260. errorResponseText,
  1261. 'captured the responseText');
  1262. });
  1263. QUnit.test('switches media playlists when requested', function(assert) {
  1264. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1265. loader.load();
  1266. this.requests.pop().respond(200, null,
  1267. '#EXTM3U\n' +
  1268. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1269. 'low.m3u8\n' +
  1270. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1271. 'high.m3u8\n');
  1272. this.requests.pop().respond(200, null,
  1273. '#EXTM3U\n' +
  1274. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1275. '#EXTINF:10,\n' +
  1276. 'low-0.ts\n');
  1277. loader.media(loader.master.playlists[1]);
  1278. assert.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
  1279. this.requests.pop().respond(200, null,
  1280. '#EXTM3U\n' +
  1281. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1282. '#EXTINF:10,\n' +
  1283. 'high-0.ts\n');
  1284. assert.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
  1285. assert.strictEqual(loader.media(),
  1286. loader.master.playlists[1],
  1287. 'updated the active media');
  1288. });
  1289. QUnit.test('can switch playlists immediately after the master is downloaded',
  1290. function(assert) {
  1291. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1292. loader.load();
  1293. loader.on('loadedplaylist', function() {
  1294. loader.media('high.m3u8');
  1295. });
  1296. this.requests.pop().respond(200, null,
  1297. '#EXTM3U\n' +
  1298. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1299. 'low.m3u8\n' +
  1300. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1301. 'high.m3u8\n');
  1302. assert.equal(this.requests[0].url, urlTo('high.m3u8'), 'switched variants immediately');
  1303. });
  1304. QUnit.test('can switch media playlists based on URI', function(assert) {
  1305. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1306. loader.load();
  1307. this.requests.pop().respond(200, null,
  1308. '#EXTM3U\n' +
  1309. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1310. 'low.m3u8\n' +
  1311. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1312. 'high.m3u8\n');
  1313. this.requests.pop().respond(200, null,
  1314. '#EXTM3U\n' +
  1315. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1316. '#EXTINF:10,\n' +
  1317. 'low-0.ts\n');
  1318. loader.media('high.m3u8');
  1319. assert.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
  1320. this.requests.pop().respond(200, null,
  1321. '#EXTM3U\n' +
  1322. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1323. '#EXTINF:10,\n' +
  1324. 'high-0.ts\n');
  1325. assert.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
  1326. assert.strictEqual(loader.media(),
  1327. loader.master.playlists[1],
  1328. 'updated the active media');
  1329. });
  1330. QUnit.test('aborts in-flight playlist refreshes when switching', function(assert) {
  1331. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1332. loader.load();
  1333. this.requests.pop().respond(200, null,
  1334. '#EXTM3U\n' +
  1335. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1336. 'low.m3u8\n' +
  1337. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1338. 'high.m3u8\n');
  1339. this.requests.pop().respond(200, null,
  1340. '#EXTM3U\n' +
  1341. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1342. '#EXTINF:10,\n' +
  1343. 'low-0.ts\n');
  1344. this.clock.tick(10 * 1000);
  1345. loader.media('high.m3u8');
  1346. assert.strictEqual(this.requests[0].aborted, true, 'aborted refresh request');
  1347. assert.ok(!this.requests[0].onreadystatechange,
  1348. 'onreadystatechange handlers should be removed on abort');
  1349. assert.strictEqual(loader.state,
  1350. 'HAVE_METADATA',
  1351. 'the state is set accoring to the startingState');
  1352. });
  1353. QUnit.test('switching to the active playlist is a no-op', function(assert) {
  1354. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1355. loader.load();
  1356. this.requests.pop().respond(200, null,
  1357. '#EXTM3U\n' +
  1358. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1359. 'low.m3u8\n' +
  1360. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1361. 'high.m3u8\n');
  1362. this.requests.pop().respond(200, null,
  1363. '#EXTM3U\n' +
  1364. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1365. '#EXTINF:10,\n' +
  1366. 'low-0.ts\n' +
  1367. '#EXT-X-ENDLIST\n');
  1368. loader.media('low.m3u8');
  1369. assert.strictEqual(this.requests.length, 0, 'no requests are sent');
  1370. });
  1371. QUnit.test('switching to the active live playlist is a no-op', function(assert) {
  1372. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1373. loader.load();
  1374. this.requests.pop().respond(200, null,
  1375. '#EXTM3U\n' +
  1376. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1377. 'low.m3u8\n' +
  1378. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1379. 'high.m3u8\n');
  1380. this.requests.pop().respond(200, null,
  1381. '#EXTM3U\n' +
  1382. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1383. '#EXTINF:10,\n' +
  1384. 'low-0.ts\n');
  1385. loader.media('low.m3u8');
  1386. assert.strictEqual(this.requests.length, 0, 'no requests are sent');
  1387. });
  1388. QUnit.test('switches back to loaded playlists without re-requesting them',
  1389. function(assert) {
  1390. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1391. loader.load();
  1392. this.requests.pop().respond(200, null,
  1393. '#EXTM3U\n' +
  1394. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1395. 'low.m3u8\n' +
  1396. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1397. 'high.m3u8\n');
  1398. this.requests.pop().respond(200, null,
  1399. '#EXTM3U\n' +
  1400. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1401. '#EXTINF:10,\n' +
  1402. 'low-0.ts\n' +
  1403. '#EXT-X-ENDLIST\n');
  1404. loader.media('high.m3u8');
  1405. this.requests.pop().respond(200, null,
  1406. '#EXTM3U\n' +
  1407. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1408. '#EXTINF:10,\n' +
  1409. 'high-0.ts\n' +
  1410. '#EXT-X-ENDLIST\n');
  1411. loader.media('low.m3u8');
  1412. assert.strictEqual(this.requests.length, 0, 'no outstanding requests');
  1413. assert.strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist');
  1414. });
  1415. QUnit.test('aborts outstanding requests if switching back to an already loaded playlist',
  1416. function(assert) {
  1417. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1418. loader.load();
  1419. this.requests.pop().respond(200, null,
  1420. '#EXTM3U\n' +
  1421. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1422. 'low.m3u8\n' +
  1423. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1424. 'high.m3u8\n');
  1425. this.requests.pop().respond(200, null,
  1426. '#EXTM3U\n' +
  1427. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1428. '#EXTINF:10,\n' +
  1429. 'low-0.ts\n' +
  1430. '#EXT-X-ENDLIST\n');
  1431. loader.media('high.m3u8');
  1432. loader.media('low.m3u8');
  1433. assert.strictEqual(this.requests.length,
  1434. 1,
  1435. 'requested high playlist');
  1436. assert.ok(this.requests[0].aborted,
  1437. 'aborted playlist request');
  1438. assert.ok(!this.requests[0].onreadystatechange,
  1439. 'onreadystatechange handlers should be removed on abort');
  1440. assert.strictEqual(loader.state,
  1441. 'HAVE_METADATA',
  1442. 'returned to loaded playlist');
  1443. assert.strictEqual(loader.media(),
  1444. loader.master.playlists[0],
  1445. 'switched to loaded playlist');
  1446. });
  1447. QUnit.test('does not abort requests when the same playlist is re-requested',
  1448. function(assert) {
  1449. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1450. loader.load();
  1451. this.requests.pop().respond(200, null,
  1452. '#EXTM3U\n' +
  1453. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1454. 'low.m3u8\n' +
  1455. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1456. 'high.m3u8\n');
  1457. this.requests.pop().respond(200, null,
  1458. '#EXTM3U\n' +
  1459. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1460. '#EXTINF:10,\n' +
  1461. 'low-0.ts\n' +
  1462. '#EXT-X-ENDLIST\n');
  1463. loader.media('high.m3u8');
  1464. loader.media('high.m3u8');
  1465. assert.strictEqual(this.requests.length, 1, 'made only one request');
  1466. assert.ok(!this.requests[0].aborted, 'request not aborted');
  1467. });
  1468. QUnit.test('throws an error if a media switch is initiated too early', function(assert) {
  1469. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1470. loader.load();
  1471. assert.throws(function() {
  1472. loader.media('high.m3u8');
  1473. }, 'threw an error from HAVE_NOTHING');
  1474. this.requests.pop().respond(200, null,
  1475. '#EXTM3U\n' +
  1476. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1477. 'low.m3u8\n' +
  1478. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1479. 'high.m3u8\n');
  1480. });
  1481. QUnit.test('throws an error if a switch to an unrecognized playlist is requested',
  1482. function(assert) {
  1483. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1484. loader.load();
  1485. this.requests.pop().respond(200, null,
  1486. '#EXTM3U\n' +
  1487. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1488. 'media.m3u8\n');
  1489. assert.throws(function() {
  1490. loader.media('unrecognized.m3u8');
  1491. }, 'throws an error');
  1492. });
  1493. QUnit.test('dispose cancels the refresh timeout', function(assert) {
  1494. let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
  1495. loader.load();
  1496. this.requests.pop().respond(200, null,
  1497. '#EXTM3U\n' +
  1498. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1499. '#EXTINF:10,\n' +
  1500. '0.ts\n');
  1501. loader.dispose();
  1502. // a lot of time passes...
  1503. this.clock.tick(15 * 1000);
  1504. assert.strictEqual(this.requests.length, 0, 'no refresh request was made');
  1505. });
  1506. QUnit.test('dispose aborts pending refresh requests', function(assert) {
  1507. let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
  1508. loader.load();
  1509. this.requests.pop().respond(200, null,
  1510. '#EXTM3U\n' +
  1511. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1512. '#EXTINF:10,\n' +
  1513. '0.ts\n');
  1514. this.clock.tick(10 * 1000);
  1515. loader.dispose();
  1516. assert.ok(this.requests[0].aborted, 'refresh request aborted');
  1517. assert.ok(!this.requests[0].onreadystatechange,
  1518. 'onreadystatechange handler should not exist after dispose called'
  1519. );
  1520. });
  1521. QUnit.test('errors if requests take longer than 45s', function(assert) {
  1522. let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
  1523. let errors = 0;
  1524. loader.load();
  1525. loader.on('error', function() {
  1526. errors++;
  1527. });
  1528. this.clock.tick(45 * 1000);
  1529. assert.strictEqual(errors, 1, 'fired one error');
  1530. assert.strictEqual(loader.error.code, 2, 'fired a network error');
  1531. });
  1532. QUnit.test('triggers an event when the active media changes', function(assert) {
  1533. let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
  1534. let mediaChanges = 0;
  1535. let mediaChangings = 0;
  1536. loader.load();
  1537. loader.on('mediachange', function() {
  1538. mediaChanges++;
  1539. });
  1540. loader.on('mediachanging', function() {
  1541. mediaChangings++;
  1542. });
  1543. this.requests.pop().respond(200, null,
  1544. '#EXTM3U\n' +
  1545. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1546. 'low.m3u8\n' +
  1547. '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
  1548. 'high.m3u8\n');
  1549. this.requests.shift().respond(200, null,
  1550. '#EXTM3U\n' +
  1551. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1552. '#EXTINF:10,\n' +
  1553. 'low-0.ts\n' +
  1554. '#EXT-X-ENDLIST\n');
  1555. assert.strictEqual(mediaChangings, 0, 'initial selection is not a media changing');
  1556. assert.strictEqual(mediaChanges, 0, 'initial selection is not a media change');
  1557. loader.media('high.m3u8');
  1558. assert.strictEqual(mediaChangings, 1, 'mediachanging fires immediately');
  1559. assert.strictEqual(mediaChanges, 0, 'mediachange does not fire immediately');
  1560. this.requests.shift().respond(200, null,
  1561. '#EXTM3U\n' +
  1562. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1563. '#EXTINF:10,\n' +
  1564. 'high-0.ts\n' +
  1565. '#EXT-X-ENDLIST\n');
  1566. assert.strictEqual(mediaChangings, 1, 'still one mediachanging');
  1567. assert.strictEqual(mediaChanges, 1, 'fired a mediachange');
  1568. // switch back to an already loaded playlist
  1569. loader.media('low.m3u8');
  1570. assert.strictEqual(mediaChangings, 2, 'mediachanging fires');
  1571. assert.strictEqual(mediaChanges, 2, 'fired a mediachange');
  1572. // trigger a no-op switch
  1573. loader.media('low.m3u8');
  1574. assert.strictEqual(mediaChangings, 2, 'mediachanging ignored the no-op');
  1575. assert.strictEqual(mediaChanges, 2, 'ignored a no-op media change');
  1576. });
  1577. QUnit.test('does not misintrepret playlists missing newlines at the end',
  1578. function(assert) {
  1579. let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
  1580. loader.load();
  1581. // no newline
  1582. this.requests.shift().respond(200, null,
  1583. '#EXTM3U\n' +
  1584. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  1585. '#EXTINF:10,\n' +
  1586. 'low-0.ts\n' +
  1587. '#EXT-X-ENDLIST');
  1588. assert.ok(loader.media().endList, 'flushed the final line of input');
  1589. });