Home Reference Source

src/controller/base-playlist-controller.ts

  1. import type Hls from '../hls';
  2. import type { NetworkComponentAPI } from '../types/component-api';
  3. import { getSkipValue, HlsSkip, HlsUrlParameters } from '../types/level';
  4. import { computeReloadInterval } from './level-helper';
  5. import { logger } from '../utils/logger';
  6. import type LevelDetails from '../loader/level-details';
  7. import type { MediaPlaylist } from '../types/media-playlist';
  8. import type { AudioTrackLoadedData, LevelLoadedData, TrackLoadedData } from '../types/events';
  9. import { ErrorData } from '../types/events';
  10. import * as LevelHelper from './level-helper';
  11. import { Events } from '../events';
  12. import { ErrorTypes } from '../errors';
  13.  
  14. export default class BasePlaylistController implements NetworkComponentAPI {
  15. protected hls: Hls;
  16. protected timer: number = -1;
  17. protected canLoad: boolean = false;
  18. protected retryCount: number = 0;
  19. protected readonly log: (msg: any) => void;
  20. protected readonly warn: (msg: any) => void;
  21.  
  22. constructor (hls: Hls, logPrefix: string) {
  23. this.log = logger.log.bind(logger, `${logPrefix}:`);
  24. this.warn = logger.warn.bind(logger, `${logPrefix}:`);
  25. this.hls = hls;
  26. }
  27.  
  28. public destroy (): void {
  29. this.clearTimer();
  30. }
  31.  
  32. protected onError (event: Events.ERROR, data: ErrorData): void {
  33. if (data.fatal && data.type === ErrorTypes.NETWORK_ERROR) {
  34. this.clearTimer();
  35. }
  36. }
  37.  
  38. protected clearTimer (): void {
  39. clearTimeout(this.timer);
  40. this.timer = -1;
  41. }
  42.  
  43. public startLoad (): void {
  44. this.canLoad = true;
  45. this.retryCount = 0;
  46. this.loadPlaylist();
  47. }
  48.  
  49. public stopLoad (): void {
  50. this.canLoad = false;
  51. this.clearTimer();
  52. }
  53.  
  54. protected switchParams (playlistUri: string, previous?: LevelDetails): HlsUrlParameters | undefined {
  55. const renditionReports = previous?.renditionReports;
  56. if (renditionReports) {
  57. for (let i = 0; i < renditionReports.length; i++) {
  58. const attr = renditionReports[i];
  59. const uri = '' + attr.URI;
  60. if (uri === playlistUri.substr(-uri.length)) {
  61. const msn = parseInt(attr['LAST-MSN']);
  62. let part = parseInt(attr['LAST-PART']);
  63. if (previous && this.hls.config.lowLatencyMode) {
  64. const currentGoal = Math.min(previous.age - previous.partTarget, previous.targetduration);
  65. if (part !== undefined && currentGoal > previous.partTarget) {
  66. part += 1;
  67. }
  68. }
  69. if (Number.isFinite(msn)) {
  70. return new HlsUrlParameters(msn, Number.isFinite(part) ? part : undefined, HlsSkip.No);
  71. }
  72. }
  73. }
  74. }
  75. }
  76.  
  77. protected loadPlaylist (hlsUrlParameters?: HlsUrlParameters): void {}
  78.  
  79. protected shouldLoadTrack (track: MediaPlaylist): boolean {
  80. return this.canLoad && track && !!track.url && (!track.details || track.details.live);
  81. }
  82.  
  83. protected playlistLoaded (index: number, data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData, previousDetails?: LevelDetails) {
  84. const { details, stats } = data;
  85.  
  86. // Set last updated date-time
  87. const elapsed = stats.loading.end ? Math.max(0, self.performance.now() - stats.loading.end) : 0;
  88. details.advancedDateTime = Date.now() - elapsed;
  89.  
  90. // if current playlist is a live playlist, arm a timer to reload it
  91. if (details.live || previousDetails?.live) {
  92. details.reloaded(previousDetails);
  93. if (previousDetails) {
  94. this.log(`live playlist ${index} ${details.advanced ? ('REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex) : 'MISSED'}`);
  95. }
  96. // Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
  97. if (previousDetails && details.fragments.length > 0) {
  98. LevelHelper.mergeDetails(previousDetails, details);
  99. if (!details.advanced) {
  100. details.advancedDateTime = previousDetails.advancedDateTime;
  101. }
  102. }
  103. if (!this.canLoad || !details.live) {
  104. return;
  105. }
  106. if (details.canBlockReload && details.endSN && details.advanced) {
  107. // Load level with LL-HLS delivery directives
  108. const lowLatencyMode = this.hls.config.lowLatencyMode;
  109. const lastPartIndex = details.lastPartIndex;
  110. let msn;
  111. let part;
  112. if (lowLatencyMode) {
  113. msn = lastPartIndex !== -1 ? details.lastPartSn : details.endSN + 1;
  114. part = lastPartIndex !== -1 ? lastPartIndex + 1 : undefined;
  115. } else {
  116. // This playlist update will be late by one part (0). There is no way to know the last part number,
  117. // or request just the next sn without a part in most implementations.
  118. msn = lastPartIndex !== -1 ? details.lastPartSn + 1 : details.endSN + 1;
  119. part = lastPartIndex !== -1 ? 0 : undefined;
  120. }
  121. // Low-Latency CDN Tune-in: "age" header and time since load indicates we're behind by more than one part
  122. // Update directives to obtain the Playlist that has the estimated additional duration of media
  123. const lastAdvanced = details.age;
  124. const cdnAge = lastAdvanced + details.ageHeader;
  125. let currentGoal = Math.min(cdnAge - details.partTarget, details.targetduration * 1.5);
  126. if (currentGoal > 0) {
  127. if (previousDetails && currentGoal > previousDetails.tuneInGoal) {
  128. // If we attempted to get the next or latest playlist update, but currentGoal increased,
  129. // then we either can't catchup, or the "age" header cannot be trusted.
  130. this.warn(`CDN Tune-in goal increased from: ${previousDetails.tuneInGoal} to: ${currentGoal} with playlist age: ${details.age}`);
  131. currentGoal = 0;
  132. } else {
  133. const segments = Math.floor(currentGoal / details.targetduration);
  134. msn += segments;
  135. if (part !== undefined) {
  136. const parts = Math.round((currentGoal % details.targetduration) / details.partTarget);
  137. part += parts;
  138. }
  139. this.log(`CDN Tune-in age: ${details.ageHeader}s last advanced ${lastAdvanced.toFixed(2)}s goal: ${currentGoal} skip sn ${segments} to part ${part}`);
  140. }
  141. details.tuneInGoal = currentGoal;
  142. }
  143. let skip = getSkipValue(details, msn);
  144. if (data.deliveryDirectives?.skip) {
  145. if (details.deltaUpdateFailed) {
  146. msn = data.deliveryDirectives.msn;
  147. part = data.deliveryDirectives.part;
  148. skip = HlsSkip.No;
  149. }
  150. }
  151. this.loadPlaylist(new HlsUrlParameters(msn, part, skip));
  152. return;
  153. }
  154. const reloadInterval = computeReloadInterval(details, stats);
  155. this.log(`reload live playlist ${index} in ${Math.round(reloadInterval)} ms`);
  156. this.timer = self.setTimeout(() => this.loadPlaylist(), reloadInterval);
  157. } else {
  158. this.clearTimer();
  159. }
  160. }
  161.  
  162. protected retryLoadingOrFail (errorEvent: ErrorData): boolean {
  163. const { config } = this.hls;
  164. const retry = this.retryCount < config.levelLoadingMaxRetry;
  165. if (retry) {
  166. this.retryCount++;
  167. if (errorEvent.details.indexOf('LoadTimeOut') > -1 && errorEvent.context?.deliveryDirectives) {
  168. // The LL-HLS request already timed out so retry immediately
  169. this.warn(`retry playlist loading #${this.retryCount} after "${errorEvent.details}"`);
  170. this.loadPlaylist();
  171. } else {
  172. // exponential backoff capped to max retry timeout
  173. const delay = Math.min(Math.pow(2, this.retryCount) * config.levelLoadingRetryDelay, config.levelLoadingMaxRetryTimeout);
  174. // Schedule level/track reload
  175. this.timer = self.setTimeout(() => this.loadPlaylist(), delay);
  176. this.warn(`retry playlist loading #${this.retryCount} in ${delay} ms after "${errorEvent.details}"`);
  177. }
  178. } else {
  179. this.warn(`cannot recover from error "${errorEvent.details}"`);
  180. // stopping live reloading timer if any
  181. this.clearTimer();
  182. // switch error to fatal
  183. errorEvent.fatal = true;
  184. }
  185. return retry;
  186. }
  187. }