13、视频播放器:SurfaceView与TextureView、硬解码适配、字幕支持
视频播放器,可以说是车载多媒体应用里最核心、也最折腾人的模块。我这些年做过的车载项目,几乎每个都在视频播放上踩过坑。今天咱们就把SurfaceView、TextureView、硬解码适配、字幕支持这几个硬骨头啃下来。
13.1 SurfaceView vs TextureView:选谁?
先说结论:车载场景下,我个人更倾向SurfaceView。为什么?咱们掰扯掰扯。
SurfaceView:老将出马
SurfaceView的工作原理,说白了就是在WMS(窗口管理器)里单独开一个图层。这个图层独立于View层级,有自己的Surface。视频数据直接渲染到这个Surface上,不走主线程的绘制流程。
好处很明显:
- 性能好:视频渲染不占用主线程,不会卡UI
- 内存低:不需要额外的缓冲区拷贝
- 硬解码友好:很多硬件解码器直接输出到Surface
但坑也不少:
- 不能做动画:平移、缩放、旋转?SurfaceView不支持View级别的变换
- 层级问题:它永远在普通View的下面,想在上面盖个按钮?得用Z-order
- 黑屏闪烁:创建和销毁时会有短暂黑屏,我曾在项目里被这个坑惨了
TextureView:后起之秀
TextureView是Android 4.0引入的。它本质上是一个普通的View,但内部持有一个Texture。视频数据渲染到这个Texture上,然后作为View的一部分参与绘制。
优势:
- 支持变换:可以随便做动画、旋转、缩放
- 层级正常:和其他View一样,可以盖在上面
- 无黑屏:创建销毁平滑
代价:
- 性能开销:每次绘制都要走View的绘制流程,GPU负担重
- 内存占用高:需要额外的FBO(帧缓冲对象)
- 硬解码兼容性:部分老芯片的硬解码器不支持TextureView
我的建议:
- 车载主界面、全屏播放 → SurfaceView
- 需要做画中画、动画效果 → TextureView
- 低端芯片(比如瑞芯微某些型号)→ 老老实实用SurfaceView
13.2 硬解码适配:别让CPU烧起来
车载设备散热差,软解码分分钟让CPU飙到80度。硬解码是必须的。但硬解码的坑,比SurfaceView和TextureView的选择还多。
MediaCodec:硬解码的核心API
Android的硬解码通过MediaCodec实现。基本流程:
// 创建解码器
MediaCodec codec = MediaCodec.createDecoderByType(mimeType);
// 配置解码器
MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);
codec.configure(format, surface, null, 0);
// 启动
codec.start();
// 循环处理输入输出
while (!isEOS) {
int inputIndex = codec.dequeueInputBuffer(timeoutUs);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
// 填充数据...
codec.queueInputBuffer(inputIndex, offset, size, pts, flags);
}
int outputIndex = codec.dequeueOutputBuffer(bufferInfo, timeoutUs);
if (outputIndex >= 0) {
codec.releaseOutputBuffer(outputIndex, true);
}
}
嗯,看着简单吧?但实际项目里,我遇到过这些破事:
常见硬解码问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 解码器创建失败 | 芯片不支持该格式 | 降级到软解码,或提示用户 |
| 花屏/绿屏 | 关键帧丢失或PTS错乱 | 检查码流完整性,重新封装 |
| 解码延迟高 | 输入缓冲区积压 | 控制入队速度,不要一股脑塞 |
| 内存泄漏 | 没有及时release | 用try-finally保证释放 |
避坑指南:我曾经在一个项目里,发现某款高通芯片的硬解码器对H.264的profile支持有限。high profile的视频能解,但main profile反而花屏。后来查了芯片文档才知道,这芯片的硬解码器只支持high profile的特定级别。所以,一定要在代码里做解码器能力检测。
解码器能力检测
MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
if (!codecInfo.isEncoder()) {
String[] types = codecInfo.getSupportedTypes();
for (String type : types) {
if (type.equalsIgnoreCase(mimeType)) {
MediaCodecInfo.CodecCapabilities caps =
codecInfo.getCapabilitiesForType(type);
// 检查分辨率、帧率、profile等
int maxWidth = caps.getVideoCapabilities().getSupportedWidths().getUpper();
int maxHeight = caps.getVideoCapabilities().getSupportedHeights().getUpper();
// 做适配...
}
}
}
}
13.3 字幕支持:别让用户看哑剧
车载场景下,字幕不是可选项,是刚需。尤其是后排乘客看视频,没字幕谁受得了?
字幕格式与解析
常见的字幕格式:
- SRT:最简单,纯文本,时间戳+内容
- ASS/SSA:支持样式、特效,解析复杂
- VTT:WebVTT,HTML5视频常用
- 内嵌字幕:封装在MKV、MP4里的字幕流
我个人建议,车载场景优先支持SRT和VTT。ASS虽然炫酷,但解析开销大,低端芯片扛不住。
字幕渲染:别用TextView硬上
很多人图省事,直接用TextView显示字幕。但TextView不支持多行居中、不支持字体描边、不支持滚动。你想想看,字幕在视频底部,背景是动态画面,没有描边根本看不清。
正确的做法:
public class SubtitleView extends View {
private Paint textPaint;
private Paint strokePaint;
private String subtitleText;
private float textSize;
public SubtitleView(Context context) {
super(context);
init();
}
private void init() {
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(textSize);
textPaint.setTextAlign(Paint.Align.CENTER);
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setColor(Color.BLACK);
strokePaint.setStrokeWidth(4);
strokePaint.setTextSize(textSize);
strokePaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
if (subtitleText == null) return;
int x = getWidth() / 2;
int y = getHeight() - 50;
// 先画描边
canvas.drawText(subtitleText, x, y, strokePaint);
// 再画文字
canvas.drawText(subtitleText, x, y, textPaint);
}
}
嗯,这里要注意:描边宽度不要太大,4像素就够了。太宽了文字会糊在一起。
字幕同步:PTS是关键
字幕显示的时间点,必须和视频的PTS(显示时间戳)对齐。不能简单用System.currentTimeMillis(),因为视频可能暂停、快进、快退。
正确的做法:
// 在播放器回调中更新字幕
player.addOnVideoSizeChangedListener((videoWidth, videoHeight) -> {
// 获取当前播放位置
long currentPosition = player.getCurrentPosition();
// 查找当前时间对应的字幕
SubtitleItem item = subtitleManager.getSubtitleAt(currentPosition);
if (item != null) {
subtitleView.setText(item.text);
} else {
subtitleView.clear();
}
});
13.4 整体架构:一张图说清楚
说了这么多,咱们用一张图把整个视频播放器的架构串起来:
这张图把整个流程串起来了。数据源进来,经过解封装,分出视频流、音频流和字幕流。视频走解码→渲染,音频走解码→输出,字幕单独解析后叠加到视频上。中间那个虚线框是PTS同步控制,保证音画字幕三者对齐。
13.5 性能优化:别让播放器成为电老虎
车载设备电池有限,播放器优化不好,分分钟把电耗光。我总结几个关键点:
- 解码器复用:不要频繁创建销毁MediaCodec,复用能省不少资源
- 缓冲区大小:输入缓冲区不要设太大,我一般设10个就够了,多了反而增加延迟
- 帧率控制:如果屏幕是60Hz,解码帧率控制在30fps就够了,别解码60fps然后丢帧,浪费电
- 休眠策略:播放暂停时,释放解码器资源,不要空转
我的经验:曾经有个项目,播放器在后台运行时CPU占用率一直居高不下。查了半天,发现是MediaCodec的dequeueOutputBuffer在没数据时还在轮询。后来加了超时机制,CPU占用从30%降到了2%。
好了,关于视频播放器的这几个核心点,咱们就聊到这儿。SurfaceView和TextureView的选择、硬解码的适配、字幕的支持,每一个都是实战中绕不开的坎。希望这些经验能帮你少走些弯路。