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解码后,数据流向完全不同:
5.6 实际项目中的选择建议
你可能会问:到底该用哪个?我根据经验给几个场景建议:
- 做视频播放器App:用SurfaceView。性能好,省电,而且全屏播放不需要变换。
- 做直播带货:用TextureView。因为需要叠加商品列表、弹幕、点赞动画。
- 做视频编辑:用TextureView。因为要做缩放、旋转、滤镜。
- 做监控摄像头:用SurfaceView。低延迟是关键。
我的经验:如果你不确定,先用TextureView。因为从TextureView切到SurfaceView容易,反过来难。TextureView的灵活性可以覆盖大部分场景,性能损失在主流设备上几乎感觉不到。
5.7 常见问题与避坑
- 黑屏问题:SurfaceView在Activity启动时会有短暂黑屏。解决方案:在布局中设置背景色为黑色,或者使用TextureView。
- 内存泄漏:MediaPlayer必须在Activity销毁时释放。我习惯在onDestroy里调用mediaPlayer.release(),并把mediaPlayer置为null。
- 画面拉伸:视频宽高比和Surface尺寸不一致时,画面会变形。解决方案:计算视频宽高比,动态调整SurfaceView或TextureView的尺寸。
- TextureView的帧率:如果发现TextureView播放卡顿,可以尝试在onSurfaceTextureUpdated里减少不必要的操作。
好了,这一章的内容就到这里。两种方案各有千秋,关键是根据你的业务场景做选择。下一章我们会深入MediaPlayer的状态机,看看播放、暂停、停止这些操作背后到底发生了什么。