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