Home Reference Source

src/controller/latency-controller.ts

  1. import { LevelDetails } from '../loader/level-details';
  2. import { ErrorDetails } from '../errors';
  3. import { Events } from '../events';
  4. import type {
  5. ErrorData,
  6. LevelUpdatedData,
  7. MediaAttachingData,
  8. } from '../types/events';
  9. import { logger } from '../utils/logger';
  10. import type { ComponentAPI } from '../types/component-api';
  11. import type Hls from '../hls';
  12. import type { HlsConfig } from '../config';
  13.  
  14. export default class LatencyController implements ComponentAPI {
  15. private hls: Hls;
  16. private readonly config: HlsConfig;
  17. private media: HTMLMediaElement | null = null;
  18. private levelDetails: LevelDetails | null = null;
  19. private currentTime: number = 0;
  20. private stallCount: number = 0;
  21. private _latency: number | null = null;
  22. private timeupdateHandler = () => this.timeupdate();
  23.  
  24. constructor(hls: Hls) {
  25. this.hls = hls;
  26. this.config = hls.config;
  27. this.registerListeners();
  28. }
  29.  
  30. get latency(): number {
  31. return this._latency || 0;
  32. }
  33.  
  34. get maxLatency(): number {
  35. const { config, levelDetails } = this;
  36. if (config.liveMaxLatencyDuration !== undefined) {
  37. return config.liveMaxLatencyDuration;
  38. }
  39. return levelDetails
  40. ? config.liveMaxLatencyDurationCount * levelDetails.targetduration
  41. : 0;
  42. }
  43.  
  44. get targetLatency(): number | null {
  45. const { levelDetails } = this;
  46. if (levelDetails === null) {
  47. return null;
  48. }
  49. const { holdBack, partHoldBack, targetduration } = levelDetails;
  50. const { liveSyncDuration, liveSyncDurationCount, lowLatencyMode } =
  51. this.config;
  52. const userConfig = this.hls.userConfig;
  53. let targetLatency = lowLatencyMode ? partHoldBack || holdBack : holdBack;
  54. if (
  55. userConfig.liveSyncDuration ||
  56. userConfig.liveSyncDurationCount ||
  57. targetLatency === 0
  58. ) {
  59. targetLatency =
  60. liveSyncDuration !== undefined
  61. ? liveSyncDuration
  62. : liveSyncDurationCount * targetduration;
  63. }
  64. const maxLiveSyncOnStallIncrease = targetduration;
  65. const liveSyncOnStallIncrease = 1.0;
  66. return (
  67. targetLatency +
  68. Math.min(
  69. this.stallCount * liveSyncOnStallIncrease,
  70. maxLiveSyncOnStallIncrease
  71. )
  72. );
  73. }
  74.  
  75. get liveSyncPosition(): number | null {
  76. const liveEdge = this.estimateLiveEdge();
  77. const targetLatency = this.targetLatency;
  78. const levelDetails = this.levelDetails;
  79. if (liveEdge === null || targetLatency === null || levelDetails === null) {
  80. return null;
  81. }
  82. const edge = levelDetails.edge;
  83. const syncPosition = liveEdge - targetLatency - this.edgeStalled;
  84. const min = edge - levelDetails.totalduration;
  85. const max =
  86. edge -
  87. ((this.config.lowLatencyMode && levelDetails.partTarget) ||
  88. levelDetails.targetduration);
  89. return Math.min(Math.max(min, syncPosition), max);
  90. }
  91.  
  92. get drift(): number {
  93. const { levelDetails } = this;
  94. if (levelDetails === null) {
  95. return 1;
  96. }
  97. return levelDetails.drift;
  98. }
  99.  
  100. get edgeStalled(): number {
  101. const { levelDetails } = this;
  102. if (levelDetails === null) {
  103. return 0;
  104. }
  105. const maxLevelUpdateAge =
  106. ((this.config.lowLatencyMode && levelDetails.partTarget) ||
  107. levelDetails.targetduration) * 3;
  108. return Math.max(levelDetails.age - maxLevelUpdateAge, 0);
  109. }
  110.  
  111. private get forwardBufferLength(): number {
  112. const { media, levelDetails } = this;
  113. if (!media || !levelDetails) {
  114. return 0;
  115. }
  116. const bufferedRanges = media.buffered.length;
  117. return (
  118. (bufferedRanges
  119. ? media.buffered.end(bufferedRanges - 1)
  120. : levelDetails.edge) - this.currentTime
  121. );
  122. }
  123.  
  124. public destroy(): void {
  125. this.unregisterListeners();
  126. this.onMediaDetaching();
  127. this.levelDetails = null;
  128. // @ts-ignore
  129. this.hls = this.timeupdateHandler = null;
  130. }
  131.  
  132. private registerListeners() {
  133. this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
  134. this.hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
  135. this.hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
  136. this.hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
  137. this.hls.on(Events.ERROR, this.onError, this);
  138. }
  139.  
  140. private unregisterListeners() {
  141. this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached);
  142. this.hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching);
  143. this.hls.off(Events.MANIFEST_LOADING, this.onManifestLoading);
  144. this.hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated);
  145. this.hls.off(Events.ERROR, this.onError);
  146. }
  147.  
  148. private onMediaAttached(
  149. event: Events.MEDIA_ATTACHED,
  150. data: MediaAttachingData
  151. ) {
  152. this.media = data.media;
  153. this.media.addEventListener('timeupdate', this.timeupdateHandler);
  154. }
  155.  
  156. private onMediaDetaching() {
  157. if (this.media) {
  158. this.media.removeEventListener('timeupdate', this.timeupdateHandler);
  159. this.media = null;
  160. }
  161. }
  162.  
  163. private onManifestLoading() {
  164. this.levelDetails = null;
  165. this._latency = null;
  166. this.stallCount = 0;
  167. }
  168.  
  169. private onLevelUpdated(
  170. event: Events.LEVEL_UPDATED,
  171. { details }: LevelUpdatedData
  172. ) {
  173. this.levelDetails = details;
  174. if (details.advanced) {
  175. this.timeupdate();
  176. }
  177. if (!details.live && this.media) {
  178. this.media.removeEventListener('timeupdate', this.timeupdateHandler);
  179. }
  180. }
  181.  
  182. private onError(event: Events.ERROR, data: ErrorData) {
  183. if (data.details !== ErrorDetails.BUFFER_STALLED_ERROR) {
  184. return;
  185. }
  186. this.stallCount++;
  187. logger.warn(
  188. '[playback-rate-controller]: Stall detected, adjusting target latency'
  189. );
  190. }
  191.  
  192. private timeupdate() {
  193. const { media, levelDetails } = this;
  194. if (!media || !levelDetails) {
  195. return;
  196. }
  197. this.currentTime = media.currentTime;
  198.  
  199. const latency = this.computeLatency();
  200. if (latency === null) {
  201. return;
  202. }
  203. this._latency = latency;
  204.  
  205. // Adapt playbackRate to meet target latency in low-latency mode
  206. const { lowLatencyMode, maxLiveSyncPlaybackRate } = this.config;
  207. if (!lowLatencyMode || maxLiveSyncPlaybackRate === 1) {
  208. return;
  209. }
  210. const targetLatency = this.targetLatency;
  211. if (targetLatency === null) {
  212. return;
  213. }
  214. const distanceFromTarget = latency - targetLatency;
  215. // Only adjust playbackRate when within one target duration of targetLatency
  216. // and more than one second from under-buffering.
  217. // Playback further than one target duration from target can be considered DVR playback.
  218. const liveMinLatencyDuration = Math.min(
  219. this.maxLatency,
  220. targetLatency + levelDetails.targetduration
  221. );
  222. const inLiveRange = distanceFromTarget < liveMinLatencyDuration;
  223. if (
  224. levelDetails.live &&
  225. inLiveRange &&
  226. distanceFromTarget > 0.05 &&
  227. this.forwardBufferLength > 1
  228. ) {
  229. const max = Math.min(2, Math.max(1.0, maxLiveSyncPlaybackRate));
  230. const rate =
  231. Math.round(
  232. (2 / (1 + Math.exp(-0.75 * distanceFromTarget - this.edgeStalled))) *
  233. 20
  234. ) / 20;
  235. media.playbackRate = Math.min(max, Math.max(1, rate));
  236. } else if (media.playbackRate !== 1 && media.playbackRate !== 0) {
  237. media.playbackRate = 1;
  238. }
  239. }
  240.  
  241. private estimateLiveEdge(): number | null {
  242. const { levelDetails } = this;
  243. if (levelDetails === null) {
  244. return null;
  245. }
  246. return levelDetails.edge + levelDetails.age;
  247. }
  248.  
  249. private computeLatency(): number | null {
  250. const liveEdge = this.estimateLiveEdge();
  251. if (liveEdge === null) {
  252. return null;
  253. }
  254. return liveEdge - this.currentTime;
  255. }
  256. }