Home Reference Source

src/hls.ts

  1. import * as URLToolkit from 'url-toolkit';
  2.  
  3. import {
  4. ErrorTypes,
  5. ErrorDetails
  6. } from './errors';
  7.  
  8. import PlaylistLoader from './loader/playlist-loader';
  9. import KeyLoader from './loader/key-loader';
  10.  
  11. import { FragmentTracker } from './controller/fragment-tracker';
  12. import StreamController from './controller/stream-controller';
  13. import LevelController from './controller/level-controller';
  14.  
  15. import { isSupported } from './is-supported';
  16. import { logger, enableLogs } from './utils/logger';
  17. import { enableStreamingMode, HlsConfig, hlsDefaultConfig, mergeConfig } from './config';
  18.  
  19. import { Events } from './events';
  20. import { EventEmitter } from 'eventemitter3';
  21. import { Level } from './types/level';
  22. import { MediaPlaylist } from './types/media-playlist';
  23. import AudioTrackController from './controller/audio-track-controller';
  24. import SubtitleTrackController from './controller/subtitle-track-controller';
  25. import ID3TrackController from './controller/id3-track-controller';
  26. import EMEController from './controller/eme-controller';
  27. import CapLevelController from './controller/cap-level-controller';
  28. import AbrController from './controller/abr-controller';
  29. import LatencyController from './controller/latency-controller';
  30. import { ComponentAPI, NetworkComponentAPI } from './types/component-api';
  31. import type { HlsEventEmitter, HlsListeners } from './events';
  32.  
  33. /**
  34. * @module Hls
  35. * @class
  36. * @constructor
  37. */
  38. export default class Hls implements HlsEventEmitter {
  39. private static defaultConfig?: HlsConfig;
  40.  
  41. public readonly config: HlsConfig;
  42. public readonly userConfig: Partial<HlsConfig>;
  43.  
  44. private coreComponents: ComponentAPI[];
  45. private networkControllers: NetworkComponentAPI[];
  46.  
  47. private _emitter: HlsEventEmitter = new EventEmitter();
  48. private _autoLevelCapping: number;
  49. private abrController: AbrController;
  50. private capLevelController: CapLevelController;
  51. private latencyController: LatencyController;
  52. private levelController: LevelController;
  53. private streamController: StreamController;
  54. private audioTrackController: AudioTrackController;
  55. private subtitleTrackController: SubtitleTrackController;
  56. private emeController: EMEController;
  57.  
  58. private _media: HTMLMediaElement | null = null;
  59. private url: string | null = null;
  60.  
  61. static get version (): string {
  62. return __VERSION__;
  63. }
  64.  
  65. static isSupported (): boolean {
  66. return isSupported();
  67. }
  68.  
  69. static get Events () {
  70. return Events;
  71. }
  72.  
  73. static get ErrorTypes () {
  74. return ErrorTypes;
  75. }
  76.  
  77. static get ErrorDetails () {
  78. return ErrorDetails;
  79. }
  80.  
  81. static get DefaultConfig (): HlsConfig {
  82. if (!Hls.defaultConfig) {
  83. return hlsDefaultConfig;
  84. }
  85.  
  86. return Hls.defaultConfig;
  87. }
  88.  
  89. /**
  90. * @type {HlsConfig}
  91. */
  92. static set DefaultConfig (defaultConfig: HlsConfig) {
  93. Hls.defaultConfig = defaultConfig;
  94. }
  95.  
  96. /**
  97. * Creates an instance of an HLS client that can attach to exactly one `HTMLMediaElement`.
  98. *
  99. * @constructs Hls
  100. * @param {HlsConfig} config
  101. */
  102. constructor (userConfig: Partial<HlsConfig> = {}) {
  103. const config = this.config = mergeConfig(Hls.DefaultConfig, userConfig);
  104. this.userConfig = userConfig;
  105. enableLogs(config.debug);
  106.  
  107. this._autoLevelCapping = -1;
  108.  
  109. if (config.progressive) {
  110. enableStreamingMode(config);
  111. }
  112.  
  113. // core controllers and network loaders
  114. const abrController = this.abrController = new config.abrController(this); // eslint-disable-line new-cap
  115. const bufferController = new config.bufferController(this); // eslint-disable-line new-cap
  116. const capLevelController = this.capLevelController = new config.capLevelController(this); // eslint-disable-line new-cap
  117. const fpsController = new config.fpsController(this); // eslint-disable-line new-cap
  118. const playListLoader = new PlaylistLoader(this);
  119. const keyLoader = new KeyLoader(this);
  120. const id3TrackController = new ID3TrackController(this);
  121.  
  122. // network controllers
  123. const levelController = this.levelController = new LevelController(this);
  124. // FragmentTracker must be defined before StreamController because the order of event handling is important
  125. const fragmentTracker = new FragmentTracker(this);
  126. const streamController = this.streamController = new StreamController(this, fragmentTracker);
  127.  
  128. // Level Controller initiates loading after all controllers have received MANIFEST_PARSED
  129. levelController.onParsedComplete = () => {
  130. if (config.autoStartLoad || streamController.forceStartLoad) {
  131. this.startLoad(config.startPosition);
  132. }
  133. };
  134.  
  135. // Cap level controller uses streamController to flush the buffer
  136. capLevelController.setStreamController(streamController);
  137. // fpsController uses streamController to switch when frames are being dropped
  138. fpsController.setStreamController(streamController);
  139.  
  140. const networkControllers = [
  141. levelController,
  142. streamController
  143. ];
  144.  
  145. this.networkControllers = networkControllers;
  146. const coreComponents = [
  147. playListLoader,
  148. keyLoader,
  149. abrController,
  150. bufferController,
  151. capLevelController,
  152. fpsController,
  153. id3TrackController,
  154. fragmentTracker
  155. ];
  156.  
  157. this.audioTrackController = this.createController(config.audioTrackController, null, networkControllers);
  158. this.createController(config.audioStreamController, fragmentTracker, networkControllers);
  159. // subtitleTrackController must be defined before because the order of event handling is important
  160. this.subtitleTrackController = this.createController(config.subtitleTrackController, null, networkControllers);
  161. this.createController(config.subtitleStreamController, fragmentTracker, networkControllers);
  162. this.createController(config.timelineController, null, coreComponents);
  163. this.emeController = this.createController(config.emeController, null, coreComponents);
  164. this.latencyController = this.createController(LatencyController, null, coreComponents);
  165.  
  166. this.coreComponents = coreComponents;
  167. }
  168.  
  169. createController (ControllerClass, fragmentTracker, components) {
  170. if (ControllerClass) {
  171. const controllerInstance = fragmentTracker ? new ControllerClass(this, fragmentTracker) : new ControllerClass(this);
  172. if (components) {
  173. components.push(controllerInstance);
  174. }
  175. return controllerInstance;
  176. }
  177. return null;
  178. }
  179.  
  180. // Delegate the EventEmitter through the public API of Hls.js
  181. on<E extends keyof HlsListeners, Context = undefined> (event: E, listener: HlsListeners[E], context?: Context) {
  182. const hlsjs = this;
  183. this._emitter.on(event, function (this: Context, ...args: unknown[]) {
  184. if (hlsjs.config.debug) {
  185. listener.apply(this, args);
  186. } else {
  187. try {
  188. listener.apply(this, args);
  189. } catch (e) {
  190. logger.error('An internal error happened while handling event ' + event + '. Error message: "' + e.message + '". Here is a stacktrace:', e);
  191. hlsjs.trigger(Events.ERROR, {
  192. type: ErrorTypes.OTHER_ERROR,
  193. details: ErrorDetails.INTERNAL_EXCEPTION,
  194. fatal: false,
  195. event: event,
  196. error: e
  197. });
  198. }
  199. }
  200. }, context);
  201. }
  202.  
  203. once<E extends keyof HlsListeners, Context = undefined> (event: E, listener: HlsListeners[E], context?: Context) {
  204. const hlsjs = this;
  205. this._emitter.once(event, function (this: Context, ...args: unknown[]) {
  206. if (hlsjs.config.debug) {
  207. listener.apply(this, args);
  208. } else {
  209. try {
  210. listener.apply(this, args);
  211. } catch (e) {
  212. logger.error('An internal error happened while handling event ' + event + '. Error message: "' + e.message + '". Here is a stacktrace:', e);
  213. hlsjs.trigger(Events.ERROR, {
  214. type: ErrorTypes.OTHER_ERROR,
  215. details: ErrorDetails.INTERNAL_EXCEPTION,
  216. fatal: false,
  217. event: event,
  218. error: e
  219. });
  220. }
  221. }
  222. }, context);
  223. }
  224.  
  225. removeAllListeners<E extends keyof HlsListeners> (event?: E | undefined) {
  226. this._emitter.removeAllListeners(event);
  227. }
  228.  
  229. off<E extends keyof HlsListeners, Context = undefined> (event: E, listener?: HlsListeners[E] | undefined, context?: Context, once?: boolean | undefined) {
  230. this._emitter.off(event, listener, context, once);
  231. }
  232.  
  233. listeners<E extends keyof HlsListeners> (event: E): HlsListeners[E][] {
  234. return this._emitter.listeners(event);
  235. }
  236.  
  237. emit<E extends keyof HlsListeners> (event: E, name: E, eventObject: Parameters<HlsListeners[E]>[1]): boolean {
  238. return this._emitter.emit(event, name, eventObject);
  239. }
  240.  
  241. trigger<E extends keyof HlsListeners> (event: E, eventObject: Parameters<HlsListeners[E]>[1]): boolean {
  242. return this._emitter.emit(event, event, eventObject);
  243. }
  244.  
  245. listenerCount<E extends keyof HlsListeners> (event: E): number {
  246. return this._emitter.listenerCount(event);
  247. }
  248.  
  249. /**
  250. * Dispose of the instance
  251. */
  252. destroy () {
  253. logger.log('destroy');
  254. this.trigger(Events.DESTROYING, undefined);
  255. this.detachMedia();
  256. this.networkControllers.forEach(component => component.destroy());
  257. this.coreComponents.forEach(component => component.destroy());
  258. this.url = null;
  259. this.removeAllListeners();
  260. this._autoLevelCapping = -1;
  261. }
  262.  
  263. /**
  264. * Attaches Hls.js to a media element
  265. * @param {HTMLMediaElement} media
  266. */
  267. attachMedia (media: HTMLMediaElement) {
  268. logger.log('attachMedia');
  269. this._media = media;
  270. this.trigger(Events.MEDIA_ATTACHING, { media: media });
  271. }
  272.  
  273. /**
  274. * Detach Hls.js from the media
  275. */
  276. detachMedia () {
  277. logger.log('detachMedia');
  278. this.trigger(Events.MEDIA_DETACHING, undefined);
  279. this._media = null;
  280. }
  281.  
  282. /**
  283. * Set the source URL. Can be relative or absolute.
  284. * @param {string} url
  285. */
  286. loadSource (url: string) {
  287. this.stopLoad();
  288. const media = this.media;
  289. if (media && this.url) {
  290. this.detachMedia();
  291. this.attachMedia(media);
  292. }
  293. url = URLToolkit.buildAbsoluteURL(self.location.href, url, { alwaysNormalize: true });
  294. logger.log(`loadSource:${url}`);
  295. this.url = url;
  296. // when attaching to a source URL, trigger a playlist load
  297. this.trigger(Events.MANIFEST_LOADING, { url: url });
  298. }
  299.  
  300. /**
  301. * Start loading data from the stream source.
  302. * Depending on default config, client starts loading automatically when a source is set.
  303. *
  304. * @param {number} startPosition Set the start position to stream from
  305. * @default -1 None (from earliest point)
  306. */
  307. startLoad (startPosition: number = -1) {
  308. logger.log(`startLoad(${startPosition})`);
  309. this.networkControllers.forEach(controller => {
  310. controller.startLoad(startPosition);
  311. });
  312. }
  313.  
  314. /**
  315. * Stop loading of any stream data.
  316. */
  317. stopLoad () {
  318. logger.log('stopLoad');
  319. this.networkControllers.forEach(controller => {
  320. controller.stopLoad();
  321. });
  322. }
  323.  
  324. /**
  325. * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1)
  326. */
  327. swapAudioCodec () {
  328. logger.log('swapAudioCodec');
  329. this.streamController.swapAudioCodec();
  330. }
  331.  
  332. /**
  333. * When the media-element fails, this allows to detach and then re-attach it
  334. * as one call (convenience method).
  335. *
  336. * Automatic recovery of media-errors by this process is configurable.
  337. */
  338. recoverMediaError () {
  339. logger.log('recoverMediaError');
  340. const media = this._media;
  341. this.detachMedia();
  342. if (media) {
  343. this.attachMedia(media);
  344. }
  345. }
  346.  
  347. removeLevel (levelIndex, urlId = 0) {
  348. this.levelController.removeLevel(levelIndex, urlId);
  349. }
  350.  
  351. /**
  352. * @type {Level[]}
  353. */
  354. get levels (): Array<Level> {
  355. return this.levelController.levels ? this.levelController.levels : [];
  356. }
  357.  
  358. /**
  359. * Index of quality level currently played
  360. * @type {number}
  361. */
  362. get currentLevel (): number {
  363. return this.streamController.currentLevel;
  364. }
  365.  
  366. /**
  367. * Set quality level index immediately .
  368. * This will flush the current buffer to replace the quality asap.
  369. * That means playback will interrupt at least shortly to re-buffer and re-sync eventually.
  370. * @type {number} -1 for automatic level selection
  371. */
  372. set currentLevel (newLevel: number) {
  373. logger.log(`set currentLevel:${newLevel}`);
  374. this.loadLevel = newLevel;
  375. this.streamController.immediateLevelSwitch();
  376. }
  377.  
  378. /**
  379. * Index of next quality level loaded as scheduled by stream controller.
  380. * @type {number}
  381. */
  382. get nextLevel (): number {
  383. return this.streamController.nextLevel;
  384. }
  385.  
  386. /**
  387. * Set quality level index for next loaded data.
  388. * This will switch the video quality asap, without interrupting playback.
  389. * May abort current loading of data, and flush parts of buffer (outside currently played fragment region).
  390. * @type {number} -1 for automatic level selection
  391. */
  392. set nextLevel (newLevel: number) {
  393. logger.log(`set nextLevel:${newLevel}`);
  394. this.levelController.manualLevel = newLevel;
  395. this.streamController.nextLevelSwitch();
  396. }
  397.  
  398. /**
  399. * Return the quality level of the currently or last (of none is loaded currently) segment
  400. * @type {number}
  401. */
  402. get loadLevel (): number {
  403. return this.levelController.level;
  404. }
  405.  
  406. /**
  407. * Set quality level index for next loaded data in a conservative way.
  408. * This will switch the quality without flushing, but interrupt current loading.
  409. * Thus the moment when the quality switch will appear in effect will only be after the already existing buffer.
  410. * @type {number} newLevel -1 for automatic level selection
  411. */
  412. set loadLevel (newLevel: number) {
  413. logger.log(`set loadLevel:${newLevel}`);
  414. this.levelController.manualLevel = newLevel;
  415. }
  416.  
  417. /**
  418. * get next quality level loaded
  419. * @type {number}
  420. */
  421. get nextLoadLevel (): number {
  422. return this.levelController.nextLoadLevel;
  423. }
  424.  
  425. /**
  426. * Set quality level of next loaded segment in a fully "non-destructive" way.
  427. * Same as `loadLevel` but will wait for next switch (until current loading is done).
  428. * @type {number} level
  429. */
  430. set nextLoadLevel (level: number) {
  431. this.levelController.nextLoadLevel = level;
  432. }
  433.  
  434. /**
  435. * Return "first level": like a default level, if not set,
  436. * falls back to index of first level referenced in manifest
  437. * @type {number}
  438. */
  439. get firstLevel (): number {
  440. return Math.max(this.levelController.firstLevel, this.minAutoLevel);
  441. }
  442.  
  443. /**
  444. * Sets "first-level", see getter.
  445. * @type {number}
  446. */
  447. set firstLevel (newLevel: number) {
  448. logger.log(`set firstLevel:${newLevel}`);
  449. this.levelController.firstLevel = newLevel;
  450. }
  451.  
  452. /**
  453. * Return start level (level of first fragment that will be played back)
  454. * if not overrided by user, first level appearing in manifest will be used as start level
  455. * if -1 : automatic start level selection, playback will start from level matching download bandwidth
  456. * (determined from download of first segment)
  457. * @type {number}
  458. */
  459. get startLevel (): number {
  460. return this.levelController.startLevel;
  461. }
  462.  
  463. /**
  464. * set start level (level of first fragment that will be played back)
  465. * if not overrided by user, first level appearing in manifest will be used as start level
  466. * if -1 : automatic start level selection, playback will start from level matching download bandwidth
  467. * (determined from download of first segment)
  468. * @type {number} newLevel
  469. */
  470. set startLevel (newLevel: number) {
  471. logger.log(`set startLevel:${newLevel}`);
  472. // if not in automatic start level detection, ensure startLevel is greater than minAutoLevel
  473. if (newLevel !== -1) {
  474. newLevel = Math.max(newLevel, this.minAutoLevel);
  475. }
  476.  
  477. this.levelController.startLevel = newLevel;
  478. }
  479.  
  480. /**
  481. * Get the current setting for capLevelToPlayerSize
  482. *
  483. * @type {boolean}
  484. */
  485. get capLevelToPlayerSize (): boolean {
  486. return this.config.capLevelToPlayerSize;
  487. }
  488.  
  489. /**
  490. * set dynamically set capLevelToPlayerSize against (`CapLevelController`)
  491. *
  492. * @type {boolean}
  493. */
  494. set capLevelToPlayerSize (shouldStartCapping: boolean) {
  495. const newCapLevelToPlayerSize = !!shouldStartCapping;
  496.  
  497. if (newCapLevelToPlayerSize !== this.config.capLevelToPlayerSize) {
  498. if (newCapLevelToPlayerSize) {
  499. this.capLevelController.startCapping(); // If capping occurs, nextLevelSwitch will happen based on size.
  500. } else {
  501. this.capLevelController.stopCapping();
  502. this.autoLevelCapping = -1;
  503. this.streamController.nextLevelSwitch(); // Now we're uncapped, get the next level asap.
  504. }
  505.  
  506. this.config.capLevelToPlayerSize = newCapLevelToPlayerSize;
  507. }
  508. }
  509.  
  510. /**
  511. * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`)
  512. * @type {number}
  513. */
  514. get autoLevelCapping (): number {
  515. return this._autoLevelCapping;
  516. }
  517.  
  518. /**
  519. * get bandwidth estimate
  520. * @type {number}
  521. */
  522. get bandwidthEstimate (): number {
  523. return this.abrController.bwEstimator.getEstimate();
  524. }
  525.  
  526. /**
  527. * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`)
  528. * @type {number}
  529. */
  530. set autoLevelCapping (newLevel: number) {
  531. if (this._autoLevelCapping !== newLevel) {
  532. logger.log(`set autoLevelCapping:${newLevel}`);
  533. this._autoLevelCapping = newLevel;
  534. }
  535. }
  536.  
  537. /**
  538. * True when automatic level selection enabled
  539. * @type {boolean}
  540. */
  541. get autoLevelEnabled (): boolean {
  542. return (this.levelController.manualLevel === -1);
  543. }
  544.  
  545. /**
  546. * Level set manually (if any)
  547. * @type {number}
  548. */
  549. get manualLevel (): number {
  550. return this.levelController.manualLevel;
  551. }
  552.  
  553. /**
  554. * min level selectable in auto mode according to config.minAutoBitrate
  555. * @type {number}
  556. */
  557. get minAutoLevel (): number {
  558. const { levels, config: { minAutoBitrate } } = this;
  559. if (!levels) return 0;
  560.  
  561. const len = levels.length;
  562. for (let i = 0; i < len; i++) {
  563. if (levels[i].maxBitrate > minAutoBitrate) {
  564. return i;
  565. }
  566. }
  567.  
  568. return 0;
  569. }
  570.  
  571. /**
  572. * max level selectable in auto mode according to autoLevelCapping
  573. * @type {number}
  574. */
  575. get maxAutoLevel (): number {
  576. const { levels, autoLevelCapping } = this;
  577.  
  578. let maxAutoLevel;
  579. if (autoLevelCapping === -1 && levels && levels.length) {
  580. maxAutoLevel = levels.length - 1;
  581. } else {
  582. maxAutoLevel = autoLevelCapping;
  583. }
  584.  
  585. return maxAutoLevel;
  586. }
  587.  
  588. /**
  589. * next automatically selected quality level
  590. * @type {number}
  591. */
  592. get nextAutoLevel (): number {
  593. // ensure next auto level is between min and max auto level
  594. return Math.min(Math.max(this.abrController.nextAutoLevel, this.minAutoLevel), this.maxAutoLevel);
  595. }
  596.  
  597. /**
  598. * this setter is used to force next auto level.
  599. * this is useful to force a switch down in auto mode:
  600. * in case of load error on level N, hls.js can set nextAutoLevel to N-1 for example)
  601. * forced value is valid for one fragment. upon succesful frag loading at forced level,
  602. * this value will be resetted to -1 by ABR controller.
  603. * @type {number}
  604. */
  605. set nextAutoLevel (nextLevel: number) {
  606. this.abrController.nextAutoLevel = Math.max(this.minAutoLevel, nextLevel);
  607. }
  608.  
  609. /**
  610. * @type {AudioTrack[]}
  611. */
  612. get audioTracks (): Array<MediaPlaylist> {
  613. const audioTrackController = this.audioTrackController;
  614. return audioTrackController ? audioTrackController.audioTracks : [];
  615. }
  616.  
  617. /**
  618. * index of the selected audio track (index in audio track lists)
  619. * @type {number}
  620. */
  621. get audioTrack (): number {
  622. const audioTrackController = this.audioTrackController;
  623. return audioTrackController ? audioTrackController.audioTrack : -1;
  624. }
  625.  
  626. /**
  627. * selects an audio track, based on its index in audio track lists
  628. * @type {number}
  629. */
  630. set audioTrack (audioTrackId: number) {
  631. const audioTrackController = this.audioTrackController;
  632. if (audioTrackController) {
  633. audioTrackController.audioTrack = audioTrackId;
  634. }
  635. }
  636.  
  637. /**
  638. * get alternate subtitle tracks list from playlist
  639. * @type {MediaPlaylist[]}
  640. */
  641. get subtitleTracks (): Array<MediaPlaylist> {
  642. const subtitleTrackController = this.subtitleTrackController;
  643. return subtitleTrackController ? subtitleTrackController.subtitleTracks : [];
  644. }
  645.  
  646. /**
  647. * index of the selected subtitle track (index in subtitle track lists)
  648. * @type {number}
  649. */
  650. get subtitleTrack (): number {
  651. const subtitleTrackController = this.subtitleTrackController;
  652. return subtitleTrackController ? subtitleTrackController.subtitleTrack : -1;
  653. }
  654.  
  655. get media () {
  656. return this._media;
  657. }
  658.  
  659. /**
  660. * select an subtitle track, based on its index in subtitle track lists
  661. * @type {number}
  662. */
  663. set subtitleTrack (subtitleTrackId: number) {
  664. const subtitleTrackController = this.subtitleTrackController;
  665. if (subtitleTrackController) {
  666. subtitleTrackController.subtitleTrack = subtitleTrackId;
  667. }
  668. }
  669.  
  670. /**
  671. * @type {boolean}
  672. */
  673. get subtitleDisplay (): boolean {
  674. const subtitleTrackController = this.subtitleTrackController;
  675. return subtitleTrackController ? subtitleTrackController.subtitleDisplay : false;
  676. }
  677.  
  678. /**
  679. * Enable/disable subtitle display rendering
  680. * @type {boolean}
  681. */
  682. set subtitleDisplay (value: boolean) {
  683. const subtitleTrackController = this.subtitleTrackController;
  684. if (subtitleTrackController) {
  685. subtitleTrackController.subtitleDisplay = value;
  686. }
  687. }
  688.  
  689. /**
  690. * get mode for Low-Latency HLS loading
  691. * @type {boolean}
  692. */
  693. get lowLatencyMode () {
  694. return this.config.lowLatencyMode;
  695. }
  696.  
  697. /**
  698. * Enable/disable Low-Latency HLS part playlist and segment loading, and start live streams at playlist PART-HOLD-BACK rather than HOLD-BACK.
  699. * @type {boolean}
  700. */
  701. set lowLatencyMode (mode: boolean) {
  702. this.config.lowLatencyMode = mode;
  703. }
  704.  
  705. /**
  706. * position (in seconds) of live sync point (ie edge of live position minus safety delay defined by ```hls.config.liveSyncDuration```)
  707. * @type {number}
  708. */
  709. get liveSyncPosition (): number | null {
  710. return this.latencyController.liveSyncPosition;
  711. }
  712.  
  713. /**
  714. * estimated position (in seconds) of live edge (ie edge of live playlist plus time sync playlist advanced)
  715. * returns 0 before first playlist is loaded
  716. * @type {number}
  717. */
  718. get latency () {
  719. return this.latencyController.latency;
  720. }
  721.  
  722. /**
  723. * maximum distance from the edge before the player seeks forward to ```hls.liveSyncPosition```
  724. * configured using ```liveMaxLatencyDurationCount``` (multiple of target duration) or ```liveMaxLatencyDuration```
  725. * returns 0 before first playlist is loaded
  726. * @type {number}
  727. */
  728. get maxLatency (): number {
  729. return this.latencyController.maxLatency;
  730. }
  731.  
  732. /**
  733. * target distance from the edge as calculated by the latency controller
  734. * @type {number}
  735. */
  736. get targetLatency (): number | null {
  737. return this.latencyController.targetLatency;
  738. }
  739. }