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
193 changes: 193 additions & 0 deletions lib/media/segment_prefetch.js
Original file line number Diff line number Diff line change
@@ -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;
Loading