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, mergeDetails } 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 {
  9. AudioTrackLoadedData,
  10. LevelLoadedData,
  11. TrackLoadedData,
  12. } from '../types/events';
  13. import { ErrorData } from '../types/events';
  14. import { Events } from '../events';
  15. import { ErrorTypes } from '../errors';
  16.  
  17. export default class BasePlaylistController implements NetworkComponentAPI {
  18. protected hls: Hls;
  19. protected timer: number = -1;
  20. protected canLoad: boolean = false;
  21. protected retryCount: number = 0;
  22. protected log: (msg: any) => void;
  23. protected warn: (msg: any) => void;
  24.  
  25. constructor(hls: Hls, logPrefix: string) {
  26. this.log = logger.log.bind(logger, `${logPrefix}:`);
  27. this.warn = logger.warn.bind(logger, `${logPrefix}:`);
  28. this.hls = hls;
  29. }
  30.  
  31. public destroy(): void {
  32. this.clearTimer();
  33. // @ts-ignore
  34. this.hls = this.log = this.warn = null;
  35. }
  36.  
  37. protected onError(event: Events.ERROR, data: ErrorData): void {
  38. if (data.fatal && data.type === ErrorTypes.NETWORK_ERROR) {
  39. this.clearTimer();
  40. }
  41. }
  42.  
  43. protected clearTimer(): void {
  44. clearTimeout(this.timer);
  45. this.timer = -1;
  46. }
  47.  
  48. public startLoad(): void {
  49. this.canLoad = true;
  50. this.retryCount = 0;
  51. this.loadPlaylist();
  52. }
  53.  
  54. public stopLoad(): void {
  55. this.canLoad = false;
  56. this.clearTimer();
  57. }
  58.  
  59. protected switchParams(
  60. playlistUri: string,
  61. previous?: LevelDetails
  62. ): HlsUrlParameters | undefined {
  63. const renditionReports = previous?.renditionReports;
  64. if (renditionReports) {
  65. for (let i = 0; i < renditionReports.length; i++) {
  66. const attr = renditionReports[i];
  67. const uri = '' + attr.URI;
  68. if (uri === playlistUri.slice(-uri.length)) {
  69. const msn = parseInt(attr['LAST-MSN']);
  70. let part = parseInt(attr['LAST-PART']);
  71. if (previous && this.hls.config.lowLatencyMode) {
  72. const currentGoal = Math.min(
  73. previous.age - previous.partTarget,
  74. previous.targetduration
  75. );
  76. if (part !== undefined && currentGoal > previous.partTarget) {
  77. part += 1;
  78. }
  79. }
  80. if (Number.isFinite(msn)) {
  81. return new HlsUrlParameters(
  82. msn,
  83. Number.isFinite(part) ? part : undefined,
  84. HlsSkip.No
  85. );
  86. }
  87. }
  88. }
  89. }
  90. }
  91.  
  92. protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {}
  93.  
  94. protected shouldLoadTrack(track: MediaPlaylist): boolean {
  95. return (
  96. this.canLoad &&
  97. track &&
  98. !!track.url &&
  99. (!track.details || track.details.live)
  100. );
  101. }
  102.  
  103. protected playlistLoaded(
  104. index: number,
  105. data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData,
  106. previousDetails?: LevelDetails
  107. ) {
  108. const { details, stats } = data;
  109.  
  110. // Set last updated date-time
  111. const elapsed = stats.loading.end
  112. ? Math.max(0, self.performance.now() - stats.loading.end)
  113. : 0;
  114. details.advancedDateTime = Date.now() - elapsed;
  115.  
  116. // if current playlist is a live playlist, arm a timer to reload it
  117. if (details.live || previousDetails?.live) {
  118. details.reloaded(previousDetails);
  119. if (previousDetails) {
  120. this.log(
  121. `live playlist ${index} ${
  122. details.advanced
  123. ? 'REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex
  124. : 'MISSED'
  125. }`
  126. );
  127. }
  128. // Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
  129. if (previousDetails && details.fragments.length > 0) {
  130. mergeDetails(previousDetails, details);
  131. }
  132. if (!this.canLoad || !details.live) {
  133. return;
  134. }
  135. let deliveryDirectives: HlsUrlParameters;
  136. let msn: number | undefined = undefined;
  137. let part: number | undefined = undefined;
  138. if (details.canBlockReload && details.endSN && details.advanced) {
  139. // Load level with LL-HLS delivery directives
  140. const lowLatencyMode = this.hls.config.lowLatencyMode;
  141. const lastPartSn = details.lastPartSn;
  142. const endSn = details.endSN;
  143. const lastPartIndex = details.lastPartIndex;
  144. const hasParts = lastPartIndex !== -1;
  145. const lastPart = lastPartSn === endSn;
  146. // When low latency mode is disabled, we'll skip part requests once the last part index is found
  147. const nextSnStartIndex = lowLatencyMode ? 0 : lastPartIndex;
  148. if (hasParts) {
  149. msn = lastPart ? endSn + 1 : lastPartSn;
  150. part = lastPart ? nextSnStartIndex : lastPartIndex + 1;
  151. } else {
  152. msn = endSn + 1;
  153. }
  154. // Low-Latency CDN Tune-in: "age" header and time since load indicates we're behind by more than one part
  155. // Update directives to obtain the Playlist that has the estimated additional duration of media
  156. const lastAdvanced = details.age;
  157. const cdnAge = lastAdvanced + details.ageHeader;
  158. let currentGoal = Math.min(
  159. cdnAge - details.partTarget,
  160. details.targetduration * 1.5
  161. );
  162. if (currentGoal > 0) {
  163. if (previousDetails && currentGoal > previousDetails.tuneInGoal) {
  164. // If we attempted to get the next or latest playlist update, but currentGoal increased,
  165. // then we either can't catchup, or the "age" header cannot be trusted.
  166. this.warn(
  167. `CDN Tune-in goal increased from: ${previousDetails.tuneInGoal} to: ${currentGoal} with playlist age: ${details.age}`
  168. );
  169. currentGoal = 0;
  170. } else {
  171. const segments = Math.floor(currentGoal / details.targetduration);
  172. msn += segments;
  173. if (part !== undefined) {
  174. const parts = Math.round(
  175. (currentGoal % details.targetduration) / details.partTarget
  176. );
  177. part += parts;
  178. }
  179. this.log(
  180. `CDN Tune-in age: ${
  181. details.ageHeader
  182. }s last advanced ${lastAdvanced.toFixed(
  183. 2
  184. )}s goal: ${currentGoal} skip sn ${segments} to part ${part}`
  185. );
  186. }
  187. details.tuneInGoal = currentGoal;
  188. }
  189. deliveryDirectives = this.getDeliveryDirectives(
  190. details,
  191. data.deliveryDirectives,
  192. msn,
  193. part
  194. );
  195. if (lowLatencyMode || !lastPart) {
  196. this.loadPlaylist(deliveryDirectives);
  197. return;
  198. }
  199. } else {
  200. deliveryDirectives = this.getDeliveryDirectives(
  201. details,
  202. data.deliveryDirectives,
  203. msn,
  204. part
  205. );
  206. }
  207. let reloadInterval = computeReloadInterval(details, stats);
  208. if (msn !== undefined && details.canBlockReload) {
  209. reloadInterval -= details.partTarget || 1;
  210. }
  211. this.log(
  212. `reload live playlist ${index} in ${Math.round(reloadInterval)} ms`
  213. );
  214. this.timer = self.setTimeout(
  215. () => this.loadPlaylist(deliveryDirectives),
  216. reloadInterval
  217. );
  218. } else {
  219. this.clearTimer();
  220. }
  221. }
  222.  
  223. private getDeliveryDirectives(
  224. details: LevelDetails,
  225. previousDeliveryDirectives: HlsUrlParameters | null,
  226. msn?: number,
  227. part?: number
  228. ): HlsUrlParameters {
  229. let skip = getSkipValue(details, msn);
  230. if (previousDeliveryDirectives?.skip && details.deltaUpdateFailed) {
  231. msn = previousDeliveryDirectives.msn;
  232. part = previousDeliveryDirectives.part;
  233. skip = HlsSkip.No;
  234. }
  235. return new HlsUrlParameters(msn, part, skip);
  236. }
  237.  
  238. protected retryLoadingOrFail(errorEvent: ErrorData): boolean {
  239. const { config } = this.hls;
  240. const retry = this.retryCount < config.levelLoadingMaxRetry;
  241. if (retry) {
  242. this.retryCount++;
  243. if (
  244. errorEvent.details.indexOf('LoadTimeOut') > -1 &&
  245. errorEvent.context?.deliveryDirectives
  246. ) {
  247. // The LL-HLS request already timed out so retry immediately
  248. this.warn(
  249. `retry playlist loading #${this.retryCount} after "${errorEvent.details}"`
  250. );
  251. this.loadPlaylist();
  252. } else {
  253. // exponential backoff capped to max retry timeout
  254. const delay = Math.min(
  255. Math.pow(2, this.retryCount) * config.levelLoadingRetryDelay,
  256. config.levelLoadingMaxRetryTimeout
  257. );
  258. // Schedule level/track reload
  259. this.timer = self.setTimeout(() => this.loadPlaylist(), delay);
  260. this.warn(
  261. `retry playlist loading #${this.retryCount} in ${delay} ms after "${errorEvent.details}"`
  262. );
  263. }
  264. } else {
  265. this.warn(`cannot recover from error "${errorEvent.details}"`);
  266. // stopping live reloading timer if any
  267. this.clearTimer();
  268. // switch error to fatal
  269. errorEvent.fatal = true;
  270. }
  271. return retry;
  272. }
  273. }