车载媒体应用开发:MediaSession与MediaBrowserService、音频焦点管理、CarMediaSource集成、播放列表管理

车载媒体应用,说白了就是让用户在开车时能安全地听歌、听播客、听有声书。这活儿看着简单,但坑不少。我最早做车载媒体开发时,以为就是把手机端的播放器搬过来,结果被音频焦点、蓝牙连接、CarMediaSource这些玩意儿折腾得够呛。今天咱们就把这块儿掰开揉碎了讲清楚。

核心要点:车载媒体应用不是简单的播放器移植,而是要在Android Automotive框架下,通过MediaSession、MediaBrowserService、音频焦点管理和CarMediaSource四驾马车,构建一个安全、稳定、符合车载规范的媒体体验。

MediaSession与MediaBrowserService:媒体应用的双引擎

先说说MediaSession。这玩意儿是Android媒体框架的核心,它负责管理播放状态、处理媒体控制命令。我习惯把它比作「遥控器」——用户通过它来控制播放、暂停、切歌等操作。

// 创建MediaSession
MediaSessionCompat mediaSession = new MediaSessionCompat(context, "MyMediaSession");
mediaSession.setCallback(new MediaSessionCompat.Callback() {
    @Override
    public void onPlay() {
        // 处理播放请求
        startPlayback();
    }

    @Override
    public void onPause() {
        // 处理暂停请求
        pausePlayback();
    }

    @Override
    public void onSkipToNext() {
        // 切到下一首
        skipToNextTrack();
    }
});
mediaSession.setActive(true);

嗯,这里要注意:MediaSession必须保持活跃状态,否则车载系统无法识别你的应用。我在项目中遇到过一个问题——用户切到后台后,MediaSession被系统回收了,结果方向盘上的切歌按钮完全失效。后来我加了个Service来保活,问题才解决。

MediaBrowserService呢?它负责向外部提供媒体内容目录。说白了,就是告诉系统「我有哪些歌、哪些专辑、哪些播放列表」。车载系统通过它来浏览你的媒体库。

public class MyMediaBrowserService extends MediaBrowserServiceCompat {
    @Override
    public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
                                 Bundle rootHints) {
        // 验证客户端身份
        if (!isValidClient(clientPackageName, clientUid)) {
            return null;
        }
        return new BrowserRoot("root", null);
    }

    @Override
    public void onLoadChildren(String parentId,
                               Result<List<MediaItem>> result) {
        // 加载子节点
        List<MediaItem> mediaItems = buildMediaItems(parentId);
        result.sendResult(mediaItems);
    }
}

你想想看,MediaSession和MediaBrowserService就像一对搭档:一个负责「怎么播」,一个负责「播什么」。两者缺一不可。

音频焦点管理:别让声音打架

车载环境里,音频源太多了——导航、电话、音乐、语音助手……如果不管好音频焦点,就会出现导航播报时音乐还在响,或者电话进来时音乐不暂停的尴尬情况。

音频焦点管理,说白了就是「谁有发言权」。我建议用AudioManager来请求和释放焦点:

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioAttributes attributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();

// 请求音频焦点
int result = audioManager.requestAudioFocus(
        focusRequest -> {
            switch (focusRequest.getAudioFocusChange()) {
                case AudioManager.AUDIOFOCUS_GAIN:
                    // 获得焦点,恢复播放
                    resumePlayback();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS:
                    // 永久失去焦点,停止播放
                    stopPlayback();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    // 暂时失去焦点,暂停播放
                    pausePlayback();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    // 可以降低音量继续播放
                    duckVolume();
                    break;
            }
        },
        attributes,
        AudioManager.AUDIOFOCUS_GAIN,
        0
);

避坑指南:我曾经在某个项目中,没有处理AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK的情况。结果导航播报时,音乐音量没降,用户完全听不清导航。后来我加了个音量衰减逻辑,问题才解决。记住:车载环境下,导航优先级永远高于媒体。

CarMediaSource集成:让车载系统认识你的媒体

CarMediaSource是Android Automotive特有的API,它让你的媒体应用能被车载系统的媒体中心识别。我刚开始接触时觉得这玩意儿挺玄乎,其实说白了就是注册一个媒体源。

// 在AndroidManifest.xml中声明
<service
    android:name=".MyMediaBrowserService"
    android:exported="true">
    <intent-filter>
        <action android:name="android.media.browse.MediaBrowserService" />
    </intent-filter>
</service>

// 在代码中注册CarMediaSource
CarMediaSource mediaSource = new CarMediaSource.Builder(context)
        .setPackageName(context.getPackageName())
        .setClassName(MyMediaBrowserService.class.getName())
        .setDisplayName("我的音乐")
        .setIcon(Icon.createWithResource(context, R.drawable.ic_music))
        .build();

Car car = Car.createCar(context);
MediaCarService mediaCarService = (MediaCarService) car.getCarManager(Car.MEDIA_SERVICE);
mediaCarService.registerMediaSource(mediaSource);

这里有个细节:displayName和icon会显示在车载系统的媒体中心里。我建议用简洁明了的名称,别整花里胡哨的。用户开车时没工夫看你的艺术字。

播放列表管理:别让用户手忙脚乱

播放列表管理,说白了就是「下一首播什么」。车载环境下,用户不能像手机那样频繁操作,所以播放列表的设计要更智能。

我习惯用MediaSession的Queue来管理播放列表:

// 设置播放队列
List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
for (int i = 0; i < tracks.size(); i++) {
    Track track = tracks.get(i);
    MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
            .setMediaId(track.getId())
            .setTitle(track.getTitle())
            .setSubtitle(track.getArtist())
            .build();
    queue.add(new MediaSessionCompat.QueueItem(description, i));
}
mediaSession.setQueue(queue);
mediaSession.setQueueTitle("我的播放列表");

个人经验:我在做播放列表时,发现一个常见问题——用户切到下一首后,如果网络不好,会卡住。我的解决方案是预加载下一首的元数据,并在播放列表里维护一个「待播队列」。这样即使用户快速切歌,也不会出现空白。

另外,播放列表的持久化也很重要。用户可能熄火后重新上车,这时候播放列表应该能恢复。我建议用Room数据库保存播放状态:

@Entity
public class PlaybackState {
    @PrimaryKey
    public int id = 1;
    public String currentMediaId;
    public long position;
    public List<String> queueIds; // 用Gson序列化
}

知识体系总览

下面这张图,是我梳理的车载媒体应用开发的核心逻辑。你看一眼就能明白各个组件之间的关系:

车载媒体应用核心架构 用户交互(方向盘/触屏) MediaSession(播放控制中枢) 音频焦点管理 MediaBrowserService CarMediaSource集成 播放列表管理(Queue + 持久化) 车载系统媒体中心

从这张图你能看到,用户交互通过MediaSession下发指令,MediaSession再协调音频焦点、MediaBrowserService和播放列表管理。最后通过CarMediaSource集成到车载系统的媒体中心。环环相扣,缺一不可。

好了,车载媒体应用开发的核心内容就这些。记住:安全第一,稳定第二,功能第三。别为了炫酷的功能牺牲用户体验。我在项目中见过太多「功能很牛但开车时根本没法用」的案例了。