第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首歌就更新一次进度。

知识体系总览

下面这张图,是我梳理的媒体应用开发核心架构。你看一眼就能明白各个模块之间的关系:

Android Automotive 媒体应用架构 CarMediaService 媒体浏览与播放 音频焦点管理 媒体源管理 MediaBrowserService onGetRoot / onLoadChildren MediaSession / MediaController AudioFocus AUDIOFOCUS_GAIN AUDIOFOCUS_LOSS_TRANSIENT 蓝牙 / USB A2DP / AVRCP 协议 StorageVolume 监听 流畅、安全、无干扰的媒体体验

从这张图你能看到,CarMediaService在最上层,下面分了三块:媒体浏览与播放、音频焦点管理、媒体源管理。每一块都有对应的实现方式。嗯,这就是车载媒体应用的整体骨架。

好了,这一章的内容就到这里。记住:车载媒体开发的核心不是功能多炫酷,而是稳定、安全、不干扰驾驶。下次我们聊聊如何优化媒体浏览的性能,让用户翻歌单时不再卡顿。


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