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