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