Home Reference Source

src/controller/buffer-controller.ts

  1. import { Events } from '../events';
  2. import { logger } from '../utils/logger';
  3. import { ErrorDetails, ErrorTypes } from '../errors';
  4. import { BufferHelper } from '../utils/buffer-helper';
  5. import { getMediaSource } from '../utils/mediasource-helper';
  6. import { ElementaryStreamTypes } from '../loader/fragment';
  7. import type { TrackSet } from '../types/track';
  8. import BufferOperationQueue from './buffer-operation-queue';
  9. import {
  10. BufferOperation,
  11. SourceBuffers,
  12. SourceBufferName,
  13. SourceBufferListeners,
  14. } from '../types/buffer';
  15. import type {
  16. LevelUpdatedData,
  17. BufferAppendingData,
  18. MediaAttachingData,
  19. ManifestParsedData,
  20. BufferCodecsData,
  21. BufferEOSData,
  22. BufferFlushingData,
  23. FragParsedData,
  24. FragChangedData,
  25. } from '../types/events';
  26. import type { ComponentAPI } from '../types/component-api';
  27. import type Hls from '../hls';
  28. import { LevelDetails } from '../loader/level-details';
  29.  
  30. const MediaSource = getMediaSource();
  31. const VIDEO_CODEC_PROFILE_REPACE = /([ha]vc.)(?:\.[^.,]+)+/;
  32.  
  33. export default class BufferController implements ComponentAPI {
  34. // The level details used to determine duration, target-duration and live
  35. private details: LevelDetails | null = null;
  36. // cache the self generated object url to detect hijack of video tag
  37. private _objectUrl: string | null = null;
  38. // A queue of buffer operations which require the SourceBuffer to not be updating upon execution
  39. private operationQueue!: BufferOperationQueue;
  40. // References to event listeners for each SourceBuffer, so that they can be referenced for event removal
  41. private listeners!: SourceBufferListeners;
  42.  
  43. private hls: Hls;
  44.  
  45. // The number of BUFFER_CODEC events received before any sourceBuffers are created
  46. public bufferCodecEventsExpected: number = 0;
  47.  
  48. // The total number of BUFFER_CODEC events received
  49. private _bufferCodecEventsTotal: number = 0;
  50.  
  51. // A reference to the attached media element
  52. public media: HTMLMediaElement | null = null;
  53.  
  54. // A reference to the active media source
  55. public mediaSource: MediaSource | null = null;
  56.  
  57. // counters
  58. public appendError: number = 0;
  59.  
  60. public tracks: TrackSet = {};
  61. public pendingTracks: TrackSet = {};
  62. public sourceBuffer!: SourceBuffers;
  63.  
  64. constructor(hls: Hls) {
  65. this.hls = hls;
  66. this._initSourceBuffer();
  67. this.registerListeners();
  68. }
  69.  
  70. public hasSourceTypes(): boolean {
  71. return (
  72. this.getSourceBufferTypes().length > 0 ||
  73. Object.keys(this.pendingTracks).length > 0
  74. );
  75. }
  76.  
  77. public destroy() {
  78. this.unregisterListeners();
  79. this.details = null;
  80. }
  81.  
  82. protected registerListeners() {
  83. const { hls } = this;
  84. hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
  85. hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
  86. hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
  87. hls.on(Events.BUFFER_RESET, this.onBufferReset, this);
  88. hls.on(Events.BUFFER_APPENDING, this.onBufferAppending, this);
  89. hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this);
  90. hls.on(Events.BUFFER_EOS, this.onBufferEos, this);
  91. hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
  92. hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
  93. hls.on(Events.FRAG_PARSED, this.onFragParsed, this);
  94. hls.on(Events.FRAG_CHANGED, this.onFragChanged, this);
  95. }
  96.  
  97. protected unregisterListeners() {
  98. const { hls } = this;
  99. hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
  100. hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
  101. hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
  102. hls.off(Events.BUFFER_RESET, this.onBufferReset, this);
  103. hls.off(Events.BUFFER_APPENDING, this.onBufferAppending, this);
  104. hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this);
  105. hls.off(Events.BUFFER_EOS, this.onBufferEos, this);
  106. hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
  107. hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
  108. hls.off(Events.FRAG_PARSED, this.onFragParsed, this);
  109. hls.off(Events.FRAG_CHANGED, this.onFragChanged, this);
  110. }
  111.  
  112. private _initSourceBuffer() {
  113. this.sourceBuffer = {};
  114. this.operationQueue = new BufferOperationQueue(this.sourceBuffer);
  115. this.listeners = {
  116. audio: [],
  117. video: [],
  118. audiovideo: [],
  119. };
  120. }
  121.  
  122. protected onManifestParsed(
  123. event: Events.MANIFEST_PARSED,
  124. data: ManifestParsedData
  125. ) {
  126. // in case of alt audio 2 BUFFER_CODECS events will be triggered, one per stream controller
  127. // sourcebuffers will be created all at once when the expected nb of tracks will be reached
  128. // in case alt audio is not used, only one BUFFER_CODEC event will be fired from main stream controller
  129. // it will contain the expected nb of source buffers, no need to compute it
  130. let codecEvents: number = 2;
  131. if ((data.audio && !data.video) || !data.altAudio) {
  132. codecEvents = 1;
  133. }
  134. this.bufferCodecEventsExpected = this._bufferCodecEventsTotal = codecEvents;
  135. this.details = null;
  136. logger.log(
  137. `${this.bufferCodecEventsExpected} bufferCodec event(s) expected`
  138. );
  139. }
  140.  
  141. protected onMediaAttaching(
  142. event: Events.MEDIA_ATTACHING,
  143. data: MediaAttachingData
  144. ) {
  145. const media = (this.media = data.media);
  146. if (media && MediaSource) {
  147. const ms = (this.mediaSource = new MediaSource());
  148. // MediaSource listeners are arrow functions with a lexical scope, and do not need to be bound
  149. ms.addEventListener('sourceopen', this._onMediaSourceOpen);
  150. ms.addEventListener('sourceended', this._onMediaSourceEnded);
  151. ms.addEventListener('sourceclose', this._onMediaSourceClose);
  152. // link video and media Source
  153. media.src = self.URL.createObjectURL(ms);
  154. // cache the locally generated object url
  155. this._objectUrl = media.src;
  156. }
  157. }
  158.  
  159. protected onMediaDetaching() {
  160. const { media, mediaSource, _objectUrl } = this;
  161. if (mediaSource) {
  162. logger.log('[buffer-controller]: media source detaching');
  163. if (mediaSource.readyState === 'open') {
  164. try {
  165. // endOfStream could trigger exception if any sourcebuffer is in updating state
  166. // we don't really care about checking sourcebuffer state here,
  167. // as we are anyway detaching the MediaSource
  168. // let's just avoid this exception to propagate
  169. mediaSource.endOfStream();
  170. } catch (err) {
  171. logger.warn(
  172. `[buffer-controller]: onMediaDetaching: ${err.message} while calling endOfStream`
  173. );
  174. }
  175. }
  176. // Clean up the SourceBuffers by invoking onBufferReset
  177. this.onBufferReset();
  178. mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen);
  179. mediaSource.removeEventListener('sourceended', this._onMediaSourceEnded);
  180. mediaSource.removeEventListener('sourceclose', this._onMediaSourceClose);
  181.  
  182. // Detach properly the MediaSource from the HTMLMediaElement as
  183. // suggested in https://github.com/w3c/media-source/issues/53.
  184. if (media) {
  185. if (_objectUrl) {
  186. self.URL.revokeObjectURL(_objectUrl);
  187. }
  188.  
  189. // clean up video tag src only if it's our own url. some external libraries might
  190. // hijack the video tag and change its 'src' without destroying the Hls instance first
  191. if (media.src === _objectUrl) {
  192. media.removeAttribute('src');
  193. media.load();
  194. } else {
  195. logger.warn(
  196. '[buffer-controller]: media.src was changed by a third party - skip cleanup'
  197. );
  198. }
  199. }
  200.  
  201. this.mediaSource = null;
  202. this.media = null;
  203. this._objectUrl = null;
  204. this.bufferCodecEventsExpected = this._bufferCodecEventsTotal;
  205. this.pendingTracks = {};
  206. this.tracks = {};
  207. }
  208.  
  209. this.hls.trigger(Events.MEDIA_DETACHED, undefined);
  210. }
  211.  
  212. protected onBufferReset() {
  213. this.getSourceBufferTypes().forEach((type) => {
  214. const sb = this.sourceBuffer[type];
  215. try {
  216. if (sb) {
  217. this.removeBufferListeners(type);
  218. if (this.mediaSource) {
  219. this.mediaSource.removeSourceBuffer(sb);
  220. }
  221. // Synchronously remove the SB from the map before the next call in order to prevent an async function from
  222. // accessing it
  223. this.sourceBuffer[type] = undefined;
  224. }
  225. } catch (err) {
  226. logger.warn(
  227. `[buffer-controller]: Failed to reset the ${type} buffer`,
  228. err
  229. );
  230. }
  231. });
  232. this._initSourceBuffer();
  233. }
  234.  
  235. protected onBufferCodecs(
  236. event: Events.BUFFER_CODECS,
  237. data: BufferCodecsData
  238. ) {
  239. const sourceBufferCount = this.getSourceBufferTypes().length;
  240.  
  241. Object.keys(data).forEach((trackName) => {
  242. if (sourceBufferCount) {
  243. // check if SourceBuffer codec needs to change
  244. const track = this.tracks[trackName];
  245. if (track && typeof track.buffer.changeType === 'function') {
  246. const { id, codec, levelCodec, container, metadata } =
  247. data[trackName];
  248. const currentCodec = (track.levelCodec || track.codec).replace(
  249. VIDEO_CODEC_PROFILE_REPACE,
  250. '$1'
  251. );
  252. const nextCodec = (levelCodec || codec).replace(
  253. VIDEO_CODEC_PROFILE_REPACE,
  254. '$1'
  255. );
  256. if (currentCodec !== nextCodec) {
  257. const mimeType = `${container};codecs=${levelCodec || codec}`;
  258. this.appendChangeType(trackName, mimeType);
  259. logger.log(
  260. `[buffer-controller]: switching codec ${currentCodec} to ${nextCodec}`
  261. );
  262. this.tracks[trackName] = {
  263. buffer: track.buffer,
  264. codec,
  265. container,
  266. levelCodec,
  267. metadata,
  268. id,
  269. };
  270. }
  271. }
  272. } else {
  273. // if source buffer(s) not created yet, appended buffer tracks in this.pendingTracks
  274. this.pendingTracks[trackName] = data[trackName];
  275. }
  276. });
  277.  
  278. // if sourcebuffers already created, do nothing ...
  279. if (sourceBufferCount) {
  280. return;
  281. }
  282.  
  283. this.bufferCodecEventsExpected = Math.max(
  284. this.bufferCodecEventsExpected - 1,
  285. 0
  286. );
  287. if (this.mediaSource && this.mediaSource.readyState === 'open') {
  288. this.checkPendingTracks();
  289. }
  290. }
  291.  
  292. protected appendChangeType(type, mimeType) {
  293. const { operationQueue } = this;
  294. const operation: BufferOperation = {
  295. execute: () => {
  296. const sb = this.sourceBuffer[type];
  297. if (sb) {
  298. logger.log(
  299. `[buffer-controller]: changing ${type} sourceBuffer type to ${mimeType}`
  300. );
  301. sb.changeType(mimeType);
  302. }
  303. operationQueue.shiftAndExecuteNext(type);
  304. },
  305. onStart: () => {},
  306. onComplete: () => {},
  307. onError: (e) => {
  308. logger.warn(
  309. `[buffer-controller]: Failed to change ${type} SourceBuffer type`,
  310. e
  311. );
  312. },
  313. };
  314.  
  315. operationQueue.append(operation, type);
  316. }
  317.  
  318. protected onBufferAppending(
  319. event: Events.BUFFER_APPENDING,
  320. eventData: BufferAppendingData
  321. ) {
  322. const { hls, operationQueue, tracks } = this;
  323. const { data, type, frag, part, chunkMeta } = eventData;
  324. const chunkStats = chunkMeta.buffering[type];
  325.  
  326. const bufferAppendingStart = self.performance.now();
  327. chunkStats.start = bufferAppendingStart;
  328. const fragBuffering = frag.stats.buffering;
  329. const partBuffering = part ? part.stats.buffering : null;
  330. if (fragBuffering.start === 0) {
  331. fragBuffering.start = bufferAppendingStart;
  332. }
  333. if (partBuffering && partBuffering.start === 0) {
  334. partBuffering.start = bufferAppendingStart;
  335. }
  336.  
  337. // TODO: Only update timestampOffset when audio/mpeg fragment or part is not contiguous with previously appended
  338. // Adjusting `SourceBuffer.timestampOffset` (desired point in the timeline where the next frames should be appended)
  339. // in Chrome browser when we detect MPEG audio container and time delta between level PTS and `SourceBuffer.timestampOffset`
  340. // is greater than 100ms (this is enough to handle seek for VOD or level change for LIVE videos).
  341. // More info here: https://github.com/video-dev/hls.js/issues/332#issuecomment-257986486
  342. const audioTrack = tracks.audio;
  343. const checkTimestampOffset =
  344. type === 'audio' &&
  345. chunkMeta.id === 1 &&
  346. audioTrack?.container === 'audio/mpeg';
  347.  
  348. const operation: BufferOperation = {
  349. execute: () => {
  350. chunkStats.executeStart = self.performance.now();
  351. if (checkTimestampOffset) {
  352. const sb = this.sourceBuffer[type];
  353. if (sb) {
  354. const delta = frag.start - sb.timestampOffset;
  355. if (Math.abs(delta) >= 0.1) {
  356. logger.log(
  357. `[buffer-controller]: Updating audio SourceBuffer timestampOffset to ${frag.start} (delta: ${delta}) sn: ${frag.sn})`
  358. );
  359. sb.timestampOffset = frag.start;
  360. }
  361. }
  362. }
  363. this.appendExecutor(data, type);
  364. },
  365. onStart: () => {
  366. // logger.debug(`[buffer-controller]: ${type} SourceBuffer updatestart`);
  367. },
  368. onComplete: () => {
  369. // logger.debug(`[buffer-controller]: ${type} SourceBuffer updateend`);
  370. const end = self.performance.now();
  371. chunkStats.executeEnd = chunkStats.end = end;
  372. if (fragBuffering.first === 0) {
  373. fragBuffering.first = end;
  374. }
  375. if (partBuffering && partBuffering.first === 0) {
  376. partBuffering.first = end;
  377. }
  378.  
  379. const { sourceBuffer } = this;
  380. const timeRanges = {};
  381. for (const type in sourceBuffer) {
  382. timeRanges[type] = BufferHelper.getBuffered(sourceBuffer[type]);
  383. }
  384. this.appendError = 0;
  385. this.hls.trigger(Events.BUFFER_APPENDED, {
  386. type,
  387. frag,
  388. part,
  389. chunkMeta,
  390. parent: frag.type,
  391. timeRanges,
  392. });
  393. },
  394. onError: (err) => {
  395. // in case any error occured while appending, put back segment in segments table
  396. logger.error(
  397. `[buffer-controller]: Error encountered while trying to append to the ${type} SourceBuffer`,
  398. err
  399. );
  400. const event = {
  401. type: ErrorTypes.MEDIA_ERROR,
  402. parent: frag.type,
  403. details: ErrorDetails.BUFFER_APPEND_ERROR,
  404. err,
  405. fatal: false,
  406. };
  407.  
  408. if (err.code === DOMException.QUOTA_EXCEEDED_ERR) {
  409. // QuotaExceededError: http://www.w3.org/TR/html5/infrastructure.html#quotaexceedederror
  410. // let's stop appending any segments, and report BUFFER_FULL_ERROR error
  411. event.details = ErrorDetails.BUFFER_FULL_ERROR;
  412. } else {
  413. this.appendError++;
  414. event.details = ErrorDetails.BUFFER_APPEND_ERROR;
  415. /* with UHD content, we could get loop of quota exceeded error until
  416. browser is able to evict some data from sourcebuffer. Retrying can help recover.
  417. */
  418. if (this.appendError > hls.config.appendErrorMaxRetry) {
  419. logger.error(
  420. `[buffer-controller]: Failed ${hls.config.appendErrorMaxRetry} times to append segment in sourceBuffer`
  421. );
  422. event.fatal = true;
  423. hls.stopLoad();
  424. }
  425. }
  426. hls.trigger(Events.ERROR, event);
  427. },
  428. };
  429. operationQueue.append(operation, type);
  430. }
  431.  
  432. protected onBufferFlushing(
  433. event: Events.BUFFER_FLUSHING,
  434. data: BufferFlushingData
  435. ) {
  436. const { operationQueue } = this;
  437. const flushOperation = (type: SourceBufferName): BufferOperation => ({
  438. execute: this.removeExecutor.bind(
  439. this,
  440. type,
  441. data.startOffset,
  442. data.endOffset
  443. ),
  444. onStart: () => {
  445. // logger.debug(`[buffer-controller]: Started flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`);
  446. },
  447. onComplete: () => {
  448. // logger.debug(`[buffer-controller]: Finished flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`);
  449. this.hls.trigger(Events.BUFFER_FLUSHED, { type });
  450. },
  451. onError: (e) => {
  452. logger.warn(
  453. `[buffer-controller]: Failed to remove from ${type} SourceBuffer`,
  454. e
  455. );
  456. },
  457. });
  458.  
  459. if (data.type) {
  460. operationQueue.append(flushOperation(data.type), data.type);
  461. } else {
  462. this.getSourceBufferTypes().forEach((type: SourceBufferName) => {
  463. operationQueue.append(flushOperation(type), type);
  464. });
  465. }
  466. }
  467.  
  468. protected onFragParsed(event: Events.FRAG_PARSED, data: FragParsedData) {
  469. const { frag, part } = data;
  470. const buffersAppendedTo: Array<SourceBufferName> = [];
  471. const elementaryStreams = part
  472. ? part.elementaryStreams
  473. : frag.elementaryStreams;
  474. if (elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO]) {
  475. buffersAppendedTo.push('audiovideo');
  476. } else {
  477. if (elementaryStreams[ElementaryStreamTypes.AUDIO]) {
  478. buffersAppendedTo.push('audio');
  479. }
  480. if (elementaryStreams[ElementaryStreamTypes.VIDEO]) {
  481. buffersAppendedTo.push('video');
  482. }
  483. }
  484.  
  485. const onUnblocked = () => {
  486. const now = self.performance.now();
  487. frag.stats.buffering.end = now;
  488. if (part) {
  489. part.stats.buffering.end = now;
  490. }
  491. const stats = part ? part.stats : frag.stats;
  492. this.hls.trigger(Events.FRAG_BUFFERED, {
  493. frag,
  494. part,
  495. stats,
  496. id: frag.type,
  497. });
  498. };
  499.  
  500. if (buffersAppendedTo.length === 0) {
  501. logger.warn(
  502. `Fragments must have at least one ElementaryStreamType set. type: ${frag.type} level: ${frag.level} sn: ${frag.sn}`
  503. );
  504. }
  505.  
  506. this.blockBuffers(onUnblocked, buffersAppendedTo);
  507. }
  508.  
  509. private onFragChanged(event: Events.FRAG_CHANGED, data: FragChangedData) {
  510. this.flushBackBuffer();
  511. }
  512.  
  513. // on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos()
  514. // an undefined data.type will mark all buffers as EOS.
  515. protected onBufferEos(event: Events.BUFFER_EOS, data: BufferEOSData) {
  516. const ended = this.getSourceBufferTypes().reduce((acc, type) => {
  517. const sb = this.sourceBuffer[type];
  518. if (!data.type || data.type === type) {
  519. if (sb && !sb.ended) {
  520. sb.ended = true;
  521. logger.log(`[buffer-controller]: ${type} sourceBuffer now EOS`);
  522. }
  523. }
  524. return acc && !!(!sb || sb.ended);
  525. }, true);
  526.  
  527. if (ended) {
  528. this.blockBuffers(() => {
  529. const { mediaSource } = this;
  530. if (!mediaSource || mediaSource.readyState !== 'open') {
  531. return;
  532. }
  533. // Allow this to throw and be caught by the enqueueing function
  534. mediaSource.endOfStream();
  535. });
  536. }
  537. }
  538.  
  539. protected onLevelUpdated(
  540. event: Events.LEVEL_UPDATED,
  541. { details }: LevelUpdatedData
  542. ) {
  543. if (!details.fragments.length) {
  544. return;
  545. }
  546. this.details = details;
  547.  
  548. if (this.getSourceBufferTypes().length) {
  549. this.blockBuffers(this.updateMediaElementDuration.bind(this));
  550. } else {
  551. this.updateMediaElementDuration();
  552. }
  553. }
  554.  
  555. flushBackBuffer() {
  556. const { hls, details, media, sourceBuffer } = this;
  557. if (!media || details === null) {
  558. return;
  559. }
  560.  
  561. const sourceBufferTypes = this.getSourceBufferTypes();
  562. if (!sourceBufferTypes.length) {
  563. return;
  564. }
  565.  
  566. // Support for deprecated liveBackBufferLength
  567. const backBufferLength =
  568. details.live && hls.config.liveBackBufferLength !== null
  569. ? hls.config.liveBackBufferLength
  570. : hls.config.backBufferLength;
  571.  
  572. if (!Number.isFinite(backBufferLength) || backBufferLength < 0) {
  573. return;
  574. }
  575.  
  576. const currentTime = media.currentTime;
  577. const targetDuration = details.levelTargetDuration;
  578. const maxBackBufferLength = Math.max(backBufferLength, targetDuration);
  579. const targetBackBufferPosition =
  580. Math.floor(currentTime / targetDuration) * targetDuration -
  581. maxBackBufferLength;
  582. sourceBufferTypes.forEach((type: SourceBufferName) => {
  583. const sb = sourceBuffer[type];
  584. if (sb) {
  585. const buffered = BufferHelper.getBuffered(sb);
  586. // when target buffer start exceeds actual buffer start
  587. if (
  588. buffered.length > 0 &&
  589. targetBackBufferPosition > buffered.start(0)
  590. ) {
  591. hls.trigger(Events.BACK_BUFFER_REACHED, {
  592. bufferEnd: targetBackBufferPosition,
  593. });
  594.  
  595. // Support for deprecated event:
  596. if (details.live) {
  597. hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, {
  598. bufferEnd: targetBackBufferPosition,
  599. });
  600. }
  601.  
  602. hls.trigger(Events.BUFFER_FLUSHING, {
  603. startOffset: 0,
  604. endOffset: targetBackBufferPosition,
  605. type,
  606. });
  607. }
  608. }
  609. });
  610. }
  611.  
  612. /**
  613. * Update Media Source duration to current level duration or override to Infinity if configuration parameter
  614. * 'liveDurationInfinity` is set to `true`
  615. * More details: https://github.com/video-dev/hls.js/issues/355
  616. */
  617. private updateMediaElementDuration() {
  618. if (
  619. !this.details ||
  620. !this.media ||
  621. !this.mediaSource ||
  622. this.mediaSource.readyState !== 'open'
  623. ) {
  624. return;
  625. }
  626. const { details, hls, media, mediaSource } = this;
  627. const levelDuration = details.fragments[0].start + details.totalduration;
  628. const mediaDuration = media.duration;
  629. const msDuration = Number.isFinite(mediaSource.duration)
  630. ? mediaSource.duration
  631. : 0;
  632.  
  633. if (details.live && hls.config.liveDurationInfinity) {
  634. // Override duration to Infinity
  635. logger.log(
  636. '[buffer-controller]: Media Source duration is set to Infinity'
  637. );
  638. mediaSource.duration = Infinity;
  639. this.updateSeekableRange(details);
  640. } else if (
  641. (levelDuration > msDuration && levelDuration > mediaDuration) ||
  642. !Number.isFinite(mediaDuration)
  643. ) {
  644. // levelDuration was the last value we set.
  645. // not using mediaSource.duration as the browser may tweak this value
  646. // only update Media Source duration if its value increase, this is to avoid
  647. // flushing already buffered portion when switching between quality level
  648. logger.log(
  649. `[buffer-controller]: Updating Media Source duration to ${levelDuration.toFixed(
  650. 3
  651. )}`
  652. );
  653. mediaSource.duration = levelDuration;
  654. }
  655. }
  656.  
  657. updateSeekableRange(levelDetails) {
  658. const mediaSource = this.mediaSource;
  659. const fragments = levelDetails.fragments;
  660. const len = fragments.length;
  661. if (len && levelDetails.live && mediaSource?.setLiveSeekableRange) {
  662. const start = Math.max(0, fragments[0].start);
  663. const end = Math.max(start, start + levelDetails.totalduration);
  664. mediaSource.setLiveSeekableRange(start, end);
  665. }
  666. }
  667.  
  668. protected checkPendingTracks() {
  669. const { bufferCodecEventsExpected, operationQueue, pendingTracks } = this;
  670.  
  671. // Check if we've received all of the expected bufferCodec events. When none remain, create all the sourceBuffers at once.
  672. // This is important because the MSE spec allows implementations to throw QuotaExceededErrors if creating new sourceBuffers after
  673. // data has been appended to existing ones.
  674. // 2 tracks is the max (one for audio, one for video). If we've reach this max go ahead and create the buffers.
  675. const pendingTracksCount = Object.keys(pendingTracks).length;
  676. if (
  677. (pendingTracksCount && !bufferCodecEventsExpected) ||
  678. pendingTracksCount === 2
  679. ) {
  680. // ok, let's create them now !
  681. this.createSourceBuffers(pendingTracks);
  682. this.pendingTracks = {};
  683. // append any pending segments now !
  684. const buffers = this.getSourceBufferTypes();
  685. if (buffers.length === 0) {
  686. this.hls.trigger(Events.ERROR, {
  687. type: ErrorTypes.MEDIA_ERROR,
  688. details: ErrorDetails.BUFFER_INCOMPATIBLE_CODECS_ERROR,
  689. fatal: true,
  690. reason: 'could not create source buffer for media codec(s)',
  691. });
  692. return;
  693. }
  694. buffers.forEach((type: SourceBufferName) => {
  695. operationQueue.executeNext(type);
  696. });
  697. }
  698. }
  699.  
  700. protected createSourceBuffers(tracks: TrackSet) {
  701. const { sourceBuffer, mediaSource } = this;
  702. if (!mediaSource) {
  703. throw Error('createSourceBuffers called when mediaSource was null');
  704. }
  705. let tracksCreated = 0;
  706. for (const trackName in tracks) {
  707. if (!sourceBuffer[trackName]) {
  708. const track = tracks[trackName as keyof TrackSet];
  709. if (!track) {
  710. throw Error(
  711. `source buffer exists for track ${trackName}, however track does not`
  712. );
  713. }
  714. // use levelCodec as first priority
  715. const codec = track.levelCodec || track.codec;
  716. const mimeType = `${track.container};codecs=${codec}`;
  717. logger.log(`[buffer-controller]: creating sourceBuffer(${mimeType})`);
  718. try {
  719. const sb = (sourceBuffer[trackName] =
  720. mediaSource.addSourceBuffer(mimeType));
  721. const sbName = trackName as SourceBufferName;
  722. this.addBufferListener(sbName, 'updatestart', this._onSBUpdateStart);
  723. this.addBufferListener(sbName, 'updateend', this._onSBUpdateEnd);
  724. this.addBufferListener(sbName, 'error', this._onSBUpdateError);
  725. this.tracks[trackName] = {
  726. buffer: sb,
  727. codec: codec,
  728. container: track.container,
  729. levelCodec: track.levelCodec,
  730. metadata: track.metadata,
  731. id: track.id,
  732. };
  733. tracksCreated++;
  734. } catch (err) {
  735. logger.error(
  736. `[buffer-controller]: error while trying to add sourceBuffer: ${err.message}`
  737. );
  738. this.hls.trigger(Events.ERROR, {
  739. type: ErrorTypes.MEDIA_ERROR,
  740. details: ErrorDetails.BUFFER_ADD_CODEC_ERROR,
  741. fatal: false,
  742. error: err,
  743. mimeType: mimeType,
  744. });
  745. }
  746. }
  747. }
  748. if (tracksCreated) {
  749. this.hls.trigger(Events.BUFFER_CREATED, { tracks: this.tracks });
  750. }
  751. }
  752.  
  753. // Keep as arrow functions so that we can directly reference these functions directly as event listeners
  754. private _onMediaSourceOpen = () => {
  755. const { hls, media, mediaSource } = this;
  756. logger.log('[buffer-controller]: Media source opened');
  757. if (media) {
  758. this.updateMediaElementDuration();
  759. hls.trigger(Events.MEDIA_ATTACHED, { media });
  760. }
  761.  
  762. if (mediaSource) {
  763. // once received, don't listen anymore to sourceopen event
  764. mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen);
  765. }
  766. this.checkPendingTracks();
  767. };
  768.  
  769. private _onMediaSourceClose = () => {
  770. logger.log('[buffer-controller]: Media source closed');
  771. };
  772.  
  773. private _onMediaSourceEnded = () => {
  774. logger.log('[buffer-controller]: Media source ended');
  775. };
  776.  
  777. private _onSBUpdateStart(type: SourceBufferName) {
  778. const { operationQueue } = this;
  779. const operation = operationQueue.current(type);
  780. operation.onStart();
  781. }
  782.  
  783. private _onSBUpdateEnd(type: SourceBufferName) {
  784. const { operationQueue } = this;
  785. const operation = operationQueue.current(type);
  786. operation.onComplete();
  787. operationQueue.shiftAndExecuteNext(type);
  788. }
  789.  
  790. private _onSBUpdateError(type: SourceBufferName, event: Event) {
  791. logger.error(`[buffer-controller]: ${type} SourceBuffer error`, event);
  792. // according to http://www.w3.org/TR/media-source/#sourcebuffer-append-error
  793. // SourceBuffer errors are not necessarily fatal; if so, the HTMLMediaElement will fire an error event
  794. this.hls.trigger(Events.ERROR, {
  795. type: ErrorTypes.MEDIA_ERROR,
  796. details: ErrorDetails.BUFFER_APPENDING_ERROR,
  797. fatal: false,
  798. });
  799. // updateend is always fired after error, so we'll allow that to shift the current operation off of the queue
  800. const operation = this.operationQueue.current(type);
  801. if (operation) {
  802. operation.onError(event);
  803. }
  804. }
  805.  
  806. // This method must result in an updateend event; if remove is not called, _onSBUpdateEnd must be called manually
  807. private removeExecutor(
  808. type: SourceBufferName,
  809. startOffset: number,
  810. endOffset: number
  811. ) {
  812. const { media, mediaSource, operationQueue, sourceBuffer } = this;
  813. const sb = sourceBuffer[type];
  814. if (!media || !mediaSource || !sb) {
  815. logger.warn(
  816. `[buffer-controller]: Attempting to remove from the ${type} SourceBuffer, but it does not exist`
  817. );
  818. operationQueue.shiftAndExecuteNext(type);
  819. return;
  820. }
  821. const mediaDuration = Number.isFinite(media.duration)
  822. ? media.duration
  823. : Infinity;
  824. const msDuration = Number.isFinite(mediaSource.duration)
  825. ? mediaSource.duration
  826. : Infinity;
  827. const removeStart = Math.max(0, startOffset);
  828. const removeEnd = Math.min(endOffset, mediaDuration, msDuration);
  829. if (removeEnd > removeStart) {
  830. logger.log(
  831. `[buffer-controller]: Removing [${removeStart},${removeEnd}] from the ${type} SourceBuffer`
  832. );
  833. console.assert(!sb.updating, `${type} sourceBuffer must not be updating`);
  834. sb.remove(removeStart, removeEnd);
  835. } else {
  836. // Cycle the queue
  837. operationQueue.shiftAndExecuteNext(type);
  838. }
  839. }
  840.  
  841. // This method must result in an updateend event; if append is not called, _onSBUpdateEnd must be called manually
  842. private appendExecutor(data: Uint8Array, type: SourceBufferName) {
  843. const { operationQueue, sourceBuffer } = this;
  844. const sb = sourceBuffer[type];
  845. if (!sb) {
  846. logger.warn(
  847. `[buffer-controller]: Attempting to append to the ${type} SourceBuffer, but it does not exist`
  848. );
  849. operationQueue.shiftAndExecuteNext(type);
  850. return;
  851. }
  852.  
  853. sb.ended = false;
  854. console.assert(!sb.updating, `${type} sourceBuffer must not be updating`);
  855. sb.appendBuffer(data);
  856. }
  857.  
  858. // Enqueues an operation to each SourceBuffer queue which, upon execution, resolves a promise. When all promises
  859. // resolve, the onUnblocked function is executed. Functions calling this method do not need to unblock the queue
  860. // upon completion, since we already do it here
  861. private blockBuffers(
  862. onUnblocked: () => void,
  863. buffers: Array<SourceBufferName> = this.getSourceBufferTypes()
  864. ) {
  865. if (!buffers.length) {
  866. logger.log(
  867. '[buffer-controller]: Blocking operation requested, but no SourceBuffers exist'
  868. );
  869. Promise.resolve().then(onUnblocked);
  870. return;
  871. }
  872. const { operationQueue } = this;
  873.  
  874. // logger.debug(`[buffer-controller]: Blocking ${buffers} SourceBuffer`);
  875. const blockingOperations = buffers.map((type) =>
  876. operationQueue.appendBlocker(type as SourceBufferName)
  877. );
  878. Promise.all(blockingOperations).then(() => {
  879. // logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`);
  880. onUnblocked();
  881. buffers.forEach((type) => {
  882. const sb = this.sourceBuffer[type];
  883. // Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to
  884. // true when changing the MediaSource duration (https://bugs.chromium.org/p/chromium/issues/detail?id=959359&can=2&q=mediasource%20duration)
  885. // While this is a workaround, it's probably useful to have around
  886. if (!sb || !sb.updating) {
  887. operationQueue.shiftAndExecuteNext(type);
  888. }
  889. });
  890. });
  891. }
  892.  
  893. private getSourceBufferTypes(): Array<SourceBufferName> {
  894. return Object.keys(this.sourceBuffer) as Array<SourceBufferName>;
  895. }
  896.  
  897. private addBufferListener(
  898. type: SourceBufferName,
  899. event: string,
  900. fn: Function
  901. ) {
  902. const buffer = this.sourceBuffer[type];
  903. if (!buffer) {
  904. return;
  905. }
  906. const listener = fn.bind(this, type);
  907. this.listeners[type].push({ event, listener });
  908. buffer.addEventListener(event, listener);
  909. }
  910.  
  911. private removeBufferListeners(type: SourceBufferName) {
  912. const buffer = this.sourceBuffer[type];
  913. if (!buffer) {
  914. return;
  915. }
  916. this.listeners[type].forEach((l) => {
  917. buffer.removeEventListener(l.event, l.listener);
  918. });
  919. }
  920. }