3. 媒体播放器(MediaPlayer)的创建与状态机

做Android多媒体开发,绕不开MediaPlayer。我刚开始接触Stagefright时,第一反应是:不就是个播放器吗?new一个对象,调个start就完事了。结果第一次做流媒体项目就被状态机坑了一把——在错误的状态调了错误的方法,直接抛异常。嗯,从那以后我老老实实把状态图打印出来贴在工位上。

今天我们就来聊聊MediaPlayer的创建、状态流转,以及几个关键流程的实现细节。

3.1 MediaPlayer状态图:一张图看懂所有状态

先看这张状态图,我建议你保存下来。每次写播放器代码前扫一眼,能省不少调试时间。

MediaPlayer 状态机 Idle Initialized Prepared Started Paused Stopped Playback
Completed End Error setDataSource() prepare()/prepareAsync() start() pause() start() 播放结束 start() 重新播放 stop() prepare()/prepareAsync() 错误发生 reset() release()

这张图我画了好几个版本,最终选了最清晰的一个。你看,状态流转其实不复杂,核心就几个关键路径:

  • Idle → Initialized:调用setDataSource()后进入
  • Initialized → Prepared:调用prepare()或prepareAsync()
  • Prepared → Started:调用start()开始播放
  • Started ↔ Paused:暂停和恢复
  • Started → PlaybackCompleted:播放到末尾
⚠️ 注意:千万不要在错误的状态调用不该调的方法。比如在Idle状态调start(),直接抛IllegalStateException。我曾经在线上环境遇到过这种bug——某个机型在快速切换歌曲时,状态没同步好就调了start(),结果播放器直接崩了。

3.2 setDataSource流程:从文件路径到MediaSource

setDataSource是播放器的入口。说白了,就是告诉MediaPlayer:你要播什么内容。

我们看看典型的调用方式:

MediaPlayer mp = new MediaPlayer();
// 方式一:本地文件
mp.setDataSource("/sdcard/music/song.mp3");

// 方式二:网络流
mp.setDataSource("https://example.com/stream.mp3");

// 方式三:FileDescriptor
AssetFileDescriptor fd = getAssets().openFd("music.mp3");
mp.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());

底层是怎么处理的?我简单梳理一下:

  1. Java层:MediaPlayer.setDataSource() 通过JNI调用Native层
  2. Native层:创建DataSource实例,根据URI判断是本地文件还是网络流
  3. Stagefright层:创建MediaExtractor,解析文件格式
  4. 返回状态:成功则状态变为Initialized,失败则进入Error状态

关键点:setDataSource不会真正开始解析媒体文件。它只是把数据源的信息记录下来,真正的解析工作是在prepare阶段完成的。这个设计很巧妙——你可以在UI线程调setDataSource,不会卡住界面。

3.3 prepare与prepareAsync:同步还是异步?

这是新手最容易搞混的地方。我直接说结论:

特性 prepare() prepareAsync()
调用线程 任意线程(但会阻塞) 任意线程(不阻塞)
是否阻塞 是,直到准备完成 否,立即返回
完成通知 函数返回即完成 通过OnPreparedListener回调
适用场景 本地小文件 网络流、大文件
异常处理 直接抛异常 通过OnErrorListener回调

我个人习惯:永远用prepareAsync()。为什么?

你想想看,用户点了个播放按钮,如果文件在网络上,prepare()可能会卡住几秒甚至十几秒。UI线程一卡,ANR就来了。我早期做的一个音乐App就踩过这个坑——用户点播放,界面卡死,然后系统弹窗"是否等待"。

正确的做法是这样:

MediaPlayer mp = new MediaPlayer();
mp.setDataSource(url);
mp.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mp) {
        // 准备完成,可以开始播放了
        mp.start();
    }
});
mp.setOnErrorListener(new MediaPlayer.OnErrorListener() {
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        // 处理错误
        Log.e("MediaPlayer", "Error: " + what + ", " + extra);
        return true;
    }
});
mp.prepareAsync(); // 异步准备,不阻塞

💡 小技巧:如果你一定要用prepare(),记得在子线程调用。但说实话,既然有prepareAsync(),何必自找麻烦呢?

3.4 避坑指南:我踩过的那些坑

做播放器开发这么多年,我总结几个常见问题:

  • 重复调用prepare():同一个MediaPlayer对象,prepare()只能调一次。如果想重新播放,得先reset()再重新setDataSource。
  • 忘记注册监听器:用prepareAsync()却不注册OnPreparedListener,播放器准备好了你都不知道。
  • 在错误状态调release():release()可以在任何状态调,但调完之后MediaPlayer就不能再用了,必须重新new一个。
  • 网络流没有设置User-Agent:有些服务器会检查User-Agent,不设置可能被拒绝访问。

嗯,今天就聊到这儿。MediaPlayer的状态机其实不复杂,但细节很多。记住一点:状态不对,啥也别干。下次我们聊聊MediaPlayer的播放控制——start、pause、stop这些操作背后的实现逻辑。


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