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