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() 之后,之前设置的 OnPreparedListenerOnCompletionListener 等回调依然有效。所以记得在 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 知识体系总览

这一章的内容比较多,我画了一张图帮你梳理一下核心逻辑:

MediaPlayer 进阶 音频焦点处理 核心操作 requestAudioFocus onAudioFocusChange 播放列表管理 核心操作 List<MediaItem> next/previous/setPlayMode 错误处理 onError → 释放/重试/跳过 回调监听 OnPrepared / OnCompletion / ... 四者协同,构建稳定播放器

嗯,这张图把这一章的核心知识点串起来了。音频焦点解决“冲突”,播放列表解决“顺序”,错误处理解决“意外”,回调监听解决“感知”。四者缺一不可。

实际开发中,我建议你先把回调监听搭好,因为它是其他三个功能的基础。然后加上错误处理,再逐步完善播放列表和音频焦点。这样每一步都能验证,不容易出大问题。

一个小技巧: 调试音频焦点时,可以用系统自带的音乐 App 和你的 App 同时播放,然后切来切去,观察焦点变化是否如预期。我当年就是这么测的,效果很直观。

好了,这一章的内容就到这里。代码量不大,但逻辑很重要。你把这些处理好,播放器的“骨架”就算立起来了。


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