4. MediaPlayer + SurfaceView:MediaPlayer基础使用、MediaPlayer状态机、SurfaceView作为显示载体、音视频同步基础概念

好,咱们正式开始进入实战环节。

这一章,我打算把 MediaPlayer + SurfaceView 这套组合拳讲透。说白了,这是 Android 视频渲染最经典、最基础的一套方案。你想想看,很多播放器、短视频 App,底层逻辑都离不开它。

我个人习惯,学任何技术先搞懂两个东西:它是什么,以及它的状态怎么变。MediaPlayer 也不例外。

4.1 MediaPlayer 基础使用

MediaPlayer 是 Android 官方提供的多媒体播放器 API。它支持音频、视频,甚至流媒体。用起来其实不复杂,核心就几步:

  1. 创建实例:new 一个 MediaPlayer 对象。
  2. 设置数据源:可以是本地文件、网络 URL,或者 ContentProvider 的 Uri。
  3. 准备播放:同步调用 prepare(),或者异步调用 prepareAsync()
  4. 启动播放:调用 start()
  5. 释放资源:用完后记得 release()

嗯,这里要注意:千万别在主线程调用 prepare()。为什么?因为它会阻塞 UI 线程,一旦网络慢或者文件大,你的界面就卡死了。我早期做项目时就踩过这个坑,用户反馈播放视频时界面点不动,查了半天才发现是同步 prepare 惹的祸。

来看一段最基础的代码:

MediaPlayer mediaPlayer = new MediaPlayer();
try {
    // 设置数据源,这里用网络视频
    mediaPlayer.setDataSource("https://example.com/video.mp4");
    // 异步准备,避免阻塞主线程
    mediaPlayer.prepareAsync();
    // 设置准备完成的监听
    mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mp) {
            // 准备完成,开始播放
            mp.start();
        }
    });
} catch (IOException e) {
    e.printStackTrace();
}

这段代码看起来简单,但背后藏着不少门道。比如 prepareAsync() 之后,MediaPlayer 内部会启动一个后台线程去加载数据,加载完了回调 onPrepared。这个回调是在主线程执行的,所以你可以放心地在里面更新 UI。

4.2 MediaPlayer 状态机

MediaPlayer 内部维护了一个状态机。不理解状态机,你写出来的代码很容易出 bug。我曾经见过一个同事,在 stop() 之后直接调 start(),结果抛了 IllegalStateException。为什么?因为状态不对。

官方文档给了一张状态图,我把它简化了一下,画成 SVG 方便你理解:

Idle Initialized Prepared Started Paused Stopped PlaybackCompleted End setDataSource() prepare()/prepareAsync() start() pause() start() 播放完成 stop() stop() prepare() start() release()

这张图里,有几个关键点我必须强调:

  • Idle 状态:刚 new 出来的时候。此时什么都不能做,必须先 setDataSource。
  • Initialized 状态:设置完数据源后。此时可以调用 prepare 或 prepareAsync。
  • Prepared 状态:准备完成。可以 start、stop,或者 seekTo。
  • Started 状态:正在播放。可以 pause、stop。
  • Paused 状态:暂停了。可以 start 恢复,或者 stop 停止。
  • Stopped 状态:停止了。不能直接 start,必须重新 prepare。
  • PlaybackCompleted 状态:播放完了。可以 start 重播,或者 seekTo 跳转。
  • End 状态:调用了 release()。此时 MediaPlayer 对象不能再用了。
⚠️ 重要警告: 千万不要在 Stopped 状态直接调 start()!必须先 prepare() 回到 Prepared 状态。否则会抛 IllegalStateException。我当年调试这个异常调了一下午,最后发现是状态机没搞懂。

4.3 SurfaceView 作为显示载体

MediaPlayer 本身只负责解码和播放,它不负责渲染画面。那画面显示到哪里去?答案是 SurfaceView

SurfaceView 和普通的 View 不太一样。它有一个独立的 Surface,这个 Surface 运行在单独的线程里。MediaPlayer 可以直接把视频帧渲染到这个 Surface 上,效率非常高。

使用方式很简单:

// 在布局中定义 SurfaceView
// <SurfaceView android:id="@+id/surface_view" ... />

SurfaceView surfaceView = findViewById(R.id.surface_view);
SurfaceHolder holder = surfaceView.getHolder();

// 设置 Surface 创建完成的监听
holder.addCallback(new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // Surface 创建好了,把 MediaPlayer 的显示载体设置给它
        mediaPlayer.setDisplay(holder);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // Surface 尺寸或格式变了,可以在这里做适配
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface 销毁了,记得释放 MediaPlayer 的显示资源
        mediaPlayer.setDisplay(null);
    }
});

这里有个细节:一定要在 surfaceCreated 之后才调用 setDisplay。如果 Surface 还没创建好就设置,MediaPlayer 会找不到显示目标,画面就黑屏了。我见过不少新手犯这个错。

💡 个人经验: 我习惯在 surfaceCreated 回调里调用 mediaPlayer.start(),而不是在 onPrepared 里。为什么?因为如果 Surface 还没准备好就 start,画面会卡在第一帧,直到 Surface 可用。提前 start 反而可能导致黑屏。

4.4 音视频同步基础概念

音视频同步,简称 AVSync。说白了,就是让声音和画面在时间上对齐。你想想看,如果视频里人物张嘴说话,声音却慢了半拍,那体验多糟糕。

MediaPlayer 内部已经帮我们做了基础的音视频同步。它默认以音频时钟为基准。为什么?因为人耳对声音的延迟更敏感。画面稍微快一点或慢一点,人眼可能察觉不到,但声音不同步,耳朵一下就听出来了。

同步的基本原理是这样的:

  1. MediaPlayer 解码音频和视频时,会给每一帧打上时间戳(PTS,Presentation Time Stamp)。
  2. 播放时,音频和视频各自独立解码,但渲染时会根据时间戳对齐。
  3. 如果视频帧比音频快了,就丢帧或者等待;如果慢了,就加速渲染或者重复显示上一帧

MediaPlayer 的同步策略是:

情况 处理方式
视频帧比音频早到 等待,直到音频时钟追上
视频帧比音频晚到 丢弃该帧,直接渲染下一帧
音频帧比视频早到 音频继续播放,视频加速追赶
音频帧比视频晚到 音频等待,视频减速或重复帧

嗯,这里要注意:MediaPlayer 的同步是自动的,你不需要手动干预。但如果你自己写解码器(比如用 MediaCodec),那音视频同步就得自己实现了。那是后面章节的内容,这里先有个概念就行。

📌 核心要点: MediaPlayer + SurfaceView 这套方案,MediaPlayer 负责解码和同步,SurfaceView 负责显示。你只需要把两者通过 setDisplay() 连接起来,剩下的交给系统。

最后,我再说一个实战中容易忽略的点:生命周期管理。Activity 或 Fragment 销毁时,一定要记得 release() MediaPlayer,否则会内存泄漏。我习惯在 onDestroy() 里做这件事:

@Override
protected void onDestroy() {
    super.onDestroy();
    if (mediaPlayer != null) {
        mediaPlayer.release();
        mediaPlayer = null;
    }
}

好了,这一章的内容就到这里。MediaPlayer 的状态机、SurfaceView 的配合、音视频同步的概念,都是后面实战的基础。把这些搞懂了,后面写代码会顺手很多。


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