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

How can I load a specific chunk/frag/segment OR all segments? #4745

Closed
danday74 opened this issue Jun 17, 2022 · 3 comments
Closed

How can I load a specific chunk/frag/segment OR all segments? #4745

danday74 opened this issue Jun 17, 2022 · 3 comments

Comments

@danday74
Copy link

danday74 commented Jun 17, 2022

What do you want to do with Hls.js?

SHORT QUESTION (for those in a hurry)

With hls.js how do I load all or one segment/chunk/frag from the current level for a video?

QUESTION WITH BACKGROUND

I switch currentLevel with hls.js (via FlowPlayer) like so:

private setBestQuality() {
  this.player.hls.currentLevel = this.hlsLevelsCount - 1
}

private setWorstQuality() {
  this.player.hls.currentLevel = 0
}

This is standard stuff and does an (almost) immediate level switch. The reason I am doing this is because I noticed that when scrubbing/seeking, the player is much more responsive when using a low quality level. When scrubbing/seeking starts I switch to the lowest available quality level and when it ends I then switch back to the highest quality level. My boss loves this.

One issue though, when I switch to low quality, lets assume the video currentTime is 30/60 seconds and the video has 5 chunks/segment. Hls.js will buffer the current chunk/segment (e.g. segment 3) and the next segments (e.g. segments 4 & 5) but it does not buffer earlier segments (e.g. segments 1 & 2) in the video. As a result, when scrubbing backwards, there's some jerking whilst unbuffered segments load on demand.

This is all great behaviour for normal playback, however the app I am building relies heavily on seeking.

Is there any way to force hls.js to buffer/load all chunks so as to avoid the jerking during segment loading? These segments/chunks are small and the videos are short so the time taken to load/buffer all the chunks should not be too much of a concern. Any help much appreciated. TIA

NB: I can see this behaviour via hooking into the FRAG_BUFFERED event and logging which chunks have just been buffered.

What have you tried so far?

Tried to manually load all chunks (without success):

this.player.hls.stopLoad()
this.player.hls.trigger(Hls.Events.BUFFER_FLUSHING, { startOffset: 0, endOffset: this.player.duration })
this.player.hls.startLoad(0)

But here startLoad does not start loading all chunks from 0secs onwards.

I even tried caching all the frags in an array when they first load and then reloading them later like so, but no success either, it does not seem to load any frags (note this is not an ideal solution since it would only work if chunks had previously been loaded, which may not be the case on a level switch):

this.player.hls.trigger(Hls.Events.BUFFER_FLUSHING, { startOffset: 0, endOffset: this.player.duration })
this.frags.forEach((frag, i) => {
  console.log('trying to load frag', i + 1)
  this.player.hls.trigger(Hls.Events.FRAG_LOADING, { frag })
})

Possibly, an alternative solution I've thought about is to write a custom fragment loader - but I have no idea how to do this and there are no examples - this latter approach seems quite complicated.

A workaround for this would be to use 2 players, one at the high level and one at the low level. And only show the relevant one. Using this approach all segments would be buffered. But I am trying to avoid this.

NOTE: docs say:

hls.currentLevel
set: Trigger an immediate quality level switch to new quality level. This will abort the current fragment request if any, flush the whole buffer, and fetch fragment matching with current position and requested quality level.

What I actually want it to do is:

Trigger an immediate quality level switch to new quality level. This will abort the current fragment request if any, flush the whole buffer, (I would prefer if it did not do this), and fetch ALL fragments (ideally in an intelligent order, e.g. current frag first) for requested quality level.

If I was able to load a chunk programatically I could manually load unbuffered chunks.

Alternatively, if hls.js allowed me to load all unbuffered chunks on a level switch, that would also suffice - either by means of an option or a command.

This seems like a reasonable use case and if this is not possible could this question be turned into a feature request. TIA.

@danday74 danday74 added Needs Triage If there is a suspected stream issue, apply this label to triage if it is something we should fix. Question labels Jun 17, 2022
@danday74
Copy link
Author

@danday74 danday74 changed the title How can I load a specific chunk/segment/frag from a specific level? How can I load a specific chunk/frag/segment OR all segments? Jun 21, 2022
@danday74
Copy link
Author

danday74 commented Jun 23, 2022

STACKBLITZ TO DEMO THE PROBLEM

https://stackblitz.com/edit/angular-ivy-pmu8py - EDIT

https://angular-ivy-pmu8py.stackblitz.io/ - FULL SCREEN WITHOUT CODE

Basically this proves that scrubbing backwards on lowest quality when all frags are loaded is as smooth as a baby's bottom (but NOT when all frags are NOT loaded as a result of dynamically switching quality level)

@robwalch
Copy link
Collaborator

robwalch commented Jan 6, 2023

I even tried caching all the frags in an array when they first load and then reloading them later like so, but no success either, it does not seem to load any frags (note this is not an ideal solution since it would only work if chunks had previously been loaded, which may not be the case on a level switch):

this.player.hls.trigger(Hls.Events.BUFFER_FLUSHING, { startOffset: 0, endOffset: this.player.duration })
this.frags.forEach((frag, i) => {
 console.log('trying to load frag', i + 1)
 this.player.hls.trigger(Hls.Events.FRAG_LOADING, { frag })
})

Very interesting!

You can get the list of fragments from any level event in event.details.fragments.

The stream-controller(s) are only designed to load one segment at a time (stream-controller state is based on playlist fragment loading/parsing/appending state). Emitting the FRAG_LOADING event is only part of the process. When a stream-controller is in the IDLE state, it calls loadFragment(frag, levelDetails, targetBufferTime) to load the next fragment needed to fill the buffer. This method and the streamController are protected/private, so calling it externally is not supported.

If going the undocumented/unsupported route is OK, you could tell the player what to load a fragment on FRAG_BUFFERED as long as the player is IDLE and not already loading what it determines as needed by the buffer configuration:

This is a POC that shows you could force any arbitrary fragment to be loaded as long as another one is not already loading. I'll leave it to you to figure out which segment to load next. You can find what has been loaded already by looking at each fragment's stats, or (also private) hls.streamController.fragmentTracker.fragments to get a hash of loaded fragments.

hls.on('hlsFragBuffered', (name, event) => {
    // You could also verify that `hls.streamController === 'idle'` here
    const notLoadingAnotherFragment = event.frag === hls.streamController.fragCurrent;
    if (notLoadingAnotherFragment) {
        const levelDetails = hls.streamController.levels[hls.streamController.levelLastLoaded]?.details;
        if (levelDetails) {
            const frag = levelDetails.fragments[0]; // if stats show this is loaded grab the next one (or maybe start closest to fragCurrent and work backwards)
            hls.streamController.loadFragment(frag, levelDetails, frag.start);
        }
    }
});

Be sure to adjust to buffer threshold and back buffer options so that segments are not flushed. Note that browsers have a quota on how much can be appended before appending will error. If your timeline is too long you are going to hit this error and could prevent the forward buffer from advancing as needed.

This could be a nice way to solve #4553 without us adding wrapping target buffer time when loop is enabled on a media element. I don't however see a compelling way to support filling the back buffer automatically with quota limits being what they are.

@robwalch robwalch added answered and removed Needs Triage If there is a suspected stream issue, apply this label to triage if it is something we should fix. labels Jan 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants