Repository navigation
Conversation
|
@robwalch Hi, did you get a chance yet to review this? Appreciate it! 🙏 |
|
Hi @slhck, I understand the motivation and logic. The volume of code and where it's been added - stream-controller vs abr-controller - is something I'm still undecided on. I also haven't been able to verify that the change works as intended and that it would not completely prevent switching in certain cases. |
|
Added milestone so that you get a review within the next release cycle. |
18a2333 to
aa9060c
Compare
|
Thanks for the feedback, I understand. |
|
Noting similar PR #8087. Commented that I'd like us to primarily switch on segment boundaries (and specifically avoid switching when non-independent parts remain) while loading parts. While this change is more concerned with non-low-latency live buffer and playlist position, I'd prefer that the logic for when to change be simpler and unified. The issue with both PRs is that they still read the |
|
Thanks for the comment. I see your point that we read Would you consider a smaller version of the patch? If you'd rather handle this as part of your v1.8 work, that's fine. One could test it against the Das Erste stream ( |
That sounds better. See the comment I left #8087 (comment) regarding this work. |
| if ( | ||
| this.hls.manualLevel !== -1 || | ||
| !levels || |
There was a problem hiding this comment.
Manual selection is covered in nextLoadLevel.
| const currentDetails = levels[currentLevel]?.details; | ||
| const candidateDetails = levels[candidateLevel]?.details; | ||
|
|
||
| // do not switch up if either playlist is not live or has parts (low-latency) |
There was a problem hiding this comment.
| // do not switch up if either playlist is not live or has parts (low-latency) | |
| // switch up if either playlist is not live or has parts (low-latency) |
Do switch up right?
What we care about wrt to LL-HLS is if this.loadingParts is true. #8087 includes discussion of allowing switching on segment boundaries, and potentially on independent parts only.
| // helper to check if there is a fragment in the playlist that extends past the end of the buffer, within tolerance. | ||
| private hasFragmentAtBufferEnd( | ||
| details: LevelDetails, | ||
| bufferEnd: number, | ||
| ): boolean { | ||
| const fragment = this.getNextFragment(bufferEnd, details); | ||
| return !!( | ||
| fragment && fragment.end > bufferEnd + this.config.maxFragLookUpTolerance | ||
| ); | ||
| } |
There was a problem hiding this comment.
This method seems unnecessary or should be renamed to hasFragmentAt with time as the target param. It's called twice, both times with bufferInfo.end + this.config.maxFragLookUpTolerance on either level details.
| const currentReloadAt = currentDetails.requestScheduled; | ||
| const candidateReloadAt = candidateDetails?.requestScheduled ?? -1; | ||
| // Time until the candidate playlist's next scheduled refresh, in seconds. | ||
| const candidateReloadDelay = Math.max( | ||
| 0, | ||
| (candidateReloadAt - self.performance.now()) / 1000, | ||
| ); | ||
| // Retain half a target duration to request and append the candidate fragment. | ||
| const minBufferAfterCandidateReload = | ||
| currentDetails.levelTargetDuration / 2; | ||
| const canWaitForCandidate = | ||
| !!candidateDetails && | ||
| bufferInfo.len - | ||
| candidateReloadDelay + | ||
| this.config.maxFragLookUpTolerance >= | ||
| minBufferAfterCandidateReload; | ||
|
|
||
| const currentHasFragment = this.hasFragmentAtBufferEnd( | ||
| currentDetails, | ||
| bufferInfo.end, | ||
| ); | ||
|
|
||
| if (currentHasFragment) { | ||
| // Continue at the current level unless the buffer covers the candidate refresh. | ||
| return canWaitForCandidate ? candidateLevel : currentLevel; |
There was a problem hiding this comment.
This appears to recreate logic that does or should reside in abr-controller. It's nice that the requestScheduled delay is factored in, but why not have that as part of the calculation in getTimeToLoadFrag's playlistLoadSec estimation? Also consider that it is irrelevant if the fragment or part needed is present in the candidate details after #8107 is merged.
This PR will...
This change checks whether segments are available before committing to an automatic live upswitch near the playlist
edge, and defers the upswitch if the next segment is not yet (predicted to be) available.
If the higher rendition already contains the next segment (i.e., a fragment that extends the forward buffer), the switch continues normally (as currently the case).
If the higher rendition has no next segment, hls.js either waits for its scheduled playlist refresh or just continues at the current rendition (i.e., it defers the upswitch).
We assume that we want at least half a segment duration remaining after the refresh. So we check the time stamps when the refresh will occur, and calculate the reserve.
Example 1:
→ This would keep the current rendition and load its available next segment.
Example 2:
→ the switch is permitted, we wait for the candidate playlist refresh.
This only applies to live playlists near the edge. We do not change VoD behavior, manual level selection, downswitches,
LL-HLS part playlists, or live playback where enough segments are published ahead of time.
Why is this Pull Request needed?
We are investigating a streaming setup where the live playback sometimes stalls even though the estimated bandwidth
would be sufficient. The problem is that a low edge latency is the goal, yet playlist refreshes cannot always happen
in time.
Specifically, the setup uses
liveSyncDurationCount: 2. While we acknowledge that this is explicitly documented asprone to causing stalls, it still menas that a live stream can start with only one segment of playable buffer remaining.
The ABR would then select a higher rendition because the estimated bandwidth is indeed sufficient, even though that rendition's media playlist does not even contain the segment needed at the end of the current buffer.
The current
StreamControllerjust commits to this switch and enters theWAITING_LEVEL. Then, if the buffer isexhausted, the player emits a short
waitingevent, then hls.js reports abufferStalledError.Once the missing segment becomes available, everything is back to normal.
These
waitingevents would trigger user-visible stalls and warnings during stream monitoring.Reproduction
We reproduced this on current
masterwith the public Das Erste live master playlist:https://daserste-live.ard-mcdn.de/daserste/live/hls/de/master.m3u8.We added unit test coverage to create
LevelDetailsthat mimic what is going on in that live stream.Configuration:
When we ran the current
mastervs this PR against the stream, we could observe no stalls with this PR while they didoccur on
master.We also verified that even with the defensive logic of this PR, it allowed all sessions to reach 1080p within a short amount of time (less than 1 s after startup), so it really only defers what is needed to avoid stalls, not more.
Are there any points in the code the reviewer needs to double check?
There are some points:
We aren't quite sure if the target duration / 2 metric is the best choice. When we tried a target duration ⨉ 2 threshold, we found that in 40% of the test runs the quality wouldn't switch up at all. This would've been unacceptable.
As we're not so familiar with the code base, please check if
LevelDetails.requestScheduledis used correctly here.Is excluding LL-HLS part playlists done correctly?
Full disclosure: unit test coverage has been added with Codex + GPT-5.6 Sol (High). The unit tests were then reviewed and run by real humans™.
Resolves issues:
No issue number.
Test plan
Checklist
tests/unit/controller/stream-controller.tsunderlive upswitch availability)