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, /* allowNonIndepedent= */ 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 prefech 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()) {
  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|shaka.media.InitSegmentReference)}
  159. * 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. * @private
  207. */
  208. clearMap_(map) {
  209. for (const reference of map.keys()) {
  210. if (reference) {
  211. this.abortPrefetchedSegment_(reference);
  212. }
  213. }
  214. }
  215. /** */
  216. resetPosition() {
  217. this.iterator_ = null;
  218. }
  219. /**
  220. * Clear all segment data.
  221. * @public
  222. */
  223. clearAll() {
  224. this.clearMap_(this.segmentPrefetchMap_);
  225. this.clearMap_(this.initSegmentPrefetchMap_);
  226. this.resetPosition();
  227. const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
  228. shaka.log.debug(logPrefix, 'cleared all');
  229. }
  230. /**
  231. * Remove a reference of prefetched segment if already exists.
  232. * @param {!shaka.media.SegmentReference} reference
  233. * @public
  234. */
  235. removeReference(reference) {
  236. this.abortPrefetchedSegment_(reference);
  237. }
  238. /**
  239. * @param {number} time
  240. * @param {boolean=} clearInitSegments
  241. */
  242. evict(time, clearInitSegments = false) {
  243. for (const ref of this.segmentPrefetchMap_.keys()) {
  244. if (time > ref.endTime) {
  245. this.abortPrefetchedSegment_(ref);
  246. }
  247. }
  248. if (clearInitSegments) {
  249. this.clearInitSegments_();
  250. }
  251. }
  252. /**
  253. * @param {boolean} reverse
  254. */
  255. setReverse(reverse) {
  256. this.reverse_ = reverse;
  257. if (this.iterator_) {
  258. this.iterator_.setReverse(reverse);
  259. }
  260. }
  261. /**
  262. * Remove all init segments that don't have associated segments in
  263. * the segment prefetch map.
  264. * By default, with delete on get, the init segments should get removed as
  265. * they are used. With deleteOnGet set to false, we need to clear them
  266. * every so often once the segments that are associated with each init segment
  267. * is no longer prefetched.
  268. * @private
  269. */
  270. clearInitSegments_() {
  271. const segmentReferences = Array.from(this.segmentPrefetchMap_.keys());
  272. for (const initSegmentReference of this.initSegmentPrefetchMap_.keys()) {
  273. // if no segment references this init segment, we should remove it.
  274. const someReference = segmentReferences.some((segmentReference) => {
  275. return shaka.media.InitSegmentReference.equal(
  276. segmentReference.initSegmentReference, initSegmentReference);
  277. });
  278. if (!someReference) {
  279. this.abortPrefetchedSegment_(initSegmentReference);
  280. }
  281. }
  282. }
  283. /**
  284. * Reset the prefetchLimit and clear all internal states.
  285. * Called by StreamingEngine when configure() was called.
  286. * @param {number} newPrefetchLimit
  287. * @public
  288. */
  289. resetLimit(newPrefetchLimit) {
  290. goog.asserts.assert(newPrefetchLimit >= 0,
  291. 'The new prefetch limit must be >= 0.');
  292. const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
  293. shaka.log.debug(logPrefix, 'resetting prefetch limit to', newPrefetchLimit);
  294. this.prefetchLimit_ = newPrefetchLimit;
  295. const keyArr = Array.from(this.segmentPrefetchMap_.keys());
  296. while (keyArr.length > newPrefetchLimit) {
  297. const reference = keyArr.pop();
  298. if (reference) {
  299. this.abortPrefetchedSegment_(reference);
  300. }
  301. }
  302. this.clearInitSegments_();
  303. }
  304. /**
  305. * Called by Streaming Engine when switching variant.
  306. * @param {shaka.extern.Stream} stream
  307. * @public
  308. */
  309. switchStream(stream) {
  310. if (stream && stream !== this.stream_) {
  311. this.clearAll();
  312. this.stream_ = stream;
  313. }
  314. }
  315. /**
  316. * Get the current stream.
  317. * @public
  318. * @return {shaka.extern.Stream}
  319. */
  320. getStream() {
  321. return this.stream_;
  322. }
  323. /**
  324. * Remove a segment from prefetch map and abort it.
  325. * @param {!(shaka.media.SegmentReference|shaka.media.InitSegmentReference)}
  326. * reference
  327. * @private
  328. */
  329. abortPrefetchedSegment_(reference) {
  330. const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
  331. let prefetchMap = this.segmentPrefetchMap_;
  332. if (reference instanceof shaka.media.InitSegmentReference) {
  333. prefetchMap = this.initSegmentPrefetchMap_;
  334. }
  335. const segmentPrefetchOperation = prefetchMap.get(reference);
  336. prefetchMap.delete(reference);
  337. if (segmentPrefetchOperation) {
  338. segmentPrefetchOperation.abort();
  339. if (reference instanceof shaka.media.SegmentReference) {
  340. shaka.log.debug(
  341. logPrefix,
  342. 'pop and abort prefetched segment at time:', reference.startTime);
  343. } else {
  344. shaka.log.debug(logPrefix, 'pop and abort prefetched init segment');
  345. }
  346. }
  347. }
  348. /**
  349. * The prefix of the logs that are created in this class.
  350. * @return {string}
  351. * @private
  352. */
  353. static logPrefix_(stream) {
  354. return 'SegmentPrefetch(' + stream.type + ':' + stream.id + ')';
  355. }
  356. };
  357. /**
  358. * @summary
  359. * This class manages a segment prefetch operation.
  360. */
  361. shaka.media.SegmentPrefetchOperation = class {
  362. /**
  363. * @param {shaka.media.SegmentPrefetch.FetchDispatcher} fetchDispatcher
  364. */
  365. constructor(fetchDispatcher) {
  366. /** @private {shaka.media.SegmentPrefetch.FetchDispatcher} */
  367. this.fetchDispatcher_ = fetchDispatcher;
  368. /** @private {?function(BufferSource):!Promise} */
  369. this.streamDataCallback_ = null;
  370. /** @private {?shaka.net.NetworkingEngine.PendingRequest} */
  371. this.operation_ = null;
  372. }
  373. /**
  374. * @param {shaka.media.SegmentPrefetch.FetchDispatcher} fetchDispatcher
  375. */
  376. replaceFetchDispatcher(fetchDispatcher) {
  377. this.fetchDispatcher_ = fetchDispatcher;
  378. }
  379. /**
  380. * Fetch a segments
  381. *
  382. * @param {!(shaka.media.SegmentReference|shaka.media.InitSegmentReference)}
  383. * reference
  384. * @param {!shaka.extern.Stream} stream
  385. * @return {!Promise}
  386. * @public
  387. */
  388. dispatchFetch(reference, stream) {
  389. // We need to store the data, because streamDataCallback_ might not be
  390. // available when you start getting the first data.
  391. let buffered = new Uint8Array(0);
  392. this.operation_ = this.fetchDispatcher_(
  393. reference, stream, async (data) => {
  394. if (buffered.byteLength > 0) {
  395. buffered = shaka.util.Uint8ArrayUtils.concat(buffered, data);
  396. } else {
  397. buffered = data;
  398. }
  399. if (this.streamDataCallback_) {
  400. await this.streamDataCallback_(buffered);
  401. buffered = new Uint8Array(0);
  402. }
  403. });
  404. return this.operation_.promise.catch((e) => {
  405. // Ignore OPERATION_ABORTED errors.
  406. if (e instanceof shaka.util.Error &&
  407. e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  408. return Promise.resolve();
  409. }
  410. // Continue to surface other errors.
  411. return Promise.reject(e);
  412. });
  413. }
  414. /**
  415. * Get the operation of prefetched segment if already exists.
  416. *
  417. * @return {?shaka.net.NetworkingEngine.PendingRequest} op
  418. * @public
  419. */
  420. getOperation() {
  421. return this.operation_;
  422. }
  423. /**
  424. * @param {?function(BufferSource):!Promise} streamDataCallback
  425. * @public
  426. */
  427. setStreamDataCallback(streamDataCallback) {
  428. this.streamDataCallback_ = streamDataCallback;
  429. }
  430. /**
  431. * Abort the current operation if exists.
  432. */
  433. abort() {
  434. if (this.operation_) {
  435. this.operation_.abort();
  436. }
  437. }
  438. };
  439. /**
  440. * @typedef {function(
  441. * !(shaka.media.InitSegmentReference|shaka.media.SegmentReference),
  442. * shaka.extern.Stream,
  443. * ?function(BufferSource):!Promise=
  444. * ):!shaka.net.NetworkingEngine.PendingRequest}
  445. *
  446. * @description
  447. * A callback function that fetches a segment.
  448. * @export
  449. */
  450. shaka.media.SegmentPrefetch.FetchDispatcher;