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
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1feddea
fix: Only check for bad seeks after seeking and an append
brandonocasey Sep 8, 2021
96d5e9f
Apply suggestions from code review
brandonocasey Sep 14, 2021
b071028
Merge branch 'main' into fix/use-seeking
brandonocasey Sep 15, 2021
c6144ad
Merge branch 'main' into fix/use-seeking
brandonocasey Sep 15, 2021
ff350fa
fix: more llhls fixes
brandonocasey Sep 15, 2021
9e74eae
do the same for forward duration
brandonocasey Sep 15, 2021
c42b1e1
do not switch to the same media twice
brandonocasey Sep 16, 2021
e5fd656
bad part guess and reset loader rather than resync
brandonocasey Sep 17, 2021
a3b9f09
update video.js
brandonocasey Sep 17, 2021
1704056
small segment choice fixes
brandonocasey Sep 17, 2021
cf340b0
independent and timeAheadOf fixes
brandonocasey Sep 17, 2021
33052bb
import ranges once
brandonocasey Sep 17, 2021
b40a28c
less time for llhls to skip bad seeks
brandonocasey Sep 17, 2021
5fd0ff3
add/update tests
brandonocasey Sep 20, 2021
21463d4
add more tests
brandonocasey Sep 21, 2021
5387192
Merge remote-tracking branch 'origin/main' into fix/more-llhls-fixes
brandonocasey Sep 22, 2021
c7765ba
Merge branch 'main' into fix/more-llhls-fixes
brandonocasey Sep 23, 2021
2927cd8
Merge branch 'main' into fix/more-llhls-fixes
brandonocasey Oct 18, 2021
9b2395b
add helper for preload segment duration
brandonocasey Oct 18, 2021
124fcc9
segmentInfoString tests
brandonocasey Oct 18, 2021
bd6adce
reset for live and resync for vod
brandonocasey Oct 18, 2021
64d513f
add test
brandonocasey Oct 18, 2021
fe2c2a6
code review
brandonocasey Oct 19, 2021
feabe07
move isBuffered and forwardBuffer to shouldSwitch
brandonocasey Oct 19, 2021
42fd68c
Update src/master-playlist-controller.js
brandonocasey Oct 20, 2021
db2da0f
Update src/master-playlist-controller.js
brandonocasey Oct 20, 2021
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
20 changes: 13 additions & 7 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ const sumLoaderStat = function(stat) {
};
const shouldSwitchToMedia = function({
currentPlaylist,
isBuffered,
buffered,
currentTime,
nextPlaylist,
forwardBuffer,
bufferLowWaterLine,
bufferHighWaterLine,
duration,
Expand All @@ -74,11 +74,16 @@ const shouldSwitchToMedia = function({
return false;
}

// determine if current time is in a buffered range.
const isBuffered = Boolean(Ranges.findRange(buffered, currentTime).length);

// If the playlist is live, then we want to not take low water line into account.
// This is because in LIVE, the player plays 3 segments from the end of the
// 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) {
// We don't really want to switch at the start of playback for llhls live streams
// before we have actually started. It almost doubles are time to first playback.
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.

return false;
Expand All @@ -87,6 +92,7 @@ const shouldSwitchToMedia = function({
return true;
}

const forwardBuffer = Ranges.timeAheadOf(buffered, currentTime);
const maxBufferLowWaterLine = experimentalBufferBasedABR ?
Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE;

Expand Down Expand Up @@ -739,18 +745,16 @@ export class MasterPlaylistController extends videojs.EventTarget {
shouldSwitchToMedia_(nextPlaylist) {
const currentPlaylist = this.masterPlaylistLoader_.media() ||
this.masterPlaylistLoader_.pendingMedia_;
const buffered = this.tech_.buffered();
const currentTime = this.tech_.currentTime();
const forwardBuffer = Ranges.timeAheadOf(buffered, currentTime);
const bufferLowWaterLine = this.bufferLowWaterLine();
const bufferHighWaterLine = this.bufferHighWaterLine();
const isBuffered = Boolean(Ranges.findRange(buffered, currentTime).length);
const buffered = this.tech_.buffered();

return shouldSwitchToMedia({
isBuffered,
buffered,
currentTime,
currentPlaylist,
nextPlaylist,
forwardBuffer,
bufferLowWaterLine,
bufferHighWaterLine,
duration: this.duration(),
Expand Down Expand Up @@ -1441,6 +1445,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
onSyncInfoUpdate_() {
let audioSeekable;

// If we have two source buffers and only one is created. The seekable range will be incorrect
// we should wait until all source buffers are created.
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

return;
}
Expand Down
4 changes: 2 additions & 2 deletions src/playback-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ export default class PlaybackWatcher {

// 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 :
const minAppendedDuration = media.partTargetDuration ? media.partTargetDuration :
(media.targetDuration - Ranges.TIME_FUDGE_FACTOR) * 2;

// verify that at least two segment durations have been
Expand All @@ -372,7 +372,7 @@ export default class PlaybackWatcher {

// if we are less than two video/audio segment durations or one part
// duration behind we haven't appended enough to call this a bad seek.
if (timeAhead < durations) {
if (timeAhead < minAppendedDuration) {
return false;
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,12 @@ export default class SegmentLoader extends videojs.EventTarget {
// we must reset/resync the segment loader when we switch renditions and
// the segment loader is already synced to the previous rendition

// for live streams reset, for vod resync
// on playlist changes we want it to be possible to fetch
// at the buffer for vod but not for live. So we use resetLoader
// for live and resyncLoader for vod. We want this because
// if a playlist uses independent and non-independent segments/parts the
// buffer may not accurately reflect the next segment that we should try
// downloading.
if (!newPlaylist.endList) {
this.resetLoader();
} else {
Expand Down
37 changes: 27 additions & 10 deletions test/loader-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -845,10 +845,19 @@ export const LoaderCommonFactory = ({

loader.load();
loader.mediaIndex = 0;
let resyncCalled = false;
let resetCalled = false;
const origReset = loader.resetLoader;
const origResync = loader.resyncLoader;

loader.resetLoader = function() {
resetCalled = true;
return origReset.call(loader);
};

loader.resyncLoader = function() {
resyncCalled = true;
return origResync.call(loader);
};

const newPlaylist = playlistWithDuration(50, {
Expand All @@ -861,6 +870,7 @@ export const LoaderCommonFactory = ({
loader.playlist(newPlaylist);

assert.true(resetCalled, 'reset was called');
assert.true(resyncCalled, 'resync was called');

return Promise.resolve();
});
Expand All @@ -877,9 +887,18 @@ export const LoaderCommonFactory = ({
loader.load();
loader.mediaIndex = 0;
let resyncCalled = false;
let resetCalled = false;
const origReset = loader.resetLoader;
const origResync = loader.resyncLoader;

loader.resetLoader = function() {
resetCalled = true;
return origReset.call(loader);
};

loader.resyncLoader = function() {
resyncCalled = true;
return origResync.call(loader);
};

const newPlaylist = playlistWithDuration(50, {
Expand All @@ -892,6 +911,7 @@ export const LoaderCommonFactory = ({
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.

assert.false(resetCalled, 'reset was not called');

return Promise.resolve();
});
Expand Down Expand Up @@ -1342,26 +1362,23 @@ export const LoaderCommonFactory = ({
const playlist = playlistWithDuration(50, {llhls: true});

loader.hasPlayed_ = () => true;
loader.currentTime_ = () => 46;
loader.syncPoint_ = null;

loader.playlist(playlist);
loader.load();
// in a real scenario this probably won't happen
// but it makes which segment/part is chosen easier.
loader.mediaIndex = 4;
loader.partIndex = 1;

// force segmentIndex 4 and part 2 to be choosen
loader.currentTime_ = () => 46;
// make the previous part indepenent so we go back to it
playlist.segments[4].parts[1].independent = true;
const segmentInfo = loader.chooseNextRequest_();

assert.equal(segmentInfo.partIndex, 1, 'previous part');
assert.equal(segmentInfo.partIndex, 1, 'still chooses partIndex 1');
assert.equal(segmentInfo.mediaIndex, 4, 'same segment');

// in a real scenario this probably won't happen
// but it makes which segment/part is chosen easier.
loader.mediaIndex = 3;
loader.partIndex = 4;
// force segmentIndex 4 and part 0 to be choosen
loader.currentTime_ = () => 42;
// make the previous part independent
playlist.segments[3].parts[4].independent = true;
const segmentInfo2 = loader.chooseNextRequest_();

Expand Down
8 changes: 4 additions & 4 deletions test/master-playlist-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6085,23 +6085,23 @@ QUnit.test('false without nextPlaylist', function(assert) {
this.env.log.warn.callCount = 0;
});

QUnit.test('false if llhls playlist and video not buffered', function(assert) {
QUnit.test('false if llhls playlist and no buffered', function(assert) {
const mpc = this.masterPlaylistController;

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');
assert.notOk(mpc.shouldSwitchToMedia_(nextPlaylist), 'should not switch when nothing is buffered');
});

QUnit.test('true if llhls playlist and video buffered', function(assert) {
QUnit.test('true if llhls playlist and we have buffered', function(assert) {
const mpc = this.masterPlaylistController;

mpc.tech_.buffered = () => videojs.createTimeRange([[0, 10]]);
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');
assert.ok(mpc.shouldSwitchToMedia_(nextPlaylist), 'should switch if buffered');
});

QUnit.module('MasterPlaylistController blacklistCurrentPlaylist', sharedHooks);
Expand Down