Skip to content

fix: player crash when switching audio language (#89) - #129

Merged
ProdigyV21 merged 1 commit into
mainfrom
fix/player-track-selection-crash
Apr 5, 2026
Merged

fix: player crash when switching audio language (#89)#129
ProdigyV21 merged 1 commit into
mainfrom
fix/player-track-selection-crash

Conversation

@ProdigyV21

Copy link
Copy Markdown
Owner

Summary

Closes #89.

Fixes the player crash users reported when switching audio language from the track menu during playback. Also removes a second, related bug where switching audio from the D-pad handler path ran duplicated unsafe code.

Root cause

Two audio-track selection code paths in PlayerScreen.kt (the D-pad menu handler at ~line 1485 and the onSelectAudio callback at ~line 2075) built a TrackSelectionOverride(group, trackIndex) after only validating:

  1. track.groupIndex < trackGroups.size
  2. group.type == TRACK_TYPE_AUDIO
  3. track.trackIndex < group.lengthmissing

There was also no try/catch around exoPlayer.trackSelectionParameters = params.build().

AudioTrackInfo captures groupIndex / trackIndex at the moment onTracksChanged fires. Between that moment and the user actually picking a track from the menu (several seconds later, especially if they first opened the subtitles menu, browsed, then switched to audio), the player may have re-prepared due to:

  • adaptive stream switch
  • user switching source from the source picker
  • MediaItem rebuild when a new external subtitle is selected
  • automatic recovery from a transient network error

After any of those events the track groups layout can change, leaving the cached trackIndex out of bounds for the new group. Media3's TrackSelectionOverride constructor throws IllegalArgumentException and the crash propagates up to the Composable, tearing down the player.

Fix

Extracted the audio-track selection logic into a single private helper applyAudioTrackSelection(exoPlayer, track, audioTracks): Int? with proper defense:

private fun applyAudioTrackSelection(
    exoPlayer: ExoPlayer,
    track: AudioTrackInfo,
    audioTracks: List<AudioTrackInfo>
): Int? {
    return try {
        val params = exoPlayer.trackSelectionParameters.buildUpon()
            .clearOverridesOfType(C.TRACK_TYPE_AUDIO)
            .setPreferredAudioLanguage(track.language)

        val trackGroups = exoPlayer.currentTracks.groups
        val groupInRange = track.groupIndex in trackGroups.indices
        if (groupInRange) {
            val group = trackGroups[track.groupIndex]
            val isAudioGroup = group.type == C.TRACK_TYPE_AUDIO
            val trackInRange = track.trackIndex in 0 until group.length
            if (isAudioGroup && trackInRange) {
                params.setOverrideForType(
                    TrackSelectionOverride(group.mediaTrackGroup, track.trackIndex)
                )
            }
            // If stale, still apply setPreferredAudioLanguage so Media3 picks the
            // closest matching track on its own rather than crashing or leaving
            // the user on the wrong language.
        }

        exoPlayer.trackSelectionParameters = params.build()
        audioTracks.indexOfFirst { ... }.takeIf { it >= 0 } ?: track.index
    } catch (e: IllegalArgumentException) { ... null }
      catch (e: IllegalStateException) { ... null }
      catch (e: Exception) { ... null }
}

Key defensive additions:

  1. Full try/catch around the whole block — the outer params.build() assignment could also throw on some devices when the player is in an invalid state.
  2. clearOverridesOfType(C.TRACK_TYPE_AUDIO) first — stale overrides from prior selections no longer pin the player to a track index that may not exist in the new group layout.
  3. trackIndex in 0 until group.length bounds check — this is the specific validation that was missing and caused the crash.
  4. Graceful fallback — if the group/track validation fails, the setPreferredAudioLanguage hint still applies, so Media3 picks the closest matching track on its own rather than leaving the user on the wrong language.
  5. Returns Int? — caller uses ?.let { selectedAudioIndex = it } so when the selection fails we keep the previous index rather than jumping to an incorrect one.

Both audio-selection call sites now shrink from ~20 lines to 3.

Not touched in this PR

The subtitle-selection paths at lines 934 and 983 already validate groupIndex in groups.indices and have been stable — they're left alone to keep the diff focused on the reported crash. They could be migrated to a similar helper in a follow-up if desired.

Risk

Low. The helper is strictly more defensive than the inlined code it replaces. In the normal case (fresh tracks, user picks immediately), behavior is identical. In the edge case (stale indices after re-prepare), the old code crashed and the new code either silently picks the correct track via language hint, or keeps the previous selection. Either is strictly better than a crash.

Reported by users: the player crashed (or showed an error) as soon as
they picked a different audio language from the track menu during
playback. Two audio-track selection call sites in PlayerScreen.kt built
a `TrackSelectionOverride(group, trackIndex)` after only validating
`track.groupIndex < trackGroups.size` and `group.type == TRACK_TYPE_AUDIO`
\u2014 but not `trackIndex < group.length`. There was also no try/catch
around `exoPlayer.trackSelectionParameters = params.build()`.

Root cause: `AudioTrackInfo` captures `groupIndex` / `trackIndex` at the
moment `onTracksChanged` fires. Between that moment and the user actually
picking a track from the menu (which can be several seconds later), the
player may have re-prepared due to:
- adaptive stream switch
- user switching source from the source picker
- MediaItem rebuild when a new external subtitle is selected
- automatic recovery from a transient network error

After any of those events the track groups layout can change, leaving
the cached `trackIndex` out of bounds for the new group. Media3's
`TrackSelectionOverride` constructor throws `IllegalArgumentException`
and the crash propagates up to the Composable, tearing down the player.

Changes:
- Extract the audio-track selection logic into a single private helper
  `applyAudioTrackSelection(exoPlayer, track, audioTracks)` that:
  - Wraps the whole operation in try/catch for IllegalArgumentException,
    IllegalStateException, and generic Exception.
  - Calls `clearOverridesOfType(C.TRACK_TYPE_AUDIO)` before setting the
    new override so stale overrides from prior selections don't pin the
    player to a no-longer-present track.
  - Validates `groupIndex in groups.indices`, `group.type == TRACK_TYPE_AUDIO`,
    AND `trackIndex in 0 until group.length` before calling
    `TrackSelectionOverride(...)`.
  - If the group/track validation fails but the override call site
    doesn't throw, still applies the `setPreferredAudioLanguage` hint so
    Media3 picks the closest matching track on its own rather than
    leaving the user on the wrong language.
  - Returns the resolved `selectedAudioIndex` or `null` on failure, so
    the caller can keep the previous selection rather than jumping to
    an incorrect one.
- Replace both audio-selection call sites (the D-pad menu handler at
  ~line 1485 and the `onSelectAudio` callback at ~line 2075) with calls
  to the helper. Both sites shrank from ~20 lines to 3 lines.

The subtitle-selection paths in the same file (lines 934, 983) already
validate `groupIndex in groups.indices` and have been stable, so they
are not touched in this PR to keep the diff focused on the reported
crash. They can be migrated to a similar helper in a follow-up if
wanted.

Closes #89
@ProdigyV21
ProdigyV21 merged commit 604ffa7 into main Apr 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Player crashes on change of language

1 participant