You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
5.5 KiB

  1. /**
  2. * @file source-updater.js
  3. */
  4. import videojs from 'video.js';
  5. import { printableRange } from './ranges';
  6. import logger from './util/logger';
  7. import noop from './util/noop';
  8. /**
  9. * A queue of callbacks to be serialized and applied when a
  10. * MediaSource and its associated SourceBuffers are not in the
  11. * updating state. It is used by the segment loader to update the
  12. * underlying SourceBuffers when new data is loaded, for instance.
  13. *
  14. * @class SourceUpdater
  15. * @param {MediaSource} mediaSource the MediaSource to create the
  16. * SourceBuffer from
  17. * @param {String} mimeType the desired MIME type of the underlying
  18. * SourceBuffer
  19. * @param {Object} sourceBufferEmitter an event emitter that fires when a source buffer is
  20. * added to the media source
  21. */
  22. export default class SourceUpdater {
  23. constructor(mediaSource, mimeType, type, sourceBufferEmitter) {
  24. this.callbacks_ = [];
  25. this.pendingCallback_ = null;
  26. this.timestampOffset_ = 0;
  27. this.mediaSource = mediaSource;
  28. this.processedAppend_ = false;
  29. this.type_ = type;
  30. this.mimeType_ = mimeType;
  31. this.logger_ = logger(`SourceUpdater[${type}][${mimeType}]`);
  32. if (mediaSource.readyState === 'closed') {
  33. mediaSource.addEventListener(
  34. 'sourceopen', this.createSourceBuffer_.bind(this, mimeType, sourceBufferEmitter));
  35. } else {
  36. this.createSourceBuffer_(mimeType, sourceBufferEmitter);
  37. }
  38. }
  39. createSourceBuffer_(mimeType, sourceBufferEmitter) {
  40. this.sourceBuffer_ = this.mediaSource.addSourceBuffer(mimeType);
  41. this.logger_('created SourceBuffer');
  42. if (sourceBufferEmitter) {
  43. sourceBufferEmitter.trigger('sourcebufferadded');
  44. if (this.mediaSource.sourceBuffers.length < 2) {
  45. // There's another source buffer we must wait for before we can start updating
  46. // our own (or else we can get into a bad state, i.e., appending video/audio data
  47. // before the other video/audio source buffer is available and leading to a video
  48. // or audio only buffer).
  49. sourceBufferEmitter.on('sourcebufferadded', () => {
  50. this.start_();
  51. });
  52. return;
  53. }
  54. }
  55. this.start_();
  56. }
  57. start_() {
  58. this.started_ = true;
  59. // run completion handlers and process callbacks as updateend
  60. // events fire
  61. this.onUpdateendCallback_ = () => {
  62. let pendingCallback = this.pendingCallback_;
  63. this.pendingCallback_ = null;
  64. this.logger_(`buffered [${printableRange(this.buffered())}]`);
  65. if (pendingCallback) {
  66. pendingCallback();
  67. }
  68. this.runCallback_();
  69. };
  70. this.sourceBuffer_.addEventListener('updateend', this.onUpdateendCallback_);
  71. this.runCallback_();
  72. }
  73. /**
  74. * Aborts the current segment and resets the segment parser.
  75. *
  76. * @param {Function} done function to call when done
  77. * @see http://w3c.github.io/media-source/#widl-SourceBuffer-abort-void
  78. */
  79. abort(done) {
  80. if (this.processedAppend_) {
  81. this.queueCallback_(() => {
  82. this.sourceBuffer_.abort();
  83. }, done);
  84. }
  85. }
  86. /**
  87. * Queue an update to append an ArrayBuffer.
  88. *
  89. * @param {ArrayBuffer} bytes
  90. * @param {Function} done the function to call when done
  91. * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data
  92. */
  93. appendBuffer(bytes, done) {
  94. this.processedAppend_ = true;
  95. this.queueCallback_(() => {
  96. this.sourceBuffer_.appendBuffer(bytes);
  97. }, done);
  98. }
  99. /**
  100. * Indicates what TimeRanges are buffered in the managed SourceBuffer.
  101. *
  102. * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-buffered
  103. */
  104. buffered() {
  105. if (!this.sourceBuffer_) {
  106. return videojs.createTimeRanges();
  107. }
  108. return this.sourceBuffer_.buffered;
  109. }
  110. /**
  111. * Queue an update to remove a time range from the buffer.
  112. *
  113. * @param {Number} start where to start the removal
  114. * @param {Number} end where to end the removal
  115. * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
  116. */
  117. remove(start, end) {
  118. if (this.processedAppend_) {
  119. this.queueCallback_(() => {
  120. this.logger_(`remove [${start} => ${end}]`);
  121. this.sourceBuffer_.remove(start, end);
  122. }, noop);
  123. }
  124. }
  125. /**
  126. * Whether the underlying sourceBuffer is updating or not
  127. *
  128. * @return {Boolean} the updating status of the SourceBuffer
  129. */
  130. updating() {
  131. return !this.sourceBuffer_ || this.sourceBuffer_.updating || this.pendingCallback_;
  132. }
  133. /**
  134. * Set/get the timestampoffset on the SourceBuffer
  135. *
  136. * @return {Number} the timestamp offset
  137. */
  138. timestampOffset(offset) {
  139. if (typeof offset !== 'undefined') {
  140. this.queueCallback_(() => {
  141. this.sourceBuffer_.timestampOffset = offset;
  142. });
  143. this.timestampOffset_ = offset;
  144. }
  145. return this.timestampOffset_;
  146. }
  147. /**
  148. * Queue a callback to run
  149. */
  150. queueCallback_(callback, done) {
  151. this.callbacks_.push([callback.bind(this), done]);
  152. this.runCallback_();
  153. }
  154. /**
  155. * Run a queued callback
  156. */
  157. runCallback_() {
  158. let callbacks;
  159. if (!this.updating() &&
  160. this.callbacks_.length &&
  161. this.started_) {
  162. callbacks = this.callbacks_.shift();
  163. this.pendingCallback_ = callbacks[1];
  164. callbacks[0]();
  165. }
  166. }
  167. /**
  168. * dispose of the source updater and the underlying sourceBuffer
  169. */
  170. dispose() {
  171. this.sourceBuffer_.removeEventListener('updateend', this.onUpdateendCallback_);
  172. if (this.sourceBuffer_ && this.mediaSource.readyState === 'open') {
  173. this.sourceBuffer_.abort();
  174. }
  175. }
  176. }