5、MediaPlayer播放控制:prepare与prepareAsync、start/pause/stop/seekTo、循环播放与单曲播放、播放状态监听
好,咱们继续往下走。上一节我们把MediaPlayer的创建和Surface绑定聊完了,现在视频源已经就位,Surface也准备好了。但有个关键问题——你总不能直接对着一个没准备好的播放器喊“开始”吧?
嗯,这节我们就来搞定播放控制的核心操作。说白了,就是怎么让视频真正跑起来,怎么暂停、怎么跳转,以及怎么监听它的一举一动。
5.1 准备阶段:prepare vs prepareAsync
MediaPlayer拿到数据源之后,不能直接start。它需要先进入“准备就绪”状态。这个准备过程,其实就是解析媒体文件、分配解码资源、初始化音视频输出管道。
Android提供了两种方式:
| 方法 | 特点 | 适用场景 |
|---|---|---|
| prepare() | 同步阻塞,直到准备完成才返回 | 本地小文件、资源文件(raw/asset) |
| prepareAsync() | 异步非阻塞,通过回调通知结果 | 网络流、大文件、任何需要流畅UI的场景 |
我个人习惯,只要不是加载raw目录下几MB的小视频,一律用prepareAsync。你想想看,如果用户点开一个视频,UI线程卡住两三秒,那体验得多糟糕。
核心原则:永远不要在UI线程调用prepare()。如果你非要用同步方式,请确保它跑在子线程里。
来看代码。这是异步准备的典型写法:
mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(videoPath);
mediaPlayer.setSurface(surface);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// 准备完成,可以开始播放了
mp.start();
}
});
mediaPlayer.prepareAsync(); // 异步准备,立即返回
这里有个坑,我曾经踩过——setOnPreparedListener必须在prepareAsync之前调用。否则异步线程跑完了,回调还没注册上,你就永远等不到onPrepared了。
5.2 核心控制:start / pause / stop / seekTo
准备完成后,播放器就进入了“已准备”状态。这时候我们可以开始控制它了。
start() 与 pause()
这两个方法成对出现,控制播放的启停。调用start后,视频开始渲染到Surface上;调用pause后,画面定格在当前帧。
// 开始播放
if (!mediaPlayer.isPlaying()) {
mediaPlayer.start();
}
// 暂停播放
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
}
注意isPlaying()这个判断。我见过不少新手直接连续调用两次pause,结果第二次调用抛了IllegalStateException。嗯,状态机这东西,你得尊重它。
stop() 与重新播放
stop()比较特殊。它会把播放器踢回“已停止”状态,这个状态下你不能直接start,必须重新调用prepareAsync才能再次播放。
mediaPlayer.stop();
// 此时不能直接start,需要重新prepare
mediaPlayer.prepareAsync(); // 重新准备
所以如果你只是想暂停一下再继续,用pause就好,别用stop。stop更适合“用户退出播放页面”这种场景。
seekTo() 跳转播放
seekTo用于跳转到指定时间位置。单位是毫秒。
// 跳转到第30秒
mediaPlayer.seekTo(30000);
seekTo有个特点——它是异步的。调用后不会立即跳转,而是等解码器处理完当前数据后再跳。如果你想在跳转完成后做点什么,可以监听OnSeekCompleteListener:
mediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
@Override
public void onSeekComplete(MediaPlayer mp) {
// 跳转完成,可以更新进度条了
}
});
小技巧:seekTo之后,如果你紧接着调用pause,某些设备上可能会出现画面卡顿或跳帧。我建议seek完成后等100ms再执行其他操作。
5.3 循环播放与单曲播放
这个需求很常见——视频播放完了,是停下来还是重新开始?
MediaPlayer提供了非常简单的控制方式:
// 开启循环播放
mediaPlayer.setLooping(true);
// 关闭循环播放(默认就是false)
mediaPlayer.setLooping(false);
就这么一行。但注意,setLooping只对正常播放结束有效。如果你调用了stop()或者发生了错误,循环是不会触发的。
如果你想实现更复杂的“单曲播放”逻辑(比如播完一首自动播下一首),那就不能靠setLooping了。你需要监听播放完成事件:
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
// 播放结束,可以加载下一首
// 或者重置播放器,重新播放当前视频
mp.seekTo(0);
mp.start();
}
});
我个人习惯,真正的“单曲循环”用setLooping(true)就够了。但如果你需要“播完列表里的所有视频”,那就得在onCompletion里手动切换数据源。
5.4 播放状态监听
MediaPlayer是一个典型的状态机。它内部维护了多种状态,比如Idle、Initialized、Prepared、Started、Paused、Stopped、PlaybackCompleted、Error等。
你不需要记住所有状态,但必须知道几个关键的回调:
- OnPreparedListener:准备完成,可以开始播放
- OnCompletionListener:播放正常结束
- OnErrorListener:播放出错,需要处理
- OnInfoListener:一些提示信息(比如缓冲开始/结束)
- OnSeekCompleteListener:跳转完成
- OnVideoSizeChangedListener:视频宽高信息可用
其中OnErrorListener特别重要。我曾经在一个项目里没加这个监听,结果用户播放一个损坏的视频文件,播放器直接崩溃了。加上之后,至少能优雅地提示用户“视频无法播放”。
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// what 和 extra 包含了具体的错误码
// 返回true表示你已经处理了错误,播放器不会触发OnCompletion
// 返回false则播放器会调用OnCompletion
Log.e("MediaPlayer", "播放出错: what=" + what + ", extra=" + extra);
return true;
}
});
注意:OnErrorListener的返回值很关键。返回true表示你接管了错误处理,播放器不会再有后续动作。返回false的话,播放器会尝试调用OnCompletionListener,这可能导致逻辑混乱。
5.5 知识体系总览
这一节的内容比较多,我画了一张图帮你理清思路:
这一节的内容其实不难,但细节很多。尤其是状态转换,稍不注意就会踩坑。我的建议是:写代码之前,先在脑子里过一遍播放器的状态流转。从Idle到Initialized,再到Prepared,然后才是Started/Paused/Stopped。每一步都有对应的回调和方法约束。
嗯,掌握了这些,你的播放器就能稳稳地跑起来了。下一节我们会聊更进阶的话题——如何把视频渲染到自定义的Surface上,实现一些酷炫的效果。
公众号:蓝海资料掘金营,微信deep3321