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
相关产品推荐
相关产品推荐

