Source: lib/media/media_source_engine.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.MediaSourceEngine');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.media.ContentWorkarounds');
  10. goog.require('shaka.media.IClosedCaptionParser');
  11. goog.require('shaka.media.TimeRangesUtils');
  12. goog.require('shaka.media.Transmuxer');
  13. goog.require('shaka.text.TextEngine');
  14. goog.require('shaka.util.Destroyer');
  15. goog.require('shaka.util.Error');
  16. goog.require('shaka.util.EventManager');
  17. goog.require('shaka.util.Functional');
  18. goog.require('shaka.util.IDestroyable');
  19. goog.require('shaka.util.ManifestParserUtils');
  20. goog.require('shaka.util.MimeUtils');
  21. goog.require('shaka.util.Platform');
  22. goog.require('shaka.util.PublicPromise');
  23. /**
  24. * @summary
  25. * MediaSourceEngine wraps all operations on MediaSource and SourceBuffers.
  26. * All asynchronous operations return a Promise, and all operations are
  27. * internally synchronized and serialized as needed. Operations that can
  28. * be done in parallel will be done in parallel.
  29. *
  30. * @implements {shaka.util.IDestroyable}
  31. */
  32. shaka.media.MediaSourceEngine = class {
  33. /**
  34. * @param {HTMLMediaElement} video The video element, whose source is tied to
  35. * MediaSource during the lifetime of the MediaSourceEngine.
  36. * @param {!shaka.media.IClosedCaptionParser} closedCaptionParser
  37. * The closed caption parser that should be used to parser closed captions
  38. * from the video stream. MediaSourceEngine takes ownership of the parser.
  39. * When MediaSourceEngine is destroyed, it will destroy the parser.
  40. * @param {!shaka.extern.TextDisplayer} textDisplayer
  41. * The text displayer that will be used with the text engine.
  42. * MediaSourceEngine takes ownership of the displayer. When
  43. * MediaSourceEngine is destroyed, it will destroy the displayer.
  44. * @param {!function(!Array.<shaka.extern.ID3Metadata>, number, ?number)=}
  45. * onMetadata
  46. */
  47. constructor(video, closedCaptionParser, textDisplayer, onMetadata) {
  48. /** @private {HTMLMediaElement} */
  49. this.video_ = video;
  50. /** @private {shaka.extern.TextDisplayer} */
  51. this.textDisplayer_ = textDisplayer;
  52. /** @private {!Object.<shaka.util.ManifestParserUtils.ContentType,
  53. SourceBuffer>} */
  54. this.sourceBuffers_ = {};
  55. /** @private {!Object.<shaka.util.ManifestParserUtils.ContentType,
  56. string>} */
  57. this.sourceBufferTypes_ = {};
  58. /** @private {!Object.<shaka.util.ManifestParserUtils.ContentType,
  59. boolean>} */
  60. this.expectedEncryption_ = {};
  61. /** @private {shaka.text.TextEngine} */
  62. this.textEngine_ = null;
  63. const onMetadataNoOp = (metadata, timestampOffset, segmentEnd) => {};
  64. /** @private {!function(!Array.<shaka.extern.ID3Metadata>,
  65. number, ?number)} */
  66. this.onMetadata_ = onMetadata || onMetadataNoOp;
  67. /**
  68. * @private {!Object.<string,
  69. * !Array.<shaka.media.MediaSourceEngine.Operation>>}
  70. */
  71. this.queues_ = {};
  72. /** @private {shaka.util.EventManager} */
  73. this.eventManager_ = new shaka.util.EventManager();
  74. /** @private {!Object.<string, !shaka.media.Transmuxer>} */
  75. this.transmuxers_ = {};
  76. /** @private {shaka.media.IClosedCaptionParser} */
  77. this.captionParser_ = closedCaptionParser;
  78. /** @private {!shaka.util.PublicPromise} */
  79. this.mediaSourceOpen_ = new shaka.util.PublicPromise();
  80. /** @private {MediaSource} */
  81. this.mediaSource_ = this.createMediaSource(this.mediaSourceOpen_);
  82. /** @type {!shaka.util.Destroyer} */
  83. this.destroyer_ = new shaka.util.Destroyer(() => this.doDestroy_());
  84. /** @private {string} */
  85. this.url_ = '';
  86. }
  87. /**
  88. * Create a MediaSource object, attach it to the video element, and return it.
  89. * Resolves the given promise when the MediaSource is ready.
  90. *
  91. * Replaced by unit tests.
  92. *
  93. * @param {!shaka.util.PublicPromise} p
  94. * @return {!MediaSource}
  95. */
  96. createMediaSource(p) {
  97. const mediaSource = new MediaSource();
  98. // Set up MediaSource on the video element.
  99. this.eventManager_.listenOnce(
  100. mediaSource, 'sourceopen', () => this.onSourceOpen_(p));
  101. // Store the object URL for releasing it later.
  102. this.url_ = shaka.media.MediaSourceEngine.createObjectURL(mediaSource);
  103. this.video_.src = this.url_;
  104. return mediaSource;
  105. }
  106. /**
  107. * @param {!shaka.util.PublicPromise} p
  108. * @private
  109. */
  110. onSourceOpen_(p) {
  111. // Release the object URL that was previously created, to prevent memory
  112. // leak.
  113. // createObjectURL creates a strong reference to the MediaSource object
  114. // inside the browser. Setting the src of the video then creates another
  115. // reference within the video element. revokeObjectURL will remove the
  116. // strong reference to the MediaSource object, and allow it to be
  117. // garbage-collected later.
  118. URL.revokeObjectURL(this.url_);
  119. p.resolve();
  120. }
  121. /**
  122. * Checks if a certain type is supported.
  123. *
  124. * @param {shaka.extern.Stream} stream
  125. * @return {boolean}
  126. */
  127. static isStreamSupported(stream) {
  128. const fullMimeType = shaka.util.MimeUtils.getFullType(
  129. stream.mimeType, stream.codecs);
  130. const extendedMimeType = shaka.util.MimeUtils.getExtendedType(stream);
  131. return shaka.text.TextEngine.isTypeSupported(fullMimeType) ||
  132. MediaSource.isTypeSupported(extendedMimeType) ||
  133. shaka.media.Transmuxer.isSupported(fullMimeType, stream.type);
  134. }
  135. /**
  136. * Returns a map of MediaSource support for well-known types.
  137. *
  138. * @return {!Object.<string, boolean>}
  139. */
  140. static probeSupport() {
  141. const testMimeTypes = [
  142. // MP4 types
  143. 'video/mp4; codecs="avc1.42E01E"',
  144. 'video/mp4; codecs="avc3.42E01E"',
  145. 'video/mp4; codecs="hev1.1.6.L93.90"',
  146. 'video/mp4; codecs="hvc1.1.6.L93.90"',
  147. 'video/mp4; codecs="hev1.2.4.L153.B0"; eotf="smpte2084"', // HDR HEVC
  148. 'video/mp4; codecs="hvc1.2.4.L153.B0"; eotf="smpte2084"', // HDR HEVC
  149. 'video/mp4; codecs="vp9"',
  150. 'video/mp4; codecs="vp09.00.10.08"',
  151. 'video/mp4; codecs="av01.0.01M.08"',
  152. 'audio/mp4; codecs="mp4a.40.2"',
  153. 'audio/mp4; codecs="ac-3"',
  154. 'audio/mp4; codecs="ec-3"',
  155. 'audio/mp4; codecs="opus"',
  156. 'audio/mp4; codecs="flac"',
  157. 'audio/mp4; codecs="dtsc"', // DTS Digital Surround
  158. 'audio/mp4; codecs="dtse"', // DTS Express
  159. 'audio/mp4; codecs="dtsx"', // DTS:X
  160. // WebM types
  161. 'video/webm; codecs="vp8"',
  162. 'video/webm; codecs="vp9"',
  163. 'video/webm; codecs="vp09.00.10.08"',
  164. 'audio/webm; codecs="vorbis"',
  165. 'audio/webm; codecs="opus"',
  166. // MPEG2 TS types (video/ is also used for audio: https://bit.ly/TsMse)
  167. 'video/mp2t; codecs="avc1.42E01E"',
  168. 'video/mp2t; codecs="avc3.42E01E"',
  169. 'video/mp2t; codecs="hvc1.1.6.L93.90"',
  170. 'video/mp2t; codecs="mp4a.40.2"',
  171. 'video/mp2t; codecs="ac-3"',
  172. 'video/mp2t; codecs="ec-3"',
  173. // WebVTT types
  174. 'text/vtt',
  175. 'application/mp4; codecs="wvtt"',
  176. // TTML types
  177. 'application/ttml+xml',
  178. 'application/mp4; codecs="stpp"',
  179. ];
  180. const support = {};
  181. for (const type of testMimeTypes) {
  182. if (shaka.util.Platform.supportsMediaSource()) {
  183. // Our TextEngine is only effective for MSE platforms at the moment.
  184. if (shaka.text.TextEngine.isTypeSupported(type)) {
  185. support[type] = true;
  186. } else {
  187. support[type] = MediaSource.isTypeSupported(type) ||
  188. shaka.media.Transmuxer.isSupported(type);
  189. }
  190. } else {
  191. support[type] = shaka.util.Platform.supportsMediaType(type);
  192. }
  193. const basicType = type.split(';')[0];
  194. support[basicType] = support[basicType] || support[type];
  195. }
  196. return support;
  197. }
  198. /** @override */
  199. destroy() {
  200. return this.destroyer_.destroy();
  201. }
  202. /** @private */
  203. async doDestroy_() {
  204. const Functional = shaka.util.Functional;
  205. const cleanup = [];
  206. for (const contentType in this.queues_) {
  207. // Make a local copy of the queue and the first item.
  208. const q = this.queues_[contentType];
  209. const inProgress = q[0];
  210. // Drop everything else out of the original queue.
  211. this.queues_[contentType] = q.slice(0, 1);
  212. // We will wait for this item to complete/fail.
  213. if (inProgress) {
  214. cleanup.push(inProgress.p.catch(Functional.noop));
  215. }
  216. // The rest will be rejected silently if possible.
  217. for (const item of q.slice(1)) {
  218. item.p.reject(shaka.util.Destroyer.destroyedError());
  219. }
  220. }
  221. if (this.textEngine_) {
  222. cleanup.push(this.textEngine_.destroy());
  223. }
  224. if (this.textDisplayer_) {
  225. cleanup.push(this.textDisplayer_.destroy());
  226. }
  227. for (const contentType in this.transmuxers_) {
  228. cleanup.push(this.transmuxers_[contentType].destroy());
  229. }
  230. await Promise.all(cleanup);
  231. if (this.eventManager_) {
  232. this.eventManager_.release();
  233. this.eventManager_ = null;
  234. }
  235. if (this.video_) {
  236. // "unload" the video element.
  237. this.video_.removeAttribute('src');
  238. this.video_.load();
  239. this.video_ = null;
  240. }
  241. this.mediaSource_ = null;
  242. this.textEngine_ = null;
  243. this.textDisplayer_ = null;
  244. this.sourceBuffers_ = {};
  245. this.transmuxers_ = {};
  246. this.captionParser_ = null;
  247. if (goog.DEBUG) {
  248. for (const contentType in this.queues_) {
  249. goog.asserts.assert(
  250. this.queues_[contentType].length == 0,
  251. contentType + ' queue should be empty after destroy!');
  252. }
  253. }
  254. this.queues_ = {};
  255. }
  256. /**
  257. * @return {!Promise} Resolved when MediaSource is open and attached to the
  258. * media element. This process is actually initiated by the constructor.
  259. */
  260. open() {
  261. return this.mediaSourceOpen_;
  262. }
  263. /**
  264. * Initialize MediaSourceEngine.
  265. *
  266. * Note that it is not valid to call this multiple times, except to add or
  267. * reinitialize text streams.
  268. *
  269. * @param {!Map.<shaka.util.ManifestParserUtils.ContentType,
  270. * shaka.extern.Stream>} streamsByType
  271. * A map of content types to streams. All streams must be supported
  272. * according to MediaSourceEngine.isStreamSupported.
  273. * @param {boolean} forceTransmuxTS
  274. * If true, this will transmux TS content even if it is natively supported.
  275. *
  276. * @return {!Promise}
  277. */
  278. async init(streamsByType, forceTransmuxTS) {
  279. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  280. await this.mediaSourceOpen_;
  281. for (const contentType of streamsByType.keys()) {
  282. const stream = streamsByType.get(contentType);
  283. goog.asserts.assert(
  284. shaka.media.MediaSourceEngine.isStreamSupported(stream),
  285. 'Type negotiation should happen before MediaSourceEngine.init!');
  286. let mimeType = shaka.util.MimeUtils.getFullType(
  287. stream.mimeType, stream.codecs);
  288. if (contentType == ContentType.TEXT) {
  289. this.reinitText(mimeType);
  290. } else {
  291. if ((forceTransmuxTS || !MediaSource.isTypeSupported(mimeType)) &&
  292. shaka.media.Transmuxer.isSupported(mimeType, contentType)) {
  293. this.transmuxers_[contentType] = new shaka.media.Transmuxer();
  294. mimeType =
  295. shaka.media.Transmuxer.convertTsCodecs(contentType, mimeType);
  296. }
  297. const sourceBuffer = this.mediaSource_.addSourceBuffer(mimeType);
  298. this.eventManager_.listen(
  299. sourceBuffer, 'error',
  300. () => this.onError_(contentType));
  301. this.eventManager_.listen(
  302. sourceBuffer, 'updateend',
  303. () => this.onUpdateEnd_(contentType));
  304. this.sourceBuffers_[contentType] = sourceBuffer;
  305. this.sourceBufferTypes_[contentType] = mimeType;
  306. this.queues_[contentType] = [];
  307. this.expectedEncryption_[contentType] = !!stream.drmInfos.length;
  308. }
  309. }
  310. }
  311. /**
  312. * Reinitialize the TextEngine for a new text type.
  313. * @param {string} mimeType
  314. */
  315. reinitText(mimeType) {
  316. if (!this.textEngine_) {
  317. this.textEngine_ = new shaka.text.TextEngine(this.textDisplayer_);
  318. }
  319. this.textEngine_.initParser(mimeType);
  320. }
  321. /**
  322. * @return {boolean} True if the MediaSource is in an "ended" state, or if the
  323. * object has been destroyed.
  324. */
  325. ended() {
  326. return this.mediaSource_ ? this.mediaSource_.readyState == 'ended' : true;
  327. }
  328. /**
  329. * Gets the first timestamp in buffer for the given content type.
  330. *
  331. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  332. * @return {?number} The timestamp in seconds, or null if nothing is buffered.
  333. */
  334. bufferStart(contentType) {
  335. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  336. if (contentType == ContentType.TEXT) {
  337. return this.textEngine_.bufferStart();
  338. }
  339. return shaka.media.TimeRangesUtils.bufferStart(
  340. this.getBuffered_(contentType));
  341. }
  342. /**
  343. * Gets the last timestamp in buffer for the given content type.
  344. *
  345. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  346. * @return {?number} The timestamp in seconds, or null if nothing is buffered.
  347. */
  348. bufferEnd(contentType) {
  349. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  350. if (contentType == ContentType.TEXT) {
  351. return this.textEngine_.bufferEnd();
  352. }
  353. return shaka.media.TimeRangesUtils.bufferEnd(
  354. this.getBuffered_(contentType));
  355. }
  356. /**
  357. * Determines if the given time is inside the buffered range of the given
  358. * content type.
  359. *
  360. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  361. * @param {number} time Playhead time
  362. * @param {number=} smallGapLimit
  363. * @return {boolean}
  364. */
  365. isBuffered(contentType, time, smallGapLimit) {
  366. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  367. if (contentType == ContentType.TEXT) {
  368. return this.textEngine_.isBuffered(time);
  369. } else {
  370. const buffered = this.getBuffered_(contentType);
  371. return shaka.media.TimeRangesUtils.isBuffered(
  372. buffered, time, smallGapLimit);
  373. }
  374. }
  375. /**
  376. * Computes how far ahead of the given timestamp is buffered for the given
  377. * content type.
  378. *
  379. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  380. * @param {number} time
  381. * @return {number} The amount of time buffered ahead in seconds.
  382. */
  383. bufferedAheadOf(contentType, time) {
  384. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  385. if (contentType == ContentType.TEXT) {
  386. return this.textEngine_.bufferedAheadOf(time);
  387. } else {
  388. const buffered = this.getBuffered_(contentType);
  389. return shaka.media.TimeRangesUtils.bufferedAheadOf(buffered, time);
  390. }
  391. }
  392. /**
  393. * Returns info about what is currently buffered.
  394. * @return {shaka.extern.BufferedInfo}
  395. */
  396. getBufferedInfo() {
  397. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  398. const TimeRangeUtils = shaka.media.TimeRangesUtils;
  399. const info = {
  400. total: TimeRangeUtils.getBufferedInfo(this.video_.buffered),
  401. audio: TimeRangeUtils.getBufferedInfo(
  402. this.getBuffered_(ContentType.AUDIO)),
  403. video: TimeRangeUtils.getBufferedInfo(
  404. this.getBuffered_(ContentType.VIDEO)),
  405. text: [],
  406. };
  407. if (this.textEngine_) {
  408. const start = this.textEngine_.bufferStart();
  409. const end = this.textEngine_.bufferEnd();
  410. if (start != null && end != null) {
  411. info.text.push({start: start, end: end});
  412. }
  413. }
  414. return info;
  415. }
  416. /**
  417. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  418. * @return {TimeRanges} The buffered ranges for the given content type, or
  419. * null if the buffered ranges could not be obtained.
  420. * @private
  421. */
  422. getBuffered_(contentType) {
  423. try {
  424. return this.sourceBuffers_[contentType].buffered;
  425. } catch (exception) {
  426. if (contentType in this.sourceBuffers_) {
  427. // Note: previous MediaSource errors may cause access to |buffered| to
  428. // throw.
  429. shaka.log.error('failed to get buffered range for ' + contentType,
  430. exception);
  431. }
  432. return null;
  433. }
  434. }
  435. /**
  436. * Enqueue an operation to append data to the SourceBuffer.
  437. * Start and end times are needed for TextEngine, but not for MediaSource.
  438. * Start and end times may be null for initialization segments; if present
  439. * they are relative to the presentation timeline.
  440. *
  441. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  442. * @param {!BufferSource} data
  443. * @param {?number} startTime relative to the start of the presentation
  444. * @param {?number} endTime relative to the start of the presentation
  445. * @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed
  446. * captions
  447. * @return {!Promise}
  448. */
  449. async appendBuffer(contentType, data, startTime, endTime, hasClosedCaptions) {
  450. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  451. if (contentType == ContentType.TEXT) {
  452. await this.textEngine_.appendBuffer(data, startTime, endTime);
  453. } else if (this.transmuxers_[contentType]) {
  454. const transmuxedData =
  455. await this.transmuxers_[contentType].transmux(data);
  456. // For HLS CEA-608/708 CLOSED-CAPTIONS, text data is embedded in
  457. // the video stream, so textEngine may not have been initialized.
  458. if (!this.textEngine_) {
  459. this.reinitText(shaka.util.MimeUtils.CEA608_CLOSED_CAPTION_MIMETYPE);
  460. }
  461. if (transmuxedData.metadata) {
  462. const timestampOffset =
  463. this.sourceBuffers_[contentType].timestampOffset;
  464. this.onMetadata_(transmuxedData.metadata, timestampOffset, endTime);
  465. }
  466. // This doesn't work for native TS support (ex. Edge/Chromecast),
  467. // since no transmuxing is needed for native TS.
  468. if (transmuxedData.captions && transmuxedData.captions.length) {
  469. const videoOffset =
  470. this.sourceBuffers_[ContentType.VIDEO].timestampOffset;
  471. const closedCaptions = this.textEngine_
  472. .convertMuxjsCaptionsToShakaCaptions(transmuxedData.captions);
  473. this.textEngine_.storeAndAppendClosedCaptions(
  474. closedCaptions, startTime, endTime, videoOffset);
  475. }
  476. let transmuxedSegment = transmuxedData.data;
  477. transmuxedSegment = this.workAroundBrokenPlatforms_(
  478. transmuxedSegment, startTime, contentType);
  479. await this.enqueueOperation_(
  480. contentType, () => this.append_(contentType, transmuxedSegment));
  481. } else if (hasClosedCaptions) {
  482. if (!this.textEngine_) {
  483. this.reinitText(shaka.util.MimeUtils.CEA608_CLOSED_CAPTION_MIMETYPE);
  484. }
  485. // If it is the init segment for closed captions, initialize the closed
  486. // caption parser.
  487. if (startTime == null && endTime == null) {
  488. this.captionParser_.init(data);
  489. } else {
  490. const closedCaptions = this.captionParser_.parseFrom(data);
  491. if (closedCaptions.length) {
  492. const videoOffset =
  493. this.sourceBuffers_[ContentType.VIDEO].timestampOffset;
  494. this.textEngine_.storeAndAppendClosedCaptions(
  495. closedCaptions, startTime, endTime, videoOffset);
  496. }
  497. }
  498. data = this.workAroundBrokenPlatforms_(data, startTime, contentType);
  499. await this.enqueueOperation_(
  500. contentType,
  501. () => this.append_(contentType, data));
  502. } else {
  503. data = this.workAroundBrokenPlatforms_(data, startTime, contentType);
  504. await this.enqueueOperation_(
  505. contentType,
  506. () => this.append_(contentType, data));
  507. }
  508. }
  509. /**
  510. * Set the selected closed captions Id and language.
  511. *
  512. * @param {string} id
  513. */
  514. setSelectedClosedCaptionId(id) {
  515. const VIDEO = shaka.util.ManifestParserUtils.ContentType.VIDEO;
  516. const videoBufferEndTime = this.bufferEnd(VIDEO) || 0;
  517. this.textEngine_.setSelectedClosedCaptionId(id, videoBufferEndTime);
  518. }
  519. /** Disable embedded closed captions. */
  520. clearSelectedClosedCaptionId() {
  521. if (this.textEngine_) {
  522. this.textEngine_.setSelectedClosedCaptionId('', 0);
  523. }
  524. }
  525. /**
  526. * Enqueue an operation to remove data from the SourceBuffer.
  527. *
  528. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  529. * @param {number} startTime relative to the start of the presentation
  530. * @param {number} endTime relative to the start of the presentation
  531. * @return {!Promise}
  532. */
  533. async remove(contentType, startTime, endTime) {
  534. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  535. if (contentType == ContentType.TEXT) {
  536. await this.textEngine_.remove(startTime, endTime);
  537. } else {
  538. await this.enqueueOperation_(
  539. contentType,
  540. () => this.remove_(contentType, startTime, endTime));
  541. }
  542. }
  543. /**
  544. * Enqueue an operation to clear the SourceBuffer.
  545. *
  546. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  547. * @return {!Promise}
  548. */
  549. async clear(contentType) {
  550. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  551. if (contentType == ContentType.TEXT) {
  552. if (!this.textEngine_) {
  553. return;
  554. }
  555. await this.textEngine_.remove(0, Infinity);
  556. } else {
  557. // Note that not all platforms allow clearing to Infinity.
  558. await this.enqueueOperation_(
  559. contentType,
  560. () => this.remove_(contentType, 0, this.mediaSource_.duration));
  561. }
  562. }
  563. /**
  564. * Fully reset the state of the caption parser owned by MediaSourceEngine.
  565. */
  566. resetCaptionParser() {
  567. this.captionParser_.reset();
  568. }
  569. /**
  570. * Enqueue an operation to flush the SourceBuffer.
  571. * This is a workaround for what we believe is a Chromecast bug.
  572. *
  573. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  574. * @return {!Promise}
  575. */
  576. async flush(contentType) {
  577. // Flush the pipeline. Necessary on Chromecast, even though we have removed
  578. // everything.
  579. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  580. if (contentType == ContentType.TEXT) {
  581. // Nothing to flush for text.
  582. return;
  583. }
  584. await this.enqueueOperation_(
  585. contentType,
  586. () => this.flush_(contentType));
  587. }
  588. /**
  589. * Sets the timestamp offset and append window end for the given content type.
  590. *
  591. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  592. * @param {number} timestampOffset The timestamp offset. Segments which start
  593. * at time t will be inserted at time t + timestampOffset instead. This
  594. * value does not affect segments which have already been inserted.
  595. * @param {number} appendWindowStart The timestamp to set the append window
  596. * start to. For future appends, frames/samples with timestamps less than
  597. * this value will be dropped.
  598. * @param {number} appendWindowEnd The timestamp to set the append window end
  599. * to. For future appends, frames/samples with timestamps greater than this
  600. * value will be dropped.
  601. * @return {!Promise}
  602. */
  603. async setStreamProperties(
  604. contentType, timestampOffset, appendWindowStart, appendWindowEnd) {
  605. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  606. if (contentType == ContentType.TEXT) {
  607. this.textEngine_.setTimestampOffset(timestampOffset);
  608. this.textEngine_.setAppendWindow(appendWindowStart, appendWindowEnd);
  609. return;
  610. }
  611. await Promise.all([
  612. // Queue an abort() to help MSE splice together overlapping segments.
  613. // We set appendWindowEnd when we change periods in DASH content, and the
  614. // period transition may result in overlap.
  615. //
  616. // An abort() also helps with MPEG2-TS. When we append a TS segment, we
  617. // always enter a PARSING_MEDIA_SEGMENT state and we can't change the
  618. // timestamp offset. By calling abort(), we reset the state so we can
  619. // set it.
  620. this.enqueueOperation_(
  621. contentType,
  622. () => this.abort_(contentType)),
  623. this.enqueueOperation_(
  624. contentType,
  625. () => this.setTimestampOffset_(contentType, timestampOffset)),
  626. this.enqueueOperation_(
  627. contentType,
  628. () => this.setAppendWindow_(
  629. contentType, appendWindowStart, appendWindowEnd)),
  630. ]);
  631. }
  632. /**
  633. * @param {string=} reason Valid reasons are 'network' and 'decode'.
  634. * @return {!Promise}
  635. * @see http://w3c.github.io/media-source/#idl-def-EndOfStreamError
  636. */
  637. async endOfStream(reason) {
  638. await this.enqueueBlockingOperation_(() => {
  639. // If endOfStream() has already been called on the media source,
  640. // don't call it again. Also do not call if readyState is
  641. // 'closed' (not attached to video element) since it is not a
  642. // valid operation.
  643. if (this.ended() || this.mediaSource_.readyState === 'closed') {
  644. return;
  645. }
  646. // Tizen won't let us pass undefined, but it will let us omit the
  647. // argument.
  648. if (reason) {
  649. this.mediaSource_.endOfStream(reason);
  650. } else {
  651. this.mediaSource_.endOfStream();
  652. }
  653. });
  654. }
  655. /**
  656. * We only support increasing duration at this time. Decreasing duration
  657. * causes the MSE removal algorithm to run, which results in an 'updateend'
  658. * event. Supporting this scenario would be complicated, and is not currently
  659. * needed.
  660. *
  661. * @param {number} duration
  662. * @return {!Promise}
  663. */
  664. async setDuration(duration) {
  665. goog.asserts.assert(
  666. isNaN(this.mediaSource_.duration) ||
  667. this.mediaSource_.duration <= duration,
  668. 'duration cannot decrease: ' + this.mediaSource_.duration + ' -> ' +
  669. duration);
  670. await this.enqueueBlockingOperation_(() => {
  671. this.mediaSource_.duration = duration;
  672. });
  673. }
  674. /**
  675. * Get the current MediaSource duration.
  676. *
  677. * @return {number}
  678. */
  679. getDuration() {
  680. return this.mediaSource_.duration;
  681. }
  682. /**
  683. * Append data to the SourceBuffer.
  684. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  685. * @param {BufferSource} data
  686. * @private
  687. */
  688. append_(contentType, data) {
  689. // This will trigger an 'updateend' event.
  690. this.sourceBuffers_[contentType].appendBuffer(data);
  691. }
  692. /**
  693. * Remove data from the SourceBuffer.
  694. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  695. * @param {number} startTime relative to the start of the presentation
  696. * @param {number} endTime relative to the start of the presentation
  697. * @private
  698. */
  699. remove_(contentType, startTime, endTime) {
  700. if (endTime <= startTime) {
  701. // Ignore removal of inverted or empty ranges.
  702. // Fake 'updateend' event to resolve the operation.
  703. this.onUpdateEnd_(contentType);
  704. return;
  705. }
  706. // This will trigger an 'updateend' event.
  707. this.sourceBuffers_[contentType].remove(startTime, endTime);
  708. }
  709. /**
  710. * Call abort() on the SourceBuffer.
  711. * This resets MSE's last_decode_timestamp on all track buffers, which should
  712. * trigger the splicing logic for overlapping segments.
  713. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  714. * @private
  715. */
  716. abort_(contentType) {
  717. // Save the append window, which is reset on abort().
  718. const appendWindowStart =
  719. this.sourceBuffers_[contentType].appendWindowStart;
  720. const appendWindowEnd = this.sourceBuffers_[contentType].appendWindowEnd;
  721. // This will not trigger an 'updateend' event, since nothing is happening.
  722. // This is only to reset MSE internals, not to abort an actual operation.
  723. this.sourceBuffers_[contentType].abort();
  724. // Restore the append window.
  725. this.sourceBuffers_[contentType].appendWindowStart = appendWindowStart;
  726. this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd;
  727. // Fake an 'updateend' event to resolve the operation.
  728. this.onUpdateEnd_(contentType);
  729. }
  730. /**
  731. * Nudge the playhead to force the media pipeline to be flushed.
  732. * This seems to be necessary on Chromecast to get new content to replace old
  733. * content.
  734. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  735. * @private
  736. */
  737. flush_(contentType) {
  738. // Never use flush_ if there's data. It causes a hiccup in playback.
  739. goog.asserts.assert(
  740. this.video_.buffered.length == 0, 'MediaSourceEngine.flush_ should ' +
  741. 'only be used after clearing all data!');
  742. // Seeking forces the pipeline to be flushed.
  743. this.video_.currentTime -= 0.001;
  744. // Fake an 'updateend' event to resolve the operation.
  745. this.onUpdateEnd_(contentType);
  746. }
  747. /**
  748. * Set the SourceBuffer's timestamp offset.
  749. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  750. * @param {number} timestampOffset
  751. * @private
  752. */
  753. setTimestampOffset_(contentType, timestampOffset) {
  754. // Work around for
  755. // https://github.com/shaka-project/shaka-player/issues/1281:
  756. // TODO(https://bit.ly/2ttKiBU): follow up when this is fixed in Edge
  757. if (timestampOffset < 0) {
  758. // Try to prevent rounding errors in Edge from removing the first
  759. // keyframe.
  760. timestampOffset += 0.001;
  761. }
  762. this.sourceBuffers_[contentType].timestampOffset = timestampOffset;
  763. // Fake an 'updateend' event to resolve the operation.
  764. this.onUpdateEnd_(contentType);
  765. }
  766. /**
  767. * Set the SourceBuffer's append window end.
  768. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  769. * @param {number} appendWindowStart
  770. * @param {number} appendWindowEnd
  771. * @private
  772. */
  773. setAppendWindow_(contentType, appendWindowStart, appendWindowEnd) {
  774. // You can't set start > end, so first set start to 0, then set the new
  775. // end, then set the new start. That way, there are no intermediate
  776. // states which are invalid.
  777. this.sourceBuffers_[contentType].appendWindowStart = 0;
  778. this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd;
  779. this.sourceBuffers_[contentType].appendWindowStart = appendWindowStart;
  780. // Fake an 'updateend' event to resolve the operation.
  781. this.onUpdateEnd_(contentType);
  782. }
  783. /**
  784. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  785. * @private
  786. */
  787. onError_(contentType) {
  788. const operation = this.queues_[contentType][0];
  789. goog.asserts.assert(operation, 'Spurious error event!');
  790. goog.asserts.assert(!this.sourceBuffers_[contentType].updating,
  791. 'SourceBuffer should not be updating on error!');
  792. const code = this.video_.error ? this.video_.error.code : 0;
  793. operation.p.reject(new shaka.util.Error(
  794. shaka.util.Error.Severity.CRITICAL,
  795. shaka.util.Error.Category.MEDIA,
  796. shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED,
  797. code));
  798. // Do not pop from queue. An 'updateend' event will fire next, and to
  799. // avoid synchronizing these two event handlers, we will allow that one to
  800. // pop from the queue as normal. Note that because the operation has
  801. // already been rejected, the call to resolve() in the 'updateend' handler
  802. // will have no effect.
  803. }
  804. /**
  805. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  806. * @private
  807. */
  808. onUpdateEnd_(contentType) {
  809. const operation = this.queues_[contentType][0];
  810. goog.asserts.assert(operation, 'Spurious updateend event!');
  811. if (!operation) {
  812. return;
  813. }
  814. goog.asserts.assert(!this.sourceBuffers_[contentType].updating,
  815. 'SourceBuffer should not be updating on updateend!');
  816. operation.p.resolve();
  817. this.popFromQueue_(contentType);
  818. }
  819. /**
  820. * Enqueue an operation and start it if appropriate.
  821. *
  822. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  823. * @param {function()} start
  824. * @return {!Promise}
  825. * @private
  826. */
  827. enqueueOperation_(contentType, start) {
  828. this.destroyer_.ensureNotDestroyed();
  829. const operation = {
  830. start: start,
  831. p: new shaka.util.PublicPromise(),
  832. };
  833. this.queues_[contentType].push(operation);
  834. if (this.queues_[contentType].length == 1) {
  835. this.startOperation_(contentType);
  836. }
  837. return operation.p;
  838. }
  839. /**
  840. * Enqueue an operation which must block all other operations on all
  841. * SourceBuffers.
  842. *
  843. * @param {function()} run
  844. * @return {!Promise}
  845. * @private
  846. */
  847. async enqueueBlockingOperation_(run) {
  848. this.destroyer_.ensureNotDestroyed();
  849. /** @type {!Array.<!shaka.util.PublicPromise>} */
  850. const allWaiters = [];
  851. // Enqueue a 'wait' operation onto each queue.
  852. // This operation signals its readiness when it starts.
  853. // When all wait operations are ready, the real operation takes place.
  854. for (const contentType in this.sourceBuffers_) {
  855. const ready = new shaka.util.PublicPromise();
  856. const operation = {
  857. start: () => ready.resolve(),
  858. p: ready,
  859. };
  860. this.queues_[contentType].push(operation);
  861. allWaiters.push(ready);
  862. if (this.queues_[contentType].length == 1) {
  863. operation.start();
  864. }
  865. }
  866. // Return a Promise to the real operation, which waits to begin until
  867. // there are no other in-progress operations on any SourceBuffers.
  868. try {
  869. await Promise.all(allWaiters);
  870. } catch (error) {
  871. // One of the waiters failed, which means we've been destroyed.
  872. goog.asserts.assert(
  873. this.destroyer_.destroyed(), 'Should be destroyed by now');
  874. // We haven't popped from the queue. Canceled waiters have been removed
  875. // by destroy. What's left now should just be resolved waiters. In
  876. // uncompiled mode, we will maintain good hygiene and make sure the
  877. // assert at the end of destroy passes. In compiled mode, the queues
  878. // are wiped in destroy.
  879. if (goog.DEBUG) {
  880. for (const contentType in this.sourceBuffers_) {
  881. if (this.queues_[contentType].length) {
  882. goog.asserts.assert(
  883. this.queues_[contentType].length == 1,
  884. 'Should be at most one item in queue!');
  885. goog.asserts.assert(
  886. allWaiters.includes(this.queues_[contentType][0].p),
  887. 'The item in queue should be one of our waiters!');
  888. this.queues_[contentType].shift();
  889. }
  890. }
  891. }
  892. throw error;
  893. }
  894. if (goog.DEBUG) {
  895. // If we did it correctly, nothing is updating.
  896. for (const contentType in this.sourceBuffers_) {
  897. goog.asserts.assert(
  898. this.sourceBuffers_[contentType].updating == false,
  899. 'SourceBuffers should not be updating after a blocking op!');
  900. }
  901. }
  902. // Run the real operation, which is synchronous.
  903. try {
  904. run();
  905. } catch (exception) {
  906. throw new shaka.util.Error(
  907. shaka.util.Error.Severity.CRITICAL,
  908. shaka.util.Error.Category.MEDIA,
  909. shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
  910. exception);
  911. } finally {
  912. // Unblock the queues.
  913. for (const contentType in this.sourceBuffers_) {
  914. this.popFromQueue_(contentType);
  915. }
  916. }
  917. }
  918. /**
  919. * Pop from the front of the queue and start a new operation.
  920. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  921. * @private
  922. */
  923. popFromQueue_(contentType) {
  924. // Remove the in-progress operation, which is now complete.
  925. this.queues_[contentType].shift();
  926. this.startOperation_(contentType);
  927. }
  928. /**
  929. * Starts the next operation in the queue.
  930. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  931. * @private
  932. */
  933. startOperation_(contentType) {
  934. // Retrieve the next operation, if any, from the queue and start it.
  935. const next = this.queues_[contentType][0];
  936. if (next) {
  937. try {
  938. next.start();
  939. } catch (exception) {
  940. if (exception.name == 'QuotaExceededError') {
  941. next.p.reject(new shaka.util.Error(
  942. shaka.util.Error.Severity.CRITICAL,
  943. shaka.util.Error.Category.MEDIA,
  944. shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR,
  945. contentType));
  946. } else {
  947. next.p.reject(new shaka.util.Error(
  948. shaka.util.Error.Severity.CRITICAL,
  949. shaka.util.Error.Category.MEDIA,
  950. shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
  951. exception));
  952. }
  953. this.popFromQueue_(contentType);
  954. }
  955. }
  956. }
  957. /**
  958. * @return {!shaka.extern.TextDisplayer}
  959. */
  960. getTextDisplayer() {
  961. goog.asserts.assert(
  962. this.textDisplayer_,
  963. 'TextDisplayer should only be null when this is destroyed');
  964. return this.textDisplayer_;
  965. }
  966. /**
  967. * @param {!shaka.extern.TextDisplayer} textDisplayer
  968. */
  969. setTextDisplayer(textDisplayer) {
  970. const oldTextDisplayer = this.textDisplayer_;
  971. this.textDisplayer_ = textDisplayer;
  972. if (oldTextDisplayer) {
  973. textDisplayer.setTextVisibility(oldTextDisplayer.isTextVisible());
  974. oldTextDisplayer.destroy();
  975. }
  976. if (this.textEngine_) {
  977. this.textEngine_.setDisplayer(textDisplayer);
  978. }
  979. }
  980. /**
  981. * Apply platform-specific transformations to this segment to work around
  982. * issues in the platform.
  983. *
  984. * @param {!BufferSource} segment
  985. * @param {?number} startTime
  986. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  987. * @return {!BufferSource}
  988. * @private
  989. */
  990. workAroundBrokenPlatforms_(segment, startTime, contentType) {
  991. const isInitSegment = startTime == null;
  992. const encryptionExpected = this.expectedEncryption_[contentType];
  993. // If:
  994. // 1. this is an init segment,
  995. // 2. and encryption is expected,
  996. // 3. and the platform requires encryption in all init segments,
  997. // 4. and the content is MP4 (mimeType == "video/mp4" or "audio/mp4"),
  998. // then insert fake encryption metadata for init segments that lack it.
  999. // The MP4 requirement is because we can currently only do this
  1000. // transformation on MP4 containers.
  1001. // See: https://github.com/shaka-project/shaka-player/issues/2759
  1002. if (isInitSegment &&
  1003. encryptionExpected &&
  1004. shaka.util.Platform.requiresEncryptionInfoInAllInitSegments() &&
  1005. shaka.util.MimeUtils.getContainerType(
  1006. this.sourceBufferTypes_[contentType]) == 'mp4') {
  1007. shaka.log.debug('Forcing fake encryption information in init segment.');
  1008. segment = shaka.media.ContentWorkarounds.fakeEncryption(segment);
  1009. }
  1010. return segment;
  1011. }
  1012. };
  1013. /**
  1014. * Internal reference to window.URL.createObjectURL function to avoid
  1015. * compatibility issues with other libraries and frameworks such as React
  1016. * Native. For use in unit tests only, not meant for external use.
  1017. *
  1018. * @type {function(?):string}
  1019. */
  1020. shaka.media.MediaSourceEngine.createObjectURL = window.URL.createObjectURL;
  1021. /**
  1022. * @typedef {{
  1023. * start: function(),
  1024. * p: !shaka.util.PublicPromise
  1025. * }}
  1026. *
  1027. * @summary An operation in queue.
  1028. * @property {function()} start
  1029. * The function which starts the operation.
  1030. * @property {!shaka.util.PublicPromise} p
  1031. * The PublicPromise which is associated with this operation.
  1032. */
  1033. shaka.media.MediaSourceEngine.Operation;