Home Reference Source

src/utils/discontinuities.ts

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