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