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 '../utils/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. // sort on buffer.start/smaller end (IE does not always return sorted buffered range)
  95. buffered.sort(function (a, b) {
  96. const diff = a.start - b.start;
  97. if (diff) {
  98. return diff;
  99. } else {
  100. return b.end - a.end;
  101. }
  102. });
  103.  
  104. let buffered2: BufferTimeRange[] = [];
  105. if (maxHoleDuration) {
  106. // there might be some small holes between buffer time range
  107. // consider that holes smaller than maxHoleDuration are irrelevant and build another
  108. // buffer time range representations that discards those holes
  109. for (let i = 0; i < buffered.length; i++) {
  110. const buf2len = buffered2.length;
  111. if (buf2len) {
  112. const buf2end = buffered2[buf2len - 1].end;
  113. // if small hole (value between 0 or maxHoleDuration ) or overlapping (negative)
  114. if (buffered[i].start - buf2end < maxHoleDuration) {
  115. // merge overlapping time ranges
  116. // update lastRange.end only if smaller than item.end
  117. // e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end)
  118. // whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15])
  119. if (buffered[i].end > buf2end) {
  120. buffered2[buf2len - 1].end = buffered[i].end;
  121. }
  122. } else {
  123. // big hole
  124. buffered2.push(buffered[i]);
  125. }
  126. } else {
  127. // first value
  128. buffered2.push(buffered[i]);
  129. }
  130. }
  131. } else {
  132. buffered2 = buffered;
  133. }
  134.  
  135. let bufferLen = 0;
  136.  
  137. // bufferStartNext can possibly be undefined based on the conditional logic below
  138. let bufferStartNext: number | undefined;
  139.  
  140. // bufferStart and bufferEnd are buffer boundaries around current video position
  141. let bufferStart: number = pos;
  142. let bufferEnd: number = pos;
  143. for (let i = 0; i < buffered2.length; i++) {
  144. const start = buffered2[i].start;
  145. const end = buffered2[i].end;
  146. // logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i));
  147. if (pos + maxHoleDuration >= start && pos < end) {
  148. // play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
  149. bufferStart = start;
  150. bufferEnd = end;
  151. bufferLen = bufferEnd - pos;
  152. } else if (pos + maxHoleDuration < start) {
  153. bufferStartNext = start;
  154. break;
  155. }
  156. }
  157. return {
  158. len: bufferLen,
  159. start: bufferStart || 0,
  160. end: bufferEnd || 0,
  161. nextStart: bufferStartNext,
  162. };
  163. }
  164.  
  165. /**
  166. * Safe method to get buffered property.
  167. * SourceBuffer.buffered may throw if SourceBuffer is removed from it's MediaSource
  168. */
  169. static getBuffered(media: Bufferable): TimeRanges {
  170. try {
  171. return media.buffered;
  172. } catch (e) {
  173. logger.log('failed to get media.buffered', e);
  174. return noopBuffered;
  175. }
  176. }
  177. }