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