playlist-merge.test.js 26 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040
  1. import {
  2. getUniqueTimelineStarts,
  3. findPlaylistWithName,
  4. getMediaGroupPlaylists,
  5. updateMediaSequenceForPlaylist,
  6. updateSequenceNumbers,
  7. positionManifestOnTimeline
  8. } from '../src/playlist-merge';
  9. import { merge } from '../src/utils/object';
  10. import QUnit from 'qunit';
  11. QUnit.module('getUniqueTimelineStarts');
  12. QUnit.test('handles multiple playlists', function(assert) {
  13. const listOfTimelineStartLists = [
  14. [{ start: 0, timeline: 0 }, { start: 10, timeline: 10 }, { start: 20, timeline: 20 }],
  15. [{ start: 0, timeline: 0 }, { start: 10, timeline: 10 }, { start: 20, timeline: 20 }],
  16. [{ start: 10, timeline: 10 }, { start: 20, timeline: 20 }],
  17. [{ start: 0, timeline: 0 }, { start: 20, timeline: 20 }],
  18. [{ start: 30, timeline: 30 }]
  19. ];
  20. assert.deepEqual(
  21. getUniqueTimelineStarts(listOfTimelineStartLists),
  22. [{
  23. start: 0,
  24. timeline: 0
  25. }, {
  26. start: 10,
  27. timeline: 10
  28. }, {
  29. start: 20,
  30. timeline: 20
  31. }, {
  32. start: 30,
  33. timeline: 30
  34. }],
  35. 'handled multiple playlists with differing timeline starts'
  36. );
  37. });
  38. QUnit.module('findPlaylistWithName');
  39. QUnit.test('returns nothing when no playlists', function(assert) {
  40. assert.notOk(findPlaylistWithName([], 'A'), 'nothing when no playlists');
  41. });
  42. QUnit.test('returns nothing when no match', function(assert) {
  43. const playlists = [
  44. { attributes: { NAME: 'B' } }
  45. ];
  46. assert.notOk(findPlaylistWithName(playlists, 'A'), 'nothing when no match');
  47. });
  48. QUnit.test('returns matching playlist', function(assert) {
  49. const playlists = [
  50. { attributes: { NAME: 'A' } },
  51. { attributes: { NAME: 'B' } },
  52. { attributes: { NAME: 'C' } }
  53. ];
  54. assert.deepEqual(
  55. findPlaylistWithName(playlists, 'B'),
  56. playlists[1],
  57. 'returns matching playlist'
  58. );
  59. });
  60. QUnit.module('getMediaGroupPlaylists');
  61. QUnit.test('returns nothing when no media group playlists', function(assert) {
  62. const manifest = {
  63. mediaGroups: {
  64. AUDIO: {}
  65. }
  66. };
  67. assert.deepEqual(
  68. getMediaGroupPlaylists(manifest),
  69. [],
  70. 'nothing when no media group playlists'
  71. );
  72. });
  73. QUnit.test('returns media group playlists', function(assert) {
  74. const playlistEnA = { attributes: { NAME: 'A' } };
  75. const playlistEnB = { attributes: { NAME: 'B' } };
  76. const playlistEnC = { attributes: { NAME: 'C' } };
  77. const playlistFrA = { attributes: { NAME: 'A' } };
  78. const playlistFrB = { attributes: { NAME: 'B' } };
  79. const manifest = {
  80. mediaGroups: {
  81. AUDIO: {
  82. audio: {
  83. en: {
  84. playlists: [playlistEnA, playlistEnB, playlistEnC]
  85. },
  86. fr: {
  87. playlists: [playlistFrA, playlistFrB]
  88. }
  89. }
  90. }
  91. }
  92. };
  93. assert.deepEqual(
  94. getMediaGroupPlaylists(manifest),
  95. [playlistEnA, playlistEnB, playlistEnC, playlistFrA, playlistFrB],
  96. 'returns media group playlists'
  97. );
  98. });
  99. QUnit.module('updateMediaSequenceForPlaylist');
  100. QUnit.test('no segments means only top level mediaSequence is updated', function(assert) {
  101. const playlist = { mediaSequence: 1, segments: [] };
  102. updateMediaSequenceForPlaylist({ playlist, mediaSequence: 3 });
  103. assert.deepEqual(
  104. playlist,
  105. { mediaSequence: 3, segments: [] },
  106. 'updated only top level mediaSequence'
  107. );
  108. });
  109. QUnit.test('updates top level mediaSequence and segments', function(assert) {
  110. const playlist = {
  111. mediaSequence: 1,
  112. segments: [{ number: 1 }, { number: 2 }, { number: 3 }]
  113. };
  114. updateMediaSequenceForPlaylist({ playlist, mediaSequence: 3 });
  115. assert.deepEqual(
  116. playlist,
  117. { mediaSequence: 3, segments: [{ number: 3 }, { number: 4 }, { number: 5 }] },
  118. 'updated top level mediaSequence and segments'
  119. );
  120. });
  121. QUnit.module('updateSequenceNumbers');
  122. QUnit.test('no playlists, no update', function(assert) {
  123. const oldPlaylists = [];
  124. const newPlaylists = [];
  125. const timelineStarts = [];
  126. updateSequenceNumbers({ oldPlaylists, newPlaylists, timelineStarts });
  127. assert.deepEqual(newPlaylists, [], 'new playlists unchanged');
  128. });
  129. QUnit.test('no matching playlists only updates discontinuity sequence', function(assert) {
  130. const oldPlaylists = [{
  131. discontinuitySequence: 1,
  132. discontinuityStarts: [],
  133. timeline: 5,
  134. attributes: { NAME: 'A' }
  135. }, {
  136. discontinuitySequence: 2,
  137. discontinuityStarts: [],
  138. timeline: 10,
  139. attributes: { NAME: 'B' }
  140. }];
  141. const newPlaylists = [{
  142. discontinuitySequence: 0,
  143. discontinuityStarts: [],
  144. timeline: 5,
  145. attributes: { NAME: 'C' }
  146. }, {
  147. discontinuitySequence: 0,
  148. discontinuityStarts: [],
  149. timeline: 10,
  150. attributes: { NAME: 'D' }
  151. }];
  152. const timelineStarts = [
  153. { start: 0, timeline: 0 },
  154. { start: 5, timeline: 5 },
  155. { start: 10, timeline: 10 }
  156. ];
  157. updateSequenceNumbers({ oldPlaylists, newPlaylists, timelineStarts });
  158. assert.deepEqual(
  159. newPlaylists,
  160. [{
  161. discontinuitySequence: 1,
  162. discontinuityStarts: [],
  163. timeline: 5,
  164. attributes: { NAME: 'C' }
  165. }, {
  166. discontinuitySequence: 2,
  167. discontinuityStarts: [],
  168. timeline: 10,
  169. attributes: { NAME: 'D' }
  170. }],
  171. 'new playlist discontinuity sequence numbers updated'
  172. );
  173. });
  174. QUnit.test('segment match of matching playlist', function(assert) {
  175. const oldPlaylists = [{
  176. discontinuitySequence: 1,
  177. discontinuityStarts: [],
  178. mediaSequence: 5,
  179. timeline: 5,
  180. attributes: { NAME: 'C' },
  181. segments: [{
  182. presentationTime: 10,
  183. timeline: 5,
  184. number: 5
  185. }, {
  186. presentationTime: 12,
  187. timeline: 5,
  188. number: 6
  189. }, {
  190. presentationTime: 14,
  191. timeline: 5,
  192. number: 7
  193. }]
  194. }];
  195. const newPlaylists = [{
  196. discontinuitySequence: 0,
  197. discontinuityStarts: [],
  198. mediaSequence: 0,
  199. timeline: 5,
  200. attributes: { NAME: 'C' },
  201. segments: [{
  202. presentationTime: 12,
  203. timeline: 5,
  204. number: 0
  205. }, {
  206. presentationTime: 14,
  207. timeline: 5,
  208. number: 1
  209. }]
  210. }];
  211. const timelineStarts = [
  212. { start: 0, timeline: 0 },
  213. { start: 5, timeline: 5 }
  214. ];
  215. updateSequenceNumbers({ oldPlaylists, newPlaylists, timelineStarts });
  216. assert.deepEqual(
  217. newPlaylists,
  218. [{
  219. discontinuitySequence: 1,
  220. discontinuityStarts: [],
  221. mediaSequence: 6,
  222. timeline: 5,
  223. attributes: { NAME: 'C' },
  224. segments: [{
  225. presentationTime: 12,
  226. timeline: 5,
  227. number: 6
  228. }, {
  229. presentationTime: 14,
  230. timeline: 5,
  231. number: 7
  232. }]
  233. }],
  234. 'new playlist updated'
  235. );
  236. });
  237. QUnit.test('complete refresh of matching playlist', function(assert) {
  238. const oldPlaylists = [{
  239. discontinuitySequence: 1,
  240. discontinuityStarts: [],
  241. mediaSequence: 5,
  242. timeline: 5,
  243. attributes: { NAME: 'C' },
  244. segments: [{
  245. presentationTime: 10,
  246. timeline: 5,
  247. number: 5
  248. }, {
  249. presentationTime: 12,
  250. timeline: 5,
  251. number: 6
  252. }, {
  253. presentationTime: 14,
  254. timeline: 5,
  255. number: 7
  256. }]
  257. }];
  258. const newPlaylists = [{
  259. discontinuitySequence: 0,
  260. discontinuityStarts: [],
  261. mediaSequence: 0,
  262. timeline: 5,
  263. attributes: { NAME: 'C' },
  264. segments: [{
  265. presentationTime: 16,
  266. timeline: 5,
  267. number: 0
  268. }, {
  269. presentationTime: 18,
  270. timeline: 5,
  271. number: 1
  272. }]
  273. }];
  274. const timelineStarts = [
  275. { start: 0, timeline: 0 },
  276. { start: 5, timeline: 5 }
  277. ];
  278. updateSequenceNumbers({ oldPlaylists, newPlaylists, timelineStarts });
  279. assert.deepEqual(
  280. newPlaylists,
  281. [{
  282. discontinuitySequence: 1,
  283. discontinuityStarts: [0],
  284. mediaSequence: 8,
  285. timeline: 5,
  286. attributes: { NAME: 'C' },
  287. segments: [{
  288. discontinuity: true,
  289. presentationTime: 16,
  290. timeline: 5,
  291. number: 8
  292. }, {
  293. presentationTime: 18,
  294. timeline: 5,
  295. number: 9
  296. }]
  297. }],
  298. 'new playlist updated after complete refresh'
  299. );
  300. });
  301. QUnit.module('positionManifestOnTimeline');
  302. QUnit.test('handles multiple playlists, including added and removed', function(assert) {
  303. const oldPlaylistA = {
  304. attributes: { NAME: 'A' },
  305. mediaSequence: 12,
  306. discontinuitySequence: 2,
  307. discontinuityStarts: [1],
  308. timelineStarts: [
  309. // only this playlist has the 20 timeline
  310. { start: 20, timeline: 20 },
  311. { start: 33, timeline: 33 }
  312. ],
  313. timeline: 20,
  314. segments: [{
  315. number: 12,
  316. timeline: 20,
  317. presentationTime: 31
  318. }, {
  319. discontinuity: true,
  320. number: 13,
  321. timeline: 33,
  322. presentationTime: 33
  323. }, {
  324. number: 14,
  325. timeline: 33,
  326. presentationTime: 35
  327. }]
  328. };
  329. const oldPlaylistB = {
  330. attributes: { NAME: 'B' },
  331. mediaSequence: 13,
  332. discontinuitySequence: 2,
  333. discontinuityStarts: [],
  334. timelineStarts: [
  335. { start: 33, timeline: 33 }
  336. ],
  337. timeline: 33,
  338. segments: [{
  339. discontinuity: true,
  340. number: 13,
  341. timeline: 33,
  342. presentationTime: 33
  343. }, {
  344. number: 14,
  345. timeline: 33,
  346. presentationTime: 35
  347. }]
  348. };
  349. // same as A just with a different name
  350. const oldPlaylistC = merge(oldPlaylistA, {
  351. attributes: { NAME: 'C' }
  352. });
  353. const newPlaylistA = {
  354. attributes: { NAME: 'A' },
  355. mediaSequence: 0,
  356. discontinuitySequence: 0,
  357. discontinuityStarts: [],
  358. timelineStarts: [
  359. { start: 33, timeline: 33 }
  360. ],
  361. timeline: 33,
  362. segments: [{
  363. // no discontinuity is marked because from the context of the new playlist, it's the
  364. // first period
  365. number: 0,
  366. timeline: 33,
  367. presentationTime: 33
  368. }, {
  369. number: 1,
  370. timeline: 33,
  371. presentationTime: 35
  372. }]
  373. };
  374. const newPlaylistB = {
  375. attributes: { NAME: 'B' },
  376. mediaSequence: 0,
  377. discontinuitySequence: 0,
  378. discontinuityStarts: [],
  379. timelineStarts: [
  380. { start: 33, timeline: 33 }
  381. ],
  382. timeline: 33,
  383. segments: [{
  384. number: 0,
  385. timeline: 33,
  386. presentationTime: 35
  387. }]
  388. };
  389. // same as B but with a different name
  390. const newPlaylistD = merge(newPlaylistB, {
  391. attributes: { NAME: 'D' }
  392. });
  393. const oldManifest = {
  394. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  395. // The manifest's timeline starts will account for all seen timeline starts. Since the
  396. // lowest playlist discontinuity sequence is 2, that means there are two additional
  397. // timeline starts here that aren't present in any playlists.
  398. timelineStarts: [
  399. { start: 10, timeline: 10 },
  400. { start: 15, timeline: 15 },
  401. { start: 20, timeline: 20 },
  402. { start: 33, timeline: 33 }
  403. ],
  404. playlists: [oldPlaylistA, oldPlaylistB, oldPlaylistC]
  405. };
  406. const newManifest = {
  407. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  408. timelineStarts: [
  409. // removed 20 timeline
  410. { start: 33, timeline: 33 }
  411. ],
  412. // removed C, added D
  413. playlists: [newPlaylistA, newPlaylistB, newPlaylistD]
  414. };
  415. assert.deepEqual(
  416. positionManifestOnTimeline({ oldManifest, newManifest }),
  417. {
  418. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  419. timelineStarts: [
  420. { start: 10, timeline: 10 },
  421. { start: 15, timeline: 15 },
  422. { start: 20, timeline: 20 },
  423. { start: 33, timeline: 33 }
  424. ],
  425. playlists: [{
  426. attributes: { NAME: 'A' },
  427. // updated mediaSequence
  428. mediaSequence: 13,
  429. // updated discontinuitySequence
  430. discontinuitySequence: 2,
  431. discontinuityStarts: [0],
  432. timelineStarts: [
  433. { start: 33, timeline: 33 }
  434. ],
  435. timeline: 33,
  436. segments: [{
  437. discontinuity: true,
  438. // updated sequence number
  439. number: 13,
  440. timeline: 33,
  441. presentationTime: 33
  442. }, {
  443. // updated sequence number
  444. number: 14,
  445. timeline: 33,
  446. presentationTime: 35
  447. }]
  448. }, {
  449. attributes: { NAME: 'B' },
  450. // updated mediaSequence
  451. mediaSequence: 14,
  452. // updated discontinuitySequence, note that it's one greater than A
  453. discontinuitySequence: 3,
  454. discontinuityStarts: [],
  455. timelineStarts: [
  456. { start: 33, timeline: 33 }
  457. ],
  458. timeline: 33,
  459. segments: [{
  460. // updated sequence number
  461. number: 14,
  462. timeline: 33,
  463. presentationTime: 35
  464. }]
  465. }, {
  466. attributes: { NAME: 'D' },
  467. // since D is a new playlist, media sequence is 0
  468. mediaSequence: 0,
  469. // updated discontinuitySequence, note that it's one greater than A
  470. discontinuitySequence: 3,
  471. discontinuityStarts: [],
  472. timelineStarts: [
  473. { start: 33, timeline: 33 }
  474. ],
  475. timeline: 33,
  476. segments: [{
  477. // updated sequence number
  478. number: 14,
  479. timeline: 33,
  480. presentationTime: 35
  481. }]
  482. }]
  483. },
  484. 'handled updated, removed, and added playlists'
  485. );
  486. });
  487. QUnit.test('handles playlist and media group playlist', function(assert) {
  488. const oldPlaylistA = {
  489. attributes: { NAME: 'A' },
  490. mediaSequence: 12,
  491. discontinuitySequence: 2,
  492. discontinuityStarts: [1],
  493. timelineStarts: [
  494. // only this playlist has the 20 timeline
  495. { start: 20, timeline: 20 },
  496. { start: 33, timeline: 33 }
  497. ],
  498. timeline: 20,
  499. segments: [{
  500. number: 12,
  501. timeline: 20,
  502. presentationTime: 31
  503. }, {
  504. discontinuity: true,
  505. number: 13,
  506. timeline: 33,
  507. presentationTime: 33
  508. }, {
  509. number: 14,
  510. timeline: 33,
  511. presentationTime: 35
  512. }]
  513. };
  514. const oldPlaylistB = {
  515. attributes: { NAME: 'B' },
  516. mediaSequence: 13,
  517. discontinuitySequence: 2,
  518. discontinuityStarts: [],
  519. timelineStarts: [
  520. { start: 33, timeline: 33 }
  521. ],
  522. timeline: 33,
  523. segments: [{
  524. discontinuity: true,
  525. number: 13,
  526. timeline: 33,
  527. presentationTime: 33
  528. }, {
  529. number: 14,
  530. timeline: 33,
  531. presentationTime: 35
  532. }]
  533. };
  534. const newPlaylistA = {
  535. attributes: { NAME: 'A' },
  536. mediaSequence: 0,
  537. discontinuitySequence: 0,
  538. discontinuityStarts: [],
  539. timelineStarts: [
  540. { start: 33, timeline: 33 }
  541. ],
  542. timeline: 33,
  543. segments: [{
  544. // no discontinuity is marked because from the context of the new playlist, it's the
  545. // first period
  546. number: 0,
  547. timeline: 33,
  548. presentationTime: 33
  549. }, {
  550. number: 1,
  551. timeline: 33,
  552. presentationTime: 35
  553. }]
  554. };
  555. const newPlaylistB = {
  556. attributes: { NAME: 'B' },
  557. mediaSequence: 0,
  558. discontinuitySequence: 0,
  559. discontinuityStarts: [],
  560. timelineStarts: [
  561. { start: 33, timeline: 33 }
  562. ],
  563. timeline: 33,
  564. segments: [{
  565. number: 0,
  566. timeline: 33,
  567. presentationTime: 35
  568. }]
  569. };
  570. const oldManifest = {
  571. mediaGroups: {
  572. AUDIO: {
  573. audio: {
  574. en: {
  575. playlists: [oldPlaylistA]
  576. }
  577. }
  578. },
  579. VIDEO: {},
  580. ['CLOSED-CAPTIONS']: {},
  581. SUBTITLES: {}
  582. },
  583. // The manifest's timeline starts will account for all seen timeline starts. Since the
  584. // lowest playlist discontinuity sequence is 2, that means there are two additional
  585. // timeline starts here that aren't present in any playlists.
  586. timelineStarts: [
  587. { start: 10, timeline: 10 },
  588. { start: 15, timeline: 15 },
  589. { start: 20, timeline: 20 },
  590. { start: 33, timeline: 33 }
  591. ],
  592. playlists: [oldPlaylistB]
  593. };
  594. const newManifest = {
  595. mediaGroups: {
  596. AUDIO: {
  597. audio: {
  598. en: {
  599. playlists: [newPlaylistA]
  600. }
  601. }
  602. },
  603. VIDEO: {},
  604. ['CLOSED-CAPTIONS']: {},
  605. SUBTITLES: {}
  606. },
  607. timelineStarts: [
  608. // removed 20 timeline
  609. { start: 33, timeline: 33 }
  610. ],
  611. // removed C, added D
  612. playlists: [newPlaylistB]
  613. };
  614. assert.deepEqual(
  615. positionManifestOnTimeline({ oldManifest, newManifest }),
  616. {
  617. mediaGroups: {
  618. AUDIO: {
  619. audio: {
  620. en: {
  621. playlists: [{
  622. attributes: { NAME: 'A' },
  623. // updated mediaSequence
  624. mediaSequence: 13,
  625. // updated discontinuitySequence
  626. discontinuitySequence: 2,
  627. discontinuityStarts: [0],
  628. timelineStarts: [
  629. { start: 33, timeline: 33 }
  630. ],
  631. timeline: 33,
  632. segments: [{
  633. discontinuity: true,
  634. // updated sequence number
  635. number: 13,
  636. timeline: 33,
  637. presentationTime: 33
  638. }, {
  639. // updated sequence number
  640. number: 14,
  641. timeline: 33,
  642. presentationTime: 35
  643. }]
  644. }]
  645. }
  646. }
  647. },
  648. VIDEO: {},
  649. ['CLOSED-CAPTIONS']: {},
  650. SUBTITLES: {}
  651. },
  652. timelineStarts: [
  653. { start: 10, timeline: 10 },
  654. { start: 15, timeline: 15 },
  655. { start: 20, timeline: 20 },
  656. { start: 33, timeline: 33 }
  657. ],
  658. playlists: [{
  659. attributes: { NAME: 'B' },
  660. // updated mediaSequence
  661. mediaSequence: 14,
  662. // updated discontinuitySequence, note that it's one greater than A
  663. discontinuitySequence: 3,
  664. discontinuityStarts: [],
  665. timelineStarts: [
  666. { start: 33, timeline: 33 }
  667. ],
  668. timeline: 33,
  669. segments: [{
  670. // updated sequence number
  671. number: 14,
  672. timeline: 33,
  673. presentationTime: 35
  674. }]
  675. }]
  676. },
  677. 'handled playlist and media group playlist'
  678. );
  679. });
  680. QUnit.test('complete refresh same timeline', function(assert) {
  681. const oldPlaylistA = {
  682. attributes: { NAME: 'A' },
  683. mediaSequence: 12,
  684. discontinuitySequence: 2,
  685. discontinuityStarts: [1],
  686. timelineStarts: [
  687. { start: 20, timeline: 20 },
  688. { start: 33, timeline: 33 }
  689. ],
  690. timeline: 20,
  691. segments: [{
  692. number: 12,
  693. timeline: 20,
  694. presentationTime: 31
  695. }, {
  696. discontinuity: true,
  697. number: 13,
  698. timeline: 33,
  699. presentationTime: 33
  700. }, {
  701. number: 14,
  702. timeline: 33,
  703. presentationTime: 35
  704. }]
  705. };
  706. const newPlaylistA = {
  707. attributes: { NAME: 'A' },
  708. mediaSequence: 0,
  709. discontinuitySequence: 0,
  710. discontinuityStarts: [],
  711. timelineStarts: [
  712. { start: 33, timeline: 33 }
  713. ],
  714. timeline: 33,
  715. segments: [{
  716. number: 0,
  717. timeline: 33,
  718. // missed a large portion of time, but still within same timeline
  719. presentationTime: 50
  720. }, {
  721. number: 1,
  722. timeline: 33,
  723. presentationTime: 52
  724. }]
  725. };
  726. const oldManifest = {
  727. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  728. // The manifest's timeline starts will account for all seen timeline starts. Since the
  729. // lowest playlist discontinuity sequence is 2, that means there are two additional
  730. // timeline starts here that aren't present in any playlists.
  731. timelineStarts: [
  732. { start: 10, timeline: 10 },
  733. { start: 15, timeline: 15 },
  734. { start: 20, timeline: 20 },
  735. { start: 33, timeline: 33 }
  736. ],
  737. playlists: [oldPlaylistA]
  738. };
  739. const newManifest = {
  740. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  741. timelineStarts: [
  742. // removed 20 timeline
  743. { start: 33, timeline: 33 }
  744. ],
  745. playlists: [newPlaylistA]
  746. };
  747. assert.deepEqual(
  748. positionManifestOnTimeline({ oldManifest, newManifest }),
  749. {
  750. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  751. timelineStarts: [
  752. { start: 10, timeline: 10 },
  753. { start: 15, timeline: 15 },
  754. { start: 20, timeline: 20 },
  755. { start: 33, timeline: 33 }
  756. ],
  757. playlists: [{
  758. attributes: { NAME: 'A' },
  759. // updated mediaSequence to one greater than last seen
  760. mediaSequence: 15,
  761. // updated discontinuitySequence to account for timeline 33 discontinuity falling
  762. // off
  763. discontinuitySequence: 3,
  764. // added discontinuity to beginning
  765. discontinuityStarts: [0],
  766. timelineStarts: [
  767. { start: 33, timeline: 33 }
  768. ],
  769. timeline: 33,
  770. segments: [{
  771. discontinuity: true,
  772. // updated sequence number
  773. number: 15,
  774. timeline: 33,
  775. presentationTime: 50
  776. }, {
  777. // updated sequence number
  778. number: 16,
  779. timeline: 33,
  780. presentationTime: 52
  781. }]
  782. }]
  783. },
  784. 'handled complete refresh on same timeline'
  785. );
  786. });
  787. QUnit.test('complete refresh different timeline', function(assert) {
  788. const oldPlaylistA = {
  789. attributes: { NAME: 'A' },
  790. mediaSequence: 12,
  791. // timeline 10 is the start (thus no disco)
  792. // removed 1 disco at timeline 15, and another for the start of timeline 20
  793. discontinuitySequence: 2,
  794. discontinuityStarts: [1],
  795. timelineStarts: [
  796. // only this playlist has the 20 timeline
  797. { start: 20, timeline: 20 },
  798. { start: 33, timeline: 33 }
  799. ],
  800. timeline: 20,
  801. segments: [{
  802. number: 12,
  803. timeline: 20,
  804. presentationTime: 31
  805. }, {
  806. discontinuity: true,
  807. number: 13,
  808. timeline: 33,
  809. presentationTime: 33
  810. }, {
  811. number: 14,
  812. timeline: 33,
  813. presentationTime: 35
  814. }]
  815. };
  816. const newPlaylistA = {
  817. attributes: { NAME: 'A' },
  818. mediaSequence: 0,
  819. discontinuitySequence: 0,
  820. discontinuityStarts: [],
  821. timelineStarts: [
  822. { start: 50, timeline: 50 }
  823. ],
  824. timeline: 50,
  825. segments: [{
  826. number: 0,
  827. // new timeline
  828. timeline: 50,
  829. presentationTime: 50
  830. }, {
  831. number: 1,
  832. timeline: 50,
  833. presentationTime: 52
  834. }]
  835. };
  836. const oldManifest = {
  837. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  838. // The manifest's timeline starts will account for all seen timeline starts. Since the
  839. // lowest playlist discontinuity sequence is 2, that means there are two additional
  840. // timeline starts here that aren't present in any playlists.
  841. timelineStarts: [
  842. { start: 10, timeline: 10 },
  843. { start: 15, timeline: 15 },
  844. { start: 20, timeline: 20 },
  845. { start: 33, timeline: 33 }
  846. ],
  847. playlists: [oldPlaylistA]
  848. };
  849. const newManifest = {
  850. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  851. timelineStarts: [
  852. // removed 20 and 33 timelines, added 50
  853. { start: 50, timeline: 50 }
  854. ],
  855. playlists: [newPlaylistA]
  856. };
  857. assert.deepEqual(
  858. positionManifestOnTimeline({ oldManifest, newManifest }),
  859. {
  860. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  861. timelineStarts: [
  862. { start: 10, timeline: 10 },
  863. { start: 15, timeline: 15 },
  864. { start: 20, timeline: 20 },
  865. { start: 33, timeline: 33 },
  866. // added new timeline and retained the ones from before
  867. { start: 50, timeline: 50 }
  868. ],
  869. playlists: [{
  870. attributes: { NAME: 'A' },
  871. // updated mediaSequence to one greater than last seen
  872. mediaSequence: 15,
  873. // updated discontinuitySequence to account for removed discos (previously the
  874. // disco at 15 and 20, plus the disco starting timeline 33 on the refresh)
  875. discontinuitySequence: 3,
  876. // added discontinuity to beginning
  877. discontinuityStarts: [0],
  878. timelineStarts: [
  879. { start: 50, timeline: 50 }
  880. ],
  881. timeline: 50,
  882. segments: [{
  883. discontinuity: true,
  884. // updated sequence number
  885. number: 15,
  886. timeline: 50,
  887. presentationTime: 50
  888. }, {
  889. // updated sequence number
  890. number: 16,
  891. timeline: 50,
  892. presentationTime: 52
  893. }]
  894. }]
  895. },
  896. 'handled complete refresh on different timeline'
  897. );
  898. });
  899. QUnit.test('no change, first segment a discontinuity', function(assert) {
  900. const oldPlaylistA = {
  901. attributes: { NAME: 'A' },
  902. mediaSequence: 13,
  903. discontinuitySequence: 2,
  904. discontinuityStarts: [0],
  905. timelineStarts: [
  906. { start: 33, timeline: 33 }
  907. ],
  908. timeline: 33,
  909. segments: [{
  910. discontinuity: true,
  911. number: 13,
  912. timeline: 33,
  913. presentationTime: 33
  914. }, {
  915. number: 14,
  916. timeline: 33,
  917. presentationTime: 35
  918. }]
  919. };
  920. const newPlaylistA = {
  921. attributes: { NAME: 'A' },
  922. mediaSequence: 0,
  923. discontinuitySequence: 0,
  924. discontinuityStarts: [],
  925. timelineStarts: [
  926. { start: 33, timeline: 33 }
  927. ],
  928. timeline: 33,
  929. segments: [{
  930. // no discontinuity marked for the refresh, should be added by merging logic
  931. number: 0,
  932. timeline: 33,
  933. presentationTime: 33
  934. }, {
  935. number: 1,
  936. timeline: 33,
  937. presentationTime: 35
  938. }]
  939. };
  940. const oldManifest = {
  941. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  942. // The manifest's timeline starts will account for all seen timeline starts. Since the
  943. // lowest playlist discontinuity sequence is 3, that means there are three additional
  944. // timeline starts here that aren't present in any playlists, plus the original at 0.
  945. timelineStarts: [
  946. { start: 10, timeline: 10 },
  947. { start: 15, timeline: 15 },
  948. { start: 20, timeline: 20 },
  949. { start: 33, timeline: 33 }
  950. ],
  951. playlists: [oldPlaylistA]
  952. };
  953. const newManifest = {
  954. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  955. timelineStarts: [{ start: 33, timeline: 33 }],
  956. playlists: [newPlaylistA]
  957. };
  958. assert.deepEqual(
  959. positionManifestOnTimeline({ oldManifest, newManifest }),
  960. {
  961. mediaGroups: { AUDIO: {}, VIDEO: {}, ['CLOSED-CAPTIONS']: {}, SUBTITLES: {} },
  962. timelineStarts: [
  963. { start: 10, timeline: 10 },
  964. { start: 15, timeline: 15 },
  965. { start: 20, timeline: 20 },
  966. { start: 33, timeline: 33 }
  967. ],
  968. playlists: [{
  969. attributes: { NAME: 'A' },
  970. mediaSequence: 13,
  971. discontinuitySequence: 2,
  972. // discontinuity at beginning
  973. discontinuityStarts: [0],
  974. timelineStarts: [
  975. { start: 33, timeline: 33 }
  976. ],
  977. timeline: 33,
  978. segments: [{
  979. // discontinuity added to new playlist
  980. discontinuity: true,
  981. // updated sequence number
  982. number: 13,
  983. timeline: 33,
  984. presentationTime: 33
  985. }, {
  986. // updated sequence number
  987. number: 14,
  988. timeline: 33,
  989. presentationTime: 35
  990. }]
  991. }]
  992. },
  993. 'handled no change with first segment a discontinuity'
  994. );
  995. });