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