第1章 媒体应用开发:从零开始构建车载娱乐系统
大家好,我是老张。在Android Automotive上做媒体开发,说实话,跟手机端完全是两码事。你想想看,用户在车里听歌,场景比手机复杂得多——可能连着蓝牙,可能插着U盘,还可能突然来个电话。嗯,今天我们就从最核心的CarMediaService开始聊起。
1.1 CarMediaService:车载媒体的心脏
CarMediaService是什么?说白了,它就是车载媒体应用和系统之间的桥梁。我刚开始接触这个服务时,以为就是个普通的Binder服务,后来才发现它承载了太多东西——媒体源管理、播放控制、音频焦点协调,全得靠它。
核心职责:
- 管理所有媒体源(蓝牙、USB、在线音乐等)
- 提供统一的媒体浏览接口
- 协调音频焦点分配
- 处理媒体按键事件
要使用CarMediaService,首先得在AndroidManifest中声明权限:
<uses-permission android:name="android.car.permission.CAR_MEDIA" />
然后通过CarManager获取服务实例。我个人习惯在Application中初始化:
public class MyCarApp extends Application {
private Car car;
private CarMediaService mediaService;
@Override
public void onCreate() {
super.onCreate();
car = Car.createCar(this, new Car.CarLifecycleListener() {
@Override
public void onCarConnected(Car car) {
mediaService = (CarMediaService) car.getCarManager(Car.CAR_MEDIA_SERVICE);
}
@Override
public void onCarDisconnected(Car car) {
mediaService = null;
}
});
car.connect();
}
}
注意:Car连接是异步的,千万别在onCreate里直接调用getCarManager。我曾经见过同事因为这个原因导致空指针,排查了半天。
1.2 媒体浏览与播放:让用户找到想听的歌
媒体浏览这块,Android Automotive用的是MediaBrowserService的模式。你想想看,用户开车时不可能盯着屏幕翻歌单,所以浏览结构必须清晰、层级不能太深。
我建议采用三层结构:
- 根节点:媒体源列表(蓝牙、USB、在线)
- 二级节点:艺术家/专辑/播放列表
- 三级节点:具体曲目
来看一个简单的MediaBrowserService实现:
public class CarMediaBrowserService extends MediaBrowserService {
private static final String ROOT_ID = "root";
@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
// 只允许系统媒体应用连接
if (!clientPackageName.equals("com.android.car.media")) {
return null;
}
return new BrowserRoot(ROOT_ID, null);
}
@Override
public void onLoadChildren(String parentId, Result<List<MediaItem>> result) {
List<MediaItem> items = new ArrayList<>();
if (ROOT_ID.equals(parentId)) {
// 加载媒体源
items.add(createMediaItem("bluetooth", "蓝牙音乐", MediaItem.FLAG_BROWSABLE));
items.add(createMediaItem("usb", "USB音乐", MediaItem.FLAG_BROWSABLE));
items.add(createMediaItem("online", "在线音乐", MediaItem.FLAG_BROWSABLE));
} else if ("bluetooth".equals(parentId)) {
// 加载蓝牙设备中的艺术家列表
items.addAll(loadBluetoothArtists());
}
result.sendResult(items);
}
private MediaItem createMediaItem(String id, String title, int flags) {
MediaDescription description = new MediaDescription.Builder()
.setMediaId(id)
.setTitle(title)
.build();
return new MediaItem(description, flags);
}
}
小技巧:在onLoadChildren中,如果数据加载耗时较长,记得调用result.detach(),然后在新线程中加载完再调用sendResult。否则ANR会找上门来。
1.3 音频焦点管理:别让导航和音乐打架
音频焦点,这是车载开发里最容易出问题的地方。你想想看,用户正在听歌,导航突然说"前方300米右转",这时候音乐应该自动降低音量。如果处理不好,用户体验会非常糟糕。
Android Automotive的音频焦点机制比手机版更严格。我建议遵循以下原则:
- 音乐应用:请求AUDIOFOCUS_GAIN,允许短暂失去焦点
- 导航应用:请求AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
- 电话应用:请求AUDIOFOCUS_GAIN_TRANSIENT,独占焦点
代码实现:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setOnAudioFocusChangeListener(focusChange -> {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
// 永久失去焦点,暂停播放
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// 短暂失去焦点,暂停播放
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 可以降低音量继续播放
setVolume(0.3f);
break;
case AudioManager.AUDIOFOCUS_GAIN:
// 重新获得焦点,恢复播放
resumePlayback();
break;
}
})
.build();
int result = audioManager.requestAudioFocus(focusRequest);
避坑指南:我曾经遇到过一个bug——音乐应用在收到AUDIOFOCUS_LOSS后没有及时暂停,结果导航语音和音乐同时播放,用户差点把车开到沟里。记住:失去焦点必须立即停止播放,不能有任何延迟。
1.4 蓝牙与USB媒体源:两种最常见的输入方式
蓝牙和USB,是车载媒体最常用的两种来源。它们的工作原理完全不同,但最终都要统一到CarMediaService的框架下。
蓝牙媒体源
蓝牙音频走的是A2DP协议。系统会通过BluetoothDevice回调通知应用:
public class BluetoothMediaSource extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// 连接成功,开始浏览媒体
connectToDevice(device);
} else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) {
// 断开连接,清理资源
disconnectDevice();
}
}
}
这里有个关键点:蓝牙设备的媒体信息是通过AVRCP协议获取的。说白了,就是手机告诉车机"我现在在播什么歌"。所以你会发现,蓝牙音乐的封面、歌名、歌手信息,其实都是从手机端推送过来的。
USB媒体源
USB媒体源就简单多了。系统会通过StorageVolume监听U盘插入事件:
public class UsbMediaSource {
private static final String VOLUME_NAME = "usb";
public void onUsbMounted(StorageVolume volume) {
if (!VOLUME_NAME.equals(volume.getDescription())) {
return;
}
File usbRoot = volume.getDirectory();
// 扫描音乐文件
List<MediaItem> musicFiles = scanMusicFiles(usbRoot);
// 更新媒体库
updateMediaLibrary(musicFiles);
}
private List<MediaItem> scanMusicFiles(File directory) {
List<MediaItem> items = new ArrayList<>();
File[] files = directory.listFiles();
if (files == null) return items;
for (File file : files) {
if (file.isDirectory()) {
items.addAll(scanMusicFiles(file));
} else if (isMusicFile(file.getName())) {
// 解析音乐文件元数据
MediaItem item = parseMusicFile(file);
if (item != null) {
items.add(item);
}
}
}
return items;
}
private boolean isMusicFile(String fileName) {
String lower = fileName.toLowerCase();
return lower.endsWith(".mp3") || lower.endsWith(".flac") || lower.endsWith(".wav");
}
}
经验之谈:USB扫描一定要在后台线程做,而且要做好进度反馈。我见过一个应用扫描5000首歌花了30秒,用户以为死机了。建议每扫描100首歌就更新一次进度。
知识体系总览
下面这张图,是我梳理的媒体应用开发核心架构。你看一眼就能明白各个模块之间的关系:
从这张图你能看到,CarMediaService在最上层,下面分了三块:媒体浏览与播放、音频焦点管理、媒体源管理。每一块都有对应的实现方式。嗯,这就是车载媒体应用的整体骨架。
好了,这一章的内容就到这里。记住:车载媒体开发的核心不是功能多炫酷,而是稳定、安全、不干扰驾驶。下次我们聊聊如何优化媒体浏览的性能,让用户翻歌单时不再卡顿。