7、媒体应用开发:MediaBrowserService与MediaSession、CarMediaSource、播放列表管理、音频焦点处理

各位同学,今天我们来聊聊车载媒体应用开发的核心部分。说实话,这部分内容在Android Automotive OS里,可以说是最考验基本功的地方。我在做车载项目时,踩过的坑有一半都跟媒体相关。你想想看,用户在车上听歌、听播客,体验好不好,全看这一块做得怎么样。

7.1 MediaBrowserService:媒体的“后台管家”

MediaBrowserService,说白了就是你的媒体应用在系统里的“门面”。它负责向车载系统暴露你的媒体内容。为什么需要它?因为车载系统需要知道你的应用里有什么歌、什么专辑,才能让用户去浏览和选择。

我个人习惯把MediaBrowserService理解成一个“内容提供者”。它不负责播放,只负责告诉系统:“嘿,我这里有哪些媒体资源”。

核心要点:MediaBrowserService必须实现onGetRoot()和onLoadChildren()两个方法。前者告诉系统你的服务是否可用,后者返回具体的媒体条目。

来看一个简单的实现示例:

class MyMediaService : MediaBrowserServiceCompat() {
    override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? {
        // 检查客户端是否合法
        if (!isValidClient(clientPackageName, clientUid)) {
            return null
        }
        return BrowserRoot(MEDIA_ROOT_ID, null)
    }

    override fun onLoadChildren(
        parentId: String,
        result: Result<List<MediaItem>>
    ) {
        result.sendResult(buildMediaItems(parentId))
    }
}

嗯,这里要注意:onGetRoot()返回null的话,客户端就完全连不上你的服务。我在项目中遇到过,有些开发者直接返回一个固定的root,不做任何校验,结果被其他应用随意访问媒体数据。这其实是个安全隐患。

7.2 MediaSession:播放控制的“遥控器”

MediaSession和MediaBrowserService是搭档。如果说MediaBrowserService是“后台管家”,那MediaSession就是“遥控器”。它负责接收用户的播放指令——播放、暂停、下一首、上一首,然后把这些指令传递给实际的播放器。

为什么需要分开?因为职责不同。MediaBrowserService管“浏览”,MediaSession管“控制”。这样设计的好处是,系统可以只跟MediaSession交互,而不需要知道你的播放器具体怎么实现的。

我的经验:MediaSession的Callback里,一定要处理好各个回调方法。特别是onPlay()和onPause(),这两个是车载系统最频繁调用的。我曾经因为onPlay()里忘记调用播放器的prepare(),导致点击播放后等了3秒才有声音,用户体验极差。

代码示例:

class MyMediaSession(private val context: Context) {
    private val session = MediaSessionCompat(context, TAG)
    private val callback = object : MediaSessionCompat.Callback() {
        override fun onPlay() {
            // 开始播放
            player.play()
            session.setPlaybackState(buildPlaybackState(PlaybackStateCompat.STATE_PLAYING))
        }

        override fun onPause() {
            player.pause()
            session.setPlaybackState(buildPlaybackState(PlaybackStateCompat.STATE_PAUSED))
        }

        override fun onSkipToNext() {
            player.skipToNext()
        }
    }

    fun init() {
        session.setCallback(callback)
        session.setFlags(
            MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
            MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
        )
    }
}

7.3 CarMediaSource:车载专用的媒体源

CarMediaSource是Android Automotive OS专门为车载场景设计的媒体源接口。它跟普通的MediaSource有什么区别?说白了,它更懂车载的需求。

举个例子:用户在车上搜索歌曲,CarMediaSource可以支持增量搜索——用户输入“周”,系统就开始返回“周杰伦”的结果,而不是等用户输完才搜索。这种体验在开车时特别重要,因为用户分心的时间越短越好。

关键特性:
  • 支持分页加载:车载屏幕空间有限,一次加载太多内容反而不好
  • 支持搜索过滤:通过SearchFilter对象传递搜索条件
  • 支持媒体项分组:按专辑、艺术家、播放列表等维度组织

我记得第一次用CarMediaSource时,被它的分页机制搞晕了。它不像普通列表那样一次性返回所有数据,而是通过onLoadChildren()配合分页参数,一页一页地加载。这样做的好处是,即使用户的媒体库有上万首歌,也不会一次性占满内存。

7.4 播放列表管理:别小看这个“队列”

播放列表管理,听起来简单,做起来坑不少。我见过太多应用,播放列表逻辑写得一团糟,导致切歌时闪退、列表顺序错乱、重复播放等问题。

在Android Automotive OS中,播放列表通常通过MediaSessionCompat.QueueItem来管理。每个QueueItem代表一首歌,包含一个唯一的ID和对应的MediaDescription。

避坑指南:我曾经在项目中遇到一个bug——用户从播放列表中间开始播放,然后切到下一首,结果跳到了列表末尾。原因是QueueItem的ID没有保持唯一性,导致系统无法正确识别当前播放的是哪一首。记住:每个QueueItem的ID必须是全局唯一的,不能重复。

播放列表管理的核心逻辑:

class PlaylistManager {
    private val queue = mutableListOf<MediaSessionCompat.QueueItem>()
    private var currentIndex = 0

    fun addToQueue(mediaId: String, title: String) {
        val description = MediaDescriptionCompat.Builder()
            .setMediaId(mediaId)
            .setTitle(title)
            .build()
        val item = MediaSessionCompat.QueueItem(
            description,
            generateUniqueId() // 必须唯一
        )
        queue.add(item)
    }

    fun getNextItem(): MediaSessionCompat.QueueItem? {
        if (queue.isEmpty()) return null
        currentIndex = (currentIndex + 1) % queue.size
        return queue[currentIndex]
    }

    fun getPreviousItem(): MediaSessionCompat.QueueItem? {
        if (queue.isEmpty()) return null
        currentIndex = if (currentIndex - 1 < 0) queue.size - 1 else currentIndex - 1
        return queue[currentIndex]
    }
}

7.5 音频焦点处理:别跟其他应用“抢麦”

音频焦点,是车载媒体应用开发中最容易被忽视、但又最重要的一环。你想想看,用户在车上可能同时开着导航、接听电话、听音乐。如果每个应用都自顾自地播放,那车里就乱套了。

Android Automotive OS的音频焦点机制,说白了就是一套“谁说话,谁闭嘴”的规则。当你的应用失去音频焦点时,应该暂停播放;当重新获得焦点时,再恢复播放。

音频焦点类型:
焦点类型 说明 使用场景
AUDIOFOCUS_GAIN 长期持有焦点 音乐播放
AUDIOFOCUS_GAIN_TRANSIENT 短暂持有焦点 导航语音
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 短暂持有,允许其他应用降低音量 通知提示音

处理音频焦点的代码:

class AudioFocusHelper(private val context: Context) {
    private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    private val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
        when (focusChange) {
            AudioManager.AUDIOFOCUS_LOSS -> {
                // 永久失去焦点,暂停播放
                player.pause()
            }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                // 暂时失去焦点,暂停播放
                player.pause()
            }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                // 可以降低音量继续播放
                player.setVolume(0.3f)
            }
            AudioManager.AUDIOFOCUS_GAIN -> {
                // 重新获得焦点,恢复播放
                player.setVolume(1.0f)
                player.play()
            }
        }
    }

    fun requestFocus(): Int {
        return audioManager.requestAudioFocus(
            focusChangeListener,
            AudioManager.STREAM_MUSIC,
            AudioManager.AUDIOFOCUS_GAIN
        )
    }
}
小技巧:在车载环境下,导航语音通常使用AUDIOFOCUS_GAIN_TRANSIENT。这意味着导航说话时,音乐应该暂停,而不是降低音量。为什么?因为降低音量后,用户可能还是听不清导航在说什么,尤其是在高速行驶时。这个细节,我在项目中被测试团队提过bug才意识到。

7.6 知识体系总览

下面这张图,我把本章的核心知识点串起来了。你可以看到,MediaBrowserService和MediaSession是“双核”,一个管浏览,一个管控制。CarMediaSource是车载专用的数据源,播放列表管理负责维护播放队列,音频焦点处理则确保多个应用和谐共存。

车载媒体应用开发核心架构 MediaBrowserService 内容浏览与暴露 MediaSession 播放控制与状态管理 CarMediaSource 车载专用媒体数据源 播放列表管理 QueueItem队列维护 音频焦点处理 多应用音频协调机制 MediaBrowserService + MediaSession 双核驱动,CarMediaSource 提供数据,播放列表管理维护队列,音频焦点确保和谐

好了,这一章的内容就到这里。媒体应用开发在车载系统里确实是个重头戏,但只要你把MediaBrowserService、MediaSession、CarMediaSource、播放列表管理和音频焦点处理这几个核心组件搞明白,大部分场景都能应对。记住我在项目中踩过的那些坑,你就能少走很多弯路。


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