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: prefetch audio languages. #6139

Merged
merged 14 commits into from Jan 29, 2024
16 changes: 14 additions & 2 deletions demo/config.js
Expand Up @@ -424,7 +424,17 @@ shakaDemo.Config = class {
.addBoolInput_('Parse PRFT box',
'streaming.parsePrftBox')
.addNumberInput_('Segment Prefetch Limit',
'streaming.segmentPrefetchLimit')
'streaming.segmentPrefetchLimit',
/* canBeDecimal= */ false,
/* canBeZero= */ true,
/* canBeUnset= */ true)
.addCustomTextInput_('Prefetch audio languages', (input) => {
shakaDemoMain.configure(
'streaming.prefetchAudioLanguages',
input.value.split(',').filter(Boolean));
})
.addBoolInput_('Disable Video Prefetch',
'streaming.disableVideoPrefetch')
.addBoolInput_('Live Sync', 'streaming.liveSync')
.addNumberInput_('Max latency for live sync',
'streaming.liveSyncMaxLatency',
Expand Down Expand Up @@ -752,7 +762,9 @@ shakaDemo.Config = class {
addNumberInput_(name, valueName, canBeDecimal = false, canBeZero = true,
canBeUnset = false, tooltipMessage) {
const onChange = (input) => {
shakaDemoMain.resetConfiguration(valueName);
if (valueName !== 'streaming.segmentPrefetchLimit') {
gkatsev marked this conversation as resolved.
Show resolved Hide resolved
shakaDemoMain.resetConfiguration(valueName);
}
shakaDemoMain.remakeHash();
if (input.value == 'Infinity') {
shakaDemoMain.configure(valueName, Infinity);
Expand Down
11 changes: 10 additions & 1 deletion externs/shaka/player.js
Expand Up @@ -1137,6 +1137,8 @@ shaka.extern.ManifestConfiguration;
* maxDisabledTime: number,
* parsePrftBox: boolean,
* segmentPrefetchLimit: number,
* prefetchAudioLanguages: !Array<string>,
* disableVideoPrefetch: boolean,
* liveSync: boolean,
* liveSyncMaxLatency: number,
* liveSyncPlaybackRate: number,
Expand Down Expand Up @@ -1255,11 +1257,18 @@ 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
* @property {number} segmentPrefetchLimit
avelad marked this conversation as resolved.
Show resolved Hide resolved
* 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>.
* @property {!Array<string>} prefetchAudioLanguages
* The audio languages to prefetch.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switching is a lot faster if rebufferingGoal is set to a low value. Is this something worth noting here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I do not think it's necessary

* Defaults to an empty array.
* @property {boolean} disableVideoPrefetch
* If set and prefetch limit is defined, it will prevent from prefetching data
* for video.
* Defaults to <code>false</code>.
* @property {boolean} liveSync
* Enable the live stream sync against the live edge by changing the playback
* rate. Defaults to <code>false</code>.
Expand Down
172 changes: 140 additions & 32 deletions lib/media/segment_prefetch.js
Expand Up @@ -25,8 +25,9 @@ shaka.media.SegmentPrefetch = class {
* @param {number} prefetchLimit
* @param {shaka.extern.Stream} stream
* @param {shaka.media.SegmentPrefetch.FetchDispatcher} fetchDispatcher
* @param {boolean} deleteOnGet
*/
constructor(prefetchLimit, stream, fetchDispatcher) {
constructor(prefetchLimit, stream, fetchDispatcher, deleteOnGet = true) {
/** @private {number} */
this.prefetchLimit_ = prefetchLimit;

Expand All @@ -39,12 +40,29 @@ shaka.media.SegmentPrefetch = class {
/** @private {shaka.media.SegmentPrefetch.FetchDispatcher} */
this.fetchDispatcher_ = fetchDispatcher;

/** @private {boolean} */
this.deleteOnGet_ = deleteOnGet;

/**
* @private {!Map.<
* !(shaka.media.SegmentReference|shaka.media.InitSegmentReference),
* !(shaka.media.SegmentReference),
* !shaka.media.SegmentPrefetchOperation>}
*/
this.segmentPrefetchMap_ = new Map();

/**
* @private {!Map.<
* !(shaka.media.InitSegmentReference),
* !shaka.media.SegmentPrefetchOperation>}
*/
this.initSegmentPrefetchMap_ = new Map();
}

/**
* @return {number}
*/
getLastKnownPosition() {
return this.prefetchPosTime_;
}

/**
Expand All @@ -55,28 +73,40 @@ shaka.media.SegmentPrefetch = class {
* @public
*/
prefetchSegments(startReference, skipFirst = false) {
gkatsev marked this conversation as resolved.
Show resolved Hide resolved
this.prefetchSegmentsByTime(startReference.startTime, false, skipFirst);
}

/**
* Fetch next segments ahead of current time.
*
* @param {number} currTime
* @param {boolean=} fetchInit
* @param {boolean=} skipFirst
* @public
*/
prefetchSegmentsByTime(currTime, fetchInit = false, skipFirst = false) {
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');
shaka.log.debug(logPrefix, 'missing segmentIndex');
return;
}
const currTime = startReference.startTime;
const maxTime = Math.max(currTime, this.prefetchPosTime_);
const iterator = this.stream_.segmentIndex.getIteratorForTime(maxTime);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I realized that this would not work for partial segments in HLS, so I am going to make a change so that it works as well.

if (!iterator) {
return;
}
let reference = startReference;
let reference = iterator.next().value;
if (skipFirst) {
reference = iterator.next().value;
if (reference &&
reference.startTime == startReference.startTime &&
reference.endTime == startReference.endTime) {
reference = null;
}
}
if (!reference) {
return;
}
if (fetchInit && reference.initSegmentReference) {
this.prefetchInitSegment(reference.initSegmentReference);
}
while (this.segmentPrefetchMap_.size < this.prefetchLimit_ &&
reference != null) {
Expand Down Expand Up @@ -117,17 +147,18 @@ shaka.media.SegmentPrefetch = class {

const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
if (!this.stream_.segmentIndex) {
shaka.log.info(logPrefix, 'missing segmentIndex');
shaka.log.debug(logPrefix, 'missing segmentIndex');
return;
}

// init segments are ignored from the prefetch limit
if (this.segmentPrefetchMap_.size < this.prefetchLimit_) {
if (!this.segmentPrefetchMap_.has(initSegmentReference)) {
if (!this.initSegmentPrefetchMap_.has(initSegmentReference)) {
const segmentPrefetchOperation =
new shaka.media.SegmentPrefetchOperation(this.fetchDispatcher_);
segmentPrefetchOperation.dispatchFetch(
initSegmentReference, this.stream_);
this.segmentPrefetchMap_.set(
this.initSegmentPrefetchMap_.set(
initSegmentReference, segmentPrefetchOperation);
}
}
Expand All @@ -147,56 +178,102 @@ shaka.media.SegmentPrefetch = class {

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

if (this.segmentPrefetchMap_.has(reference)) {
const segmentPrefetchOperation = this.segmentPrefetchMap_.get(reference);
let prefetchMap = this.segmentPrefetchMap_;
if (reference instanceof shaka.media.InitSegmentReference) {
prefetchMap = this.initSegmentPrefetchMap_;
}

if (prefetchMap.has(reference)) {
const segmentPrefetchOperation = prefetchMap.get(reference);
if (streamDataCallback) {
segmentPrefetchOperation.setStreamDataCallback(streamDataCallback);
}
this.segmentPrefetchMap_.delete(reference);
if (this.deleteOnGet_) {
prefetchMap.delete(reference);
}
if (reference instanceof shaka.media.SegmentReference) {
shaka.log.debug(
logPrefix,
'reused prefetched segment at time:', reference.startTime,
'mapSize', this.segmentPrefetchMap_.size);
'mapSize', prefetchMap.size);
} else {
shaka.log.debug(
logPrefix,
'reused prefetched init segment at time, mapSize',
this.segmentPrefetchMap_.size);
prefetchMap.size);
}
return segmentPrefetchOperation.getOperation();
} else {
if (reference instanceof shaka.media.SegmentReference) {
shaka.log.debug(
logPrefix,
'missed segment at time:', reference.startTime,
'mapSize', this.segmentPrefetchMap_.size);
'mapSize', prefetchMap.size);
} else {
shaka.log.debug(
logPrefix,
'missed init segment at time, mapSize',
this.segmentPrefetchMap_.size);
prefetchMap.size);
}
return null;
}
}

/**
* Clear All Helper
* @private
*/
clearMap_(map) {
for (const reference of map.keys()) {
if (reference) {
this.abortPrefetchedSegment_(reference);
}
}
}

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

/**
* @param {number} time
*/
evict(time) {
for (const ref of this.segmentPrefetchMap_.keys()) {
if (time > ref.endTime) {
this.abortPrefetchedSegment_(ref);
}
}
this.clearInitSegments_();
}

/**
* Remove all init segments that don't have associated segments in
* the segment prefetch map.
* By default, with delete on get, the init segments should get removed as
* they are used. With deleteOnGet set to false, we need to clear them
* every so often once the segments that are associated with each init segment
* is no longer prefetched.
* @private
*/
clearInitSegments_() {
const segmentReferences = Array.from(this.segmentPrefetchMap_.keys());
for (const initSegmentReference of this.initSegmentPrefetchMap_.keys()) {
// if no segment references this init segment, we should remove it.
if (!segmentReferences.some(
(segmentReference) =>
segmentReference.initSegmentReference === initSegmentReference)) {
this.abortPrefetchedSegment_(initSegmentReference);
}
}
shaka.log.info(logPrefix, 'cleared all');
this.prefetchPosTime_ = 0;
}

/**
Expand All @@ -208,6 +285,9 @@ shaka.media.SegmentPrefetch = class {
resetLimit(newPrefetchLimit) {
goog.asserts.assert(newPrefetchLimit >= 0,
'The new prefetch limit must be >= 0.');

const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
shaka.log.debug(logPrefix, 'resetting prefetch limit to', newPrefetchLimit);
this.prefetchLimit_ = newPrefetchLimit;
const keyArr = Array.from(this.segmentPrefetchMap_.keys());
while (keyArr.length > newPrefetchLimit) {
Expand All @@ -218,18 +298,39 @@ shaka.media.SegmentPrefetch = class {
}
}

/**
* Update deleteOnGet.
* @param {boolean} newDeleteOnGet
* @public
*/
deleteOnGet(newDeleteOnGet) {
this.deleteOnGet_ = newDeleteOnGet;
}

/**
* Called by Streaming Engine when switching variant.
* @param {shaka.extern.Stream} stream
* @public
*/
switchStream(stream) {
goog.asserts.assert(this.deleteOnGet_,
'switchStream should only be used if deleteOnGet is true');

if (stream && stream !== this.stream_) {
this.clearAll();
this.stream_ = stream;
}
}

/**
* Get the current stream.
* @public
* @return {shaka.extern.Stream}
*/
getStream() {
return this.stream_;
}

/**
* Remove a segment from prefetch map and abort it.
* @param {!(shaka.media.SegmentReference|shaka.media.InitSegmentReference)}
Expand All @@ -238,16 +339,23 @@ shaka.media.SegmentPrefetch = class {
*/
abortPrefetchedSegment_(reference) {
const logPrefix = shaka.media.SegmentPrefetch.logPrefix_(this.stream_);
const segmentPrefetchOperation = this.segmentPrefetchMap_.get(reference);
this.segmentPrefetchMap_.delete(reference);

let prefetchMap = this.segmentPrefetchMap_;
if (reference instanceof shaka.media.InitSegmentReference) {
prefetchMap = this.initSegmentPrefetchMap_;
}

const segmentPrefetchOperation = prefetchMap.get(reference);
prefetchMap.delete(reference);

if (segmentPrefetchOperation) {
segmentPrefetchOperation.abort();
if (reference instanceof shaka.media.SegmentReference) {
shaka.log.info(
shaka.log.debug(
logPrefix,
'pop and abort prefetched segment at time:', reference.startTime);
} else {
shaka.log.info(logPrefix, 'pop and abort prefetched init segment');
shaka.log.debug(logPrefix, 'pop and abort prefetched init segment');
}
}
}
Expand Down