3. MediaPlayer 进阶:音频焦点处理、播放列表管理、错误处理与回调监听
好,咱们继续往下走。上一章我们把 MediaPlayer 的基本用法捋了一遍,播放、暂停、停止这些操作,说白了就是调几个 API 的事。但真正做播放器,光会这些远远不够。
你想想看,用户正在听歌,突然来个电话,或者另一个 App 开始播放语音,这时候你的播放器该怎么反应?还有,用户想听下一首,你总不能每次都重新 new 一个 MediaPlayer 吧?播放过程中网络断了、文件损坏了,你又该怎么处理?
嗯,这一章我们就来解决这些实际问题。我会把我在项目中踩过的坑、积累的经验,都揉碎了讲给你听。
3.1 音频焦点:别让你的播放器“打架”
音频焦点(Audio Focus)是 Android 系统用来协调多个 App 同时播放音频的机制。说白了,就是系统问各个 App:“现在有人要用音频了,你让不让?”
我刚开始做播放器时,没太在意这个。结果用户反馈说:“听歌的时候来微信语音,两个声音混在一起,啥也听不清。” 嗯,这就是典型的没处理音频焦点。
3.1.1 请求与放弃焦点
使用 AudioManager 来请求焦点。代码其实很简单:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(focusChangeListener)
.build();
int result = audioManager.requestAudioFocus(focusRequest);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 成功获取焦点,开始播放
mediaPlayer.start();
}
这里有个细节:AUDIOFOCUS_GAIN 表示你要长期占用焦点(比如播放音乐)。如果你只是短暂播放一个提示音,用 AUDIOFOCUS_GAIN_TRANSIENT 更合适。
3.1.2 焦点变化监听
焦点不是拿到就完事了。别人也可能抢走它。你需要监听焦点变化:
private AudioManager.OnAudioFocusChangeListener focusChangeListener =
new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
// 焦点永久丢失,停止播放
pausePlayer();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// 暂时丢失,暂停播放
pausePlayer();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 可以“闪避”,降低音量
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.setVolume(0.3f, 0.3f);
}
break;
case AudioManager.AUDIOFOCUS_GAIN:
// 重新获得焦点,恢复播放和音量
resumePlayer();
mediaPlayer.setVolume(1.0f, 1.0f);
break;
}
}
};
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK,很多 App 选择直接暂停。我个人习惯是降低音量,因为用户可能正在开车或走路,突然静音反而更奇怪。当然,这取决于你的应用场景。
3.2 播放列表管理:从单曲到歌单
单曲播放太单调了。用户要的是连续播放、循环、随机。这一节我们聊聊怎么管理播放列表。
3.2.1 数据结构设计
我建议用 List<MediaItem> 来管理。MediaItem 可以是一个简单的数据类:
public class MediaItem {
private String title;
private String artist;
private String url; // 本地路径或网络URL
private long duration;
private String coverUrl; // 封面图
// 构造方法、getter/setter 省略
}
播放列表的核心逻辑就三个:当前索引、上一首、下一首。
public class PlaylistManager {
private List<MediaItem> playlist;
private int currentIndex = -1;
public MediaItem getCurrentItem() {
if (currentIndex >= 0 && currentIndex < playlist.size()) {
return playlist.get(currentIndex);
}
return null;
}
public MediaItem next() {
if (playlist == null || playlist.isEmpty()) return null;
currentIndex = (currentIndex + 1) % playlist.size();
return playlist.get(currentIndex);
}
public MediaItem previous() {
if (playlist == null || playlist.isEmpty()) return null;
currentIndex = (currentIndex - 1 + playlist.size()) % playlist.size();
return playlist.get(currentIndex);
}
public void setPlayMode(PlayMode mode) { /* 循环、随机、单曲 */ }
}
Math.random() 直接跳转。我遇到过用户反馈“随机播放老是重复那几首”。正确的做法是打乱列表顺序,或者维护一个“已播放”队列。
3.2.2 切换歌曲时的 MediaPlayer 重置
切换歌曲时,不能直接调用 mediaPlayer.start()。你需要先重置:
public void switchTo(MediaItem item) {
mediaPlayer.reset();
try {
mediaPlayer.setDataSource(item.getUrl());
mediaPlayer.prepareAsync(); // 异步准备
} catch (IOException e) {
e.printStackTrace();
// 错误处理,见下一节
}
}
这里有个坑:reset() 之后,之前设置的 OnPreparedListener、OnCompletionListener 等回调依然有效。所以记得在 onPrepared 里调用 start()。
3.3 错误处理:别让播放器静悄悄地死掉
播放器在运行中会遇到各种问题:网络超时、文件格式不支持、解码器异常…… 如果不处理,用户看到的就是一个“卡住”的界面。
3.3.1 OnErrorListener
MediaPlayer 提供了 OnErrorListener 回调:
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// what 是错误类型,extra 是附加信息
switch (what) {
case MediaPlayer.MEDIA_ERROR_UNKNOWN:
Log.e(TAG, "未知错误,extra: " + extra);
break;
case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
Log.e(TAG, "媒体服务挂了");
// 这种情况需要释放并重新创建 MediaPlayer
releaseAndRecreate();
break;
}
// 返回 true 表示你处理了错误,系统不会再调用 OnCompletionListener
return true;
}
});
onError 里直接调用 mediaPlayer.start() 试图恢复播放。结果导致无限循环的错误-重试-错误。正确的做法是:先释放资源,然后根据错误类型决定是重试还是跳过当前歌曲。
3.3.2 常见错误场景与处理策略
| 错误场景 | what 值 | 推荐处理方式 |
|---|---|---|
| 网络超时 | MEDIA_ERROR_IO | 提示用户检查网络,3秒后自动重试 |
| 格式不支持 | MEDIA_ERROR_UNSUPPORTED | 跳过当前歌曲,播放下一个 |
| 解码器异常 | MEDIA_ERROR_MALFORMED | 释放 MediaPlayer,重新创建 |
| 服务端断开 | MEDIA_ERROR_SERVER_DIED | 释放并重建,重新连接 |
3.4 回调监听:掌控播放器的每一个状态
MediaPlayer 的状态变化,全靠回调来通知。我把常用的回调整理了一下:
- OnPreparedListener:准备完成,可以开始播放了。这里可以更新 UI 上的总时长。
- OnCompletionListener:播放完成。这里触发下一首。
- OnBufferingUpdateListener:缓冲进度变化。用于显示缓冲百分比。
- OnSeekCompleteListener:拖动进度条完成。可以恢复播放。
- OnVideoSizeChangedListener:视频尺寸变化。用于调整 SurfaceView 的宽高比。
我习惯把这些回调统一封装到一个 PlayerCallback 接口里,方便管理:
public interface PlayerCallback {
void onPrepared();
void onCompletion();
void onError(int what, int extra);
void onBufferingUpdate(int percent);
void onSeekComplete();
}
然后在 Activity 或 Fragment 里实现这个接口,代码会清晰很多。
3.5 知识体系总览
这一章的内容比较多,我画了一张图帮你梳理一下核心逻辑:
嗯,这张图把这一章的核心知识点串起来了。音频焦点解决“冲突”,播放列表解决“顺序”,错误处理解决“意外”,回调监听解决“感知”。四者缺一不可。
实际开发中,我建议你先把回调监听搭好,因为它是其他三个功能的基础。然后加上错误处理,再逐步完善播放列表和音频焦点。这样每一步都能验证,不容易出大问题。
好了,这一章的内容就到这里。代码量不大,但逻辑很重要。你把这些处理好,播放器的“骨架”就算立起来了。
公众号:蓝海资料掘金营,微信deep3321