13、视频播放器:SurfaceView与TextureView、硬解码适配、字幕支持

视频播放器,可以说是车载多媒体应用里最核心、也最折腾人的模块。我这些年做过的车载项目,几乎每个都在视频播放上踩过坑。今天咱们就把SurfaceView、TextureView、硬解码适配、字幕支持这几个硬骨头啃下来。

13.1 SurfaceView vs TextureView:选谁?

先说结论:车载场景下,我个人更倾向SurfaceView。为什么?咱们掰扯掰扯。

SurfaceView:老将出马

SurfaceView的工作原理,说白了就是在WMS(窗口管理器)里单独开一个图层。这个图层独立于View层级,有自己的Surface。视频数据直接渲染到这个Surface上,不走主线程的绘制流程。

好处很明显:

  • 性能好:视频渲染不占用主线程,不会卡UI
  • 内存低:不需要额外的缓冲区拷贝
  • 硬解码友好:很多硬件解码器直接输出到Surface

但坑也不少:

  • 不能做动画:平移、缩放、旋转?SurfaceView不支持View级别的变换
  • 层级问题:它永远在普通View的下面,想在上面盖个按钮?得用Z-order
  • 黑屏闪烁:创建和销毁时会有短暂黑屏,我曾在项目里被这个坑惨了
注意:SurfaceView的创建和销毁是异步的。你调了surfaceCreated回调,不代表Surface已经可用。我曾经在surfaceCreated里直接开始播放,结果画面黑了一秒多。

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();
    }
});
注意:字幕的显示和隐藏要平滑。不要突然闪现,也不要延迟消失。我习惯在字幕结束前500ms开始淡出,效果会好很多。

13.4 整体架构:一张图说清楚

说了这么多,咱们用一张图把整个视频播放器的架构串起来:

车载视频播放器架构图 数据源(本地/网络/流媒体) 解封装(MediaExtractor) 视频解码(硬解/软解) 音频解码(硬解/软解) 视频渲染(SurfaceView/TextureView) 音频输出(AudioTrack) 字幕解析与渲染 PTS同步控制

这张图把整个流程串起来了。数据源进来,经过解封装,分出视频流、音频流和字幕流。视频走解码→渲染,音频走解码→输出,字幕单独解析后叠加到视频上。中间那个虚线框是PTS同步控制,保证音画字幕三者对齐。

13.5 性能优化:别让播放器成为电老虎

车载设备电池有限,播放器优化不好,分分钟把电耗光。我总结几个关键点:

  • 解码器复用:不要频繁创建销毁MediaCodec,复用能省不少资源
  • 缓冲区大小:输入缓冲区不要设太大,我一般设10个就够了,多了反而增加延迟
  • 帧率控制:如果屏幕是60Hz,解码帧率控制在30fps就够了,别解码60fps然后丢帧,浪费电
  • 休眠策略:播放暂停时,释放解码器资源,不要空转

我的经验:曾经有个项目,播放器在后台运行时CPU占用率一直居高不下。查了半天,发现是MediaCodec的dequeueOutputBuffer在没数据时还在轮询。后来加了超时机制,CPU占用从30%降到了2%。

好了,关于视频播放器的这几个核心点,咱们就聊到这儿。SurfaceView和TextureView的选择、硬解码的适配、字幕的支持,每一个都是实战中绕不开的坎。希望这些经验能帮你少走些弯路。

公众号:蓝海资料掘金营,微信deep3321