Home Reference Source

src/controller/base-stream-controller.ts

  1. import TaskLoop from '../task-loop';
  2. import { FragmentState } from './fragment-tracker';
  3. import { BufferHelper } from '../utils/buffer-helper';
  4. import { logger } from '../utils/logger';
  5. import { Events } from '../events';
  6. import { ErrorDetails } from '../errors';
  7. import * as LevelHelper from './level-helper';
  8. import { ChunkMetadata } from '../types/transmuxer';
  9. import { appendUint8Array } from '../utils/mp4-tools';
  10. import { alignStream } from '../utils/discontinuities';
  11. import {
  12. findFragmentByPDT,
  13. findFragmentByPTS,
  14. findFragWithCC,
  15. } from './fragment-finders';
  16. import TransmuxerInterface from '../demux/transmuxer-interface';
  17. import Fragment, { Part } from '../loader/fragment';
  18. import FragmentLoader, {
  19. FragmentLoadProgressCallback,
  20. LoadError,
  21. } from '../loader/fragment-loader';
  22. import LevelDetails from '../loader/level-details';
  23. import {
  24. BufferAppendingData,
  25. ErrorData,
  26. FragLoadedData,
  27. PartsLoadedData,
  28. KeyLoadedData,
  29. MediaAttachingData,
  30. BufferFlushingData,
  31. } from '../types/events';
  32. import Decrypter from '../crypt/decrypter';
  33. import TimeRanges from '../utils/time-ranges';
  34. import type { FragmentTracker } from './fragment-tracker';
  35. import type { Level } from '../types/level';
  36. import type { RemuxedTrack } from '../types/remuxer';
  37. import type Hls from '../hls';
  38. import type { HlsConfig } from '../config';
  39. import type { HlsEventEmitter } from '../events';
  40. import type { NetworkComponentAPI } from '../types/component-api';
  41. import type { SourceBufferName } from '../types/buffer';
  42.  
  43. export const State = {
  44. STOPPED: 'STOPPED',
  45. IDLE: 'IDLE',
  46. KEY_LOADING: 'KEY_LOADING',
  47. FRAG_LOADING: 'FRAG_LOADING',
  48. FRAG_LOADING_WAITING_RETRY: 'FRAG_LOADING_WAITING_RETRY',
  49. WAITING_TRACK: 'WAITING_TRACK',
  50. PARSING: 'PARSING',
  51. PARSED: 'PARSED',
  52. BACKTRACKING: 'BACKTRACKING',
  53. ENDED: 'ENDED',
  54. ERROR: 'ERROR',
  55. WAITING_INIT_PTS: 'WAITING_INIT_PTS',
  56. WAITING_LEVEL: 'WAITING_LEVEL',
  57. };
  58.  
  59. export default class BaseStreamController
  60. extends TaskLoop
  61. implements NetworkComponentAPI {
  62. protected hls: Hls;
  63.  
  64. protected fragPrevious: Fragment | null = null;
  65. protected fragCurrent: Fragment | null = null;
  66. protected fragmentTracker: FragmentTracker;
  67. protected transmuxer: TransmuxerInterface | null = null;
  68. protected _state: string = State.STOPPED;
  69. protected media?: any;
  70. protected mediaBuffer?: any;
  71. protected config: HlsConfig;
  72. protected lastCurrentTime: number = 0;
  73. protected nextLoadPosition: number = 0;
  74. protected startPosition: number = 0;
  75. protected loadedmetadata: boolean = false;
  76. protected fragLoadError: number = 0;
  77. protected levels: Array<Level> | null = null;
  78. protected fragmentLoader!: FragmentLoader;
  79. protected levelLastLoaded: number | null = null;
  80. protected startFragRequested: boolean = false;
  81. protected decrypter: Decrypter;
  82. protected initPTS: Array<number> = [];
  83. protected onvseeking: EventListener | null = null;
  84. protected onvended: EventListener | null = null;
  85.  
  86. private readonly logPrefix: string = '';
  87. protected readonly log: (msg: any) => void;
  88. protected readonly warn: (msg: any) => void;
  89.  
  90. constructor(hls: Hls, fragmentTracker: FragmentTracker, logPrefix: string) {
  91. super();
  92. this.logPrefix = logPrefix;
  93. this.log = logger.log.bind(logger, `${logPrefix}:`);
  94. this.warn = logger.warn.bind(logger, `${logPrefix}:`);
  95. this.hls = hls;
  96. this.fragmentTracker = fragmentTracker;
  97. this.config = hls.config;
  98. this.decrypter = new Decrypter(hls as HlsEventEmitter, hls.config);
  99. hls.on(Events.KEY_LOADED, this.onKeyLoaded, this);
  100. }
  101.  
  102. protected doTick() {
  103. this.onTickEnd();
  104. }
  105.  
  106. protected onTickEnd() {}
  107.  
  108. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  109. public startLoad(startPosition: number): void {}
  110.  
  111. public stopLoad() {
  112. const frag = this.fragCurrent;
  113. if (frag) {
  114. if (frag.loader) {
  115. frag.loader.abort();
  116. }
  117. this.fragmentTracker.removeFragment(frag);
  118. }
  119. if (this.transmuxer) {
  120. this.transmuxer.destroy();
  121. this.transmuxer = null;
  122. }
  123. this.fragCurrent = null;
  124. this.fragPrevious = null;
  125. this.clearInterval();
  126. this.clearNextTick();
  127. this.state = State.STOPPED;
  128. }
  129.  
  130. protected _streamEnded(bufferInfo, levelDetails) {
  131. const { fragCurrent, fragmentTracker } = this;
  132. // we just got done loading the final fragment and there is no other buffered range after ...
  133. // rationale is that in case there are any buffered ranges after, it means that there are unbuffered portion in between
  134. // so we should not switch to ENDED in that case, to be able to buffer them
  135. if (
  136. !levelDetails.live &&
  137. fragCurrent &&
  138. fragCurrent.sn === levelDetails.endSN &&
  139. !bufferInfo.nextStart
  140. ) {
  141. const fragState = fragmentTracker.getState(fragCurrent);
  142. return (
  143. fragState === FragmentState.PARTIAL || fragState === FragmentState.OK
  144. );
  145. }
  146. return false;
  147. }
  148.  
  149. protected onMediaAttached(
  150. event: Events.MEDIA_ATTACHED,
  151. data: MediaAttachingData
  152. ) {
  153. const media = (this.media = this.mediaBuffer = data.media);
  154. this.onvseeking = this.onMediaSeeking.bind(this);
  155. this.onvended = this.onMediaEnded.bind(this);
  156. media.addEventListener('seeking', this.onvseeking as EventListener);
  157. media.addEventListener('ended', this.onvended as EventListener);
  158. const config = this.config;
  159. if (this.levels && config.autoStartLoad && this.state === State.STOPPED) {
  160. this.startLoad(config.startPosition);
  161. }
  162. }
  163.  
  164. protected onMediaDetaching() {
  165. const media = this.media;
  166. if (media?.ended) {
  167. this.log('MSE detaching and video ended, reset startPosition');
  168. this.startPosition = this.lastCurrentTime = 0;
  169. }
  170.  
  171. // remove video listeners
  172. if (media) {
  173. media.removeEventListener('seeking', this.onvseeking);
  174. media.removeEventListener('ended', this.onvended);
  175. this.onvseeking = this.onvended = null;
  176. }
  177. this.media = this.mediaBuffer = null;
  178. this.loadedmetadata = false;
  179. this.fragmentTracker.removeAllFragments();
  180. this.stopLoad();
  181. }
  182.  
  183. protected onMediaSeeking() {
  184. const { config, fragCurrent, media, mediaBuffer, state } = this;
  185. const currentTime = media ? media.currentTime : null;
  186. const bufferInfo = BufferHelper.bufferInfo(
  187. mediaBuffer || media,
  188. currentTime,
  189. config.maxBufferHole
  190. );
  191.  
  192. this.log(
  193. `media seeking to ${
  194. Number.isFinite(currentTime) ? currentTime.toFixed(3) : currentTime
  195. }, state: ${state}`
  196. );
  197.  
  198. if (state === State.ENDED) {
  199. // if seeking to unbuffered area, clean up fragPrevious
  200. if (!bufferInfo.len) {
  201. this.fragPrevious = null;
  202. this.fragCurrent = null;
  203. }
  204. // switch to IDLE state to check for potential new fragment
  205. this.state = State.IDLE;
  206. } else if (fragCurrent && !bufferInfo.len) {
  207. // check if we are seeking to a unbuffered area AND if frag loading is in progress
  208. const tolerance = config.maxFragLookUpTolerance;
  209. const fragStartOffset = fragCurrent.start - tolerance;
  210. const fragEndOffset =
  211. fragCurrent.start + fragCurrent.duration + tolerance;
  212. // check if we seek position will be out of currently loaded frag range : if out cancel frag load, if in, don't do anything
  213. if (currentTime < fragStartOffset || currentTime > fragEndOffset) {
  214. if (fragCurrent.loader) {
  215. this.log(
  216. 'seeking outside of buffer while fragment load in progress, cancel fragment load'
  217. );
  218. fragCurrent.loader.abort();
  219. }
  220. this.fragCurrent = null;
  221. this.fragPrevious = null;
  222. // switch to IDLE state to load new fragment
  223. this.state = State.IDLE;
  224. }
  225. }
  226.  
  227. if (media) {
  228. this.lastCurrentTime = currentTime;
  229. }
  230.  
  231. // in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target
  232. if (!this.loadedmetadata) {
  233. this.nextLoadPosition = this.startPosition = currentTime;
  234. }
  235.  
  236. // tick to speed up processing
  237. this.tick();
  238. }
  239.  
  240. protected onMediaEnded() {
  241. // reset startPosition and lastCurrentTime to restart playback @ stream beginning
  242. this.startPosition = this.lastCurrentTime = 0;
  243. }
  244.  
  245. onKeyLoaded(event: Events.KEY_LOADED, data: KeyLoadedData) {
  246. if (this.state === State.KEY_LOADING && this.levels) {
  247. this.state = State.IDLE;
  248. const levelDetails = this.levels[data.frag.level].details;
  249. if (levelDetails) {
  250. this.loadFragment(data.frag, levelDetails, data.frag.start);
  251. }
  252. }
  253. }
  254.  
  255. protected onHandlerDestroying() {
  256. this.stopLoad();
  257. super.onHandlerDestroying();
  258. }
  259.  
  260. protected onHandlerDestroyed() {
  261. this.state = State.STOPPED;
  262. this.hls.off(Events.KEY_LOADED, this.onKeyLoaded, this);
  263. super.onHandlerDestroyed();
  264. }
  265.  
  266. protected loadFragment(
  267. frag: Fragment,
  268. levelDetails: LevelDetails,
  269. targetBufferTime: number
  270. ) {
  271. this._loadFragForPlayback(frag, levelDetails, targetBufferTime);
  272. }
  273.  
  274. private _loadFragForPlayback(
  275. frag: Fragment,
  276. levelDetails: LevelDetails,
  277. targetBufferTime: number
  278. ) {
  279. const progressCallback: FragmentLoadProgressCallback = (
  280. data: FragLoadedData
  281. ) => {
  282. if (this.fragContextChanged(frag)) {
  283. this.warn(
  284. `Fragment ${frag.sn}${
  285. data.part ? ' p: ' + data.part.index : ''
  286. } of level ${frag.level} was dropped during download.`
  287. );
  288. this.fragmentTracker.removeFragment(frag);
  289. return;
  290. }
  291. frag.stats.chunkCount++;
  292. this._handleFragmentLoadProgress(data);
  293. };
  294.  
  295. this._doFragLoad(
  296. frag,
  297. levelDetails,
  298. targetBufferTime,
  299. progressCallback
  300. ).then((data) => {
  301. if (!data) {
  302. // if we're here we probably needed to backtrack or are waiting for more parts
  303. return;
  304. }
  305. this.fragLoadError = 0;
  306. if (this.fragContextChanged(frag)) {
  307. if (
  308. this.state === State.FRAG_LOADING ||
  309. this.state === State.BACKTRACKING
  310. ) {
  311. this.fragmentTracker.removeFragment(frag);
  312. this.state = State.IDLE;
  313. }
  314. return;
  315. }
  316.  
  317. if ('payload' in data) {
  318. this.log(`Loaded fragment ${frag.sn} of level ${frag.level}`);
  319. this.hls.trigger(Events.FRAG_LOADED, data);
  320.  
  321. // Tracker backtrack must be called after onFragLoaded to update the fragment entity state to BACKTRACKED
  322. // This happens after handleTransmuxComplete when the worker or progressive is disabled
  323. if (this.state === State.BACKTRACKING) {
  324. this.fragmentTracker.backtrack(frag, data);
  325. return;
  326. }
  327. }
  328.  
  329. // Pass through the whole payload; controllers not implementing progressive loading receive data from this callback
  330. this._handleFragmentLoadComplete(data);
  331. });
  332. }
  333.  
  334. protected flushMainBuffer(
  335. startOffset: number,
  336. endOffset: number,
  337. type: SourceBufferName | null = null
  338. ) {
  339. // When alternate audio is playing, the audio-stream-controller is responsible for the audio buffer. Otherwise,
  340. // passing a null type flushes both buffers
  341. const flushScope: BufferFlushingData = { startOffset, endOffset, type };
  342. // Reset load errors on flush
  343. this.fragLoadError = 0;
  344. this.hls.trigger(Events.BUFFER_FLUSHING, flushScope);
  345. }
  346.  
  347. protected _loadInitSegment(frag: Fragment) {
  348. this._doFragLoad(frag)
  349. .then((data) => {
  350. if (!data || this.fragContextChanged(frag) || !this.levels) {
  351. throw new Error('init load aborted');
  352. }
  353.  
  354. return data;
  355. })
  356. .then((data: FragLoadedData) => {
  357. const { hls } = this;
  358. const { payload } = data;
  359. const decryptData = frag.decryptdata;
  360.  
  361. // check to see if the payload needs to be decrypted
  362. if (
  363. payload &&
  364. payload.byteLength > 0 &&
  365. decryptData &&
  366. decryptData.key &&
  367. decryptData.iv &&
  368. decryptData.method === 'AES-128'
  369. ) {
  370. const startTime = self.performance.now();
  371. // decrypt the subtitles
  372. return this.decrypter
  373. .webCryptoDecrypt(
  374. new Uint8Array(payload),
  375. decryptData.key.buffer,
  376. decryptData.iv.buffer
  377. )
  378. .then((decryptedData) => {
  379. const endTime = self.performance.now();
  380. hls.trigger(Events.FRAG_DECRYPTED, {
  381. frag,
  382. payload: decryptedData,
  383. stats: {
  384. tstart: startTime,
  385. tdecrypt: endTime,
  386. },
  387. });
  388. data.payload = decryptedData;
  389.  
  390. return data;
  391. });
  392. }
  393.  
  394. return data;
  395. })
  396. .then((data: FragLoadedData) => {
  397. const { fragCurrent, hls, levels } = this;
  398. if (!levels) {
  399. throw new Error('init load aborted, missing levels');
  400. }
  401.  
  402. const details = levels[frag.level].details as LevelDetails;
  403. console.assert(
  404. details,
  405. 'Level details are defined when init segment is loaded'
  406. );
  407. const initSegment = details.initSegment as Fragment;
  408. console.assert(
  409. initSegment,
  410. 'Fragment initSegment is defined when init segment is loaded'
  411. );
  412.  
  413. const stats = frag.stats;
  414. this.state = State.IDLE;
  415. this.fragLoadError = 0;
  416. initSegment.data = new Uint8Array(data.payload);
  417. stats.parsing.start = stats.buffering.start = self.performance.now();
  418. stats.parsing.end = stats.buffering.end = self.performance.now();
  419.  
  420. // Silence FRAG_BUFFERED event if fragCurrent is null
  421. if (data.frag === fragCurrent) {
  422. hls.trigger(Events.FRAG_BUFFERED, {
  423. stats,
  424. frag: fragCurrent,
  425. part: null,
  426. id: frag.type,
  427. });
  428. }
  429. this.tick();
  430. })
  431. .catch((reason) => {
  432. this.warn(reason);
  433. });
  434. }
  435.  
  436. protected fragContextChanged(frag: Fragment | null) {
  437. const { fragCurrent } = this;
  438. return (
  439. !frag ||
  440. !fragCurrent ||
  441. frag.level !== fragCurrent.level ||
  442. frag.sn !== fragCurrent.sn ||
  443. frag.urlId !== fragCurrent.urlId
  444. );
  445. }
  446.  
  447. protected fragBufferedComplete(frag: Fragment, part: Part | null) {
  448. const media = this.mediaBuffer ? this.mediaBuffer : this.media;
  449. this.log(
  450. `Buffered ${frag.type} sn: ${frag.sn}${
  451. part ? ' part: ' + part.index : ''
  452. } of ${this.logPrefix === '[stream-controller]' ? 'level' : 'track'} ${
  453. frag.level
  454. } ${TimeRanges.toString(BufferHelper.getBuffered(media))}`
  455. );
  456. this.state = State.IDLE;
  457. this.tick();
  458. }
  459.  
  460. protected _handleFragmentLoadComplete(fragLoadedEndData: PartsLoadedData) {
  461. const { transmuxer } = this;
  462. if (!transmuxer) {
  463. return;
  464. }
  465. const { frag, part, partsLoaded } = fragLoadedEndData;
  466. // If we did not load parts, or loaded all parts, we have complete (not partial) fragment data
  467. const complete =
  468. !partsLoaded ||
  469. (partsLoaded &&
  470. (partsLoaded.length === 0 ||
  471. partsLoaded.some((fragLoaded) => !fragLoaded)));
  472. const chunkMeta = new ChunkMetadata(
  473. frag.level,
  474. frag.sn as number,
  475. frag.stats.chunkCount + 1,
  476. 0,
  477. part ? part.index : -1,
  478. !complete
  479. );
  480. transmuxer.flush(chunkMeta);
  481. }
  482.  
  483. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  484. protected _handleFragmentLoadProgress(frag: FragLoadedData) {}
  485.  
  486. protected _doFragLoad(
  487. frag: Fragment,
  488. details?: LevelDetails,
  489. targetBufferTime: number | null = null,
  490. progressCallback?: FragmentLoadProgressCallback
  491. ): Promise<PartsLoadedData | FragLoadedData | null> {
  492. if (!this.levels) {
  493. throw new Error('frag load aborted, missing levels');
  494. }
  495. targetBufferTime = Math.max(frag.start, targetBufferTime || 0);
  496. if (this.config.lowLatencyMode && details) {
  497. const partList = details.partList;
  498. if (partList && progressCallback) {
  499. const partIndex = this.getNextPart(partList, frag, targetBufferTime);
  500. if (partIndex > -1) {
  501. const part = partList[partIndex];
  502. this.log(
  503. `Loading part sn: ${frag.sn} p: ${part.index} cc: ${
  504. frag.cc
  505. } of playlist [${details.startSN}-${
  506. details.endSN
  507. }] parts [0-${partIndex}-${partList.length - 1}] ${
  508. this.logPrefix === '[stream-controller]' ? 'level' : 'track'
  509. }: ${frag.level}, target: ${parseFloat(
  510. targetBufferTime.toFixed(3)
  511. )}`
  512. );
  513. this.state = State.FRAG_LOADING;
  514. this.hls.trigger(Events.FRAG_LOADING, {
  515. frag,
  516. part: partList[partIndex],
  517. targetBufferTime,
  518. });
  519. return this.doFragPartsLoad(
  520. frag,
  521. partList,
  522. partIndex,
  523. progressCallback
  524. ).catch((error: LoadError) => this.handleFragError(error));
  525. } else if (
  526. !frag.url ||
  527. this.loadedEndOfParts(partList, targetBufferTime)
  528. ) {
  529. // Fragment hint has no parts
  530. return Promise.resolve(null);
  531. }
  532. }
  533. }
  534.  
  535. this.log(
  536. `Loading fragment ${frag.sn} cc: ${frag.cc} ${
  537. details ? 'of [' + details.startSN + '-' + details.endSN + '] ' : ''
  538. }${this.logPrefix === '[stream-controller]' ? 'level' : 'track'}: ${
  539. frag.level
  540. }, target: ${parseFloat(targetBufferTime.toFixed(3))}`
  541. );
  542.  
  543. this.state = State.FRAG_LOADING;
  544. this.hls.trigger(Events.FRAG_LOADING, { frag, targetBufferTime });
  545.  
  546. return this.fragmentLoader
  547. .load(frag, progressCallback)
  548. .catch((error: LoadError) => this.handleFragError(error));
  549. }
  550.  
  551. private doFragPartsLoad(
  552. frag: Fragment,
  553. partList: Part[],
  554. partIndex: number,
  555. progressCallback: FragmentLoadProgressCallback
  556. ): Promise<PartsLoadedData | null> {
  557. return new Promise(
  558. (resolve: (FragLoadedEndData) => void, reject: (LoadError) => void) => {
  559. const partsLoaded: FragLoadedData[] = [];
  560. const loadPartIndex = (index: number) => {
  561. const part = partList[index];
  562. this.fragmentLoader
  563. .loadPart(frag, part, progressCallback)
  564. .then((partLoadedData: FragLoadedData) => {
  565. partsLoaded[part.index] = partLoadedData;
  566. const loadedPart = partLoadedData.part as Part;
  567. this.hls.trigger(Events.FRAG_LOADED, partLoadedData);
  568. const nextPart = partList[index + 1];
  569. if (nextPart && nextPart.fragment === frag) {
  570. loadPartIndex(index + 1);
  571. } else {
  572. return resolve({
  573. frag,
  574. part: loadedPart,
  575. partsLoaded,
  576. });
  577. }
  578. })
  579. .catch(reject);
  580. };
  581. loadPartIndex(partIndex);
  582. }
  583. );
  584. }
  585.  
  586. private handleFragError({ data }: LoadError) {
  587. if (data && data.details === ErrorDetails.INTERNAL_ABORTED) {
  588. this.handleFragLoadAborted(data.frag, data.part);
  589. } else {
  590. this.hls.trigger(Events.ERROR, data as ErrorData);
  591. }
  592. return null;
  593. }
  594.  
  595. protected _handleTransmuxerFlush(chunkMeta: ChunkMetadata) {
  596. if (this.state !== State.PARSING) {
  597. this.warn(
  598. `State is expected to be PARSING on transmuxer flush, but is ${this.state}.`
  599. );
  600. return;
  601. }
  602.  
  603. const context = this.getCurrentContext(chunkMeta);
  604. if (!context) {
  605. return;
  606. }
  607. const { frag, part, level } = context;
  608. const now = self.performance.now();
  609. frag.stats.parsing.end = now;
  610. if (part) {
  611. part.stats.parsing.end = now;
  612. }
  613. this.updateLevelTiming(frag, level, chunkMeta.partial);
  614. this.state = State.PARSED;
  615. this.hls.trigger(Events.FRAG_PARSED, { frag, part });
  616. }
  617.  
  618. protected getCurrentContext(
  619. chunkMeta: ChunkMetadata
  620. ): { frag: Fragment; part: Part | null; level: Level } | null {
  621. const { levels } = this;
  622. const { level: levelIndex, sn, part: partIndex } = chunkMeta;
  623. if (!levels || !levels[levelIndex]) {
  624. this.warn(
  625. `Levels object was unset while buffering fragment ${sn} of level ${levelIndex}. The current chunk will not be buffered.`
  626. );
  627. return null;
  628. }
  629. const level = levels[levelIndex];
  630. const part =
  631. partIndex > -1 ? LevelHelper.getPartWith(level, sn, partIndex) : null;
  632. const frag = part
  633. ? part.fragment
  634. : LevelHelper.getFragmentWithSN(level, sn);
  635. if (!frag) {
  636. return null;
  637. }
  638. return { frag, part, level };
  639. }
  640.  
  641. protected bufferFragmentData(
  642. data: RemuxedTrack,
  643. frag: Fragment,
  644. part: Part | null,
  645. chunkMeta: ChunkMetadata
  646. ) {
  647. if (!data || this.state !== State.PARSING) {
  648. return;
  649. }
  650.  
  651. const { data1, data2 } = data;
  652. let buffer = data1;
  653. if (data1 && data2) {
  654. // Combine the moof + mdat so that we buffer with a single append
  655. buffer = appendUint8Array(data1, data2);
  656. }
  657.  
  658. if (!buffer || !buffer.length) {
  659. return;
  660. }
  661.  
  662. const segment: BufferAppendingData = {
  663. type: data.type,
  664. data: buffer,
  665. frag,
  666. part,
  667. chunkMeta,
  668. };
  669. this.hls.trigger(Events.BUFFER_APPENDING, segment);
  670.  
  671. if (data.dropped && data.independent && !part) {
  672. // Clear buffer so that we reload previous segments sequentially if required
  673. this.flushMainBuffer(0, frag.start);
  674. }
  675. }
  676.  
  677. protected reduceMaxBufferLength(threshold?: number) {
  678. const config = this.config;
  679. const minLength = threshold || config.maxBufferLength;
  680. if (config.maxMaxBufferLength >= minLength) {
  681. // reduce max buffer length as it might be too high. we do this to avoid loop flushing ...
  682. config.maxMaxBufferLength /= 2;
  683. this.warn(`Reduce max buffer length to ${config.maxMaxBufferLength}s`);
  684. return true;
  685. }
  686. return false;
  687. }
  688.  
  689. protected getNextFragment(
  690. pos: number,
  691. levelDetails: LevelDetails
  692. ): Fragment | null {
  693. const { config, startFragRequested } = this;
  694. const fragments = levelDetails.fragments;
  695. const fragLen = fragments.length;
  696.  
  697. if (!fragLen) {
  698. return null;
  699. }
  700.  
  701. // find fragment index, contiguous with end of buffer position
  702. const start = fragments[0].start;
  703. let frag;
  704.  
  705. // If an initSegment is present, it must be buffered first
  706. if (levelDetails.initSegment && !levelDetails.initSegment.data) {
  707. frag = levelDetails.initSegment;
  708. } else if (levelDetails.live) {
  709. const initialLiveManifestSize = config.initialLiveManifestSize;
  710. if (fragLen < initialLiveManifestSize) {
  711. this.warn(
  712. `Not enough fragments to start playback (have: ${fragLen}, need: ${initialLiveManifestSize})`
  713. );
  714. return null;
  715. }
  716. // The real fragment start times for a live stream are only known after the PTS range for that level is known.
  717. // In order to discover the range, we load the best matching fragment for that level and demux it.
  718. // Do not load using live logic if the starting frag is requested - we want to use getFragmentAtPosition() so that
  719. // we get the fragment matching that start time
  720. if (!levelDetails.PTSKnown && !startFragRequested) {
  721. frag = this.getInitialLiveFragment(levelDetails, fragments);
  722. }
  723. } else if (pos <= start) {
  724. // VoD playlist: if loadPosition before start of playlist, load first fragment
  725. frag = fragments[0];
  726. }
  727.  
  728. // If we haven't run into any special cases already, just load the fragment most closely matching the requested position
  729. if (!frag) {
  730. const end = config.lowLatencyMode
  731. ? levelDetails.partEnd
  732. : levelDetails.fragmentEnd;
  733. frag = this.getFragmentAtPosition(pos, end, levelDetails);
  734. }
  735.  
  736. return frag;
  737. }
  738.  
  739. getNextPart(
  740. partList: Part[],
  741. frag: Fragment,
  742. targetBufferTime: number
  743. ): number {
  744. let nextPart = -1;
  745. let contiguous = false;
  746. for (let i = 0, len = partList.length; i < len; i++) {
  747. const part = partList[i];
  748. if (nextPart > -1 && targetBufferTime < part.start) {
  749. break;
  750. }
  751. const loaded = part.loaded;
  752. if (
  753. !loaded &&
  754. (contiguous || part.independent) &&
  755. part.fragment === frag
  756. ) {
  757. nextPart = i;
  758. }
  759. contiguous = loaded;
  760. }
  761. return nextPart;
  762. }
  763.  
  764. private loadedEndOfParts(
  765. partList: Part[],
  766. targetBufferTime: number
  767. ): boolean {
  768. const lastPart = partList[partList.length - 1];
  769. return lastPart && targetBufferTime > lastPart.start && lastPart.loaded;
  770. }
  771.  
  772. /*
  773. This method is used find the best matching first fragment for a live playlist. This fragment is used to calculate the
  774. "sliding" of the playlist, which is its offset from the start of playback. After sliding we can compute the real
  775. start and end times for each fragment in the playlist (after which this method will not need to be called).
  776. */
  777. protected getInitialLiveFragment(
  778. levelDetails: LevelDetails,
  779. fragments: Array<Fragment>
  780. ): Fragment | null {
  781. const { config, fragPrevious } = this;
  782. let frag: Fragment | null = null;
  783. if (fragPrevious) {
  784. if (levelDetails.hasProgramDateTime) {
  785. // Prefer using PDT, because it can be accurate enough to choose the correct fragment without knowing the level sliding
  786. this.log(
  787. `Live playlist, switching playlist, load frag with same PDT: ${fragPrevious.programDateTime}`
  788. );
  789. frag = findFragmentByPDT(
  790. fragments,
  791. fragPrevious.endProgramDateTime,
  792. config.maxFragLookUpTolerance
  793. );
  794. } else {
  795. // SN does not need to be accurate between renditions, but depending on the packaging it may be so.
  796. const targetSN = (fragPrevious.sn as number) + 1;
  797. if (
  798. targetSN >= levelDetails.startSN &&
  799. targetSN <= levelDetails.endSN
  800. ) {
  801. const fragNext = fragments[targetSN - levelDetails.startSN];
  802. // Ensure that we're staying within the continuity range, since PTS resets upon a new range
  803. if (fragPrevious.cc === fragNext.cc) {
  804. frag = fragNext;
  805. this.log(
  806. `Live playlist, switching playlist, load frag with next SN: ${
  807. frag!.sn
  808. }`
  809. );
  810. }
  811. }
  812. // It's important to stay within the continuity range if available; otherwise the fragments in the playlist
  813. // will have the wrong start times
  814. if (!frag) {
  815. frag = findFragWithCC(fragments, fragPrevious.cc);
  816. if (frag) {
  817. this.log(
  818. `Live playlist, switching playlist, load frag with same CC: ${frag.sn}`
  819. );
  820. }
  821. }
  822. }
  823. }
  824.  
  825. return frag;
  826. }
  827.  
  828. /*
  829. This method finds the best matching fragment given the provided position.
  830. */
  831. protected getFragmentAtPosition(
  832. bufferEnd: number,
  833. end: number,
  834. levelDetails: LevelDetails
  835. ): Fragment | null {
  836. const { config, fragPrevious } = this;
  837. let { fragments, endSN } = levelDetails;
  838. const { fragmentHint } = levelDetails;
  839. const tolerance = config.maxFragLookUpTolerance;
  840.  
  841. const loadingParts = !!(
  842. config.lowLatencyMode &&
  843. levelDetails.partList &&
  844. fragmentHint
  845. );
  846. if (loadingParts && fragmentHint) {
  847. // Include incomplete fragment with parts at end
  848. fragments = fragments.concat(fragmentHint);
  849. endSN = fragmentHint.sn as number;
  850. }
  851.  
  852. let frag;
  853. if (bufferEnd < end) {
  854. const lookupTolerance = bufferEnd > end - tolerance ? 0 : tolerance;
  855. // Remove the tolerance if it would put the bufferEnd past the actual end of stream
  856. // Uses buffer and sequence number to calculate switch segment (required if using EXT-X-DISCONTINUITY-SEQUENCE)
  857. frag = findFragmentByPTS(
  858. fragPrevious,
  859. fragments,
  860. bufferEnd,
  861. lookupTolerance
  862. );
  863. } else {
  864. // reach end of playlist
  865. frag = fragments[fragments.length - 1];
  866. }
  867.  
  868. if (frag) {
  869. const curSNIdx = frag.sn - levelDetails.startSN;
  870. const sameLevel = fragPrevious && frag.level === fragPrevious.level;
  871. const nextFrag = fragments[curSNIdx + 1];
  872. const fragState = this.fragmentTracker.getState(frag);
  873. if (fragState === FragmentState.BACKTRACKED) {
  874. frag = null;
  875. let i = curSNIdx;
  876. while (
  877. fragments[i] &&
  878. this.fragmentTracker.getState(fragments[i]) ===
  879. FragmentState.BACKTRACKED
  880. ) {
  881. // When fragPrevious is null, backtrack to first the first fragment is not BACKTRACKED for loading
  882. // When fragPrevious is set, we want the first BACKTRACKED fragment for parsing and buffering
  883. if (!fragPrevious) {
  884. frag = fragments[--i];
  885. } else {
  886. frag = fragments[i--];
  887. }
  888. }
  889. if (!frag) {
  890. frag = nextFrag;
  891. }
  892. } else if (fragPrevious && frag.sn === fragPrevious.sn && !loadingParts) {
  893. // Force the next fragment to load if the previous one was already selected. This can occasionally happen with
  894. // non-uniform fragment durations
  895. if (sameLevel) {
  896. if (
  897. frag.sn < endSN &&
  898. this.fragmentTracker.getState(nextFrag) !== FragmentState.OK
  899. ) {
  900. this.log(
  901. `SN ${frag.sn} just loaded, load next one: ${nextFrag.sn}`
  902. );
  903. frag = nextFrag;
  904. } else {
  905. frag = null;
  906. }
  907. }
  908. }
  909. }
  910. return frag;
  911. }
  912.  
  913. protected synchronizeToLiveEdge(levelDetails: LevelDetails): number | null {
  914. const { config, media } = this;
  915. const liveSyncPosition = this.hls.liveSyncPosition;
  916. const currentTime = media.currentTime;
  917. if (
  918. liveSyncPosition !== null &&
  919. media?.readyState &&
  920. media.duration > liveSyncPosition &&
  921. liveSyncPosition > currentTime
  922. ) {
  923. const maxLatency =
  924. config.liveMaxLatencyDuration !== undefined
  925. ? config.liveMaxLatencyDuration
  926. : config.liveMaxLatencyDurationCount * levelDetails.targetduration;
  927. const start = levelDetails.fragments[0].start;
  928. const end = levelDetails.edge;
  929. if (
  930. currentTime <
  931. Math.max(start - config.maxFragLookUpTolerance, end - maxLatency)
  932. ) {
  933. this.warn(
  934. `Playback: ${currentTime.toFixed(
  935. 3
  936. )} is located too far from the end of live sliding playlist: ${end}, reset currentTime to : ${liveSyncPosition.toFixed(
  937. 3
  938. )}`
  939. );
  940. if (!this.loadedmetadata) {
  941. this.nextLoadPosition = liveSyncPosition;
  942. }
  943. media.currentTime = liveSyncPosition;
  944. return liveSyncPosition;
  945. }
  946. }
  947. return null;
  948. }
  949.  
  950. protected alignPlaylists(
  951. details: LevelDetails,
  952. previousDetails?: LevelDetails
  953. ): number {
  954. const { levels, levelLastLoaded } = this;
  955. const lastLevel: Level | null =
  956. levelLastLoaded !== null ? levels![levelLastLoaded] : null;
  957.  
  958. // FIXME: If not for `shouldAlignOnDiscontinuities` requiring fragPrevious.cc,
  959. // this could all go in LevelHelper.mergeDetails
  960. let sliding = 0;
  961. if (previousDetails && details.fragments.length > 0) {
  962. sliding = details.fragments[0].start;
  963. if (details.alignedSliding && Number.isFinite(sliding)) {
  964. this.log(`Live playlist sliding:${sliding.toFixed(3)}`);
  965. } else if (!sliding) {
  966. this.warn(
  967. `[${this.constructor.name}] Live playlist - outdated PTS, unknown sliding`
  968. );
  969. alignStream(this.fragPrevious, lastLevel, details);
  970. }
  971. } else {
  972. this.log('Live playlist - first load, unknown sliding');
  973. alignStream(this.fragPrevious, lastLevel, details);
  974. }
  975.  
  976. return sliding;
  977. }
  978.  
  979. protected waitForCdnTuneIn(details: LevelDetails) {
  980. // Wait for Low-Latency CDN Tune-in to get an updated playlist
  981. const advancePartLimit = 3;
  982. return (
  983. details.live &&
  984. details.canBlockReload &&
  985. details.tuneInGoal >
  986. Math.max(details.partHoldBack, details.partTarget * advancePartLimit)
  987. );
  988. }
  989.  
  990. protected setStartPosition(details: LevelDetails, sliding: number) {
  991. // compute start position if set to -1. use it straight away if value is defined
  992. if (this.startPosition === -1 || this.lastCurrentTime === -1) {
  993. // first, check if start time offset has been set in playlist, if yes, use this value
  994. let startTimeOffset = details.startTimeOffset!;
  995. if (Number.isFinite(startTimeOffset)) {
  996. if (startTimeOffset < 0) {
  997. this.log(
  998. `Negative start time offset ${startTimeOffset}, count from end of last fragment`
  999. );
  1000. startTimeOffset = sliding + details.totalduration + startTimeOffset;
  1001. }
  1002. this.log(
  1003. `Start time offset found in playlist, adjust startPosition to ${startTimeOffset}`
  1004. );
  1005. this.startPosition = startTimeOffset;
  1006. } else {
  1007. if (details.live) {
  1008. this.startPosition = this.hls.liveSyncPosition || sliding;
  1009. this.log(`Configure startPosition to ${this.startPosition}`);
  1010. } else {
  1011. this.startPosition = 0;
  1012. }
  1013. }
  1014. this.lastCurrentTime = this.startPosition;
  1015. }
  1016. this.nextLoadPosition = this.startPosition;
  1017. }
  1018.  
  1019. protected getLoadPosition(): number {
  1020. const { media } = this;
  1021. // if we have not yet loaded any fragment, start loading from start position
  1022. let pos = 0;
  1023. if (this.loadedmetadata) {
  1024. pos = media.currentTime;
  1025. } else if (this.nextLoadPosition) {
  1026. pos = this.nextLoadPosition;
  1027. }
  1028.  
  1029. return pos;
  1030. }
  1031.  
  1032. private handleFragLoadAborted(frag: Fragment, part: Part | undefined) {
  1033. if (this.transmuxer && frag.sn !== 'initSegment') {
  1034. this.log(
  1035. `Fragment ${frag.sn} of level ${frag.level} was aborted, flushing transmuxer`
  1036. );
  1037. this.transmuxer.flush(
  1038. new ChunkMetadata(
  1039. frag.level,
  1040. frag.sn,
  1041. frag.stats.chunkCount + 1,
  1042. 0,
  1043. part ? part.index : -1,
  1044. true
  1045. )
  1046. );
  1047. }
  1048. }
  1049.  
  1050. private updateLevelTiming(frag: Fragment, level: Level, partial: boolean) {
  1051. const details = level.details as LevelDetails;
  1052. console.assert(!!details, 'level.details must be defined');
  1053. Object.keys(frag.elementaryStreams).forEach((type) => {
  1054. const info = frag.elementaryStreams[type];
  1055. if (info) {
  1056. const parsedDuration = info.endPTS - info.startPTS;
  1057. if (parsedDuration <= 0) {
  1058. // Destroy the transmuxer after it's next time offset failed to advance because duration was <= 0.
  1059. // The new transmuxer will be configured with a time offset matching the next fragment start, preventing the timeline from shifting.
  1060. this.warn(
  1061. `Could not parse fragment ${frag.sn} ${type} duration reliably (${parsedDuration}) resetting transmuxer to fallback to playlist timing`
  1062. );
  1063. if (this.transmuxer) {
  1064. this.transmuxer.destroy();
  1065. this.transmuxer = null;
  1066. }
  1067. }
  1068. const drift = partial
  1069. ? 0
  1070. : LevelHelper.updateFragPTSDTS(
  1071. details,
  1072. frag,
  1073. info.startPTS,
  1074. info.endPTS,
  1075. info.startDTS,
  1076. info.endDTS
  1077. );
  1078. this.hls.trigger(Events.LEVEL_PTS_UPDATED, {
  1079. details,
  1080. level,
  1081. drift,
  1082. type,
  1083. frag,
  1084. start: info.startPTS,
  1085. end: info.endPTS,
  1086. });
  1087. }
  1088. });
  1089. }
  1090.  
  1091. set state(nextState) {
  1092. const previousState = this._state;
  1093. if (previousState !== nextState) {
  1094. this._state = nextState;
  1095. this.log(`${previousState}->${nextState}`);
  1096. }
  1097. }
  1098.  
  1099. get state() {
  1100. return this._state;
  1101. }
  1102. }