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 负责控制流,音频焦点负责资源协调,播放列表同步负责数据流,歌词显示负责视觉反馈。四个模块缺一不可。
好了,这一章的内容就这些。媒体播放集成,说白了就是让车机端和手机端“心有灵犀”。控制指令要传得准,音频资源要抢得合理,播放列表要对得上,歌词要显示得流畅。把这四点做好,用户的听歌体验基本就稳了。