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