6、媒体播放器核心:MediaSession、MediaBrowserService、播放控制逻辑
好,咱们今天聊点硬核的。车载多媒体应用里,最核心的那几块骨头,就是 MediaSession、MediaBrowserService,还有播放控制逻辑。这三个东西,说白了就是播放器的「大脑」、「神经」和「肌肉」。
我个人习惯把 MediaSession 看作「对外窗口」,MediaBrowserService 是「内容仓库」,而播放控制逻辑是「调度中心」。这三者配合好了,你的播放器才能跑得稳、响应快、不崩溃。
6.1 MediaSession:你的播放器「对外窗口」
MediaSession 是什么?它负责和系统、其他应用(比如语音助手、方向盘按键)通信。你按下方向盘上的「下一曲」,系统通过 MediaSession 告诉你的播放器:该切歌了。
核心职责:
- 接收外部控制指令(播放、暂停、切歌、快进等)
- 向外暴露当前播放状态(歌曲名、艺术家、进度、封面等)
- 管理播放队列和元数据
我记得在某个项目里,客户反馈说方向盘按键偶尔失灵。查了半天,发现是 MediaSession 的 setFlags() 没设对,导致系统认为这个 Session 不可用。嗯,这里要注意:一定要设置 FLAG_HANDLES_MEDIA_BUTTONS 和 FLAG_HANDLES_TRANSPORT_CONTROLS。
// 创建 MediaSession 的正确姿势
MediaSession session = new MediaSession(context, "CarMediaSession");
session.setFlags(
MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
);
// 设置回调
session.setCallback(new MediaSession.Callback() {
@Override
public void onPlay() {
// 开始播放
}
@Override
public void onPause() {
// 暂停播放
}
@Override
public void onSkipToNext() {
// 下一曲
}
});
避坑指南:我曾经在回调里直接做了网络请求,结果 ANR 了。记住,onPlay() 这些回调是在主线程跑的,耗时操作请扔到子线程或协程里。
6.2 MediaBrowserService:你的「内容仓库」
MediaBrowserService 负责管理媒体库。车载场景下,你的媒体源可能是本地存储、USB、蓝牙,或者在线流媒体。MediaBrowserService 把这些统一成一个「树状结构」,让客户端(比如 UI 界面)可以浏览和选择。
你想想看,用户点开「专辑」列表,系统怎么知道有哪些专辑?就是通过 MediaBrowserService 的 onLoadChildren() 方法一层层往下查的。
public class CarMediaBrowserService extends MediaBrowserService {
@Override
public void onCreate() {
super.onCreate();
// 设置根节点
setBrowserRoot(new BrowserRoot("root", null));
}
@Override
public void onLoadChildren(
String parentId,
Result<List<MediaItem>> result
) {
// 根据 parentId 返回子节点列表
List<MediaItem> items = new ArrayList<>();
if ("root".equals(parentId)) {
items.add(new MediaItem(
"专辑1",
MediaItem.FLAG_BROWSABLE
));
items.add(new MediaItem(
"歌手1",
MediaItem.FLAG_BROWSABLE
));
}
result.sendResult(items);
}
}
注意:车载环境下,媒体库可能很大(比如 USB 里有几万首歌)。onLoadChildren() 里不要一次性加载全部数据,否则内存会炸。我建议分页加载,或者用懒加载策略。
6.3 播放控制逻辑:你的「调度中心」
播放控制逻辑,就是连接 MediaSession 和实际播放器的桥梁。它负责:
- 解析 MediaSession 传来的指令
- 调用底层播放器(ExoPlayer、MediaPlayer 等)执行操作
- 更新播放状态,并同步回 MediaSession
- 处理异常情况(播放失败、网络断开、音频焦点变化等)
我习惯把播放控制逻辑封装成一个独立的 PlaybackController 类。这样 MediaSession 只负责通信,不负责具体播放,职责清晰。
public class PlaybackController {
private ExoPlayer player;
private MediaSession mediaSession;
public void play() {
try {
player.play();
mediaSession.setPlaybackState(
buildState(PlaybackStateCompat.STATE_PLAYING)
);
} catch (Exception e) {
// 播放失败,通知 UI
mediaSession.setPlaybackState(
buildState(PlaybackStateCompat.STATE_ERROR)
);
}
}
public void pause() {
player.pause();
mediaSession.setPlaybackState(
buildState(PlaybackStateCompat.STATE_PAUSED)
);
}
private PlaybackStateCompat buildState(int state) {
return new PlaybackStateCompat.Builder()
.setState(state, player.getCurrentPosition(), 1.0f)
.build();
}
}
6.4 三者协作流程
下面这张图,是我自己总结的「三剑客协作流程」。你看完就明白它们怎么配合了。
6.5 实战中的坑与经验
做车载多媒体,有几个坑我踩过,分享给你:
- 音频焦点:车载场景下,导航语音、电话、语音助手都会抢音频焦点。你的播放器必须正确处理
onAudioFocusChange(),该暂停就暂停,该降低音量就降低音量。我曾经没处理好,导航播报时音乐还在大声放,被客户骂惨了。 - 生命周期:MediaSession 和 MediaBrowserService 的生命周期要管理好。车机重启、应用被杀、USB 拔出等情况,都要能恢复播放状态。我建议用
onSaveInstanceState()保存当前播放位置和队列。 - 多用户:有些车机支持多用户(驾驶员、乘客)。每个用户的播放状态要隔离。MediaSession 的
setSessionActivity()可以绑定到特定用户的任务栈。
我的习惯:在开发阶段,我会在 MediaSession 的 Callback 里加日志,打印每条指令的接收时间和参数。这样调试时一眼就能看出是哪个环节出了问题。
6.6 代码结构建议
最后,给你一个我常用的包结构参考:
com.car.mediaplayer/
├── service/
│ ├── CarMediaBrowserService.java // MediaBrowserService 实现
│ └── CarMediaSession.java // MediaSession 封装
├── controller/
│ └── PlaybackController.java // 播放控制逻辑
├── player/
│ └── CarExoPlayer.java // 底层播放器封装
└── model/
├── MediaItem.java // 媒体项
└── PlaybackState.java // 播放状态
这样分层,每个类职责单一,后期维护也方便。你想想看,如果所有逻辑都塞在一个 Activity 里,那改起来得多痛苦。
好了,这一章的内容就到这。MediaSession、MediaBrowserService、播放控制逻辑,这三块是车载多媒体应用的基石。理解透了,后面的开发就顺了。