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. }
  227. }
  228. );
  229.  
  230. if (currentInitSegment) {
  231. const fragmentsToCheck = newDetails.fragmentHint
  232. ? newDetails.fragments.concat(newDetails.fragmentHint)
  233. : newDetails.fragments;
  234. fragmentsToCheck.forEach((frag) => {
  235. if (
  236. !frag.initSegment ||
  237. frag.initSegment.relurl === currentInitSegment?.relurl
  238. ) {
  239. frag.initSegment = currentInitSegment;
  240. }
  241. });
  242. }
  243.  
  244. if (newDetails.skippedSegments) {
  245. newDetails.deltaUpdateFailed = newDetails.fragments.some((frag) => !frag);
  246. if (newDetails.deltaUpdateFailed) {
  247. logger.warn(
  248. '[level-helper] Previous playlist missing segments skipped in delta playlist'
  249. );
  250. for (let i = newDetails.skippedSegments; i--; ) {
  251. newDetails.fragments.shift();
  252. }
  253. newDetails.startSN = newDetails.fragments[0].sn as number;
  254. newDetails.startCC = newDetails.fragments[0].cc;
  255. }
  256. }
  257.  
  258. const newFragments = newDetails.fragments;
  259. if (ccOffset) {
  260. logger.warn('discontinuity sliding from playlist, take drift into account');
  261. for (let i = 0; i < newFragments.length; i++) {
  262. newFragments[i].cc += ccOffset;
  263. }
  264. }
  265. if (newDetails.skippedSegments) {
  266. newDetails.startCC = newDetails.fragments[0].cc;
  267. }
  268.  
  269. // Merge parts
  270. mapPartIntersection(
  271. oldDetails.partList,
  272. newDetails.partList,
  273. (oldPart: Part, newPart: Part) => {
  274. newPart.elementaryStreams = oldPart.elementaryStreams;
  275. newPart.stats = oldPart.stats;
  276. }
  277. );
  278.  
  279. // if at least one fragment contains PTS info, recompute PTS information for all fragments
  280. if (PTSFrag) {
  281. updateFragPTSDTS(
  282. newDetails,
  283. PTSFrag,
  284. PTSFrag.startPTS,
  285. PTSFrag.endPTS,
  286. PTSFrag.startDTS,
  287. PTSFrag.endDTS
  288. );
  289. } else {
  290. // ensure that delta is within oldFragments range
  291. // also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61])
  292. // in that case we also need to adjust start offset of all fragments
  293. adjustSliding(oldDetails, newDetails);
  294. }
  295.  
  296. if (newFragments.length) {
  297. newDetails.totalduration = newDetails.edge - newFragments[0].start;
  298. }
  299.  
  300. newDetails.driftStartTime = oldDetails.driftStartTime;
  301. newDetails.driftStart = oldDetails.driftStart;
  302. const advancedDateTime = newDetails.advancedDateTime;
  303. if (newDetails.advanced && advancedDateTime) {
  304. const edge = newDetails.edge;
  305. if (!newDetails.driftStart) {
  306. newDetails.driftStartTime = advancedDateTime;
  307. newDetails.driftStart = edge;
  308. }
  309. newDetails.driftEndTime = advancedDateTime;
  310. newDetails.driftEnd = edge;
  311. } else {
  312. newDetails.driftEndTime = oldDetails.driftEndTime;
  313. newDetails.driftEnd = oldDetails.driftEnd;
  314. newDetails.advancedDateTime = oldDetails.advancedDateTime;
  315. }
  316. }
  317.  
  318. export function mapPartIntersection(
  319. oldParts: Part[] | null,
  320. newParts: Part[] | null,
  321. intersectionFn: PartIntersection
  322. ) {
  323. if (oldParts && newParts) {
  324. let delta = 0;
  325. for (let i = 0, len = oldParts.length; i <= len; i++) {
  326. const oldPart = oldParts[i];
  327. const newPart = newParts[i + delta];
  328. if (
  329. oldPart &&
  330. newPart &&
  331. oldPart.index === newPart.index &&
  332. oldPart.fragment.sn === newPart.fragment.sn
  333. ) {
  334. intersectionFn(oldPart, newPart);
  335. } else {
  336. delta--;
  337. }
  338. }
  339. }
  340. }
  341.  
  342. export function mapFragmentIntersection(
  343. oldDetails: LevelDetails,
  344. newDetails: LevelDetails,
  345. intersectionFn: FragmentIntersection
  346. ): void {
  347. const skippedSegments = newDetails.skippedSegments;
  348. const start =
  349. Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN;
  350. const end =
  351. (oldDetails.fragmentHint ? 1 : 0) +
  352. (skippedSegments
  353. ? newDetails.endSN
  354. : Math.min(oldDetails.endSN, newDetails.endSN)) -
  355. newDetails.startSN;
  356. const delta = newDetails.startSN - oldDetails.startSN;
  357. const newFrags = newDetails.fragmentHint
  358. ? newDetails.fragments.concat(newDetails.fragmentHint)
  359. : newDetails.fragments;
  360. const oldFrags = oldDetails.fragmentHint
  361. ? oldDetails.fragments.concat(oldDetails.fragmentHint)
  362. : oldDetails.fragments;
  363.  
  364. for (let i = start; i <= end; i++) {
  365. const oldFrag = oldFrags[delta + i];
  366. let newFrag = newFrags[i];
  367. if (skippedSegments && !newFrag && i < skippedSegments) {
  368. // Fill in skipped segments in delta playlist
  369. newFrag = newDetails.fragments[i] = oldFrag;
  370. }
  371. if (oldFrag && newFrag) {
  372. intersectionFn(oldFrag, newFrag);
  373. }
  374. }
  375. }
  376.  
  377. export function adjustSliding(
  378. oldDetails: LevelDetails,
  379. newDetails: LevelDetails
  380. ): void {
  381. const delta =
  382. newDetails.startSN + newDetails.skippedSegments - oldDetails.startSN;
  383. const oldFragments = oldDetails.fragments;
  384. if (delta < 0 || delta >= oldFragments.length) {
  385. return;
  386. }
  387. addSliding(newDetails, oldFragments[delta].start);
  388. }
  389.  
  390. export function addSliding(details: LevelDetails, start: number) {
  391. if (start) {
  392. const fragments = details.fragments;
  393. for (let i = details.skippedSegments; i < fragments.length; i++) {
  394. fragments[i].start += start;
  395. }
  396. if (details.fragmentHint) {
  397. details.fragmentHint.start += start;
  398. }
  399. }
  400. }
  401.  
  402. export function computeReloadInterval(
  403. newDetails: LevelDetails,
  404. stats: LoaderStats
  405. ): number {
  406. const reloadInterval = 1000 * newDetails.levelTargetDuration;
  407. const reloadIntervalAfterMiss = reloadInterval / 2;
  408. const timeSinceLastModified = newDetails.age;
  409. const useLastModified =
  410. timeSinceLastModified > 0 && timeSinceLastModified < reloadInterval * 3;
  411. const roundTrip = stats.loading.end - stats.loading.start;
  412.  
  413. let estimatedTimeUntilUpdate;
  414. let availabilityDelay = newDetails.availabilityDelay;
  415. // let estimate = 'average';
  416.  
  417. if (newDetails.updated === false) {
  418. if (useLastModified) {
  419. // estimate = 'miss round trip';
  420. // We should have had a hit so try again in the time it takes to get a response,
  421. // but no less than 1/3 second.
  422. const minRetry = 333 * newDetails.misses;
  423. estimatedTimeUntilUpdate = Math.max(
  424. Math.min(reloadIntervalAfterMiss, roundTrip * 2),
  425. minRetry
  426. );
  427. newDetails.availabilityDelay =
  428. (newDetails.availabilityDelay || 0) + estimatedTimeUntilUpdate;
  429. } else {
  430. // estimate = 'miss half average';
  431. // follow HLS Spec, If the client reloads a Playlist file and finds that it has not
  432. // changed then it MUST wait for a period of one-half the target
  433. // duration before retrying.
  434. estimatedTimeUntilUpdate = reloadIntervalAfterMiss;
  435. }
  436. } else if (useLastModified) {
  437. // estimate = 'next modified date';
  438. // Get the closest we've been to timeSinceLastModified on update
  439. availabilityDelay = Math.min(
  440. availabilityDelay || reloadInterval / 2,
  441. timeSinceLastModified
  442. );
  443. newDetails.availabilityDelay = availabilityDelay;
  444. estimatedTimeUntilUpdate =
  445. availabilityDelay + reloadInterval - timeSinceLastModified;
  446. } else {
  447. estimatedTimeUntilUpdate = reloadInterval - roundTrip;
  448. }
  449.  
  450. // console.log(`[computeReloadInterval] live reload ${newDetails.updated ? 'REFRESHED' : 'MISSED'}`,
  451. // '\n method', estimate,
  452. // '\n estimated time until update =>', estimatedTimeUntilUpdate,
  453. // '\n average target duration', reloadInterval,
  454. // '\n time since modified', timeSinceLastModified,
  455. // '\n time round trip', roundTrip,
  456. // '\n availability delay', availabilityDelay);
  457.  
  458. return Math.round(estimatedTimeUntilUpdate);
  459. }
  460.  
  461. export function getFragmentWithSN(
  462. level: Level,
  463. sn: number,
  464. fragCurrent: Fragment | null
  465. ): Fragment | null {
  466. if (!level || !level.details) {
  467. return null;
  468. }
  469. const levelDetails = level.details;
  470. let fragment: Fragment | undefined =
  471. levelDetails.fragments[sn - levelDetails.startSN];
  472. if (fragment) {
  473. return fragment;
  474. }
  475. fragment = levelDetails.fragmentHint;
  476. if (fragment && fragment.sn === sn) {
  477. return fragment;
  478. }
  479. if (sn < levelDetails.startSN && fragCurrent && fragCurrent.sn === sn) {
  480. return fragCurrent;
  481. }
  482. return null;
  483. }
  484.  
  485. export function getPartWith(
  486. level: Level,
  487. sn: number,
  488. partIndex: number
  489. ): Part | null {
  490. if (!level || !level.details) {
  491. return null;
  492. }
  493. const partList = level.details.partList;
  494. if (partList) {
  495. for (let i = partList.length; i--; ) {
  496. const part = partList[i];
  497. if (part.index === partIndex && part.fragment.sn === sn) {
  498. return part;
  499. }
  500. }
  501. }
  502. return null;
  503. }