You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ExoPlayer 2.18.0切换轨道后isTrackSelected始终返回默认轨道问题

ExoPlayer 2.18.0+ 轨道切换后选中状态识别异常修复

问题现象

参考ExoPlayer官方示例实现音轨、字幕轨、视频清晰度轨切换逻辑后,切换操作本身可正常生效,但调用trackGroup.isTrackSelected(i)获取选中轨道时,始终返回默认轨道为选中状态,无法正确识别手动切换后的选中项。

官方给出的轨道切换示例代码如下:

player.setTrackSelectionParameters(
    player.getTrackSelectionParameters()
        .buildUpon()
        .setOverrideForType(
            new TrackSelectionOverride(
                audioTrackGroup.getMediaTrackGroup(),
                /* trackIndex= */ 0))
        .build());

2.18.0版本前可正常工作的旧版字幕切换代码如下:

private fun setSubTrack(
    trackSelector: DefaultTrackSelector?,
    subLanguage: String
) {
    trackSelector?.setParameters(trackSelector.buildUponParameters()
        .setRendererDisabled(C.TRACK_TYPE_VIDEO, false)
        .setPreferredTextLanguage(subLanguage))
}

问题根因

代码存在3个核心问题,导致选中状态识别异常:

  • 轨道索引硬编码错误:setAudioTrack、setSubTrack方法中构造TrackSelectionOverride时,第二个参数(轨道在组内的索引)写死为0,无论用户选了哪个轨道,实际都强制选中对应组的第一个轨道,和记录的selectedPosition完全不匹配。
  • 轨道组索引映射错误:遍历轨道时把所有同类型TrackGroup下的轨道平铺到同一个列表里,但选中后直接用selectedPosition -1取audioTrackGroups/textTrackGroups里的组,没有记录每个列表项对应的「轨道组+组内索引」映射,很容易取错组,导致override设置到错误的轨道组上。
  • 状态读取时机+遍历逻辑错误:currentTracks是播放器异步更新的,设置完trackSelectionParameters后不会立刻刷新;同时遍历同类型轨道组时,只要遇到选中的轨道就覆盖selectedXxxPosition变量,遇到多轨道组场景会被后续组的状态覆盖,拿到错误的位置。

修复方案

第一步:新增数据结构记录轨道映射关系

不要用纯String列表存轨道选项,每个选项要绑定对应的TrackGroup和组内索引,避免索引错位:

// 新增轨道项数据类,存每个选项对应的组和索引
private data class TrackOption(
    val group: Tracks.Group?,
    val trackIndex: Int, // -1表示禁用,-2表示自动(仅视频)
    val displayName: String
)

第二步:修正轨道选择弹窗的遍历逻辑

遍历轨道时逐个构造TrackOption,同时正确判断选中状态,不要硬编码索引。以音轨选择为例,改造后的核心逻辑:

fun selectAudioTrack(context: Context, player: ExoPlayer?) {
    player ?: return
    val tracks = player.currentTracks
    val trackOptions = mutableListOf<TrackOption>()
    // 添加禁用选项
    trackOptions.add(TrackOption(null, -1, "Disable"))

    // 遍历所有音轨组
    for (trackGroup in tracks.groups) {
        if (trackGroup.type != C.TRACK_TYPE_AUDIO) continue
        for (i in 0 until trackGroup.length) {
            if (!trackGroup.isTrackSupported(i)) continue
            val format = trackGroup.getTrackFormat(i)
            // 生成显示名称
            val label = format.label?.let { " - $it" } ?: ""
            val lang = Locale(format.language ?: "").displayLanguage
            val displayName = if (lang.contains("und") || lang.contains("null")) {
                "Audio track #${i + 1}$label"
            } else {
                "$lang$label"
            }
            trackOptions.add(TrackOption(trackGroup, i, displayName))
        }
    }

    // 找到当前选中项的位置
    var selectedPos = 0
    val isAudioDisabled = player.trackSelectionParameters.isTrackTypeDisabled(C.TRACK_TYPE_AUDIO)
    if (!isAudioDisabled) {
        // 遍历找当前实际选中的轨道
        for ((index, option) in trackOptions.withIndex()) {
            if (option.group == null) continue
            if (option.group.isTrackSelected(option.trackIndex)) {
                selectedPos = index
                break
            }
        }
    }

    val optionNames = trackOptions.map { it.displayName }.toTypedArray<CharSequence>()
    var tempSelected = selectedPos
    MaterialAlertDialogBuilder(context)
        .setTitle("Audio Track")
        .setNeutralButton(R.string.cancel) { _, _ -> }
        .setPositiveButton(R.string.ok) { _, _ ->
            val selectedOption = trackOptions[tempSelected]
            setAudioTrack(player, selectedOption)
        }
        .setSingleChoiceItems(optionNames, selectedPos) { _, pos ->
            tempSelected = pos
        }
        .show()
}

第三步:修正轨道设置方法,去掉硬编码索引

对应的setAudioTrack方法改造,直接用传入的TrackOption设置参数,不要写死trackIndex=0:

private fun setAudioTrack(player: ExoPlayer, option: TrackOption) {
    val paramsBuilder = player.trackSelectionParameters.buildUpon()
    if (option.trackIndex == -1) {
        // 禁用音轨
        paramsBuilder.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, true)
        paramsBuilder.clearOverridesOfType(C.TRACK_TYPE_AUDIO)
    } else {
        paramsBuilder.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, false)
        // 清除同类型旧的override,避免多个override冲突
        paramsBuilder.clearOverridesOfType(C.TRACK_TYPE_AUDIO)
        // 用option里存的正确索引设置override
        paramsBuilder.setOverrideForType(
            TrackSelectionOverride(option.group!!.mediaTrackGroup, option.trackIndex)
        )
    }
    player.trackSelectionParameters = paramsBuilder.build()
}

第四步:补充状态刷新时机

如果需要在切换后立刻刷新UI/选中状态,不要设置完参数马上读currentTracks,注册Player.Listener监听onTracksChanged回调,在回调触发后再读取选中状态,此时播放器已经完成轨道切换,isTrackSelected会返回正确结果:

player.addListener(object : Player.Listener {
    override fun onTracksChanged(tracks: Tracks) {
        // 在这里更新记录的selectedXxxPosition变量,刷新UI状态
    }
})

字幕轨、视频轨的修复逻辑和音轨完全一致:不要硬编码索引、用绑定了组和索引的实体类存选项、设置前清除同类型旧的override、在onTracksChanged回调里读选中状态即可。视频轨的Auto选项对应TrackOption的trackIndex=-2,设置时清除所有视频类型的override,让播放器自适应选择即可,不要用setMaxVideoSizeSd()这种限制清晰度的接口。


内容的提问来源于stack exchange,提问作者Mubashir P A

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 05:39:18