Home Reference Source

src/utils/discontinuities.ts

  1. import { logger } from './logger';
  2. import { adjustSliding } from '../controller/level-helper';
  3.  
  4. import type { Fragment } from '../loader/fragment';
  5. import type { LevelDetails } from '../loader/level-details';
  6. import type { Level } from '../types/level';
  7. import type { RequiredProperties } from '../types/general';
  8.  
  9. export function findFirstFragWithCC(fragments: Fragment[], cc: number) {
  10. let firstFrag: Fragment | null = null;
  11.  
  12. for (let i = 0, len = fragments.length; i < len; i++) {
  13. const currentFrag = fragments[i];
  14. if (currentFrag && currentFrag.cc === cc) {
  15. firstFrag = currentFrag;
  16. break;
  17. }
  18. }
  19.  
  20. return firstFrag;
  21. }
  22.  
  23. export function shouldAlignOnDiscontinuities(
  24. lastFrag: Fragment | null,
  25. lastLevel: Level,
  26. details: LevelDetails
  27. ): lastLevel is RequiredProperties<Level, 'details'> {
  28. if (lastLevel.details) {
  29. if (
  30. details.endCC > details.startCC ||
  31. (lastFrag && lastFrag.cc < details.startCC)
  32. ) {
  33. return true;
  34. }
  35. }
  36. return false;
  37. }
  38.  
  39. // Find the first frag in the previous level which matches the CC of the first frag of the new level
  40. export function findDiscontinuousReferenceFrag(
  41. prevDetails: LevelDetails,
  42. curDetails: LevelDetails,
  43. referenceIndex: number = 0
  44. ) {
  45. const prevFrags = prevDetails.fragments;
  46. const curFrags = curDetails.fragments;
  47.  
  48. if (!curFrags.length || !prevFrags.length) {
  49. logger.log('No fragments to align');
  50. return;
  51. }
  52.  
  53. const prevStartFrag = findFirstFragWithCC(prevFrags, curFrags[0].cc);
  54.  
  55. if (!prevStartFrag || (prevStartFrag && !prevStartFrag.startPTS)) {
  56. logger.log('No frag in previous level to align on');
  57. return;
  58. }
  59.  
  60. return prevStartFrag;
  61. }
  62.  
  63. function adjustFragmentStart(frag: Fragment, sliding: number) {
  64. if (frag) {
  65. const start = frag.start + sliding;
  66. frag.start = frag.startPTS = start;
  67. frag.endPTS = start + frag.duration;
  68. }
  69. }
  70.  
  71. export function adjustSlidingStart(sliding: number, details: LevelDetails) {
  72. // Update segments
  73. const fragments = details.fragments;
  74. for (let i = 0, len = fragments.length; i < len; i++) {
  75. adjustFragmentStart(fragments[i], sliding);
  76. }
  77. // Update LL-HLS parts at the end of the playlist
  78. if (details.fragmentHint) {
  79. adjustFragmentStart(details.fragmentHint, sliding);
  80. }
  81. details.alignedSliding = true;
  82. }
  83.  
  84. /**
  85. * Using the parameters of the last level, this function computes PTS' of the new fragments so that they form a
  86. * contiguous stream with the last fragments.
  87. * The PTS of a fragment lets Hls.js know where it fits into a stream - by knowing every PTS, we know which fragment to
  88. * download at any given time. PTS is normally computed when the fragment is demuxed, so taking this step saves us time
  89. * and an extra download.
  90. * @param lastFrag
  91. * @param lastLevel
  92. * @param details
  93. */
  94. export function alignStream(
  95. lastFrag: Fragment | null,
  96. lastLevel: Level | null,
  97. details: LevelDetails
  98. ) {
  99. if (!lastLevel) {
  100. return;
  101. }
  102. alignDiscontinuities(lastFrag, details, lastLevel);
  103. if (!details.alignedSliding && lastLevel.details) {
  104. // If the PTS wasn't figured out via discontinuity sequence that means there was no CC increase within the level.
  105. // Aligning via Program Date Time should therefore be reliable, since PDT should be the same within the same
  106. // discontinuity sequence.
  107. alignPDT(details, lastLevel.details);
  108. }
  109. if (
  110. !details.alignedSliding &&
  111. lastLevel.details &&
  112. !details.skippedSegments
  113. ) {
  114. // Try to align on sn so that we pick a better start fragment.
  115. // Do not perform this on playlists with delta updates as this is only to align levels on switch
  116. // and adjustSliding only adjusts fragments after skippedSegments.
  117. adjustSliding(lastLevel.details, details);
  118. }
  119. }
  120.  
  121. /**
  122. * Computes the PTS if a new level's fragments using the PTS of a fragment in the last level which shares the same
  123. * discontinuity sequence.
  124. * @param lastFrag - The last Fragment which shares the same discontinuity sequence
  125. * @param lastLevel - The details of the last loaded level
  126. * @param details - The details of the new level
  127. */
  128. function alignDiscontinuities(
  129. lastFrag: Fragment | null,
  130. details: LevelDetails,
  131. lastLevel: Level
  132. ) {
  133. if (shouldAlignOnDiscontinuities(lastFrag, lastLevel, details)) {
  134. const referenceFrag = findDiscontinuousReferenceFrag(
  135. lastLevel.details,
  136. details
  137. );
  138. if (referenceFrag && Number.isFinite(referenceFrag.start)) {
  139. logger.log(
  140. `Adjusting PTS using last level due to CC increase within current level ${details.url}`
  141. );
  142. adjustSlidingStart(referenceFrag.start, details);
  143. }
  144. }
  145. }
  146.  
  147. /**
  148. * Computes the PTS of a new level's fragments using the difference in Program Date Time from the last level.
  149. * @param details - The details of the new level
  150. * @param lastDetails - The details of the last loaded level
  151. */
  152. export function alignPDT(details: LevelDetails, lastDetails: LevelDetails) {
  153. // This check protects the unsafe "!" usage below for null program date time access.
  154. if (
  155. !lastDetails.fragments.length ||
  156. !details.hasProgramDateTime ||
  157. !lastDetails.hasProgramDateTime
  158. ) {
  159. return;
  160. }
  161. // if last level sliding is 1000 and its first frag PROGRAM-DATE-TIME is 2017-08-20 1:10:00 AM
  162. // and if new details first frag PROGRAM DATE-TIME is 2017-08-20 1:10:08 AM
  163. // then we can deduce that playlist B sliding is 1000+8 = 1008s
  164. const lastPDT = lastDetails.fragments[0].programDateTime!; // hasProgramDateTime check above makes this safe.
  165. const newPDT = details.fragments[0].programDateTime!;
  166. // date diff is in ms. frag.start is in seconds
  167. const sliding = (newPDT - lastPDT) / 1000 + lastDetails.fragments[0].start;
  168. if (sliding && Number.isFinite(sliding)) {
  169. logger.log(
  170. `Adjusting PTS using programDateTime delta ${
  171. newPDT - lastPDT
  172. }ms, sliding:${sliding.toFixed(3)} ${details.url} `
  173. );
  174. adjustSlidingStart(sliding, details);
  175. }
  176. }
  177.  
  178. /**
  179. * Ensures appropriate time-alignment between renditions based on PDT. Unlike `alignPDT`, which adjusts
  180. * the timeline based on the delta between PDTs of the 0th fragment of two playlists/`LevelDetails`,
  181. * this function assumes the timelines represented in `refDetails` are accurate, including the PDTs,
  182. * and uses the "wallclock"/PDT timeline as a cross-reference to `details`, adjusting the presentation
  183. * times/timelines of `details` accordingly.
  184. * Given the asynchronous nature of fetches and initial loads of live `main` and audio/subtitle tracks,
  185. * the primary purpose of this function is to ensure the "local timelines" of audio/subtitle tracks
  186. * are aligned to the main/video timeline, using PDT as the cross-reference/"anchor" that should
  187. * be consistent across playlists, per the HLS spec.
  188. * @param details - The details of the rendition you'd like to time-align (e.g. an audio rendition).
  189. * @param refDetails - The details of the reference rendition with start and PDT times for alignment.
  190. */
  191. export function alignMediaPlaylistByPDT(
  192. details: LevelDetails,
  193. refDetails: LevelDetails
  194. ) {
  195. if (!details.hasProgramDateTime || !refDetails.hasProgramDateTime) {
  196. return;
  197. }
  198.  
  199. const fragments = details.fragments;
  200. const refFragments = refDetails.fragments;
  201. if (!fragments.length || !refFragments.length) {
  202. return;
  203. }
  204.  
  205. // Calculate a delta to apply to all fragments according to the delta in PDT times and start times
  206. // of a fragment in the reference details, and a fragment in the target details of the same discontinuity.
  207. // If a fragment of the same discontinuity was not found use the middle fragment of both.
  208. const middleFrag = Math.round(refFragments.length / 2) - 1;
  209. const refFrag = refFragments[middleFrag];
  210. const frag =
  211. findFirstFragWithCC(fragments, refFrag.cc) ||
  212. fragments[Math.round(fragments.length / 2) - 1];
  213.  
  214. const refPDT = refFrag.programDateTime;
  215. const targetPDT = frag.programDateTime;
  216. if (refPDT === null || targetPDT === null) {
  217. return;
  218. }
  219.  
  220. const delta = (targetPDT - refPDT) / 1000 - (frag.start - refFrag.start);
  221. adjustSlidingStart(delta, details);
  222. }