Home Reference Source

src/controller/base-stream-controller.ts

  1. import TaskLoop from '../task-loop';
  2. import { FragmentState } from './fragment-tracker';
  3. import { Bufferable, BufferHelper, BufferInfo } from '../utils/buffer-helper';
  4. import { logger } from '../utils/logger';
  5. import { Events } from '../events';
  6. import { ErrorDetails, ErrorTypes } from '../errors';
  7. import { ChunkMetadata } from '../types/transmuxer';
  8. import { appendUint8Array } from '../utils/mp4-tools';
  9. import { alignStream } from '../utils/discontinuities';
  10. import {
  11. findFragmentByPDT,
  12. findFragmentByPTS,
  13. findFragWithCC,
  14. } from './fragment-finders';
  15. import {
  16. getFragmentWithSN,
  17. getPartWith,
  18. updateFragPTSDTS,
  19. } from './level-helper';
  20. import TransmuxerInterface from '../demux/transmuxer-interface';
  21. import { Fragment, Part } from '../loader/fragment';
  22. import FragmentLoader, {
  23. FragmentLoadProgressCallback,
  24. LoadError,
  25. } from '../loader/fragment-loader';
  26. import KeyLoader from '../loader/key-loader';
  27. import { LevelDetails } from '../loader/level-details';
  28. import Decrypter from '../crypt/decrypter';
  29. import TimeRanges from '../utils/time-ranges';
  30. import { PlaylistLevelType } from '../types/loader';
  31. import type {
  32. BufferAppendingData,
  33. ErrorData,
  34. FragLoadedData,
  35. PartsLoadedData,
  36. KeyLoadedData,
  37. MediaAttachingData,
  38. BufferFlushingData,
  39. LevelSwitchingData,
  40. } from '../types/events';
  41. import type { FragmentTracker } from './fragment-tracker';
  42. import type { Level } from '../types/level';
  43. import type { RemuxedTrack } from '../types/remuxer';
  44. import type Hls from '../hls';
  45. import type { HlsConfig } from '../config';
  46. import type { NetworkComponentAPI } from '../types/component-api';
  47. import type { SourceBufferName } from '../types/buffer';
  48.  
  49. type ResolveFragLoaded = (FragLoadedEndData) => void;
  50. type RejectFragLoaded = (LoadError) => void;
  51.  
  52. export const State = {
  53. STOPPED: 'STOPPED',
  54. IDLE: 'IDLE',
  55. KEY_LOADING: 'KEY_LOADING',
  56. FRAG_LOADING: 'FRAG_LOADING',
  57. FRAG_LOADING_WAITING_RETRY: 'FRAG_LOADING_WAITING_RETRY',
  58. WAITING_TRACK: 'WAITING_TRACK',
  59. PARSING: 'PARSING',
  60. PARSED: 'PARSED',
  61. ENDED: 'ENDED',
  62. ERROR: 'ERROR',
  63. WAITING_INIT_PTS: 'WAITING_INIT_PTS',
  64. WAITING_LEVEL: 'WAITING_LEVEL',
  65. };
  66.  
  67. export default class BaseStreamController
  68. extends TaskLoop
  69. implements NetworkComponentAPI
  70. {
  71. protected hls: Hls;
  72.  
  73. protected fragPrevious: Fragment | null = null;
  74. protected fragCurrent: Fragment | null = null;
  75. protected fragmentTracker: FragmentTracker;
  76. protected transmuxer: TransmuxerInterface | null = null;
  77. protected _state: string = State.STOPPED;
  78. protected media: HTMLMediaElement | null = null;
  79. protected mediaBuffer: Bufferable | null = null;
  80. protected config: HlsConfig;
  81. protected bitrateTest: boolean = false;
  82. protected lastCurrentTime: number = 0;
  83. protected nextLoadPosition: number = 0;
  84. protected startPosition: number = 0;
  85. protected loadedmetadata: boolean = false;
  86. protected fragLoadError: number = 0;
  87. protected retryDate: number = 0;
  88. protected levels: Array<Level> | null = null;
  89. protected fragmentLoader: FragmentLoader;
  90. protected keyLoader: KeyLoader;
  91. protected levelLastLoaded: number | null = null;
  92. protected startFragRequested: boolean = false;
  93. protected decrypter: Decrypter;
  94. protected initPTS: Array<number> = [];
  95. protected onvseeking: EventListener | null = null;
  96. protected onvended: EventListener | null = null;
  97.  
  98. private readonly logPrefix: string = '';
  99. protected log: (msg: any) => void;
  100. protected warn: (msg: any) => void;
  101.  
  102. constructor(
  103. hls: Hls,
  104. fragmentTracker: FragmentTracker,
  105. keyLoader: KeyLoader,
  106. logPrefix: string
  107. ) {
  108. super();
  109. this.logPrefix = logPrefix;
  110. this.log = logger.log.bind(logger, `${logPrefix}:`);
  111. this.warn = logger.warn.bind(logger, `${logPrefix}:`);
  112. this.hls = hls;
  113. this.fragmentLoader = new FragmentLoader(hls.config);
  114. this.keyLoader = keyLoader;
  115. this.fragmentTracker = fragmentTracker;
  116. this.config = hls.config;
  117. this.decrypter = new Decrypter(hls.config);
  118. hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
  119. }
  120.  
  121. protected doTick() {
  122. this.onTickEnd();
  123. }
  124.  
  125. protected onTickEnd() {}
  126.  
  127. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  128. public startLoad(startPosition: number): void {}
  129.  
  130. public stopLoad() {
  131. this.fragmentLoader.abort();
  132. this.keyLoader.abort();
  133. const frag = this.fragCurrent;
  134. if (frag) {
  135. frag.abortRequests();
  136. this.fragmentTracker.removeFragment(frag);
  137. }
  138. this.resetTransmuxer();
  139. this.fragCurrent = null;
  140. this.fragPrevious = null;
  141. this.clearInterval();
  142. this.clearNextTick();
  143. this.state = State.STOPPED;
  144. }
  145.  
  146. protected _streamEnded(
  147. bufferInfo: BufferInfo,
  148. levelDetails: LevelDetails
  149. ): boolean {
  150. // If playlist is live, there is another buffered range after the current range, nothing buffered, media is detached,
  151. // of nothing loading/loaded return false
  152. if (
  153. levelDetails.live ||
  154. bufferInfo.nextStart ||
  155. !bufferInfo.end ||
  156. !this.media
  157. ) {
  158. return false;
  159. }
  160. const partList = levelDetails.partList;
  161. // Since the last part isn't guaranteed to correspond to the last playlist segment for Low-Latency HLS,
  162. // check instead if the last part is buffered.
  163. if (partList?.length) {
  164. const lastPart = partList[partList.length - 1];
  165.  
  166. // Checking the midpoint of the part for potential margin of error and related issues.
  167. // NOTE: Technically I believe parts could yield content that is < the computed duration (including potential a duration of 0)
  168. // and still be spec-compliant, so there may still be edge cases here. Likewise, there could be issues in end of stream
  169. // part mismatches for independent audio and video playlists/segments.
  170. const lastPartBuffered = BufferHelper.isBuffered(
  171. this.media,
  172. lastPart.start + lastPart.duration / 2
  173. );
  174. return lastPartBuffered;
  175. }
  176.  
  177. const playlistType =
  178. levelDetails.fragments[levelDetails.fragments.length - 1].type;
  179. return this.fragmentTracker.isEndListAppended(playlistType);
  180. }
  181.  
  182. protected getLevelDetails(): LevelDetails | undefined {
  183. if (this.levels && this.levelLastLoaded !== null) {
  184. return this.levels[this.levelLastLoaded]?.details;
  185. }
  186. }
  187.  
  188. protected onMediaAttached(
  189. event: Events.MEDIA_ATTACHED,
  190. data: MediaAttachingData
  191. ) {
  192. const media = (this.media = this.mediaBuffer = data.media);
  193. this.onvseeking = this.onMediaSeeking.bind(this) as EventListener;
  194. this.onvended = this.onMediaEnded.bind(this) as EventListener;
  195. media.addEventListener('seeking', this.onvseeking);
  196. media.addEventListener('ended', this.onvended);
  197. const config = this.config;
  198. if (this.levels && config.autoStartLoad && this.state === State.STOPPED) {
  199. this.startLoad(config.startPosition);
  200. }
  201. }
  202.  
  203. protected onMediaDetaching() {
  204. const media = this.media;
  205. if (media?.ended) {
  206. this.log('MSE detaching and video ended, reset startPosition');
  207. this.startPosition = this.lastCurrentTime = 0;
  208. }
  209.  
  210. // remove video listeners
  211. if (media && this.onvseeking && this.onvended) {
  212. media.removeEventListener('seeking', this.onvseeking);
  213. media.removeEventListener('ended', this.onvended);
  214. this.onvseeking = this.onvended = null;
  215. }
  216. if (this.keyLoader) {
  217. this.keyLoader.detach();
  218. }
  219. this.media = this.mediaBuffer = null;
  220. this.loadedmetadata = false;
  221. this.fragmentTracker.removeAllFragments();
  222. this.stopLoad();
  223. }
  224.  
  225. protected onMediaSeeking() {
  226. const { config, fragCurrent, media, mediaBuffer, state } = this;
  227. const currentTime: number = media ? media.currentTime : 0;
  228. const bufferInfo = BufferHelper.bufferInfo(
  229. mediaBuffer ? mediaBuffer : media,
  230. currentTime,
  231. config.maxBufferHole
  232. );
  233.  
  234. this.log(
  235. `media seeking to ${
  236. Number.isFinite(currentTime) ? currentTime.toFixed(3) : currentTime
  237. }, state: ${state}`
  238. );
  239.  
  240. if (this.state === State.ENDED) {
  241. this.resetLoadingState();
  242. } else if (fragCurrent) {
  243. // Seeking while frag load is in progress
  244. const tolerance = config.maxFragLookUpTolerance;
  245. const fragStartOffset = fragCurrent.start - tolerance;
  246. const fragEndOffset =
  247. fragCurrent.start + fragCurrent.duration + tolerance;
  248. // if seeking out of buffered range or into new one
  249. if (
  250. !bufferInfo.len ||
  251. fragEndOffset < bufferInfo.start ||
  252. fragStartOffset > bufferInfo.end
  253. ) {
  254. const pastFragment = currentTime > fragEndOffset;
  255. // if the seek position is outside the current fragment range
  256. if (currentTime < fragStartOffset || pastFragment) {
  257. if (pastFragment && fragCurrent.loader) {
  258. this.log(
  259. 'seeking outside of buffer while fragment load in progress, cancel fragment load'
  260. );
  261. fragCurrent.abortRequests();
  262. }
  263. this.resetLoadingState();
  264. }
  265. }
  266. }
  267.  
  268. if (media) {
  269. this.lastCurrentTime = currentTime;
  270. }
  271.  
  272. // in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target
  273. if (!this.loadedmetadata && !bufferInfo.len) {
  274. this.nextLoadPosition = this.startPosition = currentTime;
  275. }
  276.  
  277. // Async tick to speed up processing
  278. this.tickImmediate();
  279. }
  280.  
  281. protected onMediaEnded() {
  282. // reset startPosition and lastCurrentTime to restart playback @ stream beginning
  283. this.startPosition = this.lastCurrentTime = 0;
  284. }
  285.  
  286. protected onLevelSwitching(
  287. event: Events.LEVEL_SWITCHING,
  288. data: LevelSwitchingData
  289. ): void {
  290. this.fragLoadError = 0;
  291. }
  292.  
  293. protected onHandlerDestroying() {
  294. this.stopLoad();
  295. super.onHandlerDestroying();
  296. }
  297.  
  298. protected onHandlerDestroyed() {
  299. this.state = State.STOPPED;
  300. this.hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
  301. if (this.fragmentLoader) {
  302. this.fragmentLoader.destroy();
  303. }
  304. if (this.keyLoader) {
  305. this.keyLoader.destroy();
  306. }
  307. if (this.decrypter) {
  308. this.decrypter.destroy();
  309. }
  310.  
  311. this.hls =
  312. this.log =
  313. this.warn =
  314. this.decrypter =
  315. this.keyLoader =
  316. this.fragmentLoader =
  317. this.fragmentTracker =
  318. null as any;
  319. super.onHandlerDestroyed();
  320. }
  321.  
  322. protected loadFragment(
  323. frag: Fragment,
  324. levelDetails: LevelDetails,
  325. targetBufferTime: number
  326. ) {
  327. this._loadFragForPlayback(frag, levelDetails, targetBufferTime);
  328. }
  329.  
  330. private _loadFragForPlayback(
  331. frag: Fragment,
  332. levelDetails: LevelDetails,
  333. targetBufferTime: number
  334. ) {
  335. const progressCallback: FragmentLoadProgressCallback = (
  336. data: FragLoadedData
  337. ) => {
  338. if (this.fragContextChanged(frag)) {
  339. this.warn(
  340. `Fragment ${frag.sn}${
  341. data.part ? ' p: ' + data.part.index : ''
  342. } of level ${frag.level} was dropped during download.`
  343. );
  344. this.fragmentTracker.removeFragment(frag);
  345. return;
  346. }
  347. frag.stats.chunkCount++;
  348. this._handleFragmentLoadProgress(data);
  349. };
  350.  
  351. this._doFragLoad(frag, levelDetails, targetBufferTime, progressCallback)
  352. .then((data) => {
  353. if (!data) {
  354. // if we're here we probably needed to backtrack or are waiting for more parts
  355. return;
  356. }
  357. this.fragLoadError = 0;
  358. const state = this.state;
  359. if (this.fragContextChanged(frag)) {
  360. if (
  361. state === State.FRAG_LOADING ||
  362. (!this.fragCurrent && state === State.PARSING)
  363. ) {
  364. this.fragmentTracker.removeFragment(frag);
  365. this.state = State.IDLE;
  366. }
  367. return;
  368. }
  369.  
  370. if ('payload' in data) {
  371. this.log(`Loaded fragment ${frag.sn} of level ${frag.level}`);
  372. this.hls.trigger(Events.FRAG_LOADED, data);
  373. }
  374.  
  375. // Pass through the whole payload; controllers not implementing progressive loading receive data from this callback
  376. this._handleFragmentLoadComplete(data);
  377. })
  378. .catch((reason) => {
  379. if (this.state === State.STOPPED || this.state === State.ERROR) {
  380. return;
  381. }
  382. this.warn(reason);
  383. this.resetFragmentLoading(frag);
  384. });
  385. }
  386.  
  387. protected flushMainBuffer(
  388. startOffset: number,
  389. endOffset: number,
  390. type: SourceBufferName | null = null
  391. ) {
  392. if (!(startOffset - endOffset)) {
  393. return;
  394. }
  395. // When alternate audio is playing, the audio-stream-controller is responsible for the audio buffer. Otherwise,
  396. // passing a null type flushes both buffers
  397. const flushScope: BufferFlushingData = { startOffset, endOffset, type };
  398. // Reset load errors on flush
  399. this.fragLoadError = 0;
  400. this.hls.trigger(Events.BUFFER_FLUSHING, flushScope);
  401. }
  402.  
  403. protected _loadInitSegment(frag: Fragment, details: LevelDetails) {
  404. this._doFragLoad(frag, details)
  405. .then((data) => {
  406. if (!data || this.fragContextChanged(frag) || !this.levels) {
  407. throw new Error('init load aborted');
  408. }
  409.  
  410. return data;
  411. })
  412. .then((data: FragLoadedData) => {
  413. const { hls } = this;
  414. const { payload } = data;
  415. const decryptData = frag.decryptdata;
  416.  
  417. // check to see if the payload needs to be decrypted
  418. if (
  419. payload &&
  420. payload.byteLength > 0 &&
  421. decryptData &&
  422. decryptData.key &&
  423. decryptData.iv &&
  424. decryptData.method === 'AES-128'
  425. ) {
  426. const startTime = self.performance.now();
  427. // decrypt the subtitles
  428. return this.decrypter
  429. .decrypt(
  430. new Uint8Array(payload),
  431. decryptData.key.buffer,
  432. decryptData.iv.buffer
  433. )
  434. .then((decryptedData) => {
  435. const endTime = self.performance.now();
  436. hls.trigger(Events.FRAG_DECRYPTED, {
  437. frag,
  438. payload: decryptedData,
  439. stats: {
  440. tstart: startTime,
  441. tdecrypt: endTime,
  442. },
  443. });
  444. data.payload = decryptedData;
  445.  
  446. return data;
  447. });
  448. }
  449.  
  450. return data;
  451. })
  452. .then((data: FragLoadedData) => {
  453. const { fragCurrent, hls, levels } = this;
  454. if (!levels) {
  455. throw new Error('init load aborted, missing levels');
  456. }
  457.  
  458. const details = levels[frag.level].details as LevelDetails;
  459. console.assert(
  460. details,
  461. 'Level details are defined when init segment is loaded'
  462. );
  463.  
  464. const stats = frag.stats;
  465. this.state = State.IDLE;
  466. this.fragLoadError = 0;
  467. frag.data = new Uint8Array(data.payload);
  468. stats.parsing.start = stats.buffering.start = self.performance.now();
  469. stats.parsing.end = stats.buffering.end = self.performance.now();
  470.  
  471. // Silence FRAG_BUFFERED event if fragCurrent is null
  472. if (data.frag === fragCurrent) {
  473. hls.trigger(Events.FRAG_BUFFERED, {
  474. stats,
  475. frag: fragCurrent,
  476. part: null,
  477. id: frag.type,
  478. });
  479. }
  480. this.tick();
  481. })
  482. .catch((reason) => {
  483. if (this.state === State.STOPPED || this.state === State.ERROR) {
  484. return;
  485. }
  486. this.warn(reason);
  487. this.resetFragmentLoading(frag);
  488. });
  489. }
  490.  
  491. protected fragContextChanged(frag: Fragment | null) {
  492. const { fragCurrent } = this;
  493. return (
  494. !frag ||
  495. !fragCurrent ||
  496. frag.level !== fragCurrent.level ||
  497. frag.sn !== fragCurrent.sn ||
  498. frag.urlId !== fragCurrent.urlId
  499. );
  500. }
  501.  
  502. protected fragBufferedComplete(frag: Fragment, part: Part | null) {
  503. const media = this.mediaBuffer ? this.mediaBuffer : this.media;
  504. this.log(
  505. `Buffered ${frag.type} sn: ${frag.sn}${
  506. part ? ' part: ' + part.index : ''
  507. } of ${this.logPrefix === '[stream-controller]' ? 'level' : 'track'} ${
  508. frag.level
  509. } (frag:[${(frag.startPTS || NaN).toFixed(3)}-${(
  510. frag.endPTS || NaN
  511. ).toFixed(3)}] > buffer:${
  512. media
  513. ? TimeRanges.toString(BufferHelper.getBuffered(media))
  514. : '(detached)'
  515. })`
  516. );
  517. this.state = State.IDLE;
  518. if (!media) {
  519. return;
  520. }
  521. if (
  522. !this.loadedmetadata &&
  523. frag.type == PlaylistLevelType.MAIN &&
  524. media.buffered.length &&
  525. this.fragCurrent?.sn === this.fragPrevious?.sn
  526. ) {
  527. this.loadedmetadata = true;
  528. this.seekToStartPos();
  529. }
  530. this.tick();
  531. }
  532.  
  533. protected seekToStartPos() {}
  534.  
  535. protected _handleFragmentLoadComplete(fragLoadedEndData: PartsLoadedData) {
  536. const { transmuxer } = this;
  537. if (!transmuxer) {
  538. return;
  539. }
  540. const { frag, part, partsLoaded } = fragLoadedEndData;
  541. // If we did not load parts, or loaded all parts, we have complete (not partial) fragment data
  542. const complete =
  543. !partsLoaded ||
  544. partsLoaded.length === 0 ||
  545. partsLoaded.some((fragLoaded) => !fragLoaded);
  546. const chunkMeta = new ChunkMetadata(
  547. frag.level,
  548. frag.sn as number,
  549. frag.stats.chunkCount + 1,
  550. 0,
  551. part ? part.index : -1,
  552. !complete
  553. );
  554. transmuxer.flush(chunkMeta);
  555. }
  556.  
  557. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  558. protected _handleFragmentLoadProgress(
  559. frag: PartsLoadedData | FragLoadedData
  560. ) {}
  561.  
  562. protected _doFragLoad(
  563. frag: Fragment,
  564. details: LevelDetails,
  565. targetBufferTime: number | null = null,
  566. progressCallback?: FragmentLoadProgressCallback
  567. ): Promise<PartsLoadedData | FragLoadedData | null> {
  568. if (!this.levels) {
  569. throw new Error('frag load aborted, missing levels');
  570. }
  571.  
  572. let keyLoadingPromise: Promise<KeyLoadedData | void> | null = null;
  573. if (frag.encrypted && !frag.decryptdata?.key) {
  574. this.log(
  575. `Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${
  576. this.logPrefix === '[stream-controller]' ? 'level' : 'track'
  577. } ${frag.level}`
  578. );
  579. this.state = State.KEY_LOADING;
  580. this.fragCurrent = frag;
  581. keyLoadingPromise = this.keyLoader.load(frag).then((keyLoadedData) => {
  582. if (!this.fragContextChanged(keyLoadedData.frag)) {
  583. this.hls.trigger(Events.KEY_LOADED, keyLoadedData);
  584. if (this.state === State.KEY_LOADING) {
  585. this.state = State.IDLE;
  586. }
  587. return keyLoadedData;
  588. }
  589. });
  590. this.hls.trigger(Events.KEY_LOADING, { frag });
  591. this.throwIfFragContextChanged('KEY_LOADING');
  592. } else if (!frag.encrypted && details.encryptedFragments.length) {
  593. this.keyLoader.loadClear(frag, details.encryptedFragments);
  594. }
  595.  
  596. targetBufferTime = Math.max(frag.start, targetBufferTime || 0);
  597. if (this.config.lowLatencyMode && details) {
  598. const partList = details.partList;
  599. if (partList && progressCallback) {
  600. if (targetBufferTime > frag.end && details.fragmentHint) {
  601. frag = details.fragmentHint;
  602. }
  603. const partIndex = this.getNextPart(partList, frag, targetBufferTime);
  604. if (partIndex > -1) {
  605. const part = partList[partIndex];
  606. this.log(
  607. `Loading part sn: ${frag.sn} p: ${part.index} cc: ${
  608. frag.cc
  609. } of playlist [${details.startSN}-${
  610. details.endSN
  611. }] parts [0-${partIndex}-${partList.length - 1}] ${
  612. this.logPrefix === '[stream-controller]' ? 'level' : 'track'
  613. }: ${frag.level}, target: ${parseFloat(
  614. targetBufferTime.toFixed(3)
  615. )}`
  616. );
  617. this.nextLoadPosition = part.start + part.duration;
  618. this.state = State.FRAG_LOADING;
  619. this.hls.trigger(Events.FRAG_LOADING, {
  620. frag,
  621. part: partList[partIndex],
  622. targetBufferTime,
  623. });
  624. this.throwIfFragContextChanged('FRAG_LOADING parts');
  625. if (keyLoadingPromise) {
  626. return keyLoadingPromise
  627. .then((keyLoadedData) => {
  628. if (
  629. !keyLoadedData ||
  630. this.fragContextChanged(keyLoadedData.frag)
  631. ) {
  632. return null;
  633. }
  634. return this.doFragPartsLoad(
  635. frag,
  636. partList,
  637. partIndex,
  638. progressCallback
  639. );
  640. })
  641. .catch((error) => this.handleFragLoadError(error));
  642. }
  643.  
  644. return this.doFragPartsLoad(
  645. frag,
  646. partList,
  647. partIndex,
  648. progressCallback
  649. ).catch((error: LoadError) => this.handleFragLoadError(error));
  650. } else if (
  651. !frag.url ||
  652. this.loadedEndOfParts(partList, targetBufferTime)
  653. ) {
  654. // Fragment hint has no parts
  655. return Promise.resolve(null);
  656. }
  657. }
  658. }
  659.  
  660. this.log(
  661. `Loading fragment ${frag.sn} cc: ${frag.cc} ${
  662. details ? 'of [' + details.startSN + '-' + details.endSN + '] ' : ''
  663. }${this.logPrefix === '[stream-controller]' ? 'level' : 'track'}: ${
  664. frag.level
  665. }, target: ${parseFloat(targetBufferTime.toFixed(3))}`
  666. );
  667. // Don't update nextLoadPosition for fragments which are not buffered
  668. if (Number.isFinite(frag.sn as number) && !this.bitrateTest) {
  669. this.nextLoadPosition = frag.start + frag.duration;
  670. }
  671. this.state = State.FRAG_LOADING;
  672. this.hls.trigger(Events.FRAG_LOADING, { frag, targetBufferTime });
  673. this.throwIfFragContextChanged('FRAG_LOADING');
  674.  
  675. // Load key before streaming fragment data
  676. const dataOnProgress = this.config.progressive;
  677. if (dataOnProgress && keyLoadingPromise) {
  678. return keyLoadingPromise
  679. .then((keyLoadedData) => {
  680. if (!keyLoadedData || this.fragContextChanged(keyLoadedData?.frag)) {
  681. return null;
  682. }
  683. return this.fragmentLoader.load(frag, progressCallback);
  684. })
  685. .catch((error) => this.handleFragLoadError(error));
  686. }
  687.  
  688. // load unencrypted fragment data with progress event,
  689. // or handle fragment result after key and fragment are finished loading
  690. return Promise.all([
  691. this.fragmentLoader.load(
  692. frag,
  693. dataOnProgress ? progressCallback : undefined
  694. ),
  695. keyLoadingPromise,
  696. ])
  697. .then(([fragLoadedData]) => {
  698. if (!dataOnProgress && fragLoadedData && progressCallback) {
  699. progressCallback(fragLoadedData);
  700. }
  701. return fragLoadedData;
  702. })
  703. .catch((error) => this.handleFragLoadError(error));
  704. }
  705.  
  706. private throwIfFragContextChanged(context: string): void | never {
  707. // exit if context changed during event loop
  708. if (this.fragCurrent === null) {
  709. throw new Error(`frag load aborted, context changed in ${context}`);
  710. }
  711. }
  712.  
  713. private doFragPartsLoad(
  714. frag: Fragment,
  715. partList: Part[],
  716. partIndex: number,
  717. progressCallback: FragmentLoadProgressCallback
  718. ): Promise<PartsLoadedData | null> {
  719. return new Promise(
  720. (resolve: ResolveFragLoaded, reject: RejectFragLoaded) => {
  721. const partsLoaded: FragLoadedData[] = [];
  722. const loadPartIndex = (index: number) => {
  723. const part = partList[index];
  724. this.fragmentLoader
  725. .loadPart(frag, part, progressCallback)
  726. .then((partLoadedData: FragLoadedData) => {
  727. partsLoaded[part.index] = partLoadedData;
  728. const loadedPart = partLoadedData.part as Part;
  729. this.hls.trigger(Events.FRAG_LOADED, partLoadedData);
  730. const nextPart = partList[index + 1];
  731. if (nextPart && nextPart.fragment === frag) {
  732. loadPartIndex(index + 1);
  733. } else {
  734. return resolve({
  735. frag,
  736. part: loadedPart,
  737. partsLoaded,
  738. });
  739. }
  740. })
  741. .catch(reject);
  742. };
  743. loadPartIndex(partIndex);
  744. }
  745. );
  746. }
  747.  
  748. private handleFragLoadError(error: LoadError | Error) {
  749. if ('data' in error) {
  750. const data = error.data;
  751. if (error.data && data.details === ErrorDetails.INTERNAL_ABORTED) {
  752. this.handleFragLoadAborted(data.frag, data.part);
  753. } else {
  754. this.hls.trigger(Events.ERROR, data as ErrorData);
  755. }
  756. } else {
  757. this.hls.trigger(Events.ERROR, {
  758. type: ErrorTypes.OTHER_ERROR,
  759. details: ErrorDetails.INTERNAL_EXCEPTION,
  760. err: error,
  761. fatal: true,
  762. });
  763. }
  764. return null;
  765. }
  766.  
  767. protected _handleTransmuxerFlush(chunkMeta: ChunkMetadata) {
  768. const context = this.getCurrentContext(chunkMeta);
  769. if (!context || this.state !== State.PARSING) {
  770. if (
  771. !this.fragCurrent &&
  772. this.state !== State.STOPPED &&
  773. this.state !== State.ERROR
  774. ) {
  775. this.state = State.IDLE;
  776. }
  777. return;
  778. }
  779. const { frag, part, level } = context;
  780. const now = self.performance.now();
  781. frag.stats.parsing.end = now;
  782. if (part) {
  783. part.stats.parsing.end = now;
  784. }
  785. this.updateLevelTiming(frag, part, level, chunkMeta.partial);
  786. }
  787.  
  788. protected getCurrentContext(
  789. chunkMeta: ChunkMetadata
  790. ): { frag: Fragment; part: Part | null; level: Level } | null {
  791. const { levels } = this;
  792. const { level: levelIndex, sn, part: partIndex } = chunkMeta;
  793. if (!levels || !levels[levelIndex]) {
  794. this.warn(
  795. `Levels object was unset while buffering fragment ${sn} of level ${levelIndex}. The current chunk will not be buffered.`
  796. );
  797. return null;
  798. }
  799. const level = levels[levelIndex];
  800. const part = partIndex > -1 ? getPartWith(level, sn, partIndex) : null;
  801. const frag = part
  802. ? part.fragment
  803. : getFragmentWithSN(level, sn, this.fragCurrent);
  804. if (!frag) {
  805. return null;
  806. }
  807. return { frag, part, level };
  808. }
  809.  
  810. protected bufferFragmentData(
  811. data: RemuxedTrack,
  812. frag: Fragment,
  813. part: Part | null,
  814. chunkMeta: ChunkMetadata
  815. ) {
  816. if (!data || this.state !== State.PARSING) {
  817. return;
  818. }
  819.  
  820. const { data1, data2 } = data;
  821. let buffer = data1;
  822. if (data1 && data2) {
  823. // Combine the moof + mdat so that we buffer with a single append
  824. buffer = appendUint8Array(data1, data2);
  825. }
  826.  
  827. if (!buffer || !buffer.length) {
  828. return;
  829. }
  830.  
  831. const segment: BufferAppendingData = {
  832. type: data.type,
  833. frag,
  834. part,
  835. chunkMeta,
  836. parent: frag.type,
  837. data: buffer,
  838. };
  839. this.hls.trigger(Events.BUFFER_APPENDING, segment);
  840.  
  841. if (data.dropped && data.independent && !part) {
  842. // Clear buffer so that we reload previous segments sequentially if required
  843. this.flushBufferGap(frag);
  844. }
  845. }
  846.  
  847. protected flushBufferGap(frag: Fragment) {
  848. const media = this.media;
  849. if (!media) {
  850. return;
  851. }
  852. // If currentTime is not buffered, clear the back buffer so that we can backtrack as much as needed
  853. if (!BufferHelper.isBuffered(media, media.currentTime)) {
  854. this.flushMainBuffer(0, frag.start);
  855. return;
  856. }
  857. // Remove back-buffer without interrupting playback to allow back tracking
  858. const currentTime = media.currentTime;
  859. const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0);
  860. const fragDuration = frag.duration;
  861. const segmentFraction = Math.min(
  862. this.config.maxFragLookUpTolerance * 2,
  863. fragDuration * 0.25
  864. );
  865. const start = Math.max(
  866. Math.min(frag.start - segmentFraction, bufferInfo.end - segmentFraction),
  867. currentTime + segmentFraction
  868. );
  869. if (frag.start - start > segmentFraction) {
  870. this.flushMainBuffer(start, frag.start);
  871. }
  872. }
  873.  
  874. protected getFwdBufferInfo(
  875. bufferable: Bufferable | null,
  876. type: PlaylistLevelType
  877. ): BufferInfo | null {
  878. const { config } = this;
  879. const pos = this.getLoadPosition();
  880. if (!Number.isFinite(pos)) {
  881. return null;
  882. }
  883. const bufferInfo = BufferHelper.bufferInfo(
  884. bufferable,
  885. pos,
  886. config.maxBufferHole
  887. );
  888. // Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos
  889. if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) {
  890. const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type);
  891. if (bufferedFragAtPos && bufferInfo.nextStart < bufferedFragAtPos.end) {
  892. return BufferHelper.bufferInfo(
  893. bufferable,
  894. pos,
  895. Math.max(bufferInfo.nextStart, config.maxBufferHole)
  896. );
  897. }
  898. }
  899. return bufferInfo;
  900. }
  901.  
  902. protected getMaxBufferLength(levelBitrate?: number): number {
  903. const { config } = this;
  904. let maxBufLen;
  905. if (levelBitrate) {
  906. maxBufLen = Math.max(
  907. (8 * config.maxBufferSize) / levelBitrate,
  908. config.maxBufferLength
  909. );
  910. } else {
  911. maxBufLen = config.maxBufferLength;
  912. }
  913. return Math.min(maxBufLen, config.maxMaxBufferLength);
  914. }
  915.  
  916. protected reduceMaxBufferLength(threshold?: number) {
  917. const config = this.config;
  918. const minLength = threshold || config.maxBufferLength;
  919. if (config.maxMaxBufferLength >= minLength) {
  920. // reduce max buffer length as it might be too high. we do this to avoid loop flushing ...
  921. config.maxMaxBufferLength /= 2;
  922. this.warn(`Reduce max buffer length to ${config.maxMaxBufferLength}s`);
  923. return true;
  924. }
  925. return false;
  926. }
  927.  
  928. protected getNextFragment(
  929. pos: number,
  930. levelDetails: LevelDetails
  931. ): Fragment | null {
  932. const fragments = levelDetails.fragments;
  933. const fragLen = fragments.length;
  934.  
  935. if (!fragLen) {
  936. return null;
  937. }
  938.  
  939. // find fragment index, contiguous with end of buffer position
  940. const { config } = this;
  941. const start = fragments[0].start;
  942. let frag;
  943.  
  944. if (levelDetails.live) {
  945. const initialLiveManifestSize = config.initialLiveManifestSize;
  946. if (fragLen < initialLiveManifestSize) {
  947. this.warn(
  948. `Not enough fragments to start playback (have: ${fragLen}, need: ${initialLiveManifestSize})`
  949. );
  950. return null;
  951. }
  952. // The real fragment start times for a live stream are only known after the PTS range for that level is known.
  953. // In order to discover the range, we load the best matching fragment for that level and demux it.
  954. // Do not load using live logic if the starting frag is requested - we want to use getFragmentAtPosition() so that
  955. // we get the fragment matching that start time
  956. if (
  957. !levelDetails.PTSKnown &&
  958. !this.startFragRequested &&
  959. this.startPosition === -1
  960. ) {
  961. frag = this.getInitialLiveFragment(levelDetails, fragments);
  962. this.startPosition = frag
  963. ? this.hls.liveSyncPosition || frag.start
  964. : pos;
  965. }
  966. } else if (pos <= start) {
  967. // VoD playlist: if loadPosition before start of playlist, load first fragment
  968. frag = fragments[0];
  969. }
  970.  
  971. // If we haven't run into any special cases already, just load the fragment most closely matching the requested position
  972. if (!frag) {
  973. const end = config.lowLatencyMode
  974. ? levelDetails.partEnd
  975. : levelDetails.fragmentEnd;
  976. frag = this.getFragmentAtPosition(pos, end, levelDetails);
  977. }
  978.  
  979. return this.mapToInitFragWhenRequired(frag);
  980. }
  981.  
  982. mapToInitFragWhenRequired(frag: Fragment | null): typeof frag {
  983. // If an initSegment is present, it must be buffered first
  984. if (frag?.initSegment && !frag?.initSegment.data && !this.bitrateTest) {
  985. return frag.initSegment;
  986. }
  987.  
  988. return frag;
  989. }
  990.  
  991. getNextPart(
  992. partList: Part[],
  993. frag: Fragment,
  994. targetBufferTime: number
  995. ): number {
  996. let nextPart = -1;
  997. let contiguous = false;
  998. let independentAttrOmitted = true;
  999. for (let i = 0, len = partList.length; i < len; i++) {
  1000. const part = partList[i];
  1001. independentAttrOmitted = independentAttrOmitted && !part.independent;
  1002. if (nextPart > -1 && targetBufferTime < part.start) {
  1003. break;
  1004. }
  1005. const loaded = part.loaded;
  1006. if (loaded) {
  1007. nextPart = -1;
  1008. } else if (
  1009. (contiguous || part.independent || independentAttrOmitted) &&
  1010. part.fragment === frag
  1011. ) {
  1012. nextPart = i;
  1013. }
  1014. contiguous = loaded;
  1015. }
  1016. return nextPart;
  1017. }
  1018.  
  1019. private loadedEndOfParts(
  1020. partList: Part[],
  1021. targetBufferTime: number
  1022. ): boolean {
  1023. const lastPart = partList[partList.length - 1];
  1024. return lastPart && targetBufferTime > lastPart.start && lastPart.loaded;
  1025. }
  1026.  
  1027. /*
  1028. This method is used find the best matching first fragment for a live playlist. This fragment is used to calculate the
  1029. "sliding" of the playlist, which is its offset from the start of playback. After sliding we can compute the real
  1030. start and end times for each fragment in the playlist (after which this method will not need to be called).
  1031. */
  1032. protected getInitialLiveFragment(
  1033. levelDetails: LevelDetails,
  1034. fragments: Array<Fragment>
  1035. ): Fragment | null {
  1036. const fragPrevious = this.fragPrevious;
  1037. let frag: Fragment | null = null;
  1038. if (fragPrevious) {
  1039. if (levelDetails.hasProgramDateTime) {
  1040. // Prefer using PDT, because it can be accurate enough to choose the correct fragment without knowing the level sliding
  1041. this.log(
  1042. `Live playlist, switching playlist, load frag with same PDT: ${fragPrevious.programDateTime}`
  1043. );
  1044. frag = findFragmentByPDT(
  1045. fragments,
  1046. fragPrevious.endProgramDateTime,
  1047. this.config.maxFragLookUpTolerance
  1048. );
  1049. }
  1050. if (!frag) {
  1051. // SN does not need to be accurate between renditions, but depending on the packaging it may be so.
  1052. const targetSN = (fragPrevious.sn as number) + 1;
  1053. if (
  1054. targetSN >= levelDetails.startSN &&
  1055. targetSN <= levelDetails.endSN
  1056. ) {
  1057. const fragNext = fragments[targetSN - levelDetails.startSN];
  1058. // Ensure that we're staying within the continuity range, since PTS resets upon a new range
  1059. if (fragPrevious.cc === fragNext.cc) {
  1060. frag = fragNext;
  1061. this.log(
  1062. `Live playlist, switching playlist, load frag with next SN: ${
  1063. frag!.sn
  1064. }`
  1065. );
  1066. }
  1067. }
  1068. // It's important to stay within the continuity range if available; otherwise the fragments in the playlist
  1069. // will have the wrong start times
  1070. if (!frag) {
  1071. frag = findFragWithCC(fragments, fragPrevious.cc);
  1072. if (frag) {
  1073. this.log(
  1074. `Live playlist, switching playlist, load frag with same CC: ${frag.sn}`
  1075. );
  1076. }
  1077. }
  1078. }
  1079. } else {
  1080. // Find a new start fragment when fragPrevious is null
  1081. const liveStart = this.hls.liveSyncPosition;
  1082. if (liveStart !== null) {
  1083. frag = this.getFragmentAtPosition(
  1084. liveStart,
  1085. this.bitrateTest ? levelDetails.fragmentEnd : levelDetails.edge,
  1086. levelDetails
  1087. );
  1088. }
  1089. }
  1090.  
  1091. return frag;
  1092. }
  1093.  
  1094. /*
  1095. This method finds the best matching fragment given the provided position.
  1096. */
  1097. protected getFragmentAtPosition(
  1098. bufferEnd: number,
  1099. end: number,
  1100. levelDetails: LevelDetails
  1101. ): Fragment | null {
  1102. const { config } = this;
  1103. let { fragPrevious } = this;
  1104. let { fragments, endSN } = levelDetails;
  1105. const { fragmentHint } = levelDetails;
  1106. const tolerance = config.maxFragLookUpTolerance;
  1107.  
  1108. const loadingParts = !!(
  1109. config.lowLatencyMode &&
  1110. levelDetails.partList &&
  1111. fragmentHint
  1112. );
  1113. if (loadingParts && fragmentHint && !this.bitrateTest) {
  1114. // Include incomplete fragment with parts at end
  1115. fragments = fragments.concat(fragmentHint);
  1116. endSN = fragmentHint.sn as number;
  1117. }
  1118.  
  1119. let frag;
  1120. if (bufferEnd < end) {
  1121. const lookupTolerance = bufferEnd > end - tolerance ? 0 : tolerance;
  1122. // Remove the tolerance if it would put the bufferEnd past the actual end of stream
  1123. // Uses buffer and sequence number to calculate switch segment (required if using EXT-X-DISCONTINUITY-SEQUENCE)
  1124. frag = findFragmentByPTS(
  1125. fragPrevious,
  1126. fragments,
  1127. bufferEnd,
  1128. lookupTolerance
  1129. );
  1130. } else {
  1131. // reach end of playlist
  1132. frag = fragments[fragments.length - 1];
  1133. }
  1134.  
  1135. if (frag) {
  1136. const curSNIdx = frag.sn - levelDetails.startSN;
  1137. // Move fragPrevious forward to support forcing the next fragment to load
  1138. // when the buffer catches up to a previously buffered range.
  1139. if (this.fragmentTracker.getState(frag) === FragmentState.OK) {
  1140. fragPrevious = frag;
  1141. }
  1142. if (fragPrevious && frag.sn === fragPrevious.sn && !loadingParts) {
  1143. // Force the next fragment to load if the previous one was already selected. This can occasionally happen with
  1144. // non-uniform fragment durations
  1145. const sameLevel = fragPrevious && frag.level === fragPrevious.level;
  1146. if (sameLevel) {
  1147. const nextFrag = fragments[curSNIdx + 1];
  1148. if (
  1149. frag.sn < endSN &&
  1150. this.fragmentTracker.getState(nextFrag) !== FragmentState.OK
  1151. ) {
  1152. this.log(
  1153. `SN ${frag.sn} just loaded, load next one: ${nextFrag.sn}`
  1154. );
  1155. frag = nextFrag;
  1156. } else {
  1157. frag = null;
  1158. }
  1159. }
  1160. }
  1161. }
  1162. return frag;
  1163. }
  1164.  
  1165. protected synchronizeToLiveEdge(levelDetails: LevelDetails) {
  1166. const { config, media } = this;
  1167. if (!media) {
  1168. return;
  1169. }
  1170. const liveSyncPosition = this.hls.liveSyncPosition;
  1171. const currentTime = media.currentTime;
  1172. const start = levelDetails.fragments[0].start;
  1173. const end = levelDetails.edge;
  1174. const withinSlidingWindow =
  1175. currentTime >= start - config.maxFragLookUpTolerance &&
  1176. currentTime <= end;
  1177. // Continue if we can seek forward to sync position or if current time is outside of sliding window
  1178. if (
  1179. liveSyncPosition !== null &&
  1180. media.duration > liveSyncPosition &&
  1181. (currentTime < liveSyncPosition || !withinSlidingWindow)
  1182. ) {
  1183. // Continue if buffer is starving or if current time is behind max latency
  1184. const maxLatency =
  1185. config.liveMaxLatencyDuration !== undefined
  1186. ? config.liveMaxLatencyDuration
  1187. : config.liveMaxLatencyDurationCount * levelDetails.targetduration;
  1188. if (
  1189. (!withinSlidingWindow && media.readyState < 4) ||
  1190. currentTime < end - maxLatency
  1191. ) {
  1192. if (!this.loadedmetadata) {
  1193. this.nextLoadPosition = liveSyncPosition;
  1194. }
  1195. // Only seek if ready and there is not a significant forward buffer available for playback
  1196. if (media.readyState) {
  1197. this.warn(
  1198. `Playback: ${currentTime.toFixed(
  1199. 3
  1200. )} is located too far from the end of live sliding playlist: ${end}, reset currentTime to : ${liveSyncPosition.toFixed(
  1201. 3
  1202. )}`
  1203. );
  1204. media.currentTime = liveSyncPosition;
  1205. }
  1206. }
  1207. }
  1208. }
  1209.  
  1210. protected alignPlaylists(
  1211. details: LevelDetails,
  1212. previousDetails?: LevelDetails
  1213. ): number {
  1214. const { levels, levelLastLoaded, fragPrevious } = this;
  1215. const lastLevel: Level | null =
  1216. levelLastLoaded !== null ? levels![levelLastLoaded] : null;
  1217.  
  1218. // FIXME: If not for `shouldAlignOnDiscontinuities` requiring fragPrevious.cc,
  1219. // this could all go in level-helper mergeDetails()
  1220. const length = details.fragments.length;
  1221. if (!length) {
  1222. this.warn(`No fragments in live playlist`);
  1223. return 0;
  1224. }
  1225. const slidingStart = details.fragments[0].start;
  1226. const firstLevelLoad = !previousDetails;
  1227. const aligned = details.alignedSliding && Number.isFinite(slidingStart);
  1228. if (firstLevelLoad || (!aligned && !slidingStart)) {
  1229. alignStream(fragPrevious, lastLevel, details);
  1230. const alignedSlidingStart = details.fragments[0].start;
  1231. this.log(
  1232. `Live playlist sliding: ${alignedSlidingStart.toFixed(2)} start-sn: ${
  1233. previousDetails ? previousDetails.startSN : 'na'
  1234. }->${details.startSN} prev-sn: ${
  1235. fragPrevious ? fragPrevious.sn : 'na'
  1236. } fragments: ${length}`
  1237. );
  1238. return alignedSlidingStart;
  1239. }
  1240. return slidingStart;
  1241. }
  1242.  
  1243. protected waitForCdnTuneIn(details: LevelDetails) {
  1244. // Wait for Low-Latency CDN Tune-in to get an updated playlist
  1245. const advancePartLimit = 3;
  1246. return (
  1247. details.live &&
  1248. details.canBlockReload &&
  1249. details.partTarget &&
  1250. details.tuneInGoal >
  1251. Math.max(details.partHoldBack, details.partTarget * advancePartLimit)
  1252. );
  1253. }
  1254.  
  1255. protected setStartPosition(details: LevelDetails, sliding: number) {
  1256. // compute start position if set to -1. use it straight away if value is defined
  1257. let startPosition = this.startPosition;
  1258. if (startPosition < sliding) {
  1259. startPosition = -1;
  1260. }
  1261. if (startPosition === -1 || this.lastCurrentTime === -1) {
  1262. // first, check if start time offset has been set in playlist, if yes, use this value
  1263. const startTimeOffset = details.startTimeOffset!;
  1264. if (Number.isFinite(startTimeOffset)) {
  1265. startPosition = sliding + startTimeOffset;
  1266. if (startTimeOffset < 0) {
  1267. startPosition += details.totalduration;
  1268. }
  1269. startPosition = Math.min(
  1270. Math.max(sliding, startPosition),
  1271. sliding + details.totalduration
  1272. );
  1273. this.log(
  1274. `Start time offset ${startTimeOffset} found in playlist, adjust startPosition to ${startPosition}`
  1275. );
  1276. this.startPosition = startPosition;
  1277. } else if (details.live) {
  1278. // Leave this.startPosition at -1, so that we can use `getInitialLiveFragment` logic when startPosition has
  1279. // not been specified via the config or an as an argument to startLoad (#3736).
  1280. startPosition = this.hls.liveSyncPosition || sliding;
  1281. } else {
  1282. this.startPosition = startPosition = 0;
  1283. }
  1284. this.lastCurrentTime = startPosition;
  1285. }
  1286. this.nextLoadPosition = startPosition;
  1287. }
  1288.  
  1289. protected getLoadPosition(): number {
  1290. const { media } = this;
  1291. // if we have not yet loaded any fragment, start loading from start position
  1292. let pos = 0;
  1293. if (this.loadedmetadata && media) {
  1294. pos = media.currentTime;
  1295. } else if (this.nextLoadPosition) {
  1296. pos = this.nextLoadPosition;
  1297. }
  1298.  
  1299. return pos;
  1300. }
  1301.  
  1302. private handleFragLoadAborted(frag: Fragment, part: Part | undefined) {
  1303. if (this.transmuxer && frag.sn !== 'initSegment' && frag.stats.aborted) {
  1304. this.warn(
  1305. `Fragment ${frag.sn}${part ? ' part' + part.index : ''} of level ${
  1306. frag.level
  1307. } was aborted`
  1308. );
  1309. this.resetFragmentLoading(frag);
  1310. }
  1311. }
  1312.  
  1313. protected resetFragmentLoading(frag: Fragment) {
  1314. if (
  1315. !this.fragCurrent ||
  1316. (!this.fragContextChanged(frag) &&
  1317. this.state !== State.FRAG_LOADING_WAITING_RETRY)
  1318. ) {
  1319. this.state = State.IDLE;
  1320. }
  1321. }
  1322.  
  1323. protected onFragmentOrKeyLoadError(
  1324. filterType: PlaylistLevelType,
  1325. data: ErrorData
  1326. ) {
  1327. if (data.fatal) {
  1328. this.stopLoad();
  1329. this.state = State.ERROR;
  1330. return;
  1331. }
  1332. const config = this.config;
  1333. if (data.chunkMeta) {
  1334. // Parsing Error: no retries
  1335. const context = this.getCurrentContext(data.chunkMeta);
  1336. if (context) {
  1337. data.frag = context.frag;
  1338. data.levelRetry = true;
  1339. this.fragLoadError = config.fragLoadingMaxRetry;
  1340. }
  1341. }
  1342. const frag = data.frag;
  1343. // Handle frag error related to caller's filterType
  1344. if (!frag || frag.type !== filterType) {
  1345. return;
  1346. }
  1347. const fragCurrent = this.fragCurrent;
  1348. console.assert(
  1349. fragCurrent &&
  1350. frag.sn === fragCurrent.sn &&
  1351. frag.level === fragCurrent.level &&
  1352. frag.urlId === fragCurrent.urlId,
  1353. 'Frag load error must match current frag to retry'
  1354. );
  1355. // keep retrying until the limit will be reached
  1356. if (this.fragLoadError + 1 <= config.fragLoadingMaxRetry) {
  1357. if (!this.loadedmetadata) {
  1358. this.startFragRequested = false;
  1359. this.nextLoadPosition = this.startPosition;
  1360. }
  1361. // exponential backoff capped to config.fragLoadingMaxRetryTimeout
  1362. const delay = Math.min(
  1363. Math.pow(2, this.fragLoadError) * config.fragLoadingRetryDelay,
  1364. config.fragLoadingMaxRetryTimeout
  1365. );
  1366. this.warn(
  1367. `Fragment ${frag.sn} of ${filterType} ${frag.level} failed to load, retrying in ${delay}ms`
  1368. );
  1369. this.retryDate = self.performance.now() + delay;
  1370. this.fragLoadError++;
  1371. this.state = State.FRAG_LOADING_WAITING_RETRY;
  1372. } else if (data.levelRetry) {
  1373. if (filterType === PlaylistLevelType.AUDIO) {
  1374. // Reset current fragment since audio track audio is essential and may not have a fail-over track
  1375. this.fragCurrent = null;
  1376. }
  1377. // Fragment errors that result in a level switch or redundant fail-over
  1378. // should reset the stream controller state to idle
  1379. this.fragLoadError = 0;
  1380. this.state = State.IDLE;
  1381. } else {
  1382. logger.error(
  1383. `${data.details} reaches max retry, redispatch as fatal ...`
  1384. );
  1385. // switch error to fatal
  1386. data.fatal = true;
  1387. this.hls.stopLoad();
  1388. this.state = State.ERROR;
  1389. }
  1390. }
  1391.  
  1392. protected afterBufferFlushed(
  1393. media: Bufferable,
  1394. bufferType: SourceBufferName,
  1395. playlistType: PlaylistLevelType
  1396. ) {
  1397. if (!media) {
  1398. return;
  1399. }
  1400. // After successful buffer flushing, filter flushed fragments from bufferedFrags use mediaBuffered instead of media
  1401. // (so that we will check against video.buffered ranges in case of alt audio track)
  1402. const bufferedTimeRanges = BufferHelper.getBuffered(media);
  1403. this.fragmentTracker.detectEvictedFragments(
  1404. bufferType,
  1405. bufferedTimeRanges,
  1406. playlistType
  1407. );
  1408. if (this.state === State.ENDED) {
  1409. this.resetLoadingState();
  1410. }
  1411. }
  1412.  
  1413. protected resetLoadingState() {
  1414. this.log('Reset loading state');
  1415. this.fragCurrent = null;
  1416. this.fragPrevious = null;
  1417. this.state = State.IDLE;
  1418. }
  1419.  
  1420. protected resetStartWhenNotLoaded(level: number): void {
  1421. // if loadedmetadata is not set, it means that first frag request failed
  1422. // in that case, reset startFragRequested flag
  1423. if (!this.loadedmetadata) {
  1424. this.startFragRequested = false;
  1425. const details = this.levels ? this.levels[level].details : null;
  1426. if (details?.live) {
  1427. // Update the start position and return to IDLE to recover live start
  1428. this.startPosition = -1;
  1429. this.setStartPosition(details, 0);
  1430. this.resetLoadingState();
  1431. } else {
  1432. this.nextLoadPosition = this.startPosition;
  1433. }
  1434. }
  1435. }
  1436.  
  1437. private updateLevelTiming(
  1438. frag: Fragment,
  1439. part: Part | null,
  1440. level: Level,
  1441. partial: boolean
  1442. ) {
  1443. const details = level.details as LevelDetails;
  1444. console.assert(!!details, 'level.details must be defined');
  1445. const parsed = Object.keys(frag.elementaryStreams).reduce(
  1446. (result, type) => {
  1447. const info = frag.elementaryStreams[type];
  1448. if (info) {
  1449. const parsedDuration = info.endPTS - info.startPTS;
  1450. if (parsedDuration <= 0) {
  1451. // Destroy the transmuxer after it's next time offset failed to advance because duration was <= 0.
  1452. // The new transmuxer will be configured with a time offset matching the next fragment start,
  1453. // preventing the timeline from shifting.
  1454. this.warn(
  1455. `Could not parse fragment ${frag.sn} ${type} duration reliably (${parsedDuration})`
  1456. );
  1457. return result || false;
  1458. }
  1459. const drift = partial
  1460. ? 0
  1461. : updateFragPTSDTS(
  1462. details,
  1463. frag,
  1464. info.startPTS,
  1465. info.endPTS,
  1466. info.startDTS,
  1467. info.endDTS
  1468. );
  1469. this.hls.trigger(Events.LEVEL_PTS_UPDATED, {
  1470. details,
  1471. level,
  1472. drift,
  1473. type,
  1474. frag,
  1475. start: info.startPTS,
  1476. end: info.endPTS,
  1477. });
  1478. return true;
  1479. }
  1480. return result;
  1481. },
  1482. false
  1483. );
  1484. if (!parsed) {
  1485. this.warn(
  1486. `Found no media in fragment ${frag.sn} of level ${level.id} resetting transmuxer to fallback to playlist timing`
  1487. );
  1488. this.resetTransmuxer();
  1489. }
  1490. this.state = State.PARSED;
  1491. this.hls.trigger(Events.FRAG_PARSED, { frag, part });
  1492. }
  1493.  
  1494. protected resetTransmuxer() {
  1495. if (this.transmuxer) {
  1496. this.transmuxer.destroy();
  1497. this.transmuxer = null;
  1498. }
  1499. }
  1500.  
  1501. set state(nextState) {
  1502. const previousState = this._state;
  1503. if (previousState !== nextState) {
  1504. this._state = nextState;
  1505. this.log(`${previousState}->${nextState}`);
  1506. }
  1507. }
  1508.  
  1509. get state() {
  1510. return this._state;
  1511. }
  1512. }