Source: lib/media/segment_prefetch.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.SegmentPrefetch');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.media.InitSegmentReference');
  10. goog.require('shaka.media.SegmentIterator');
  11. goog.require('shaka.media.SegmentReference');
  12. goog.require('shaka.net.NetworkingEngine');
  13. goog.require('shaka.util.Error');
  14. goog.require('shaka.util.Uint8ArrayUtils');
  15. /**
  16. * @summary
  17. * This class manages segment prefetch operations.
  18. * Called by StreamingEngine to prefetch next N segments
  19. * ahead of playhead, to reduce the chances of rebuffering.
  20. */
  21. shaka.media.SegmentPrefetch = class {
  22. /**
  23. * @param {number} prefetchLimit
  24. * @param {shaka.extern.Stream} stream
  25. * @param {shaka.media.SegmentPrefetch.FetchDispatcher} fetchDispatcher
  26. * @param {boolean} reverse
  27. */
  28. constructor(prefetchLimit, stream, fetchDispatcher, reverse) {
  29. /** @private {number} */
  30. this.prefetchLimit_ = prefetchLimit;
  31. /** @private {shaka.extern.Stream} */
  32. this.stream_ = stream;
  33. /** @private {shaka.media.SegmentPrefetch.FetchDispatcher} */
  34. this.fetchDispatcher_ = fetchDispatcher;
  35. /**
  36. * @private {!Map<
  37. * !shaka.media.SegmentReference,
  38. * !shaka.media.SegmentPrefetchOperation>}
  39. */
  40. this.segmentPrefetchMap_ = new Map();
  41. /**
  42. * @private {!Map<
  43. * !shaka.media.InitSegmentReference,
  44. * !shaka.media.SegmentPrefetchOperation>}
  45. */
  46. this.initSegmentPrefetchMap_ = new Map();
  47. /** @private {?shaka.media.SegmentIterator} */
  48. this.iterator_ = null;
  49. /** @private {boolean} */
  50. this.reverse_ = reverse;
  51. }
  52. /**
  53. * @param {shaka.media.SegmentPrefetch.FetchDispatcher} fetchDispatcher
  54. */
  55. replaceFetchDispatcher(fetchDispatcher) {
  56. this.fetchDispatcher_ = fetchDispatcher;
  57. for (const operation of this.segmentPrefetchMap_.values()) {
  58. operation.replaceFetchDispatcher(fetchDispatcher);
  59. }
  60. }
  61. /**
  62. * Fetch next segments ahead of current time.
  63. *
  64. * @param {number} currTime
  65. * @param {boolean=} skipFirst
  66. * @return {!Promise}
  67. * @public
  68. */
  69. prefetchSegmentsByTime(currTime, skipFirst = false) {
  70. goog.asserts.assert(this.prefetchLimit_ > 0,
  71. 'SegmentPrefetch can not be used when prefetchLimit <= 0.');
  72. const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
  73. if (!this.stream_.segmentIndex) {
  74. shaka.log.debug(logPrefix, 'missing segmentIndex');
  75. return Promise.resolve();
  76. }
  77. if (!this.iterator_) {
  78. this.iterator_ = this.stream_.segmentIndex.getIteratorForTime(
  79. currTime, /* allowNonIndependent= */ true, this.reverse_);
  80. }
  81. if (!this.iterator_) {
  82. shaka.log.debug(logPrefix, 'missing iterator');
  83. return Promise.resolve();
  84. }
  85. if (skipFirst) {
  86. this.iterator_.next();
  87. }
  88. const promises = [];
  89. while (this.segmentPrefetchMap_.size < this.prefetchLimit_) {
  90. const reference = this.iterator_.next().value;
  91. if (!reference) {
  92. break;
  93. }
  94. // By default doesn't prefetch preload partial segments when using
  95. // byterange
  96. let prefetchAllowed = true;
  97. if (reference.isPreload() && reference.endByte != null) {
  98. prefetchAllowed = false;
  99. }
  100. if (reference.getStatus() ==
  101. shaka.media.SegmentReference.Status.MISSING) {
  102. prefetchAllowed = false;
  103. }
  104. if (reference.getSegmentData(/* allowDeleteOnSingleUse= */ false)) {
  105. prefetchAllowed = false;
  106. }
  107. if (prefetchAllowed && reference.initSegmentReference) {
  108. promises.push(this.prefetchInitSegment(
  109. reference.initSegmentReference));
  110. }
  111. if (prefetchAllowed && !this.segmentPrefetchMap_.has(reference)) {
  112. const segmentPrefetchOperation =
  113. new shaka.media.SegmentPrefetchOperation(this.fetchDispatcher_);
  114. promises.push(segmentPrefetchOperation.dispatchFetch(
  115. reference, this.stream_));
  116. this.segmentPrefetchMap_.set(reference, segmentPrefetchOperation);
  117. }
  118. }
  119. this.clearInitSegments_();
  120. return Promise.all(promises);
  121. }
  122. /**
  123. * Fetch init segment.
  124. *
  125. * @param {!shaka.media.InitSegmentReference} initSegmentReference
  126. * @return {!Promise}
  127. */
  128. prefetchInitSegment(initSegmentReference) {
  129. goog.asserts.assert(this.prefetchLimit_ > 0,
  130. 'SegmentPrefetch can not be used when prefetchLimit <= 0.');
  131. const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
  132. if (!this.stream_.segmentIndex) {
  133. shaka.log.debug(logPrefix, 'missing segmentIndex');
  134. return Promise.resolve();
  135. }
  136. if (initSegmentReference.getSegmentData()) {
  137. return Promise.resolve();
  138. }
  139. // init segments are ignored from the prefetch limit
  140. const initSegments = Array.from(this.initSegmentPrefetchMap_.keys());
  141. const someReference = initSegments.some((reference) => {
  142. return shaka.media.InitSegmentReference.equal(
  143. reference, initSegmentReference);
  144. });
  145. if (someReference) {
  146. return Promise.resolve();
  147. }
  148. const segmentPrefetchOperation = new shaka.media.SegmentPrefetchOperation(
  149. this.fetchDispatcher_);
  150. const promise = segmentPrefetchOperation.dispatchFetch(
  151. initSegmentReference, this.stream_);
  152. this.initSegmentPrefetchMap_.set(
  153. initSegmentReference, segmentPrefetchOperation);
  154. return promise;
  155. }
  156. /**
  157. * Get the result of prefetched segment if already exists.
  158. * @param {!(shaka.media.SegmentReference|
  159. * shaka.media.InitSegmentReference)} reference
  160. * @param {?function(BufferSource):!Promise=} streamDataCallback
  161. * @return {?shaka.net.NetworkingEngine.PendingRequest} op
  162. * @public
  163. */
  164. getPrefetchedSegment(reference, streamDataCallback) {
  165. goog.asserts.assert(this.prefetchLimit_ > 0,
  166. 'SegmentPrefetch can not be used when prefetchLimit <= 0.');
  167. const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
  168. let prefetchMap = this.segmentPrefetchMap_;
  169. if (reference instanceof shaka.media.InitSegmentReference) {
  170. prefetchMap = this.initSegmentPrefetchMap_;
  171. }
  172. if (prefetchMap.has(reference)) {
  173. const segmentPrefetchOperation = prefetchMap.get(reference);
  174. if (streamDataCallback) {
  175. segmentPrefetchOperation.setStreamDataCallback(streamDataCallback);
  176. }
  177. if (reference instanceof shaka.media.SegmentReference) {
  178. shaka.log.debug(
  179. logPrefix,
  180. 'reused prefetched segment at time:', reference.startTime,
  181. 'mapSize', prefetchMap.size);
  182. } else {
  183. shaka.log.debug(
  184. logPrefix,
  185. 'reused prefetched init segment at time, mapSize',
  186. prefetchMap.size);
  187. }
  188. return segmentPrefetchOperation.getOperation();
  189. } else {
  190. if (reference instanceof shaka.media.SegmentReference) {
  191. shaka.log.debug(
  192. logPrefix,
  193. 'missed segment at time:', reference.startTime,
  194. 'mapSize', prefetchMap.size);
  195. } else {
  196. shaka.log.debug(
  197. logPrefix,
  198. 'missed init segment at time, mapSize',
  199. prefetchMap.size);
  200. }
  201. return null;
  202. }
  203. }
  204. /**
  205. * Clear All Helper
  206. * @param {!Map<T,
  207. * !shaka.media.SegmentPrefetchOperation>} map
  208. * @template T SegmentReference or InitSegmentReference
  209. * @private
  210. */
  211. clearMap_(map) {
  212. for (const reference of map.keys()) {
  213. if (reference) {
  214. this.abortPrefetchedSegment_(reference);
  215. }
  216. }
  217. }
  218. /** */
  219. resetPosition() {
  220. this.iterator_ = null;
  221. }
  222. /**
  223. * Clear all segment data.
  224. * @public
  225. */
  226. clearAll() {
  227. this.clearMap_(this.segmentPrefetchMap_);
  228. this.clearMap_(this.initSegmentPrefetchMap_);
  229. this.resetPosition();
  230. const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
  231. shaka.log.debug(logPrefix, 'cleared all');
  232. }
  233. /**
  234. * Remove a reference of prefetched segment if already exists.
  235. * @param {!shaka.media.SegmentReference} reference
  236. * @public
  237. */
  238. removeReference(reference) {
  239. this.abortPrefetchedSegment_(reference);
  240. }
  241. /**
  242. * @param {number} time
  243. * @param {boolean=} clearInitSegments
  244. */
  245. evict(time, clearInitSegments = false) {
  246. for (const ref of this.segmentPrefetchMap_.keys()) {
  247. if (time > ref.endTime) {
  248. this.abortPrefetchedSegment_(ref);
  249. }
  250. }
  251. if (clearInitSegments) {
  252. this.clearInitSegments_();
  253. }
  254. }
  255. /**
  256. * @param {boolean} reverse
  257. */
  258. setReverse(reverse) {
  259. this.reverse_ = reverse;
  260. if (this.iterator_) {
  261. this.iterator_.setReverse(reverse);
  262. }
  263. }
  264. /**
  265. * Remove all init segments that don't have associated segments in
  266. * the segment prefetch map.
  267. * By default, with delete on get, the init segments should get removed as
  268. * they are used. With deleteOnGet set to false, we need to clear them
  269. * every so often once the segments that are associated with each init segment
  270. * is no longer prefetched.
  271. * @private
  272. */
  273. clearInitSegments_() {
  274. const segmentReferences = Array.from(this.segmentPrefetchMap_.keys());
  275. for (const initSegmentReference of this.initSegmentPrefetchMap_.keys()) {
  276. // if no segment references this init segment, we should remove it.
  277. const someReference = segmentReferences.some((segmentReference) => {
  278. return shaka.media.InitSegmentReference.equal(
  279. segmentReference.initSegmentReference, initSegmentReference);
  280. });
  281. if (!someReference) {
  282. this.abortPrefetchedSegment_(initSegmentReference);
  283. }
  284. }
  285. }
  286. /**
  287. * Reset the prefetchLimit and clear all internal states.
  288. * Called by StreamingEngine when configure() was called.
  289. * @param {number} newPrefetchLimit
  290. * @public
  291. */
  292. resetLimit(newPrefetchLimit) {
  293. goog.asserts.assert(newPrefetchLimit >= 0,
  294. 'The new prefetch limit must be >= 0.');
  295. const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
  296. shaka.log.debug(logPrefix, 'resetting prefetch limit to', newPrefetchLimit);
  297. this.prefetchLimit_ = newPrefetchLimit;
  298. const keyArr = Array.from(this.segmentPrefetchMap_.keys());
  299. while (keyArr.length > newPrefetchLimit) {
  300. const reference = keyArr.pop();
  301. if (reference) {
  302. this.abortPrefetchedSegment_(reference);
  303. }
  304. }
  305. this.clearInitSegments_();
  306. }
  307. /**
  308. * Called by Streaming Engine when switching variant.
  309. * @param {shaka.extern.Stream} stream
  310. * @public
  311. */
  312. switchStream(stream) {
  313. if (stream && stream !== this.stream_) {
  314. this.clearAll();
  315. this.stream_ = stream;
  316. }
  317. }
  318. /**
  319. * Get the current stream.
  320. * @public
  321. * @return {shaka.extern.Stream}
  322. */
  323. getStream() {
  324. return this.stream_;
  325. }
  326. /**
  327. * Remove a segment from prefetch map and abort it.
  328. * @param {!(shaka.media.SegmentReference|
  329. * shaka.media.InitSegmentReference)} reference
  330. * @private
  331. */
  332. abortPrefetchedSegment_(reference) {
  333. const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
  334. let prefetchMap = this.segmentPrefetchMap_;
  335. if (reference instanceof shaka.media.InitSegmentReference) {
  336. prefetchMap = this.initSegmentPrefetchMap_;
  337. }
  338. const segmentPrefetchOperation = prefetchMap.get(reference);
  339. prefetchMap.delete(reference);
  340. if (segmentPrefetchOperation) {
  341. segmentPrefetchOperation.abort();
  342. if (reference instanceof shaka.media.SegmentReference) {
  343. shaka.log.debug(
  344. logPrefix,
  345. 'pop and abort prefetched segment at time:', reference.startTime);
  346. } else {
  347. shaka.log.debug(logPrefix, 'pop and abort prefetched init segment');
  348. }
  349. }
  350. }
  351. /**
  352. * The prefix of the logs that are created in this class.
  353. * @param {shaka.extern.Stream} stream
  354. * @return {string}
  355. * @private
  356. */
  357. static logPrefix_(stream) {
  358. return 'SegmentPrefetch(' + stream.type + ':' + stream.id + ')';
  359. }
  360. };
  361. /**
  362. * @summary
  363. * This class manages a segment prefetch operation.
  364. */
  365. shaka.media.SegmentPrefetchOperation = class {
  366. /**
  367. * @param {shaka.media.SegmentPrefetch.FetchDispatcher} fetchDispatcher
  368. */
  369. constructor(fetchDispatcher) {
  370. /** @private {shaka.media.SegmentPrefetch.FetchDispatcher} */
  371. this.fetchDispatcher_ = fetchDispatcher;
  372. /** @private {?function(BufferSource):!Promise} */
  373. this.streamDataCallback_ = null;
  374. /** @private {?shaka.net.NetworkingEngine.PendingRequest} */
  375. this.operation_ = null;
  376. }
  377. /**
  378. * @param {shaka.media.SegmentPrefetch.FetchDispatcher} fetchDispatcher
  379. */
  380. replaceFetchDispatcher(fetchDispatcher) {
  381. this.fetchDispatcher_ = fetchDispatcher;
  382. }
  383. /**
  384. * Fetch segments
  385. *
  386. * @param {!(shaka.media.SegmentReference|
  387. * shaka.media.InitSegmentReference)} reference
  388. * @param {!shaka.extern.Stream} stream
  389. * @return {!Promise}
  390. * @public
  391. */
  392. dispatchFetch(reference, stream) {
  393. // We need to store the data, because streamDataCallback_ might not be
  394. // available when you start getting the first data.
  395. let buffered = new Uint8Array(0);
  396. this.operation_ = this.fetchDispatcher_(
  397. reference, stream, async (data) => {
  398. if (buffered.byteLength > 0) {
  399. buffered = shaka.util.Uint8ArrayUtils.concat(buffered, data);
  400. } else {
  401. buffered = data;
  402. }
  403. if (this.streamDataCallback_) {
  404. await this.streamDataCallback_(buffered);
  405. buffered = new Uint8Array(0);
  406. }
  407. });
  408. return this.operation_.promise.catch((e) => {
  409. // Ignore OPERATION_ABORTED errors.
  410. if (e instanceof shaka.util.Error &&
  411. e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  412. return Promise.resolve();
  413. }
  414. // Continue to surface other errors.
  415. return Promise.reject(e);
  416. });
  417. }
  418. /**
  419. * Get the operation of prefetched segment if already exists.
  420. *
  421. * @return {?shaka.net.NetworkingEngine.PendingRequest} op
  422. * @public
  423. */
  424. getOperation() {
  425. return this.operation_;
  426. }
  427. /**
  428. * @param {?function(BufferSource):!Promise} streamDataCallback
  429. * @public
  430. */
  431. setStreamDataCallback(streamDataCallback) {
  432. this.streamDataCallback_ = streamDataCallback;
  433. }
  434. /**
  435. * Abort the current operation if exists.
  436. */
  437. abort() {
  438. if (this.operation_) {
  439. this.operation_.abort();
  440. }
  441. }
  442. };
  443. /**
  444. * @typedef {function(
  445. * !(shaka.media.InitSegmentReference|shaka.media.SegmentReference),
  446. * shaka.extern.Stream,
  447. * ?function(BufferSource):!Promise=
  448. * ):!shaka.net.NetworkingEngine.PendingRequest}
  449. *
  450. * @description
  451. * A callback function that fetches a segment.
  452. * @export
  453. */
  454. shaka.media.SegmentPrefetch.FetchDispatcher;