Source: lib/media/media_source_engine.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.media.MediaSourceEngine');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.media.TextEngine');
  21. goog.require('shaka.media.TimeRangesUtils');
  22. goog.require('shaka.util.Error');
  23. goog.require('shaka.util.EventManager');
  24. goog.require('shaka.util.Functional');
  25. goog.require('shaka.util.IDestroyable');
  26. goog.require('shaka.util.ManifestParserUtils');
  27. goog.require('shaka.util.PublicPromise');
  28. /**
  29. * MediaSourceEngine wraps all operations on MediaSource and SourceBuffers.
  30. * All asynchronous operations return a Promise, and all operations are
  31. * internally synchronized and serialized as needed. Operations that can
  32. * be done in parallel will be done in parallel.
  33. *
  34. * @param {HTMLMediaElement} video The video element, used to read error codes
  35. * when MediaSource operations fail.
  36. * @param {MediaSource} mediaSource The MediaSource, which must be in the
  37. * 'open' state.
  38. * @param {TextTrack} textTrack The TextTrack to use for subtitles/captions.
  39. *
  40. * @struct
  41. * @constructor
  42. * @implements {shaka.util.IDestroyable}
  43. */
  44. shaka.media.MediaSourceEngine = function(video, mediaSource, textTrack) {
  45. goog.asserts.assert(mediaSource.readyState == 'open',
  46. 'The MediaSource should be in the \'open\' state.');
  47. /** @private {HTMLMediaElement} */
  48. this.video_ = video;
  49. /** @private {MediaSource} */
  50. this.mediaSource_ = mediaSource;
  51. /** @private {TextTrack} */
  52. this.textTrack_ = textTrack;
  53. /** @private {!Object.<shaka.util.ManifestParserUtils.ContentType,
  54. SourceBuffer>} */
  55. this.sourceBuffers_ = {};
  56. /** @private {shaka.media.TextEngine} */
  57. this.textEngine_ = null;
  58. /**
  59. * @private {!Object.<string,
  60. * !Array.<shaka.media.MediaSourceEngine.Operation>>}
  61. */
  62. this.queues_ = {};
  63. /** @private {shaka.util.EventManager} */
  64. this.eventManager_ = new shaka.util.EventManager();
  65. /** @private {boolean} */
  66. this.destroyed_ = false;
  67. };
  68. /**
  69. * @typedef {{
  70. * start: function(),
  71. * p: !shaka.util.PublicPromise
  72. * }}
  73. *
  74. * @summary An operation in queue.
  75. * @property {function()} start
  76. * The function which starts the operation.
  77. * @property {!shaka.util.PublicPromise} p
  78. * The PublicPromise which is associated with this operation.
  79. */
  80. shaka.media.MediaSourceEngine.Operation;
  81. /**
  82. * Checks if a certain type is supported.
  83. *
  84. * @param {string} mimeType
  85. * @return {boolean}
  86. */
  87. shaka.media.MediaSourceEngine.isTypeSupported = function(mimeType) {
  88. return shaka.media.TextEngine.isTypeSupported(mimeType) ||
  89. MediaSource.isTypeSupported(mimeType);
  90. };
  91. /**
  92. * Returns true if the browser has the basic APIs we need.
  93. *
  94. * @return {boolean}
  95. */
  96. shaka.media.MediaSourceEngine.isBrowserSupported = function() {
  97. return !!window.MediaSource;
  98. };
  99. /**
  100. * Returns a map of MediaSource support for well-known types.
  101. *
  102. * @return {!Object.<string, boolean>}
  103. */
  104. shaka.media.MediaSourceEngine.probeSupport = function() {
  105. goog.asserts.assert(shaka.media.MediaSourceEngine.isBrowserSupported(),
  106. 'Requires basic support');
  107. var support = {};
  108. var testMimeTypes = [
  109. // MP4 types
  110. 'video/mp4; codecs="avc1.42E01E"',
  111. 'video/mp4; codecs="avc3.42E01E"',
  112. 'video/mp4; codecs="hvc1.1.6.L93.90"',
  113. 'audio/mp4; codecs="mp4a.40.2"',
  114. 'audio/mp4; codecs="ac-3"',
  115. 'audio/mp4; codecs="ec-3"',
  116. // WebM types
  117. 'video/webm; codecs="vp8"',
  118. 'video/webm; codecs="vp9"',
  119. 'video/webm; codecs="av1"',
  120. 'audio/webm; codecs="vorbis"',
  121. 'audio/webm; codecs="opus"',
  122. // MPEG2 TS types (video/ is also used for audio: http://goo.gl/tYHXiS)
  123. 'video/mp2t; codecs="avc1.42E01E"',
  124. 'video/mp2t; codecs="avc3.42E01E"',
  125. 'video/mp2t; codecs="hvc1.1.6.L93.90"',
  126. 'video/mp2t; codecs="mp4a.40.2"',
  127. 'video/mp2t; codecs="ac-3"',
  128. 'video/mp2t; codecs="ec-3"',
  129. 'video/mp2t; codecs="mp4a.40.2"',
  130. // WebVTT types
  131. 'text/vtt',
  132. 'application/mp4; codecs="wvtt"',
  133. // TTML types
  134. 'application/ttml+xml',
  135. 'application/mp4; codecs="stpp"'
  136. ];
  137. testMimeTypes.forEach(function(type) {
  138. support[type] = shaka.media.MediaSourceEngine.isTypeSupported(type);
  139. var basicType = type.split(';')[0];
  140. support[basicType] = support[basicType] || support[type];
  141. });
  142. return support;
  143. };
  144. /**
  145. * @override
  146. */
  147. shaka.media.MediaSourceEngine.prototype.destroy = function() {
  148. var Functional = shaka.util.Functional;
  149. this.destroyed_ = true;
  150. var cleanup = [];
  151. for (var contentType in this.queues_) {
  152. // Make a local copy of the queue and the first item.
  153. var q = this.queues_[contentType];
  154. var inProgress = q[0];
  155. // Drop everything else out of the queue.
  156. this.queues_[contentType] = q.slice(0, 1);
  157. // We will wait for this item to complete/fail.
  158. if (inProgress) {
  159. cleanup.push(inProgress.p.catch(Functional.noop));
  160. }
  161. // The rest will be rejected silently if possible.
  162. for (var i = 1; i < q.length; ++i) {
  163. q[i].p.catch(Functional.noop);
  164. q[i].p.reject();
  165. }
  166. }
  167. if (this.textEngine_) {
  168. cleanup.push(this.textEngine_.destroy());
  169. }
  170. return Promise.all(cleanup).then(function() {
  171. this.eventManager_.destroy();
  172. this.eventManager_ = null;
  173. this.video_ = null;
  174. this.mediaSource_ = null;
  175. this.textTrack_ = null;
  176. this.textEngine_ = null;
  177. this.sourceBuffers_ = {};
  178. if (!COMPILED) {
  179. for (var contentType in this.queues_) {
  180. goog.asserts.assert(
  181. this.queues_[contentType].length == 0,
  182. contentType + ' queue should be empty after destroy!');
  183. }
  184. }
  185. this.queues_ = {};
  186. }.bind(this));
  187. };
  188. /**
  189. * Initialize MediaSourceEngine.
  190. *
  191. * Note that it is not valid to call this multiple times, except to add or
  192. * reinitialize text streams.
  193. *
  194. * @param {!Object.<shaka.util.ManifestParserUtils.ContentType, string>}
  195. * typeConfig A map of content types to full MIME types.
  196. * For example: { 'audio': 'audio/webm; codecs="vorbis"',
  197. * 'video': 'video/webm; codecs="vp9"', 'text': 'text/vtt' }.
  198. * All types given must be supported.
  199. *
  200. * @throws InvalidAccessError if blank MIME types are given
  201. * @throws NotSupportedError if unsupported MIME types are given
  202. * @throws QuotaExceededError if the browser can't support that many buffers
  203. */
  204. shaka.media.MediaSourceEngine.prototype.init = function(typeConfig) {
  205. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  206. for (var contentType in typeConfig) {
  207. var mimeType = typeConfig[contentType];
  208. goog.asserts.assert(
  209. shaka.media.MediaSourceEngine.isTypeSupported(mimeType),
  210. 'Type negotiation should happen before MediaSourceEngine.init!');
  211. if (contentType == ContentType.TEXT) {
  212. this.reinitText(mimeType);
  213. } else {
  214. var sourceBuffer = this.mediaSource_.addSourceBuffer(mimeType);
  215. this.eventManager_.listen(
  216. sourceBuffer, 'error', this.onError_.bind(this, contentType));
  217. this.eventManager_.listen(
  218. sourceBuffer, 'updateend', this.onUpdateEnd_.bind(this, contentType));
  219. this.sourceBuffers_[contentType] = sourceBuffer;
  220. this.queues_[contentType] = [];
  221. }
  222. }
  223. };
  224. /**
  225. * Reinitialize the TextEngine for a new text type.
  226. * @param {string} mimeType
  227. */
  228. shaka.media.MediaSourceEngine.prototype.reinitText = function(mimeType) {
  229. if (!this.textEngine_) {
  230. this.textEngine_ = new shaka.media.TextEngine(this.textTrack_);
  231. }
  232. this.textEngine_.initParser(mimeType);
  233. };
  234. /**
  235. * Gets the first timestamp in buffer for the given content type.
  236. *
  237. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  238. * @return {?number} The timestamp in seconds, or null if nothing is buffered.
  239. */
  240. shaka.media.MediaSourceEngine.prototype.bufferStart = function(contentType) {
  241. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  242. if (contentType == ContentType.TEXT) {
  243. return this.textEngine_.bufferStart();
  244. }
  245. return shaka.media.TimeRangesUtils.bufferStart(
  246. this.getBuffered_(contentType));
  247. };
  248. /**
  249. * Gets the last timestamp in buffer for the given content type.
  250. *
  251. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  252. * @return {?number} The timestamp in seconds, or null if nothing is buffered.
  253. */
  254. shaka.media.MediaSourceEngine.prototype.bufferEnd = function(contentType) {
  255. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  256. if (contentType == ContentType.TEXT) {
  257. return this.textEngine_.bufferEnd();
  258. }
  259. return shaka.media.TimeRangesUtils.bufferEnd(this.getBuffered_(contentType));
  260. };
  261. /**
  262. * Determines if the given time is inside the buffered range of the given
  263. * content type.
  264. *
  265. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  266. * @param {number} time
  267. * @return {boolean}
  268. */
  269. shaka.media.MediaSourceEngine.prototype.isBuffered = function(
  270. contentType, time) {
  271. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  272. if (contentType == ContentType.TEXT) {
  273. return this.textEngine_.isBuffered(time);
  274. } else {
  275. var buffered = this.getBuffered_(contentType);
  276. return shaka.media.TimeRangesUtils.isBuffered(buffered, time);
  277. }
  278. };
  279. /**
  280. * Computes how far ahead of the given timestamp is buffered for the given
  281. * content type.
  282. *
  283. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  284. * @param {number} time
  285. * @return {number} The amount of time buffered ahead in seconds.
  286. */
  287. shaka.media.MediaSourceEngine.prototype.bufferedAheadOf =
  288. function(contentType, time) {
  289. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  290. if (contentType == ContentType.TEXT) {
  291. return this.textEngine_.bufferedAheadOf(time);
  292. } else {
  293. var buffered = this.getBuffered_(contentType);
  294. return shaka.media.TimeRangesUtils.bufferedAheadOf(buffered, time);
  295. }
  296. };
  297. /**
  298. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  299. * @return {TimeRanges} The buffered ranges for the given content type, or
  300. * null if the buffered ranges could not be obtained.
  301. * @private
  302. */
  303. shaka.media.MediaSourceEngine.prototype.getBuffered_ = function(contentType) {
  304. try {
  305. return this.sourceBuffers_[contentType].buffered;
  306. } catch (exception) {
  307. // Note: previous MediaSource errors may cause access to |buffered| to
  308. // throw.
  309. shaka.log.error('failed to get buffered range for ' + contentType,
  310. exception);
  311. return null;
  312. }
  313. };
  314. /**
  315. * Enqueue an operation to append data to the SourceBuffer.
  316. * Start and end times are needed for TextEngine, but not for MediaSource.
  317. * Start and end times may be null for initialization segments, if present they
  318. * are relative to the presentation timeline.
  319. *
  320. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  321. * @param {!ArrayBuffer} data
  322. * @param {?number} startTime
  323. * @param {?number} endTime
  324. * @return {!Promise}
  325. */
  326. shaka.media.MediaSourceEngine.prototype.appendBuffer =
  327. function(contentType, data, startTime, endTime) {
  328. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  329. if (contentType == ContentType.TEXT) {
  330. return this.textEngine_.appendBuffer(data, startTime, endTime);
  331. }
  332. return this.enqueueOperation_(
  333. contentType,
  334. this.append_.bind(this, contentType, data));
  335. };
  336. /**
  337. * Enqueue an operation to remove data from the SourceBuffer.
  338. *
  339. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  340. * @param {number} startTime
  341. * @param {number} endTime
  342. * @return {!Promise}
  343. */
  344. shaka.media.MediaSourceEngine.prototype.remove =
  345. function(contentType, startTime, endTime) {
  346. // On IE11, this operation would be permitted, but would have no effect!
  347. // See https://github.com/google/shaka-player/issues/251
  348. goog.asserts.assert(endTime < Number.MAX_VALUE,
  349. 'remove() with MAX_VALUE or Infinity is not IE-compatible!');
  350. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  351. if (contentType == ContentType.TEXT) {
  352. return this.textEngine_.remove(startTime, endTime);
  353. }
  354. return this.enqueueOperation_(
  355. contentType,
  356. this.remove_.bind(this, contentType, startTime, endTime));
  357. };
  358. /**
  359. * Enqueue an operation to clear the SourceBuffer.
  360. *
  361. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  362. * @return {!Promise}
  363. */
  364. shaka.media.MediaSourceEngine.prototype.clear = function(contentType) {
  365. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  366. if (contentType == ContentType.TEXT) {
  367. return this.textEngine_.remove(0, Infinity);
  368. }
  369. // Note that not all platforms allow clearing to Infinity.
  370. return this.enqueueOperation_(
  371. contentType,
  372. this.remove_.bind(this, contentType, 0, this.mediaSource_.duration));
  373. };
  374. /**
  375. * Enqueue an operation to flush the SourceBuffer.
  376. * This is a workaround for what we believe is a Chromecast bug.
  377. *
  378. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  379. * @return {!Promise}
  380. */
  381. shaka.media.MediaSourceEngine.prototype.flush = function(contentType) {
  382. // Flush the pipeline. Necessary on Chromecast, even though we have removed
  383. // everything.
  384. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  385. if (contentType == ContentType.TEXT) {
  386. // Nothing to flush for text.
  387. return Promise.resolve();
  388. }
  389. return this.enqueueOperation_(
  390. contentType,
  391. this.flush_.bind(this, contentType));
  392. };
  393. /**
  394. * Sets the timestamp offset and append window end for the given content type.
  395. *
  396. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  397. * @param {number} timestampOffset The timestamp offset. Segments which start
  398. * at time t will be inserted at time t + timestampOffset instead. This
  399. * value does not affect segments which have already been inserted.
  400. * @param {?number} appendWindowEnd The timestamp to set the append window end
  401. * to. Media beyond this value will be truncated.
  402. * @return {!Promise}
  403. */
  404. shaka.media.MediaSourceEngine.prototype.setStreamProperties = function(
  405. contentType, timestampOffset, appendWindowEnd) {
  406. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  407. if (contentType == ContentType.TEXT) {
  408. this.textEngine_.setTimestampOffset(timestampOffset);
  409. if (appendWindowEnd != null)
  410. this.textEngine_.setAppendWindowEnd(appendWindowEnd);
  411. return Promise.resolve();
  412. }
  413. if (appendWindowEnd == null)
  414. appendWindowEnd = Infinity;
  415. return Promise.all([
  416. // Queue an abort() to help MSE splice together overlapping segments.
  417. // We set appendWindowEnd when we change periods in DASH content, and the
  418. // period transition may result in overlap.
  419. //
  420. // An abort() also helps with MPEG2-TS. When we append a TS segment, we
  421. // always enter a PARSING_MEDIA_SEGMENT state and we can't change the
  422. // timestamp offset. By calling abort(), we reset the state so we can
  423. // set it.
  424. //
  425. // Note that abort() resets both appendWindowStart and appendWindowEnd;
  426. // however, we don't use appendWindowStart.
  427. this.enqueueOperation_(
  428. contentType,
  429. this.abort_.bind(this, contentType)),
  430. this.enqueueOperation_(
  431. contentType,
  432. this.setTimestampOffset_.bind(this, contentType, timestampOffset)),
  433. this.enqueueOperation_(
  434. contentType,
  435. this.setAppendWindowEnd_.bind(this, contentType, appendWindowEnd))
  436. ]);
  437. };
  438. /**
  439. * @param {string=} opt_reason Valid reasons are 'network' and 'decode'.
  440. * @return {!Promise}
  441. * @see http://w3c.github.io/media-source/#idl-def-EndOfStreamError
  442. */
  443. shaka.media.MediaSourceEngine.prototype.endOfStream = function(opt_reason) {
  444. return this.enqueueBlockingOperation_(function() {
  445. // Chrome won't let me pass undefined, but it will let me omit the
  446. // argument. Firefox does not have this problem.
  447. // TODO: File a bug about this.
  448. if (opt_reason) {
  449. this.mediaSource_.endOfStream(opt_reason);
  450. } else {
  451. this.mediaSource_.endOfStream();
  452. }
  453. }.bind(this));
  454. };
  455. /**
  456. * We only support increasing duration at this time. Decreasing duration
  457. * causes the MSE removal algorithm to run, which results in an 'updateend'
  458. * event. Supporting this scenario would be complicated, and is not currently
  459. * needed.
  460. *
  461. * @param {number} duration
  462. * @return {!Promise}
  463. */
  464. shaka.media.MediaSourceEngine.prototype.setDuration = function(duration) {
  465. goog.asserts.assert(
  466. isNaN(this.mediaSource_.duration) ||
  467. this.mediaSource_.duration <= duration,
  468. 'duration cannot decrease: ' + this.mediaSource_.duration + ' -> ' +
  469. duration);
  470. return this.enqueueBlockingOperation_(function() {
  471. this.mediaSource_.duration = duration;
  472. }.bind(this));
  473. };
  474. /**
  475. * Get the current MediaSource duration.
  476. *
  477. * @return {number}
  478. */
  479. shaka.media.MediaSourceEngine.prototype.getDuration = function() {
  480. return this.mediaSource_.duration;
  481. };
  482. /**
  483. * Append data to the SourceBuffer.
  484. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  485. * @param {!ArrayBuffer} data
  486. * @throws QuotaExceededError if the browser's buffer is full
  487. * @private
  488. */
  489. shaka.media.MediaSourceEngine.prototype.append_ =
  490. function(contentType, data) {
  491. // This will trigger an 'updateend' event.
  492. this.sourceBuffers_[contentType].appendBuffer(data);
  493. };
  494. /**
  495. * Remove data from the SourceBuffer.
  496. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  497. * @param {number} startTime
  498. * @param {number} endTime
  499. * @private
  500. */
  501. shaka.media.MediaSourceEngine.prototype.remove_ =
  502. function(contentType, startTime, endTime) {
  503. if (endTime <= startTime) {
  504. // Ignore removal of inverted or empty ranges.
  505. // Fake 'updateend' event to resolve the operation.
  506. this.onUpdateEnd_(contentType);
  507. return;
  508. }
  509. // This will trigger an 'updateend' event.
  510. this.sourceBuffers_[contentType].remove(startTime, endTime);
  511. };
  512. /**
  513. * Call abort() on the SourceBuffer.
  514. * This resets MSE's last_decode_timestamp on all track buffers, which should
  515. * trigger the splicing logic for overlapping segments.
  516. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  517. * @private
  518. */
  519. shaka.media.MediaSourceEngine.prototype.abort_ = function(contentType) {
  520. // Save the append window end, which is reset on abort().
  521. var appendWindowEnd = this.sourceBuffers_[contentType].appendWindowEnd;
  522. // This will not trigger an 'updateend' event, since nothing is happening.
  523. // This is only to reset MSE internals, not to abort an actual operation.
  524. this.sourceBuffers_[contentType].abort();
  525. // Restore the append window end.
  526. this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd;
  527. // Fake 'updateend' event to resolve the operation.
  528. this.onUpdateEnd_(contentType);
  529. };
  530. /**
  531. * Nudge the playhead to force the media pipeline to be flushed.
  532. * This seems to be necessary on Chromecast to get new content to replace old
  533. * content.
  534. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  535. * @private
  536. */
  537. shaka.media.MediaSourceEngine.prototype.flush_ = function(contentType) {
  538. // Never use flush_ if there's data. It causes a hiccup in playback.
  539. goog.asserts.assert(
  540. this.video_.buffered.length == 0,
  541. 'MediaSourceEngine.flush_ should only be used after clearing all data!');
  542. // Seeking forces the pipeline to be flushed.
  543. this.video_.currentTime -= 0.001;
  544. // Fake 'updateend' event to resolve the operation.
  545. this.onUpdateEnd_(contentType);
  546. };
  547. /**
  548. * Set the SourceBuffer's timestamp offset.
  549. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  550. * @param {number} timestampOffset
  551. * @private
  552. */
  553. shaka.media.MediaSourceEngine.prototype.setTimestampOffset_ =
  554. function(contentType, timestampOffset) {
  555. this.sourceBuffers_[contentType].timestampOffset = timestampOffset;
  556. // Fake 'updateend' event to resolve the operation.
  557. this.onUpdateEnd_(contentType);
  558. };
  559. /**
  560. * Set the SourceBuffer's append window end.
  561. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  562. * @param {number} appendWindowEnd
  563. * @private
  564. */
  565. shaka.media.MediaSourceEngine.prototype.setAppendWindowEnd_ =
  566. function(contentType, appendWindowEnd) {
  567. var fudge = 1 / 25; // one frame, assuming a low framerate
  568. this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd + fudge;
  569. // Fake 'updateend' event to resolve the operation.
  570. this.onUpdateEnd_(contentType);
  571. };
  572. /**
  573. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  574. * @param {!Event} event
  575. * @private
  576. */
  577. shaka.media.MediaSourceEngine.prototype.onError_ =
  578. function(contentType, event) {
  579. var operation = this.queues_[contentType][0];
  580. goog.asserts.assert(operation, 'Spurious error event!');
  581. goog.asserts.assert(!this.sourceBuffers_[contentType].updating,
  582. 'SourceBuffer should not be updating on error!');
  583. var code = this.video_.error ? this.video_.error.code : 0;
  584. operation.p.reject(new shaka.util.Error(
  585. shaka.util.Error.Severity.CRITICAL,
  586. shaka.util.Error.Category.MEDIA,
  587. shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED,
  588. code));
  589. // Do not pop from queue. An 'updateend' event will fire next, and to avoid
  590. // synchronizing these two event handlers, we will allow that one to pop from
  591. // the queue as normal. Note that because the operation has already been
  592. // rejected, the call to resolve() in the 'updateend' handler will have no
  593. // effect.
  594. };
  595. /**
  596. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  597. * @private
  598. */
  599. shaka.media.MediaSourceEngine.prototype.onUpdateEnd_ = function(contentType) {
  600. var operation = this.queues_[contentType][0];
  601. goog.asserts.assert(operation, 'Spurious updateend event!');
  602. if (!operation) return;
  603. goog.asserts.assert(!this.sourceBuffers_[contentType].updating,
  604. 'SourceBuffer should not be updating on updateend!');
  605. operation.p.resolve();
  606. this.popFromQueue_(contentType);
  607. };
  608. /**
  609. * Enqueue an operation and start it if appropriate.
  610. *
  611. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  612. * @param {function()} start
  613. * @return {!Promise}
  614. * @private
  615. */
  616. shaka.media.MediaSourceEngine.prototype.enqueueOperation_ =
  617. function(contentType, start) {
  618. if (this.destroyed_) return Promise.reject();
  619. var operation = {
  620. start: start,
  621. p: new shaka.util.PublicPromise()
  622. };
  623. this.queues_[contentType].push(operation);
  624. if (this.queues_[contentType].length == 1) {
  625. try {
  626. operation.start();
  627. } catch (exception) {
  628. if (exception.name == 'QuotaExceededError') {
  629. operation.p.reject(new shaka.util.Error(
  630. shaka.util.Error.Severity.CRITICAL,
  631. shaka.util.Error.Category.MEDIA,
  632. shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR,
  633. contentType));
  634. } else {
  635. operation.p.reject(new shaka.util.Error(
  636. shaka.util.Error.Severity.CRITICAL,
  637. shaka.util.Error.Category.MEDIA,
  638. shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
  639. exception));
  640. }
  641. this.popFromQueue_(contentType);
  642. }
  643. }
  644. return operation.p;
  645. };
  646. /**
  647. * Enqueue an operation which must block all other operations on all
  648. * SourceBuffers.
  649. *
  650. * @param {function()} run
  651. * @return {!Promise}
  652. * @private
  653. */
  654. shaka.media.MediaSourceEngine.prototype.enqueueBlockingOperation_ =
  655. function(run) {
  656. if (this.destroyed_) return Promise.reject();
  657. var allWaiters = [];
  658. // Enqueue a 'wait' operation onto each queue.
  659. // This operation signals its readiness when it starts.
  660. // When all wait operations are ready, the real operation takes place.
  661. for (var contentType in this.sourceBuffers_) {
  662. var ready = new shaka.util.PublicPromise();
  663. var operation = {
  664. start: function(ready) { ready.resolve(); }.bind(null, ready),
  665. p: ready
  666. };
  667. this.queues_[contentType].push(operation);
  668. allWaiters.push(ready);
  669. if (this.queues_[contentType].length == 1) {
  670. operation.start();
  671. }
  672. }
  673. // Return a Promise to the real operation, which waits to begin until there
  674. // are no other in-progress operations on any SourceBuffers.
  675. return Promise.all(allWaiters).then(function() {
  676. if (!COMPILED) {
  677. // If we did it correctly, nothing is updating.
  678. for (var contentType in this.sourceBuffers_) {
  679. goog.asserts.assert(
  680. this.sourceBuffers_[contentType].updating == false,
  681. 'SourceBuffers should not be updating after a blocking op!');
  682. }
  683. }
  684. var ret;
  685. // Run the real operation, which is synchronous.
  686. try {
  687. run();
  688. } catch (exception) {
  689. ret = Promise.reject(new shaka.util.Error(
  690. shaka.util.Error.Severity.CRITICAL,
  691. shaka.util.Error.Category.MEDIA,
  692. shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
  693. exception));
  694. }
  695. // Unblock the queues.
  696. for (var contentType in this.sourceBuffers_) {
  697. this.popFromQueue_(contentType);
  698. }
  699. return ret;
  700. }.bind(this), function() {
  701. // One of the waiters failed, which means we've been destroyed.
  702. goog.asserts.assert(this.destroyed_, 'Should be destroyed by now');
  703. // We haven't popped from the queue. Canceled waiters have been removed by
  704. // destroy. What's left now should just be resolved waiters. In uncompiled
  705. // mode, we will maintain good hygiene and make sure the assert at the end
  706. // of destroy passes. In compiled mode, the queues are wiped in destroy.
  707. if (!COMPILED) {
  708. for (var contentType in this.sourceBuffers_) {
  709. if (this.queues_[contentType].length) {
  710. goog.asserts.assert(
  711. this.queues_[contentType].length == 1,
  712. 'Should be at most one item in queue!');
  713. goog.asserts.assert(
  714. allWaiters.indexOf(this.queues_[contentType][0].p) != -1,
  715. 'The item in queue should be one of our waiters!');
  716. this.queues_[contentType].shift();
  717. }
  718. }
  719. }
  720. return Promise.reject();
  721. }.bind(this));
  722. };
  723. /**
  724. * Pop from the front of the queue and start a new operation.
  725. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  726. * @private
  727. */
  728. shaka.media.MediaSourceEngine.prototype.popFromQueue_ = function(contentType) {
  729. // Remove the in-progress operation, which is now complete.
  730. this.queues_[contentType].shift();
  731. // Retrieve the next operation, if any, from the queue and start it.
  732. var next = this.queues_[contentType][0];
  733. if (next) {
  734. try {
  735. next.start();
  736. } catch (exception) {
  737. next.p.reject(new shaka.util.Error(
  738. shaka.util.Error.Severity.CRITICAL,
  739. shaka.util.Error.Category.MEDIA,
  740. shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
  741. exception));
  742. this.popFromQueue_(contentType);
  743. }
  744. }
  745. };