6、媒体播放器核心:MediaSession、MediaBrowserService、播放控制逻辑

好,咱们今天聊点硬核的。车载多媒体应用里,最核心的那几块骨头,就是 MediaSession、MediaBrowserService,还有播放控制逻辑。这三个东西,说白了就是播放器的「大脑」、「神经」和「肌肉」。

我个人习惯把 MediaSession 看作「对外窗口」,MediaBrowserService 是「内容仓库」,而播放控制逻辑是「调度中心」。这三者配合好了,你的播放器才能跑得稳、响应快、不崩溃。

6.1 MediaSession:你的播放器「对外窗口」

MediaSession 是什么?它负责和系统、其他应用(比如语音助手、方向盘按键)通信。你按下方向盘上的「下一曲」,系统通过 MediaSession 告诉你的播放器:该切歌了。

核心职责:

  • 接收外部控制指令(播放、暂停、切歌、快进等)
  • 向外暴露当前播放状态(歌曲名、艺术家、进度、封面等)
  • 管理播放队列和元数据

我记得在某个项目里,客户反馈说方向盘按键偶尔失灵。查了半天,发现是 MediaSession 的 setFlags() 没设对,导致系统认为这个 Session 不可用。嗯,这里要注意:一定要设置 FLAG_HANDLES_MEDIA_BUTTONSFLAG_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 三者协作流程

下面这张图,是我自己总结的「三剑客协作流程」。你看完就明白它们怎么配合了。

MediaSession、MediaBrowserService、播放控制逻辑协作流程 MediaSession 对外窗口 MediaBrowserService 内容仓库 PlaybackController 调度中心 外部指令(方向盘、语音) 控制指令 状态更新 媒体数据 关键流程: 1. 用户按下方向盘「下一曲」 2. MediaSession 收到指令,回调 onSkipToNext() 3. PlaybackController 执行切歌,并更新状态 4. MediaSession 将新状态广播给系统 UI

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、播放控制逻辑,这三块是车载多媒体应用的基石。理解透了,后面的开发就顺了。


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