Exoplayer HLS Adaptive Streaming selecting track variant per network bandwidth

789 views Asked by At

I am using Exoplayer in my Android app for streaming hls audio, I am trying to play adaptive stream using my master HLS playlist which contains various variant of the same audio track but in different bitrate as per suitable available network bandwidth.

But what i have seen, the Exoplayer starts playing with the initial variant in the master playlist, and then immediately moves to the highest available bitrate variant of audio, every time. It doesn't uses any of the other bitrate even if I have changed my network to 2G, 3G or any other lower bandwidth, it doesn't adapts to lower bitrate in any case and follows the above pattern. It will keep buffering for very long, but never use any lower track variant as per the bandwidth.

Here's the code of my Exoplayer implementation -

private void initialisePlayer(){
        if(exoPlayer == null) {
            AdaptiveTrackSelection.Factory videoTrackSelectionFactory =  new AdaptiveTrackSelection.Factory();
            TrackSelector trackSelector = new DefaultTrackSelector(this, videoTrackSelectionFactory);
            LoadControl loadControl = new DefaultLoadControl();
            exoPlayer = new ExoPlayer.Builder(getApplicationContext())
                    .setTrackSelector(trackSelector).setLoadControl(loadControl).build();
            exoPlayer.addListener(listener);
            exoPlayer.setAudioAttributes(audioAttributes, /* handleAudioFocus= */ true);
            exoPlayer.setHandleAudioBecomingNoisy(true);
        }
    }

   private void playAudio(){

    DefaultBandwidthMeter bandwidthMeter =  DefaultBandwidthMeter.getSingletonInstance(this);
    DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this).setTransferListener(bandwidthMeter);

    HlsMediaSource audioSource = HlsMediaSource.Factory(dataSourceFactory)
                    .setLoadErrorHandlingPolicy(new CustomPolicy())
                    .createMediaSource(MediaItem.fromUri("my-hls-master-url"));

    exoPlayer.setMediaSource(audioSource);
    exoPlayer.prepare();
    exoPlayer.setPlayWhenReady(true);

  
   }

I am using -

implementation 'androidx.media3:media3-exoplayer:1.1.1'
implementation "androidx.media3:media3-exoplayer-hls:1.1.1"

I have my master playlist as like -

#EXT-X-VERSION:4

# Audio Renditions

#EXT-X-STREAM-INF:BANDWIDTH=54000,CODECS="mp4a.40.5"
songs54/audio.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=84000,CODECS="mp4a.40.5"
songs84/audio.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=126000,CODECS="mp4a.40.2"
songs126/audio.m3u8


#EXT-X-STREAM-INF:BANDWIDTH=160000,CODECS="mp4a.40.5"
songs160/audio.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=320000,CODECS="mp4a.40.2"
songs320/audio.m3u8

I have been trying to fix this from last 2 days, any help will be really appreciated.

1

There are 1 answers

12
VonC On BEST ANSWER

To understand more why your ExoPlayer is not adapting to the network conditions as you expect, you could start with including the bandwidth meter in the track selector. That helps ExoPlayer to make adaptive decisions.

AdaptiveTrackSelection.Factory videoTrackSelectionFactory =  new AdaptiveTrackSelection.Factory(bandwidthMeter);

Although the DefaultLoadControl should be sufficient in most cases, you might want to set custom values for buffer size, buffer duration, etc., to fine-tune the adaptive behavior.

You would need to update the initialisePlayer and playAudio methods with the bandwidth meter included in the track selection:

private void initialisePlayer(){
    TrackSelector trackSelector = new DefaultTrackSelector(this);
    LoadControl loadControl = new DefaultLoadControl();
    exoPlayer = new ExoPlayer.Builder(this)
            .setTrackSelector(trackSelector)
            .setLoadControl(loadControl)
            .build();
    exoPlayer.addListener(listener);
    exoPlayer.addAnalyticsListener(new EventLogger(trackSelector));
    exoPlayer.setAudioAttributes(audioAttributes, /* handleAudioFocus= */ true);
    exoPlayer.setHandleAudioBecomingNoisy(true);
}

private void playAudio(){
    DefaultBandwidthMeter bandwidthMeter =  DefaultBandwidthMeter.getSingletonInstance(this);
    DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this)
            .setTransferListener(bandwidthMeter);
    
    HlsMediaSource audioSource = new HlsMediaSource.Factory(dataSourceFactory)
                .setLoadErrorHandlingPolicy(new CustomPolicy())
                .createMediaSource(MediaItem.fromUri("my-hls-master-url"));

    exoPlayer.setMediaSource(audioSource);
    exoPlayer.prepare();
    exoPlayer.setPlayWhenReady(true);
}

The DefaultBandwidthMeter should provide you with bandwidth estimates. You could log these to see if they change when you switch networks. If the bandwidth estimates do not change, that might be the issue.
Even though you can't inject the BandwidthMeter into the AdaptiveTrackSelection.Factory, you can still add an event listener to it to log changes in estimated bandwidth.

bandwidthMeter.addEventListener(new Handler(Looper.getMainLooper()), new BandwidthMeter.EventListener() {
    @Override
    public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
    Log.d("BANDWIDTH_SAMPLE", "bitrate = " + bitrate);
    }
});

I have added the bandwidthMeter.addEventListener and have seen that bitrate logged is always returning a higher number like - 3200000 or 3099619 or 2929804 and even when I change to 2G network the bitrate is something between 117245 and 165935.
But exoplayer is still not switching the audio variants.

Your bandwidth estimate on a 2G network is showing values like 117245 and 165935 so... ExoPlayer thinks the bandwidth is sufficient to play the highest bitrate available in your master playlist, which has a max bitrate of 320000 according to what you posted.

Test if you can manually set the track to confirm whether the issue is with ExoPlayer's adaptive mechanism or something else.

MappingTrackSelector.MappedTrackInfo mappedTrackInfo = ((DefaultTrackSelector) trackSelector).getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
    int rendererIndex = 0; // For audio. Change it as per your needs
    int trackIndexToBeUsed = 1; // Use 0, 1, 2... based on available tracks
    DefaultTrackSelector.SelectionOverride override = new DefaultTrackSelector.SelectionOverride(trackIndexToBeUsed, 0);
    DefaultTrackSelector.ParametersBuilder parametersBuilder = trackSelector.buildUponParameters();
    parametersBuilder.setSelectionOverride(rendererIndex, mappedTrackInfo.getTrackGroups(rendererIndex), override);
    trackSelector.setParameters(parametersBuilder);
}

Add the above code snippet just after preparing the player with exoPlayer.prepare(); to manually set the track.


I have used the codes MappingTrackSelector.MappedTrackInfo .... trackSelector.setParameters(parametersBuilder); right after exoplayer.prepare(); but still couldn't find any difference.

Exoplayer still uses the first audio variant in the initial load and then immediately move to the highest variant audio and never changes the audio variant irrespective of the bandwidth.

I can see bitrate being logged by bandwidthmeter as - 990000 in 2G network for multiple times initially, and then it changes slowly and subsequently to lower bitrates like 39000 to 189000 in the same network.

Sometimes, issues with track selection can be related to when exactly you are calling certain methods. Make sure you are calling the manual track selection code after the exoPlayer.prepare() call has completed. You might want to place the track selection code in a Player.Listener callback when the player state changes to Player.STATE_READY.

exoPlayer.addListener(new Player.Listener() {
    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
        if (playbackState == Player.STATE_READY) {
            // Manual track selection code here
        }
    }
});

Although ExoPlayer should handle this itself, for debugging purposes, you could explicitly set some parameters for adaptation, such as the minimum duration of media that needs to be buffered for playback to start or continue. ExoPlayer allows you to set such thresholds through DefaultLoadControl.


Now I can manually select the track, but it selects only the lower variant of audio track now, even in 5G Network.

If your manual track selection is successful but only results in the player using the lower bitrate track even on a high-speed network like 5G, this suggests that the track selection override is taking precedence over ExoPlayer's own adaptive mechanisms. When you set an explicit track selection override, ExoPlayer will stick with that choice and will not adapt based on network conditions.

So, remove the manual override for adaptation and try a conditional manual override: Implement logic to manually switch tracks based on certain conditions, such as if the bandwidth is below a particular threshold. In this way, you could have a mix of manual and adaptive behavior.

bandwidthMeter.addEventListener(new Handler(Looper.getMainLooper()), new BandwidthMeter.EventListener() {
    @Override
    public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
        if (bitrate > SOME_THRESHOLD) {
            // Revert to adaptive track selection
            DefaultTrackSelector.ParametersBuilder parametersBuilder = trackSelector.buildUponParameters();
            parametersBuilder.clearSelectionOverrides(/* rendererIndex= */ 0);
            trackSelector.setParameters(parametersBuilder);
        }
    }
});

Again, this is for testing: while it is possible to mix manual and adaptive track selection, doing so can make the behavior of your player harder to predict and debug.


Whenever the bitrate is low with my manual track selection the lowest bitrate is selected and when the bitrate is above my threshold then the adaptive behaviour selects only the highest bitrate, but the bitrate in between the two is not selected.

The issue is adaptive track selection is still not working properly and I have seen sometimes the bitrate is 102773336, with internet speed having 32 Mbps. The bitrate returned in onBandwidthSample is also wrong what I believe.

Also, if the adaptive track selection doesn't work then only way what I am thinking is to do all track selection manually based on the bitrate from onBandwidthSample method in BandwidthMeter.EventListener. But the wrong bitrate returned from the method is also an another issue in this.

As a last resort, if ExoPlayer's adaptive algorithm is not meeting your needs and you are also not getting reliable bandwidth estimates, you may indeed have to take over complete control of track selection.

In such a case, you can write a logic that maps bandwidth estimates to specific tracks. However, make sure to apply some form of smoothing or averaging to the bandwidth estimates to avoid rapid track switching, which can be disruptive to the user.

bandwidthMeter.addEventListener(new Handler(Looper.getMainLooper()), new BandwidthMeter.EventListener() {
    @Override
    public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
        // Your own logic to select a track based on the current bitrate
        int trackIndexToBeUsed = mapBitrateToTrack(bitrate);  // Implement this function based on your requirements
        DefaultTrackSelector.SelectionOverride override = new DefaultTrackSelector.SelectionOverride(trackIndexToBeUsed, 0);
        DefaultTrackSelector.ParametersBuilder parametersBuilder = trackSelector.buildUponParameters();
        parametersBuilder.setSelectionOverride(0, trackGroups, override);
        trackSelector.setParameters(parametersBuilder);
    }
});

As an alternative, you could try and use Android's network capabilities: The ConnectivityManager class in Android can provide general information about the type and speed of the current network connection. This information is less granular than a bandwidth estimate but can still be useful for choosing between high, medium, and low-quality streams.

ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkCapabilities nc = cm.getNetworkCapabilities(cm.getActiveNetwork());
int downSpeed = nc.getLinkDownstreamBandwidthKbps();
int upSpeed = nc.getLinkUpstreamBandwidthKbps();

That or use a third-party network measurement library, like square/okhttp.


You could also implement your own bandwidth estimation logic.
One approach is to download a small file at the beginning of the application and measure the download speed. Be cautious with this approach as it introduces latency and could vary based on server speeds, concurrent network operations, or other factors.