Skip to content

Commit

Permalink
fix: Fix HLS dynamic to static transition (#4483)
Browse files Browse the repository at this point in the history
fix: Fix HLS dynamic to static transition

 - Keep maxSegmentEndTime_ updated in PresentationTimeline by calling
   notifySegments on each HLS playlist update, so that the seek range
   doesn't revert to the original playlist size when it becomes
   static.
 - Wait to change the presentation type to VOD until after _all_
   active playlists have an ENDLIST tag, to avoid missing the final
   segments in one type or the other.
 - Stop updating the playlists after transition to VOD.
 - Update the MSE duration at exactly the same time as we transition
   to VOD, to avoid a loophole where the UI knows it's VOD, but
   doesn't have any way to get the correct duration.  Previously, this
   state would persist until the final segments were appended.

Closes #4431
  • Loading branch information
joeyparrish authored and JulianDomingo committed Oct 6, 2022
1 parent e3776b5 commit d9ecbf3
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 269 deletions.
5 changes: 4 additions & 1 deletion externs/shaka/manifest_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ shaka.extern.ManifestParser = class {
* onError: function(!shaka.util.Error),
* isLowLatencyMode: function():boolean,
* isAutoLowLatencyMode: function():boolean,
* enableLowLatencyMode: function()
* enableLowLatencyMode: function(),
* updateDuration: function()
* }}
*
* @description
Expand Down Expand Up @@ -147,6 +148,8 @@ shaka.extern.ManifestParser = class {
* Return true if auto low latency streaming mode is enabled.
* @property {function()} enableLowLatencyMode
* Enable low latency streaming mode.
* @property {function()} updateDuration
* Update the presentation duration based on PresentationTimeline.
* @exportDoc
*/
shaka.extern.ManifestParser.PlayerInterface;
Expand Down
38 changes: 30 additions & 8 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,21 @@ shaka.hls.HlsParser = class {
}

await Promise.all(updates);

this.notifySegments_();

// If any hasEndList is false, the stream is still live.
const stillLive = streamInfos.some((s) => s.hasEndList == false);
if (!stillLive) {
// Convert the presentation to VOD and set the duration.
const PresentationType = shaka.hls.HlsParser.PresentationType_;
this.setPresentationType_(PresentationType.VOD);

const maxTimestamps = streamInfos.map((s) => s.maxTimestamp);
// The duration is the minimum of the end times of all streams.
this.presentationTimeline_.setDuration(Math.min(...maxTimestamps));
this.playerInterface_.updateDuration();
}
}

/**
Expand All @@ -294,7 +309,6 @@ shaka.hls.HlsParser = class {
* @private
*/
async updateStream_(streamInfo) {
const PresentationType = shaka.hls.HlsParser.PresentationType_;
const manifestUri = streamInfo.absoluteMediaPlaylistUri;
const uriObj = new goog.Uri(manifestUri);
if (this.lowLatencyMode_ && streamInfo.canSkipSegments) {
Expand Down Expand Up @@ -327,6 +341,7 @@ shaka.hls.HlsParser = class {
streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables,
stream.codecs);
this.segmentsToNotifyByStream_.push(segments);

stream.segmentIndex.mergeAndEvict(
segments, this.presentationTimeline_.getSegmentAvailabilityStart());
Expand All @@ -347,10 +362,10 @@ shaka.hls.HlsParser = class {
shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');

if (endListTag) {
// Convert the presentation to VOD and set the duration to the last
// segment's end time.
this.setPresentationType_(PresentationType.VOD);
this.presentationTimeline_.setDuration(newestSegment.endTime);
// Flag this for later. We don't convert the whole presentation into VOD
// until we've seen the ENDLIST tag for all active playlists.
streamInfo.hasEndList = true;
streamInfo.maxTimestamp = newestSegment.endTime;
}
}

Expand Down Expand Up @@ -1707,6 +1722,7 @@ shaka.hls.HlsParser = class {
maxTimestamp: lastEndTime,
mediaSequenceToStartTime,
canSkipSegments,
hasEndList: false,
};
}

Expand Down Expand Up @@ -2557,8 +2573,11 @@ shaka.hls.HlsParser = class {
try {
await this.update();

const delay = this.updatePlaylistDelay_;
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
// This may have converted to VOD, in which case we stop updating.
if (this.isLive_()) {
const delay = this.updatePlaylistDelay_;
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
}
} catch (error) {
// Detect a call to stop() during this.update()
if (!this.playerInterface_) {
Expand Down Expand Up @@ -2749,7 +2768,8 @@ shaka.hls.HlsParser = class {
* absoluteMediaPlaylistUri: string,
* maxTimestamp: number,
* mediaSequenceToStartTime: !Map.<number, number>,
* canSkipSegments: boolean
* canSkipSegments: boolean,
* hasEndList: boolean
* }}
*
* @description
Expand All @@ -2772,6 +2792,8 @@ shaka.hls.HlsParser = class {
* @property {boolean} canSkipSegments
* True if the server supports delta playlist updates, and we can send a
* request for a playlist that can skip older media segments.
* @property {boolean} hasEndList
* True if the stream has an EXT-X-ENDLIST tag.
*/
shaka.hls.HlsParser.StreamInfo;

Expand Down
24 changes: 14 additions & 10 deletions lib/media/media_source_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -849,21 +849,25 @@ shaka.media.MediaSourceEngine = class {
}

/**
* We only support increasing duration at this time. Decreasing duration
* causes the MSE removal algorithm to run, which results in an 'updateend'
* event. Supporting this scenario would be complicated, and is not currently
* needed.
*
* @param {number} duration
* @return {!Promise}
*/
async setDuration(duration) {
goog.asserts.assert(
isNaN(this.mediaSource_.duration) ||
this.mediaSource_.duration <= duration,
'duration cannot decrease: ' + this.mediaSource_.duration + ' -> ' +
duration);
await this.enqueueBlockingOperation_(() => {
// Reducing the duration causes the MSE removal algorithm to run, which
// triggers an 'updateend' event to fire. To handle this scenario, we
// have to insert a dummy operation into the beginning of each queue,
// which the 'updateend' handler will remove.
if (duration < this.mediaSource_.duration) {
for (const contentType in this.sourceBuffers_) {
const dummyOperation = {
start: () => {},
p: new shaka.util.PublicPromise(),
};
this.queues_[contentType].unshift(dummyOperation);
}
}

this.mediaSource_.duration = duration;
});
}
Expand Down
5 changes: 2 additions & 3 deletions lib/media/streaming_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ shaka.media.StreamingEngine = class {
this.manifest_.sequenceMode);
this.destroyer_.ensureNotDestroyed();

this.setDuration_();
this.updateDuration();

for (const type of streamsByType.keys()) {
const stream = streamsByType.get(type);
Expand Down Expand Up @@ -824,9 +824,8 @@ shaka.media.StreamingEngine = class {

/**
* Sets the MediaSource's duration.
* @private
*/
setDuration_() {
updateDuration() {
const duration = this.manifest_.presentationTimeline.getDuration();
if (duration < Infinity) {
this.playerInterface_.mediaSourceEngine.setDuration(duration);
Expand Down
1 change: 1 addition & 0 deletions lib/offline/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,7 @@ shaka.offline.Storage = class {
isLowLatencyMode: () => false,
isAutoLowLatencyMode: () => false,
enableLowLatencyMode: () => {},
updateDuration: () => {},
};

parser.configure(config.manifest);
Expand Down
5 changes: 5 additions & 0 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
enableLowLatencyMode: () => {
this.configure('streaming.lowLatencyMode', true);
},
updateDuration: () => {
if (this.streamingEngine_) {
this.streamingEngine_.updateDuration();
}
},
};

const startTime = Date.now() / 1000;
Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_content_protection_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('DashParser ContentProtection', () => {
isLowLatencyMode: () => false,
isAutoLowLatencyMode: () => false,
enableLowLatencyMode: () => {},
updateDuration: () => {},
};

const actual = await dashParser.start(
Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_live_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('DashParser Live', () => {
isLowLatencyMode: () => false,
isAutoLowLatencyMode: () => false,
enableLowLatencyMode: () => {},
updateDuration: () => {},
};
});

Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_manifest_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('DashParser Manifest', () => {
isLowLatencyMode: () => false,
isAutoLowLatencyMode: () => false,
enableLowLatencyMode: () => {},
updateDuration: () => {},
};
});

Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_segment_base_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('DashParser SegmentBase', () => {
isLowLatencyMode: () => false,
isAutoLowLatencyMode: () => false,
enableLowLatencyMode: () => {},
updateDuration: () => {},
};
});

Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_segment_list_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ describe('DashParser SegmentList', () => {
isLowLatencyMode: () => false,
isAutoLowLatencyMode: () => false,
enableLowLatencyMode: () => {},
updateDuration: () => {},
};
const manifest = await dashParser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
Expand Down
1 change: 1 addition & 0 deletions test/dash/dash_parser_segment_template_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('DashParser SegmentTemplate', () => {
isLowLatencyMode: () => false,
isAutoLowLatencyMode: () => false,
enableLowLatencyMode: () => {},
updateDuration: () => {},
};
});

Expand Down
Loading

0 comments on commit d9ecbf3

Please sign in to comment.