Skip to content

NPE in HlsSampleStreamWrapper #2495

Closed
Closed

Description

@alexsullivan-substack

Version

Media3 1.6.1

More version details

No response

Devices that reproduce the issue

Seems to be happening across a wide variety of devices and android versions (Samsung, Redmi, Oppo, Sony, Motorozola devices on Android versions 10,11,12,13, and 15)

Devices that do not reproduce the issue

Unknown

Reproducible in the demo app?

No

Reproduction steps

Unfortunately I don't have repro steps

Expected result

The app doesn't crash

Actual result

The app crashes with the following stack trace:

java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.media3.exoplayer.hls.n.c()' on a null object reference
    at androidx.media3.exoplayer.hls.HlsSampleStreamWrapper.mapSampleQueuesToMatchTrackGroups(HlsSampleStreamWrapper.java:1370)
    at androidx.media3.exoplayer.hls.HlsSampleStreamWrapper.maybeFinishPrepare(HlsSampleStreamWrapper.java:1344)
    at androidx.media3.exoplayer.hls.HlsSampleStreamWrapper.$r8$lambda$UK4TNmzu4rF7l558xzg8C7fYdDI(HlsSampleStreamWrapper.java:0)
    at androidx.media3.exoplayer.hls.HlsSampleStreamWrapper$$ExternalSyntheticLambda1.run(HlsSampleStreamWrapper:0)
    at android.os.Handler.handleCallback(Handler.java:883)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loop(Looper.java:214)
    at android.os.HandlerThread.run(HandlerThread.java:67)

Media

Seems to only be happening for HLS streams but I can't point to any specific media.

Bug Report

Activity

  1. self-assigned this
    on Jun 2, 2025
  2. icbaker commented on Jun 2, 2025

    @icbaker
    Collaborator

    Are you able to retrace the error, to deobfuscate the androidx.media3.exoplayer.hls.n.c() symbol?

    HlsSampleStreamWrapper.java:1370 is this at version 1.6.1:

    for (HlsSampleStream sampleStream : hlsSampleStreams) {
    sampleStream.bindSampleQueue();
    }

    So the error suggests that the this.hlsSampleStreams list contains a null element.

    hlsSampleStreams is only added to in one place, and only non-null entries are added:

    for (@Nullable SampleStream stream : streams) {
    if (stream != null) {
    hlsSampleStreams.add((HlsSampleStream) stream);
    }
    }

    So looking just at the code - this NPE looks like it should be impossible...

  3. alexsullivan-substack commented on Jun 2, 2025

    @alexsullivan-substack
    Author

    From manually looking at the mapping file (not sure why our crash reporting tool isn't de-obfuscating), I can confirm that hls.n is exoplayer.hls.HlsSampleStream and c is bindSampleQueue...

  4. alexsullivan-substack commented on Jun 2, 2025

    @alexsullivan-substack
    Author

    One thing I want to note is it seems like this only started appearing after we started relying on the media3 preload manager and the media3 downloading support. I don't see why either of those things would have an impact but I wanted to throw it out there.

    Looking at the code I also don't see how this is possible, but it's happening a large amount - it's not like only one crash popping up; it's our top crash ATM.

  5. icbaker commented on Jun 3, 2025

    @icbaker
    Collaborator

    One thing I want to note is it seems like this only started appearing after we started relying on the media3 preload manager and the media3 downloading support. I don't see why either of those things would have an impact but I wanted to throw it out there.

    Following this clue - can you give a bit of info about how you set up the preload manager? Does it share the same looper as ExoPlayer.getPlaybackLooper?

  6. assigned and unassigned on Jun 3, 2025
  7. wongcain commented on Jun 3, 2025

    @wongcain

    I work with @alexsullivan-substack and set up the PreloadManager. I'm using it with as little configuration as was needed to get it working. It's created via:

    
        DefaultPreloadManager.Builder(appContext) {
            DefaultPreloadManager.Status(
              DefaultPreloadManager.Status.STAGE_LOADED_FOR_DURATION_MS,
              PRELOAD_DURATION_MS,
            )
          }
          .setMediaSourceFactory(mediaSourceFactory)
          .build()
    

    I am also using the DefaultPreloadManager.Builder to configure ExoPlayer instances like so:

        val player =
          DefaultPreloadManager.Builder(appContext) {
              DefaultPreloadManager.Status(
                DefaultPreloadManager.Status.STAGE_LOADED_FOR_DURATION_MS,
                PRELOAD_DURATION_MS,
              )
            }
            .setMediaSourceFactory(mediaSourceFactory)
            .buildExoPlayer(
              ExoPlayer.Builder(appContext)
                .setSeekBackIncrementMs(PlayerService.SKIP_BACK_AMOUNT_MS.toLong())
                .setSeekForwardIncrementMs(PlayerService.SKIP_FORWARD_AMOUNT_MS.toLong())
                .setAudioAttributes(DEFAULT_AUDIO_ATTRIBUTES, autoAudioFocus)
            )
    
  8. tianyif commented on Jun 3, 2025

    @tianyif
    Contributor

    @wongcain, quick check - do the DefaultPreloadManager and the ExoPlayer are built from the same DefaultPreloadManager.Builder instance?

  9. 2 remaining items

  10. wongcain commented on Jun 3, 2025

    @wongcain

    @tianyif Thank you! I will get a new app version with this change out asap and report back.

  11. wongcain commented on Jun 9, 2025

    @wongcain

    @tianyif I am not longer seeing the NPE exception, but now am seeing this one as our top crasher.

    java.lang.IllegalStateException: null
        at androidx.media3.common.util.Assertions.checkStateNotNull(Contexts.java:117)
        at androidx.media3.exoplayer.trackselection.TrackSelector.getBandwidthMeter(TrackSelector.java:253)
        at androidx.media3.exoplayer.trackselection.DefaultTrackSelector.selectTracks(DefaultTrackSelector.java:2631)
        at androidx.media3.exoplayer.trackselection.MappingTrackSelector.selectTracks(MappingTrackSelector.java:429)
        at androidx.media3.exoplayer.source.preload.PreloadMediaSource$PreloadMediaPeriodCallback.lambda$onPrepared$0(PreloadMediaSource.java:480)
        at t1.PreloadMediaSource$PreloadMediaPeriodCallback$$ExternalSyntheticLambda0.run(PreloadMediaSource:0)
        at android.os.Handler.handleCallback(Handler.java:958)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loopOnce(Looper.java:205)
        at android.os.Looper.loop(Looper.java:294)
        at android.os.HandlerThread.run(HandlerThread.java:67)
    

    Any tips on how to resolve?

  12. tianyif commented on Jun 9, 2025

    @tianyif
    Contributor

    @wongcain,

    Did you set a custom TrackSelector.Factory via DefaultPreloadManager.Builder ?

    public Builder setTrackSelectorFactory(TrackSelector.Factory trackSelectorFactory) {

    If yes, could you paste what's the custom TrackSelector.Factory like?

    Or, when did you release the DefaultPreloadManager?

  13. wongcain commented on Jun 9, 2025

    @wongcain

    @tianyif I am not calling setTrackSelectorFactory(). I am releasing the DefaultPreloadManager only upon leaving the app (via onCleared() of a ViewModel that is scoped to the Activity in a single-Activity application.

    One thing that I just noticed, however, is that I am removing media from the PreloadManager immediately before releasing the player that is using it. I suspect the order of these operation may be to blame. What do you think?

  14. tianyif commented on Jun 9, 2025

    @tianyif
    Contributor

    @wongcain,

    The exception happens when PreloadMediaSource tries to select tracks after the track selector is released.

    If setTrackSelectorFactory is not called, the DefaultPreloadManager and ExoPlayer will use DefaultTrackSelector but in different instances (the library guarantees that), which means that even the player releases its TrackSelector, the one in DefaultPreloadManager (in PreloadMediaSource) shouldn't be released. At this point, I don't think the order of removing media from preload manager and releasing the player matters.

    One possible reason is that the period triggers onPrepared after the DefaultPreloadManager is released. In theory, this shouldn't happen as we will release all the sources and its holding PreloadMediaPeriod before releasing DefaultPreloadManager (where its TrackSelector is released), all this operations are already on preload looper. But things can still go wrong if we failed to prevent onPrepared to be triggered after content period is released.

    Did you observe this issue only from specific stream (eg. HLS)?

  15. wongcain commented on Jun 9, 2025

    @wongcain

    @tianyif All our streams are HLS. I have not been able to replicate this error myself. I am only seeing it in our crash logs. It doesn't appear to be specific to any particular stream.

  16. wongcain commented on Jun 9, 2025

    @wongcain

    @tianyif I was able to repro the crash for the first time. It occurred about 1/2 second after staring to play a video. When I returned to the app and replayed the same video there was no error. I will keep an eye out for it happening again, to see if there are any patterns that I can see when it happens.

  17. wongcain commented on Jun 9, 2025

    @wongcain

    Fwiw, the stack trace was slightly different:

    java.lang.IllegalStateException: null
        at androidx.media3.common.util.Assertions.checkStateNotNull(RoundRect.kt:117)
        at androidx.media3.exoplayer.trackselection.TrackSelector.getBandwidthMeter(TrackSelector.java:253)
        at androidx.media3.exoplayer.trackselection.DefaultTrackSelector.selectTracks(DefaultTrackSelector.java:2631)
        at androidx.media3.exoplayer.trackselection.MappingTrackSelector.selectTracks(MappingTrackSelector.java:429)
        at androidx.media3.exoplayer.source.preload.PreloadMediaSource$PreloadMediaPeriodCallback.lambda$onPrepared$0(PreloadMediaSource.java:480)
        at t1.PreloadMediaSource$PreloadMediaPeriodCallback$$ExternalSyntheticLambda0.run(PreloadMediaSource:0)
        at android.os.Handler.handleCallback(Handler.java:991)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loopOnce(Looper.java:232)
        at android.os.Looper.loop(Looper.java:317)
        at android.os.HandlerThread.run(HandlerThread.java:85)
    
  18. tianyif commented on Jun 10, 2025

    @tianyif
    Contributor

    @wongcain,

    It occurred about 1/2 second after staring to play a video.

    So when you reproduced this issue, there hasn't been a chance that DefaultPreloadManager.release is called? Interesting, as for now the only suspicious direction that I felt is around releasing the preload manager. But I'll go ahead to verify the case in my mind first and fix it if it is valid.

  19. wongcain commented on Jun 10, 2025

    @wongcain

    @tianyif I found the issue! You were right that the DefaultPreloadManager.release is called. I was confused due to the scope of an injected dependency. I figured out a path to reproduce the crash reliably, and so can implement a fix. Thank you very much for your help with it.

  20. tianyif commented on Jun 10, 2025

    @tianyif
    Contributor

    @wongcain,

    Thanks for the update! Could you please explain the step of reproducing and where you call DefaultPreloadManager.release that introducing this issue? Just need to see if there is anything from the library should fix, if it's some corner case we also missed.

  21. wongcain commented on Jun 10, 2025

    @wongcain

    @tianyif Sure. The problem is related to my architecture and lifecycle. I have a PlayerManager class that is injected into a ViewModel. The VM gets instances of ExoPlayer and DefaultPreloadManager from the PlayerManger. The VM was also releasing the DefaultPreloadManager in onCleared which in itself was fine. The problem is that PlayerManager is injected as a singleton, and it is also injected into a Service. The crash happens if I exit the UI of the application while the service is running and return to the UI while the service continues to run. In this case, the VM would receive the same instance of PlayerManager which would provide the same instance of DefaultPreloadManager that it provided before... which has already been released. So then as soon as I tried playing anything, it crashes.

  22. tianyif commented on Jun 12, 2025

    @tianyif
    Contributor

    Also manually linking the commit 4811418 here, which signals the thread inconsistency early and informatively (addressing the initial problem).

  23. locked and limited conversation to collaborators on Aug 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions