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