Source: lib/media/streaming_engine.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. /**
  7. * @fileoverview
  8. * @suppress {missingRequire} TODO(b/152540451): this shouldn't be needed
  9. */
  10. goog.provide('shaka.media.StreamingEngine');
  11. goog.require('goog.asserts');
  12. goog.require('shaka.log');
  13. goog.require('shaka.media.InitSegmentReference');
  14. goog.require('shaka.media.MediaSourceEngine');
  15. goog.require('shaka.media.SegmentIterator');
  16. goog.require('shaka.media.SegmentReference');
  17. goog.require('shaka.net.Backoff');
  18. goog.require('shaka.net.NetworkingEngine');
  19. goog.require('shaka.util.DelayedTick');
  20. goog.require('shaka.util.Destroyer');
  21. goog.require('shaka.util.Error');
  22. goog.require('shaka.util.FakeEvent');
  23. goog.require('shaka.util.IDestroyable');
  24. goog.require('shaka.util.ManifestParserUtils');
  25. goog.require('shaka.util.MimeUtils');
  26. goog.require('shaka.util.Mp4Parser');
  27. goog.require('shaka.util.Networking');
  28. /**
  29. * @summary Creates a Streaming Engine.
  30. * The StreamingEngine is responsible for setting up the Manifest's Streams
  31. * (i.e., for calling each Stream's createSegmentIndex() function), for
  32. * downloading segments, for co-ordinating audio, video, and text buffering.
  33. * The StreamingEngine provides an interface to switch between Streams, but it
  34. * does not choose which Streams to switch to.
  35. *
  36. * The StreamingEngine does not need to be notified about changes to the
  37. * Manifest's SegmentIndexes; however, it does need to be notified when new
  38. * Variants are added to the Manifest.
  39. *
  40. * To start the StreamingEngine the owner must first call configure(), followed
  41. * by one call to switchVariant(), one optional call to switchTextStream(), and
  42. * finally a call to start(). After start() resolves, switch*() can be used
  43. * freely.
  44. *
  45. * The owner must call seeked() each time the playhead moves to a new location
  46. * within the presentation timeline; however, the owner may forego calling
  47. * seeked() when the playhead moves outside the presentation timeline.
  48. *
  49. * @implements {shaka.util.IDestroyable}
  50. */
  51. shaka.media.StreamingEngine = class {
  52. /**
  53. * @param {shaka.extern.Manifest} manifest
  54. * @param {shaka.media.StreamingEngine.PlayerInterface} playerInterface
  55. */
  56. constructor(manifest, playerInterface) {
  57. /** @private {?shaka.media.StreamingEngine.PlayerInterface} */
  58. this.playerInterface_ = playerInterface;
  59. /** @private {?shaka.extern.Manifest} */
  60. this.manifest_ = manifest;
  61. /** @private {?shaka.extern.StreamingConfiguration} */
  62. this.config_ = null;
  63. /** @private {number} */
  64. this.bufferingGoalScale_ = 1;
  65. /** @private {?shaka.extern.Variant} */
  66. this.currentVariant_ = null;
  67. /** @private {?shaka.extern.Stream} */
  68. this.currentTextStream_ = null;
  69. /**
  70. * Maps a content type, e.g., 'audio', 'video', or 'text', to a MediaState.
  71. *
  72. * @private {!Map.<shaka.util.ManifestParserUtils.ContentType,
  73. * !shaka.media.StreamingEngine.MediaState_>}
  74. */
  75. this.mediaStates_ = new Map();
  76. /**
  77. * Set to true once the initial media states have been created.
  78. *
  79. * @private {boolean}
  80. */
  81. this.startupComplete_ = false;
  82. /**
  83. * Used for delay and backoff of failure callbacks, so that apps do not
  84. * retry instantly.
  85. *
  86. * @private {shaka.net.Backoff}
  87. */
  88. this.failureCallbackBackoff_ = null;
  89. /**
  90. * Set to true on fatal error. Interrupts fetchAndAppend_().
  91. *
  92. * @private {boolean}
  93. */
  94. this.fatalError_ = false;
  95. /** @private {!shaka.util.Destroyer} */
  96. this.destroyer_ = new shaka.util.Destroyer(() => this.doDestroy_());
  97. }
  98. /** @override */
  99. destroy() {
  100. return this.destroyer_.destroy();
  101. }
  102. /**
  103. * @return {!Promise}
  104. * @private
  105. */
  106. async doDestroy_() {
  107. const aborts = [];
  108. for (const state of this.mediaStates_.values()) {
  109. this.cancelUpdate_(state);
  110. aborts.push(this.abortOperations_(state));
  111. }
  112. await Promise.all(aborts);
  113. this.mediaStates_.clear();
  114. this.playerInterface_ = null;
  115. this.manifest_ = null;
  116. this.config_ = null;
  117. }
  118. /**
  119. * Called by the Player to provide an updated configuration any time it
  120. * changes. Must be called at least once before start().
  121. *
  122. * @param {shaka.extern.StreamingConfiguration} config
  123. */
  124. configure(config) {
  125. this.config_ = config;
  126. // Create separate parameters for backoff during streaming failure.
  127. /** @type {shaka.extern.RetryParameters} */
  128. const failureRetryParams = {
  129. // The term "attempts" includes the initial attempt, plus all retries.
  130. // In order to see a delay, there would have to be at least 2 attempts.
  131. maxAttempts: Math.max(config.retryParameters.maxAttempts, 2),
  132. baseDelay: config.retryParameters.baseDelay,
  133. backoffFactor: config.retryParameters.backoffFactor,
  134. fuzzFactor: config.retryParameters.fuzzFactor,
  135. timeout: 0, // irrelevant
  136. stallTimeout: 0, // irrelevant
  137. connectionTimeout: 0, // irrelevant
  138. };
  139. // We don't want to ever run out of attempts. The application should be
  140. // allowed to retry streaming infinitely if it wishes.
  141. const autoReset = true;
  142. this.failureCallbackBackoff_ =
  143. new shaka.net.Backoff(failureRetryParams, autoReset);
  144. }
  145. /**
  146. * Initialize and start streaming.
  147. *
  148. * By calling this method, StreamingEngine will start streaming the variant
  149. * chosen by a prior call to switchVariant(), and optionally, the text stream
  150. * chosen by a prior call to switchTextStream(). Once the Promise resolves,
  151. * switch*() may be called freely.
  152. *
  153. * @return {!Promise}
  154. */
  155. async start() {
  156. goog.asserts.assert(this.config_,
  157. 'StreamingEngine configure() must be called before init()!');
  158. // Setup the initial set of Streams and then begin each update cycle.
  159. await this.initStreams_();
  160. this.destroyer_.ensureNotDestroyed();
  161. shaka.log.debug('init: completed initial Stream setup');
  162. this.startupComplete_ = true;
  163. }
  164. /**
  165. * Get the current variant we are streaming. Returns null if nothing is
  166. * streaming.
  167. * @return {?shaka.extern.Variant}
  168. */
  169. getCurrentVariant() {
  170. return this.currentVariant_;
  171. }
  172. /**
  173. * Get the text stream we are streaming. Returns null if there is no text
  174. * streaming.
  175. * @return {?shaka.extern.Stream}
  176. */
  177. getCurrentTextStream() {
  178. return this.currentTextStream_;
  179. }
  180. /**
  181. * Start streaming text, creating a new media state.
  182. *
  183. * @param {shaka.extern.Stream} stream
  184. * @return {!Promise}
  185. * @private
  186. */
  187. async loadNewTextStream_(stream) {
  188. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  189. goog.asserts.assert(!this.mediaStates_.has(ContentType.TEXT),
  190. 'Should not call loadNewTextStream_ while streaming text!');
  191. try {
  192. // Clear MediaSource's buffered text, so that the new text stream will
  193. // properly replace the old buffered text.
  194. // TODO: Should this happen in unloadTextStream() instead?
  195. await this.playerInterface_.mediaSourceEngine.clear(ContentType.TEXT);
  196. } catch (error) {
  197. if (this.playerInterface_) {
  198. this.playerInterface_.onError(error);
  199. }
  200. }
  201. const mimeType = shaka.util.MimeUtils.getFullType(
  202. stream.mimeType, stream.codecs);
  203. this.playerInterface_.mediaSourceEngine.reinitText(mimeType);
  204. const textDisplayer =
  205. this.playerInterface_.mediaSourceEngine.getTextDisplayer();
  206. const streamText =
  207. textDisplayer.isTextVisible() || this.config_.alwaysStreamText;
  208. if (streamText) {
  209. const state = this.createMediaState_(stream);
  210. this.mediaStates_.set(ContentType.TEXT, state);
  211. this.scheduleUpdate_(state, 0);
  212. }
  213. }
  214. /**
  215. * Stop fetching text stream when the user chooses to hide the captions.
  216. */
  217. unloadTextStream() {
  218. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  219. const state = this.mediaStates_.get(ContentType.TEXT);
  220. if (state) {
  221. this.cancelUpdate_(state);
  222. this.abortOperations_(state).catch(() => {});
  223. this.mediaStates_.delete(ContentType.TEXT);
  224. }
  225. this.currentTextStream_ = null;
  226. }
  227. /**
  228. * Set trick play on or off.
  229. * If trick play is on, related trick play streams will be used when possible.
  230. * @param {boolean} on
  231. */
  232. setTrickPlay(on) {
  233. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  234. const mediaState = this.mediaStates_.get(ContentType.VIDEO);
  235. if (!mediaState) {
  236. return;
  237. }
  238. const stream = mediaState.stream;
  239. if (!stream) {
  240. return;
  241. }
  242. shaka.log.debug('setTrickPlay', on);
  243. if (on) {
  244. const trickModeVideo = stream.trickModeVideo;
  245. if (!trickModeVideo) {
  246. return; // Can't engage trick play.
  247. }
  248. const normalVideo = mediaState.restoreStreamAfterTrickPlay;
  249. if (normalVideo) {
  250. return; // Already in trick play.
  251. }
  252. shaka.log.debug('Engaging trick mode stream', trickModeVideo);
  253. this.switchInternal_(trickModeVideo, /* clearBuffer= */ false,
  254. /* safeMargin= */ 0, /* force= */ false);
  255. mediaState.restoreStreamAfterTrickPlay = stream;
  256. } else {
  257. const normalVideo = mediaState.restoreStreamAfterTrickPlay;
  258. if (!normalVideo) {
  259. return;
  260. }
  261. shaka.log.debug('Restoring non-trick-mode stream', normalVideo);
  262. mediaState.restoreStreamAfterTrickPlay = null;
  263. this.switchInternal_(normalVideo, /* clearBuffer= */ true,
  264. /* safeMargin= */ 0, /* force= */ false);
  265. }
  266. }
  267. /**
  268. * @param {shaka.extern.Variant} variant
  269. * @param {boolean=} clearBuffer
  270. * @param {number=} safeMargin
  271. * @param {boolean=} force
  272. * If true, reload the variant even if it did not change.
  273. */
  274. switchVariant(variant, clearBuffer = false, safeMargin = 0, force = false) {
  275. this.currentVariant_ = variant;
  276. if (!this.startupComplete_) {
  277. // The selected variant will be used in start().
  278. return;
  279. }
  280. if (variant.video) {
  281. this.switchInternal_(
  282. variant.video, /* clearBuffer= */ clearBuffer,
  283. /* safeMargin= */ safeMargin, /* force= */ force);
  284. }
  285. if (variant.audio) {
  286. this.switchInternal_(
  287. variant.audio, /* clearBuffer= */ clearBuffer,
  288. /* safeMargin= */ safeMargin, /* force= */ force);
  289. }
  290. }
  291. /**
  292. * @param {shaka.extern.Stream} textStream
  293. */
  294. switchTextStream(textStream) {
  295. this.currentTextStream_ = textStream;
  296. if (!this.startupComplete_) {
  297. // The selected text stream will be used in start().
  298. return;
  299. }
  300. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  301. goog.asserts.assert(textStream && textStream.type == ContentType.TEXT,
  302. 'Wrong stream type passed to switchTextStream!');
  303. this.switchInternal_(
  304. textStream, /* clearBuffer= */ true,
  305. /* safeMargin= */ 0, /* force= */ false);
  306. }
  307. /** Reload the current text stream. */
  308. reloadTextStream() {
  309. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  310. const mediaState = this.mediaStates_.get(ContentType.TEXT);
  311. if (mediaState) { // Don't reload if there's no text to begin with.
  312. this.switchInternal_(
  313. mediaState.stream, /* clearBuffer= */ true,
  314. /* safeMargin= */ 0, /* force= */ true);
  315. }
  316. }
  317. /**
  318. * Switches to the given Stream. |stream| may be from any Variant.
  319. *
  320. * @param {shaka.extern.Stream} stream
  321. * @param {boolean} clearBuffer
  322. * @param {number} safeMargin
  323. * @param {boolean} force
  324. * If true, reload the text stream even if it did not change.
  325. * @private
  326. */
  327. switchInternal_(stream, clearBuffer, safeMargin, force) {
  328. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  329. const type = /** @type {!ContentType} */(stream.type);
  330. const mediaState = this.mediaStates_.get(type);
  331. if (!mediaState && stream.type == ContentType.TEXT) {
  332. this.loadNewTextStream_(stream);
  333. return;
  334. }
  335. goog.asserts.assert(mediaState, 'switch: expected mediaState to exist');
  336. if (!mediaState) {
  337. return;
  338. }
  339. if (mediaState.restoreStreamAfterTrickPlay) {
  340. shaka.log.debug('switch during trick play mode', stream);
  341. // Already in trick play mode, so stick with trick mode tracks if
  342. // possible.
  343. if (stream.trickModeVideo) {
  344. // Use the trick mode stream, but revert to the new selection later.
  345. mediaState.restoreStreamAfterTrickPlay = stream;
  346. stream = stream.trickModeVideo;
  347. shaka.log.debug('switch found trick play stream', stream);
  348. } else {
  349. // There is no special trick mode video for this stream!
  350. mediaState.restoreStreamAfterTrickPlay = null;
  351. shaka.log.debug('switch found no special trick play stream');
  352. }
  353. }
  354. if (mediaState.stream == stream && !force) {
  355. const streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
  356. shaka.log.debug('switch: Stream ' + streamTag + ' already active');
  357. return;
  358. }
  359. if (stream.type == ContentType.TEXT) {
  360. // Mime types are allowed to change for text streams.
  361. // Reinitialize the text parser, but only if we are going to fetch the
  362. // init segment again.
  363. const fullMimeType = shaka.util.MimeUtils.getFullType(
  364. stream.mimeType, stream.codecs);
  365. this.playerInterface_.mediaSourceEngine.reinitText(fullMimeType);
  366. }
  367. // Releases the segmentIndex of the old stream.
  368. if (mediaState.stream.closeSegmentIndex) {
  369. mediaState.stream.closeSegmentIndex();
  370. }
  371. mediaState.stream = stream;
  372. mediaState.segmentIterator = null;
  373. const streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
  374. shaka.log.debug('switch: switching to Stream ' + streamTag);
  375. if (clearBuffer) {
  376. if (mediaState.clearingBuffer) {
  377. // We are already going to clear the buffer, but make sure it is also
  378. // flushed.
  379. mediaState.waitingToFlushBuffer = true;
  380. } else if (mediaState.performingUpdate) {
  381. // We are performing an update, so we have to wait until it's finished.
  382. // onUpdate_() will call clearBuffer_() when the update has finished.
  383. // We need to save the safe margin because its value will be needed when
  384. // clearing the buffer after the update.
  385. mediaState.waitingToClearBuffer = true;
  386. mediaState.clearBufferSafeMargin = safeMargin;
  387. mediaState.waitingToFlushBuffer = true;
  388. } else {
  389. // Cancel the update timer, if any.
  390. this.cancelUpdate_(mediaState);
  391. // Clear right away.
  392. this.clearBuffer_(mediaState, /* flush= */ true, safeMargin)
  393. .catch((error) => {
  394. if (this.playerInterface_) {
  395. goog.asserts.assert(error instanceof shaka.util.Error,
  396. 'Wrong error type!');
  397. this.playerInterface_.onError(error);
  398. }
  399. });
  400. }
  401. }
  402. this.makeAbortDecision_(mediaState).catch((error) => {
  403. if (this.playerInterface_) {
  404. goog.asserts.assert(error instanceof shaka.util.Error,
  405. 'Wrong error type!');
  406. this.playerInterface_.onError(error);
  407. }
  408. });
  409. }
  410. /**
  411. * Decide if it makes sense to abort the current operation, and abort it if
  412. * so.
  413. *
  414. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  415. * @private
  416. */
  417. async makeAbortDecision_(mediaState) {
  418. // If the operation is completed, it will be set to null, and there's no
  419. // need to abort the request.
  420. if (!mediaState.operation) {
  421. return;
  422. }
  423. const originalStream = mediaState.stream;
  424. const originalOperation = mediaState.operation;
  425. if (!originalStream.segmentIndex) {
  426. // Create the new segment index so the time taken is accounted for when
  427. // deciding whether to abort.
  428. await originalStream.createSegmentIndex();
  429. }
  430. if (mediaState.operation != originalOperation) {
  431. // The original operation completed while we were getting a segment index,
  432. // so there's nothing to do now.
  433. return;
  434. }
  435. if (mediaState.stream != originalStream) {
  436. // The stream changed again while we were getting a segment index. We
  437. // can't carry out this check, since another one might be in progress by
  438. // now.
  439. return;
  440. }
  441. goog.asserts.assert(mediaState.stream.segmentIndex,
  442. 'Segment index should exist by now!');
  443. if (this.shouldAbortCurrentRequest_(mediaState)) {
  444. shaka.log.info('Aborting current segment request.');
  445. mediaState.operation.abort();
  446. }
  447. }
  448. /**
  449. * Returns whether we should abort the current request.
  450. *
  451. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  452. * @return {boolean}
  453. * @private
  454. */
  455. shouldAbortCurrentRequest_(mediaState) {
  456. goog.asserts.assert(mediaState.operation,
  457. 'Abort logic requires an ongoing operation!');
  458. goog.asserts.assert(mediaState.stream && mediaState.stream.segmentIndex,
  459. 'Abort logic requires a segment index');
  460. const presentationTime = this.playerInterface_.getPresentationTime();
  461. const bufferEnd =
  462. this.playerInterface_.mediaSourceEngine.bufferEnd(mediaState.type);
  463. // The next segment to append from the current stream. This doesn't
  464. // account for a pending network request and will likely be different from
  465. // that since we just switched.
  466. const timeNeeded = this.getTimeNeeded_(mediaState, presentationTime);
  467. const index = mediaState.stream.segmentIndex.find(timeNeeded);
  468. const newSegment =
  469. index == null ? null : mediaState.stream.segmentIndex.get(index);
  470. let newSegmentSize = newSegment ? newSegment.getSize() : null;
  471. if (newSegment && !newSegmentSize) {
  472. // compute approximate segment size using stream bandwidth
  473. const duration = newSegment.getEndTime() - newSegment.getStartTime();
  474. const bandwidth = mediaState.stream.bandwidth || 0;
  475. // bandwidth is in bits per second, and the size is in bytes
  476. newSegmentSize = duration * bandwidth / 8;
  477. }
  478. if (!newSegmentSize) {
  479. return false;
  480. }
  481. // When switching, we'll need to download the init segment.
  482. const init = newSegment.initSegmentReference;
  483. if (init) {
  484. newSegmentSize += init.getSize() || 0;
  485. }
  486. const bandwidthEstimate = this.playerInterface_.getBandwidthEstimate();
  487. // The estimate is in bits per second, and the size is in bytes. The time
  488. // remaining is in seconds after this calculation.
  489. const timeToFetchNewSegment = (newSegmentSize * 8) / bandwidthEstimate;
  490. // If the new segment can be finished in time without risking a buffer
  491. // underflow, we should abort the old one and switch.
  492. const bufferedAhead = (bufferEnd || 0) - presentationTime;
  493. const safetyBuffer = Math.max(
  494. this.manifest_.minBufferTime || 0,
  495. this.config_.rebufferingGoal);
  496. const safeBufferedAhead = bufferedAhead - safetyBuffer;
  497. if (timeToFetchNewSegment < safeBufferedAhead) {
  498. return true;
  499. }
  500. // If the thing we want to switch to will be done more quickly than what
  501. // we've got in progress, we should abort the old one and switch.
  502. const bytesRemaining = mediaState.operation.getBytesRemaining();
  503. if (bytesRemaining > newSegmentSize) {
  504. return true;
  505. }
  506. // Otherwise, complete the operation in progress.
  507. return false;
  508. }
  509. /**
  510. * Notifies the StreamingEngine that the playhead has moved to a valid time
  511. * within the presentation timeline.
  512. */
  513. seeked() {
  514. const presentationTime = this.playerInterface_.getPresentationTime();
  515. const smallGapLimit = this.config_.smallGapLimit;
  516. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  517. const newTimeIsBuffered = (type) => {
  518. return this.playerInterface_.mediaSourceEngine.isBuffered(
  519. type, presentationTime, smallGapLimit);
  520. };
  521. let streamCleared = false;
  522. for (const type of this.mediaStates_.keys()) {
  523. const mediaState = this.mediaStates_.get(type);
  524. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  525. // Always clear the iterator since we need to start streaming from the
  526. // new time. This also happens in clearBuffer_, but if we don't clear,
  527. // we still want to reset the iterator.
  528. mediaState.segmentIterator = null;
  529. if (!newTimeIsBuffered(type)) {
  530. const bufferEnd =
  531. this.playerInterface_.mediaSourceEngine.bufferEnd(type);
  532. const somethingBuffered = bufferEnd != null;
  533. // Don't clear the buffer unless something is buffered. This extra
  534. // check prevents extra, useless calls to clear the buffer.
  535. if (somethingBuffered || mediaState.performingUpdate) {
  536. this.forceClearBuffer_(mediaState);
  537. streamCleared = true;
  538. }
  539. // If there is an operation in progress, stop it now.
  540. if (mediaState.operation) {
  541. mediaState.operation.abort();
  542. shaka.log.debug(logPrefix, 'Aborting operation due to seek');
  543. mediaState.operation = null;
  544. }
  545. // The pts has shifted from the seek, invalidating captions currently
  546. // in the text buffer. Thus, clear and reset the caption parser.
  547. if (type === ContentType.TEXT) {
  548. this.playerInterface_.mediaSourceEngine.resetCaptionParser();
  549. }
  550. }
  551. }
  552. if (!streamCleared) {
  553. shaka.log.debug(
  554. '(all): seeked: buffered seek: presentationTime=' + presentationTime);
  555. }
  556. }
  557. /**
  558. * Clear the buffer for a given stream. Unlike clearBuffer_, this will handle
  559. * cases where a MediaState is performing an update. After this runs, every
  560. * MediaState will have a pending update.
  561. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  562. * @private
  563. */
  564. forceClearBuffer_(mediaState) {
  565. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  566. if (mediaState.clearingBuffer) {
  567. // We're already clearing the buffer, so we don't need to clear the
  568. // buffer again.
  569. shaka.log.debug(logPrefix, 'clear: already clearing the buffer');
  570. return;
  571. }
  572. if (mediaState.waitingToClearBuffer) {
  573. // May not be performing an update, but an update will still happen.
  574. // See: https://github.com/shaka-project/shaka-player/issues/334
  575. shaka.log.debug(logPrefix, 'clear: already waiting');
  576. return;
  577. }
  578. if (mediaState.performingUpdate) {
  579. // We are performing an update, so we have to wait until it's finished.
  580. // onUpdate_() will call clearBuffer_() when the update has finished.
  581. shaka.log.debug(logPrefix, 'clear: currently updating');
  582. mediaState.waitingToClearBuffer = true;
  583. // We can set the offset to zero to remember that this was a call to
  584. // clearAllBuffers.
  585. mediaState.clearBufferSafeMargin = 0;
  586. return;
  587. }
  588. const type = mediaState.type;
  589. if (this.playerInterface_.mediaSourceEngine.bufferStart(type) == null) {
  590. // Nothing buffered.
  591. shaka.log.debug(logPrefix, 'clear: nothing buffered');
  592. if (mediaState.updateTimer == null) {
  593. // Note: an update cycle stops when we buffer to the end of the
  594. // presentation, or when we raise an error.
  595. this.scheduleUpdate_(mediaState, 0);
  596. }
  597. return;
  598. }
  599. // An update may be scheduled, but we can just cancel it and clear the
  600. // buffer right away. Note: clearBuffer_() will schedule the next update.
  601. shaka.log.debug(logPrefix, 'clear: handling right now');
  602. this.cancelUpdate_(mediaState);
  603. this.clearBuffer_(mediaState, /* flush= */ false, 0).catch((error) => {
  604. if (this.playerInterface_) {
  605. goog.asserts.assert(error instanceof shaka.util.Error,
  606. 'Wrong error type!');
  607. this.playerInterface_.onError(error);
  608. }
  609. });
  610. }
  611. /**
  612. * Initializes the initial streams and media states. This will schedule
  613. * updates for the given types.
  614. *
  615. * @return {!Promise}
  616. * @private
  617. */
  618. async initStreams_() {
  619. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  620. goog.asserts.assert(this.config_,
  621. 'StreamingEngine configure() must be called before init()!');
  622. if (!this.currentVariant_) {
  623. shaka.log.error('init: no Streams chosen');
  624. throw new shaka.util.Error(
  625. shaka.util.Error.Severity.CRITICAL,
  626. shaka.util.Error.Category.STREAMING,
  627. shaka.util.Error.Code.STREAMING_ENGINE_STARTUP_INVALID_STATE);
  628. }
  629. /**
  630. * @type {!Map.<shaka.util.ManifestParserUtils.ContentType,
  631. * shaka.extern.Stream>}
  632. */
  633. const streamsByType = new Map();
  634. /** @type {!Set.<shaka.extern.Stream>} */
  635. const streams = new Set();
  636. if (this.currentVariant_.audio) {
  637. streamsByType.set(ContentType.AUDIO, this.currentVariant_.audio);
  638. streams.add(this.currentVariant_.audio);
  639. }
  640. if (this.currentVariant_.video) {
  641. streamsByType.set(ContentType.VIDEO, this.currentVariant_.video);
  642. streams.add(this.currentVariant_.video);
  643. }
  644. if (this.currentTextStream_) {
  645. streamsByType.set(ContentType.TEXT, this.currentTextStream_);
  646. streams.add(this.currentTextStream_);
  647. }
  648. // Init MediaSourceEngine.
  649. const mediaSourceEngine = this.playerInterface_.mediaSourceEngine;
  650. const forceTransmuxTS = this.config_.forceTransmuxTS;
  651. await mediaSourceEngine.init(streamsByType, forceTransmuxTS);
  652. this.destroyer_.ensureNotDestroyed();
  653. this.setDuration_();
  654. for (const type of streamsByType.keys()) {
  655. const stream = streamsByType.get(type);
  656. if (!this.mediaStates_.has(type)) {
  657. const state = this.createMediaState_(stream);
  658. this.mediaStates_.set(type, state);
  659. this.scheduleUpdate_(state, 0);
  660. }
  661. }
  662. }
  663. /**
  664. * Creates a media state.
  665. *
  666. * @param {shaka.extern.Stream} stream
  667. * @return {shaka.media.StreamingEngine.MediaState_}
  668. * @private
  669. */
  670. createMediaState_(stream) {
  671. return /** @type {shaka.media.StreamingEngine.MediaState_} */ ({
  672. stream,
  673. type: stream.type,
  674. segmentIterator: null,
  675. lastSegmentReference: null,
  676. lastInitSegmentReference: null,
  677. lastTimestampOffset: null,
  678. lastAppendWindowStart: null,
  679. lastAppendWindowEnd: null,
  680. restoreStreamAfterTrickPlay: null,
  681. endOfStream: false,
  682. performingUpdate: false,
  683. updateTimer: null,
  684. waitingToClearBuffer: false,
  685. clearBufferSafeMargin: 0,
  686. waitingToFlushBuffer: false,
  687. clearingBuffer: false,
  688. recovering: false,
  689. hasError: false,
  690. operation: null,
  691. });
  692. }
  693. /**
  694. * Sets the MediaSource's duration.
  695. * @private
  696. */
  697. setDuration_() {
  698. const duration = this.manifest_.presentationTimeline.getDuration();
  699. if (duration < Infinity) {
  700. this.playerInterface_.mediaSourceEngine.setDuration(duration);
  701. } else {
  702. // Not all platforms support infinite durations, so set a finite duration
  703. // so we can append segments and so the user agent can seek.
  704. this.playerInterface_.mediaSourceEngine.setDuration(Math.pow(2, 32));
  705. }
  706. }
  707. /**
  708. * Called when |mediaState|'s update timer has expired.
  709. *
  710. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  711. * @suppress {suspiciousCode} The compiler assumes that updateTimer can't
  712. * change during the await, and so complains about the null check.
  713. * @private
  714. */
  715. async onUpdate_(mediaState) {
  716. this.destroyer_.ensureNotDestroyed();
  717. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  718. // Sanity check.
  719. goog.asserts.assert(
  720. !mediaState.performingUpdate && (mediaState.updateTimer != null),
  721. logPrefix + ' unexpected call to onUpdate_()');
  722. if (mediaState.performingUpdate || (mediaState.updateTimer == null)) {
  723. return;
  724. }
  725. goog.asserts.assert(
  726. !mediaState.clearingBuffer, logPrefix +
  727. ' onUpdate_() should not be called when clearing the buffer');
  728. if (mediaState.clearingBuffer) {
  729. return;
  730. }
  731. mediaState.updateTimer = null;
  732. // Handle pending buffer clears.
  733. if (mediaState.waitingToClearBuffer) {
  734. // Note: clearBuffer_() will schedule the next update.
  735. shaka.log.debug(logPrefix, 'skipping update and clearing the buffer');
  736. await this.clearBuffer_(
  737. mediaState, mediaState.waitingToFlushBuffer,
  738. mediaState.clearBufferSafeMargin);
  739. return;
  740. }
  741. // Make sure the segment index exists. If not, create the segment index.
  742. if (!mediaState.stream.segmentIndex) {
  743. const thisStream = mediaState.stream;
  744. await mediaState.stream.createSegmentIndex();
  745. if (thisStream != mediaState.stream) {
  746. // We switched streams while in the middle of this async call to
  747. // createSegmentIndex. Abandon this update and schedule a new one if
  748. // there's not already one pending.
  749. // Releases the segmentIndex of the old stream.
  750. if (thisStream.closeSegmentIndex) {
  751. goog.asserts.assert(!mediaState.stream.segmentIndex,
  752. 'mediastate.stream should not have segmentIndex yet.');
  753. thisStream.closeSegmentIndex();
  754. }
  755. if (mediaState.updateTimer == null) {
  756. this.scheduleUpdate_(mediaState, 0);
  757. }
  758. return;
  759. }
  760. }
  761. // Update the MediaState.
  762. try {
  763. const delay = this.update_(mediaState);
  764. if (delay != null) {
  765. this.scheduleUpdate_(mediaState, delay);
  766. mediaState.hasError = false;
  767. }
  768. } catch (error) {
  769. await this.handleStreamingError_(error);
  770. return;
  771. }
  772. const mediaStates = Array.from(this.mediaStates_.values());
  773. // Check if we've buffered to the end of the presentation. We delay adding
  774. // the audio and video media states, so it is possible for the text stream
  775. // to be the only state and buffer to the end. So we need to wait until we
  776. // have completed startup to determine if we have reached the end.
  777. if (this.startupComplete_ &&
  778. mediaStates.every((ms) => ms.endOfStream)) {
  779. shaka.log.v1(logPrefix, 'calling endOfStream()...');
  780. await this.playerInterface_.mediaSourceEngine.endOfStream();
  781. this.destroyer_.ensureNotDestroyed();
  782. // If the media segments don't reach the end, then we need to update the
  783. // timeline duration to match the final media duration to avoid
  784. // buffering forever at the end.
  785. // We should only do this if the duration needs to shrink.
  786. // Growing it by less than 1ms can actually cause buffering on
  787. // replay, as in https://github.com/shaka-project/shaka-player/issues/979
  788. // On some platforms, this can spuriously be 0, so ignore this case.
  789. // https://github.com/shaka-project/shaka-player/issues/1967,
  790. const duration = this.playerInterface_.mediaSourceEngine.getDuration();
  791. if (duration != 0 &&
  792. duration < this.manifest_.presentationTimeline.getDuration()) {
  793. this.manifest_.presentationTimeline.setDuration(duration);
  794. }
  795. }
  796. }
  797. /**
  798. * Updates the given MediaState.
  799. *
  800. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  801. * @return {?number} The number of seconds to wait until updating again or
  802. * null if another update does not need to be scheduled.
  803. * @private
  804. */
  805. update_(mediaState) {
  806. goog.asserts.assert(this.manifest_, 'manifest_ should not be null');
  807. goog.asserts.assert(this.config_, 'config_ should not be null');
  808. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  809. // Do not schedule update for closed captions text mediastate, since closed
  810. // captions are embedded in video streams.
  811. if (shaka.media.StreamingEngine.isEmbeddedText_(mediaState)) {
  812. this.playerInterface_.mediaSourceEngine.setSelectedClosedCaptionId(
  813. mediaState.stream.originalId || '');
  814. return null;
  815. } else if (mediaState.type == ContentType.TEXT) {
  816. // Disable embedded captions if not desired (e.g. if transitioning from
  817. // embedded to not-embedded captions).
  818. this.playerInterface_.mediaSourceEngine.clearSelectedClosedCaptionId();
  819. }
  820. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  821. // Compute how far we've buffered ahead of the playhead.
  822. const presentationTime = this.playerInterface_.getPresentationTime();
  823. // Get the next timestamp we need.
  824. const timeNeeded = this.getTimeNeeded_(mediaState, presentationTime);
  825. shaka.log.v2(logPrefix, 'timeNeeded=' + timeNeeded);
  826. // Get the amount of content we have buffered, accounting for drift. This
  827. // is only used to determine if we have meet the buffering goal. This
  828. // should be the same method that PlayheadObserver uses.
  829. const bufferedAhead =
  830. this.playerInterface_.mediaSourceEngine.bufferedAheadOf(
  831. mediaState.type, presentationTime);
  832. shaka.log.v2(logPrefix,
  833. 'update_:',
  834. 'presentationTime=' + presentationTime,
  835. 'bufferedAhead=' + bufferedAhead);
  836. const unscaledBufferingGoal = Math.max(
  837. this.manifest_.minBufferTime || 0,
  838. this.config_.rebufferingGoal,
  839. this.config_.bufferingGoal);
  840. const scaledBufferingGoal =
  841. unscaledBufferingGoal * this.bufferingGoalScale_;
  842. // Check if we've buffered to the end of the presentation.
  843. const timeUntilEnd =
  844. this.manifest_.presentationTimeline.getDuration() - timeNeeded;
  845. const oneMicrosecond = 1e-6;
  846. if (timeUntilEnd < oneMicrosecond) {
  847. // We shouldn't rebuffer if the playhead is close to the end of the
  848. // presentation.
  849. shaka.log.debug(logPrefix, 'buffered to end of presentation');
  850. mediaState.endOfStream = true;
  851. if (mediaState.type == ContentType.VIDEO) {
  852. // Since the text stream of CEA closed captions doesn't have update
  853. // timer, we have to set the text endOfStream based on the video
  854. // stream's endOfStream state.
  855. const textState = this.mediaStates_.get(ContentType.TEXT);
  856. if (textState &&
  857. shaka.media.StreamingEngine.isEmbeddedText_(textState)) {
  858. textState.endOfStream = true;
  859. }
  860. }
  861. return null;
  862. }
  863. mediaState.endOfStream = false;
  864. // If we've buffered to the buffering goal then schedule an update.
  865. if (bufferedAhead >= scaledBufferingGoal) {
  866. shaka.log.v2(logPrefix, 'buffering goal met');
  867. // Do not try to predict the next update. Just poll according to
  868. // configuration (seconds). The playback rate can change at any time, so
  869. // any prediction we make now could be terribly invalid soon.
  870. return this.config_.updateIntervalSeconds / 2;
  871. }
  872. const bufferEnd =
  873. this.playerInterface_.mediaSourceEngine.bufferEnd(mediaState.type);
  874. const reference = this.getSegmentReferenceNeeded_(
  875. mediaState, presentationTime, bufferEnd);
  876. if (!reference) {
  877. // The segment could not be found, does not exist, or is not available.
  878. // In any case just try again... if the manifest is incomplete or is not
  879. // being updated then we'll idle forever; otherwise, we'll end up getting
  880. // a SegmentReference eventually.
  881. return this.config_.updateIntervalSeconds;
  882. }
  883. // Do not let any one stream get far ahead of any other.
  884. let minTimeNeeded = Infinity;
  885. const mediaStates = Array.from(this.mediaStates_.values());
  886. for (const otherState of mediaStates) {
  887. // Do not consider embedded captions in this calculation. It could lead
  888. // to hangs in streaming.
  889. if (shaka.media.StreamingEngine.isEmbeddedText_(otherState)) {
  890. continue;
  891. }
  892. // If there is no next segment, ignore this stream. This happens with
  893. // text when there's a Period with no text in it.
  894. if (otherState.segmentIterator && !otherState.segmentIterator.current()) {
  895. continue;
  896. }
  897. const timeNeeded = this.getTimeNeeded_(otherState, presentationTime);
  898. minTimeNeeded = Math.min(minTimeNeeded, timeNeeded);
  899. }
  900. const maxSegmentDuration =
  901. this.manifest_.presentationTimeline.getMaxSegmentDuration();
  902. const maxRunAhead = maxSegmentDuration *
  903. shaka.media.StreamingEngine.MAX_RUN_AHEAD_SEGMENTS_;
  904. if (timeNeeded >= minTimeNeeded + maxRunAhead) {
  905. // Wait and give other media types time to catch up to this one.
  906. // For example, let video buffering catch up to audio buffering before
  907. // fetching another audio segment.
  908. shaka.log.v2(logPrefix, 'waiting for other streams to buffer');
  909. return this.config_.updateIntervalSeconds;
  910. }
  911. const p = this.fetchAndAppend_(mediaState, presentationTime, reference);
  912. p.catch(() => {}); // TODO(#1993): Handle asynchronous errors.
  913. return null;
  914. }
  915. /**
  916. * Gets the next timestamp needed. Returns the playhead's position if the
  917. * buffer is empty; otherwise, returns the time at which the last segment
  918. * appended ends.
  919. *
  920. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  921. * @param {number} presentationTime
  922. * @return {number} The next timestamp needed.
  923. * @private
  924. */
  925. getTimeNeeded_(mediaState, presentationTime) {
  926. // Get the next timestamp we need. We must use |lastSegmentReference|
  927. // to determine this and not the actual buffer for two reasons:
  928. // 1. Actual segments end slightly before their advertised end times, so
  929. // the next timestamp we need is actually larger than |bufferEnd|.
  930. // 2. There may be drift (the timestamps in the segments are ahead/behind
  931. // of the timestamps in the manifest), but we need drift-free times
  932. // when comparing times against the presentation timeline.
  933. if (!mediaState.lastSegmentReference) {
  934. return presentationTime;
  935. }
  936. return mediaState.lastSegmentReference.endTime;
  937. }
  938. /**
  939. * Gets the SegmentReference of the next segment needed.
  940. *
  941. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  942. * @param {number} presentationTime
  943. * @param {?number} bufferEnd
  944. * @return {shaka.media.SegmentReference} The SegmentReference of the
  945. * next segment needed. Returns null if a segment could not be found, does
  946. * not exist, or is not available.
  947. * @private
  948. */
  949. getSegmentReferenceNeeded_(mediaState, presentationTime, bufferEnd) {
  950. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  951. goog.asserts.assert(
  952. mediaState.stream.segmentIndex,
  953. 'segment index should have been generated already');
  954. if (mediaState.segmentIterator) {
  955. // Something is buffered from the same Stream. Use the current position
  956. // in the segment index. This is updated via next() after each segment is
  957. // appended.
  958. return mediaState.segmentIterator.current();
  959. } else if (mediaState.lastSegmentReference || bufferEnd) {
  960. // Something is buffered from another Stream.
  961. const time = mediaState.lastSegmentReference ?
  962. mediaState.lastSegmentReference.endTime :
  963. bufferEnd;
  964. goog.asserts.assert(time != null, 'Should have a time to search');
  965. shaka.log.v1(
  966. logPrefix, 'looking up segment from new stream endTime:', time);
  967. mediaState.segmentIterator =
  968. mediaState.stream.segmentIndex.getIteratorForTime(time);
  969. const ref = mediaState.segmentIterator &&
  970. mediaState.segmentIterator.next().value;
  971. if (ref == null) {
  972. shaka.log.warning(logPrefix, 'cannot find segment', 'endTime:', time);
  973. }
  974. return ref;
  975. } else {
  976. // Nothing is buffered. Start at the playhead time.
  977. // If there's positive drift then we need to adjust the lookup time, and
  978. // may wind up requesting the previous segment to be safe.
  979. // inaccurateManifestTolerance should be 0 for low latency streaming.
  980. const inaccurateTolerance = this.config_.inaccurateManifestTolerance;
  981. const lookupTime = Math.max(presentationTime - inaccurateTolerance, 0);
  982. shaka.log.v1(logPrefix, 'looking up segment',
  983. 'lookupTime:', lookupTime,
  984. 'presentationTime:', presentationTime);
  985. let ref = null;
  986. if (inaccurateTolerance) {
  987. mediaState.segmentIterator =
  988. mediaState.stream.segmentIndex.getIteratorForTime(lookupTime);
  989. ref = mediaState.segmentIterator &&
  990. mediaState.segmentIterator.next().value;
  991. }
  992. if (!ref) {
  993. // If we can't find a valid segment with the drifted time, look for a
  994. // segment with the presentation time.
  995. mediaState.segmentIterator =
  996. mediaState.stream.segmentIndex.getIteratorForTime(presentationTime);
  997. ref = mediaState.segmentIterator &&
  998. mediaState.segmentIterator.next().value;
  999. }
  1000. if (ref == null) {
  1001. shaka.log.warning(logPrefix, 'cannot find segment',
  1002. 'lookupTime:', lookupTime,
  1003. 'presentationTime:', presentationTime);
  1004. }
  1005. return ref;
  1006. }
  1007. }
  1008. /**
  1009. * Fetches and appends the given segment. Sets up the given MediaState's
  1010. * associated SourceBuffer and evicts segments if either are required
  1011. * beforehand. Schedules another update after completing successfully.
  1012. *
  1013. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1014. * @param {number} presentationTime
  1015. * @param {!shaka.media.SegmentReference} reference
  1016. * @private
  1017. */
  1018. async fetchAndAppend_(mediaState, presentationTime, reference) {
  1019. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1020. const StreamingEngine = shaka.media.StreamingEngine;
  1021. const logPrefix = StreamingEngine.logPrefix_(mediaState);
  1022. shaka.log.v1(logPrefix,
  1023. 'fetchAndAppend_:',
  1024. 'presentationTime=' + presentationTime,
  1025. 'reference.startTime=' + reference.startTime,
  1026. 'reference.endTime=' + reference.endTime);
  1027. // Subtlety: The playhead may move while asynchronous update operations are
  1028. // in progress, so we should avoid calling playhead.getTime() in any
  1029. // callbacks. Furthermore, switch() or seeked() may be called at any time,
  1030. // so we store the old iterator. This allows the mediaState to change and
  1031. // we'll update the old iterator.
  1032. const stream = mediaState.stream;
  1033. const iter = mediaState.segmentIterator;
  1034. mediaState.performingUpdate = true;
  1035. try {
  1036. await this.initSourceBuffer_(mediaState, reference);
  1037. this.destroyer_.ensureNotDestroyed();
  1038. if (this.fatalError_) {
  1039. return;
  1040. }
  1041. shaka.log.v2(logPrefix, 'fetching segment');
  1042. const isMP4 = stream.mimeType == 'video/mp4' ||
  1043. stream.mimeType == 'audio/mp4';
  1044. const isReadableStreamSupported = window.ReadableStream;
  1045. // Enable MP4 low latency streaming with ReadableStream chunked data.
  1046. if (this.config_.lowLatencyMode && isReadableStreamSupported && isMP4) {
  1047. let remaining = new Uint8Array(0);
  1048. const streamDataCallback = async (data) => {
  1049. this.destroyer_.ensureNotDestroyed();
  1050. if (this.fatalError_) {
  1051. return;
  1052. }
  1053. // Append the data with complete boxes.
  1054. // Every time streamDataCallback gets called, append the new data to
  1055. // the remaining data.
  1056. // Find the last fully completed Mdat box, and slice the data into two
  1057. // parts: the first part with completed Mdat boxes, and the second
  1058. // part with an incomplete box.
  1059. // Append the first part, and save the second part as remaining data,
  1060. // and handle it with the next streamDataCallback call.
  1061. remaining = this.concatArray_(remaining, data);
  1062. let sawMDAT = false;
  1063. let offset = 0;
  1064. new shaka.util.Mp4Parser()
  1065. .box('mdat', (box) => {
  1066. offset = box.size + box.start;
  1067. sawMDAT = true;
  1068. })
  1069. .parse(remaining, /* partialOkay= */ false,
  1070. /* isChunkedData= */ true);
  1071. if (sawMDAT) {
  1072. const dataToAppend = remaining.subarray(0, offset);
  1073. remaining = remaining.subarray(offset);
  1074. await this.append_(
  1075. mediaState, presentationTime, stream, reference, dataToAppend);
  1076. }
  1077. };
  1078. await this.fetch_(mediaState, reference, streamDataCallback);
  1079. } else {
  1080. if (this.config_.lowLatencyMode && !isReadableStreamSupported) {
  1081. shaka.log.warning('Low latency streaming mode is enabled, but ' +
  1082. 'ReadableStream is not supported by the browser.');
  1083. }
  1084. const fetchSegment = this.fetch_(mediaState, reference);
  1085. const result = await fetchSegment;
  1086. this.destroyer_.ensureNotDestroyed();
  1087. if (this.fatalError_) {
  1088. return;
  1089. }
  1090. // If the text stream gets switched between fetch_() and append_(), the
  1091. // new text parser is initialized, but the new init segment is not
  1092. // fetched yet. That would cause an error in TextParser.parseMedia().
  1093. // See http://b/168253400
  1094. if (mediaState.waitingToClearBuffer) {
  1095. shaka.log.info(logPrefix, 'waitingToClearBuffer, skip append');
  1096. mediaState.performingUpdate = false;
  1097. this.scheduleUpdate_(mediaState, 0);
  1098. return;
  1099. }
  1100. await this.append_(
  1101. mediaState, presentationTime, stream, reference, result);
  1102. }
  1103. this.destroyer_.ensureNotDestroyed();
  1104. if (this.fatalError_) {
  1105. return;
  1106. }
  1107. // move to next segment after appending the current segment.
  1108. mediaState.lastSegmentReference = reference;
  1109. const newRef = iter.next().value;
  1110. shaka.log.v2(logPrefix, 'advancing to next segment', newRef);
  1111. mediaState.performingUpdate = false;
  1112. mediaState.recovering = false;
  1113. const info = this.playerInterface_.mediaSourceEngine.getBufferedInfo();
  1114. const buffered = info[mediaState.type];
  1115. // Convert the buffered object to a string capture its properties on
  1116. // WebOS.
  1117. shaka.log.v1(logPrefix, 'finished fetch and append',
  1118. JSON.stringify(buffered));
  1119. if (!mediaState.waitingToClearBuffer) {
  1120. this.playerInterface_.onSegmentAppended();
  1121. }
  1122. // Update right away.
  1123. this.scheduleUpdate_(mediaState, 0);
  1124. } catch (error) {
  1125. this.destroyer_.ensureNotDestroyed(error);
  1126. if (this.fatalError_) {
  1127. return;
  1128. }
  1129. goog.asserts.assert(error instanceof shaka.util.Error,
  1130. 'Should only receive a Shaka error');
  1131. mediaState.performingUpdate = false;
  1132. if (error.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  1133. // If the network slows down, abort the current fetch request and start
  1134. // a new one, and ignore the error message.
  1135. mediaState.performingUpdate = false;
  1136. mediaState.updateTimer = null;
  1137. this.scheduleUpdate_(mediaState, 0);
  1138. } else if (mediaState.type == ContentType.TEXT &&
  1139. this.config_.ignoreTextStreamFailures) {
  1140. if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS) {
  1141. shaka.log.warning(logPrefix,
  1142. 'Text stream failed to download. Proceeding without it.');
  1143. } else {
  1144. shaka.log.warning(logPrefix,
  1145. 'Text stream failed to parse. Proceeding without it.');
  1146. }
  1147. this.mediaStates_.delete(ContentType.TEXT);
  1148. } else if (error.code == shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR) {
  1149. this.handleQuotaExceeded_(mediaState, error);
  1150. } else if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS &&
  1151. error.data && error.data[1] == 404) {
  1152. // The segment could not be found, does not exist, or is not available.
  1153. // In any case just try again.
  1154. // The current segment is not available. Schedule another update to
  1155. // fetch the segment again.
  1156. shaka.log.v2(logPrefix, 'segment not available.');
  1157. mediaState.performingUpdate = false;
  1158. mediaState.updateTimer = null;
  1159. this.scheduleUpdate_(mediaState, 1);
  1160. } else {
  1161. shaka.log.error(logPrefix, 'failed fetch and append: code=' +
  1162. error.code);
  1163. mediaState.hasError = true;
  1164. error.severity = shaka.util.Error.Severity.CRITICAL;
  1165. await this.handleStreamingError_(error);
  1166. }
  1167. }
  1168. }
  1169. /**
  1170. * Clear per-stream error states and retry any failed streams.
  1171. * @return {boolean} False if unable to retry.
  1172. */
  1173. retry() {
  1174. if (this.destroyer_.destroyed()) {
  1175. shaka.log.error('Unable to retry after StreamingEngine is destroyed!');
  1176. return false;
  1177. }
  1178. if (this.fatalError_) {
  1179. shaka.log.error('Unable to retry after StreamingEngine encountered a ' +
  1180. 'fatal error!');
  1181. return false;
  1182. }
  1183. for (const mediaState of this.mediaStates_.values()) {
  1184. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1185. if (mediaState.hasError) {
  1186. shaka.log.info(logPrefix, 'Retrying after failure...');
  1187. mediaState.hasError = false;
  1188. this.scheduleUpdate_(mediaState, 0.1);
  1189. }
  1190. }
  1191. return true;
  1192. }
  1193. /**
  1194. * Append the data to the remaining data.
  1195. * @param {!Uint8Array} remaining
  1196. * @param {!Uint8Array} data
  1197. * @return {!Uint8Array}
  1198. * @private
  1199. */
  1200. concatArray_(remaining, data) {
  1201. const result = new Uint8Array(remaining.length + data.length);
  1202. result.set(remaining);
  1203. result.set(data, remaining.length);
  1204. return result;
  1205. }
  1206. /**
  1207. * Handles a QUOTA_EXCEEDED_ERROR.
  1208. *
  1209. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1210. * @param {!shaka.util.Error} error
  1211. * @private
  1212. */
  1213. handleQuotaExceeded_(mediaState, error) {
  1214. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1215. // The segment cannot fit into the SourceBuffer. Ideally, MediaSource would
  1216. // have evicted old data to accommodate the segment; however, it may have
  1217. // failed to do this if the segment is very large, or if it could not find
  1218. // a suitable time range to remove.
  1219. //
  1220. // We can overcome the latter by trying to append the segment again;
  1221. // however, to avoid continuous QuotaExceededErrors we must reduce the size
  1222. // of the buffer going forward.
  1223. //
  1224. // If we've recently reduced the buffering goals, wait until the stream
  1225. // which caused the first QuotaExceededError recovers. Doing this ensures
  1226. // we don't reduce the buffering goals too quickly.
  1227. const mediaStates = Array.from(this.mediaStates_.values());
  1228. const waitingForAnotherStreamToRecover = mediaStates.some((ms) => {
  1229. return ms != mediaState && ms.recovering;
  1230. });
  1231. if (!waitingForAnotherStreamToRecover) {
  1232. // Reduction schedule: 80%, 60%, 40%, 20%, 16%, 12%, 8%, 4%, fail.
  1233. // Note: percentages are used for comparisons to avoid rounding errors.
  1234. const percentBefore = Math.round(100 * this.bufferingGoalScale_);
  1235. if (percentBefore > 20) {
  1236. this.bufferingGoalScale_ -= 0.2;
  1237. } else if (percentBefore > 4) {
  1238. this.bufferingGoalScale_ -= 0.04;
  1239. } else {
  1240. shaka.log.error(
  1241. logPrefix, 'MediaSource threw QuotaExceededError too many times');
  1242. mediaState.hasError = true;
  1243. this.fatalError_ = true;
  1244. this.playerInterface_.onError(error);
  1245. return;
  1246. }
  1247. const percentAfter = Math.round(100 * this.bufferingGoalScale_);
  1248. shaka.log.warning(
  1249. logPrefix,
  1250. 'MediaSource threw QuotaExceededError:',
  1251. 'reducing buffering goals by ' + (100 - percentAfter) + '%');
  1252. mediaState.recovering = true;
  1253. } else {
  1254. shaka.log.debug(
  1255. logPrefix,
  1256. 'MediaSource threw QuotaExceededError:',
  1257. 'waiting for another stream to recover...');
  1258. }
  1259. // QuotaExceededError gets thrown if evication didn't help to make room
  1260. // for a segment. We want to wait for a while (4 seconds is just an
  1261. // arbitrary number) before updating to give the playhead a chance to
  1262. // advance, so we don't immidiately throw again.
  1263. this.scheduleUpdate_(mediaState, 4);
  1264. }
  1265. /**
  1266. * Sets the given MediaState's associated SourceBuffer's timestamp offset,
  1267. * append window, and init segment if they have changed. If an error occurs
  1268. * then neither the timestamp offset or init segment are unset, since another
  1269. * call to switch() will end up superseding them.
  1270. *
  1271. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1272. * @param {!shaka.media.SegmentReference} reference
  1273. * @return {!Promise}
  1274. * @private
  1275. */
  1276. async initSourceBuffer_(mediaState, reference) {
  1277. const StreamingEngine = shaka.media.StreamingEngine;
  1278. const logPrefix = StreamingEngine.logPrefix_(mediaState);
  1279. /** @type {!Array.<!Promise>} */
  1280. const operations = [];
  1281. // Rounding issues can cause us to remove the first frame of a Period, so
  1282. // reduce the window start time slightly.
  1283. const appendWindowStart = Math.max(0,
  1284. reference.appendWindowStart -
  1285. StreamingEngine.APPEND_WINDOW_START_FUDGE_);
  1286. const appendWindowEnd =
  1287. reference.appendWindowEnd + StreamingEngine.APPEND_WINDOW_END_FUDGE_;
  1288. goog.asserts.assert(
  1289. reference.startTime <= appendWindowEnd,
  1290. logPrefix + ' segment should start before append window end');
  1291. const timestampOffset = reference.timestampOffset;
  1292. if (timestampOffset != mediaState.lastTimestampOffset ||
  1293. appendWindowStart != mediaState.lastAppendWindowStart ||
  1294. appendWindowEnd != mediaState.lastAppendWindowEnd) {
  1295. shaka.log.v1(logPrefix, 'setting timestamp offset to ' + timestampOffset);
  1296. shaka.log.v1(logPrefix,
  1297. 'setting append window start to ' + appendWindowStart);
  1298. shaka.log.v1(logPrefix,
  1299. 'setting append window end to ' + appendWindowEnd);
  1300. const setProperties = async () => {
  1301. try {
  1302. mediaState.lastAppendWindowStart = appendWindowStart;
  1303. mediaState.lastAppendWindowEnd = appendWindowEnd;
  1304. mediaState.lastTimestampOffset = timestampOffset;
  1305. await this.playerInterface_.mediaSourceEngine.setStreamProperties(
  1306. mediaState.type, timestampOffset, appendWindowStart,
  1307. appendWindowEnd);
  1308. } catch (error) {
  1309. mediaState.lastAppendWindowStart = null;
  1310. mediaState.lastAppendWindowEnd = null;
  1311. mediaState.lastTimestampOffset = null;
  1312. throw error;
  1313. }
  1314. };
  1315. operations.push(setProperties());
  1316. }
  1317. if (!shaka.media.InitSegmentReference.equal(
  1318. reference.initSegmentReference, mediaState.lastInitSegmentReference)) {
  1319. mediaState.lastInitSegmentReference = reference.initSegmentReference;
  1320. if (reference.initSegmentReference) {
  1321. shaka.log.v1(logPrefix, 'fetching init segment');
  1322. const fetchInit =
  1323. this.fetch_(mediaState, reference.initSegmentReference);
  1324. const append = async () => {
  1325. try {
  1326. const initSegment = await fetchInit;
  1327. this.destroyer_.ensureNotDestroyed();
  1328. shaka.log.v1(logPrefix, 'appending init segment');
  1329. const hasClosedCaptions = mediaState.stream.closedCaptions &&
  1330. mediaState.stream.closedCaptions.size > 0;
  1331. await this.playerInterface_.mediaSourceEngine.appendBuffer(
  1332. mediaState.type, initSegment, /* startTime= */ null,
  1333. /* endTime= */ null, hasClosedCaptions);
  1334. } catch (error) {
  1335. mediaState.lastInitSegmentReference = null;
  1336. throw error;
  1337. }
  1338. };
  1339. this.playerInterface_.onInitSegmentAppended(
  1340. reference.startTime, reference.initSegmentReference);
  1341. operations.push(append());
  1342. }
  1343. }
  1344. await Promise.all(operations);
  1345. }
  1346. /**
  1347. * Appends the given segment and evicts content if required to append.
  1348. *
  1349. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1350. * @param {number} presentationTime
  1351. * @param {shaka.extern.Stream} stream
  1352. * @param {!shaka.media.SegmentReference} reference
  1353. * @param {BufferSource} segment
  1354. * @return {!Promise}
  1355. * @private
  1356. */
  1357. async append_(mediaState, presentationTime, stream, reference,
  1358. segment) {
  1359. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1360. const hasClosedCaptions = stream.closedCaptions &&
  1361. stream.closedCaptions.size > 0;
  1362. if ((stream.emsgSchemeIdUris != null &&
  1363. stream.emsgSchemeIdUris.length > 0) ||
  1364. this.config_.dispatchAllEmsgBoxes) {
  1365. new shaka.util.Mp4Parser()
  1366. .fullBox(
  1367. 'emsg',
  1368. (box) => this.parseEMSG_(
  1369. reference, stream.emsgSchemeIdUris, box))
  1370. .parse(segment);
  1371. }
  1372. await this.evict_(mediaState, presentationTime);
  1373. this.destroyer_.ensureNotDestroyed();
  1374. shaka.log.v1(logPrefix, 'appending media segment');
  1375. await this.playerInterface_.mediaSourceEngine.appendBuffer(
  1376. mediaState.type,
  1377. segment,
  1378. reference.startTime,
  1379. reference.endTime,
  1380. hasClosedCaptions);
  1381. this.destroyer_.ensureNotDestroyed();
  1382. shaka.log.v2(logPrefix, 'appended media segment');
  1383. }
  1384. /**
  1385. * Parse the EMSG box from a MP4 container.
  1386. *
  1387. * @param {!shaka.media.SegmentReference} reference
  1388. * @param {?Array.<string>} emsgSchemeIdUris Array of emsg
  1389. * scheme_id_uri for which emsg boxes should be parsed.
  1390. * @param {!shaka.extern.ParsedBox} box
  1391. * @private
  1392. * https://dashif-documents.azurewebsites.net/Events/master/event.html#emsg-format
  1393. * aligned(8) class DASHEventMessageBox
  1394. * extends FullBox(‘emsg’, version, flags = 0){
  1395. * if (version==0) {
  1396. * string scheme_id_uri;
  1397. * string value;
  1398. * unsigned int(32) timescale;
  1399. * unsigned int(32) presentation_time_delta;
  1400. * unsigned int(32) event_duration;
  1401. * unsigned int(32) id;
  1402. * } else if (version==1) {
  1403. * unsigned int(32) timescale;
  1404. * unsigned int(64) presentation_time;
  1405. * unsigned int(32) event_duration;
  1406. * unsigned int(32) id;
  1407. * string scheme_id_uri;
  1408. * string value;
  1409. * }
  1410. * unsigned int(8) message_data[];
  1411. */
  1412. parseEMSG_(reference, emsgSchemeIdUris, box) {
  1413. let timescale;
  1414. let id;
  1415. let eventDuration;
  1416. let schemeId;
  1417. let startTime;
  1418. let presentationTimeDelta;
  1419. let value;
  1420. if (box.version === 0) {
  1421. schemeId = box.reader.readTerminatedString();
  1422. value = box.reader.readTerminatedString();
  1423. timescale = box.reader.readUint32();
  1424. presentationTimeDelta = box.reader.readUint32();
  1425. eventDuration = box.reader.readUint32();
  1426. id = box.reader.readUint32();
  1427. startTime = reference.startTime + (presentationTimeDelta / timescale);
  1428. } else {
  1429. timescale = box.reader.readUint32();
  1430. const pts = box.reader.readUint64();
  1431. startTime = (pts / timescale) + reference.timestampOffset;
  1432. presentationTimeDelta = startTime - reference.startTime;
  1433. eventDuration = box.reader.readUint32();
  1434. id = box.reader.readUint32();
  1435. schemeId = box.reader.readTerminatedString();
  1436. value = box.reader.readTerminatedString();
  1437. }
  1438. const messageData = box.reader.readBytes(
  1439. box.reader.getLength() - box.reader.getPosition());
  1440. // See DASH sec. 5.10.3.3.1
  1441. // If a DASH client detects an event message box with a scheme that is not
  1442. // defined in MPD, the client is expected to ignore it.
  1443. if ((emsgSchemeIdUris && emsgSchemeIdUris.includes(schemeId)) ||
  1444. this.config_.dispatchAllEmsgBoxes) {
  1445. // See DASH sec. 5.10.4.1
  1446. // A special scheme in DASH used to signal manifest updates.
  1447. if (schemeId == 'urn:mpeg:dash:event:2012') {
  1448. this.playerInterface_.onManifestUpdate();
  1449. } else {
  1450. /** @type {shaka.extern.EmsgInfo} */
  1451. const emsg = {
  1452. startTime: startTime,
  1453. endTime: startTime + (eventDuration / timescale),
  1454. schemeIdUri: schemeId,
  1455. value: value,
  1456. timescale: timescale,
  1457. presentationTimeDelta: presentationTimeDelta,
  1458. eventDuration: eventDuration,
  1459. id: id,
  1460. messageData: messageData,
  1461. };
  1462. // Dispatch an event to notify the application about the emsg box.
  1463. const eventName = shaka.Player.EventName.Emsg;
  1464. const data = (new Map()).set('detail', emsg);
  1465. const event = new shaka.util.FakeEvent(eventName, data);
  1466. this.playerInterface_.onEvent(event);
  1467. }
  1468. }
  1469. }
  1470. /**
  1471. * Evicts media to meet the max buffer behind limit.
  1472. *
  1473. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1474. * @param {number} presentationTime
  1475. * @private
  1476. */
  1477. async evict_(mediaState, presentationTime) {
  1478. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1479. shaka.log.v2(logPrefix, 'checking buffer length');
  1480. // Use the max segment duration, if it is longer than the bufferBehind, to
  1481. // avoid accidentally clearing too much data when dealing with a manifest
  1482. // with a long keyframe interval.
  1483. const bufferBehind = Math.max(this.config_.bufferBehind,
  1484. this.manifest_.presentationTimeline.getMaxSegmentDuration());
  1485. const startTime =
  1486. this.playerInterface_.mediaSourceEngine.bufferStart(mediaState.type);
  1487. if (startTime == null) {
  1488. shaka.log.v2(logPrefix,
  1489. 'buffer behind okay because nothing buffered:',
  1490. 'presentationTime=' + presentationTime,
  1491. 'bufferBehind=' + bufferBehind);
  1492. return;
  1493. }
  1494. const bufferedBehind = presentationTime - startTime;
  1495. const overflow = bufferedBehind - bufferBehind;
  1496. // See: https://github.com/shaka-project/shaka-player/issues/2982
  1497. if (overflow <= 0.01) {
  1498. shaka.log.v2(logPrefix,
  1499. 'buffer behind okay:',
  1500. 'presentationTime=' + presentationTime,
  1501. 'bufferedBehind=' + bufferedBehind,
  1502. 'bufferBehind=' + bufferBehind,
  1503. 'underflow=' + Math.abs(overflow));
  1504. return;
  1505. }
  1506. shaka.log.v1(logPrefix,
  1507. 'buffer behind too large:',
  1508. 'presentationTime=' + presentationTime,
  1509. 'bufferedBehind=' + bufferedBehind,
  1510. 'bufferBehind=' + bufferBehind,
  1511. 'overflow=' + overflow);
  1512. await this.playerInterface_.mediaSourceEngine.remove(mediaState.type,
  1513. startTime, startTime + overflow);
  1514. this.destroyer_.ensureNotDestroyed();
  1515. shaka.log.v1(logPrefix, 'evicted ' + overflow + ' seconds');
  1516. }
  1517. /**
  1518. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1519. * @return {boolean}
  1520. * @private
  1521. */
  1522. static isEmbeddedText_(mediaState) {
  1523. const MimeUtils = shaka.util.MimeUtils;
  1524. const CEA608_MIME = MimeUtils.CEA608_CLOSED_CAPTION_MIMETYPE;
  1525. const CEA708_MIME = MimeUtils.CEA708_CLOSED_CAPTION_MIMETYPE;
  1526. return mediaState &&
  1527. mediaState.type == shaka.util.ManifestParserUtils.ContentType.TEXT &&
  1528. (mediaState.stream.mimeType == CEA608_MIME ||
  1529. mediaState.stream.mimeType == CEA708_MIME);
  1530. }
  1531. /**
  1532. * Fetches the given segment.
  1533. *
  1534. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1535. * @param {(!shaka.media.InitSegmentReference|!shaka.media.SegmentReference)}
  1536. * reference
  1537. * @param {?function(BufferSource):!Promise=} streamDataCallback
  1538. *
  1539. * @return {!Promise.<BufferSource>}
  1540. * @private
  1541. * @suppress {strictMissingProperties}
  1542. */
  1543. async fetch_(mediaState, reference, streamDataCallback) {
  1544. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  1545. const request = shaka.util.Networking.createSegmentRequest(
  1546. reference.getUris(),
  1547. reference.startByte,
  1548. reference.endByte,
  1549. this.config_.retryParameters,
  1550. streamDataCallback);
  1551. shaka.log.v2('fetching: reference=', reference);
  1552. const stream = mediaState.stream;
  1553. this.playerInterface_.modifySegmentRequest(
  1554. request,
  1555. {
  1556. type: stream.type,
  1557. init: reference instanceof shaka.media.InitSegmentReference,
  1558. duration: reference.endTime - reference.startTime,
  1559. mimeType: stream.mimeType,
  1560. codecs: stream.codecs,
  1561. bandwidth: stream.bandwidth,
  1562. },
  1563. );
  1564. const op = this.playerInterface_.netEngine.request(requestType, request);
  1565. mediaState.operation = op;
  1566. const response = await op.promise;
  1567. mediaState.operation = null;
  1568. return response.data;
  1569. }
  1570. /**
  1571. * Clears the buffer and schedules another update.
  1572. * The optional parameter safeMargin allows to retain a certain amount
  1573. * of buffer, which can help avoiding rebuffering events.
  1574. * The value of the safe margin should be provided by the ABR manager.
  1575. *
  1576. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1577. * @param {boolean} flush
  1578. * @param {number} safeMargin
  1579. * @private
  1580. */
  1581. async clearBuffer_(mediaState, flush, safeMargin) {
  1582. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1583. goog.asserts.assert(
  1584. !mediaState.performingUpdate && (mediaState.updateTimer == null),
  1585. logPrefix + ' unexpected call to clearBuffer_()');
  1586. mediaState.waitingToClearBuffer = false;
  1587. mediaState.waitingToFlushBuffer = false;
  1588. mediaState.clearBufferSafeMargin = 0;
  1589. mediaState.clearingBuffer = true;
  1590. mediaState.lastSegmentReference = null;
  1591. mediaState.lastInitSegmentReference = null;
  1592. mediaState.segmentIterator = null;
  1593. shaka.log.debug(logPrefix, 'clearing buffer');
  1594. if (safeMargin) {
  1595. const presentationTime = this.playerInterface_.getPresentationTime();
  1596. const duration = this.playerInterface_.mediaSourceEngine.getDuration();
  1597. await this.playerInterface_.mediaSourceEngine.remove(
  1598. mediaState.type, presentationTime + safeMargin, duration);
  1599. } else {
  1600. await this.playerInterface_.mediaSourceEngine.clear(mediaState.type);
  1601. this.destroyer_.ensureNotDestroyed();
  1602. if (flush) {
  1603. await this.playerInterface_.mediaSourceEngine.flush(
  1604. mediaState.type);
  1605. }
  1606. }
  1607. this.destroyer_.ensureNotDestroyed();
  1608. shaka.log.debug(logPrefix, 'cleared buffer');
  1609. mediaState.clearingBuffer = false;
  1610. mediaState.endOfStream = false;
  1611. this.scheduleUpdate_(mediaState, 0);
  1612. }
  1613. /**
  1614. * Schedules |mediaState|'s next update.
  1615. *
  1616. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1617. * @param {number} delay The delay in seconds.
  1618. * @private
  1619. */
  1620. scheduleUpdate_(mediaState, delay) {
  1621. const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1622. // If the text's update is canceled and its mediaState is deleted, stop
  1623. // scheduling another update.
  1624. const type = mediaState.type;
  1625. if (type == shaka.util.ManifestParserUtils.ContentType.TEXT &&
  1626. !this.mediaStates_.has(type)) {
  1627. shaka.log.v1(logPrefix, 'Text stream is unloaded. No update is needed.');
  1628. return;
  1629. }
  1630. shaka.log.v2(logPrefix, 'updating in ' + delay + ' seconds');
  1631. goog.asserts.assert(mediaState.updateTimer == null,
  1632. logPrefix + ' did not expect update to be scheduled');
  1633. mediaState.updateTimer = new shaka.util.DelayedTick(async () => {
  1634. try {
  1635. await this.onUpdate_(mediaState);
  1636. } catch (error) {
  1637. if (this.playerInterface_) {
  1638. this.playerInterface_.onError(error);
  1639. }
  1640. }
  1641. }).tickAfter(delay);
  1642. }
  1643. /**
  1644. * If |mediaState| is scheduled to update, stop it.
  1645. *
  1646. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1647. * @private
  1648. */
  1649. cancelUpdate_(mediaState) {
  1650. if (mediaState.updateTimer == null) {
  1651. return;
  1652. }
  1653. mediaState.updateTimer.stop();
  1654. mediaState.updateTimer = null;
  1655. }
  1656. /**
  1657. * If |mediaState| holds any in-progress operations, abort them.
  1658. *
  1659. * @return {!Promise}
  1660. * @private
  1661. */
  1662. async abortOperations_(mediaState) {
  1663. if (mediaState.operation) {
  1664. await mediaState.operation.abort();
  1665. }
  1666. }
  1667. /**
  1668. * Handle streaming errors by delaying, then notifying the application by
  1669. * error callback and by streaming failure callback.
  1670. *
  1671. * @param {!shaka.util.Error} error
  1672. * @return {!Promise}
  1673. * @private
  1674. */
  1675. async handleStreamingError_(error) {
  1676. // If we invoke the callback right away, the application could trigger a
  1677. // rapid retry cycle that could be very unkind to the server. Instead,
  1678. // use the backoff system to delay and backoff the error handling.
  1679. await this.failureCallbackBackoff_.attempt();
  1680. this.destroyer_.ensureNotDestroyed();
  1681. // First fire an error event.
  1682. this.playerInterface_.onError(error);
  1683. // If the error was not handled by the application, call the failure
  1684. // callback.
  1685. if (!error.handled) {
  1686. this.config_.failureCallback(error);
  1687. }
  1688. }
  1689. /**
  1690. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1691. * @return {string} A log prefix of the form ($CONTENT_TYPE:$STREAM_ID), e.g.,
  1692. * "(audio:5)" or "(video:hd)".
  1693. * @private
  1694. */
  1695. static logPrefix_(mediaState) {
  1696. return '(' + mediaState.type + ':' + mediaState.stream.id + ')';
  1697. }
  1698. };
  1699. /**
  1700. * @typedef {{
  1701. * getPresentationTime: function():number,
  1702. * getBandwidthEstimate: function():number,
  1703. * modifySegmentRequest: function(shaka.extern.Request,
  1704. * shaka.util.CmcdManager.SegmentInfo),
  1705. * mediaSourceEngine: !shaka.media.MediaSourceEngine,
  1706. * netEngine: shaka.net.NetworkingEngine,
  1707. * onError: function(!shaka.util.Error),
  1708. * onEvent: function(!Event),
  1709. * onManifestUpdate: function(),
  1710. * onSegmentAppended: function(),
  1711. * onInitSegmentAppended: function(!number,!shaka.media.InitSegmentReference)
  1712. * }}
  1713. *
  1714. * @property {function():number} getPresentationTime
  1715. * Get the position in the presentation (in seconds) of the content that the
  1716. * viewer is seeing on screen right now.
  1717. * @property {function():number} getBandwidthEstimate
  1718. * Get the estimated bandwidth in bits per second.
  1719. * @property {function(shaka.extern.Request,
  1720. * shaka.extern.Cmcd.SegmentInfo)} modifySegmentRequest
  1721. * The request modifier
  1722. * @property {!shaka.media.MediaSourceEngine} mediaSourceEngine
  1723. * The MediaSourceEngine. The caller retains ownership.
  1724. * @property {shaka.net.NetworkingEngine} netEngine
  1725. * The NetworkingEngine instance to use. The caller retains ownership.
  1726. * @property {function(!shaka.util.Error)} onError
  1727. * Called when an error occurs. If the error is recoverable (see
  1728. * {@link shaka.util.Error}) then the caller may invoke either
  1729. * StreamingEngine.switch*() or StreamingEngine.seeked() to attempt recovery.
  1730. * @property {function(!Event)} onEvent
  1731. * Called when an event occurs that should be sent to the app.
  1732. * @property {function()} onManifestUpdate
  1733. * Called when an embedded 'emsg' box should trigger a manifest update.
  1734. * @property {function()} onSegmentAppended
  1735. * Called after a segment is successfully appended to a MediaSource.
  1736. * @property
  1737. * {function(!number, !shaka.media.InitSegmentReference)} onInitSegmentAppended
  1738. * Called when an init segment is appended to a MediaSource.
  1739. */
  1740. shaka.media.StreamingEngine.PlayerInterface;
  1741. /**
  1742. * @typedef {{
  1743. * type: shaka.util.ManifestParserUtils.ContentType,
  1744. * stream: shaka.extern.Stream,
  1745. * segmentIterator: shaka.media.SegmentIterator,
  1746. * lastSegmentReference: shaka.media.SegmentReference,
  1747. * lastInitSegmentReference: shaka.media.InitSegmentReference,
  1748. * lastTimestampOffset: ?number,
  1749. * lastAppendWindowStart: ?number,
  1750. * lastAppendWindowEnd: ?number,
  1751. * restoreStreamAfterTrickPlay: ?shaka.extern.Stream,
  1752. * endOfStream: boolean,
  1753. * performingUpdate: boolean,
  1754. * updateTimer: shaka.util.DelayedTick,
  1755. * waitingToClearBuffer: boolean,
  1756. * waitingToFlushBuffer: boolean,
  1757. * clearBufferSafeMargin: number,
  1758. * clearingBuffer: boolean,
  1759. * recovering: boolean,
  1760. * hasError: boolean,
  1761. * operation: shaka.net.NetworkingEngine.PendingRequest
  1762. * }}
  1763. *
  1764. * @description
  1765. * Contains the state of a logical stream, i.e., a sequence of segmented data
  1766. * for a particular content type. At any given time there is a Stream object
  1767. * associated with the state of the logical stream.
  1768. *
  1769. * @property {shaka.util.ManifestParserUtils.ContentType} type
  1770. * The stream's content type, e.g., 'audio', 'video', or 'text'.
  1771. * @property {shaka.extern.Stream} stream
  1772. * The current Stream.
  1773. * @property {shaka.media.SegmentIndexIterator} segmentIterator
  1774. * An iterator through the segments of |stream|.
  1775. * @property {shaka.media.SegmentReference} lastSegmentReference
  1776. * The SegmentReference of the last segment that was appended.
  1777. * @property {shaka.media.InitSegmentReference} lastInitSegmentReference
  1778. * The InitSegmentReference of the last init segment that was appended.
  1779. * @property {?number} lastTimestampOffset
  1780. * The last timestamp offset given to MediaSourceEngine for this type.
  1781. * @property {?number} lastAppendWindowStart
  1782. * The last append window start given to MediaSourceEngine for this type.
  1783. * @property {?number} lastAppendWindowEnd
  1784. * The last append window end given to MediaSourceEngine for this type.
  1785. * @property {?shaka.extern.Stream} restoreStreamAfterTrickPlay
  1786. * The Stream to restore after trick play mode is turned off.
  1787. * @property {boolean} endOfStream
  1788. * True indicates that the end of the buffer has hit the end of the
  1789. * presentation.
  1790. * @property {boolean} performingUpdate
  1791. * True indicates that an update is in progress.
  1792. * @property {shaka.util.DelayedTick} updateTimer
  1793. * A timer used to update the media state.
  1794. * @property {boolean} waitingToClearBuffer
  1795. * True indicates that the buffer must be cleared after the current update
  1796. * finishes.
  1797. * @property {boolean} waitingToFlushBuffer
  1798. * True indicates that the buffer must be flushed after it is cleared.
  1799. * @property {number} clearBufferSafeMargin
  1800. * The amount of buffer to retain when clearing the buffer after the update.
  1801. * @property {boolean} clearingBuffer
  1802. * True indicates that the buffer is being cleared.
  1803. * @property {boolean} recovering
  1804. * True indicates that the last segment was not appended because it could not
  1805. * fit in the buffer.
  1806. * @property {boolean} hasError
  1807. * True indicates that the stream has encountered an error and has stopped
  1808. * updating.
  1809. * @property {shaka.net.NetworkingEngine.PendingRequest} operation
  1810. * Operation with the number of bytes to be downloaded.
  1811. */
  1812. shaka.media.StreamingEngine.MediaState_;
  1813. /**
  1814. * The fudge factor for appendWindowStart. By adjusting the window backward, we
  1815. * avoid rounding errors that could cause us to remove the keyframe at the start
  1816. * of the Period.
  1817. *
  1818. * NOTE: This was increased as part of the solution to
  1819. * https://github.com/shaka-project/shaka-player/issues/1281
  1820. *
  1821. * @const {number}
  1822. * @private
  1823. */
  1824. shaka.media.StreamingEngine.APPEND_WINDOW_START_FUDGE_ = 0.1;
  1825. /**
  1826. * The fudge factor for appendWindowEnd. By adjusting the window backward, we
  1827. * avoid rounding errors that could cause us to remove the last few samples of
  1828. * the Period. This rounding error could then create an artificial gap and a
  1829. * stutter when the gap-jumping logic takes over.
  1830. *
  1831. * https://github.com/shaka-project/shaka-player/issues/1597
  1832. *
  1833. * @const {number}
  1834. * @private
  1835. */
  1836. shaka.media.StreamingEngine.APPEND_WINDOW_END_FUDGE_ = 0.01;
  1837. /**
  1838. * The maximum number of segments by which a stream can get ahead of other
  1839. * streams.
  1840. *
  1841. * Introduced to keep StreamingEngine from letting one media type get too far
  1842. * ahead of another. For example, audio segments are typically much smaller
  1843. * than video segments, so in the time it takes to fetch one video segment, we
  1844. * could fetch many audio segments. This doesn't help with buffering, though,
  1845. * since the intersection of the two buffered ranges is what counts.
  1846. *
  1847. * @const {number}
  1848. * @private
  1849. */
  1850. shaka.media.StreamingEngine.MAX_RUN_AHEAD_SEGMENTS_ = 1;