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