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

Fix/more llhls fixes #1201

Merged
merged 26 commits into from
Oct 20, 2021
Merged

Fix/more llhls fixes #1201

merged 26 commits into from
Oct 20, 2021

Conversation

brandonocasey
Copy link
Contributor

Description

Based on #1195 for now, but we can pull it out if we want as only one of the fixes requires it.

@@ -1431,7 +1431,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
onSyncInfoUpdate_() {
let audioSeekable;

if (!this.masterPlaylistLoader_) {
if (!this.masterPlaylistLoader_ || this.sourceUpdater_.hasCreatedSourceBuffers()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was causing us to:

  1. As soon as audio/video segment loader fires this we come in here
  2. Since only one of the loaders is actually ready at this point, we get seekable from just that one
  3. We seek to the end of this invalid seekable range
  4. We come back in here again and this time we get an intersection between the two and a valid seekable
  5. We seek to the valid seekable end via playback watcher
  6. This causes content to be removes and things to be downloaded/appended again.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would be worth adding a small comment above stating that. Something like: // if only one source buffer has been created, the seekable range will be invalid

@@ -350,7 +350,9 @@ export default class PlaybackWatcher {

// verify that at least two segment durations have been
// appended before checking for a gap.
const twoSegmentDurations = (this.media().targetDuration - Ranges.TIME_FUDGE_FACTOR) * 2;
const targetDuration = this.media().partTargetDuration || this.media().targetDuration;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This helps skip gaps faster in llhls, mostly this is helping with our bad segment choice at the start of a playlist. It is likely something we want to do anyway though, as we don't want to wait very long to skip gaps in llhls.

@@ -257,7 +257,8 @@ const getAllSegments = function(media) {
export const isPlaylistUnchanged = (a, b) => a === b ||
(a.segments && b.segments && a.segments.length === b.segments.length &&
a.endList === b.endList &&
a.mediaSequence === b.mediaSequence);
a.mediaSequence === b.mediaSequence &&
(a.preloadSegment && b.preloadSegment && a.preloadSegment === b.preloadSegment));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without this line we can trigger playlistunchaned for llhls playlists that have changed.

src/playlist.js Outdated
@@ -117,7 +117,19 @@ const backwardDuration = function(playlist, endSequence) {
return { result: result + segment.end, precise: true };
}

result += segment.duration;
if (segment.preload) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This prevents us from considering the full segment duration for preload segments that do not have all of their parts yet.

Copy link
Member

Choose a reason for hiding this comment

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

should we have a helper like segmentDurationWithParts(segment) which does this piece and below?

@@ -321,19 +333,19 @@ export const playlistEnd = function(playlist, expired, useSafeLiveEnd, liveEdgeP

expired = expired || 0;

let lastSegmentTime = intervalDuration(
let lastSegmentEndTime = intervalDuration(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A more descriptive name as it isn't the start time, but the end time we are getting here.

@@ -1984,7 +1984,7 @@ export default class SegmentLoader extends videojs.EventTarget {
this.setTimeMapping_(segmentInfo.timeline);

// for tracking overall stats
this.updateMediaSecondsLoaded_(segmentInfo.segment);
this.updateMediaSecondsLoaded_(segmentInfo.part || segmentInfo.segment);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

stats fix

@@ -662,6 +665,8 @@ export default class PlaylistLoader extends EventTarget {
this.trigger('mediachanging');
}

this.pendingMedia_ = playlist;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added this to prevent switching to the same playlist twice at the start of playback.

Copy link
Contributor

Choose a reason for hiding this comment

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

Where does it get used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

masterPlaylistLoader_.shouldSwitchToMedia_()

buffered.end(buffered.length - 1) - this.tech_.currentTime() : 0;

const currentTime = this.tech_.currentTime();
const forwardBuffer = Ranges.timeAheadOf(buffered, currentTime);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use timeAheadOf so that we consider gaps in our forward buffer and don't try to switch when we have enough buffer but have gaps.


return shouldSwitchToMedia({
isBuffered,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't really want to switch at the start of playback for live streams before we have actually started. It almost doubles are time to first playback.

// appended before checking for a gap.
const twoSegmentDurations = (this.media().targetDuration - Ranges.TIME_FUDGE_FACTOR) * 2;
const durations = media.partTargetDuration ? media.partTargetDuration : media.targetDuration * 2;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I lowered this to one part target duration. parts are so tiny that its almost impossible not be be off by a few ms.

@@ -46,35 +46,33 @@ export const syncPointStrategies = [
const datetimeMapping =
syncController.timelineToDatetimeMappings[segment.timeline];

if (!datetimeMapping) {
if (!datetimeMapping || !segment.dateTimeObject) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

short circuit earlier

@codecov
Copy link

codecov bot commented Sep 20, 2021

Codecov Report

Merging #1201 (db2da0f) into main (1fe2df1) will increase coverage by 0.08%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1201      +/-   ##
==========================================
+ Coverage   86.33%   86.42%   +0.08%     
==========================================
  Files          39       39              
  Lines        9701     9743      +42     
  Branches     2243     2263      +20     
==========================================
+ Hits         8375     8420      +45     
+ Misses       1326     1323       -3     
Impacted Files Coverage Δ
src/master-playlist-controller.js 94.78% <100.00%> (+0.16%) ⬆️
src/playback-watcher.js 98.80% <100.00%> (+<0.01%) ⬆️
src/playlist-loader.js 95.04% <100.00%> (+0.33%) ⬆️
src/playlist.js 94.56% <100.00%> (+0.23%) ⬆️
src/segment-loader.js 96.31% <100.00%> (+0.07%) ⬆️
src/sync-controller.js 97.44% <100.00%> (+0.49%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1fe2df1...db2da0f. Read the comment docs.

Base automatically changed from fix/use-seeking to main September 22, 2021 19:10
src/playlist.js Outdated
@@ -117,7 +117,19 @@ const backwardDuration = function(playlist, endSequence) {
return { result: result + segment.end, precise: true };
}

result += segment.duration;
if (segment.preload) {
Copy link
Member

Choose a reason for hiding this comment

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

should we have a helper like segmentDurationWithParts(segment) which does this piece and below?

Comment on lines 1030 to 1032
// we must "resync" the segment loader when we switch renditions and
// the segment loader is already synced to the previous rendition
this.resyncLoader();
this.resetLoader();
Copy link
Member

Choose a reason for hiding this comment

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

How come we're switching from resync to reset here? Also, the comment would need to be changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only extra thing that reset does that resync does not is fetchAtBuffer_ = false. I think that we cannot use the end of the buffer for live stream segment selection reliably. This was especially true when changing renditions near the start of playback, as it would put us behind. Non-independent segments also don't change the buffer by themselves so if we switch while appending them we would be parts behind. We could change this so that it only resets during a live stream, otherwise it resyncs.

Copy link
Member

Choose a reason for hiding this comment

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

Might be nice to do this only for live streams, though, if @gesinger thinks it's fine to always do it, I won't have any objections.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed it to only reset during live streams.

@brandonocasey
Copy link
Contributor Author

Added tests for segmentInfoString and added segmentDurationWithParts + tests

gkatsev
gkatsev previously approved these changes Oct 18, 2021
Copy link
Member

@gkatsev gkatsev left a comment

Choose a reason for hiding this comment

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

LGTM and improves start-up time a lot.

@@ -47,6 +47,7 @@ const sumLoaderStat = function(stat) {
};
const shouldSwitchToMedia = function({
currentPlaylist,
isBuffered,
Copy link
Contributor

Choose a reason for hiding this comment

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

It took me a little to figure out what this reflected. It might make sense for shouldSwitchToMedia to take in the overall buffer and do the calculations on its own internally.

@@ -78,6 +79,10 @@ const shouldSwitchToMedia = function({
// playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
// in those segments, a viewer will never experience a rendition upswitch.
if (!currentPlaylist.endList) {
if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') {
log(`not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.`);
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add the comment you made in github below above here.

@@ -1431,7 +1431,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
onSyncInfoUpdate_() {
let audioSeekable;

if (!this.masterPlaylistLoader_) {
if (!this.masterPlaylistLoader_ || this.sourceUpdater_.hasCreatedSourceBuffers()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be worth adding a small comment above stating that. Something like: // if only one source buffer has been created, the seekable range will be invalid


// verify that at least two segment durations or one part duration have been
// appended before checking for a gap.
const durations = media.partTargetDuration ? media.partTargetDuration :
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we can rename this to something like minAppendedDuration.

@@ -662,6 +665,8 @@ export default class PlaylistLoader extends EventTarget {
this.trigger('mediachanging');
}

this.pendingMedia_ = playlist;
Copy link
Contributor

Choose a reason for hiding this comment

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

Where does it get used?

// the segment loader is already synced to the previous rendition
this.resyncLoader();

// for live streams reset, for vod resync
Copy link
Contributor

Choose a reason for hiding this comment

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

It may be worth adding why we reset for live vs resync for VOD.


loader.playlist(newPlaylist);

assert.true(resyncCalled, 'resync was called');
Copy link
Contributor

Choose a reason for hiding this comment

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

Since reset also calls resync, we'll want to check reset as well.

playlist.segments[4].parts[1].independent = true;
const segmentInfo = loader.chooseNextRequest_();

assert.equal(segmentInfo.partIndex, 1, 'previous part');
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't this match the partIndex above?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it does, but we also don't have anything buffered so it tries to choose sepmentIndex 4, partIndex 2 but that isn't independent so we choose the current partIndex. Let me see if I can find something better than this though.

mpc.masterPlaylistLoader_.media = () => ({id: 'foo', endList: false, partTargetDuration: 5});
const nextPlaylist = {id: 'bar', endList: false, partTargetDuration: 5};

assert.notOk(mpc.shouldSwitchToMedia_(nextPlaylist), 'should switch without currentPlaylist');
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be "shouldn't switch when video not buffered"?

mpc.masterPlaylistLoader_.media = () => ({id: 'foo', endList: false, partTargetDuration: 5});
const nextPlaylist = {id: 'bar', endList: false, partTargetDuration: 5};

assert.ok(mpc.shouldSwitchToMedia_(nextPlaylist), 'should switch without currentPlaylist');
Copy link
Contributor

Choose a reason for hiding this comment

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

There should be a currentPlaylists here I imagine?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants