Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support Parallel Segment Fetching #4784

Merged
merged 12 commits into from
Jan 31, 2023
1 change: 1 addition & 0 deletions build/types/core
Original file line number Diff line number Diff line change
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/transmuxer.js
+../../lib/media/video_wrapper.js
Expand Down
1 change: 1 addition & 0 deletions demo/common/message_ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,5 +281,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
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,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
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,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
Original file line number Diff line number Diff line change
Expand Up @@ -1042,5 +1042,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@ shaka.extern.ManifestConfiguration;
* dispatchAllEmsgBoxes: boolean,
* observeQualityChanges: boolean,
* maxDisabledTime: number,
* parsePrftBox: boolean
* parsePrftBox: boolean,
* segmentPrefetchLimit: number
* }}
*
* @description
Expand Down Expand Up @@ -1072,6 +1073,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
178 changes: 178 additions & 0 deletions lib/media/segment_prefetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*! @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
tyrelltle marked this conversation as resolved.
Show resolved Hide resolved
*/
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_(
theodab marked this conversation as resolved.
Show resolved Hide resolved
this.stream_,
);
if (!this.stream_.segmentIndex) {
shaka.log.info(
tyrelltle marked this conversation as resolved.
Show resolved Hide resolved
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;
tyrelltle marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* Get the result of prefetched segment if already exists.
* @param {(
* !shaka.media.InitSegmentReference|!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.');

let op = null;
if (!(reference instanceof shaka.media.SegmentReference)) {
return null;
}

const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(
tyrelltle marked this conversation as resolved.
Show resolved Hide resolved
this.stream_,
);

if (this.segmentPrefetchMap_.has(reference)) {
op = this.segmentPrefetchMap_.get(reference);
tyrelltle marked this conversation as resolved.
Show resolved Hide resolved
this.segmentPrefetchMap_.delete(reference);
shaka.log.info(
logPrefix,
'reused prefetched segment at time:', reference.startTime,
'mapSize', this.segmentPrefetchMap_.size);
} else {
shaka.log.info(
logPrefix,
'missed segment at time:', reference.startTime,
'mapSize', this.segmentPrefetchMap_.size);
}
return op;
}

/**
* Clear all segment data.
* @public
*/
clearAll() {
if (this.segmentPrefetchMap_.size === 0) {
return;
}
const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(
tyrelltle marked this conversation as resolved.
Show resolved Hide resolved
this.stream_,
);
for (const reference of this.segmentPrefetchMap_.keys()) {
const operation = this.segmentPrefetchMap_.get(reference);
this.segmentPrefetchMap_.delete(reference);
operation.abort();
}
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} prefetchLimit
* @public
*/
resetLimit(prefetchLimit) {
if (prefetchLimit !== this.prefetchLimit_) {
this.clearAll();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design-wise, I'm not sure it makes sense to always clear all of the segments away in this case.
For example, if you are going from a pre-fetch limit of 4 to a pre-fetch limit of 6, couldn't you just keep the stuff you have already pre-fetched?

It seems like the most efficient thing to do would be to store an array that keeps track of the order of the pre-fetched segments, and then pop and clear segments from the end of it until you fit into the new limit. That way nothing happens if the limit has been increased, and you clear away as few pre-fetched segments as possible in the case of the limit being decreased.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated. actually it turns out that Map.keys reserves the insertion order.
so instead of creating a new ordered array, i just grab segmentPrefetchMap_.keys and iterate from end to pop + clear the segments.
not sure Array.from(iterator) will actually do any iteration though, but it might have minimal performance impact since resetLimit could be called very rarely.
open to other suggestions :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushed a new commit to simplify resetLimit, by using while + arr.pop() instead of for loop from end of arr

this.prefetchLimit_ = prefetchLimit;
}
}

/**
* 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;
}
}

/** @private */
tyrelltle marked this conversation as resolved.
Show resolved Hide resolved
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;
Loading