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

AndroidX Media3 ExoPlayer自定义Compose控件的HLS字幕实现

在AndroidX Media3 ExoPlayer(useController=false+自定义Compose控件)下实现HLS字幕自动显示及切换

注意:此问题与其他类似问题不同,感谢您的帮助!

原实现代码

@Composable
fun MyPlayer() {
  val hlsUri = Uri.parse("https://some.website/video.m3u8")
  val context = LocalContext.current
  val player: ExoPlayer = remember {
    ExoPlayer.Builder(context)
        .build()
        .apply {
            setMediaItem(MediaItem.fromUri(hlsUri))
            prepare()
        }
    }

    AndroidView(
        factory = {
            PlayerView(context).apply {
                this.player = player
                useController = false // <- 因此setShowSubtitleButton(true)不再适用
                layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
            }
        }
    )

    // 下方添加自定义Jetpack Compose控件(播放、暂停、显示/隐藏字幕等)
    // ...
}

依赖配置

gradle/libs.versions.toml

# gradle/libs.versions.toml

androidXMedia = "1.1.0"
androidx-media-player = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "androidXMedia" }
androidx-media-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "androidXMedia" }
androidx-media-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "androidXMedia" }

app/build.gradle.kts

// app/build.gradle.kts

dependencies {
    // ...
    implementation(libs.androidx.media.player)
    implementation(libs.androidx.media.ui)
    implementation(libs.androidx.media.hls)
}

HLS文件内容

#EXTM3U
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",LANGUAGE="en",URI="https://.../playlist.m3u8",AUTOSELECT=YES,DEFAULT=NO
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",LANGUAGE="fr",URI="https://.../playlist.m3u8",AUTOSELECT=YES,DEFAULT=NO

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-medium",NAME="audio",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="https://.../playlist.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-high",NAME="audio",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="https://.../playlist.m3u8"

#EXT-X-STREAM-INF:SUBTITLES="subs",BANDWIDTH=2255226,AVERAGE-BANDWIDTH=1578000,RESOLUTION=1280x720,FRAME-RATE=24.000,CODECS="avc1.640020,mp4a.40.2",AUDIO="audio-high"
https://.../playlist.m3u8
#EXT-X-STREAM-INF:SUBTITLES="subs",BANDWIDTH=698963,AVERAGE-BANDWIDTH=537000,RESOLUTION=640x360,FRAME-RATE=24.000,CODECS="avc1.64001E,mp4a.40.2",AUDIO="audio-medium"
https://.../playlist.m3u8
#EXT-X-STREAM-INF:SUBTITLES="subs",BANDWIDTH=463242,AVERAGE-BANDWIDTH=332000,RESOLUTION=426x240,FRAME-RATE=24.000,CODECS="avc1.640015,mp4a.40.2",AUDIO="audio-medium"
https://.../playlist.m3u8
#EXT-X-STREAM-INF:SUBTITLES="subs",BANDWIDTH=5496152,AVERAGE-BANDWIDTH=4096000,RESOLUTION=1920x1080,FRAME-RATE=24.000,CODECS="avc1.640028,mp4a.40.2",AUDIO="audio-high"
https://.../playlist.m3u8
#EXT-X-STREAM-INF:SUBTITLES="subs",BANDWIDTH=1440054,AVERAGE-BANDWIDTH=965000,RESOLUTION=960x540,FRAME-RATE=24.000,CODECS="avc1.64001F,mp4a.40.2",AUDIO="audio-medium"
https://.../playlist.m3u8

解决方案

1. 自动触发字幕显示

你的HLS字幕标签已设置AUTOSELECT=YES,但ExoPlayer不会自动识别该标记,需手动在播放器准备完成后筛选并选中对应轨道:

val player: ExoPlayer = remember {
    ExoPlayer.Builder(context)
        .build()
        .apply {
            setMediaItem(MediaItem.fromUri(hlsUri))
            addListener(object : Player.Listener {
                override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
                    super.onMediaMetadataChanged(mediaMetadata)
                    // 播放器加载完成后自动选择字幕
                    autoSelectSubtitle(this@apply)
                }
            })
            prepare()
        }
}

// 自动筛选并选中带AUTOSELECT标记的字幕轨道
private fun autoSelectSubtitle(player: ExoPlayer) {
    for (groupIndex in 0 until player.currentTrackGroups.length) {
        val trackGroup = player.currentTrackGroups.get(groupIndex)
        for (trackIndex in 0 until trackGroup.length) {
            val format = trackGroup.getFormat(trackIndex)
            if (format.sampleMimeType?.startsWith(MimeTypes.TEXT_VTT) == true 
                && format.selectionFlags and C.SELECTION_FLAG_AUTOSELECT != 0) {
                player.trackSelectionParameters = player.trackSelectionParameters
                    .buildUpon()
                    .setTrackOverride(
                        TrackSelectionOverride(TrackGroupArray(trackGroup), trackIndex)
                    )
                    .build()
                return // 找到第一个符合条件的轨道后停止遍历
            }
        }
    }
}

2. 自定义字幕切换控件

在Compose中添加按钮和菜单,实现字幕的开启/关闭、语言切换功能:

// 缓存可用字幕轨道(格式+轨道索引)
val subtitleTracks = remember(player.currentTrackGroups) {
    mutableListOf<Pair<Format, Int>>().apply {
        for (groupIndex in 0 until player.currentTrackGroups.length) {
            val group = player.currentTrackGroups.get(groupIndex)
            for (trackIndex in 0 until group.length) {
                val format = group.getFormat(trackIndex)
                if (format.sampleMimeType?.startsWith(MimeTypes.TEXT) == true) {
                    add(format to trackIndex)
                }
            }
        }
    }
}

// 记录当前选中的字幕索引,-1代表关闭字幕
val selectedSubtitleIndex = remember { mutableStateOf(-1) }
val expanded = remember { mutableStateOf(false) }

// 字幕切换按钮
IconButton(onClick = { expanded.value = true }) {
    Icon(
        imageVector = if (selectedSubtitleIndex.value != -1) Icons.Filled.Subtitles else Icons.Outlined.Subtitles,
        contentDescription = "字幕设置",
        tint = Color.White
    )
}

// 字幕选择下拉菜单
DropdownMenu(
    expanded = expanded.value,
    onDismissRequest = { expanded.value = false }
) {
    subtitleTracks.forEachIndexed { index, (format, _) ->
        DropdownMenuItem(
            onClick = {
                selectedSubtitleIndex.value = index
                updateSubtrackSelection(player, format.trackGroupIndex, index)
                expanded.value = false
            }
        ) {
            Text(text = format.label ?: format.language ?: "字幕${index+1}")
        }
    }
    DropdownMenuItem(
        onClick = {
            selectedSubtitleIndex.value = -1
            updateSubtrackSelection(player, -1, -1)
            expanded.value = false
        }
    ) {
        Text(text = "关闭字幕")
    }
}

// 更新字幕轨道选择状态
private fun updateSubtrackSelection(player: ExoPlayer, groupIndex: Int, trackIndex: Int) {
    val paramsBuilder = player.trackSelectionParameters.buildUpon()
    if (groupIndex != -1 && trackIndex != -1) {
        val trackGroup = player.currentTrackGroups.get(groupIndex)
        paramsBuilder.setTrackOverride(TrackSelectionOverride(TrackGroupArray(trackGroup), trackIndex))
    } else {
        // 清除所有字幕轨道选择
        paramsBuilder.clearTrackOverrides(C.TRACK_TYPE_TEXT)
    }
    player.trackSelectionParameters = paramsBuilder.build()
}

3. 确保字幕渲染正常

即使关闭了默认控制器,PlayerView仍会自动渲染选中的字幕,无需额外配置,只要轨道选择逻辑正确,字幕就会显示在视频画面上。


内容的提问来源于stack exchange,提问作者Sébastien Dubois

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 20:14:56