Skip to content

Commit

Permalink
feat: Support Parallel Segment Fetching (shaka-project#4784)
Browse files Browse the repository at this point in the history
closes shaka-project#4658.

This solution is inspired by abandoned PR
shaka-project#2809, which
implements segment prefetching ahead of current play head.


![image](https://user-images.githubusercontent.com/3315733/205465795-75c605d2-c2e3-4d03-90f5-46a72a7189d2.png)
  • Loading branch information
tyrelltle committed Jan 31, 2023
1 parent 7439a26 commit de6abde
Show file tree
Hide file tree
Showing 13 changed files with 764 additions and 14 deletions.
1 change: 1 addition & 0 deletions build/types/core
Expand Up @@ -38,6 +38,7 @@
+../../lib/media/segment_reference.js
+../../lib/media/stall_detector.js
+../../lib/media/streaming_engine.js
+../../lib/media/segment_prefetch.js
+../../lib/media/time_ranges_utils.js
+../../lib/media/video_wrapper.js
+../../lib/media/webm_segment_index_parser.js
Expand Down
1 change: 1 addition & 0 deletions demo/common/message_ids.js
Expand Up @@ -283,5 +283,6 @@ shakaDemo.MessageIds = {
VIDEO_ROBUSTNESS: 'DEMO_VIDEO_ROBUSTNESS',
VNOVA: 'DEMO_VNOVA',
XLINK_FAIL_GRACEFULLY: 'DEMO_XLINK_FAIL_GRACEFULLY',
SEGMENT_PREFETCH_LIMIT: 'DEMO_SEGMENT_PREFETCH_LIMIT',
};
/* eslint-enable max-len */
4 changes: 3 additions & 1 deletion demo/config.js
Expand Up @@ -426,7 +426,9 @@ shakaDemo.Config = class {
.addBoolInput_(MessageIds.OBSERVE_QUALITY_CHANGES,
'streaming.observeQualityChanges')
.addNumberInput_(MessageIds.MAX_DISABLED_TIME,
'streaming.maxDisabledTime');
'streaming.maxDisabledTime')
.addNumberInput_(MessageIds.SEGMENT_PREFETCH_LIMIT,
'streaming.segmentPrefetchLimit');

if (!shakaDemoMain.getNativeControlsEnabled()) {
this.addBoolInput_(MessageIds.ALWAYS_STREAM_TEXT,
Expand Down
3 changes: 2 additions & 1 deletion demo/locales/en.json
Expand Up @@ -261,5 +261,6 @@
"DEMO_WIDEVINE": "Widevine DRM",
"DEMO_XLINK": "XLink",
"DEMO_XLINK_FAIL_GRACEFULLY": "Xlink Should Fail Gracefully",
"DEMO_XLINK_SEARCH": "Filters for assets that have XLINK tags in their manifests, so that they can be broken into multiple files."
"DEMO_XLINK_SEARCH": "Filters for assets that have XLINK tags in their manifests, so that they can be broken into multiple files.",
"DEMO_SEGMENT_PREFETCH_LIMIT": "Segment Prefetch Limit"
}
4 changes: 4 additions & 0 deletions demo/locales/source.json
Expand Up @@ -1050,5 +1050,9 @@
"DEMO_XLINK_SEARCH": {
"description": "A tooltip for an optional search term.",
"message": "Filters for assets that have [JARGON:XLINK] tags in their manifests, so that they can be broken into multiple files."
},
"DEMO_SEGMENT_PREFETCH_LIMIT": {
"description": "Max number of segments to be prefetched ahead of current time position.",
"message": "Segment Prefetch Limit."
}
}
1 change: 1 addition & 0 deletions docs/tutorials/config.md
Expand Up @@ -78,6 +78,7 @@ player.getConfiguration();
retryParameters: Object
startAtSegmentBoundary: false
safeSeekOffset: 5
segmentPrefetchLimit: 0
textDisplayFactory: Function


Expand Down
8 changes: 7 additions & 1 deletion externs/shaka/player.js
Expand Up @@ -973,7 +973,8 @@ shaka.extern.ManifestConfiguration;
* dispatchAllEmsgBoxes: boolean,
* observeQualityChanges: boolean,
* maxDisabledTime: number,
* parsePrftBox: boolean
* parsePrftBox: boolean,
* segmentPrefetchLimit: number
* }}
*
* @description
Expand Down Expand Up @@ -1083,6 +1084,11 @@ shaka.extern.ManifestConfiguration;
* start date will not change, and would save parsing the segment multiple
* times needlessly.
* Defaults to <code>false</code>.
* @property {boolean} segmentPrefetchLimit
* The maximum number of segments for each active stream to be prefetched
* ahead of playhead in parallel.
* If <code>0</code>, the segments will be fetched sequentially.
* Defaults to <code>0</code>.
* @exportDoc
*/
shaka.extern.StreamingConfiguration;
Expand Down
193 changes: 193 additions & 0 deletions lib/media/segment_prefetch.js
@@ -0,0 +1,193 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

goog.require('goog.asserts');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.SegmentReference');
goog.provide('shaka.media.SegmentPrefetch');
goog.require('shaka.log');

/**
* @summary
* This class manages segment prefetch operations.
* Called by StreamingEngine to prefetch next N segments
* ahead of playhead, to reduce the chances of rebuffering.
*/
shaka.media.SegmentPrefetch = class {
/**
* @param {number} prefetchLimit
* @param {shaka.extern.Stream} stream
* @param {shaka.media.SegmentPrefetch.FetchDispatcher} fetchDispatcher
*/
constructor(prefetchLimit, stream, fetchDispatcher) {
/** @private {number} */
this.prefetchLimit_ = prefetchLimit;

/** @private {shaka.extern.Stream} */
this.stream_ = stream;

/** @private {number} */
this.prefetchPosTime_ = 0;

/** @private {shaka.media.SegmentPrefetch.FetchDispatcher} */
this.fetchDispatcher_ = fetchDispatcher;

/**
* @private {!Map.<shaka.media.SegmentReference,
* !shaka.net.NetworkingEngine.PendingRequest>}
*/
this.segmentPrefetchMap_ = new Map();
}

/**
* Fetch next segments ahead of current segment.
*
* @param {(!shaka.media.SegmentReference)} startReference
* @public
*/
prefetchSegments(startReference) {
goog.asserts.assert(this.prefetchLimit_ > 0,
'SegmentPrefetch can not be used when prefetchLimit <= 0.');

const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
if (!this.stream_.segmentIndex) {
shaka.log.info(logPrefix, 'missing segmentIndex');
return;
}
const currTime = startReference.startTime;
const maxTime = Math.max(currTime, this.prefetchPosTime_);
const iterator = this.stream_.segmentIndex.getIteratorForTime(maxTime);
let reference = startReference;
while (this.segmentPrefetchMap_.size < this.prefetchLimit_ &&
reference != null) {
if (!this.segmentPrefetchMap_.has(reference)) {
const op = this.fetchDispatcher_(reference, this.stream_);
this.segmentPrefetchMap_.set(reference, op);
}
this.prefetchPosTime_ = reference.startTime;
reference = iterator.next().value;
}
}

/**
* Get the result of prefetched segment if already exists.
* @param {(!shaka.media.SegmentReference)} reference
* @return {?shaka.net.NetworkingEngine.PendingRequest} op
* @public
*/
getPrefetchedSegment(reference) {
goog.asserts.assert(this.prefetchLimit_ > 0,
'SegmentPrefetch can not be used when prefetchLimit <= 0.');
goog.asserts.assert(reference instanceof shaka.media.SegmentReference,
'getPrefetchedSegment is only used for shaka.media.SegmentReference.');

const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);

if (this.segmentPrefetchMap_.has(reference)) {
const op = this.segmentPrefetchMap_.get(reference);
this.segmentPrefetchMap_.delete(reference);
shaka.log.info(
logPrefix,
'reused prefetched segment at time:', reference.startTime,
'mapSize', this.segmentPrefetchMap_.size);
return op;
} else {
shaka.log.info(
logPrefix,
'missed segment at time:', reference.startTime,
'mapSize', this.segmentPrefetchMap_.size);
return null;
}
}

/**
* Clear all segment data.
* @public
*/
clearAll() {
if (this.segmentPrefetchMap_.size === 0) {
return;
}
const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
for (const reference of this.segmentPrefetchMap_.keys()) {
if (reference) {
this.abortPrefetchedSegment_(reference);
}
}
shaka.log.info(logPrefix, 'cleared all');
this.prefetchPosTime_ = 0;
}

/**
* Reset the prefetchLimit and clear all internal states.
* Called by StreamingEngine when configure() was called.
* @param {number} newPrefetchLimit
* @public
*/
resetLimit(newPrefetchLimit) {
goog.asserts.assert(newPrefetchLimit >= 0,
'The new prefetch limit must be >= 0.');
this.prefetchLimit_ = newPrefetchLimit;
const keyArr = Array.from(this.segmentPrefetchMap_.keys());
while (keyArr.length > newPrefetchLimit) {
const reference = keyArr.pop();
if (reference) {
this.abortPrefetchedSegment_(reference);
}
}
}

/**
* Called by Streaming Engine when switching variant.
* @param {shaka.extern.Stream} stream
* @public
*/
switchStream(stream) {
if (stream && stream !== this.stream_) {
this.clearAll();
this.stream_ = stream;
}
}

/**
* Remove a segment from prefetch map and abort it.
* @param {(!shaka.media.SegmentReference)} reference
* @private
*/
abortPrefetchedSegment_(reference) {
const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
const operation = this.segmentPrefetchMap_.get(reference);
this.segmentPrefetchMap_.delete(reference);
if (operation) {
operation.abort();
shaka.log.info(
logPrefix,
'pop and abort prefetched segment at time:', reference.startTime);
}
}

/**
* The prefix of the logs that are created in this class.
* @return {string}
* @private
*/
static logPrefix_(stream) {
return 'SegmentPrefetch(' + stream.type + ':' + stream.id + ')';
}
};

/**
* @typedef {function(
* !(shaka.media.InitSegmentReference|shaka.media.SegmentReference),
* shaka.extern.Stream
* ):!shaka.net.NetworkingEngine.PendingRequest}
*
* @description
* A callback function that fetches a segment.
* @export
*/
shaka.media.SegmentPrefetch.FetchDispatcher;

0 comments on commit de6abde

Please sign in to comment.