Home Reference Source

src/controller/buffer-controller.ts

  1. /*
  2. * Buffer Controller
  3. */
  4.  
  5. import Events from '../events';
  6. import EventHandler from '../event-handler';
  7. import { logger } from '../utils/logger';
  8. import { ErrorTypes, ErrorDetails } from '../errors';
  9. import { getMediaSource } from '../utils/mediasource-helper';
  10.  
  11. import { TrackSet } from '../types/track';
  12. import { Segment } from '../types/segment';
  13. import { BufferControllerConfig } from '../config';
  14.  
  15. // Add extension properties to SourceBuffers from the DOM API.
  16. type ExtendedSourceBuffer = SourceBuffer & {
  17. ended?: boolean
  18. };
  19.  
  20. type SourceBufferName = 'video' | 'audio';
  21. type SourceBuffers = Partial<Record<SourceBufferName, ExtendedSourceBuffer>>;
  22.  
  23. interface SourceBufferFlushRange {
  24. start: number;
  25. end: number;
  26. type: SourceBufferName
  27. }
  28.  
  29. const MediaSource = getMediaSource();
  30.  
  31. class BufferController extends EventHandler {
  32. // the value that we have set mediasource.duration to
  33. // (the actual duration may be tweaked slighly by the browser)
  34. private _msDuration: number | null = null;
  35. // the value that we want to set mediaSource.duration to
  36. private _levelDuration: number | null = null;
  37. // the target duration of the current media playlist
  38. private _levelTargetDuration: number = 10;
  39. // current stream state: true - for live broadcast, false - for VoD content
  40. private _live: boolean | null = null;
  41. // cache the self generated object url to detect hijack of video tag
  42. private _objectUrl: string | null = null;
  43.  
  44. // signals that the sourceBuffers need to be flushed
  45. private _needsFlush: boolean = false;
  46.  
  47. // signals that mediaSource should have endOfStream called
  48. private _needsEos: boolean = false;
  49.  
  50. private config: BufferControllerConfig;
  51.  
  52. // this is optional because this property is removed from the class sometimes
  53. public audioTimestampOffset?: number;
  54.  
  55. // The number of BUFFER_CODEC events received before any sourceBuffers are created
  56. public bufferCodecEventsExpected: number = 0;
  57.  
  58. // The total number of BUFFER_CODEC events received
  59. private _bufferCodecEventsTotal: number = 0;
  60.  
  61. // A reference to the attached media element
  62. public media: HTMLMediaElement | null = null;
  63.  
  64. // A reference to the active media source
  65. public mediaSource: MediaSource | null = null;
  66.  
  67. // List of pending segments to be appended to source buffer
  68. public segments: Segment[] = [];
  69.  
  70. public parent?: string;
  71.  
  72. // A guard to see if we are currently appending to the source buffer
  73. public appending: boolean = false;
  74.  
  75. // counters
  76. public appended: number = 0;
  77. public appendError: number = 0;
  78. public flushBufferCounter: number = 0;
  79.  
  80. public tracks: TrackSet = {};
  81. public pendingTracks: TrackSet = {};
  82. public sourceBuffer: SourceBuffers = {};
  83. public flushRange: SourceBufferFlushRange[] = [];
  84.  
  85. constructor (hls: any) {
  86. super(hls,
  87. Events.MEDIA_ATTACHING,
  88. Events.MEDIA_DETACHING,
  89. Events.MANIFEST_PARSED,
  90. Events.BUFFER_RESET,
  91. Events.BUFFER_APPENDING,
  92. Events.BUFFER_CODECS,
  93. Events.BUFFER_EOS,
  94. Events.BUFFER_FLUSHING,
  95. Events.LEVEL_PTS_UPDATED,
  96. Events.LEVEL_UPDATED);
  97.  
  98. this.config = hls.config;
  99. }
  100.  
  101. destroy () {
  102. EventHandler.prototype.destroy.call(this);
  103. }
  104.  
  105. onLevelPtsUpdated (data: { details, type: SourceBufferName, start: number }) {
  106. let type = data.type;
  107. let audioTrack = this.tracks.audio;
  108.  
  109. // Adjusting `SourceBuffer.timestampOffset` (desired point in the timeline where the next frames should be appended)
  110. // in Chrome browser when we detect MPEG audio container and time delta between level PTS and `SourceBuffer.timestampOffset`
  111. // is greater than 100ms (this is enough to handle seek for VOD or level change for LIVE videos). At the time of change we issue
  112. // `SourceBuffer.abort()` and adjusting `SourceBuffer.timestampOffset` if `SourceBuffer.updating` is false or awaiting `updateend`
  113. // event if SB is in updating state.
  114. // More info here: https://github.com/video-dev/hls.js/issues/332#issuecomment-257986486
  115.  
  116. if (type === 'audio' && audioTrack && audioTrack.container === 'audio/mpeg') { // Chrome audio mp3 track
  117. let audioBuffer = this.sourceBuffer.audio;
  118. if (!audioBuffer) {
  119. throw Error('Level PTS Updated and source buffer for audio uninitalized');
  120. }
  121.  
  122. let delta = Math.abs(audioBuffer.timestampOffset - data.start);
  123.  
  124. // adjust timestamp offset if time delta is greater than 100ms
  125. if (delta > 0.1) {
  126. let updating = audioBuffer.updating;
  127.  
  128. try {
  129. audioBuffer.abort();
  130. } catch (err) {
  131. logger.warn('can not abort audio buffer: ' + err);
  132. }
  133.  
  134. if (!updating) {
  135. logger.warn('change mpeg audio timestamp offset from ' + audioBuffer.timestampOffset + ' to ' + data.start);
  136. audioBuffer.timestampOffset = data.start;
  137. } else {
  138. this.audioTimestampOffset = data.start;
  139. }
  140. }
  141. }
  142.  
  143. if (this.config.liveDurationInfinity) {
  144. this.updateSeekableRange(data.details);
  145. }
  146. }
  147.  
  148. onManifestParsed (data: { altAudio: boolean, audio: boolean, video: boolean }) {
  149. // in case of alt audio (where all tracks have urls) 2 BUFFER_CODECS events will be triggered, one per stream controller
  150. // sourcebuffers will be created all at once when the expected nb of tracks will be reached
  151. // in case alt audio is not used, only one BUFFER_CODEC event will be fired from main stream controller
  152. // it will contain the expected nb of source buffers, no need to compute it
  153. let codecEvents: number = 2;
  154. if (data.audio && !data.video || !data.altAudio) {
  155. codecEvents = 1;
  156. }
  157. this.bufferCodecEventsExpected = this._bufferCodecEventsTotal = codecEvents;
  158.  
  159. logger.log(`${this.bufferCodecEventsExpected} bufferCodec event(s) expected`);
  160. }
  161.  
  162. onMediaAttaching (data: { media: HTMLMediaElement }) {
  163. let media = this.media = data.media;
  164. if (media && MediaSource) {
  165. // setup the media source
  166. let ms = this.mediaSource = new MediaSource();
  167. // Media Source listeners
  168. ms.addEventListener('sourceopen', this._onMediaSourceOpen);
  169. ms.addEventListener('sourceended', this._onMediaSourceEnded);
  170. ms.addEventListener('sourceclose', this._onMediaSourceClose);
  171. // link video and media Source
  172. media.src = window.URL.createObjectURL(ms);
  173. // cache the locally generated object url
  174. this._objectUrl = media.src;
  175. }
  176. }
  177.  
  178. onMediaDetaching () {
  179. logger.log('media source detaching');
  180. let ms = this.mediaSource;
  181. if (ms) {
  182. if (ms.readyState === 'open') {
  183. try {
  184. // endOfStream could trigger exception if any sourcebuffer is in updating state
  185. // we don't really care about checking sourcebuffer state here,
  186. // as we are anyway detaching the MediaSource
  187. // let's just avoid this exception to propagate
  188. ms.endOfStream();
  189. } catch (err) {
  190. logger.warn(`onMediaDetaching:${err.message} while calling endOfStream`);
  191. }
  192. }
  193. ms.removeEventListener('sourceopen', this._onMediaSourceOpen);
  194. ms.removeEventListener('sourceended', this._onMediaSourceEnded);
  195. ms.removeEventListener('sourceclose', this._onMediaSourceClose);
  196.  
  197. // Detach properly the MediaSource from the HTMLMediaElement as
  198. // suggested in https://github.com/w3c/media-source/issues/53.
  199. if (this.media) {
  200. if (this._objectUrl) {
  201. window.URL.revokeObjectURL(this._objectUrl);
  202. }
  203.  
  204. // clean up video tag src only if it's our own url. some external libraries might
  205. // hijack the video tag and change its 'src' without destroying the Hls instance first
  206. if (this.media.src === this._objectUrl) {
  207. this.media.removeAttribute('src');
  208. this.media.load();
  209. } else {
  210. logger.warn('media.src was changed by a third party - skip cleanup');
  211. }
  212. }
  213.  
  214. this.mediaSource = null;
  215. this.media = null;
  216. this._objectUrl = null;
  217. this.bufferCodecEventsExpected = this._bufferCodecEventsTotal;
  218. this.pendingTracks = {};
  219. this.tracks = {};
  220. this.sourceBuffer = {};
  221. this.flushRange = [];
  222. this.segments = [];
  223. this.appended = 0;
  224. }
  225.  
  226. this.hls.trigger(Events.MEDIA_DETACHED);
  227. }
  228.  
  229. checkPendingTracks () {
  230. let { bufferCodecEventsExpected, pendingTracks } = this;
  231.  
  232. // Check if we've received all of the expected bufferCodec events. When none remain, create all the sourceBuffers at once.
  233. // This is important because the MSE spec allows implementations to throw QuotaExceededErrors if creating new sourceBuffers after
  234. // data has been appended to existing ones.
  235. // 2 tracks is the max (one for audio, one for video). If we've reach this max go ahead and create the buffers.
  236. const pendingTracksCount = Object.keys(pendingTracks).length;
  237. if ((pendingTracksCount && !bufferCodecEventsExpected) || pendingTracksCount === 2) {
  238. // ok, let's create them now !
  239. this.createSourceBuffers(pendingTracks);
  240. this.pendingTracks = {};
  241. // append any pending segments now !
  242. this.doAppending();
  243. }
  244. }
  245.  
  246. private _onMediaSourceOpen = () => {
  247. logger.log('media source opened');
  248. this.hls.trigger(Events.MEDIA_ATTACHED, { media: this.media });
  249. let mediaSource = this.mediaSource;
  250. if (mediaSource) {
  251. // once received, don't listen anymore to sourceopen event
  252. mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen);
  253. }
  254. this.checkPendingTracks();
  255. }
  256.  
  257. private _onMediaSourceClose = () => {
  258. logger.log('media source closed');
  259. }
  260.  
  261. private _onMediaSourceEnded = () => {
  262. logger.log('media source ended');
  263. }
  264.  
  265. private _onSBUpdateEnd = () => {
  266. // update timestampOffset
  267. if (this.audioTimestampOffset && this.sourceBuffer.audio) {
  268. let audioBuffer = this.sourceBuffer.audio;
  269.  
  270. logger.warn(`change mpeg audio timestamp offset from ${audioBuffer.timestampOffset} to ${this.audioTimestampOffset}`);
  271. audioBuffer.timestampOffset = this.audioTimestampOffset;
  272. delete this.audioTimestampOffset;
  273. }
  274.  
  275. if (this._needsFlush) {
  276. this.doFlush();
  277. }
  278.  
  279. if (this._needsEos) {
  280. this.checkEos();
  281. }
  282.  
  283. this.appending = false;
  284. let parent = this.parent;
  285. // count nb of pending segments waiting for appending on this sourcebuffer
  286. let pending = this.segments.reduce((counter, segment) => (segment.parent === parent) ? counter + 1 : counter, 0);
  287.  
  288. // this.sourceBuffer is better to use than media.buffered as it is closer to the PTS data from the fragments
  289. const timeRanges: Partial<Record<SourceBufferName, TimeRanges>> = {};
  290. const sbSet = this.sourceBuffer;
  291. for (let streamType in sbSet) {
  292. const sb = sbSet[streamType as SourceBufferName];
  293. if (!sb) {
  294. throw Error(`handling source buffer update end error: source buffer for ${streamType} uninitilized and unable to update buffered TimeRanges.`);
  295. }
  296. timeRanges[streamType as SourceBufferName] = sb.buffered;
  297. }
  298.  
  299. this.hls.trigger(Events.BUFFER_APPENDED, { parent, pending, timeRanges });
  300. // don't append in flushing mode
  301. if (!this._needsFlush) {
  302. this.doAppending();
  303. }
  304.  
  305. this.updateMediaElementDuration();
  306.  
  307. // appending goes first
  308. if (pending === 0) {
  309. this.flushLiveBackBuffer();
  310. }
  311. }
  312.  
  313. private _onSBUpdateError = (event: Event) => {
  314. logger.error('sourceBuffer error:', event);
  315. // according to http://www.w3.org/TR/media-source/#sourcebuffer-append-error
  316. // this error might not always be fatal (it is fatal if decode error is set, in that case
  317. // it will be followed by a mediaElement error ...)
  318. this.hls.trigger(Events.ERROR, { type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.BUFFER_APPENDING_ERROR, fatal: false });
  319. // we don't need to do more than that, as accordin to the spec, updateend will be fired just after
  320. }
  321.  
  322. onBufferReset () {
  323. const sourceBuffer = this.sourceBuffer;
  324. for (let type in sourceBuffer) {
  325. const sb = sourceBuffer[type];
  326. try {
  327. if (sb) {
  328. if (this.mediaSource) {
  329. this.mediaSource.removeSourceBuffer(sb);
  330. }
  331. sb.removeEventListener('updateend', this._onSBUpdateEnd);
  332. sb.removeEventListener('error', this._onSBUpdateError);
  333. }
  334. } catch (err) {
  335. }
  336. }
  337. this.sourceBuffer = {};
  338. this.flushRange = [];
  339. this.segments = [];
  340. this.appended = 0;
  341. }
  342.  
  343. onBufferCodecs (tracks: TrackSet) {
  344. // if source buffer(s) not created yet, appended buffer tracks in this.pendingTracks
  345. // if sourcebuffers already created, do nothing ...
  346. if (Object.keys(this.sourceBuffer).length) {
  347. return;
  348. }
  349.  
  350. Object.keys(tracks).forEach(trackName => {
  351. this.pendingTracks[trackName] = tracks[trackName];
  352. });
  353.  
  354. this.bufferCodecEventsExpected = Math.max(this.bufferCodecEventsExpected - 1, 0);
  355. if (this.mediaSource && this.mediaSource.readyState === 'open') {
  356. this.checkPendingTracks();
  357. }
  358. }
  359.  
  360. createSourceBuffers (tracks: TrackSet) {
  361. const { sourceBuffer, mediaSource } = this;
  362. if (!mediaSource) {
  363. throw Error('createSourceBuffers called when mediaSource was null');
  364. }
  365.  
  366. for (let trackName in tracks) {
  367. if (!sourceBuffer[trackName]) {
  368. let track = tracks[trackName as keyof TrackSet];
  369. if (!track) {
  370. throw Error(`source buffer exists for track ${trackName}, however track does not`);
  371. }
  372. // use levelCodec as first priority
  373. let codec = track.levelCodec || track.codec;
  374. let mimeType = `${track.container};codecs=${codec}`;
  375. logger.log(`creating sourceBuffer(${mimeType})`);
  376. try {
  377. let sb = sourceBuffer[trackName] = mediaSource.addSourceBuffer(mimeType);
  378. sb.addEventListener('updateend', this._onSBUpdateEnd);
  379. sb.addEventListener('error', this._onSBUpdateError);
  380. this.tracks[trackName] = {
  381. buffer: sb,
  382. codec: codec,
  383. id: track.id,
  384. container: track.container,
  385. levelCodec: track.levelCodec
  386. };
  387. } catch (err) {
  388. logger.error(`error while trying to add sourceBuffer:${err.message}`);
  389. this.hls.trigger(Events.ERROR, { type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.BUFFER_ADD_CODEC_ERROR, fatal: false, err: err, mimeType: mimeType });
  390. }
  391. }
  392. }
  393. this.hls.trigger(Events.BUFFER_CREATED, { tracks: this.tracks });
  394. }
  395.  
  396. onBufferAppending (data: Segment) {
  397. if (!this._needsFlush) {
  398. if (!this.segments) {
  399. this.segments = [ data ];
  400. } else {
  401. this.segments.push(data);
  402. }
  403.  
  404. this.doAppending();
  405. }
  406. }
  407.  
  408. // on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos()
  409. // an undefined data.type will mark all buffers as EOS.
  410. onBufferEos (data: { type?: SourceBufferName }) {
  411. for (const type in this.sourceBuffer) {
  412. if (!data.type || data.type === type) {
  413. const sb = this.sourceBuffer[type as SourceBufferName];
  414. if (sb && !sb.ended) {
  415. sb.ended = true;
  416. logger.log(`${type} sourceBuffer now EOS`);
  417. }
  418. }
  419. }
  420.  
  421. this.checkEos();
  422. }
  423.  
  424. // if all source buffers are marked as ended, signal endOfStream() to MediaSource.
  425. checkEos () {
  426. const { sourceBuffer, mediaSource } = this;
  427. if (!mediaSource || mediaSource.readyState !== 'open') {
  428. this._needsEos = false;
  429. return;
  430. }
  431.  
  432. for (let type in sourceBuffer) {
  433. const sb = sourceBuffer[type as SourceBufferName];
  434. if (!sb) continue;
  435.  
  436. if (!sb.ended) {
  437. return;
  438. }
  439.  
  440. if (sb.updating) {
  441. this._needsEos = true;
  442. return;
  443. }
  444. }
  445.  
  446. logger.log('all media data are available, signal endOfStream() to MediaSource and stop loading fragment');
  447. // Notify the media element that it now has all of the media data
  448. try {
  449. mediaSource.endOfStream();
  450. } catch (e) {
  451. logger.warn('exception while calling mediaSource.endOfStream()');
  452. }
  453. this._needsEos = false;
  454. }
  455.  
  456. onBufferFlushing (data: { startOffset: number, endOffset: number, type?: SourceBufferName }) {
  457. if (data.type) {
  458. this.flushRange.push({ start: data.startOffset, end: data.endOffset, type: data.type });
  459. } else {
  460. this.flushRange.push({ start: data.startOffset, end: data.endOffset, type: 'video' });
  461. this.flushRange.push({ start: data.startOffset, end: data.endOffset, type: 'audio' });
  462. }
  463.  
  464. // attempt flush immediately
  465. this.flushBufferCounter = 0;
  466. this.doFlush();
  467. }
  468.  
  469. flushLiveBackBuffer () {
  470. // clear back buffer for live only
  471. if (!this._live) {
  472. return;
  473. }
  474.  
  475. const liveBackBufferLength = this.config.liveBackBufferLength;
  476. if (!isFinite(liveBackBufferLength) || liveBackBufferLength < 0) {
  477. return;
  478. }
  479.  
  480. if (!this.media) {
  481. logger.error('flushLiveBackBuffer called without attaching media');
  482. return;
  483. }
  484.  
  485. const currentTime = this.media.currentTime;
  486. const sourceBuffer = this.sourceBuffer;
  487. const bufferTypes = Object.keys(sourceBuffer);
  488. const targetBackBufferPosition = currentTime - Math.max(liveBackBufferLength, this._levelTargetDuration);
  489.  
  490. for (let index = bufferTypes.length - 1; index >= 0; index--) {
  491. const bufferType = bufferTypes[index];
  492. const sb = sourceBuffer[bufferType as SourceBufferName];
  493. if (sb) {
  494. const buffered = sb.buffered;
  495. // when target buffer start exceeds actual buffer start
  496. if (buffered.length > 0 && targetBackBufferPosition > buffered.start(0)) {
  497. // remove buffer up until current time minus minimum back buffer length (removing buffer too close to current
  498. // time will lead to playback freezing)
  499. // credits for level target duration - https://github.com/videojs/http-streaming/blob/3132933b6aa99ddefab29c10447624efd6fd6e52/src/segment-loader.js#L91
  500. if (this.removeBufferRange(bufferType, sb, 0, targetBackBufferPosition)) {
  501. this.hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, { bufferEnd: targetBackBufferPosition });
  502. }
  503. }
  504. }
  505. }
  506. }
  507.  
  508. onLevelUpdated ({ details }: { details: { totalduration: number, targetduration?: number, averagetargetduration?: number, live: boolean, fragments: any[] } }) {
  509. if (details.fragments.length > 0) {
  510. this._levelDuration = details.totalduration + details.fragments[0].start;
  511. this._levelTargetDuration = details.averagetargetduration || details.targetduration || 10;
  512. this._live = details.live;
  513. this.updateMediaElementDuration();
  514. if (this.config.liveDurationInfinity) {
  515. this.updateSeekableRange(details);
  516. }
  517. }
  518. }
  519.  
  520. /**
  521. * Update Media Source duration to current level duration or override to Infinity if configuration parameter
  522. * 'liveDurationInfinity` is set to `true`
  523. * More details: https://github.com/video-dev/hls.js/issues/355
  524. */
  525. updateMediaElementDuration () {
  526. let { config } = this;
  527. let duration: number;
  528.  
  529. if (this._levelDuration === null ||
  530. !this.media ||
  531. !this.mediaSource ||
  532. !this.sourceBuffer ||
  533. this.media.readyState === 0 ||
  534. this.mediaSource.readyState !== 'open') {
  535. return;
  536. }
  537.  
  538. for (let type in this.sourceBuffer) {
  539. const sb = this.sourceBuffer[type];
  540. if (sb && sb.updating === true) {
  541. // can't set duration whilst a buffer is updating
  542. return;
  543. }
  544. }
  545.  
  546. duration = this.media.duration;
  547. // initialise to the value that the media source is reporting
  548. if (this._msDuration === null) {
  549. this._msDuration = this.mediaSource.duration;
  550. }
  551.  
  552. if (this._live === true && config.liveDurationInfinity) {
  553. // Override duration to Infinity
  554. logger.log('Media Source duration is set to Infinity');
  555. this._msDuration = this.mediaSource.duration = Infinity;
  556. } else if ((this._levelDuration > this._msDuration && this._levelDuration > duration) || !Number.isFinite(duration)) {
  557. // levelDuration was the last value we set.
  558. // not using mediaSource.duration as the browser may tweak this value
  559. // only update Media Source duration if its value increase, this is to avoid
  560. // flushing already buffered portion when switching between quality level
  561. logger.log(`Updating Media Source duration to ${this._levelDuration.toFixed(3)}`);
  562. this._msDuration = this.mediaSource.duration = this._levelDuration;
  563. }
  564. }
  565.  
  566. updateSeekableRange (levelDetails) {
  567. const mediaSource = this.mediaSource;
  568. const fragments = levelDetails.fragments;
  569. const len = fragments.length;
  570. if (len && mediaSource?.setLiveSeekableRange) {
  571. const start = fragments[0]?.start;
  572. const end = fragments[len - 1].start + fragments[len - 1].duration;
  573. mediaSource.setLiveSeekableRange(start, end);
  574. }
  575. }
  576.  
  577. doFlush () {
  578. // loop through all buffer ranges to flush
  579. while (this.flushRange.length) {
  580. let range = this.flushRange[0];
  581. // flushBuffer will abort any buffer append in progress and flush Audio/Video Buffer
  582. if (this.flushBuffer(range.start, range.end, range.type)) {
  583. // range flushed, remove from flush array
  584. this.flushRange.shift();
  585. this.flushBufferCounter = 0;
  586. } else {
  587. this._needsFlush = true;
  588. // avoid looping, wait for SB update end to retrigger a flush
  589. return;
  590. }
  591. }
  592. if (this.flushRange.length === 0) {
  593. // everything flushed
  594. this._needsFlush = false;
  595.  
  596. // let's recompute this.appended, which is used to avoid flush looping
  597. let appended = 0;
  598. let sourceBuffer = this.sourceBuffer;
  599. try {
  600. for (let type in sourceBuffer) {
  601. const sb = sourceBuffer[type];
  602. if (sb) {
  603. appended += sb.buffered.length;
  604. }
  605. }
  606. } catch (error) {
  607. // error could be thrown while accessing buffered, in case sourcebuffer has already been removed from MediaSource
  608. // this is harmess at this stage, catch this to avoid reporting an internal exception
  609. logger.error('error while accessing sourceBuffer.buffered');
  610. }
  611. this.appended = appended;
  612. this.hls.trigger(Events.BUFFER_FLUSHED);
  613. }
  614. }
  615.  
  616. doAppending () {
  617. let { config, hls, segments, sourceBuffer } = this;
  618. if (!Object.keys(sourceBuffer).length) {
  619. // early exit if no source buffers have been initialized yet
  620. return;
  621. }
  622.  
  623. if (!this.media || this.media.error) {
  624. this.segments = [];
  625. logger.error('trying to append although a media error occured, flush segment and abort');
  626. return;
  627. }
  628.  
  629. if (this.appending) {
  630. // logger.log(`sb appending in progress`);
  631. return;
  632. }
  633.  
  634. const segment = segments.shift();
  635. if (!segment) { // handle undefined shift
  636. return;
  637. }
  638.  
  639. try {
  640. const sb = sourceBuffer[segment.type];
  641. if (!sb) {
  642. // in case we don't have any source buffer matching with this segment type,
  643. // it means that Mediasource fails to create sourcebuffer
  644. // discard this segment, and trigger update end
  645. this._onSBUpdateEnd();
  646. return;
  647. }
  648.  
  649. if (sb.updating) {
  650. // if we are still updating the source buffer from the last segment, place this back at the front of the queue
  651. segments.unshift(segment);
  652. return;
  653. }
  654.  
  655. // reset sourceBuffer ended flag before appending segment
  656. sb.ended = false;
  657. // logger.log(`appending ${segment.content} ${type} SB, size:${segment.data.length}, ${segment.parent}`);
  658. this.parent = segment.parent;
  659. sb.appendBuffer(segment.data);
  660. this.appendError = 0;
  661. this.appended++;
  662. this.appending = true;
  663. } catch (err) {
  664. // in case any error occured while appending, put back segment in segments table
  665. logger.error(`error while trying to append buffer:${err.message}`);
  666. segments.unshift(segment);
  667. let event = { type: ErrorTypes.MEDIA_ERROR, parent: segment.parent, details: '', fatal: false };
  668. if (err.code === 22) {
  669. // QuotaExceededError: http://www.w3.org/TR/html5/infrastructure.html#quotaexceedederror
  670. // let's stop appending any segments, and report BUFFER_FULL_ERROR error
  671. this.segments = [];
  672. event.details = ErrorDetails.BUFFER_FULL_ERROR;
  673. } else {
  674. this.appendError++;
  675. event.details = ErrorDetails.BUFFER_APPEND_ERROR;
  676. /* with UHD content, we could get loop of quota exceeded error until
  677. browser is able to evict some data from sourcebuffer. retrying help recovering this
  678. */
  679. if (this.appendError > config.appendErrorMaxRetry) {
  680. logger.log(`fail ${config.appendErrorMaxRetry} times to append segment in sourceBuffer`);
  681. this.segments = [];
  682. event.fatal = true;
  683. }
  684. }
  685. hls.trigger(Events.ERROR, event);
  686. }
  687. }
  688.  
  689. /*
  690. flush specified buffered range,
  691. return true once range has been flushed.
  692. as sourceBuffer.remove() is asynchronous, flushBuffer will be retriggered on sourceBuffer update end
  693. */
  694. flushBuffer (startOffset: number, endOffset: number, sbType: SourceBufferName): boolean {
  695. const sourceBuffer = this.sourceBuffer;
  696. // exit if no sourceBuffers are initialized
  697. if (!Object.keys(sourceBuffer).length) {
  698. return true;
  699. }
  700.  
  701. let currentTime: string = 'null';
  702. if (this.media) {
  703. currentTime = this.media.currentTime.toFixed(3);
  704. }
  705. logger.log(`flushBuffer,pos/start/end: ${currentTime}/${startOffset}/${endOffset}`);
  706.  
  707. // safeguard to avoid infinite looping : don't try to flush more than the nb of appended segments
  708. if (this.flushBufferCounter >= this.appended) {
  709. logger.warn('abort flushing too many retries');
  710. return true;
  711. }
  712.  
  713. const sb = sourceBuffer[sbType];
  714. // we are going to flush buffer, mark source buffer as 'not ended'
  715. if (sb) {
  716. sb.ended = false;
  717. if (!sb.updating) {
  718. if (this.removeBufferRange(sbType, sb, startOffset, endOffset)) {
  719. this.flushBufferCounter++;
  720. return false;
  721. }
  722. } else {
  723. logger.warn('cannot flush, sb updating in progress');
  724. return false;
  725. }
  726. }
  727.  
  728. logger.log('buffer flushed');
  729. // everything flushed !
  730. return true;
  731. }
  732.  
  733. /**
  734. * Removes first buffered range from provided source buffer that lies within given start and end offsets.
  735. *
  736. * @param {string} type Type of the source buffer, logging purposes only.
  737. * @param {SourceBuffer} sb Target SourceBuffer instance.
  738. * @param {number} startOffset
  739. * @param {number} endOffset
  740. *
  741. * @returns {boolean} True when source buffer remove requested.
  742. */
  743. removeBufferRange (type: string, sb: ExtendedSourceBuffer, startOffset: number, endOffset: number): boolean {
  744. try {
  745. for (let i = 0; i < sb.buffered.length; i++) {
  746. let bufStart = sb.buffered.start(i);
  747. let bufEnd = sb.buffered.end(i);
  748. let removeStart = Math.max(bufStart, startOffset);
  749. let removeEnd = Math.min(bufEnd, endOffset);
  750.  
  751. /* sometimes sourcebuffer.remove() does not flush
  752. the exact expected time range.
  753. to avoid rounding issues/infinite loop,
  754. only flush buffer range of length greater than 500ms.
  755. */
  756. if (Math.min(removeEnd, bufEnd) - removeStart > 0.5) {
  757. let currentTime: string = 'null';
  758. if (this.media) {
  759. currentTime = this.media.currentTime.toString();
  760. }
  761.  
  762. logger.log(`sb remove ${type} [${removeStart},${removeEnd}], of [${bufStart},${bufEnd}], pos:${currentTime}`);
  763. sb.remove(removeStart, removeEnd);
  764. return true;
  765. }
  766. }
  767. } catch (error) {
  768. logger.warn('removeBufferRange failed', error);
  769. }
  770.  
  771. return false;
  772. }
  773. }
  774.  
  775. export default BufferController;