Source: lib/media/streaming_engine.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.media.StreamingEngine');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.media.MediaSourceEngine');
  21. goog.require('shaka.media.Playhead');
  22. goog.require('shaka.net.Backoff');
  23. goog.require('shaka.net.NetworkingEngine');
  24. goog.require('shaka.util.Error');
  25. goog.require('shaka.util.FakeEvent');
  26. goog.require('shaka.util.Functional');
  27. goog.require('shaka.util.IDestroyable');
  28. goog.require('shaka.util.ManifestParserUtils');
  29. goog.require('shaka.util.MapUtils');
  30. goog.require('shaka.util.MimeUtils');
  31. goog.require('shaka.util.Mp4Parser');
  32. goog.require('shaka.util.PublicPromise');
  33. goog.require('shaka.util.StreamUtils');
  34. /**
  35. * Creates a StreamingEngine.
  36. *
  37. * The StreamingEngine is responsible for setting up the Manifest's Streams
  38. * (i.e., for calling each Stream's createSegmentIndex() function), for
  39. * downloading segments, for co-ordinating audio, video, and text buffering,
  40. * and for handling Period transitions. The StreamingEngine provides an
  41. * interface to switch between Streams, but it does not choose which Streams to
  42. * switch to.
  43. *
  44. * The StreamingEngine notifies its owner when it needs to buffer a new Period,
  45. * so its owner can choose which Streams within that Period to initially
  46. * buffer. Moreover, the StreamingEngine also notifies its owner when any
  47. * Stream within the current Period may be switched to, so its owner can switch
  48. * bitrates, resolutions, or languages.
  49. *
  50. * The StreamingEngine does not need to be notified about changes to the
  51. * Manifest's SegmentIndexes; however, it does need to be notified when new
  52. * Periods are added to the Manifest, so it can set up that Period's Streams.
  53. *
  54. * To start the StreamingEngine the owner must first call configure() followed
  55. * by init(). The StreamingEngine will then call onChooseStreams(p) when it
  56. * needs to buffer Period p; it will then switch to the Streams returned from
  57. * that function. The StreamingEngine will call onCanSwitch() when any
  58. * Stream within the current Period may be switched to.
  59. *
  60. * The owner must call seeked() each time the playhead moves to a new location
  61. * within the presentation timeline; however, the owner may forego calling
  62. * seeked() when the playhead moves outside the presentation timeline.
  63. *
  64. * @param {shakaExtern.Manifest} manifest
  65. * @param {shaka.media.StreamingEngine.PlayerInterface} playerInterface
  66. *
  67. * @constructor
  68. * @struct
  69. * @implements {shaka.util.IDestroyable}
  70. */
  71. shaka.media.StreamingEngine = function(manifest, playerInterface) {
  72. /** @private {?shaka.media.StreamingEngine.PlayerInterface} */
  73. this.playerInterface_ = playerInterface;
  74. /** @private {?shakaExtern.Manifest} */
  75. this.manifest_ = manifest;
  76. /** @private {?shakaExtern.StreamingConfiguration} */
  77. this.config_ = null;
  78. /** @private {number} */
  79. this.bufferingGoalScale_ = 1;
  80. /** @private {Promise} */
  81. this.setupPeriodPromise_ = Promise.resolve();
  82. /**
  83. * Maps a Period's index to an object that indicates that either
  84. * 1. the Period has not been set up (undefined).
  85. * 2. the Period is being set up ([a PublicPromise, false]).
  86. * 3. the Period is set up (i.e., all Streams within the Period are set up)
  87. * and can be switched to ([a PublicPromise, true]).
  88. *
  89. * @private {Array.<?{promise: shaka.util.PublicPromise, resolved: boolean}>}
  90. */
  91. this.canSwitchPeriod_ = [];
  92. /**
  93. * Maps a Stream's ID to an object that indicates that either
  94. * 1. the Stream has not been set up (undefined).
  95. * 2. the Stream is being set up ([a Promise instance, false]).
  96. * 3. the Stream is set up and can be switched to
  97. * ([a Promise instance, true]).
  98. *
  99. * @private {Object.<number,
  100. * ?{promise: shaka.util.PublicPromise, resolved: boolean}>}
  101. */
  102. this.canSwitchStream_ = {};
  103. /**
  104. * Maps a content type, e.g., 'audio', 'video', or 'text', to a MediaState.
  105. *
  106. * @private {Object.<shaka.util.ManifestParserUtils.ContentType,
  107. !shaka.media.StreamingEngine.MediaState_>}
  108. */
  109. this.mediaStates_ = {};
  110. /**
  111. * Set to true once one segment of each content type has been buffered.
  112. *
  113. * @private {boolean}
  114. */
  115. this.startupComplete_ = false;
  116. /**
  117. * Used for delay and backoff of failure callbacks, so that apps do not retry
  118. * instantly.
  119. *
  120. * @private {shaka.net.Backoff}
  121. */
  122. this.failureCallbackBackoff_ = null;
  123. /**
  124. * Set to true on fatal error. Interrupts fetchAndAppend_().
  125. *
  126. * @private {boolean}
  127. */
  128. this.fatalError_ = false;
  129. /** @private {boolean} */
  130. this.destroyed_ = false;
  131. /**
  132. * Set to true when a request to unload text stream comes in. This is used
  133. * since loading new text stream is async, the request of unloading text
  134. * stream might come in before setting up new text stream is finished.
  135. * @private {boolean}
  136. */
  137. this.unloadingTextStream_ = false;
  138. /** @private {number} */
  139. this.textStreamSequenceId_ = 0;
  140. };
  141. /**
  142. * @typedef {{
  143. * variant: (?shakaExtern.Variant|undefined),
  144. * text: ?shakaExtern.Stream
  145. * }}
  146. *
  147. * @property {(?shakaExtern.Variant|undefined)} variant
  148. * The chosen variant. May be omitted for text re-init.
  149. * @property {?shakaExtern.Stream} text
  150. * The chosen text stream.
  151. */
  152. shaka.media.StreamingEngine.ChosenStreams;
  153. /**
  154. * @typedef {{
  155. * playhead: !shaka.media.Playhead,
  156. * mediaSourceEngine: !shaka.media.MediaSourceEngine,
  157. * netEngine: shaka.net.NetworkingEngine,
  158. * onChooseStreams: function(!shakaExtern.Period):
  159. * shaka.media.StreamingEngine.ChosenStreams,
  160. * onCanSwitch: function(),
  161. * onError: function(!shaka.util.Error),
  162. * onEvent: function(!Event),
  163. * onManifestUpdate: function(),
  164. * onSegmentAppended: function(),
  165. * onInitialStreamsSetup: (function()|undefined),
  166. * onStartupComplete: (function()|undefined)
  167. * }}
  168. *
  169. * @property {!shaka.media.Playhead} playhead
  170. * The Playhead. The caller retains ownership.
  171. * @property {!shaka.media.MediaSourceEngine} mediaSourceEngine
  172. * The MediaSourceEngine. The caller retains ownership.
  173. * @property {shaka.net.NetworkingEngine} netEngine
  174. * The NetworkingEngine instance to use. The caller retains ownership.
  175. * @property {function(!shakaExtern.Period):
  176. * shaka.media.StreamingEngine.ChosenStreams} onChooseStreams
  177. * Called by StreamingEngine when the given Period needs to be buffered.
  178. * StreamingEngine will switch to the variant and text stream returned from
  179. * this function.
  180. * The owner cannot call switch() directly until the StreamingEngine calls
  181. * onCanSwitch().
  182. * @property {function()} onCanSwitch
  183. * Called by StreamingEngine when the Period is set up and switching is
  184. * permitted.
  185. * @property {function(!shaka.util.Error)} onError
  186. * Called when an error occurs. If the error is recoverable (see
  187. * {@link shaka.util.Error}) then the caller may invoke either
  188. * StreamingEngine.switch*() or StreamingEngine.seeked() to attempt recovery.
  189. * @property {function(!Event)} onEvent
  190. * Called when an event occurs that should be sent to the app.
  191. * @property {function()} onManifestUpdate
  192. * Called when an embedded 'emsg' box should trigger a manifest update.
  193. * @property {function()} onSegmentAppended
  194. * Called after a segment is successfully appended to a MediaSource.
  195. * @property {(function()|undefined)} onInitialStreamsSetup
  196. * Optional callback which is called when the initial set of Streams have been
  197. * setup. Intended to be used by tests.
  198. * @property {(function()|undefined)} onStartupComplete
  199. * Optional callback which is called when startup has completed. Intended to
  200. * be used by tests.
  201. */
  202. shaka.media.StreamingEngine.PlayerInterface;
  203. /**
  204. * @typedef {{
  205. * type: shaka.util.ManifestParserUtils.ContentType,
  206. * stream: shakaExtern.Stream,
  207. * lastStream: ?shakaExtern.Stream,
  208. * lastSegmentReference: shaka.media.SegmentReference,
  209. * restoreStreamAfterTrickPlay: ?shakaExtern.Stream,
  210. * needInitSegment: boolean,
  211. * needPeriodIndex: number,
  212. * endOfStream: boolean,
  213. * performingUpdate: boolean,
  214. * updateTimer: ?number,
  215. * waitingToClearBuffer: boolean,
  216. * waitingToFlushBuffer: boolean,
  217. * clearingBuffer: boolean,
  218. * recovering: boolean,
  219. * hasError: boolean,
  220. * resumeAt: number
  221. * }}
  222. *
  223. * @description
  224. * Contains the state of a logical stream, i.e., a sequence of segmented data
  225. * for a particular content type. At any given time there is a Stream object
  226. * associated with the state of the logical stream.
  227. *
  228. * @property {shaka.util.ManifestParserUtils.ContentType} type
  229. * The stream's content type, e.g., 'audio', 'video', or 'text'.
  230. * @property {shakaExtern.Stream} stream
  231. * The current Stream.
  232. * @property {?shakaExtern.Stream} lastStream
  233. * The Stream of the last segment that was appended.
  234. * @property {shaka.media.SegmentReference} lastSegmentReference
  235. * The SegmentReference of the last segment that was appended.
  236. * @property {?shakaExtern.Stream} restoreStreamAfterTrickPlay
  237. * The Stream to restore after trick play mode is turned off.
  238. * @property {boolean} needInitSegment
  239. * True indicates that |stream|'s init segment must be inserted before the
  240. * next media segment is appended.
  241. * @property {boolean} endOfStream
  242. * True indicates that the end of the buffer has hit the end of the
  243. * presentation.
  244. * @property {number} needPeriodIndex
  245. * The index of the Period which needs to be buffered.
  246. * @property {boolean} performingUpdate
  247. * True indicates that an update is in progress.
  248. * @property {?number} updateTimer
  249. * A non-null value indicates that an update is scheduled.
  250. * @property {boolean} waitingToClearBuffer
  251. * True indicates that the buffer must be cleared after the current update
  252. * finishes.
  253. * @property {boolean} waitingToFlushBuffer
  254. * True indicates that the buffer must be flushed after it is cleared.
  255. * @property {boolean} clearingBuffer
  256. * True indicates that the buffer is being cleared.
  257. * @property {boolean} recovering
  258. * True indicates that the last segment was not appended because it could not
  259. * fit in the buffer.
  260. * @property {boolean} hasError
  261. * True indicates that the stream has encountered an error and has stopped
  262. * updating.
  263. * @property {number} resumeAt
  264. * An override for the time to start performing updates at. If the playhead
  265. * is behind this time, update_() will still start fetching segments from
  266. * this time. If the playhead is ahead of the time, this field is ignored.
  267. */
  268. shaka.media.StreamingEngine.MediaState_;
  269. /**
  270. * The fudge factor for appendWindowStart. By adjusting the window backward, we
  271. * avoid rounding errors that could cause us to remove the keyframe at the start
  272. * of the Period.
  273. *
  274. * NOTE: This was increased as part of the solution to
  275. * https://github.com/google/shaka-player/issues/1281
  276. *
  277. * @const {number}
  278. * @private
  279. */
  280. shaka.media.StreamingEngine.APPEND_WINDOW_START_FUDGE_ = 0.1;
  281. /**
  282. * The fudge factor for appendWindowEnd. By adjusting the window backward, we
  283. * avoid rounding errors that could cause us to remove the last few samples of
  284. * the Period. This rounding error could then create an artificial gap and a
  285. * stutter when the gap-jumping logic takes over.
  286. *
  287. * https://github.com/google/shaka-player/issues/1597
  288. *
  289. * @const {number}
  290. * @private
  291. */
  292. shaka.media.StreamingEngine.APPEND_WINDOW_END_FUDGE_ = 0.01;
  293. /**
  294. * The maximum number of segments by which a stream can get ahead of other
  295. * streams.
  296. *
  297. * Introduced to keep StreamingEngine from letting one media type get too far
  298. * ahead of another. For example, audio segments are typically much smaller
  299. * than video segments, so in the time it takes to fetch one video segment, we
  300. * could fetch many audio segments. This doesn't help with buffering, though,
  301. * since the intersection of the two buffered ranges is what counts.
  302. *
  303. * @const {number}
  304. * @private
  305. */
  306. shaka.media.StreamingEngine.MAX_RUN_AHEAD_SEGMENTS_ = 1;
  307. /** @override */
  308. shaka.media.StreamingEngine.prototype.destroy = function() {
  309. for (let type in this.mediaStates_) {
  310. this.cancelUpdate_(this.mediaStates_[type]);
  311. }
  312. this.playerInterface_ = null;
  313. this.manifest_ = null;
  314. this.setupPeriodPromise_ = null;
  315. this.canSwitchPeriod_ = null;
  316. this.canSwitchStream_ = null;
  317. this.mediaStates_ = null;
  318. this.config_ = null;
  319. this.destroyed_ = true;
  320. return Promise.resolve();
  321. };
  322. /**
  323. * Called by the Player to provide an updated configuration any time it changes.
  324. * Must be called at least once before init().
  325. *
  326. * @param {shakaExtern.StreamingConfiguration} config
  327. */
  328. shaka.media.StreamingEngine.prototype.configure = function(config) {
  329. this.config_ = config;
  330. // Create separate parameters for backoff during streaming failure.
  331. /** @type {shakaExtern.RetryParameters} */
  332. let failureRetryParams = {
  333. // The term "attempts" includes the initial attempt, plus all retries.
  334. // In order to see a delay, there would have to be at least 2 attempts.
  335. maxAttempts: Math.max(config.retryParameters.maxAttempts, 2),
  336. baseDelay: config.retryParameters.baseDelay,
  337. backoffFactor: config.retryParameters.backoffFactor,
  338. fuzzFactor: config.retryParameters.fuzzFactor,
  339. timeout: 0 // irrelevant
  340. };
  341. // We don't want to ever run out of attempts. The application should be
  342. // allowed to retry streaming infinitely if it wishes.
  343. let autoReset = true;
  344. this.failureCallbackBackoff_ =
  345. new shaka.net.Backoff(failureRetryParams, autoReset);
  346. };
  347. /**
  348. * Initializes the StreamingEngine.
  349. *
  350. * After this function is called the StreamingEngine will call
  351. * onChooseStreams(p) when it needs to buffer Period p and onCanSwitch() when
  352. * any Stream within that Period may be switched to.
  353. *
  354. * After the StreamingEngine calls onChooseStreams(p) for the first time, it
  355. * will begin setting up the Streams returned from that function and
  356. * subsequently switch to them. However, the StreamingEngine will not begin
  357. * setting up any other Streams until at least one segment from each of the
  358. * initial set of Streams has been buffered (this reduces startup latency).
  359. * After the StreamingEngine completes this startup phase it will begin setting
  360. * up each Period's Streams (while buffering in parrallel).
  361. *
  362. * When the StreamingEngine needs to buffer the next Period it will have
  363. * already set up that Period's Streams. So, when the StreamingEngine calls
  364. * onChooseStreams(p) after the first time, the StreamingEngine will
  365. * immediately switch to the Streams returned from that function.
  366. *
  367. * @return {!Promise}
  368. */
  369. shaka.media.StreamingEngine.prototype.init = function() {
  370. goog.asserts.assert(this.config_,
  371. 'StreamingEngine configure() must be called before init()!');
  372. // Determine which Period we must buffer.
  373. let playheadTime = this.playerInterface_.playhead.getTime();
  374. let needPeriodIndex = this.findPeriodContainingTime_(playheadTime);
  375. // Get the initial set of Streams.
  376. let initialStreams = this.playerInterface_.onChooseStreams(
  377. this.manifest_.periods[needPeriodIndex]);
  378. if (!initialStreams.variant && !initialStreams.text) {
  379. shaka.log.error('init: no Streams chosen');
  380. return Promise.reject(new shaka.util.Error(
  381. shaka.util.Error.Severity.CRITICAL,
  382. shaka.util.Error.Category.STREAMING,
  383. shaka.util.Error.Code.INVALID_STREAMS_CHOSEN));
  384. }
  385. // Setup the initial set of Streams and then begin each update cycle. After
  386. // startup completes onUpdate_() will set up the remaining Periods.
  387. return this.initStreams_(initialStreams).then(function() {
  388. if (this.destroyed_) {
  389. return;
  390. }
  391. shaka.log.debug('init: completed initial Stream setup');
  392. // Subtlety: onInitialStreamsSetup() may call switch() or seeked(), so we
  393. // must schedule an update beforehand so |updateTimer| is set.
  394. if (this.playerInterface_ && this.playerInterface_.onInitialStreamsSetup) {
  395. shaka.log.v1('init: calling onInitialStreamsSetup()...');
  396. this.playerInterface_.onInitialStreamsSetup();
  397. }
  398. }.bind(this));
  399. };
  400. /**
  401. * Gets the current Period the stream is in. This Period might not be
  402. * initialized yet if canSwitch(period) has not been called yet.
  403. * @return {shakaExtern.Period}
  404. */
  405. shaka.media.StreamingEngine.prototype.getCurrentPeriod = function() {
  406. let playheadTime = this.playerInterface_.playhead.getTime();
  407. let needPeriodIndex = this.findPeriodContainingTime_(playheadTime);
  408. return this.manifest_.periods[needPeriodIndex];
  409. };
  410. /**
  411. * Gets the Period in which we are currently buffering. This might be different
  412. * from the Period which contains the Playhead.
  413. * @return {?shakaExtern.Period}
  414. */
  415. shaka.media.StreamingEngine.prototype.getActivePeriod = function() {
  416. goog.asserts.assert(this.mediaStates_, 'Must be initialized');
  417. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  418. let anyMediaState = this.mediaStates_[ContentType.VIDEO] ||
  419. this.mediaStates_[ContentType.AUDIO];
  420. return anyMediaState ?
  421. this.manifest_.periods[anyMediaState.needPeriodIndex] : null;
  422. };
  423. /**
  424. * Get the active audio stream. Returns null if there is no audio streaming.
  425. * @return {?shakaExtern.Stream}
  426. */
  427. shaka.media.StreamingEngine.prototype.getActiveAudio = function() {
  428. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  429. return this.getStream_(ContentType.AUDIO);
  430. };
  431. /**
  432. * Get the active video stream. Returns null if there is no video streaming.
  433. * @return {?shakaExtern.Stream}
  434. */
  435. shaka.media.StreamingEngine.prototype.getActiveVideo = function() {
  436. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  437. return this.getStream_(ContentType.VIDEO);
  438. };
  439. /**
  440. * Get the active text stream. Returns null if there is no text streaming.
  441. * @return {?shakaExtern.Stream}
  442. */
  443. shaka.media.StreamingEngine.prototype.getActiveText = function() {
  444. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  445. return this.getStream_(ContentType.TEXT);
  446. };
  447. /**
  448. * Get the active stream for the given type. Returns null if there is no stream
  449. * for the given type.
  450. * @param {shaka.util.ManifestParserUtils.ContentType} type
  451. * @return {?shakaExtern.Stream}
  452. * @private
  453. */
  454. shaka.media.StreamingEngine.prototype.getStream_ = function(type) {
  455. goog.asserts.assert(this.mediaStates_, 'Must be initialized');
  456. let state = this.mediaStates_[type];
  457. if (state) {
  458. // Don't tell the caller about trick play streams. If we're in trick
  459. // play, return the stream we will go back to after we exit trick play.
  460. return state.restoreStreamAfterTrickPlay || state.stream;
  461. } else {
  462. return null;
  463. }
  464. };
  465. /**
  466. * Notifies StreamingEngine that a new text stream was added to the manifest.
  467. * This initializes the given stream. This returns a Promise that resolves when
  468. * the stream has been set up, and a media state has been created.
  469. *
  470. * @param {shakaExtern.Stream} stream
  471. * @return {!Promise}
  472. */
  473. shaka.media.StreamingEngine.prototype.loadNewTextStream = function(
  474. stream) {
  475. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  476. // Clear MediaSource's buffered text, so that the new text stream will
  477. // properly replace the old buffered text.
  478. this.playerInterface_.mediaSourceEngine.clear(ContentType.TEXT);
  479. // Since setupStreams_() is async, if the user hides/shows captions quickly,
  480. // there would be a race condition that a new text media state is created
  481. // but the old media state is not yet deleted.
  482. // The Sequence Id is to avoid that risk condition.
  483. this.textStreamSequenceId_++;
  484. this.unloadingTextStream_ = false;
  485. let currentSequenceId = this.textStreamSequenceId_;
  486. let mediaSourceEngine = this.playerInterface_.mediaSourceEngine;
  487. return mediaSourceEngine.init({text: stream}, /** forceTansmuxTS */ false)
  488. .then(() => {
  489. return this.setupStreams_([stream]);
  490. }).then(() => {
  491. if (this.destroyed_) {
  492. return;
  493. }
  494. if ((this.textStreamSequenceId_ == currentSequenceId) &&
  495. !this.mediaStates_[ContentType.TEXT] && !this.unloadingTextStream_) {
  496. let playheadTime = this.playerInterface_.playhead.getTime();
  497. let needPeriodIndex = this.findPeriodContainingTime_(playheadTime);
  498. this.mediaStates_[ContentType.TEXT] =
  499. this.createMediaState_(stream, needPeriodIndex);
  500. this.scheduleUpdate_(this.mediaStates_[ContentType.TEXT], 0);
  501. }
  502. });
  503. };
  504. /**
  505. * Stop fetching text stream when the user chooses to hide the captions.
  506. */
  507. shaka.media.StreamingEngine.prototype.unloadTextStream = function() {
  508. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  509. this.unloadingTextStream_ = true;
  510. if (this.mediaStates_[ContentType.TEXT]) {
  511. this.cancelUpdate_(this.mediaStates_[ContentType.TEXT]);
  512. delete this.mediaStates_[ContentType.TEXT];
  513. }
  514. };
  515. /**
  516. * Set trick play on or off.
  517. * If trick play is on, related trick play streams will be used when possible.
  518. * @param {boolean} on
  519. */
  520. shaka.media.StreamingEngine.prototype.setTrickPlay = function(on) {
  521. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  522. let mediaState = this.mediaStates_[ContentType.VIDEO];
  523. if (!mediaState) return;
  524. let stream = mediaState.stream;
  525. if (!stream) return;
  526. shaka.log.debug('setTrickPlay', on);
  527. if (on) {
  528. let trickModeVideo = stream.trickModeVideo;
  529. if (!trickModeVideo) return; // Can't engage trick play.
  530. let normalVideo = mediaState.restoreStreamAfterTrickPlay;
  531. if (normalVideo) return; // Already in trick play.
  532. shaka.log.debug('Engaging trick mode stream', trickModeVideo);
  533. this.switchInternal_(trickModeVideo, false);
  534. mediaState.restoreStreamAfterTrickPlay = stream;
  535. } else {
  536. let normalVideo = mediaState.restoreStreamAfterTrickPlay;
  537. if (!normalVideo) return;
  538. shaka.log.debug('Restoring non-trick-mode stream', normalVideo);
  539. mediaState.restoreStreamAfterTrickPlay = null;
  540. this.switchInternal_(normalVideo, true);
  541. }
  542. };
  543. /**
  544. * @param {shakaExtern.Variant} variant
  545. * @param {boolean} clearBuffer
  546. */
  547. shaka.media.StreamingEngine.prototype.switchVariant =
  548. function(variant, clearBuffer) {
  549. if (variant.video) {
  550. this.switchInternal_(variant.video, clearBuffer);
  551. }
  552. if (variant.audio) {
  553. this.switchInternal_(variant.audio, clearBuffer);
  554. }
  555. };
  556. /**
  557. * @param {shakaExtern.Stream} textStream
  558. */
  559. shaka.media.StreamingEngine.prototype.switchTextStream = function(textStream) {
  560. goog.asserts.assert(textStream && textStream.type == 'text',
  561. 'Wrong stream type passed to switchTextStream!');
  562. this.switchInternal_(textStream, /* clearBuffer */ true);
  563. };
  564. /**
  565. * Switches to the given Stream. |stream| may be from any Variant or any Period.
  566. *
  567. * @param {shakaExtern.Stream} stream
  568. * @param {boolean} clearBuffer
  569. * @private
  570. */
  571. shaka.media.StreamingEngine.prototype.switchInternal_ = function(
  572. stream, clearBuffer) {
  573. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  574. let mediaState = this.mediaStates_[/** @type {!ContentType} */(stream.type)];
  575. if (!mediaState && stream.type == ContentType.TEXT &&
  576. this.config_.ignoreTextStreamFailures) {
  577. this.loadNewTextStream(stream);
  578. return;
  579. }
  580. goog.asserts.assert(mediaState, 'switch: expected mediaState to exist');
  581. if (!mediaState) return;
  582. // If we are selecting a stream from a different Period, then we need to
  583. // handle a Period transition. Simply ignore the given stream, assuming that
  584. // Player will select the same track in onChooseStreams.
  585. let periodIndex = this.findPeriodContainingStream_(stream);
  586. if (clearBuffer && periodIndex != mediaState.needPeriodIndex) {
  587. shaka.log.debug('switch: switching to stream in another Period; clearing ' +
  588. 'buffer and changing Periods');
  589. // handlePeriodTransition_ will be called on the next update because the
  590. // current Period won't match the playhead Period.
  591. this.clearAllBuffers_();
  592. return;
  593. }
  594. if (mediaState.restoreStreamAfterTrickPlay) {
  595. shaka.log.debug('switch during trick play mode', stream);
  596. // Already in trick play mode, so stick with trick mode tracks if possible.
  597. if (stream.trickModeVideo) {
  598. // Use the trick mode stream, but revert to the new selection later.
  599. mediaState.restoreStreamAfterTrickPlay = stream;
  600. stream = stream.trickModeVideo;
  601. shaka.log.debug('switch found trick play stream', stream);
  602. } else {
  603. // There is no special trick mode video for this stream!
  604. mediaState.restoreStreamAfterTrickPlay = null;
  605. shaka.log.debug('switch found no special trick play stream');
  606. }
  607. }
  608. // Ensure the Period is ready.
  609. let canSwitchRecord = this.canSwitchPeriod_[periodIndex];
  610. goog.asserts.assert(
  611. canSwitchRecord && canSwitchRecord.resolved,
  612. 'switch: expected Period ' + periodIndex + ' to be ready');
  613. if (!canSwitchRecord || !canSwitchRecord.resolved) return;
  614. // Sanity check. If the Period is ready then the Stream should be ready too.
  615. canSwitchRecord = this.canSwitchStream_[stream.id];
  616. goog.asserts.assert(canSwitchRecord && canSwitchRecord.resolved,
  617. 'switch: expected Stream ' + stream.id + ' to be ready');
  618. if (!canSwitchRecord || !canSwitchRecord.resolved) return;
  619. if (mediaState.stream == stream) {
  620. let streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
  621. shaka.log.debug('switch: Stream ' + streamTag + ' already active');
  622. return;
  623. }
  624. if (stream.type == ContentType.TEXT) {
  625. // Mime types are allowed to change for text streams.
  626. // Reinitialize the text parser, but only if we are going to fetch the init
  627. // segment again.
  628. let fullMimeType = shaka.util.MimeUtils.getFullType(
  629. stream.mimeType, stream.codecs);
  630. this.playerInterface_.mediaSourceEngine.reinitText(fullMimeType);
  631. }
  632. mediaState.stream = stream;
  633. mediaState.needInitSegment = true;
  634. let streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
  635. shaka.log.debug('switch: switching to Stream ' + streamTag);
  636. if (clearBuffer) {
  637. if (mediaState.clearingBuffer) {
  638. // We are already going to clear the buffer, but make sure it is also
  639. // flushed.
  640. mediaState.waitingToFlushBuffer = true;
  641. } else if (mediaState.performingUpdate) {
  642. // We are performing an update, so we have to wait until it's finished.
  643. // onUpdate_() will call clearBuffer_() when the update has finished.
  644. mediaState.waitingToClearBuffer = true;
  645. mediaState.waitingToFlushBuffer = true;
  646. } else {
  647. // Cancel the update timer, if any.
  648. this.cancelUpdate_(mediaState);
  649. // Clear right away.
  650. this.clearBuffer_(mediaState, /* flush */ true);
  651. }
  652. }
  653. };
  654. /**
  655. * Notifies the StreamingEngine that the playhead has moved to a valid time
  656. * within the presentation timeline.
  657. */
  658. shaka.media.StreamingEngine.prototype.seeked = function() {
  659. goog.asserts.assert(this.mediaStates_, 'Must not be destroyed');
  660. let playheadTime = this.playerInterface_.playhead.getTime();
  661. const smallGapLimit = this.config_.smallGapLimit;
  662. let isAllBuffered = Object.keys(this.mediaStates_).every(function(type) {
  663. return this.playerInterface_.mediaSourceEngine.isBuffered(
  664. type, playheadTime, smallGapLimit);
  665. }.bind(this));
  666. // Only treat this as a buffered seek if every media state has a buffer. For
  667. // example, if we have buffered text but not video, we should still clear
  668. // every buffer so all media states need the same Period.
  669. if (isAllBuffered) {
  670. shaka.log.debug(
  671. '(all): seeked: buffered seek: playheadTime=' + playheadTime);
  672. return;
  673. }
  674. // This was an unbuffered seek for at least one stream, so clear all buffers.
  675. // Don't clear only some of the buffers because we can become stalled since
  676. // the media states are waiting for different Periods.
  677. shaka.log.debug('(all): seeked: unbuffered seek: clearing all buffers');
  678. this.clearAllBuffers_();
  679. };
  680. /**
  681. * Clears the buffer for every stream. Unlike clearBuffer_, this will handle
  682. * cases where a MediaState is performing an update. After this runs, every
  683. * MediaState will have a pending update.
  684. * @private
  685. */
  686. shaka.media.StreamingEngine.prototype.clearAllBuffers_ = function() {
  687. for (let type in this.mediaStates_) {
  688. let mediaState = this.mediaStates_[type];
  689. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  690. if (mediaState.clearingBuffer) {
  691. // We're already clearing the buffer, so we don't need to clear the
  692. // buffer again.
  693. shaka.log.debug(logPrefix, 'clear: already clearing the buffer');
  694. continue;
  695. }
  696. if (mediaState.waitingToClearBuffer) {
  697. // May not be performing an update, but an update will still happen.
  698. // See: https://github.com/google/shaka-player/issues/334
  699. shaka.log.debug(logPrefix, 'clear: already waiting');
  700. continue;
  701. }
  702. if (mediaState.performingUpdate) {
  703. // We are performing an update, so we have to wait until it's finished.
  704. // onUpdate_() will call clearBuffer_() when the update has finished.
  705. shaka.log.debug(logPrefix, 'clear: currently updating');
  706. mediaState.waitingToClearBuffer = true;
  707. continue;
  708. }
  709. if (this.playerInterface_.mediaSourceEngine.bufferStart(type) == null) {
  710. // Nothing buffered.
  711. shaka.log.debug(logPrefix, 'clear: nothing buffered');
  712. if (mediaState.updateTimer == null) {
  713. // Note: an update cycle stops when we buffer to the end of the
  714. // presentation or Period, or when we raise an error.
  715. this.scheduleUpdate_(mediaState, 0);
  716. }
  717. continue;
  718. }
  719. // An update may be scheduled, but we can just cancel it and clear the
  720. // buffer right away. Note: clearBuffer_() will schedule the next update.
  721. shaka.log.debug(logPrefix, 'clear: handling right now');
  722. this.cancelUpdate_(mediaState);
  723. this.clearBuffer_(mediaState, /* flush */ false);
  724. }
  725. };
  726. /**
  727. * Initializes the given streams and media states if required. This will
  728. * schedule updates for the given types.
  729. *
  730. * @param {shaka.media.StreamingEngine.ChosenStreams} chosenStreams
  731. * @param {number=} opt_resumeAt
  732. * @return {!Promise}
  733. * @private
  734. */
  735. shaka.media.StreamingEngine.prototype.initStreams_ = function(
  736. chosenStreams, opt_resumeAt) {
  737. goog.asserts.assert(this.config_,
  738. 'StreamingEngine configure() must be called before init()!');
  739. // Determine which Period we must buffer.
  740. let playheadTime = this.playerInterface_.playhead.getTime();
  741. let needPeriodIndex = this.findPeriodContainingTime_(playheadTime);
  742. // Init/re-init MediaSourceEngine. Note that a re-init is only valid for text.
  743. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  744. /** @type {!Object.<!ContentType, shakaExtern.Stream>} */
  745. let streamsByType = {};
  746. /** @type {!Array.<shakaExtern.Stream>} */
  747. let streams = [];
  748. if (chosenStreams.variant && chosenStreams.variant.audio) {
  749. streamsByType[ContentType.AUDIO] = chosenStreams.variant.audio;
  750. streams.push(chosenStreams.variant.audio);
  751. }
  752. if (chosenStreams.variant && chosenStreams.variant.video) {
  753. streamsByType[ContentType.VIDEO] = chosenStreams.variant.video;
  754. streams.push(chosenStreams.variant.video);
  755. }
  756. if (chosenStreams.text) {
  757. streamsByType[ContentType.TEXT] = chosenStreams.text;
  758. streams.push(chosenStreams.text);
  759. }
  760. // Init MediaSourceEngine.
  761. let mediaSourceEngine = this.playerInterface_.mediaSourceEngine;
  762. let forceTransmuxTS = this.config_.forceTransmuxTS;
  763. return mediaSourceEngine.init(streamsByType, forceTransmuxTS).then(() => {
  764. if (this.destroyed_) {
  765. return;
  766. }
  767. this.setDuration_();
  768. // Setup the initial set of Streams and then begin each update cycle. After
  769. // startup completes onUpdate_() will set up the remaining Periods.
  770. return this.setupStreams_(streams);
  771. }).then(() => {
  772. if (this.destroyed_) {
  773. return;
  774. }
  775. for (let type in streamsByType) {
  776. let stream = streamsByType[type];
  777. if (!this.mediaStates_[type]) {
  778. this.mediaStates_[type] =
  779. this.createMediaState_(stream, needPeriodIndex, opt_resumeAt);
  780. this.scheduleUpdate_(this.mediaStates_[type], 0);
  781. }
  782. }
  783. });
  784. };
  785. /**
  786. * Creates a media state.
  787. *
  788. * @param {shakaExtern.Stream} stream
  789. * @param {number} needPeriodIndex
  790. * @param {number=} opt_resumeAt
  791. * @return {shaka.media.StreamingEngine.MediaState_}
  792. * @private
  793. */
  794. shaka.media.StreamingEngine.prototype.createMediaState_ = function(
  795. stream, needPeriodIndex, opt_resumeAt) {
  796. return /** @type {shaka.media.StreamingEngine.MediaState_} */ ({
  797. stream: stream,
  798. type: stream.type,
  799. lastStream: null,
  800. lastSegmentReference: null,
  801. restoreStreamAfterTrickPlay: null,
  802. needInitSegment: true,
  803. needPeriodIndex: needPeriodIndex,
  804. endOfStream: false,
  805. performingUpdate: false,
  806. updateTimer: null,
  807. waitingToClearBuffer: false,
  808. waitingToFlushBuffer: false,
  809. clearingBuffer: false,
  810. recovering: false,
  811. hasError: false,
  812. resumeAt: opt_resumeAt || 0
  813. });
  814. };
  815. /**
  816. * Sets up the given Period if necessary. Calls onError() if an error occurs.
  817. *
  818. * @param {number} periodIndex The Period's index.
  819. * @return {!Promise} A Promise which resolves when the given Period is set up.
  820. * @private
  821. */
  822. shaka.media.StreamingEngine.prototype.setupPeriod_ = function(periodIndex) {
  823. const Functional = shaka.util.Functional;
  824. let canSwitchRecord = this.canSwitchPeriod_[periodIndex];
  825. if (canSwitchRecord) {
  826. shaka.log.debug(
  827. '(all) Period ' + periodIndex + ' is being or has been set up');
  828. goog.asserts.assert(canSwitchRecord.promise, 'promise must not be null');
  829. return canSwitchRecord.promise;
  830. }
  831. shaka.log.debug('(all) setting up Period ' + periodIndex);
  832. canSwitchRecord = {
  833. promise: new shaka.util.PublicPromise(),
  834. resolved: false
  835. };
  836. this.canSwitchPeriod_[periodIndex] = canSwitchRecord;
  837. let streams = this.manifest_.periods[periodIndex].variants
  838. .map(function(variant) {
  839. let result = [];
  840. if (variant.audio) {
  841. result.push(variant.audio);
  842. }
  843. if (variant.video) {
  844. result.push(variant.video);
  845. }
  846. if (variant.video && variant.video.trickModeVideo) {
  847. result.push(variant.video.trickModeVideo);
  848. }
  849. return result;
  850. })
  851. .reduce(Functional.collapseArrays, [])
  852. .filter(Functional.isNotDuplicate);
  853. // Add text streams
  854. streams.push.apply(streams, this.manifest_.periods[periodIndex].textStreams);
  855. // Serialize Period set up.
  856. this.setupPeriodPromise_ = this.setupPeriodPromise_.then(function() {
  857. if (this.destroyed_) return;
  858. return this.setupStreams_(streams);
  859. }.bind(this)).then(function() {
  860. if (this.destroyed_) return;
  861. this.canSwitchPeriod_[periodIndex].promise.resolve();
  862. this.canSwitchPeriod_[periodIndex].resolved = true;
  863. shaka.log.v1('(all) setup Period ' + periodIndex);
  864. }.bind(this)).catch(function(error) {
  865. if (this.destroyed_) return;
  866. this.canSwitchPeriod_[periodIndex].promise.catch(() => {});
  867. this.canSwitchPeriod_[periodIndex].promise.reject();
  868. delete this.canSwitchPeriod_[periodIndex];
  869. shaka.log.warning('(all) failed to setup Period ' + periodIndex);
  870. this.playerInterface_.onError(error);
  871. // Don't stop other Periods from being set up.
  872. }.bind(this));
  873. return canSwitchRecord.promise;
  874. };
  875. /**
  876. * Sets up the given Streams if necessary. Does NOT call onError() if an
  877. * error occurs.
  878. *
  879. * @param {!Array.<!shakaExtern.Stream>} streams
  880. * @return {!Promise}
  881. * @private
  882. */
  883. shaka.media.StreamingEngine.prototype.setupStreams_ = function(streams) {
  884. // Make sure that all the streams have unique ids.
  885. // (Duplicate ids will cause the player to hang).
  886. let uniqueStreamIds = streams.map(function(s) { return s.id; })
  887. .filter(shaka.util.Functional.isNotDuplicate);
  888. goog.asserts.assert(uniqueStreamIds.length == streams.length,
  889. 'streams should have unique ids');
  890. // Parallelize Stream set up.
  891. let async = [];
  892. for (let i = 0; i < streams.length; ++i) {
  893. let stream = streams[i];
  894. let canSwitchRecord = this.canSwitchStream_[stream.id];
  895. if (canSwitchRecord) {
  896. shaka.log.debug(
  897. '(all) Stream ' + stream.id + ' is being or has been set up');
  898. async.push(canSwitchRecord.promise);
  899. } else {
  900. shaka.log.v1('(all) setting up Stream ' + stream.id);
  901. this.canSwitchStream_[stream.id] = {
  902. promise: new shaka.util.PublicPromise(),
  903. resolved: false
  904. };
  905. async.push(stream.createSegmentIndex());
  906. }
  907. }
  908. return Promise.all(async).then(function() {
  909. if (this.destroyed_) return;
  910. for (let i = 0; i < streams.length; ++i) {
  911. let stream = streams[i];
  912. let canSwitchRecord = this.canSwitchStream_[stream.id];
  913. if (!canSwitchRecord.resolved) {
  914. canSwitchRecord.promise.resolve();
  915. canSwitchRecord.resolved = true;
  916. shaka.log.v1('(all) setup Stream ' + stream.id);
  917. }
  918. }
  919. }.bind(this)).catch(function(error) {
  920. if (this.destroyed_) return;
  921. for (let i = 0; i < streams.length; i++) {
  922. this.canSwitchStream_[streams[i].id].promise.catch(() => {});
  923. this.canSwitchStream_[streams[i].id].promise.reject();
  924. delete this.canSwitchStream_[streams[i].id];
  925. }
  926. return Promise.reject(error);
  927. }.bind(this));
  928. };
  929. /**
  930. * Sets the MediaSource's duration.
  931. * @private
  932. */
  933. shaka.media.StreamingEngine.prototype.setDuration_ = function() {
  934. let duration = this.manifest_.presentationTimeline.getDuration();
  935. if (duration < Infinity) {
  936. this.playerInterface_.mediaSourceEngine.setDuration(duration);
  937. } else {
  938. // Not all platforms support infinite durations, so set a finite duration
  939. // so we can append segments and so the user agent can seek.
  940. this.playerInterface_.mediaSourceEngine.setDuration(Math.pow(2, 32));
  941. }
  942. };
  943. /**
  944. * Called when |mediaState|'s update timer has expired.
  945. *
  946. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  947. * @private
  948. */
  949. shaka.media.StreamingEngine.prototype.onUpdate_ = function(mediaState) {
  950. const MapUtils = shaka.util.MapUtils;
  951. if (this.destroyed_) return;
  952. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  953. // Sanity check.
  954. goog.asserts.assert(
  955. !mediaState.performingUpdate && (mediaState.updateTimer != null),
  956. logPrefix + ' unexpected call to onUpdate_()');
  957. if (mediaState.performingUpdate || (mediaState.updateTimer == null)) return;
  958. goog.asserts.assert(
  959. !mediaState.clearingBuffer,
  960. logPrefix + ' onUpdate_() should not be called when clearing the buffer');
  961. if (mediaState.clearingBuffer) return;
  962. mediaState.updateTimer = null;
  963. // Handle pending buffer clears.
  964. if (mediaState.waitingToClearBuffer) {
  965. // Note: clearBuffer_() will schedule the next update.
  966. shaka.log.debug(logPrefix, 'skipping update and clearing the buffer');
  967. this.clearBuffer_(mediaState, mediaState.waitingToFlushBuffer);
  968. return;
  969. }
  970. // Update the MediaState.
  971. try {
  972. let delay = this.update_(mediaState);
  973. if (delay != null) {
  974. this.scheduleUpdate_(mediaState, delay);
  975. mediaState.hasError = false;
  976. }
  977. } catch (error) {
  978. this.handleStreamingError_(error);
  979. return;
  980. }
  981. goog.asserts.assert(this.mediaStates_, 'must not be destroyed');
  982. let mediaStates = MapUtils.values(this.mediaStates_);
  983. // Check if we've buffered to the end of the Period.
  984. this.handlePeriodTransition_(mediaState);
  985. // Check if we've buffered to the end of the presentation.
  986. if (mediaStates.every(function(ms) { return ms.endOfStream; })) {
  987. shaka.log.v1(logPrefix, 'calling endOfStream()...');
  988. this.playerInterface_.mediaSourceEngine.endOfStream().then(function() {
  989. if (this.destroyed_) {
  990. return;
  991. }
  992. // If the media segments don't reach the end, then we need to update the
  993. // timeline duration to match the final media duration to avoid buffering
  994. // forever at the end. We should only do this if the duration needs to
  995. // shrink. Growing it by less than 1ms can actually cause buffering on
  996. // replay, as in https://github.com/google/shaka-player/issues/979
  997. let duration = this.playerInterface_.mediaSourceEngine.getDuration();
  998. if (duration < this.manifest_.presentationTimeline.getDuration()) {
  999. this.manifest_.presentationTimeline.setDuration(duration);
  1000. }
  1001. }.bind(this));
  1002. }
  1003. };
  1004. /**
  1005. * Updates the given MediaState.
  1006. *
  1007. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1008. * @return {?number} The number of seconds to wait until updating again or
  1009. * null if another update does not need to be scheduled.
  1010. * @throws {!shaka.util.Error} if an error occurs.
  1011. * @private
  1012. */
  1013. shaka.media.StreamingEngine.prototype.update_ = function(mediaState) {
  1014. const MapUtils = shaka.util.MapUtils;
  1015. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1016. // Compute how far we've buffered ahead of the playhead.
  1017. let playheadTime = this.playerInterface_.playhead.getTime();
  1018. // Get the next timestamp we need.
  1019. let timeNeeded = this.getTimeNeeded_(mediaState, playheadTime);
  1020. shaka.log.v2(logPrefix, 'timeNeeded=' + timeNeeded);
  1021. let currentPeriodIndex = this.findPeriodContainingStream_(mediaState.stream);
  1022. let needPeriodIndex = this.findPeriodContainingTime_(timeNeeded);
  1023. // Get the amount of content we have buffered, accounting for drift. This
  1024. // is only used to determine if we have meet the buffering goal. This should
  1025. // be the same method that PlayheadObserver uses.
  1026. let bufferedAhead = this.playerInterface_.mediaSourceEngine.bufferedAheadOf(
  1027. mediaState.type, playheadTime);
  1028. shaka.log.v2(logPrefix,
  1029. 'update_:',
  1030. 'playheadTime=' + playheadTime,
  1031. 'bufferedAhead=' + bufferedAhead);
  1032. let bufferingGoal = this.getBufferingGoal_();
  1033. // Check if we've buffered to the end of the presentation.
  1034. if (timeNeeded >= this.manifest_.presentationTimeline.getDuration()) {
  1035. // We shouldn't rebuffer if the playhead is close to the end of the
  1036. // presentation.
  1037. shaka.log.debug(logPrefix, 'buffered to end of presentation');
  1038. mediaState.endOfStream = true;
  1039. return null;
  1040. }
  1041. mediaState.endOfStream = false;
  1042. // Check if we've buffered to the end of the Period. This should be done
  1043. // before checking segment availability because the new Period may become
  1044. // available once it's switched to. Note that we don't use the non-existence
  1045. // of SegmentReferences as an indicator to determine Period boundaries
  1046. // because a SegmentIndex can provide SegmentReferences outside its Period.
  1047. mediaState.needPeriodIndex = needPeriodIndex;
  1048. if (needPeriodIndex != currentPeriodIndex) {
  1049. shaka.log.debug(logPrefix,
  1050. 'need Period ' + needPeriodIndex,
  1051. 'playheadTime=' + playheadTime,
  1052. 'timeNeeded=' + timeNeeded,
  1053. 'currentPeriodIndex=' + currentPeriodIndex);
  1054. return null;
  1055. }
  1056. // If we've buffered to the buffering goal then schedule an update.
  1057. if (bufferedAhead >= bufferingGoal) {
  1058. shaka.log.v2(logPrefix, 'buffering goal met');
  1059. // Do not try to predict the next update. Just poll twice every second.
  1060. // The playback rate can change at any time, so any prediction we make now
  1061. // could be terribly invalid soon.
  1062. return 0.5;
  1063. }
  1064. let bufferEnd =
  1065. this.playerInterface_.mediaSourceEngine.bufferEnd(mediaState.type);
  1066. let reference = this.getSegmentReferenceNeeded_(
  1067. mediaState, playheadTime, bufferEnd, currentPeriodIndex);
  1068. if (!reference) {
  1069. // The segment could not be found, does not exist, or is not available. In
  1070. // any case just try again... if the manifest is incomplete or is not being
  1071. // updated then we'll idle forever; otherwise, we'll end up getting a
  1072. // SegmentReference eventually.
  1073. return 1;
  1074. }
  1075. // Do not let any one stream get far ahead of any other.
  1076. let minTimeNeeded = Infinity;
  1077. goog.asserts.assert(this.mediaStates_, 'must not be destroyed');
  1078. const mediaStates = MapUtils.values(this.mediaStates_);
  1079. mediaStates.forEach((otherState) => {
  1080. const timeNeeded = this.getTimeNeeded_(otherState, playheadTime);
  1081. minTimeNeeded = Math.min(minTimeNeeded, timeNeeded);
  1082. });
  1083. const maxSegmentDuration =
  1084. this.manifest_.presentationTimeline.getMaxSegmentDuration();
  1085. const maxRunAhead =
  1086. maxSegmentDuration * shaka.media.StreamingEngine.MAX_RUN_AHEAD_SEGMENTS_;
  1087. if (timeNeeded >= minTimeNeeded + maxRunAhead) {
  1088. // Wait and give other media types time to catch up to this one.
  1089. // For example, let video buffering catch up to audio buffering before
  1090. // fetching another audio segment.
  1091. return 1;
  1092. }
  1093. mediaState.resumeAt = 0;
  1094. this.fetchAndAppend_(mediaState, playheadTime, currentPeriodIndex, reference);
  1095. return null;
  1096. };
  1097. /**
  1098. * Computes buffering goal.
  1099. *
  1100. * @return {number}
  1101. * @private
  1102. */
  1103. shaka.media.StreamingEngine.prototype.getBufferingGoal_ = function() {
  1104. goog.asserts.assert(this.manifest_, 'manifest_ should not be null');
  1105. goog.asserts.assert(this.config_, 'config_ should not be null');
  1106. let rebufferingGoal = shaka.util.StreamUtils.getRebufferingGoal(
  1107. this.manifest_, this.config_, this.bufferingGoalScale_);
  1108. return Math.max(
  1109. rebufferingGoal,
  1110. this.bufferingGoalScale_ * this.config_.bufferingGoal);
  1111. };
  1112. /**
  1113. * Gets the next timestamp needed. Returns the playhead's position if the
  1114. * buffer is empty; otherwise, returns the time at which the last segment
  1115. * appended ends.
  1116. *
  1117. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1118. * @param {number} playheadTime
  1119. * @return {number} The next timestamp needed.
  1120. * @throws {!shaka.util.Error} if the buffer is inconsistent with our
  1121. * expectations.
  1122. * @private
  1123. */
  1124. shaka.media.StreamingEngine.prototype.getTimeNeeded_ = function(
  1125. mediaState, playheadTime) {
  1126. // Get the next timestamp we need. We must use |lastSegmentReference|
  1127. // to determine this and not the actual buffer for two reasons:
  1128. // 1. Actual segments end slightly before their advertised end times, so
  1129. // the next timestamp we need is actually larger than |bufferEnd|.
  1130. // 2. There may be drift (the timestamps in the segments are ahead/behind
  1131. // of the timestamps in the manifest), but we need drift-free times when
  1132. // comparing times against presentation and Period boundaries.
  1133. if (!mediaState.lastStream || !mediaState.lastSegmentReference) {
  1134. return Math.max(playheadTime, mediaState.resumeAt);
  1135. }
  1136. let lastPeriodIndex =
  1137. this.findPeriodContainingStream_(mediaState.lastStream);
  1138. let lastPeriod = this.manifest_.periods[lastPeriodIndex];
  1139. return lastPeriod.startTime + mediaState.lastSegmentReference.endTime;
  1140. };
  1141. /**
  1142. * Gets the SegmentReference of the next segment needed.
  1143. *
  1144. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1145. * @param {number} playheadTime
  1146. * @param {?number} bufferEnd
  1147. * @param {number} currentPeriodIndex
  1148. * @return {shaka.media.SegmentReference} The SegmentReference of the
  1149. * next segment needed. Returns null if a segment could not be found, does not
  1150. * exist, or is not available.
  1151. * @private
  1152. */
  1153. shaka.media.StreamingEngine.prototype.getSegmentReferenceNeeded_ = function(
  1154. mediaState, playheadTime, bufferEnd, currentPeriodIndex) {
  1155. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1156. if (mediaState.lastSegmentReference &&
  1157. mediaState.stream == mediaState.lastStream) {
  1158. // Something is buffered from the same Stream.
  1159. let position = mediaState.lastSegmentReference.position + 1;
  1160. shaka.log.v2(logPrefix, 'next position known:', 'position=' + position);
  1161. return this.getSegmentReferenceIfAvailable_(
  1162. mediaState, currentPeriodIndex, position);
  1163. }
  1164. let position;
  1165. if (mediaState.lastSegmentReference) {
  1166. // Something is buffered from another Stream.
  1167. goog.asserts.assert(mediaState.lastStream, 'lastStream should not be null');
  1168. shaka.log.v1(logPrefix, 'next position unknown: another Stream buffered');
  1169. let lastPeriodIndex =
  1170. this.findPeriodContainingStream_(mediaState.lastStream);
  1171. let lastPeriod = this.manifest_.periods[lastPeriodIndex];
  1172. position = this.lookupSegmentPosition_(
  1173. mediaState,
  1174. lastPeriod.startTime + mediaState.lastSegmentReference.endTime,
  1175. currentPeriodIndex);
  1176. } else {
  1177. // Either nothing is buffered, or we have cleared part of the buffer. If
  1178. // we still have some buffered, use that time to find the segment, otherwise
  1179. // start at the playhead time.
  1180. goog.asserts.assert(!mediaState.lastStream, 'lastStream should be null');
  1181. shaka.log.v1(logPrefix, 'next position unknown: nothing buffered');
  1182. position = this.lookupSegmentPosition_(
  1183. mediaState, bufferEnd || playheadTime, currentPeriodIndex);
  1184. }
  1185. if (position == null) {
  1186. return null;
  1187. }
  1188. let reference = null;
  1189. if (bufferEnd == null) {
  1190. // If there's positive drift then we need to get the previous segment;
  1191. // however, we don't actually know how much drift there is, so we must
  1192. // unconditionally get the previous segment. If it turns out that there's
  1193. // non-positive drift then we'll just end up buffering beind the playhead a
  1194. // little more than we needed.
  1195. let optimalPosition = Math.max(0, position - 1);
  1196. reference = this.getSegmentReferenceIfAvailable_(
  1197. mediaState, currentPeriodIndex, optimalPosition);
  1198. }
  1199. return reference ||
  1200. this.getSegmentReferenceIfAvailable_(
  1201. mediaState, currentPeriodIndex, position);
  1202. };
  1203. /**
  1204. * Looks up the position of the segment containing the given timestamp.
  1205. *
  1206. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1207. * @param {number} presentationTime The timestamp needed, relative to the
  1208. * start of the presentation.
  1209. * @param {number} currentPeriodIndex
  1210. * @return {?number} A segment position, or null if a segment was not be found.
  1211. * @private
  1212. */
  1213. shaka.media.StreamingEngine.prototype.lookupSegmentPosition_ = function(
  1214. mediaState, presentationTime, currentPeriodIndex) {
  1215. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1216. let currentPeriod = this.manifest_.periods[currentPeriodIndex];
  1217. shaka.log.debug(logPrefix,
  1218. 'looking up segment:',
  1219. 'presentationTime=' + presentationTime,
  1220. 'currentPeriod.startTime=' + currentPeriod.startTime);
  1221. let lookupTime = Math.max(0, presentationTime - currentPeriod.startTime);
  1222. let position = mediaState.stream.findSegmentPosition(lookupTime);
  1223. if (position == null) {
  1224. shaka.log.warning(logPrefix,
  1225. 'cannot find segment:',
  1226. 'currentPeriod.startTime=' + currentPeriod.startTime,
  1227. 'lookupTime=' + lookupTime);
  1228. }
  1229. return position;
  1230. };
  1231. /**
  1232. * Gets the SegmentReference at the given position if it's available.
  1233. *
  1234. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1235. * @param {number} currentPeriodIndex
  1236. * @param {number} position
  1237. * @return {shaka.media.SegmentReference}
  1238. *
  1239. * @private
  1240. */
  1241. shaka.media.StreamingEngine.prototype.getSegmentReferenceIfAvailable_ =
  1242. function(mediaState, currentPeriodIndex, position) {
  1243. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1244. let currentPeriod = this.manifest_.periods[currentPeriodIndex];
  1245. let reference = mediaState.stream.getSegmentReference(position);
  1246. if (!reference) {
  1247. shaka.log.v1(logPrefix,
  1248. 'segment does not exist:',
  1249. 'currentPeriod.startTime=' + currentPeriod.startTime,
  1250. 'position=' + position);
  1251. return null;
  1252. }
  1253. let timeline = this.manifest_.presentationTimeline;
  1254. let availabilityStart = timeline.getSegmentAvailabilityStart();
  1255. let availabilityEnd = timeline.getSegmentAvailabilityEnd();
  1256. if ((currentPeriod.startTime + reference.endTime < availabilityStart) ||
  1257. (currentPeriod.startTime + reference.startTime > availabilityEnd)) {
  1258. shaka.log.v2(logPrefix,
  1259. 'segment is not available:',
  1260. 'currentPeriod.startTime=' + currentPeriod.startTime,
  1261. 'reference.startTime=' + reference.startTime,
  1262. 'reference.endTime=' + reference.endTime,
  1263. 'availabilityStart=' + availabilityStart,
  1264. 'availabilityEnd=' + availabilityEnd);
  1265. return null;
  1266. }
  1267. return reference;
  1268. };
  1269. /**
  1270. * Fetches and appends the given segment. Sets up the given MediaState's
  1271. * associated SourceBuffer and evicts segments if either are required
  1272. * beforehand. Schedules another update after completing successfully.
  1273. *
  1274. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1275. * @param {number} playheadTime
  1276. * @param {number} currentPeriodIndex The index of the current Period.
  1277. * @param {!shaka.media.SegmentReference} reference
  1278. * @private
  1279. */
  1280. shaka.media.StreamingEngine.prototype.fetchAndAppend_ = function(
  1281. mediaState, playheadTime, currentPeriodIndex, reference) {
  1282. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1283. const StreamingEngine = shaka.media.StreamingEngine;
  1284. const logPrefix = StreamingEngine.logPrefix_(mediaState);
  1285. const currentPeriod = this.manifest_.periods[currentPeriodIndex];
  1286. shaka.log.v1(logPrefix,
  1287. 'fetchAndAppend_:',
  1288. 'playheadTime=' + playheadTime,
  1289. 'currentPeriod.startTime=' + currentPeriod.startTime,
  1290. 'reference.position=' + reference.position,
  1291. 'reference.startTime=' + reference.startTime,
  1292. 'reference.endTime=' + reference.endTime);
  1293. // Subtlety: The playhead may move while asynchronous update operations are
  1294. // in progress, so we should avoid calling playhead.getTime() in any
  1295. // callbacks. Furthermore, switch() may be called at any time, so we should
  1296. // also avoid using mediaState.stream or mediaState.needInitSegment in any
  1297. // callbacks.
  1298. let stream = mediaState.stream;
  1299. // Compute the append window.
  1300. let duration = this.manifest_.presentationTimeline.getDuration();
  1301. let followingPeriod = this.manifest_.periods[currentPeriodIndex + 1];
  1302. // Rounding issues can cause us to remove the first frame of the Period, so
  1303. // reduce the start time slightly.
  1304. const appendWindowStart = Math.max(0,
  1305. currentPeriod.startTime - StreamingEngine.APPEND_WINDOW_START_FUDGE_);
  1306. const appendWindowEnd = followingPeriod ?
  1307. followingPeriod.startTime + StreamingEngine.APPEND_WINDOW_END_FUDGE_ :
  1308. duration;
  1309. goog.asserts.assert(
  1310. reference.startTime <= appendWindowEnd,
  1311. logPrefix + ' segment should start before append window end');
  1312. let initSourceBuffer = this.initSourceBuffer_(
  1313. mediaState, currentPeriodIndex, appendWindowStart, appendWindowEnd);
  1314. mediaState.performingUpdate = true;
  1315. // We may set |needInitSegment| to true in switch(), so set it to false here,
  1316. // since we want it to remain true if switch() is called.
  1317. mediaState.needInitSegment = false;
  1318. shaka.log.v2(logPrefix, 'fetching segment');
  1319. let fetchSegment = this.fetch_(reference);
  1320. Promise.all([initSourceBuffer, fetchSegment]).then(function(results) {
  1321. if (this.destroyed_ || this.fatalError_) return;
  1322. return this.append_(mediaState,
  1323. playheadTime,
  1324. currentPeriod,
  1325. stream,
  1326. reference,
  1327. results[1]);
  1328. }.bind(this)).then(function() {
  1329. if (this.destroyed_ || this.fatalError_) return;
  1330. mediaState.performingUpdate = false;
  1331. mediaState.recovering = false;
  1332. if (!mediaState.waitingToClearBuffer) {
  1333. this.playerInterface_.onSegmentAppended();
  1334. }
  1335. // Update right away.
  1336. this.scheduleUpdate_(mediaState, 0);
  1337. // Subtlety: handleStartup_() calls onStartupComplete() which may call
  1338. // switch() or seeked(), so we must schedule an update beforehand so
  1339. // |updateTimer| is set.
  1340. this.handleStartup_(mediaState, stream);
  1341. shaka.log.v1(logPrefix, 'finished fetch and append');
  1342. }.bind(this)).catch(function(error) {
  1343. if (this.destroyed_ || this.fatalError_) return;
  1344. goog.asserts.assert(error instanceof shaka.util.Error,
  1345. 'Should only receive a Shaka error');
  1346. mediaState.performingUpdate = false;
  1347. if (mediaState.type == ContentType.TEXT &&
  1348. this.config_.ignoreTextStreamFailures) {
  1349. if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS) {
  1350. shaka.log.warning(logPrefix,
  1351. 'Text stream failed to download. Proceeding without it.');
  1352. } else {
  1353. shaka.log.warning(logPrefix,
  1354. 'Text stream failed to parse. Proceeding without it.');
  1355. }
  1356. delete this.mediaStates_[ContentType.TEXT];
  1357. } else if (error.code == shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR) {
  1358. this.handleQuotaExceeded_(mediaState, error);
  1359. } else {
  1360. shaka.log.error(logPrefix, 'failed fetch and append: code=' + error.code);
  1361. mediaState.hasError = true;
  1362. error.severity = shaka.util.Error.Severity.CRITICAL;
  1363. this.handleStreamingError_(error);
  1364. }
  1365. }.bind(this));
  1366. };
  1367. /**
  1368. * Clear per-stream error states and retry any failed streams.
  1369. * @return {boolean} False if unable to retry.
  1370. */
  1371. shaka.media.StreamingEngine.prototype.retry = function() {
  1372. if (this.destroyed_) {
  1373. shaka.log.error('Unable to retry after StreamingEngine is destroyed!');
  1374. return false;
  1375. }
  1376. if (this.fatalError_) {
  1377. shaka.log.error('Unable to retry after StreamingEngine encountered a ' +
  1378. 'fatal error!');
  1379. return false;
  1380. }
  1381. for (let type in this.mediaStates_) {
  1382. let mediaState = this.mediaStates_[type];
  1383. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1384. if (mediaState.hasError) {
  1385. shaka.log.info(logPrefix, 'Retrying after failure...');
  1386. mediaState.hasError = false;
  1387. this.scheduleUpdate_(mediaState, 0.1);
  1388. }
  1389. }
  1390. return true;
  1391. };
  1392. /**
  1393. * Handles a QUOTA_EXCEEDED_ERROR.
  1394. *
  1395. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1396. * @param {!shaka.util.Error} error
  1397. * @private
  1398. */
  1399. shaka.media.StreamingEngine.prototype.handleQuotaExceeded_ = function(
  1400. mediaState, error) {
  1401. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1402. // The segment cannot fit into the SourceBuffer. Ideally, MediaSource would
  1403. // have evicted old data to accommodate the segment; however, it may have
  1404. // failed to do this if the segment is very large, or if it could not find
  1405. // a suitable time range to remove.
  1406. //
  1407. // We can overcome the latter by trying to append the segment again;
  1408. // however, to avoid continuous QuotaExceededErrors we must reduce the size
  1409. // of the buffer going forward.
  1410. //
  1411. // If we've recently reduced the buffering goals, wait until the stream
  1412. // which caused the first QuotaExceededError recovers. Doing this ensures
  1413. // we don't reduce the buffering goals too quickly.
  1414. goog.asserts.assert(this.mediaStates_, 'must not be destroyed');
  1415. let mediaStates = shaka.util.MapUtils.values(this.mediaStates_);
  1416. let waitingForAnotherStreamToRecover = mediaStates.some(function(ms) {
  1417. return ms != mediaState && ms.recovering;
  1418. });
  1419. if (!waitingForAnotherStreamToRecover) {
  1420. // Reduction schedule: 80%, 60%, 40%, 20%, 16%, 12%, 8%, 4%, fail.
  1421. // Note: percentages are used for comparisons to avoid rounding errors.
  1422. let percentBefore = Math.round(100 * this.bufferingGoalScale_);
  1423. if (percentBefore > 20) {
  1424. this.bufferingGoalScale_ -= 0.2;
  1425. } else if (percentBefore > 4) {
  1426. this.bufferingGoalScale_ -= 0.04;
  1427. } else {
  1428. shaka.log.error(
  1429. logPrefix, 'MediaSource threw QuotaExceededError too many times');
  1430. mediaState.hasError = true;
  1431. this.fatalError_ = true;
  1432. this.playerInterface_.onError(error);
  1433. return;
  1434. }
  1435. let percentAfter = Math.round(100 * this.bufferingGoalScale_);
  1436. shaka.log.warning(
  1437. logPrefix,
  1438. 'MediaSource threw QuotaExceededError:',
  1439. 'reducing buffering goals by ' + (100 - percentAfter) + '%');
  1440. mediaState.recovering = true;
  1441. } else {
  1442. shaka.log.debug(
  1443. logPrefix,
  1444. 'MediaSource threw QuotaExceededError:',
  1445. 'waiting for another stream to recover...');
  1446. }
  1447. // QuotaExceededError gets thrown if evication didn't help to make room
  1448. // for a segment. We want to wait for a while (4 seconds is just an
  1449. // arbitrary number) before updating to give the playhead a chance to
  1450. // advance, so we don't immidiately throw again.
  1451. this.scheduleUpdate_(mediaState, 4);
  1452. };
  1453. /**
  1454. * Sets the given MediaState's associated SourceBuffer's timestamp offset and
  1455. * init segment if either are required. If an error occurs then neither the
  1456. * timestamp offset or init segment are unset, since another call to switch()
  1457. * will end up superseding them.
  1458. *
  1459. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1460. * @param {number} currentPeriodIndex
  1461. * @param {number} appendWindowStart
  1462. * @param {number} appendWindowEnd
  1463. * @return {!Promise}
  1464. * @private
  1465. */
  1466. shaka.media.StreamingEngine.prototype.initSourceBuffer_ = function(
  1467. mediaState, currentPeriodIndex, appendWindowStart, appendWindowEnd) {
  1468. if (!mediaState.needInitSegment) {
  1469. return Promise.resolve();
  1470. }
  1471. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1472. let currentPeriod = this.manifest_.periods[currentPeriodIndex];
  1473. // If we need an init segment then the Stream switched, so we've either
  1474. // changed bitrates, Periods, or both. If we've changed Periods then we must
  1475. // set a new timestamp offset and append window end. Note that by setting
  1476. // these values here, we avoid having to co-ordinate ongoing updates, which
  1477. // we would have to do if we instead set them in switch().
  1478. let timestampOffset =
  1479. currentPeriod.startTime - mediaState.stream.presentationTimeOffset;
  1480. shaka.log.v1(logPrefix, 'setting timestamp offset to ' + timestampOffset);
  1481. shaka.log.v1(logPrefix,
  1482. 'setting append window start to ' + appendWindowStart);
  1483. shaka.log.v1(logPrefix, 'setting append window end to ' + appendWindowEnd);
  1484. let setStreamProperties =
  1485. this.playerInterface_.mediaSourceEngine.setStreamProperties(
  1486. mediaState.type, timestampOffset, appendWindowStart, appendWindowEnd);
  1487. if (!mediaState.stream.initSegmentReference) {
  1488. // The Stream is self initializing.
  1489. return setStreamProperties;
  1490. }
  1491. shaka.log.v1(logPrefix, 'fetching init segment');
  1492. let fetchInit = this.fetch_(mediaState.stream.initSegmentReference);
  1493. let appendInit = fetchInit.then(function(initSegment) {
  1494. if (this.destroyed_) return;
  1495. shaka.log.v1(logPrefix, 'appending init segment');
  1496. return this.playerInterface_.mediaSourceEngine.appendBuffer(
  1497. mediaState.type, initSegment, null /* startTime */, null /* endTime */);
  1498. }.bind(this)).catch(function(error) {
  1499. mediaState.needInitSegment = true;
  1500. return Promise.reject(error);
  1501. });
  1502. return Promise.all([setStreamProperties, appendInit]);
  1503. };
  1504. /**
  1505. * Appends the given segment and evicts content if required to append.
  1506. *
  1507. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1508. * @param {number} playheadTime
  1509. * @param {shakaExtern.Period} period
  1510. * @param {shakaExtern.Stream} stream
  1511. * @param {!shaka.media.SegmentReference} reference
  1512. * @param {!ArrayBuffer} segment
  1513. * @return {!Promise}
  1514. * @private
  1515. */
  1516. shaka.media.StreamingEngine.prototype.append_ = function(
  1517. mediaState, playheadTime, period, stream, reference, segment) {
  1518. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1519. if (stream.containsEmsgBoxes) {
  1520. new shaka.util.Mp4Parser()
  1521. .fullBox('emsg', this.parseEMSG_.bind(this, period, reference))
  1522. .parse(segment);
  1523. }
  1524. return this.evict_(mediaState, playheadTime).then(function() {
  1525. if (this.destroyed_) return;
  1526. shaka.log.v1(logPrefix, 'appending media segment');
  1527. // MediaSourceEngine expects times relative to the start of the
  1528. // presentation. Reference times are relative to the start of the period.
  1529. const startTime = reference.startTime + period.startTime;
  1530. const endTime = reference.endTime + period.startTime;
  1531. return this.playerInterface_.mediaSourceEngine.appendBuffer(
  1532. mediaState.type, segment, startTime, endTime);
  1533. }.bind(this)).then(function() {
  1534. if (this.destroyed_) return;
  1535. shaka.log.v2(logPrefix, 'appended media segment');
  1536. // We must use |stream| because switch() may have been called.
  1537. mediaState.lastStream = stream;
  1538. mediaState.lastSegmentReference = reference;
  1539. return Promise.resolve();
  1540. }.bind(this));
  1541. };
  1542. /**
  1543. * Parse the EMSG box from a MP4 container.
  1544. *
  1545. * @param {!shakaExtern.Period} period
  1546. * @param {!shaka.media.SegmentReference} reference
  1547. * @param {!shakaExtern.ParsedBox} box
  1548. * @private
  1549. */
  1550. shaka.media.StreamingEngine.prototype.parseEMSG_ = function(
  1551. period, reference, box) {
  1552. let schemeId = box.reader.readTerminatedString();
  1553. // Read the rest of the data.
  1554. let value = box.reader.readTerminatedString();
  1555. let timescale = box.reader.readUint32();
  1556. let presentationTimeDelta = box.reader.readUint32();
  1557. let eventDuration = box.reader.readUint32();
  1558. let id = box.reader.readUint32();
  1559. let messageData = box.reader.readBytes(
  1560. box.reader.getLength() - box.reader.getPosition());
  1561. let startTime = period.startTime + reference.startTime +
  1562. (presentationTimeDelta / timescale);
  1563. // See DASH sec. 5.10.4.1
  1564. // A special scheme in DASH used to signal manifest updates.
  1565. if (schemeId == 'urn:mpeg:dash:event:2012') {
  1566. this.playerInterface_.onManifestUpdate();
  1567. } else {
  1568. /** @type {shakaExtern.EmsgInfo} */
  1569. let emsg = {
  1570. startTime: startTime,
  1571. endTime: startTime + (eventDuration / timescale),
  1572. schemeIdUri: schemeId,
  1573. value: value,
  1574. timescale: timescale,
  1575. presentationTimeDelta: presentationTimeDelta,
  1576. eventDuration: eventDuration,
  1577. id: id,
  1578. messageData: messageData
  1579. };
  1580. // Dispatch an event to notify the application about the emsg box.
  1581. let event = new shaka.util.FakeEvent('emsg', {'detail': emsg});
  1582. this.playerInterface_.onEvent(event);
  1583. }
  1584. };
  1585. /**
  1586. * Evicts media to meet the max buffer behind limit.
  1587. *
  1588. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1589. * @param {number} playheadTime
  1590. * @return {!Promise}
  1591. * @private
  1592. */
  1593. shaka.media.StreamingEngine.prototype.evict_ = function(
  1594. mediaState, playheadTime) {
  1595. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1596. shaka.log.v2(logPrefix, 'checking buffer length');
  1597. // Use the max segment duration, if it is longer than the bufferBehind, to
  1598. // avoid accidentally clearing too much data when dealing with a manifest
  1599. // with a long keyframe interval.
  1600. let bufferBehind = Math.max(this.config_.bufferBehind,
  1601. this.manifest_.presentationTimeline.getMaxSegmentDuration());
  1602. let startTime =
  1603. this.playerInterface_.mediaSourceEngine.bufferStart(mediaState.type);
  1604. if (startTime == null) {
  1605. shaka.log.v2(logPrefix,
  1606. 'buffer behind okay because nothing buffered:',
  1607. 'playheadTime=' + playheadTime,
  1608. 'bufferBehind=' + bufferBehind);
  1609. return Promise.resolve();
  1610. }
  1611. let bufferedBehind = playheadTime - startTime;
  1612. let overflow = bufferedBehind - bufferBehind;
  1613. if (overflow <= 0) {
  1614. shaka.log.v2(logPrefix,
  1615. 'buffer behind okay:',
  1616. 'playheadTime=' + playheadTime,
  1617. 'bufferedBehind=' + bufferedBehind,
  1618. 'bufferBehind=' + bufferBehind,
  1619. 'underflow=' + (-overflow));
  1620. return Promise.resolve();
  1621. }
  1622. shaka.log.v1(logPrefix,
  1623. 'buffer behind too large:',
  1624. 'playheadTime=' + playheadTime,
  1625. 'bufferedBehind=' + bufferedBehind,
  1626. 'bufferBehind=' + bufferBehind,
  1627. 'overflow=' + overflow);
  1628. return this.playerInterface_.mediaSourceEngine.remove(
  1629. mediaState.type, startTime, startTime + overflow).then(function() {
  1630. if (this.destroyed_) return;
  1631. shaka.log.v1(logPrefix, 'evicted ' + overflow + ' seconds');
  1632. }.bind(this));
  1633. };
  1634. /**
  1635. * Sets up all known Periods when startup completes; otherwise, does nothing.
  1636. *
  1637. * @param {shaka.media.StreamingEngine.MediaState_} mediaState The last
  1638. * MediaState updated.
  1639. * @param {shakaExtern.Stream} stream
  1640. * @private
  1641. */
  1642. shaka.media.StreamingEngine.prototype.handleStartup_ = function(
  1643. mediaState, stream) {
  1644. const Functional = shaka.util.Functional;
  1645. const MapUtils = shaka.util.MapUtils;
  1646. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1647. if (this.startupComplete_) {
  1648. return;
  1649. }
  1650. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1651. goog.asserts.assert(this.mediaStates_, 'must not be destroyed');
  1652. let mediaStates = MapUtils.values(this.mediaStates_);
  1653. this.startupComplete_ = mediaStates.every(function(ms) {
  1654. // Startup completes once we have buffered at least one segment from each
  1655. // MediaState, not counting text.
  1656. if (ms.type == ContentType.TEXT) return true;
  1657. return !ms.waitingToClearBuffer &&
  1658. !ms.clearingBuffer &&
  1659. ms.lastSegmentReference;
  1660. });
  1661. if (!this.startupComplete_) {
  1662. return;
  1663. }
  1664. shaka.log.debug(logPrefix, 'startup complete');
  1665. // We must use |stream| because switch() may have been called.
  1666. let currentPeriodIndex = this.findPeriodContainingStream_(stream);
  1667. goog.asserts.assert(
  1668. mediaStates.every(function(ms) {
  1669. // It is possible for one stream (usually text) to buffer the whole
  1670. // Period and need the next one.
  1671. return ms.needPeriodIndex == currentPeriodIndex ||
  1672. ms.needPeriodIndex == currentPeriodIndex + 1;
  1673. }),
  1674. logPrefix + ' expected all MediaStates to need same Period');
  1675. // Setup the current Period if necessary, which is likely since the current
  1676. // Period is probably the initial one.
  1677. if (!this.canSwitchPeriod_[currentPeriodIndex]) {
  1678. this.setupPeriod_(currentPeriodIndex).then(function() {
  1679. if (this.destroyed_) {
  1680. return;
  1681. }
  1682. shaka.log.v1(logPrefix, 'calling onCanSwitch()...');
  1683. this.playerInterface_.onCanSwitch();
  1684. }.bind(this)).catch(Functional.noop);
  1685. }
  1686. // Now setup all known Periods.
  1687. for (let i = 0; i < this.manifest_.periods.length; ++i) {
  1688. this.setupPeriod_(i).catch(Functional.noop);
  1689. }
  1690. if (this.playerInterface_.onStartupComplete) {
  1691. shaka.log.v1(logPrefix, 'calling onStartupComplete()...');
  1692. this.playerInterface_.onStartupComplete();
  1693. }
  1694. };
  1695. /**
  1696. * Calls onChooseStreams() when necessary.
  1697. *
  1698. * @param {shaka.media.StreamingEngine.MediaState_} mediaState The last
  1699. * MediaState updated.
  1700. * @private
  1701. */
  1702. shaka.media.StreamingEngine.prototype.handlePeriodTransition_ = function(
  1703. mediaState) {
  1704. const Functional = shaka.util.Functional;
  1705. const MapUtils = shaka.util.MapUtils;
  1706. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1707. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1708. let currentPeriodIndex = this.findPeriodContainingStream_(mediaState.stream);
  1709. if (mediaState.needPeriodIndex == currentPeriodIndex) {
  1710. return;
  1711. }
  1712. let needPeriodIndex = mediaState.needPeriodIndex;
  1713. goog.asserts.assert(this.mediaStates_, 'must not be destroyed');
  1714. let mediaStates = MapUtils.values(this.mediaStates_);
  1715. // For a Period transition to work, all media states must need the same
  1716. // Period. If a stream needs a different Period than the one it currently
  1717. // has, it will try to transition or stop updates assuming that another stream
  1718. // will handle it. This only works when all streams either need the same
  1719. // Period or are still performing updates.
  1720. goog.asserts.assert(
  1721. mediaStates.every(function(ms) {
  1722. return ms.needPeriodIndex == needPeriodIndex || ms.hasError ||
  1723. !shaka.media.StreamingEngine.isIdle_(ms);
  1724. }),
  1725. 'All MediaStates should need the same Period or be performing updates.');
  1726. // Only call onChooseStreams() when all MediaStates need the same Period.
  1727. let needSamePeriod = mediaStates.every(function(ms) {
  1728. return ms.needPeriodIndex == needPeriodIndex;
  1729. });
  1730. if (!needSamePeriod) {
  1731. shaka.log.debug(
  1732. logPrefix, 'not all MediaStates need Period ' + needPeriodIndex);
  1733. return;
  1734. }
  1735. // Only call onChooseStreams() once per Period transition.
  1736. let allAreIdle = mediaStates.every(shaka.media.StreamingEngine.isIdle_);
  1737. if (!allAreIdle) {
  1738. shaka.log.debug(
  1739. logPrefix,
  1740. 'all MediaStates need Period ' + needPeriodIndex + ', ' +
  1741. 'but not all MediaStates are idle');
  1742. return;
  1743. }
  1744. shaka.log.debug(logPrefix, 'all need Period ' + needPeriodIndex);
  1745. // Ensure the Period which we need to buffer is set up and then call
  1746. // onChooseStreams().
  1747. this.setupPeriod_(needPeriodIndex).then(function() {
  1748. if (this.destroyed_) return;
  1749. // If we seek during a Period transition, we can start another transition.
  1750. // So we need to verify that:
  1751. // 1. We are still in need of the same Period.
  1752. // 2. All streams are still idle.
  1753. // 3. The current stream is not in the needed Period (another transition
  1754. // handled it).
  1755. let allReady = mediaStates.every(function(ms) {
  1756. let isIdle = shaka.media.StreamingEngine.isIdle_(ms);
  1757. let currentPeriodIndex = this.findPeriodContainingStream_(ms.stream);
  1758. return isIdle && ms.needPeriodIndex == needPeriodIndex &&
  1759. currentPeriodIndex != needPeriodIndex;
  1760. }.bind(this));
  1761. if (!allReady) {
  1762. // TODO: Write unit tests for this case.
  1763. shaka.log.debug(logPrefix, 'ignoring transition to Period',
  1764. needPeriodIndex, 'since another is happening');
  1765. return;
  1766. }
  1767. let needPeriod = this.manifest_.periods[needPeriodIndex];
  1768. shaka.log.v1(logPrefix, 'calling onChooseStreams()...');
  1769. let chosenStreams = this.playerInterface_.onChooseStreams(needPeriod);
  1770. let streamsByType = {};
  1771. if (chosenStreams.variant && chosenStreams.variant.video) {
  1772. streamsByType[ContentType.VIDEO] = chosenStreams.variant.video;
  1773. }
  1774. if (chosenStreams.variant && chosenStreams.variant.audio) {
  1775. streamsByType[ContentType.AUDIO] = chosenStreams.variant.audio;
  1776. }
  1777. if (chosenStreams.text) {
  1778. streamsByType[ContentType.TEXT] = chosenStreams.text;
  1779. }
  1780. // Vet |streamsByType| before switching.
  1781. for (let type in this.mediaStates_) {
  1782. if (streamsByType[type] || type == ContentType.TEXT) continue;
  1783. shaka.log.error(logPrefix,
  1784. 'invalid Streams chosen: missing ' + type + ' Stream');
  1785. this.playerInterface_.onError(new shaka.util.Error(
  1786. shaka.util.Error.Severity.CRITICAL,
  1787. shaka.util.Error.Category.STREAMING,
  1788. shaka.util.Error.Code.INVALID_STREAMS_CHOSEN));
  1789. return;
  1790. }
  1791. for (let type in streamsByType) {
  1792. if (this.mediaStates_[/** @type {!ContentType} */(type)]) continue;
  1793. if (type == ContentType.TEXT) {
  1794. // initStreams_ will switch streams and schedule an update.
  1795. this.initStreams_(
  1796. {text: streamsByType[ContentType.TEXT]}, needPeriod.startTime);
  1797. delete streamsByType[type];
  1798. continue;
  1799. }
  1800. shaka.log.error(logPrefix,
  1801. 'invalid Streams chosen: unusable ' + type + ' Stream');
  1802. this.playerInterface_.onError(new shaka.util.Error(
  1803. shaka.util.Error.Severity.CRITICAL,
  1804. shaka.util.Error.Category.STREAMING,
  1805. shaka.util.Error.Code.INVALID_STREAMS_CHOSEN));
  1806. return;
  1807. }
  1808. for (let type in this.mediaStates_) {
  1809. let stream = streamsByType[type];
  1810. if (stream) {
  1811. this.switchInternal_(stream, /* clearBuffer */ false);
  1812. this.scheduleUpdate_(this.mediaStates_[type], 0);
  1813. } else {
  1814. goog.asserts.assert(type == ContentType.TEXT, 'Invalid streams chosen');
  1815. delete this.mediaStates_[type];
  1816. }
  1817. }
  1818. // We've already set up the Period so call onCanSwitch() right now.
  1819. shaka.log.v1(logPrefix, 'calling onCanSwitch()...');
  1820. this.playerInterface_.onCanSwitch();
  1821. }.bind(this)).catch(Functional.noop);
  1822. };
  1823. /**
  1824. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1825. * @return {boolean} True if the given MediaState is idle; otherwise, return
  1826. * false.
  1827. * @private
  1828. */
  1829. shaka.media.StreamingEngine.isIdle_ = function(mediaState) {
  1830. return !mediaState.performingUpdate &&
  1831. (mediaState.updateTimer == null) &&
  1832. !mediaState.waitingToClearBuffer &&
  1833. !mediaState.clearingBuffer;
  1834. };
  1835. /**
  1836. * @param {number} time The time, in seconds, relative to the start of the
  1837. * presentation.
  1838. * @return {number} The index of the Period which starts after |time|
  1839. * @private
  1840. */
  1841. shaka.media.StreamingEngine.prototype.findPeriodContainingTime_ = function(
  1842. time) {
  1843. goog.asserts.assert(this.manifest_, 'Must not be destroyed');
  1844. return shaka.util.StreamUtils.findPeriodContainingTime(this.manifest_, time);
  1845. };
  1846. /**
  1847. * @param {!shakaExtern.Stream} stream
  1848. * @return {number} The index of the Period which contains |stream|, or -1 if
  1849. * no Period contains |stream|.
  1850. * @private
  1851. */
  1852. shaka.media.StreamingEngine.prototype.findPeriodContainingStream_ = function(
  1853. stream) {
  1854. goog.asserts.assert(this.manifest_, 'Must not be destroyed');
  1855. return shaka.util.StreamUtils.findPeriodContainingStream(
  1856. this.manifest_, stream);
  1857. };
  1858. /**
  1859. * Fetches the given segment.
  1860. *
  1861. * @param {(!shaka.media.InitSegmentReference|!shaka.media.SegmentReference)}
  1862. * reference
  1863. *
  1864. * @return {!Promise.<!ArrayBuffer>}
  1865. * @private
  1866. */
  1867. shaka.media.StreamingEngine.prototype.fetch_ = function(reference) {
  1868. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  1869. let request = shaka.net.NetworkingEngine.makeRequest(
  1870. reference.getUris(), this.config_.retryParameters);
  1871. // Set the Range header. Note that some web servers don't accept Range
  1872. // headers, so don't set one if it's not strictly required.
  1873. if ((reference.startByte != 0) || (reference.endByte != null)) {
  1874. let range = 'bytes=' + reference.startByte + '-';
  1875. if (reference.endByte != null) range += reference.endByte;
  1876. request.headers['Range'] = range;
  1877. }
  1878. shaka.log.v2('fetching: reference=', reference);
  1879. let op = this.playerInterface_.netEngine.request(requestType, request);
  1880. return op.promise.then(function(response) {
  1881. return response.data;
  1882. });
  1883. };
  1884. /**
  1885. * Clears the buffer and schedules another update.
  1886. *
  1887. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1888. * @param {boolean} flush
  1889. * @private
  1890. */
  1891. shaka.media.StreamingEngine.prototype.clearBuffer_ =
  1892. function(mediaState, flush) {
  1893. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1894. goog.asserts.assert(
  1895. !mediaState.performingUpdate && (mediaState.updateTimer == null),
  1896. logPrefix + ' unexpected call to clearBuffer_()');
  1897. mediaState.waitingToClearBuffer = false;
  1898. mediaState.waitingToFlushBuffer = false;
  1899. mediaState.clearingBuffer = true;
  1900. shaka.log.debug(logPrefix, 'clearing buffer');
  1901. let p = this.playerInterface_.mediaSourceEngine.clear(mediaState.type);
  1902. p.then(function() {
  1903. if (!this.destroyed_ && flush) {
  1904. return this.playerInterface_.mediaSourceEngine.flush(mediaState.type);
  1905. }
  1906. }.bind(this)).then(function() {
  1907. if (this.destroyed_) return;
  1908. shaka.log.debug(logPrefix, 'cleared buffer');
  1909. mediaState.lastStream = null;
  1910. mediaState.lastSegmentReference = null;
  1911. mediaState.clearingBuffer = false;
  1912. mediaState.endOfStream = false;
  1913. this.scheduleUpdate_(mediaState, 0);
  1914. }.bind(this));
  1915. };
  1916. /**
  1917. * Schedules |mediaState|'s next update.
  1918. *
  1919. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1920. * @param {number} delay The delay in seconds.
  1921. * @private
  1922. */
  1923. shaka.media.StreamingEngine.prototype.scheduleUpdate_ = function(
  1924. mediaState, delay) {
  1925. let logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
  1926. shaka.log.v2(logPrefix, 'updating in ' + delay + ' seconds');
  1927. goog.asserts.assert(mediaState.updateTimer == null,
  1928. logPrefix + ' did not expect update to be scheduled');
  1929. mediaState.updateTimer = window.setTimeout(
  1930. this.onUpdate_.bind(this, mediaState), delay * 1000);
  1931. };
  1932. /**
  1933. * Cancels |mediaState|'s next update if one exists.
  1934. *
  1935. * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
  1936. * @private
  1937. */
  1938. shaka.media.StreamingEngine.prototype.cancelUpdate_ = function(mediaState) {
  1939. if (mediaState.updateTimer != null) {
  1940. window.clearTimeout(mediaState.updateTimer);
  1941. mediaState.updateTimer = null;
  1942. }
  1943. };
  1944. /**
  1945. * Handle streaming errors by delaying, then notifying the application by error
  1946. * callback and by streaming failure callback.
  1947. *
  1948. * @param {!shaka.util.Error} error
  1949. * @private
  1950. */
  1951. shaka.media.StreamingEngine.prototype.handleStreamingError_ = function(error) {
  1952. // If we invoke the callback right away, the application could trigger a
  1953. // rapid retry cycle that could be very unkind to the server. Instead,
  1954. // use the backoff system to delay and backoff the error handling.
  1955. this.failureCallbackBackoff_.attempt().then(function() {
  1956. if (this.destroyed_) {
  1957. return;
  1958. }
  1959. // First fire an error event.
  1960. this.playerInterface_.onError(error);
  1961. // If the error was not handled by the application, call the failure
  1962. // callback.
  1963. if (!error.handled) {
  1964. this.config_.failureCallback(error);
  1965. }
  1966. }.bind(this));
  1967. };
  1968. /**
  1969. * @param {shaka.media.StreamingEngine.MediaState_} mediaState
  1970. * @return {string} A log prefix of the form ($CONTENT_TYPE:$STREAM_ID), e.g.,
  1971. * "(audio:5)" or "(video:hd)".
  1972. * @private
  1973. */
  1974. shaka.media.StreamingEngine.logPrefix_ = function(mediaState) {
  1975. return '(' + mediaState.type + ':' + mediaState.stream.id + ')';
  1976. };