3. 媒体播放器(MediaPlayer)的创建与状态机
做Android多媒体开发,绕不开MediaPlayer。我刚开始接触Stagefright时,第一反应是:不就是个播放器吗?new一个对象,调个start就完事了。结果第一次做流媒体项目就被状态机坑了一把——在错误的状态调了错误的方法,直接抛异常。嗯,从那以后我老老实实把状态图打印出来贴在工位上。
今天我们就来聊聊MediaPlayer的创建、状态流转,以及几个关键流程的实现细节。
3.1 MediaPlayer状态图:一张图看懂所有状态
先看这张状态图,我建议你保存下来。每次写播放器代码前扫一眼,能省不少调试时间。
这张图我画了好几个版本,最终选了最清晰的一个。你看,状态流转其实不复杂,核心就几个关键路径:
- Idle → Initialized:调用setDataSource()后进入
- Initialized → Prepared:调用prepare()或prepareAsync()
- Prepared → Started:调用start()开始播放
- Started ↔ Paused:暂停和恢复
- Started → PlaybackCompleted:播放到末尾
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());
底层是怎么处理的?我简单梳理一下:
- Java层:MediaPlayer.setDataSource() 通过JNI调用Native层
- Native层:创建DataSource实例,根据URI判断是本地文件还是网络流
- Stagefright层:创建MediaExtractor,解析文件格式
- 返回状态:成功则状态变为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