10、播放器集成:ExoPlayer在TV上的配置、播放控制与字幕支持
说到Android TV上的视频播放,ExoPlayer几乎是绕不开的选择。我刚开始做TV开发时,也纠结过要不要直接用MediaPlayer,后来发现ExoPlayer在自定义能力、字幕支持和DASH流播放上,确实比系统自带的播放器灵活太多。说白了,Google官方都在推它,咱们跟着走准没错。
这一章,我会把ExoPlayer在TV上的配置、播放控制、字幕支持这些核心点,结合我实际踩过的坑,给你讲透。
10.1 为什么TV上推荐ExoPlayer?
你想想看,TV端的播放场景和手机端其实差别挺大的。手机用户可能看个短视频就划走了,但TV用户是坐在沙发上,盯着大屏幕看半小时甚至更久。这就对播放器的稳定性、字幕渲染、焦点控制提出了更高的要求。
ExoPlayer相比MediaPlayer,有几个明显的优势:
- 可定制性强:MediaPlayer是个黑盒子,出了问题你很难干预。ExoPlayer的组件都是可替换的,比如你可以自定义加载策略、渲染器、甚至网络栈。
- 字幕支持完善:ExoPlayer原生支持SRT、TTML、WebVTT等多种字幕格式,而且渲染效果在TV大屏上表现很好。
- 自适应码率:DASH和HLS流的支持非常成熟,TV端网络环境复杂,自适应切换能保证播放流畅。
- 生命周期友好:TV应用经常涉及焦点切换、息屏、休眠等场景,ExoPlayer的状态管理比MediaPlayer清晰得多。
核心结论:如果你在TV上做视频播放,ExoPlayer是首选。MediaPlayer只适合最简单的播放场景,但凡涉及字幕、自适应、自定义UI,ExoPlayer都能省你不少事。
10.2 ExoPlayer在TV上的基础配置
先说说怎么把ExoPlayer集成到TV项目里。其实步骤不复杂,但有几个细节容易忽略。
10.2.1 添加依赖
在build.gradle里加上ExoPlayer的依赖。我个人习惯用最新的稳定版,但要注意版本号之间的兼容性。
dependencies {
// ExoPlayer核心库
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
// DASH支持
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1'
// HLS支持
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
// UI组件(包括字幕渲染)
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
}
嗯,这里要注意一点:如果你只需要播放本地文件,其实exoplayer-core就够了。但TV应用通常要播在线流,所以DASH和HLS的依赖最好都加上,免得后面临时加依赖出问题。
10.2.2 创建播放器实例
ExoPlayer的创建方式比较灵活,我建议用Builder模式,这样方便配置各种参数。
// 创建播放器
ExoPlayer player = new ExoPlayer.Builder(context)
.setRenderersFactory(new DefaultRenderersFactory(context))
.setMediaSourceFactory(new DefaultMediaSourceFactory(context))
.setLoadControl(new DefaultLoadControl())
.build();
// 绑定到PlayerView
PlayerView playerView = findViewById(R.id.player_view);
playerView.setPlayer(player);
这里有个坑:TV上PlayerView的焦点管理。默认情况下,PlayerView会拦截焦点,导致遥控器无法控制其他UI元素。我遇到过这个问题,后来发现需要在布局里设置android:focusable="false",然后把焦点交给自定义的控制栏。
10.2.3 配置MediaSource
MediaSource是ExoPlayer播放的数据源。对于TV应用,最常见的场景是播放在线视频流。
// 创建DASH媒体源
MediaItem mediaItem = MediaItem.fromUri("https://example.com/manifest.mpd");
player.setMediaItem(mediaItem);
player.prepare();
player.play();
如果是本地文件,直接用Uri.fromFile()就行。但要注意,TV设备通常存储空间有限,不建议把大视频文件放在本地。
10.3 播放控制:TV遥控器的适配
TV上的播放控制和手机完全不同。手机靠触摸,TV靠遥控器。遥控器的按键事件需要映射到播放器的控制逻辑上。
10.3.1 按键映射
我一般会在Activity或Fragment里重写onKeyDown方法,把遥控器的按键转换成播放器的操作。
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
// 播放/暂停切换
if (player.isPlaying()) {
player.pause();
} else {
player.play();
}
return true;
case KeyEvent.KEYCODE_DPAD_LEFT:
// 快退10秒
player.seekTo(player.getCurrentPosition() - 10000);
return true;
case KeyEvent.KEYCODE_DPAD_RIGHT:
// 快进10秒
player.seekTo(player.getCurrentPosition() + 10000);
return true;
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
// 媒体键播放/暂停
if (player.isPlaying()) {
player.pause();
} else {
player.play();
}
return true;
default:
return super.onKeyDown(keyCode, event);
}
}
这里有个细节:快进快退的步长,我建议TV上设10秒,手机端可以设5秒。因为TV屏幕大,用户操作频率低,10秒的步长更符合使用习惯。
10.3.2 自定义控制栏
ExoPlayer自带的PlayerControlView在TV上表现一般,因为它的布局是为触摸设计的。我建议自己写一个控制栏,用Leanback的样式。
// 自定义控制栏布局
<LinearLayout
android:id="@+id/control_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:background="#80000000">
<ImageButton
android:id="@+id/btn_rewind"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_rewind"
android:focusable="true" />
<ImageButton
android:id="@+id/btn_play_pause"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/ic_play"
android:focusable="true" />
<ImageButton
android:id="@+id/btn_forward"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_forward"
android:focusable="true" />
<!-- 进度条 -->
<TextView
android:id="@+id/tv_current_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFF" />
<SeekBar
android:id="@+id/seek_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:focusable="true" />
<TextView
android:id="@+id/tv_total_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFF" />
</LinearLayout>
控制栏的显示和隐藏,我建议用Handler做延时隐藏。用户操作后显示3秒,然后自动消失。这样既不影响观看,又能及时响应。
10.4 字幕支持:TV大屏上的关键体验
字幕在TV上比手机重要得多。手机屏幕小,字幕可有可无;但TV大屏上,字幕是很多用户的刚需,尤其是外语内容或听力障碍用户。
10.4.1 字幕格式与加载
ExoPlayer支持的字幕格式很全,我列个表给你看:
| 格式 | 说明 | TV上的表现 |
|---|---|---|
| SRT | 最通用的字幕格式 | 渲染稳定,字体清晰 |
| WebVTT | HTML5标准字幕格式 | 支持样式,可定制性强 |
| TTML | MPEG-4标准字幕 | 适合DASH流,支持多语言 |
| Embedded | 视频内嵌字幕 | 依赖视频源,兼容性一般 |
加载字幕的方式有两种:一种是字幕文件单独提供,另一种是字幕嵌入在视频流里。我建议优先用单独的字幕文件,因为这样方便切换和更新。
// 加载外部字幕文件
MediaItem.SubtitleConfiguration subtitleConfig =
new MediaItem.SubtitleConfiguration.Builder(Uri.parse("https://example.com/subtitle.srt"))
.setMimeType(MimeTypes.APPLICATION_SUBRIP)
.setLanguage("zh")
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.build();
MediaItem mediaItem = new MediaItem.Builder()
.setUri("https://example.com/video.mp4")
.setSubtitleConfigurations(Collections.singletonList(subtitleConfig))
.build();
player.setMediaItem(mediaItem);
player.prepare();
10.4.2 字幕样式定制
TV大屏上,字幕的字体大小和颜色直接影响观看体验。ExoPlayer允许通过CaptionStyleCompat来定制字幕样式。
// 自定义字幕样式
CaptionStyleCompat captionStyle = new CaptionStyleCompat(
Color.WHITE, // 字体颜色
Color.TRANSPARENT, // 背景颜色(透明)
Color.BLACK, // 窗口颜色
CaptionStyleCompat.EDGE_TYPE_OUTLINE, // 边缘类型
Color.BLACK, // 边缘颜色
Typeface.DEFAULT_BOLD // 字体
);
// 应用到PlayerView
playerView.setCaptionStyle(captionStyle);
我曾经在项目里遇到过一个问题:字幕在TV上显示太小,用户根本看不清。后来发现是默认的字幕尺寸是按手机屏幕优化的。我建议TV上字幕的字体大小至少设到24sp以上,背景用半透明黑色,这样在任何画面上都能看清。
10.4.3 多字幕切换
TV应用经常需要支持多语言字幕。ExoPlayer的TrackSelector可以方便地切换字幕轨道。
// 获取可用的字幕轨道
TrackGroupArray trackGroups = player.getCurrentTrackGroups();
for (int i = 0; i < trackGroups.length; i++) {
TrackGroup group = trackGroups.get(i);
if (group.getFormat(0).sampleMimeType.startsWith("text/")) {
// 这是字幕轨道
// 切换到指定字幕
player.setTrackSelectionParameters(
player.getTrackSelectionParameters()
.buildUpon()
.setPreferredTextLanguage("en")
.build()
);
break;
}
}
这里要注意:切换字幕时,最好在UI上给用户一个反馈,比如显示一个Toast提示“字幕已切换到英文”。TV上用户看不到触摸反馈,视觉提示很重要。
10.5 知识体系总览
为了让你更直观地理解这一章的内容结构,我画了一张图:
10.6 避坑指南与最佳实践
做TV播放器开发,有几个坑我不得不提:
坑1:播放器生命周期管理
我曾经在项目里遇到过播放器在Activity销毁后还在后台播放的情况。原因是忘了在onStop里释放播放器。记住:player.release()一定要在onStop或onDestroy里调用,否则会内存泄漏。
坑2:字幕与视频不同步
有些视频流的字幕时间戳不准,导致字幕提前或延迟。我建议在加载字幕时,加一个偏移量调整功能,让用户可以通过遥控器微调字幕时间。
小技巧:预加载提升体验
TV用户不喜欢等待。我建议在用户选择视频后,立即调用player.prepare(),但先不调用player.play()。等用户点击播放时,视频已经缓冲好了,几乎秒开。
10.7 总结
ExoPlayer在TV上的集成,说白了就是三件事:配置好播放器、处理好遥控器控制、渲染好字幕。这三件事做好了,用户的播放体验就不会差。
我个人觉得,字幕这块是TV播放器最容易出彩的地方。很多TV应用的字幕做得粗糙,字体小、背景刺眼、切换卡顿。你只要把字幕样式调好,用户就会觉得这个应用很专业。
嗯,这一章的内容就到这里。下一章我们会聊更进阶的话题,但先把基础打牢,后面才能走得更稳。