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(HLS): Poll HLS playlists using last segment duration #4779

Merged
merged 1 commit into from
Dec 7, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 78 additions & 53 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,6 @@ shaka.hls.HlsParser = class {
/** @private {shaka.hls.ManifestTextParser} */
this.manifestTextParser_ = new shaka.hls.ManifestTextParser();

/**
* This is the number of seconds we want to wait between finishing a
* manifest update and starting the next one. This will be set when we parse
* the manifest.
*
* @private {number}
*/
this.updatePlaylistDelay_ = 0;

/**
* The minimum sequence number for generated segments, when ignoring
* EXT-X-PROGRAM-DATE-TIME.
Expand Down Expand Up @@ -186,7 +177,7 @@ shaka.hls.HlsParser = class {
this.maxTargetDuration_ = 0;

/** @private {number} */
this.minTargetDuration_ = Infinity;
this.lastTargetDuration_ = Infinity;

/** Partial segments target duration.
* @private {number}
Expand Down Expand Up @@ -294,6 +285,9 @@ shaka.hls.HlsParser = class {
const updates = [];
const streamInfos = Array.from(this.uriToStreamInfosMap_.values());

// This is necessary to calculate correctly the update time.
this.lastTargetDuration_ = Infinity;

// Only update active streams.
const activeStreamInfos = streamInfos.filter((s) => s.stream.segmentIndex);
for (const streamInfo of activeStreamInfos) {
Expand Down Expand Up @@ -363,6 +357,8 @@ shaka.hls.HlsParser = class {
shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
}

this.determineLastTargetDuration_(playlist);

/** @type {!Array.<!shaka.hls.Tag>} */
const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
'EXT-X-DEFINE');
Expand Down Expand Up @@ -735,7 +731,7 @@ shaka.hls.HlsParser = class {
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
if (this.isLive_()) {
this.changePresentationTimelineToLive_();
const delay = this.updatePlaylistDelay_;
const delay = this.getUpdatePlaylistDelay_();
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
}
const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
Expand All @@ -761,13 +757,6 @@ shaka.hls.HlsParser = class {
'Presentation timeline not created!');

if (this.isLive_()) {
// The HLS spec (RFC 8216) states in 6.3.4:
// "the client MUST wait for at least the target duration before
// attempting to reload the Playlist file again".
// For LL-HLS, the server must add a new partial segment to the Playlist
// every part target duration.
this.updatePlaylistDelay_ = this.minTargetDuration_;

// The spec says nothing much about seeking in live content, but Safari's
// built-in HLS implementation does not allow it. Therefore we will set
// the availability window equal to the presentation delay. The player
Expand Down Expand Up @@ -1723,7 +1712,7 @@ shaka.hls.HlsParser = class {
this.determineDuration_();
// Finally, start the update timer, if this asset has been determined
// to be a livestream.
const delay = this.updatePlaylistDelay_;
const delay = this.getUpdatePlaylistDelay_();
if (delay > 0) {
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
}
Expand Down Expand Up @@ -2210,42 +2199,64 @@ shaka.hls.HlsParser = class {
this.setPresentationType_(PresentationType.EVENT);
}

const targetDurationTag = this.getRequiredTag_(playlist.tags,
'EXT-X-TARGETDURATION');
const targetDuration = Number(targetDurationTag.value);
const partialTargetDurationTag =
shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-PART-INF');
// According to the HLS spec, updates should not happen more often than
// once in targetDuration. It also requires us to only update the active
// variant. We might implement that later, but for now every variant
// will be updated. To get the update period, choose the smallest
// targetDuration value across all playlists.
// 1. Update the shortest one to use as update period and segment
// availability time (for LIVE).
if (this.lowLatencyMode_ && partialTargetDurationTag) {
// For low latency streaming, use the partial segment target duration.
this.partialTargetDuration_ = Number(
partialTargetDurationTag.getRequiredAttrValue('PART-TARGET'));
this.minTargetDuration_ = Math.min(
this.partialTargetDuration_, this.minTargetDuration_);
// Get the server-recommended min distance from the live edge.
const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
playlist.tags, 'EXT-X-SERVER-CONTROL');
// Use 'PART-HOLD-BACK' as the presentation delay for low latency mode.
this.lowLatencyPresentationDelay_ = serverControlTag ? Number(
serverControlTag.getRequiredAttrValue('PART-HOLD-BACK')) : 0;
} else {
// For regular HLS, use the target duration of regular segments.
this.minTargetDuration_ = Math.min(
targetDuration, this.minTargetDuration_);
this.determineLastTargetDuration_(playlist);
}
}


/**
* @param {!shaka.hls.Playlist} playlist
* @private
*/
determineLastTargetDuration_(playlist) {
const targetDurationTag = this.getRequiredTag_(playlist.tags,
'EXT-X-TARGETDURATION');
const targetDuration = Number(targetDurationTag.value);
const partialTargetDurationTag =
shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-PART-INF');
// According to the HLS spec, updates should not happen more often than
// once in targetDuration. It also requires us to only update the active
// variant. We might implement that later, but for now every variant
// will be updated. To get the update period, choose the smallest
// targetDuration value across all playlists.
// 1. Update the shortest one to use as update period and segment
// availability time (for LIVE).
if (this.lowLatencyMode_ && partialTargetDurationTag) {
// For low latency streaming, use the partial segment target duration.
this.partialTargetDuration_ = Number(
partialTargetDurationTag.getRequiredAttrValue('PART-TARGET'));
this.lastTargetDuration_ = Math.min(
this.partialTargetDuration_, this.lastTargetDuration_);
// Get the server-recommended min distance from the live edge.
const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
playlist.tags, 'EXT-X-SERVER-CONTROL');
// Use 'PART-HOLD-BACK' as the presentation delay for low latency mode.
this.lowLatencyPresentationDelay_ = serverControlTag ? Number(
serverControlTag.getRequiredAttrValue('PART-HOLD-BACK')) : 0;
} else {
let lastTargetDuration = Infinity;
const segments = playlist.segments;
if (segments.length) {
const lastSegment = segments[segments.length - 1];
const extinfTag =
shaka.hls.Utils.getFirstTagWithName(lastSegment.tags, 'EXTINF');
if (extinfTag) {
// The EXTINF tag format is '#EXTINF:<duration>,[<title>]'.
// We're interested in the duration part.
const extinfValues = extinfTag.value.split(',');
lastTargetDuration = Number(extinfValues[0]);
}
}
// 2. Update the longest target duration if need be to use as a
// presentation delay later.
this.maxTargetDuration_ = Math.max(
targetDuration, this.maxTargetDuration_);
this.lastTargetDuration_ = Math.min(
lastTargetDuration, this.lastTargetDuration_);
}
// 2. Update the longest target duration if need be to use as a
// presentation delay later.
this.maxTargetDuration_ = Math.max(
targetDuration, this.maxTargetDuration_);
}


/**
* @private
*/
Expand Down Expand Up @@ -2967,7 +2978,7 @@ shaka.hls.HlsParser = class {
shaka.log.info('Updating manifest...');

goog.asserts.assert(
this.updatePlaylistDelay_ > 0,
this.getUpdatePlaylistDelay_() > 0,
'We should only call |onUpdate_| when we are suppose to be updating.');

// Detect a call to stop()
Expand All @@ -2980,7 +2991,7 @@ shaka.hls.HlsParser = class {

// This may have converted to VOD, in which case we stop updating.
if (this.isLive_()) {
const delay = this.updatePlaylistDelay_;
const delay = this.getUpdatePlaylistDelay_();
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
}
} catch (error) {
Expand Down Expand Up @@ -3012,6 +3023,20 @@ shaka.hls.HlsParser = class {
}


/**
* @return {number}
* @private
*/
getUpdatePlaylistDelay_() {
// The HLS spec (RFC 8216) states in 6.3.4:
// "the client MUST wait for at least the target duration before
// attempting to reload the Playlist file again".
// For LL-HLS, the server must add a new partial segment to the Playlist
// every part target duration.
return this.lastTargetDuration_;
}


/**
* @param {shaka.hls.HlsParser.PresentationType_} type
* @private
Expand Down