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