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、音频焦点管理和播放列表管理。
从图中可以看到,MediaSession 是核心枢纽。用户操作通过它分发,音频焦点变化通过它响应,播放列表通过它暴露给系统。三个模块各司其职,但又紧密配合。
18.5 实战中的几个关键细节
最后分享几个我在项目中踩过的坑,希望能帮你少走弯路:
- MediaSession 的生命周期:一定要在
onCreate()里创建,在onDestroy()里释放。不要在onStart()/onStop()里反复创建销毁,否则车载系统会丢失连接。 - 音频焦点的延迟恢复:导航播报结束后,系统可能不会立即通知你恢复焦点。我建议加一个 500ms 的延迟,如果还没收到
AUDIOFOCUS_GAIN,主动恢复播放。 - 播放列表的索引校验:用户可能在后台删除了歌曲,导致播放列表索引越界。每次获取当前歌曲时,一定要做边界检查。
- 不要阻塞主线程:播放列表的数据库操作、网络请求(如加载专辑封面)都要放到子线程。车载系统对 ANR 的容忍度比手机更低。
总结一下:车载媒体开发的核心就是处理好 MediaSession、音频焦点和播放列表这三件事。MediaSession 是门面,音频焦点是规则,播放列表是内容。把这三件事理顺了,你的车载媒体应用就成功了一大半。