5、视频渲染入门:使用MediaPlayer+SurfaceView播放视频、使用MediaPlayer+TextureView播放视频

视频渲染,说白了就是把视频帧数据画到屏幕上。很多新手一开始会困惑:明明MediaPlayer已经加载了视频,为什么还要搞个SurfaceView或TextureView?

我刚开始做播放器时也踩过这个坑。当时以为MediaPlayer自带渲染能力,结果发现它只是个"数据搬运工",真正负责绘制的,是Surface。嗯,这里要注意——MediaPlayer需要一块"画布"来输出画面,而SurfaceView和TextureView,就是提供这块画布的两个主要方式。

5.1 为什么需要Surface?

MediaPlayer内部解码出来的视频帧,是YUV或RGB原始数据。这些数据不能直接显示到屏幕上,必须通过Surface来"提交"给图形系统。Surface可以理解成一个窗口的"背面",MediaPlayer往背面画,系统负责翻转到正面显示。

Android提供了两种常用的Surface载体:

  • SurfaceView:自带一个独立的Surface,有独立的窗口层级
  • TextureView:将Surface内容转换为纹理,可以像普通View一样做动画、缩放

核心区别一句话:SurfaceView性能好但不能做变换,TextureView灵活但性能稍弱。

5.2 使用MediaPlayer + SurfaceView

SurfaceView的使用方式很直接。你只需要在布局里放一个SurfaceView,等它创建好Surface后,把Surface交给MediaPlayer即可。

我个人习惯用SurfaceHolder.Callback来监听Surface的创建和销毁:

public class VideoPlayerActivity extends AppCompatActivity implements SurfaceHolder.Callback {

    private MediaPlayer mediaPlayer;
    private SurfaceView surfaceView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_player);

        surfaceView = findViewById(R.id.surfaceView);
        surfaceView.getHolder().addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // Surface已创建,可以开始播放
        mediaPlayer = new MediaPlayer();
        try {
            mediaPlayer.setDataSource("https://example.com/sample.mp4");
            mediaPlayer.setDisplay(holder);  // 关键:绑定Surface
            mediaPlayer.prepareAsync();
            mediaPlayer.setOnPreparedListener(mp -> mp.start());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // 当Surface尺寸或格式变化时触发
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface销毁时,记得释放MediaPlayer
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }
}

避坑指南:我曾经在surfaceDestroyed里只调了mediaPlayer.stop(),没调release(),结果切到后台再回来时,MediaPlayer状态乱了。记住:Surface销毁后,MediaPlayer持有的Surface引用就失效了,必须重建。

布局文件也很简单:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

5.3 使用MediaPlayer + TextureView

TextureView的使用稍微复杂一点,因为它需要你手动处理SurfaceTexture的回调。但换来的是你可以对视频画面做各种变换——比如旋转、缩放、甚至用Camera做AR效果。

我记得有个项目需要在视频上叠加水印,用SurfaceView死活搞不定,换成TextureView后,直接通过setTransform()就解决了。

public class TexturePlayerActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener {

    private MediaPlayer mediaPlayer;
    private TextureView textureView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_texture_player);

        textureView = findViewById(R.id.textureView);
        textureView.setSurfaceTextureListener(this);
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
        // SurfaceTexture可用,创建Surface并绑定MediaPlayer
        Surface surface = new Surface(surfaceTexture);
        mediaPlayer = new MediaPlayer();
        try {
            mediaPlayer.setDataSource("https://example.com/sample.mp4");
            mediaPlayer.setSurface(surface);  // 注意:这里用的是setSurface,不是setDisplay
            mediaPlayer.prepareAsync();
            mediaPlayer.setOnPreparedListener(mp -> mp.start());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
        // 尺寸变化时,可以调整视频缩放比例
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
        // 返回true表示SurfaceTexture会被释放
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
        // 每帧更新时回调,可以用来做帧率统计或截图
    }
}

注意:TextureView的onSurfaceTextureDestroyed返回值很关键。返回true,系统会帮你释放SurfaceTexture;返回false,你需要自己管理。我建议返回true,省心。

5.4 SurfaceView vs TextureView 对比

对比维度 SurfaceView TextureView
渲染方式 独立窗口,硬件合成 作为普通View,通过纹理渲染
性能 高(独立Surface,无额外拷贝) 中等(需要将Surface内容转为纹理)
变换支持 不支持(只能通过setLayoutParams调整位置) 支持(setTransform、动画、缩放)
叠加层 不能叠加普通View(会被Surface盖住) 可以叠加(因为TextureView本身是View)
生命周期 独立于Activity,需手动管理 跟随View树,自动管理
适用场景 全屏播放、直播、高性能需求 需要动画、滤镜、AR、弹幕叠加

5.5 核心流程对比图

下面这张图展示了两种方案的完整数据流。你想想看,MediaPlayer解码后,数据流向完全不同:

MediaPlayer 视频渲染两种方案数据流 MediaPlayer SurfaceView TextureView 独立Surface(窗口层级) SurfaceTexture → 纹理 屏幕显示 屏幕显示 setDisplay() setSurface()

5.6 实际项目中的选择建议

你可能会问:到底该用哪个?我根据经验给几个场景建议:

  • 做视频播放器App:用SurfaceView。性能好,省电,而且全屏播放不需要变换。
  • 做直播带货:用TextureView。因为需要叠加商品列表、弹幕、点赞动画。
  • 做视频编辑:用TextureView。因为要做缩放、旋转、滤镜。
  • 做监控摄像头:用SurfaceView。低延迟是关键。

我的经验:如果你不确定,先用TextureView。因为从TextureView切到SurfaceView容易,反过来难。TextureView的灵活性可以覆盖大部分场景,性能损失在主流设备上几乎感觉不到。

5.7 常见问题与避坑

  1. 黑屏问题:SurfaceView在Activity启动时会有短暂黑屏。解决方案:在布局中设置背景色为黑色,或者使用TextureView。
  2. 内存泄漏:MediaPlayer必须在Activity销毁时释放。我习惯在onDestroy里调用mediaPlayer.release(),并把mediaPlayer置为null。
  3. 画面拉伸:视频宽高比和Surface尺寸不一致时,画面会变形。解决方案:计算视频宽高比,动态调整SurfaceView或TextureView的尺寸。
  4. TextureView的帧率:如果发现TextureView播放卡顿,可以尝试在onSurfaceTextureUpdated里减少不必要的操作。

好了,这一章的内容就到这里。两种方案各有千秋,关键是根据你的业务场景做选择。下一章我们会深入MediaPlayer的状态机,看看播放、暂停、停止这些操作背后到底发生了什么。

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