level-helper.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. /**
  2. * Provides methods dealing with playlist sliding and drift
  3. */
  4. import { logger } from './logger';
  5. import { stringify } from './safe-json-stringify';
  6. import { DateRange } from '../loader/date-range';
  7. import { assignProgramDateTime, mapDateRanges } from '../loader/m3u8-parser';
  8. import type { Fragment, MediaFragment, Part } from '../loader/fragment';
  9. import type { LevelDetails } from '../loader/level-details';
  10. import type { Level } from '../types/level';
  11. type FragmentIntersection = (
  12. oldFrag: MediaFragment,
  13. newFrag: MediaFragment,
  14. newFragIndex: number,
  15. newFragments: MediaFragment[],
  16. ) => void;
  17. type PartIntersection = (oldPart: Part, newPart: Part) => void;
  18. export function updatePTS(
  19. fragments: MediaFragment[],
  20. fromIdx: number,
  21. toIdx: number,
  22. ): void {
  23. const fragFrom = fragments[fromIdx];
  24. const fragTo = fragments[toIdx];
  25. updateFromToPTS(fragFrom, fragTo);
  26. }
  27. function updateFromToPTS(fragFrom: MediaFragment, fragTo: MediaFragment) {
  28. const fragToPTS = fragTo.startPTS as number;
  29. // if we know startPTS[toIdx]
  30. if (Number.isFinite(fragToPTS)) {
  31. // update fragment duration.
  32. // it helps to fix drifts between playlist reported duration and fragment real duration
  33. let duration: number = 0;
  34. let frag: Fragment;
  35. if (fragTo.sn > fragFrom.sn) {
  36. duration = fragToPTS - fragFrom.start;
  37. frag = fragFrom;
  38. } else {
  39. duration = fragFrom.start - fragToPTS;
  40. frag = fragTo;
  41. }
  42. if (frag.duration !== duration) {
  43. frag.setDuration(duration);
  44. }
  45. // we dont know startPTS[toIdx]
  46. } else if (fragTo.sn > fragFrom.sn) {
  47. const contiguous = fragFrom.cc === fragTo.cc;
  48. // TODO: With part-loading end/durations we need to confirm the whole fragment is loaded before using (or setting) minEndPTS
  49. if (contiguous && fragFrom.minEndPTS) {
  50. fragTo.setStart(fragFrom.start + (fragFrom.minEndPTS - fragFrom.start));
  51. } else {
  52. fragTo.setStart(fragFrom.start + fragFrom.duration);
  53. }
  54. } else {
  55. fragTo.setStart(Math.max(fragFrom.start - fragTo.duration, 0));
  56. }
  57. }
  58. export function updateFragPTSDTS(
  59. details: LevelDetails | undefined,
  60. frag: MediaFragment,
  61. startPTS: number,
  62. endPTS: number,
  63. startDTS: number,
  64. endDTS: number,
  65. ): number {
  66. const parsedMediaDuration = endPTS - startPTS;
  67. if (parsedMediaDuration <= 0) {
  68. logger.warn('Fragment should have a positive duration', frag);
  69. endPTS = startPTS + frag.duration;
  70. endDTS = startDTS + frag.duration;
  71. }
  72. let maxStartPTS = startPTS;
  73. let minEndPTS = endPTS;
  74. const fragStartPts = frag.startPTS as number;
  75. const fragEndPts = frag.endPTS as number;
  76. if (Number.isFinite(fragStartPts)) {
  77. // delta PTS between audio and video
  78. const deltaPTS = Math.abs(fragStartPts - startPTS);
  79. if (!Number.isFinite(frag.deltaPTS as number)) {
  80. frag.deltaPTS = deltaPTS;
  81. } else {
  82. frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS as number);
  83. }
  84. maxStartPTS = Math.max(startPTS, fragStartPts);
  85. startPTS = Math.min(startPTS, fragStartPts);
  86. startDTS = Math.min(startDTS, frag.startDTS as number);
  87. minEndPTS = Math.min(endPTS, fragEndPts);
  88. endPTS = Math.max(endPTS, fragEndPts);
  89. endDTS = Math.max(endDTS, frag.endDTS as number);
  90. }
  91. const drift = startPTS - frag.start;
  92. if (frag.start !== 0) {
  93. frag.setStart(startPTS);
  94. }
  95. frag.setDuration(endPTS - frag.start);
  96. frag.startPTS = startPTS;
  97. frag.maxStartPTS = maxStartPTS;
  98. frag.startDTS = startDTS;
  99. frag.endPTS = endPTS;
  100. frag.minEndPTS = minEndPTS;
  101. frag.endDTS = endDTS;
  102. const sn = frag.sn;
  103. // exit if sn out of range
  104. if (!details || sn < details.startSN || sn > details.endSN) {
  105. return 0;
  106. }
  107. let i: number;
  108. const fragIdx = sn - details.startSN;
  109. const fragments = details.fragments;
  110. // update frag reference in fragments array
  111. // rationale is that fragments array might not contain this frag object.
  112. // this will happen if playlist has been refreshed between frag loading and call to updateFragPTSDTS()
  113. // if we don't update frag, we won't be able to propagate PTS info on the playlist
  114. // resulting in invalid sliding computation
  115. fragments[fragIdx] = frag;
  116. // adjust fragment PTS/duration from seqnum-1 to frag 0
  117. for (i = fragIdx; i > 0; i--) {
  118. updateFromToPTS(fragments[i], fragments[i - 1]);
  119. }
  120. // adjust fragment PTS/duration from seqnum to last frag
  121. for (i = fragIdx; i < fragments.length - 1; i++) {
  122. updateFromToPTS(fragments[i], fragments[i + 1]);
  123. }
  124. if (details.fragmentHint) {
  125. updateFromToPTS(fragments[fragments.length - 1], details.fragmentHint);
  126. }
  127. details.PTSKnown = details.alignedSliding = true;
  128. return drift;
  129. }
  130. export function mergeDetails(
  131. oldDetails: LevelDetails,
  132. newDetails: LevelDetails,
  133. ) {
  134. if (oldDetails === newDetails) {
  135. return;
  136. }
  137. // Track the last initSegment processed. Initialize it to the last one on the timeline.
  138. let currentInitSegment: Fragment | null = null;
  139. const oldFragments = oldDetails.fragments;
  140. for (let i = oldFragments.length - 1; i >= 0; i--) {
  141. const oldInit = oldFragments[i].initSegment;
  142. if (oldInit) {
  143. currentInitSegment = oldInit;
  144. break;
  145. }
  146. }
  147. if (oldDetails.fragmentHint) {
  148. // prevent PTS and duration from being adjusted on the next hint
  149. delete oldDetails.fragmentHint.endPTS;
  150. }
  151. // check if old/new playlists have fragments in common
  152. // loop through overlapping SN and update startPTS, cc, and duration if any found
  153. let PTSFrag: MediaFragment | undefined;
  154. mapFragmentIntersection(
  155. oldDetails,
  156. newDetails,
  157. (oldFrag, newFrag, newFragIndex, newFragments) => {
  158. if (
  159. (!newDetails.startCC || newDetails.skippedSegments) &&
  160. newFrag.cc !== oldFrag.cc
  161. ) {
  162. const ccOffset = oldFrag.cc - newFrag.cc;
  163. for (let i = newFragIndex; i < newFragments.length; i++) {
  164. newFragments[i].cc += ccOffset;
  165. }
  166. newDetails.endCC = newFragments[newFragments.length - 1].cc;
  167. }
  168. if (
  169. Number.isFinite(oldFrag.startPTS) &&
  170. Number.isFinite(oldFrag.endPTS)
  171. ) {
  172. newFrag.setStart((newFrag.startPTS = oldFrag.startPTS!));
  173. newFrag.startDTS = oldFrag.startDTS;
  174. newFrag.maxStartPTS = oldFrag.maxStartPTS;
  175. newFrag.endPTS = oldFrag.endPTS;
  176. newFrag.endDTS = oldFrag.endDTS;
  177. newFrag.minEndPTS = oldFrag.minEndPTS;
  178. newFrag.setDuration(oldFrag.endPTS! - oldFrag.startPTS!);
  179. if (newFrag.duration) {
  180. PTSFrag = newFrag;
  181. }
  182. // PTS is known when any segment has startPTS and endPTS
  183. newDetails.PTSKnown = newDetails.alignedSliding = true;
  184. }
  185. if (oldFrag.hasStreams) {
  186. newFrag.elementaryStreams = oldFrag.elementaryStreams;
  187. }
  188. newFrag.loader = oldFrag.loader;
  189. if (oldFrag.hasStats) {
  190. newFrag.stats = oldFrag.stats;
  191. }
  192. if (oldFrag.initSegment) {
  193. newFrag.initSegment = oldFrag.initSegment;
  194. currentInitSegment = oldFrag.initSegment;
  195. }
  196. },
  197. );
  198. const newFragments = newDetails.fragments;
  199. const fragmentsToCheck = newDetails.fragmentHint
  200. ? newFragments.concat(newDetails.fragmentHint)
  201. : newFragments;
  202. if (currentInitSegment) {
  203. fragmentsToCheck.forEach((frag) => {
  204. if (
  205. frag &&
  206. (!frag.initSegment ||
  207. frag.initSegment.relurl === currentInitSegment?.relurl)
  208. ) {
  209. frag.initSegment = currentInitSegment;
  210. }
  211. });
  212. }
  213. if (newDetails.skippedSegments) {
  214. newDetails.deltaUpdateFailed = newFragments.some((frag) => !frag);
  215. if (newDetails.deltaUpdateFailed) {
  216. logger.warn(
  217. '[level-helper] Previous playlist missing segments skipped in delta playlist',
  218. );
  219. for (let i = newDetails.skippedSegments; i--; ) {
  220. newFragments.shift();
  221. }
  222. newDetails.startSN = newFragments[0].sn;
  223. } else {
  224. if (newDetails.canSkipDateRanges) {
  225. newDetails.dateRanges = mergeDateRanges(
  226. oldDetails.dateRanges,
  227. newDetails,
  228. );
  229. }
  230. const programDateTimes = oldDetails.fragments.filter(
  231. (frag) => frag.rawProgramDateTime,
  232. );
  233. if (oldDetails.hasProgramDateTime && !newDetails.hasProgramDateTime) {
  234. for (let i = 1; i < fragmentsToCheck.length; i++) {
  235. if (fragmentsToCheck[i].programDateTime === null) {
  236. assignProgramDateTime(
  237. fragmentsToCheck[i],
  238. fragmentsToCheck[i - 1],
  239. programDateTimes,
  240. );
  241. }
  242. }
  243. }
  244. mapDateRanges(programDateTimes, newDetails);
  245. }
  246. newDetails.endCC = newFragments[newFragments.length - 1].cc;
  247. }
  248. if (!newDetails.startCC) {
  249. const fragPriorToNewStart = getFragmentWithSN(
  250. oldDetails,
  251. newDetails.startSN - 1,
  252. );
  253. newDetails.startCC = fragPriorToNewStart?.cc ?? newFragments[0].cc;
  254. }
  255. // Merge parts
  256. mapPartIntersection(
  257. oldDetails.partList,
  258. newDetails.partList,
  259. (oldPart: Part, newPart: Part) => {
  260. newPart.elementaryStreams = oldPart.elementaryStreams;
  261. newPart.stats = oldPart.stats;
  262. },
  263. );
  264. // if at least one fragment contains PTS info, recompute PTS information for all fragments
  265. if (PTSFrag) {
  266. updateFragPTSDTS(
  267. newDetails,
  268. PTSFrag,
  269. PTSFrag.startPTS as number,
  270. PTSFrag.endPTS as number,
  271. PTSFrag.startDTS as number,
  272. PTSFrag.endDTS as number,
  273. );
  274. } else {
  275. // ensure that delta is within oldFragments range
  276. // also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61])
  277. // in that case we also need to adjust start offset of all fragments
  278. adjustSliding(oldDetails, newDetails);
  279. }
  280. if (newFragments.length) {
  281. newDetails.totalduration = newDetails.edge - newFragments[0].start;
  282. }
  283. newDetails.driftStartTime = oldDetails.driftStartTime;
  284. newDetails.driftStart = oldDetails.driftStart;
  285. const advancedDateTime = newDetails.advancedDateTime;
  286. if (newDetails.advanced && advancedDateTime) {
  287. const edge = newDetails.edge;
  288. if (!newDetails.driftStart) {
  289. newDetails.driftStartTime = advancedDateTime;
  290. newDetails.driftStart = edge;
  291. }
  292. newDetails.driftEndTime = advancedDateTime;
  293. newDetails.driftEnd = edge;
  294. } else {
  295. newDetails.driftEndTime = oldDetails.driftEndTime;
  296. newDetails.driftEnd = oldDetails.driftEnd;
  297. newDetails.advancedDateTime = oldDetails.advancedDateTime;
  298. }
  299. if (newDetails.requestScheduled === -1) {
  300. newDetails.requestScheduled = oldDetails.requestScheduled;
  301. }
  302. }
  303. function mergeDateRanges(
  304. oldDateRanges: Record<string, DateRange>,
  305. newDetails: LevelDetails,
  306. ): Record<string, DateRange> {
  307. const { dateRanges: deltaDateRanges, recentlyRemovedDateranges } = newDetails;
  308. const dateRanges = Object.assign({}, oldDateRanges);
  309. if (recentlyRemovedDateranges) {
  310. recentlyRemovedDateranges.forEach((id) => {
  311. delete dateRanges[id];
  312. });
  313. }
  314. const mergeIds = Object.keys(dateRanges);
  315. const mergeCount = mergeIds.length;
  316. if (mergeCount) {
  317. Object.keys(deltaDateRanges).forEach((id) => {
  318. const mergedDateRange = dateRanges[id];
  319. const dateRange = new DateRange(
  320. deltaDateRanges[id].attr,
  321. mergedDateRange,
  322. );
  323. if (dateRange.isValid) {
  324. dateRanges[id] = dateRange;
  325. if (!mergedDateRange) {
  326. dateRange.tagOrder += mergeCount;
  327. }
  328. } else {
  329. logger.warn(
  330. `Ignoring invalid Playlist Delta Update DATERANGE tag: "${stringify(
  331. deltaDateRanges[id].attr,
  332. )}"`,
  333. );
  334. }
  335. });
  336. }
  337. return dateRanges;
  338. }
  339. export function mapPartIntersection(
  340. oldParts: Part[] | null,
  341. newParts: Part[] | null,
  342. intersectionFn: PartIntersection,
  343. ) {
  344. if (oldParts && newParts) {
  345. let delta = 0;
  346. for (let i = 0, len = oldParts.length; i <= len; i++) {
  347. const oldPart = oldParts[i];
  348. const newPart = newParts[i + delta];
  349. if (
  350. oldPart &&
  351. newPart &&
  352. oldPart.index === newPart.index &&
  353. oldPart.fragment.sn === newPart.fragment.sn
  354. ) {
  355. intersectionFn(oldPart, newPart);
  356. } else {
  357. delta--;
  358. }
  359. }
  360. }
  361. }
  362. export function mapFragmentIntersection(
  363. oldDetails: LevelDetails,
  364. newDetails: LevelDetails,
  365. intersectionFn: FragmentIntersection,
  366. ) {
  367. const skippedSegments = newDetails.skippedSegments;
  368. const start =
  369. Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN;
  370. const end =
  371. (oldDetails.fragmentHint ? 1 : 0) +
  372. (skippedSegments
  373. ? newDetails.endSN
  374. : Math.min(oldDetails.endSN, newDetails.endSN)) -
  375. newDetails.startSN;
  376. const delta = newDetails.startSN - oldDetails.startSN;
  377. const newFrags = newDetails.fragmentHint
  378. ? newDetails.fragments.concat(newDetails.fragmentHint)
  379. : newDetails.fragments;
  380. const oldFrags = oldDetails.fragmentHint
  381. ? oldDetails.fragments.concat(oldDetails.fragmentHint)
  382. : oldDetails.fragments;
  383. for (let i = start; i <= end; i++) {
  384. const oldFrag = oldFrags[delta + i];
  385. let newFrag = newFrags[i];
  386. if (skippedSegments && !newFrag && oldFrag) {
  387. // Fill in skipped segments in delta playlist
  388. newFrag = newDetails.fragments[i] = oldFrag;
  389. }
  390. if (oldFrag && newFrag) {
  391. intersectionFn(oldFrag, newFrag, i, newFrags);
  392. if (oldFrag.url && oldFrag.url !== newFrag.url) {
  393. newDetails.playlistParsingError = getSequenceError(
  394. `media sequence mismatch ${newFrag.sn}:`,
  395. oldDetails,
  396. newDetails,
  397. oldFrag,
  398. newFrag,
  399. );
  400. return;
  401. } else if (oldFrag.cc !== newFrag.cc) {
  402. newDetails.playlistParsingError = getSequenceError(
  403. `discontinuity sequence mismatch (${oldFrag.cc}!=${newFrag.cc})`,
  404. oldDetails,
  405. newDetails,
  406. oldFrag,
  407. newFrag,
  408. );
  409. return;
  410. }
  411. }
  412. }
  413. }
  414. function getSequenceError(
  415. message: string,
  416. oldDetails: LevelDetails,
  417. newDetails: LevelDetails,
  418. oldFrag: MediaFragment,
  419. newFrag: MediaFragment,
  420. ): Error {
  421. return new Error(
  422. `${message} ${newFrag.url}
  423. Playlist starting @${oldDetails.startSN}
  424. ${oldDetails.m3u8}
  425. Playlist starting @${newDetails.startSN}
  426. ${newDetails.m3u8}`,
  427. );
  428. }
  429. export function adjustSliding(
  430. oldDetails: LevelDetails,
  431. newDetails: LevelDetails,
  432. matchingStableVariantOrRendition: boolean = true,
  433. ): void {
  434. const delta =
  435. newDetails.startSN + newDetails.skippedSegments - oldDetails.startSN;
  436. const oldFragments = oldDetails.fragments;
  437. const advancedOrStable = delta >= 0;
  438. let sliding = 0;
  439. if (advancedOrStable && delta < oldFragments.length) {
  440. sliding = oldFragments[delta].start;
  441. } else if (advancedOrStable && newDetails.startSN === oldDetails.endSN + 1) {
  442. sliding = oldDetails.fragmentEnd;
  443. } else if (advancedOrStable && matchingStableVariantOrRendition) {
  444. // align with expected position (updated playlist start sequence is past end sequence of last update)
  445. sliding = oldDetails.fragmentStart + delta * newDetails.levelTargetDuration;
  446. } else if (!newDetails.skippedSegments && newDetails.fragmentStart === 0) {
  447. // align new start with old (playlist switch has a sequence with no overlap and should not be used for alignment)
  448. sliding = oldDetails.fragmentStart;
  449. } else {
  450. // new details already has a sliding offset or has skipped segments
  451. return;
  452. }
  453. addSliding(newDetails, sliding);
  454. }
  455. export function addSliding(details: LevelDetails, sliding: number) {
  456. if (sliding) {
  457. const fragments = details.fragments;
  458. for (let i = details.skippedSegments; i < fragments.length; i++) {
  459. fragments[i].addStart(sliding);
  460. }
  461. if (details.fragmentHint) {
  462. details.fragmentHint.addStart(sliding);
  463. }
  464. }
  465. }
  466. export function computeReloadInterval(
  467. newDetails: LevelDetails,
  468. distanceToLiveEdgeMs: number = Infinity,
  469. ): number {
  470. let reloadInterval = 1000 * newDetails.targetduration;
  471. if (newDetails.updated) {
  472. // Use last segment duration when shorter than target duration and near live edge
  473. const fragments = newDetails.fragments;
  474. const liveEdgeMaxTargetDurations = 4;
  475. if (
  476. fragments.length &&
  477. reloadInterval * liveEdgeMaxTargetDurations > distanceToLiveEdgeMs
  478. ) {
  479. const lastSegmentDuration =
  480. fragments[fragments.length - 1].duration * 1000;
  481. if (lastSegmentDuration < reloadInterval) {
  482. reloadInterval = lastSegmentDuration;
  483. }
  484. }
  485. } else {
  486. // estimate = 'miss half average';
  487. // follow HLS Spec, If the client reloads a Playlist file and finds that it has not
  488. // changed then it MUST wait for a period of one-half the target
  489. // duration before retrying.
  490. reloadInterval /= 2;
  491. }
  492. return Math.round(reloadInterval);
  493. }
  494. export function getFragmentWithSN(
  495. details: LevelDetails | undefined,
  496. sn: number,
  497. fragCurrent?: Fragment | null,
  498. ): MediaFragment | null {
  499. if (!details) {
  500. return null;
  501. }
  502. let fragment: MediaFragment | undefined =
  503. details.fragments[sn - details.startSN];
  504. if (fragment) {
  505. return fragment;
  506. }
  507. fragment = details.fragmentHint;
  508. if (fragment && fragment.sn === sn) {
  509. return fragment;
  510. }
  511. if (sn < details.startSN && fragCurrent && fragCurrent.sn === sn) {
  512. return fragCurrent as MediaFragment;
  513. }
  514. return null;
  515. }
  516. export function getPartWith(
  517. details: LevelDetails | undefined,
  518. sn: number,
  519. partIndex: number,
  520. ): Part | null {
  521. if (!details) {
  522. return null;
  523. }
  524. return findPart(details.partList, sn, partIndex);
  525. }
  526. export function findPart(
  527. partList: Part[] | null | undefined,
  528. sn: number,
  529. partIndex: number,
  530. ): Part | null {
  531. if (partList) {
  532. for (let i = partList.length; i--; ) {
  533. const part = partList[i];
  534. if (part.index === partIndex && part.fragment.sn === sn) {
  535. return part;
  536. }
  537. }
  538. }
  539. return null;
  540. }
  541. export function reassignFragmentLevelIndexes(levels: Level[]) {
  542. levels.forEach((level, index) => {
  543. level.details?.fragments.forEach((fragment) => {
  544. fragment.level = index;
  545. if (fragment.initSegment) {
  546. fragment.initSegment.level = index;
  547. }
  548. });
  549. });
  550. }