Home Reference Source

src/controller/level-helper.js

  1. /**
  2. * @module LevelHelper
  3. *
  4. * Providing methods dealing with playlist sliding and drift
  5. *
  6. * TODO: Create an actual `Level` class/model that deals with all this logic in an object-oriented-manner.
  7. *
  8. * */
  9.  
  10. import { logger } from '../utils/logger';
  11.  
  12. export function addGroupId (level, type, id) {
  13. switch (type) {
  14. case 'audio':
  15. if (!level.audioGroupIds) {
  16. level.audioGroupIds = [];
  17. }
  18. level.audioGroupIds.push(id);
  19. break;
  20. case 'text':
  21. if (!level.textGroupIds) {
  22. level.textGroupIds = [];
  23. }
  24. level.textGroupIds.push(id);
  25. break;
  26. }
  27. }
  28.  
  29. export function updatePTS (fragments, fromIdx, toIdx) {
  30. let fragFrom = fragments[fromIdx], fragTo = fragments[toIdx], fragToPTS = fragTo.startPTS;
  31. // if we know startPTS[toIdx]
  32. if (Number.isFinite(fragToPTS)) {
  33. // update fragment duration.
  34. // it helps to fix drifts between playlist reported duration and fragment real duration
  35. if (toIdx > fromIdx) {
  36. fragFrom.duration = fragToPTS - fragFrom.start;
  37. if (fragFrom.duration < 0) {
  38. logger.warn(`negative duration computed for frag ${fragFrom.sn},level ${fragFrom.level}, there should be some duration drift between playlist and fragment!`);
  39. }
  40. } else {
  41. fragTo.duration = fragFrom.start - fragToPTS;
  42. if (fragTo.duration < 0) {
  43. logger.warn(`negative duration computed for frag ${fragTo.sn},level ${fragTo.level}, there should be some duration drift between playlist and fragment!`);
  44. }
  45. }
  46. } else {
  47. // we dont know startPTS[toIdx]
  48. if (toIdx > fromIdx) {
  49. fragTo.start = fragFrom.start + fragFrom.duration;
  50. } else {
  51. fragTo.start = Math.max(fragFrom.start - fragTo.duration, 0);
  52. }
  53. }
  54. }
  55.  
  56. export function updateFragPTSDTS (details, frag, startPTS, endPTS, startDTS, endDTS) {
  57. // update frag PTS/DTS
  58. let maxStartPTS = startPTS;
  59. if (Number.isFinite(frag.startPTS)) {
  60. // delta PTS between audio and video
  61. let deltaPTS = Math.abs(frag.startPTS - startPTS);
  62. if (!Number.isFinite(frag.deltaPTS)) {
  63. frag.deltaPTS = deltaPTS;
  64. } else {
  65. frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS);
  66. }
  67.  
  68. maxStartPTS = Math.max(startPTS, frag.startPTS);
  69. startPTS = Math.min(startPTS, frag.startPTS);
  70. endPTS = Math.max(endPTS, frag.endPTS);
  71. startDTS = Math.min(startDTS, frag.startDTS);
  72. endDTS = Math.max(endDTS, frag.endDTS);
  73. }
  74.  
  75. const drift = startPTS - frag.start;
  76. frag.start = frag.startPTS = startPTS;
  77. frag.maxStartPTS = maxStartPTS;
  78. frag.endPTS = endPTS;
  79. frag.startDTS = startDTS;
  80. frag.endDTS = endDTS;
  81. frag.duration = endPTS - startPTS;
  82.  
  83. const sn = frag.sn;
  84. // exit if sn out of range
  85. if (!details || sn < details.startSN || sn > details.endSN) {
  86. return 0;
  87. }
  88.  
  89. let fragIdx, fragments, i;
  90. fragIdx = sn - details.startSN;
  91. fragments = details.fragments;
  92. // update frag reference in fragments array
  93. // rationale is that fragments array might not contain this frag object.
  94. // this will happen if playlist has been refreshed between frag loading and call to updateFragPTSDTS()
  95. // if we don't update frag, we won't be able to propagate PTS info on the playlist
  96. // resulting in invalid sliding computation
  97. fragments[fragIdx] = frag;
  98. // adjust fragment PTS/duration from seqnum-1 to frag 0
  99. for (i = fragIdx; i > 0; i--) {
  100. updatePTS(fragments, i, i - 1);
  101. }
  102.  
  103. // adjust fragment PTS/duration from seqnum to last frag
  104. for (i = fragIdx; i < fragments.length - 1; i++) {
  105. updatePTS(fragments, i, i + 1);
  106. }
  107.  
  108. details.PTSKnown = true;
  109. return drift;
  110. }
  111.  
  112. export function mergeDetails (oldDetails, newDetails) {
  113. // potentially retrieve cached initsegment
  114. if (newDetails.initSegment && oldDetails.initSegment) {
  115. newDetails.initSegment = oldDetails.initSegment;
  116. }
  117.  
  118. // check if old/new playlists have fragments in common
  119. // loop through overlapping SN and update startPTS , cc, and duration if any found
  120. let ccOffset = 0;
  121. let PTSFrag;
  122. mapFragmentIntersection(oldDetails, newDetails, (oldFrag, newFrag) => {
  123. ccOffset = oldFrag.cc - newFrag.cc;
  124. if (Number.isFinite(oldFrag.startPTS)) {
  125. newFrag.start = newFrag.startPTS = oldFrag.startPTS;
  126. newFrag.endPTS = oldFrag.endPTS;
  127. newFrag.duration = oldFrag.duration;
  128. newFrag.backtracked = oldFrag.backtracked;
  129. newFrag.dropped = oldFrag.dropped;
  130. PTSFrag = newFrag;
  131. }
  132. // PTS is known when there are overlapping segments
  133. newDetails.PTSKnown = true;
  134. });
  135.  
  136. if (!newDetails.PTSKnown) {
  137. return;
  138. }
  139.  
  140. if (ccOffset) {
  141. logger.log('discontinuity sliding from playlist, take drift into account');
  142. const newFragments = newDetails.fragments;
  143. for (let i = 0; i < newFragments.length; i++) {
  144. newFragments[i].cc += ccOffset;
  145. }
  146. }
  147.  
  148. // if at least one fragment contains PTS info, recompute PTS information for all fragments
  149. if (PTSFrag) {
  150. updateFragPTSDTS(newDetails, PTSFrag, PTSFrag.startPTS, PTSFrag.endPTS, PTSFrag.startDTS, PTSFrag.endDTS);
  151. } else {
  152. // ensure that delta is within oldFragments range
  153. // also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61])
  154. // in that case we also need to adjust start offset of all fragments
  155. adjustSliding(oldDetails, newDetails);
  156. }
  157. // if we are here, it means we have fragments overlapping between
  158. // old and new level. reliable PTS info is thus relying on old level
  159. newDetails.PTSKnown = oldDetails.PTSKnown;
  160. }
  161.  
  162. export function mergeSubtitlePlaylists (oldPlaylist, newPlaylist, referenceStart = 0) {
  163. let lastIndex = -1;
  164. mapFragmentIntersection(oldPlaylist, newPlaylist, (oldFrag, newFrag, index) => {
  165. newFrag.start = oldFrag.start;
  166. lastIndex = index;
  167. });
  168.  
  169. const frags = newPlaylist.fragments;
  170. if (lastIndex < 0) {
  171. frags.forEach(frag => {
  172. frag.start += referenceStart;
  173. });
  174. return;
  175. }
  176.  
  177. for (let i = lastIndex + 1; i < frags.length; i++) {
  178. frags[i].start = (frags[i - 1].start + frags[i - 1].duration);
  179. }
  180. }
  181.  
  182. export function mapFragmentIntersection (oldPlaylist, newPlaylist, intersectionFn) {
  183. if (!oldPlaylist || !newPlaylist) {
  184. return;
  185. }
  186.  
  187. const start = Math.max(oldPlaylist.startSN, newPlaylist.startSN) - newPlaylist.startSN;
  188. const end = Math.min(oldPlaylist.endSN, newPlaylist.endSN) - newPlaylist.startSN;
  189. const delta = newPlaylist.startSN - oldPlaylist.startSN;
  190.  
  191. for (let i = start; i <= end; i++) {
  192. const oldFrag = oldPlaylist.fragments[delta + i];
  193. const newFrag = newPlaylist.fragments[i];
  194. if (!oldFrag || !newFrag) {
  195. break;
  196. }
  197. intersectionFn(oldFrag, newFrag, i);
  198. }
  199. }
  200.  
  201. export function adjustSliding (oldPlaylist, newPlaylist) {
  202. const delta = newPlaylist.startSN - oldPlaylist.startSN;
  203. const oldFragments = oldPlaylist.fragments;
  204. const newFragments = newPlaylist.fragments;
  205.  
  206. if (delta < 0 || delta > oldFragments.length) {
  207. return;
  208. }
  209. for (let i = 0; i < newFragments.length; i++) {
  210. newFragments[i].start += oldFragments[delta].start;
  211. }
  212. }
  213.  
  214. export function computeReloadInterval (currentPlaylist, newPlaylist, lastRequestTime) {
  215. let reloadInterval = 1000 * (newPlaylist.averagetargetduration ? newPlaylist.averagetargetduration : newPlaylist.targetduration);
  216. const minReloadInterval = reloadInterval / 2;
  217. if (currentPlaylist && newPlaylist.endSN === currentPlaylist.endSN) {
  218. // follow HLS Spec, If the client reloads a Playlist file and finds that it has not
  219. // changed then it MUST wait for a period of one-half the target
  220. // duration before retrying.
  221. reloadInterval = minReloadInterval;
  222. }
  223.  
  224. if (lastRequestTime) {
  225. reloadInterval = Math.max(minReloadInterval, reloadInterval - (window.performance.now() - lastRequestTime));
  226. }
  227. // in any case, don't reload more than half of target duration
  228. return Math.round(reloadInterval);
  229. }