15、媒体播放集成:MediaSession与MediaBrowser、音频焦点协调、播放列表同步、歌词显示

车机互联中,媒体播放这块儿,说实话,是用户感知最强、也最容易出幺蛾子的模块。你想想看,用户上车,手机连上车机,第一件事儿大概率就是放歌。如果这时候声音卡顿、播放列表对不上、或者歌词显示慢半拍,那体验直接就崩了。

我个人习惯把媒体播放集成拆成四个核心模块来搞:会话控制音频资源争夺数据同步视觉反馈。咱们一个一个说。

15.1 MediaSession 与 MediaBrowser:车机端的“遥控器”

先说 MediaSession。你可以把它理解成车机端的一个“遥控器”。手机端是播放器本体,车机端通过 MediaSession 来发送播放、暂停、切歌、调整进度这些指令。

我在项目中遇到过一个问题:车机端发送了暂停指令,但手机端没响应。后来发现是 MediaSession 的 Callback 没注册对。嗯,这里要注意,MediaSession 必须绑定一个有效的 Callback,否则指令发出去就像石沉大海。

核心要点:

  • MediaSession 负责接收车机端的控制指令
  • MediaBrowser 负责向车机端提供媒体数据(播放列表、歌曲信息等)
  • 两者配合,实现“车机控制、手机播放”的经典模式

代码示例,注册 MediaSession 的基本套路:

// 手机端(服务端)注册 MediaSession
MediaSession mediaSession = new MediaSession(context, "CarMediaSession");
mediaSession.setCallback(new MediaSession.Callback() {
    @Override
    public void onPlay() {
        // 开始播放
        player.play();
    }

    @Override
    public void onPause() {
        // 暂停播放
        player.pause();
    }

    @Override
    public void onSkipToNext() {
        // 下一首
        player.skipToNext();
    }

    @Override
    public void onSeekTo(long pos) {
        // 跳转到指定位置
        player.seekTo(pos);
    }
});
mediaSession.setActive(true);

车机端(客户端)通过 MediaBrowser 连接:

// 车机端连接 MediaBrowser
MediaBrowser mediaBrowser = new MediaBrowser(context, 
    new ComponentName(context, MediaBrowserService.class),
    new MediaBrowser.ConnectionCallback() {
        @Override
        public void onConnected() {
            // 连接成功,可以获取播放列表等数据
            mediaBrowser.subscribe(parentId, subscriptionCallback);
        }
    }, 
    null);
mediaBrowser.connect();

个人经验:MediaSession 的 Token 一定要通过 IPC 传递到车机端。我见过有人直接在车机端 new 一个 MediaSession,那肯定连不上。Token 是唯一的“钥匙”。

15.2 音频焦点协调:别让导航和音乐打架

车机环境最头疼的问题之一,就是多个音频源同时发声。导航在播报,音乐也在放,两个声音混在一起,用户根本听不清。这时候就需要音频焦点(Audio Focus)来协调。

说白了,音频焦点就是一个“话筒”。谁拿到焦点,谁就能发声。其他应用得乖乖闭嘴或者降低音量。

我曾经在一个项目中,音乐播放器没有正确处理音频焦点丢失的回调。结果导航播报的时候,音乐还在大声放,用户直接投诉说“导航听不清”。后来我加上了焦点监听,问题就解决了。

避坑指南:音频焦点分为几种类型:

  • AudioManager.AUDIOFOCUS_GAIN:长期持有焦点(比如音乐播放)
  • AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:短暂持有(比如导航播报)
  • AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:短暂持有,但允许其他应用降低音量继续播放(比如消息提示音)

音乐播放器在收到 AUDIOFOCUS_LOSS 时,应该暂停播放;收到 AUDIOFOCUS_LOSS_TRANSIENT 时,可以暂停或降低音量;收到 AUDIOFOCUS_LOSS_TRANSIENT_MAY_DUCK 时,降低音量即可。

代码示例,请求和监听音频焦点:

AudioManager audioManager = (AudioManager) context.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)
        .setOnAudioFocusChangeListener(focusChange -> {
            switch (focusChange) {
                case AudioManager.AUDIOFOCUS_LOSS:
                    // 失去焦点,暂停播放
                    player.pause();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    // 短暂失去焦点,暂停播放
                    player.pause();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_MAY_DUCK:
                    // 短暂失去焦点,降低音量
                    player.setVolume(0.3f);
                    break;
                case AudioManager.AUDIOFOCUS_GAIN:
                    // 重新获得焦点,恢复播放或音量
                    player.setVolume(1.0f);
                    player.play();
                    break;
            }
        })
        .build();

int result = audioManager.requestAudioFocus(focusRequest);

小技巧:导航播报时,音乐应该“闪避”(Ducking),而不是完全暂停。这样用户既能听清导航,又能感受到音乐的氛围。我个人习惯把 Ducking 的音量降到 20%-30%。

15.3 播放列表同步:手机和车机别“各唱各的”

播放列表同步,说白了就是让车机端显示的歌曲列表和手机端保持一致。用户可能在手机上添加了一首歌,或者调整了播放顺序,车机端得立刻知道。

我建议用增量同步的方式,而不是全量同步。全量同步太费流量,而且慢。增量同步只传输变化的部分,效率高得多。

举个例子,用户把“周杰伦的《七里香》”从播放列表里删除了。手机端只需要发送一条消息:“删除歌曲ID=12345”。车机端收到后,在自己的列表里找到这首歌,删掉就行。

我曾经在项目中踩过一个坑:同步时没有处理并发问题。用户连续操作(比如添加一首歌、删除一首歌、再调整顺序),结果车机端收到的消息顺序乱了,列表变得乱七八糟。后来我引入了序列号(Sequence Number)机制,每条消息带一个递增的序号,车机端按序号处理,问题就解决了。

同步策略对比:

策略 优点 缺点
全量同步 实现简单,数据一致性好 流量大,速度慢,不适合频繁操作
增量同步 流量小,速度快,实时性好 实现复杂,需要处理并发和顺序
混合同步 结合两者优点 实现更复杂

代码示例,增量同步的消息格式:

// 手机端发送增量同步消息
JSONObject syncMessage = new JSONObject();
syncMessage.put("seq", 1024);  // 序列号
syncMessage.put("type", "delete");  // 操作类型:add/delete/reorder
syncMessage.put("songId", "12345");  // 歌曲ID
syncMessage.put("timestamp", System.currentTimeMillis());

// 通过 IPC 发送到车机端
carService.sendSyncMessage(syncMessage.toString());

15.4 歌词显示:别让用户“听天书”

歌词显示,看起来简单,其实坑不少。最核心的问题就是时间同步。歌词文件(LRC)里每一句歌词都有一个时间戳,比如 [00:15.50] 七里香。车机端需要根据当前播放进度,高亮显示对应的歌词。

我建议用逐行滚动的方式,而不是逐字滚动。逐字滚动对时间同步要求太高,稍微有点偏差,字就对不上。逐行滚动宽容度大得多。

还有一个容易被忽略的问题:歌词编码。很多歌词文件是 GBK 编码的,而车机系统默认是 UTF-8。如果不做转码,显示出来就是乱码。

避坑指南:歌词时间戳的精度问题。有些歌词文件的时间戳是 [00:15.5](只有一位小数),有些是 [00:15.50](两位小数)。解析时要注意统一精度,否则可能出现歌词“跳句”或“卡顿”。

代码示例,解析 LRC 歌词并同步显示:

// 解析 LRC 歌词
public class LrcParser {
    public static List parse(String lrcContent) {
        List lines = new ArrayList<>();
        String[] linesArray = lrcContent.split("\n");
        Pattern pattern = Pattern.compile("\\[(\\d{2}):(\\d{2})\\.(\\d{2,3})\\](.*)");

        for (String line : linesArray) {
            Matcher matcher = pattern.matcher(line);
            if (matcher.matches()) {
                int minute = Integer.parseInt(matcher.group(1));
                int second = Integer.parseInt(matcher.group(2));
                int millisecond = Integer.parseInt(matcher.group(3));
                long time = minute * 60 * 1000 + second * 1000 + millisecond * 10;
                String text = matcher.group(4).trim();
                lines.add(new LrcLine(time, text));
            }
        }
        return lines;
    }
}

// 在播放进度更新时,高亮当前歌词
public void onProgressUpdate(long currentPosition) {
    for (int i = 0; i < lrcLines.size(); i++) {
        LrcLine line = lrcLines.get(i);
        if (currentPosition >= line.getTime()) {
            // 高亮当前歌词
            highlightLine(i);
        }
    }
}

个人经验:歌词显示最好加一个“提前量”。比如当前歌词的时间戳是 15.5 秒,可以在 15.3 秒就开始高亮。这样用户看起来更流畅,不会觉得歌词“慢半拍”。

知识体系总览

下面这张图,我把本章的核心知识点串起来了。你可以看到,MediaSession 和 MediaBrowser 负责控制流,音频焦点负责资源协调,播放列表同步负责数据流,歌词显示负责视觉反馈。四个模块缺一不可。

媒体播放集成知识体系 手机端(服务端) 车机端(客户端) MediaSession(接收控制指令) MediaBrowserService(提供媒体数据) 音频焦点管理(请求/释放) 播放列表增量同步 MediaBrowser(发送控制指令) MediaSessionCompat(接收媒体数据) 音频焦点监听(闪避/暂停) 歌词解析与同步显示 IPC 控制指令 IPC 媒体数据 音频焦点协调 播放列表/歌词同步 四个核心模块:控制流 + 资源协调 + 数据同步 + 视觉反馈

好了,这一章的内容就这些。媒体播放集成,说白了就是让车机端和手机端“心有灵犀”。控制指令要传得准,音频资源要抢得合理,播放列表要对得上,歌词要显示得流畅。把这四点做好,用户的听歌体验基本就稳了。

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