Home Reference Source

src/utils/buffer-helper.ts

  1. /**
  2. * @module BufferHelper
  3. *
  4. * Providing methods dealing with buffer length retrieval for example.
  5. *
  6. * In general, a helper around HTML5 MediaElement TimeRanges gathered from `buffered` property.
  7. *
  8. * Also @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered
  9. */
  10.  
  11. import { logger } from './logger';
  12.  
  13. type BufferTimeRange = {
  14. start: number;
  15. end: number;
  16. };
  17.  
  18. export type Bufferable = {
  19. buffered: TimeRanges;
  20. };
  21.  
  22. export type BufferInfo = {
  23. len: number;
  24. start: number;
  25. end: number;
  26. nextStart?: number;
  27. };
  28.  
  29. const noopBuffered: TimeRanges = {
  30. length: 0,
  31. start: () => 0,
  32. end: () => 0,
  33. };
  34.  
  35. export class BufferHelper {
  36. /**
  37. * Return true if `media`'s buffered include `position`
  38. * @param {Bufferable} media
  39. * @param {number} position
  40. * @returns {boolean}
  41. */
  42. static isBuffered(media: Bufferable, position: number): boolean {
  43. try {
  44. if (media) {
  45. const buffered = BufferHelper.getBuffered(media);
  46. for (let i = 0; i < buffered.length; i++) {
  47. if (position >= buffered.start(i) && position <= buffered.end(i)) {
  48. return true;
  49. }
  50. }
  51. }
  52. } catch (error) {
  53. // this is to catch
  54. // InvalidStateError: Failed to read the 'buffered' property from 'SourceBuffer':
  55. // This SourceBuffer has been removed from the parent media source
  56. }
  57. return false;
  58. }
  59.  
  60. static bufferInfo(
  61. media: Bufferable | null,
  62. pos: number,
  63. maxHoleDuration: number
  64. ): BufferInfo {
  65. try {
  66. if (media) {
  67. const vbuffered = BufferHelper.getBuffered(media);
  68. const buffered: BufferTimeRange[] = [];
  69. let i: number;
  70. for (i = 0; i < vbuffered.length; i++) {
  71. buffered.push({ start: vbuffered.start(i), end: vbuffered.end(i) });
  72. }
  73.  
  74. return this.bufferedInfo(buffered, pos, maxHoleDuration);
  75. }
  76. } catch (error) {
  77. // this is to catch
  78. // InvalidStateError: Failed to read the 'buffered' property from 'SourceBuffer':
  79. // This SourceBuffer has been removed from the parent media source
  80. }
  81. return { len: 0, start: pos, end: pos, nextStart: undefined };
  82. }
  83.  
  84. static bufferedInfo(
  85. buffered: BufferTimeRange[],
  86. pos: number,
  87. maxHoleDuration: number
  88. ): {
  89. len: number;
  90. start: number;
  91. end: number;
  92. nextStart?: number;
  93. } {
  94. pos = Math.max(0, pos);
  95. // sort on buffer.start/smaller end (IE does not always return sorted buffered range)
  96. buffered.sort(function (a, b) {
  97. const diff = a.start - b.start;
  98. if (diff) {
  99. return diff;
  100. } else {
  101. return b.end - a.end;
  102. }
  103. });
  104.  
  105. let buffered2: BufferTimeRange[] = [];
  106. if (maxHoleDuration) {
  107. // there might be some small holes between buffer time range
  108. // consider that holes smaller than maxHoleDuration are irrelevant and build another
  109. // buffer time range representations that discards those holes
  110. for (let i = 0; i < buffered.length; i++) {
  111. const buf2len = buffered2.length;
  112. if (buf2len) {
  113. const buf2end = buffered2[buf2len - 1].end;
  114. // if small hole (value between 0 or maxHoleDuration ) or overlapping (negative)
  115. if (buffered[i].start - buf2end < maxHoleDuration) {
  116. // merge overlapping time ranges
  117. // update lastRange.end only if smaller than item.end
  118. // e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end)
  119. // whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15])
  120. if (buffered[i].end > buf2end) {
  121. buffered2[buf2len - 1].end = buffered[i].end;
  122. }
  123. } else {
  124. // big hole
  125. buffered2.push(buffered[i]);
  126. }
  127. } else {
  128. // first value
  129. buffered2.push(buffered[i]);
  130. }
  131. }
  132. } else {
  133. buffered2 = buffered;
  134. }
  135.  
  136. let bufferLen = 0;
  137.  
  138. // bufferStartNext can possibly be undefined based on the conditional logic below
  139. let bufferStartNext: number | undefined;
  140.  
  141. // bufferStart and bufferEnd are buffer boundaries around current video position
  142. let bufferStart: number = pos;
  143. let bufferEnd: number = pos;
  144. for (let i = 0; i < buffered2.length; i++) {
  145. const start = buffered2[i].start;
  146. const end = buffered2[i].end;
  147. // logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i));
  148. if (pos + maxHoleDuration >= start && pos < end) {
  149. // play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
  150. bufferStart = start;
  151. bufferEnd = end;
  152. bufferLen = bufferEnd - pos;
  153. } else if (pos + maxHoleDuration < start) {
  154. bufferStartNext = start;
  155. break;
  156. }
  157. }
  158. return {
  159. len: bufferLen,
  160. start: bufferStart || 0,
  161. end: bufferEnd || 0,
  162. nextStart: bufferStartNext,
  163. };
  164. }
  165.  
  166. /**
  167. * Safe method to get buffered property.
  168. * SourceBuffer.buffered may throw if SourceBuffer is removed from it's MediaSource
  169. */
  170. static getBuffered(media: Bufferable): TimeRanges {
  171. try {
  172. return media.buffered;
  173. } catch (e) {
  174. logger.log('failed to get media.buffered', e);
  175. return noopBuffered;
  176. }
  177. }
  178. }