playlist.test.js 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105
  1. import Playlist from '../src/playlist';
  2. import PlaylistLoader from '../src/playlist-loader';
  3. import QUnit from 'qunit';
  4. import xhrFactory from '../src/xhr';
  5. import { useFakeEnvironment } from './test-helpers';
  6. QUnit.module('Playlist Duration');
  7. QUnit.test('total duration for live playlists is Infinity', function(assert) {
  8. let duration = Playlist.duration({
  9. segments: [{
  10. duration: 4,
  11. uri: '0.ts'
  12. }]
  13. });
  14. assert.equal(duration, Infinity, 'duration is infinity');
  15. });
  16. QUnit.module('Playlist Interval Duration');
  17. QUnit.test('accounts for non-zero starting VOD media sequences', function(assert) {
  18. let duration = Playlist.duration({
  19. mediaSequence: 10,
  20. endList: true,
  21. segments: [{
  22. duration: 10,
  23. uri: '0.ts'
  24. }, {
  25. duration: 10,
  26. uri: '1.ts'
  27. }, {
  28. duration: 10,
  29. uri: '2.ts'
  30. }, {
  31. duration: 10,
  32. uri: '3.ts'
  33. }]
  34. });
  35. assert.equal(duration, 4 * 10, 'includes only listed segments');
  36. });
  37. QUnit.test('uses timeline values when available', function(assert) {
  38. let duration = Playlist.duration({
  39. mediaSequence: 0,
  40. endList: true,
  41. segments: [{
  42. start: 0,
  43. uri: '0.ts'
  44. }, {
  45. duration: 10,
  46. end: 2 * 10 + 2,
  47. uri: '1.ts'
  48. }, {
  49. duration: 10,
  50. end: 3 * 10 + 2,
  51. uri: '2.ts'
  52. }, {
  53. duration: 10,
  54. end: 4 * 10 + 2,
  55. uri: '3.ts'
  56. }]
  57. }, 4);
  58. assert.equal(duration, 4 * 10 + 2, 'used timeline values');
  59. });
  60. QUnit.test('works when partial timeline information is available', function(assert) {
  61. let duration = Playlist.duration({
  62. mediaSequence: 0,
  63. endList: true,
  64. segments: [{
  65. start: 0,
  66. uri: '0.ts'
  67. }, {
  68. duration: 9,
  69. uri: '1.ts'
  70. }, {
  71. duration: 10,
  72. uri: '2.ts'
  73. }, {
  74. duration: 10,
  75. start: 30.007,
  76. end: 40.002,
  77. uri: '3.ts'
  78. }, {
  79. duration: 10,
  80. end: 50.0002,
  81. uri: '4.ts'
  82. }]
  83. }, 5);
  84. assert.equal(duration, 50.0002, 'calculated with mixed intervals');
  85. });
  86. QUnit.test('uses timeline values for the expired duration of live playlists',
  87. function(assert) {
  88. let playlist = {
  89. mediaSequence: 12,
  90. segments: [{
  91. duration: 10,
  92. end: 120.5,
  93. uri: '0.ts'
  94. }, {
  95. duration: 9,
  96. uri: '1.ts'
  97. }]
  98. };
  99. let duration;
  100. duration = Playlist.duration(playlist, playlist.mediaSequence);
  101. assert.equal(duration, 110.5, 'used segment end time');
  102. duration = Playlist.duration(playlist, playlist.mediaSequence + 1);
  103. assert.equal(duration, 120.5, 'used segment end time');
  104. duration = Playlist.duration(playlist, playlist.mediaSequence + 2);
  105. assert.equal(duration, 120.5 + 9, 'used segment end time');
  106. });
  107. QUnit.test('looks outside the queried interval for live playlist timeline values',
  108. function(assert) {
  109. let playlist = {
  110. mediaSequence: 12,
  111. segments: [{
  112. duration: 10,
  113. uri: '0.ts'
  114. }, {
  115. duration: 9,
  116. end: 120.5,
  117. uri: '1.ts'
  118. }]
  119. };
  120. let duration;
  121. duration = Playlist.duration(playlist, playlist.mediaSequence);
  122. assert.equal(duration, 120.5 - 9 - 10, 'used segment end time');
  123. });
  124. QUnit.test('ignores discontinuity sequences later than the end', function(assert) {
  125. let duration = Playlist.duration({
  126. mediaSequence: 0,
  127. discontinuityStarts: [1, 3],
  128. segments: [{
  129. duration: 10,
  130. uri: '0.ts'
  131. }, {
  132. discontinuity: true,
  133. duration: 9,
  134. uri: '1.ts'
  135. }, {
  136. duration: 10,
  137. uri: '2.ts'
  138. }, {
  139. discontinuity: true,
  140. duration: 10,
  141. uri: '3.ts'
  142. }]
  143. }, 2);
  144. assert.equal(duration, 19, 'excluded the later segments');
  145. });
  146. QUnit.test('handles trailing segments without timeline information', function(assert) {
  147. let duration;
  148. let playlist = {
  149. mediaSequence: 0,
  150. endList: true,
  151. segments: [{
  152. start: 0,
  153. end: 10.5,
  154. uri: '0.ts'
  155. }, {
  156. duration: 9,
  157. uri: '1.ts'
  158. }, {
  159. duration: 10,
  160. uri: '2.ts'
  161. }, {
  162. start: 29.45,
  163. end: 39.5,
  164. uri: '3.ts'
  165. }]
  166. };
  167. duration = Playlist.duration(playlist, 3);
  168. assert.equal(duration, 29.45, 'calculated duration');
  169. duration = Playlist.duration(playlist, 2);
  170. assert.equal(duration, 19.5, 'calculated duration');
  171. });
  172. QUnit.test('uses timeline intervals when segments have them', function(assert) {
  173. let duration;
  174. let playlist = {
  175. mediaSequence: 0,
  176. segments: [{
  177. start: 0,
  178. end: 10,
  179. uri: '0.ts'
  180. }, {
  181. duration: 9,
  182. uri: '1.ts'
  183. }, {
  184. start: 20.1,
  185. end: 30.1,
  186. duration: 10,
  187. uri: '2.ts'
  188. }]
  189. };
  190. duration = Playlist.duration(playlist, 2);
  191. assert.equal(duration, 20.1, 'used the timeline-based interval');
  192. duration = Playlist.duration(playlist, 3);
  193. assert.equal(duration, 30.1, 'used the timeline-based interval');
  194. });
  195. QUnit.test('counts the time between segments as part of the earlier segment\'s duration',
  196. function(assert) {
  197. let duration = Playlist.duration({
  198. mediaSequence: 0,
  199. endList: true,
  200. segments: [{
  201. start: 0,
  202. end: 10,
  203. uri: '0.ts'
  204. }, {
  205. start: 10.1,
  206. end: 20.1,
  207. duration: 10,
  208. uri: '1.ts'
  209. }]
  210. }, 1);
  211. assert.equal(duration, 10.1, 'included the segment gap');
  212. });
  213. QUnit.test('accounts for discontinuities', function(assert) {
  214. let duration = Playlist.duration({
  215. mediaSequence: 0,
  216. endList: true,
  217. discontinuityStarts: [1],
  218. segments: [{
  219. duration: 10,
  220. uri: '0.ts'
  221. }, {
  222. discontinuity: true,
  223. duration: 10,
  224. uri: '1.ts'
  225. }]
  226. }, 2);
  227. assert.equal(duration, 10 + 10, 'handles discontinuities');
  228. });
  229. QUnit.test('a non-positive length interval has zero duration', function(assert) {
  230. let playlist = {
  231. mediaSequence: 0,
  232. discontinuityStarts: [1],
  233. segments: [{
  234. duration: 10,
  235. uri: '0.ts'
  236. }, {
  237. discontinuity: true,
  238. duration: 10,
  239. uri: '1.ts'
  240. }]
  241. };
  242. assert.equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero');
  243. assert.equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero');
  244. assert.equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero');
  245. });
  246. QUnit.module('Playlist Seekable');
  247. QUnit.test('calculates seekable time ranges from available segments', function(assert) {
  248. let playlist = {
  249. mediaSequence: 0,
  250. segments: [{
  251. duration: 10,
  252. uri: '0.ts'
  253. }, {
  254. duration: 10,
  255. uri: '1.ts'
  256. }],
  257. endList: true
  258. };
  259. let seekable = Playlist.seekable(playlist);
  260. assert.equal(seekable.length, 1, 'there are seekable ranges');
  261. assert.equal(seekable.start(0), 0, 'starts at zero');
  262. assert.equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration');
  263. });
  264. QUnit.test('calculates playlist end time from the available segments', function(assert) {
  265. let playlistEnd = Playlist.playlistEnd({
  266. mediaSequence: 0,
  267. segments: [{
  268. duration: 10,
  269. uri: '0.ts'
  270. }, {
  271. duration: 10,
  272. uri: '1.ts'
  273. }],
  274. endList: true
  275. });
  276. assert.equal(playlistEnd, 20, 'paylist end at the duration');
  277. });
  278. QUnit.test('master playlists have empty seekable ranges and no playlist end',
  279. function(assert) {
  280. let playlist = {
  281. playlists: [{
  282. uri: 'low.m3u8'
  283. }, {
  284. uri: 'high.m3u8'
  285. }]
  286. };
  287. let seekable = Playlist.seekable(playlist);
  288. let playlistEnd = Playlist.playlistEnd(playlist);
  289. assert.equal(seekable.length, 0, 'no seekable ranges from a master playlist');
  290. assert.equal(playlistEnd, null, 'no playlist end from a master playlist');
  291. });
  292. QUnit.test('seekable end is three target durations from the actual end of live playlists',
  293. function(assert) {
  294. let seekable = Playlist.seekable({
  295. mediaSequence: 0,
  296. syncInfo: {
  297. time: 0,
  298. mediaSequence: 0
  299. },
  300. targetDuration: 10,
  301. segments: [{
  302. duration: 7,
  303. uri: '0.ts'
  304. }, {
  305. duration: 10,
  306. uri: '1.ts'
  307. }, {
  308. duration: 10,
  309. uri: '2.ts'
  310. }, {
  311. duration: 10,
  312. uri: '3.ts'
  313. }]
  314. });
  315. assert.equal(seekable.length, 1, 'there are seekable ranges');
  316. assert.equal(seekable.start(0), 0, 'starts at zero');
  317. assert.equal(seekable.end(0), 7, 'ends three target durations from the last segment');
  318. });
  319. QUnit.test('seekable end and playlist end account for non-standard target durations',
  320. function(assert) {
  321. const playlist = {
  322. targetDuration: 2,
  323. mediaSequence: 0,
  324. syncInfo: {
  325. time: 0,
  326. mediaSequence: 0
  327. },
  328. segments: [{
  329. duration: 2,
  330. uri: '0.ts'
  331. }, {
  332. duration: 2,
  333. uri: '1.ts'
  334. }, {
  335. duration: 1,
  336. uri: '2.ts'
  337. }, {
  338. duration: 2,
  339. uri: '3.ts'
  340. }, {
  341. duration: 2,
  342. uri: '4.ts'
  343. }]
  344. };
  345. let seekable = Playlist.seekable(playlist);
  346. let playlistEnd = Playlist.playlistEnd(playlist);
  347. assert.equal(seekable.start(0), 0, 'starts at the earliest available segment');
  348. assert.equal(seekable.end(0),
  349. // Playlist duration is 9s. Target duration 2s. Seekable end should be at
  350. // least 6s from end. Adding segment durations starting from the end to get
  351. // that 6s target
  352. 9 - (2 + 2 + 1 + 2),
  353. 'allows seeking no further than the start of the segment 2 target' +
  354. 'durations back from the beginning of the last segment');
  355. assert.equal(playlistEnd, 9, 'playlist end at the last segment');
  356. });
  357. QUnit.test('safeLiveIndex is correct for standard segment durations', function(assert) {
  358. const playlist = {
  359. targetDuration: 6,
  360. mediaSequence: 10,
  361. syncInfo: {
  362. time: 0,
  363. mediaSequence: 10
  364. },
  365. segments: [
  366. {
  367. duration: 6
  368. },
  369. {
  370. duration: 6
  371. },
  372. {
  373. duration: 6
  374. },
  375. {
  376. duration: 6
  377. },
  378. {
  379. duration: 6
  380. },
  381. {
  382. duration: 6
  383. }
  384. ]
  385. };
  386. assert.equal(Playlist.safeLiveIndex(playlist), 3,
  387. 'correct media index for standard durations');
  388. });
  389. QUnit.test('safeLiveIndex is correct for variable segment durations', function(assert) {
  390. const playlist = {
  391. targetDuration: 6,
  392. mediaSequence: 10,
  393. syncInfo: {
  394. time: 0,
  395. mediaSequence: 10
  396. },
  397. segments: [
  398. {
  399. duration: 6
  400. },
  401. {
  402. duration: 4
  403. },
  404. {
  405. duration: 5
  406. },
  407. {
  408. // this segment is 16 seconds from the end of playlist, the safe live point
  409. duration: 6
  410. },
  411. {
  412. duration: 3
  413. },
  414. {
  415. duration: 4
  416. },
  417. {
  418. duration: 3
  419. }
  420. ]
  421. };
  422. // safe live point is no less than 15 seconds (3s + 2 * 6s) from the end of the playlist
  423. assert.equal(Playlist.safeLiveIndex(playlist), 3,
  424. 'correct media index for variable segment durations');
  425. });
  426. QUnit.test('safeLiveIndex is 0 when no safe live point', function(assert) {
  427. const playlist = {
  428. targetDuration: 6,
  429. mediaSequence: 10,
  430. syncInfo: {
  431. time: 0,
  432. mediaSequence: 10
  433. },
  434. segments: [
  435. {
  436. duration: 6
  437. },
  438. {
  439. duration: 3
  440. },
  441. {
  442. duration: 3
  443. }
  444. ]
  445. };
  446. assert.equal(Playlist.safeLiveIndex(playlist), 0,
  447. 'returns media index 0 when playlist has no safe live point');
  448. });
  449. QUnit.test(
  450. 'seekable end and playlist end account for non-zero starting VOD media sequence',
  451. function(assert) {
  452. let playlist = {
  453. targetDuration: 2,
  454. mediaSequence: 5,
  455. endList: true,
  456. segments: [{
  457. duration: 2,
  458. uri: '0.ts'
  459. }, {
  460. duration: 2,
  461. uri: '1.ts'
  462. }, {
  463. duration: 1,
  464. uri: '2.ts'
  465. }, {
  466. duration: 2,
  467. uri: '3.ts'
  468. }, {
  469. duration: 2,
  470. uri: '4.ts'
  471. }]
  472. };
  473. let seekable = Playlist.seekable(playlist);
  474. let playlistEnd = Playlist.playlistEnd(playlist);
  475. assert.equal(seekable.start(0), 0, 'starts at the earliest available segment');
  476. assert.equal(seekable.end(0), 9, 'seekable end is same as duration');
  477. assert.equal(playlistEnd, 9, 'playlist end at the last segment');
  478. });
  479. QUnit.test('playlist with no sync points has empty seekable range and empty playlist end',
  480. function(assert) {
  481. let playlist = {
  482. targetDuration: 10,
  483. mediaSequence: 0,
  484. segments: [{
  485. duration: 7,
  486. uri: '0.ts'
  487. }, {
  488. duration: 10,
  489. uri: '1.ts'
  490. }, {
  491. duration: 10,
  492. uri: '2.ts'
  493. }, {
  494. duration: 10,
  495. uri: '3.ts'
  496. }]
  497. };
  498. // seekable and playlistEnd take an optional expired parameter that is from
  499. // SyncController.getExpiredTime which returns null when there is no sync point, so
  500. // this test passes in null to simulate no sync points
  501. let seekable = Playlist.seekable(playlist, null);
  502. let playlistEnd = Playlist.playlistEnd(playlist, null);
  503. assert.equal(seekable.length, 0, 'no seekable range for playlist with no sync points');
  504. assert.equal(playlistEnd, null, 'no playlist end for playlist with no sync points');
  505. });
  506. QUnit.test('seekable and playlistEnd use available sync points for calculating',
  507. function(assert) {
  508. let playlist = {
  509. targetDuration: 10,
  510. mediaSequence: 100,
  511. syncInfo: {
  512. time: 50,
  513. mediaSequence: 95
  514. },
  515. segments: [
  516. {
  517. duration: 10,
  518. uri: '0.ts'
  519. },
  520. {
  521. duration: 10,
  522. uri: '1.ts'
  523. },
  524. {
  525. duration: 10,
  526. uri: '2.ts'
  527. },
  528. {
  529. duration: 10,
  530. uri: '3.ts'
  531. },
  532. {
  533. duration: 10,
  534. uri: '4.ts'
  535. }
  536. ]
  537. };
  538. // getExpiredTime would return 100 for this playlist
  539. let seekable = Playlist.seekable(playlist, 100);
  540. let playlistEnd = Playlist.playlistEnd(playlist, 100);
  541. assert.ok(seekable.length, 'seekable range calculated');
  542. assert.equal(seekable.start(0),
  543. 100,
  544. 'estimated start time based on expired sync point');
  545. assert.equal(seekable.end(0),
  546. 120,
  547. 'allows seeking no further than three segments from the end');
  548. assert.equal(playlistEnd, 150, 'playlist end at the last segment end');
  549. playlist = {
  550. targetDuration: 10,
  551. mediaSequence: 100,
  552. segments: [
  553. {
  554. duration: 10,
  555. uri: '0.ts'
  556. },
  557. {
  558. duration: 10,
  559. uri: '1.ts',
  560. start: 108.5,
  561. end: 118.4
  562. },
  563. {
  564. duration: 10,
  565. uri: '2.ts'
  566. },
  567. {
  568. duration: 10,
  569. uri: '3.ts'
  570. },
  571. {
  572. duration: 10,
  573. uri: '4.ts'
  574. }
  575. ]
  576. };
  577. // getExpiredTime would return 98.5
  578. seekable = Playlist.seekable(playlist, 98.5);
  579. playlistEnd = Playlist.playlistEnd(playlist, 98.5);
  580. assert.ok(seekable.length, 'seekable range calculated');
  581. assert.equal(seekable.start(0), 98.5, 'estimated start time using segmentSync');
  582. assert.equal(seekable.end(0),
  583. 118.4,
  584. 'allows seeking no further than three segments from the end');
  585. assert.equal(playlistEnd, 148.4, 'playlist end at the last segment end');
  586. playlist = {
  587. targetDuration: 10,
  588. mediaSequence: 100,
  589. syncInfo: {
  590. time: 50,
  591. mediaSequence: 95
  592. },
  593. segments: [
  594. {
  595. duration: 10,
  596. uri: '0.ts'
  597. },
  598. {
  599. duration: 10,
  600. uri: '1.ts',
  601. start: 108.5,
  602. end: 118.5
  603. },
  604. {
  605. duration: 10,
  606. uri: '2.ts'
  607. },
  608. {
  609. duration: 10,
  610. uri: '3.ts'
  611. },
  612. {
  613. duration: 10,
  614. uri: '4.ts'
  615. }
  616. ]
  617. };
  618. // getExpiredTime would return 98.5
  619. seekable = Playlist.seekable(playlist, 98.5);
  620. playlistEnd = Playlist.playlistEnd(playlist, 98.5);
  621. assert.ok(seekable.length, 'seekable range calculated');
  622. assert.equal(
  623. seekable.start(0),
  624. 98.5,
  625. 'estimated start time using nearest sync point (segmentSync in this case)');
  626. assert.equal(seekable.end(0),
  627. 118.5,
  628. 'allows seeking no further than three segments from the end');
  629. assert.equal(playlistEnd, 148.5, 'playlist end at the last segment end');
  630. playlist = {
  631. targetDuration: 10,
  632. mediaSequence: 100,
  633. syncInfo: {
  634. time: 90.8,
  635. mediaSequence: 99
  636. },
  637. segments: [
  638. {
  639. duration: 10,
  640. uri: '0.ts'
  641. },
  642. {
  643. duration: 10,
  644. uri: '1.ts'
  645. },
  646. {
  647. duration: 10,
  648. uri: '2.ts',
  649. start: 118.5,
  650. end: 128.5
  651. },
  652. {
  653. duration: 10,
  654. uri: '3.ts'
  655. },
  656. {
  657. duration: 10,
  658. uri: '4.ts'
  659. }
  660. ]
  661. };
  662. // getExpiredTime would return 100.8
  663. seekable = Playlist.seekable(playlist, 100.8);
  664. playlistEnd = Playlist.playlistEnd(playlist, 100.8);
  665. assert.ok(seekable.length, 'seekable range calculated');
  666. assert.equal(
  667. seekable.start(0),
  668. 100.8,
  669. 'estimated start time using nearest sync point (expiredSync in this case)');
  670. assert.equal(seekable.end(0),
  671. 118.5,
  672. 'allows seeking no further than three segments from the end');
  673. assert.equal(playlistEnd, 148.5, 'playlist end at the last segment end');
  674. });
  675. QUnit.module('Playlist hasAttribute');
  676. QUnit.test('correctly checks for existence of playlist attribute', function(assert) {
  677. const playlist = {};
  678. assert.notOk(Playlist.hasAttribute('BANDWIDTH', playlist),
  679. 'false for playlist with no attributes property');
  680. playlist.attributes = {};
  681. assert.notOk(Playlist.hasAttribute('BANDWIDTH', playlist),
  682. 'false for playlist with without specified attribute');
  683. playlist.attributes.BANDWIDTH = 100;
  684. assert.ok(Playlist.hasAttribute('BANDWIDTH', playlist),
  685. 'true for playlist with specified attribute');
  686. });
  687. QUnit.module('Playlist estimateSegmentRequestTime');
  688. QUnit.test('estimates segment request time based on bandwidth', function(assert) {
  689. let segmentDuration = 10;
  690. let bandwidth = 100;
  691. let playlist = { attributes: { } };
  692. let bytesReceived = 0;
  693. let estimate = Playlist.estimateSegmentRequestTime(segmentDuration,
  694. bandwidth,
  695. playlist,
  696. bytesReceived);
  697. assert.ok(isNaN(estimate), 'returns NaN when no BANDWIDTH information on playlist');
  698. playlist.attributes.BANDWIDTH = 100;
  699. estimate = Playlist.estimateSegmentRequestTime(segmentDuration,
  700. bandwidth,
  701. playlist,
  702. bytesReceived);
  703. assert.equal(estimate, 10, 'calculated estimated download time');
  704. bytesReceived = 25;
  705. estimate = Playlist.estimateSegmentRequestTime(segmentDuration,
  706. bandwidth,
  707. playlist,
  708. bytesReceived);
  709. assert.equal(estimate, 8, 'takes into account bytes already received from download');
  710. });
  711. QUnit.module('Playlist enabled states', {
  712. beforeEach(assert) {
  713. this.env = useFakeEnvironment(assert);
  714. this.clock = this.env.clock;
  715. },
  716. afterEach() {
  717. this.env.restore();
  718. }
  719. });
  720. QUnit.test('determines if a playlist is incompatible', function(assert) {
  721. // incompatible means that the playlist was blacklisted due to incompatible
  722. // configuration e.g. audio only stream when trying to playback audio and video.
  723. // incompaatibility is denoted by a blacklist of Infinity.
  724. assert.notOk(Playlist.isIncompatible({}),
  725. 'playlist not incompatible if no excludeUntil');
  726. assert.notOk(Playlist.isIncompatible({ excludeUntil: 1 }),
  727. 'playlist not incompatible if expired blacklist');
  728. assert.notOk(Playlist.isIncompatible({ excludeUntil: Date.now() + 9999 }),
  729. 'playlist not incompatible if temporarily blacklisted');
  730. assert.ok(Playlist.isIncompatible({ excludeUntil: Infinity }),
  731. 'playlist is incompatible if excludeUntil is Infinity');
  732. });
  733. QUnit.test('determines if a playlist is blacklisted', function(assert) {
  734. assert.notOk(Playlist.isBlacklisted({}),
  735. 'playlist not blacklisted if no excludeUntil');
  736. assert.notOk(Playlist.isBlacklisted({ excludeUntil: Date.now() - 1 }),
  737. 'playlist not blacklisted if expired excludeUntil');
  738. assert.ok(Playlist.isBlacklisted({ excludeUntil: Date.now() + 9999 }),
  739. 'playlist is blacklisted');
  740. assert.ok(Playlist.isBlacklisted({ excludeUntil: Infinity }),
  741. 'playlist is blacklisted if excludeUntil is Infinity');
  742. });
  743. QUnit.test('determines if a playlist is disabled', function(assert) {
  744. assert.notOk(Playlist.isDisabled({}), 'playlist not disabled');
  745. assert.ok(Playlist.isDisabled({ disabled: true }), 'playlist is disabled');
  746. });
  747. QUnit.test('playlists with no or expired blacklist are enabled', function(assert) {
  748. // enabled means not blacklisted and not disabled
  749. assert.ok(Playlist.isEnabled({}), 'playlist with no blacklist is enabled');
  750. assert.ok(Playlist.isEnabled({ excludeUntil: Date.now() - 1 }),
  751. 'playlist with expired blacklist is enabled');
  752. });
  753. QUnit.test('blacklisted playlists are not enabled', function(assert) {
  754. // enabled means not blacklisted and not disabled
  755. assert.notOk(Playlist.isEnabled({ excludeUntil: Date.now() + 9999 }),
  756. 'playlist with temporary blacklist is not enabled');
  757. assert.notOk(Playlist.isEnabled({ excludeUntil: Infinity }),
  758. 'playlist with permanent is not enabled');
  759. });
  760. QUnit.test('manually disabled playlists are not enabled regardless of blacklist state',
  761. function(assert) {
  762. // enabled means not blacklisted and not disabled
  763. assert.notOk(Playlist.isEnabled({ disabled: true }),
  764. 'disabled playlist with no blacklist is not enabled');
  765. assert.notOk(Playlist.isEnabled({ disabled: true, excludeUntil: Date.now() - 1 }),
  766. 'disabled playlist with expired blacklist is not enabled');
  767. assert.notOk(Playlist.isEnabled({ disabled: true, excludeUntil: Date.now() + 9999 }),
  768. 'disabled playlist with temporary blacklist is not enabled');
  769. assert.notOk(Playlist.isEnabled({ disabled: true, excludeUntil: Infinity }),
  770. 'disabled playlist with permanent blacklist is not enabled');
  771. });
  772. QUnit.test('isLowestEnabledRendition detects if we are on lowest rendition',
  773. function(assert) {
  774. assert.ok(
  775. Playlist.isLowestEnabledRendition(
  776. {
  777. playlists: [
  778. {attributes: {BANDWIDTH: 10}},
  779. {attributes: {BANDWIDTH: 20}}
  780. ]
  781. },
  782. {attributes: {BANDWIDTH: 10}}),
  783. 'Detected on lowest rendition');
  784. assert.ok(
  785. Playlist.isLowestEnabledRendition(
  786. {
  787. playlists: [
  788. {attributes: {BANDWIDTH: 10}},
  789. {attributes: {BANDWIDTH: 10}},
  790. {attributes: {BANDWIDTH: 10}},
  791. {attributes: {BANDWIDTH: 20}}
  792. ]
  793. },
  794. {attributes: {BANDWIDTH: 10}}),
  795. 'Detected on lowest rendition');
  796. assert.notOk(
  797. Playlist.isLowestEnabledRendition(
  798. {
  799. playlists: [
  800. {attributes: {BANDWIDTH: 10}},
  801. {attributes: {BANDWIDTH: 20}}
  802. ]
  803. },
  804. {attributes: {BANDWIDTH: 20}}),
  805. 'Detected not on lowest rendition');
  806. });
  807. QUnit.module('Playlist isAes and isFmp4', {
  808. beforeEach(assert) {
  809. this.env = useFakeEnvironment(assert);
  810. this.clock = this.env.clock;
  811. this.requests = this.env.requests;
  812. this.fakeHls = {
  813. xhr: xhrFactory()
  814. };
  815. },
  816. afterEach() {
  817. this.env.restore();
  818. }
  819. });
  820. QUnit.test('determine if playlist is an AES encrypted HLS stream', function(assert) {
  821. let media;
  822. let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
  823. loader.load();
  824. this.requests.shift().respond(
  825. 200,
  826. null,
  827. '#EXTM3U\n' +
  828. '#EXT-X-TARGETDURATION:15\n' +
  829. '#EXT-X-KEY:METHOD=AES-128,URI="http://example.com/keys/key.php"\n' +
  830. '#EXTINF:2.833,\n' +
  831. 'http://example.com/000001.ts\n' +
  832. '#EXT-X-ENDLIST\n'
  833. );
  834. media = loader.media();
  835. assert.ok(Playlist.isAes(media), 'media is an AES encrypted HLS stream');
  836. });
  837. QUnit.test('determine if playlist contains an fmp4 segment', function(assert) {
  838. let media;
  839. let loader = new PlaylistLoader('video/fmp4.m3u8', this.fakeHls);
  840. loader.load();
  841. this.requests.shift().respond(200, null,
  842. '#EXTM3U\n' +
  843. '#EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"\n' +
  844. '#EXTINF:10,\n' +
  845. '0.mp4\n' +
  846. '#EXT-X-ENDLIST\n');
  847. media = loader.media();
  848. assert.ok(Playlist.isFmp4(media), 'media contains fmp4 segment');
  849. });
  850. QUnit.module('Playlist Media Index For Time', {
  851. beforeEach(assert) {
  852. this.env = useFakeEnvironment(assert);
  853. this.clock = this.env.clock;
  854. this.requests = this.env.requests;
  855. this.fakeHls = {
  856. xhr: xhrFactory()
  857. };
  858. },
  859. afterEach() {
  860. this.env.restore();
  861. }
  862. });
  863. QUnit.test('can get media index by playback position for non-live videos',
  864. function(assert) {
  865. let media;
  866. let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
  867. loader.load();
  868. this.requests.shift().respond(200, null,
  869. '#EXTM3U\n' +
  870. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  871. '#EXTINF:4,\n' +
  872. '0.ts\n' +
  873. '#EXTINF:5,\n' +
  874. '1.ts\n' +
  875. '#EXTINF:6,\n' +
  876. '2.ts\n' +
  877. '#EXT-X-ENDLIST\n'
  878. );
  879. media = loader.media();
  880. assert.equal(Playlist.getMediaInfoForTime(media, -1, 0, 0).mediaIndex, 0,
  881. 'the index is never less than zero');
  882. assert.equal(Playlist.getMediaInfoForTime(media, 0, 0, 0).mediaIndex, 0,
  883. 'time zero is index zero');
  884. assert.equal(Playlist.getMediaInfoForTime(media, 3, 0, 0).mediaIndex, 0,
  885. 'time three is index zero');
  886. assert.equal(Playlist.getMediaInfoForTime(media, 10, 0, 0).mediaIndex, 2,
  887. 'time 10 is index 2');
  888. assert.equal(Playlist.getMediaInfoForTime(media, 22, 0, 0).mediaIndex, 2,
  889. 'time greater than the length is index 2');
  890. });
  891. QUnit.test('returns the lower index when calculating for a segment boundary',
  892. function(assert) {
  893. let media;
  894. let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
  895. loader.load();
  896. this.requests.shift().respond(200, null,
  897. '#EXTM3U\n' +
  898. '#EXT-X-MEDIA-SEQUENCE:0\n' +
  899. '#EXTINF:4,\n' +
  900. '0.ts\n' +
  901. '#EXTINF:5,\n' +
  902. '1.ts\n' +
  903. '#EXT-X-ENDLIST\n'
  904. );
  905. media = loader.media();
  906. assert.equal(Playlist.getMediaInfoForTime(media, 4, 0, 0).mediaIndex, 0,
  907. 'rounds down exact matches');
  908. assert.equal(Playlist.getMediaInfoForTime(media, 3.7, 0, 0).mediaIndex, 0,
  909. 'rounds down');
  910. assert.equal(Playlist.getMediaInfoForTime(media, 4.5, 0, 0).mediaIndex, 1,
  911. 'rounds up at 0.5');
  912. });
  913. QUnit.test(
  914. 'accounts for non-zero starting segment time when calculating media index',
  915. function(assert) {
  916. let media;
  917. let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
  918. loader.load();
  919. this.requests.shift().respond(200, null,
  920. '#EXTM3U\n' +
  921. '#EXT-X-MEDIA-SEQUENCE:1001\n' +
  922. '#EXTINF:4,\n' +
  923. '1001.ts\n' +
  924. '#EXTINF:5,\n' +
  925. '1002.ts\n'
  926. );
  927. media = loader.media();
  928. assert.equal(
  929. Playlist.getMediaInfoForTime(media, 45, 0, 150).mediaIndex,
  930. 0,
  931. 'expired content returns 0 for earliest segment available'
  932. );
  933. assert.equal(
  934. Playlist.getMediaInfoForTime(media, 75, 0, 150).mediaIndex,
  935. 0,
  936. 'expired content returns 0 for earliest segment available'
  937. );
  938. assert.equal(
  939. Playlist.getMediaInfoForTime(media, 0, 0, 150).mediaIndex,
  940. 0,
  941. 'time of 0 with no expired time returns first segment'
  942. );
  943. assert.equal(
  944. Playlist.getMediaInfoForTime(media, 50 + 100, 0, 150).mediaIndex,
  945. 0,
  946. 'calculates the earliest available position'
  947. );
  948. assert.equal(
  949. Playlist.getMediaInfoForTime(media, 50 + 100 + 2, 0, 150).mediaIndex,
  950. 0,
  951. 'calculates within the first segment'
  952. );
  953. assert.equal(
  954. Playlist.getMediaInfoForTime(media, 50 + 100 + 2, 0, 150).mediaIndex,
  955. 0,
  956. 'calculates within the first segment'
  957. );
  958. assert.equal(
  959. Playlist.getMediaInfoForTime(media, 50 + 100 + 4, 0, 150).mediaIndex,
  960. 0,
  961. 'calculates earlier segment on exact boundary match'
  962. );
  963. assert.equal(
  964. Playlist.getMediaInfoForTime(media, 50 + 100 + 4.5, 0, 150).mediaIndex,
  965. 1,
  966. 'calculates within the second segment'
  967. );
  968. assert.equal(
  969. Playlist.getMediaInfoForTime(media, 50 + 100 + 6, 0, 150).mediaIndex,
  970. 1,
  971. 'calculates within the second segment'
  972. );
  973. assert.equal(
  974. Playlist.getMediaInfoForTime(media, 159, 0, 150).mediaIndex,
  975. 1,
  976. 'returns last segment when time is equal to end of last segment'
  977. );
  978. assert.equal(
  979. Playlist.getMediaInfoForTime(media, 160, 0, 150).mediaIndex,
  980. 1,
  981. 'returns last segment when time is past end of last segment'
  982. );
  983. });