Home Reference Source

src/controller/level-helper.ts

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