18、媒体应用开发:MediaSession 在车载中的使用、音频焦点处理、播放列表管理

车载媒体开发,说白了就是让车机变成一个合格的“音乐播放器”。但这里面的坑,比手机端多得多。手机你只管自己听歌就行,车载要处理导航播报、电话、语音助手、后排娱乐……各种声音抢着出声。

我最早做车载媒体时,以为就是把手机端的 MediaSession 搬过来。结果第一次路测,导航一说话,音乐直接停了,再也恢复不了。嗯,从那以后我才真正搞懂音频焦点在车里意味着什么。

核心要点:车载媒体应用 = MediaSession(对外暴露控制能力)+ 音频焦点(管理声音优先级)+ 播放列表(管理内容队列)。三者缺一不可。

18.1 MediaSession 在车载中的特殊用法

MediaSession 是 Android 媒体框架的核心。它让应用可以对外暴露“播放、暂停、下一首”等控制能力。在手机上,这些控制会显示在通知栏、锁屏界面。在车上,它们会显示在仪表盘、HUD、中控的卡片上。

我个人习惯,在车载项目里会额外做两件事:

  • 设置正确的 MediaSession 类型:使用 setSessionActivity() 指向一个专门的车载播放界面,而不是手机端的 Activity。
  • 自定义 PlaybackState:车载系统会频繁轮询状态,确保 getPlaybackState() 返回的数据是最新的,尤其是播放位置和播放速度。
// 车载 MediaSession 初始化示例
MediaSessionCompat mediaSession = new MediaSessionCompat(context, "CarMediaSession");
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
                      MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);

// 设置回调
mediaSession.setCallback(new MediaSessionCompat.Callback() {
    @Override
    public void onPlay() {
        // 启动播放,同时请求音频焦点
        requestAudioFocus();
        startPlayback();
    }

    @Override
    public void onPause() {
        // 暂停播放,释放音频焦点
        abandonAudioFocus();
        pausePlayback();
    }

    @Override
    public void onSkipToNext() {
        // 跳到下一首,注意更新播放列表索引
        skipToNextItem();
    }
});

// 设置初始状态
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
        .setActions(PlaybackStateCompat.ACTION_PLAY |
                    PlaybackStateCompat.ACTION_PAUSE |
                    PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
                    PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)
        .setState(PlaybackStateCompat.STATE_NONE, 0, 1.0f)
        .build();
mediaSession.setPlaybackState(state);

我的经验:车载系统对 MediaSession 的 setQueue() 方法非常依赖。如果你不设置播放队列,很多车载系统的“下一首”按钮会直接变灰。我曾经因为这个被测试团队追着改了三轮。

18.2 音频焦点处理:谁该出声?

音频焦点是车载媒体开发里最容易出问题的地方。手机上的音频焦点逻辑相对简单:新来的声音抢走焦点,旧的声音暂停或降低音量。但在车上,情况复杂得多。

举个例子:导航正在播报“前方300米右转”,这时候音乐应该降低音量(闪避),而不是暂停。电话来了,音乐应该暂停,导航播报也应该暂停。语音助手唤醒时,所有声音都要暂停。

我建议在车载媒体应用中实现以下音频焦点策略:

音频类型 焦点请求类型 媒体应用响应
导航播报 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 降低音量(闪避)
电话 AUDIOFOCUS_GAIN_TRANSIENT 暂停播放
语音助手 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 暂停播放,释放焦点
系统提示音 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 短暂降低音量
// 音频焦点变化监听器
private AudioManager.OnAudioFocusChangeListener afChangeListener =
        new AudioManager.OnAudioFocusChangeListener() {
    @Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                // 重新获得焦点,恢复播放或恢复音量
                if (shouldResume) {
                    resumePlayback();
                }
                restoreVolume();
                break;

            case AudioManager.AUDIOFOCUS_LOSS:
                // 长时间失去焦点,暂停播放
                pausePlayback();
                shouldResume = false;
                break;

            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                // 短暂失去焦点,暂停播放
                pausePlayback();
                shouldResume = true;
                break;

            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // 可以闪避,降低音量到 30%
                reduceVolume(0.3f);
                break;
        }
    }
};

避坑指南:我曾经遇到过一个 bug——导航播报结束后,音乐音量没有恢复。原因是导航应用释放焦点时,系统没有正确通知媒体应用。后来我在 AUDIOFOCUS_GAIN 分支里加了延迟恢复逻辑,问题才解决。记住:不要完全依赖系统通知,自己加个超时保护更稳妥。

18.3 播放列表管理:不只是“下一首”

播放列表管理在车载场景下比手机复杂。手机用户通常自己选歌,车载用户更依赖“随机播放”和“继续播放”。而且车载系统对播放列表的持久化要求很高——车熄火再启动,播放列表和播放位置应该恢复。

我常用的播放列表管理方案包含三层:

  • 数据层:使用 Room 数据库存储播放列表元数据,包括歌曲 ID、标题、艺术家、专辑封面 URI、时长等。
  • 逻辑层:维护一个 PlaybackQueue 对象,管理当前播放顺序、随机模式、重复模式。
  • 展示层:通过 MediaSession 的 setQueue() 方法将播放列表暴露给车载系统。
// 播放列表管理核心类
public class PlaybackQueue {
    private List<MediaSessionCompat.QueueItem> queue;
    private int currentIndex;
    private boolean shuffleMode;
    private int repeatMode; // 0: 不重复, 1: 单曲循环, 2: 列表循环

    public MediaSessionCompat.QueueItem getCurrentItem() {
        if (queue == null || queue.isEmpty()) return null;
        return queue.get(currentIndex);
    }

    public MediaSessionCompat.QueueItem getNextItem() {
        if (queue == null || queue.isEmpty()) return null;
        if (shuffleMode) {
            // 随机模式下,随机选一首,但不能和当前相同
            int nextIndex;
            do {
                nextIndex = new Random().nextInt(queue.size());
            } while (nextIndex == currentIndex && queue.size() > 1);
            currentIndex = nextIndex;
        } else {
            // 顺序模式下,索引+1,考虑循环
            currentIndex = (currentIndex + 1) % queue.size();
        }
        return queue.get(currentIndex);
    }

    // 持久化保存当前状态
    public void saveState(Context context) {
        SharedPreferences prefs = context.getSharedPreferences("playback", MODE_PRIVATE);
        prefs.edit()
                .putInt("currentIndex", currentIndex)
                .putBoolean("shuffleMode", shuffleMode)
                .putInt("repeatMode", repeatMode)
                .apply();
    }

    // 恢复播放状态
    public void restoreState(Context context) {
        SharedPreferences prefs = context.getSharedPreferences("playback", MODE_PRIVATE);
        currentIndex = prefs.getInt("currentIndex", 0);
        shuffleMode = prefs.getBoolean("shuffleMode", false);
        repeatMode = prefs.getInt("repeatMode", 0);
    }
}

我的建议:播放列表的持久化一定要做,而且要在 onStop() 里保存,而不是 onDestroy()。因为车载系统可能直接杀掉进程,不会走 onDestroy()。我吃过这个亏,用户每次上车都从第一首开始听,体验极差。

18.4 整体架构与数据流

下面这张图展示了车载媒体应用的核心数据流。你可以看到 MediaSession 是中枢,连接了 UI、音频焦点管理和播放列表管理。

车载媒体应用核心架构 用户交互(UI/仪表盘) MediaSession setCallback() / setPlaybackState() / setQueue() 音频焦点管理 requestAudioFocus() onAudioFocusChange() 播放列表管理 PlaybackQueue Room 持久化 媒体播放器 ExoPlayer / MediaPlayer 音频输出 请求/释放 读取/更新 控制播放 Android 车载系统服务 CarMediaService / AudioService

从图中可以看到,MediaSession 是核心枢纽。用户操作通过它分发,音频焦点变化通过它响应,播放列表通过它暴露给系统。三个模块各司其职,但又紧密配合。

18.5 实战中的几个关键细节

最后分享几个我在项目中踩过的坑,希望能帮你少走弯路:

  1. MediaSession 的生命周期:一定要在 onCreate() 里创建,在 onDestroy() 里释放。不要在 onStart()/onStop() 里反复创建销毁,否则车载系统会丢失连接。
  2. 音频焦点的延迟恢复:导航播报结束后,系统可能不会立即通知你恢复焦点。我建议加一个 500ms 的延迟,如果还没收到 AUDIOFOCUS_GAIN,主动恢复播放。
  3. 播放列表的索引校验:用户可能在后台删除了歌曲,导致播放列表索引越界。每次获取当前歌曲时,一定要做边界检查。
  4. 不要阻塞主线程:播放列表的数据库操作、网络请求(如加载专辑封面)都要放到子线程。车载系统对 ANR 的容忍度比手机更低。

总结一下:车载媒体开发的核心就是处理好 MediaSession、音频焦点和播放列表这三件事。MediaSession 是门面,音频焦点是规则,播放列表是内容。把这三件事理顺了,你的车载媒体应用就成功了一大半。

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