实现电台功能

本页将介绍如何在硬件和软件级别实现电台功能。

  • 系统组件会说明并描述电台技术栈。
  • 广播电台硬件抽象层为原始设备制造商 (OEM) 提供了数据结构和接口,以便在硬件级别实现广播电台功能,例如 AM/FM 和数字音频广播 (DAB) 电台。
  • 电台控制实现基于 MediaSessionMediaBrowse,媒体和语音助理应用可以利用它们实现对电台的控制。 除了下面提供的内容之外,请参阅构建车载媒体应用

系统组件

广播电台堆栈包含以下组件。

广播电台架构
图 1. 广播电台架构

电台参考应用

如需详细了解如何实现电台控制,请参阅电台控制实现

Java 电台应用示例 (packages/apps/Car/Radio) 可用作实现参考。应用服务启动时,它会请求电台管理器打开电台调谐器。然后,应用可以向电台调谐器发送请求,例如调到特定电台、频率或搜寻下一个可用的电台。应用通过电台中的电台管理器和电台调谐器接收更新,例如当前节目信息、电台节目列表、配置和供应商定义的参数。参考电台应用仅支持 AM 和 FM 电台。OEM 可以根据需要修改或更换电台应用。

电台管理器

当应用请求电台管理器打开调谐器时,电台管理器 (frameworks/base/core/java/android/hardware/radio/RadioManager.java) 会请求广播电台服务打开调谐器会话,然后将该会话封装在电台调谐器 (frameworks/base/core/java/android/hardware/radio/RadioTuner.java) 中并返回给应用。电台调谐器会定义 API(例如调谐、步进和取消)。这些 API 可以从电台应用中调用,并向广播电台服务发送请求。在电台调谐器中定义的回调方法 (RadioTuner.Callback) 会从广播电台服务向应用发送有关广播电台 HAL 的更新(例如当前节目信息、节目列表和供应商定义的参数)。

广播电台服务

广播电台服务 (frameworks/base/services/core/java/com/android/server/broadcastradio) 是广播电台 HAL 的客户端服务。广播电台服务使用广播电台 HAL 协调多个电台管理器。广播电台服务支持 HAL 接口定义语言 (HIDL)Android 接口定义语言 (AIDL) 广播电台 HAL。如果存在任何 AIDL HAL 服务,广播电台服务就会关联到 AIDL HAL;否则,该服务会关联到 HIDL HAL。广播电台服务会为每个可用的 HAL 实例(例如 AM、FM 和 DAB)创建一个电台模块。

每个电台管理器都可以请求广播电台服务根据电台类型在相应的电台模块上创建调谐器会话。每个调谐器会话都可以调用(在 HAL 接口中定义的)调谐、步进和取消等方法对相应的广播电台 HAL 实例执行操作。当一个调谐器会话收到 HAL 实例对 HAL 更新(例如当前节目信息、节目列表、配置标志和供应商参数)发出的回调时,系统会将有关更新的回调发送给与同一电台模块关联的所有电台调谐器。

广播电台 HAL

如需详细了解广播电台的 HIDL 和 AIDL 接口以及两者之间的区别,请参阅广播电台 HAL 接口

广播电台硬件抽象层

下面几部分将介绍如何使用硬件层实现广播电台功能。

广播电台 HAL 接口

广播电台 HAL 在硬件级别提供数据结构和接口,以实现 AM/FM 和 DAB 电台等广播电台功能。

HIDL 2.0 和 AIDL 接口

广播电台 HAL 使用下面几部分中介绍的接口。

通知监听器

IAnnouncementListener 是通知监听器的回调接口,可以在广播电台 HAL 上注册此监听器,以便接收通知。该接口使用以下方法:

IAnnouncementListener
说明:每当通知列表发生变化时会调用。
HIDL 2.0 oneway onListUpdated(vec<Announcement> announcements)
AIDL oneway void onListUpdated(in Announcement[] announcements)
关闭句柄

ICloseHandle 是通用关闭句柄,可移除不需要活跃接口的回调。

ICloseHandle
说明:关闭手柄。
HIDL 2.0 close()
AIDL void close()

回调接口

ITunerCallback 是广播电台 HAL 调用的回调接口,用于向 HAL 客户端服务发送更新。

ITunerCallback
说明:当调谐操作(调谐、搜寻 [使用 AIDL]/扫描 [使用 HIDL] 以及步进成功时)异步失败时,由 HAL 调用。
HIDL 2.0 oneway onCurrentProgramInfoChanged(ProgramInfo info)
AIDL void onCurrentProgramInfoChanged(in ProgramInfo info)
说明:会在调谐、搜寻(使用 AIDL)/扫描(使用 HIDL)或步进成功时调用。
HIDL 2.0 oneway onTuneFailed(Result result, ProgramSelector selector)
AIDL void onTuneFailed(in Result result, in ProgramSelector selector)
说明:会在调谐、搜寻(使用 AIDL)/扫描(使用 HIDL)或步进成功时调用。
HIDL 2.0 oneway onCurrentProgramInfoChanged(ProgramInfo info)
AIDL void onCurrentProgramInfoChanged(in ProgramInfo info)
说明:会在节目列表更新时调用;每个分块的大小不得超过 500kiB。
HIDL 2.0 oneway onProgramListUpdated(ProgramListChunk chunk)
AIDL oneway onProgramListUpdated(ProgramListChunk chunk)
说明:会在天线连接或断开连接时调用。
HIDL 2.0 oneway onAntennaStateChange(bool connected)
AIDL void onCurrentProgramInfoChanged(in ProgramInfo info)
说明:会在 HAL 内部更新供应商特定参数值时调用(不应在 HAL 客户端调用 setParameters 之后再调用)。
HIDL 2.0 oneway onParametersUpdated(vec<VendorKeyValue> parameters)
AIDL void onParametersUpdated(in VendorKeyValue[] parameters)
说明:AIDL 中的新功能。会在 HAL 内部更新配置标志时调用(不应在 HAL 客户端调用 setConfigFlag 之后再调用)。
HIDL 2.0 不适用。
AIDL void onConfigFlagUpdated(in ConfigFlag flag, in boolean value)

主要广播电台 HAL 接口

IBroadcastRadio 是广播电台 HAL 的主要接口。在 HIDL 2.0 HAL 中,使用调谐器的 ITunerSession 接口来调用操作。不过,一次最多只有一个调谐器处于活跃状态(前提是每个广播电台 HAL 实例只有一个调谐器芯片)。 系统从 AIDL 接口中移除了 ITunerSession,并将其接口移到了 IBroadcastRadio

IBroadcastRadio
说明:获取模块及其功能的说明。
HIDL 2.0 getProperties() generates (Properties properties)
AIDL Properties getProperties()
说明:提取当前或可能的 AM/FM 区域配置。
HIDL 2.0 getAmFmRegionConfig(bool full) generates (Result result, AmFmRegionConfig config)
AIDL AmFmRegionConfig getAmFmRegionConfig(bool full)
说明:提取当前的 DAB 区域配置。
HIDL 2.0 getDabRegionConfig() generates (Result result, vec<DabTableEntry> config)
AIDL DabTableEntry[] getDabRegionConfig()
说明:从电台模块缓存中获取图片。在 AIDL 中,由于 binder 事务缓冲区存在硬性上限,因此图片大小必须小于 1MB。
HIDL 2.0 getImage(uint32_t id) generates (vec<uint8_t> image)
AIDL byte[] getImage(in int id)
说明:注册通知监听器。
HIDL 2.0 registerAnnouncementListener(vec<AnnouncementType> enabled,IAnnouncementListener listener) generates (Result result, ICloseHandle closeHandle)
AIDL ICloseHandle registerAnnouncementListener(in IAnnouncementListener listener, in AnnouncementType[] enabled)
说明
  • HIDL HAL:打开新的调谐器会话时,必须终止旧的会话。
  • AIDL HAL:由于没有可用的调谐器会话,因此只需设置调谐器回调。 如果调谐器回调已存在,则应取消设置旧回调。
HIDL 2.0 openSession(ITunerCallback callback) 生成 (Result result, ITunerSession session)
AIDL void setTunerCallback(in ITunerCallback callback)
说明
  • HIDL HAL:关闭调谐器会话不得失败,并且只能发出一次该命令。
  • AIDL HAL:没有调谐器,只需要取消设置调谐器回调。
HIDL 2.0 close()
AIDL unsetTunerCallback()
说明:调谐到指定的节目。
HIDL 2.0 tune(ProgramSelector program) generates (Result result)
AIDL void tune(in ProgramSelector program)
说明:寻找下一个在播送中的有效节目。为避免在 AIDL 中产生混淆,scan 已重命名为 seek
HIDL 2.0 scan(bool directionUp, bool skipSubChannel) generates (Result result)
AIDL void seek(in boolean directionUp, in boolean skipSubChannel)
说明:步进到相邻的频道,此频道可能未被任何节目占用。
HIDL 2.0 step(bool directionUp) generates (Result result)
AIDL void step(in boolean directionUp)
说明:取消待处理的调谐、扫描(使用 HIDL)/搜寻(使用 AIDL)或步进操作。
HIDL 2.0 cancel()
AIDL void cancel()
说明:对节目列表应用过滤条件,并开始通过 onProgramListUpdated 回调发送节目列表更新。
HIDL 2.0 startProgramListUpdates(ProgramFilter filter) generates (Result result)
AIDL void startProgramListUpdates(in ProgramFilter filter)
说明:停止发送节目列表更新。
HIDL 2.0 stopProgramListUpdates()
AIDL void stopProgramListUpdates()
说明:提取给定配置标志的当前设置。
HIDL 2.0 isConfigFlagSet(ConfigFlag flag) generates (Result result, bool value)
AIDL boolean isConfigFlagSet(in ConfigFlag flag)
说明:设置给定配置标志。
HIDL 2.0 setConfigFlag(ConfigFlag flag, bool value) generates (Result result)
AIDL void setConfigFlag(in ConfigFlag flag, boolean value)
说明:设置供应商特定的参数值。
HIDL 2.0 setParameters(vec<VendorKeyValue> parameters)

生成

(vec<VendorKeyValue> results)
AIDL VendorKeyValue[] setParameters(in VendorKeyValue[] parameters)
说明:检索供应商特定的参数值。
HIDL 2.0 getParameters(vec<string> keys) generates (vec<VendorKeyValue> parameters)
AIDL VendorKeyValue[] getParameters(in String[] keys)

接口说明

异步行为

由于每个调谐操作(例如调谐、扫描 [使用 HIDL]/搜寻 [使用 AIDL] 或步进)都可能非常耗时,并且不应长时间阻塞线程,因此操作流程应将耗时的操作安排在稍后执行,并快速返回状态或结果。具体而言,每项操作都应:

  • 取消所有待处理的调谐操作。
  • 检查是否可以根据方法输入和调谐器状态处理操作。
  • 安排调谐任务,然后立即返回 Result(使用 HIDL)或 status(使用 AIDL)。 如果 ResultstatusOK,则在调谐任务失败(例如由于超时)或完成时必须调用调谐器回调 tuneFailedcurrentProgramInfoChanged

同样,startProgramListUpdates 还会安排更新节目列表这项比较耗时的任务在稍后完成,并快速返回状态或结果。该方法会先取消待处理的更新请求,然后安排更新任务并快速返回结果。

竞态条件

由于调谐操作(例如调谐、扫描 [使用 HIDL]/搜寻 [使用 AIDL] 和步进)的异步行为,取消操作和调谐操作之间存在竞态条件。如果在 HAL 完成调谐操作之后、回调完成之前调用 cancel,则可以忽略取消操作,且相应回调应完成并由 HAL 客户端接收。

同样,如果在 HAL 完成节目列表更新后、onCurrentProgramInfoChanged 回调完成之前调用 stopProgramListUpdates,则可以忽略 stopProgramListUpdates,且相应回调应完成。

数据大小限制

由于 binder 事务缓冲区存在硬性限制,因此 AIDL HAL 中会说明传递可能较大的数据时某些接口方法的数据限制。

  • getImage 要求返回的图片小于 1MB。
  • onProgramListUpdate 要求每个 chunk 小于 500kiB。 较大的节目列表必须由 HAL 实现拆分为多个分块,并通过多个回调发送。

AIDL HAL 数据结构的变更

除了接口的变更之外,这些变更已被应用于广播电台 AIDL HAL 中定义的数据结构,该数据结构利用了 AIDL。

  • Constant 枚举已从 AIDL 中移除,并已在 IBroadcastRadio 中定义为常量整数。同时,ANTENNA_DISCONNECTED_TIMEOUT_MS 已重命名为 ANTENNA_STATE_CHANGE_TIMEOUT_MS。系统新增了一个常量整数 TUNER_TIMEOUT_MS。所有调谐、搜寻和步进操作都必须在这段时间内完成。
  • 枚举 RDSDeemphasis 已从 AIDL 中移除,并已在 AmFmRegionConfig 中定义为常量整数。相应地,ProgramInfo 中的 fmDeemphasisfmRds 会声明为整数,这是相应标志的位计算结果。同时,D50D75 已分别重命名为 DEEMPHASIS_D50DEEMPHASIS_D75
  • 枚举 ProgramInfoFlags 已从 AIDL 中移除,并已在 ProgramInfo 中定义为常量整数,同时添加了前缀 FLAG_。相应地,ProgramInfo 中的 infoFlags 会声明为整数,这是标志的位计算结果。TUNED 也已重命名为 FLAG_TUNABLE,以更好地描述其定义,即电台可调谐的范围。
  • 因为在 AIDL 中,scan 已重命名为 seek,所以在 AmFmBandRange 中,scanSpacing 已重命名为 seekSpacing
  • 由于 AIDL 中引入了并集的概念,因此系统不再使用 HIDL HAL 中定义的 MetadataKeyMetadata。AIDL HAL 中定义了 AIDL 并集 Metadata。之前在 MetadataKey 中的每个枚举值现在都是 Metadata 中的字段,其类型为字符串或整数,具体取决于其定义。

电台控制实现

电台控制实现基于 MediaSessionMediaBrowse,媒体和语音助理应用可以利用它们实现对电台的控制。如需了解详情,请参阅 developer.android.com 上的构建车载媒体应用

packages/apps/Car/libs 中的 car-broadcastradio-support 库中提供了一个媒体浏览树实现。此库中还包含 ProgramSelector 的扩展,用于实现与 URI 之间的来回转换。建议电台实现使用此库构建关联的浏览树。

媒体来源切换器

为在电台和媒体中显示的其他应用之间顺畅切换,car-media-common 库包含了应集成到电台应用中的类。MediaAppSelectorWidget 可添加到电台应用的 XML 中(参考媒体和电台应用中使用的图标或下拉列表):

<com.android.car.media.common.MediaAppSelectorWidget
     android:id="@+id/app_switch_container"
     android:layout_width="@dimen/app_switch_widget_width"
     android:layout_height="wrap_content"
     android:background="@drawable/app_item_background"
     android:gravity="center" />

此微件会启动 AppSelectionFragment,以显示可切换到的媒体来源列表。除了提供的界面之外,如果还需要其他界面,则可以创建自定义微件,以在应显示切换器时启动 AppSelectionFragment

AppSelectionFragment newFragment = AppSelectionFragment.create(widget,
            packageName, fullScreen);
    newFragment.show(mActivity.getSupportFragmentManager(), null);

在参考电台应用实现中提供了一个实现示例,它位于 packages/apps/Car/Radio 中。

详细的控制规范

MediaSession(通过 MediaSession.Callback)接口提供了适用于当前正在播放的电台节目的控制机制:

  • onPlayonStop。将电台播放(取消)静音。
  • onPause。时移暂停(如果支持)。
  • onPlayFromMediaId。播放顶层文件夹中的任何内容。例如“播放 FM”或“播放电台”。
  • onPlayFromUri。播放特定频率。例如,“播放 88.5 FM”。
  • onSkipToNextonSkipToPrevious。调到下一个或上一个电台。
  • onSetRating。向收藏夹中添加或从收藏夹中移除。

MediaBrowser 在三种类型的顶层目录上提供可收听的 MediaItem

  • (可选)节目(电台)这种模式通常供双调谐器电台使用,以指明用户所在位置的所有可用的可收听电台。
  • 收藏夹。添加到“收藏夹”列表的电台节目,有些节目可能无法收到(超出接收范围)。
  • 频段频道。当前区域中所有可实际收到的频道(87.9、88.1、88.3、88.5、88.7、88.9、89.1 等)。每个频段都有一个单独的顶层目录。
MediaBrowserService 树结构
图 2. MediaBrowserService 树结构

其中每个文件夹 (AM/FM/Programs) 中的每个元素都是一个具有 URI 的 MediaItem,该 URI 可与 MediaSession 配合调谐。每个顶层文件夹 (AM/FM/Programs) 都是具有 mediaId 的 MediaItem,mediaId 可与 MediaSession 配合使用以触发播放,且 mediaId 由原始设备制造商 (OEM) 自行决定。例如,“播放 FM”“播放 AM”和“播放电台”都是使用 mediaId 发送到 OEM 电台应用的非具体电台查询。由电台应用决定根据通用请求和 mediaId 播放的内容。

MediaSession

由于实时广播是无法暂停的,因此播放、暂停和停止操作并不总是适用于电台。对电台而言,“停止”操作与关闭在线播放的声音有关,而“播放”操作则与取消静音有关。

某些电台调谐器(或应用)支持通过缓存内容并在稍后播放来模拟实时广播暂停。在这种情况下,请使用 onPause

根据 mediaId 和 URI 进行播放的操作用于调到从 MediaBrowser 接口中提取的电台。mediaId 是电台应用提供的任意字符串,用于指定唯一(一个指定 ID 仅指向一项内容)且稳定的(一项指定内容在整个会话中将具有相同的 ID)值来标识给定的电台。URI 将是具有明确定义的架构。简言之,就是 ProgramSelector 的 URI 化形式。虽然这可以使 URI 保持唯一性,但它无需保持稳定性。当电台调到其他频率时,URI 可以改变。

根据设计,不使用 onPlayFromSearch。客户端(配套应用)将负责从 MediaBrowser 树中选择搜索结果。将这项责任转移到电台应用会增加复杂性,需要就字符串查询的显示方式达成正式协定,并且会导致不同硬件平台上的用户体验不一致。

注意:就电台名称搜索而言,在 MediaBrowser 接口向客户端提供的信息之外,电台应用不会包含有用的额外信息。

跳到下一个或上一个电台时,具体会跳到哪个电台取决于当前的使用情景:

  • 如果应用调到了“收藏夹”列表中的某个电台,则此应用可以调到“收藏夹”列表中的下一个电台。
  • 如果收听的是节目列表中的某个电台,应用可能会调到下一个可播放的电台(根据频道编号排序)。
  • 如果收听的是任意频道,则即使没有广播信号,应用也可能会调到下一个实际频道。

电台应用会处理这些操作。

错误处理

TransportControls 操作(播放、停止和下一个)不会提供有关操作是否成功的反馈。要指出错误,唯一的方法就是将 MediaSession 状态设置为 STATE_ERROR,并提供错误消息。

电台应用必须处理这些操作,然后予以执行或设置错误状态。如果“播放”命令不会立即执行,那么在执行该命令时,播放状态应变为 STATE_CONNECTING(如果是直接调谐)、 STATE_SKIPPING_TO_PREVIOUS NEXT

客户端应监视 PlaybackState,并确认会话是已将当前节目更改为所请求的节目,还是已进入错误状态。STATE_CONNECTING 不能超过 30 秒。但是,直接调谐到指定 AM/FM 频率的执行速度应该会快得多。

添加和移除收藏夹

MediaSession 支持评分功能,可用于控制收藏夹。使用 RATING_HEART 类型的评分调用 onSetRating 时,会在“收藏夹”列表中添加或移除当前调到的电台。

与传统预设相反,如果将每个保存的收藏电台分配给一个数字槽位(通常为 1 到 6),此模型会假设“收藏夹”列表是无序且无界的。因此,基于预设的系统与 onSetRating 操作不兼容。

MediaSession API 的局限性在于只能添加或移除当前调到的电台。例如,要移除项目,就必须先选择项目。该限制仅适用于 MediaBrowser 客户端(例如配套应用)。电台应用没有类似的限制。当应用不支持“收藏夹”时,此部分为可选内容。

MediaBrowser

为了说明在给定的区域中哪些频率或实际频道名称(如果给定的电台技术支持调到任意频道)有效,我们列出了每个频段的所有有效频道(频率)。在美国地区,在 87.8 - 108.0 MHz 的频率范围内(间隔为 0.2 MHz),有 101 个 FM 频道;在 530 - 1700 kHz 的频率范围内(间隔为 10 kHz),有 117 个 AM 频道。HD 电台使用相同的频道空间,因此没有单独列出。

当前可供播放的电台节目列表是一种简单列表,它不支持按数字音频广播 (DAB) 集合进行分组等显示方案。

“收藏夹”列表中的条目可能无法正常播放。例如,当指定节目超出频率范围时。电台应用不一定会事先检测条目是否可以播放。在这种情况下,电台应用可能不会将条目标记为可播放。

为了标识顶层文件夹,将应用蓝牙所使用的相同机制。也就是说, MediaDescription 对象的 Extra 捆绑包包含一个调谐器专属字段,其等效于蓝牙的 EXTRA_BT_FOLDER_TYPE。如果是广播电台,则需要在公共 API 中定义以下新字段:

  • EXTRA_BCRADIO_FOLDER_TYPE = "android.media.extra.EXTRA_BCRADIO_FOLDER_TYPE". 以下值之一:
    • BCRADIO_FOLDER_TYPE_PROGRAMS = 1。当前可供播放的节目。
    • BCRADIO_FOLDER_TYPE_FAVORITES = 2。收藏夹。
    • BCRADIO_FOLDER_TYPE_BAND = 3。给定频段的所有实际频道。

    您无需定义任何特定于电台的自定义元数据字段,因为所有相关数据都适合现有的 MediaBrowser.MediaItem 架构:

    • 节目名称(RDS PS、DAB 服务名称)MediaDescription.getTitle
    • FM 频率。URI(请参阅 ProgramSelector)或 MediaDescription.getTitle(如果条目位于 BROADCASTRADIO_FOLDER_TYPE_BAND 文件夹中)。
    • 电台专属标识符(RDS PI、DAB SId)。 MediaDescription.getMediaUri 解析为 ProgramSelector。

    通常,无需为当前节目或“收藏夹”列表中的条目提取 FM 频率(因为客户端应根据媒体 ID 进行操作)。但是,如果确实出现了这种需求(例如,出于显示目的),可在 URI 中找到,并可解析为 ProgramSelector。即便如此,建议不要使用 URI 来选择当前会话中的内容。如需了解详情,请参阅 ProgramSelector

    为避免发生与性能或 binder 相关的问题,MediaBrowser 服务必须支持分页:

    注意:默认情况下,在 onLoadChildren() 变体中默认会实现分页,无需处理相关选项。

    所有类型列表(原始频道、找到的节目以及收藏夹)中的相关条目可能具有不同的 mediaId(这取决于电台应用;支持库会有不同的 mediaId)。在大多数情况下,原始频道与找到的节目的 URI(采用 ProgramSelector 形式)有所不同(没有 RDS 的 FM 除外),但找到的节目和收藏夹之间的 URI 基本相同(AF 已更新等情况除外)。

    通过对来自不同类型列表的条目使用不同的 mediaId,可以对它们执行不同的操作。您可以在 onSkipToNext 上遍历“收藏夹”列表或“所有节目”列表,具体取决于最近选择的 MediaItem 文件夹(请参阅 MediaSession)。

    特殊调谐操作

    通过节目列表,用户可以调到特定的电台,但无法提出一般的要求,例如“调到 FM”,这可能会导致调到 FM 频段上最近收听的电台。

    为了支持这样的操作,一些顶层目录设置了 FLAG_PLAYABLE 标志(并针对文件夹设置了 FLAG_BROWSABLE)。

    操作 调谐目标 如何发出
    播放电台 任意电台频道 startService(ACTION_PLAY_BROADCASTRADIO)

    或,

    playFromMediaId(MediaBrowser.getRoot())
    播放 FM 任意 FM 频道 从 FM 频段的 mediaId 播放

    具体调到哪个节目取决于应用,通常是给定列表中最近调到的频道。如需详细了解 ACTION_PLAY_BROADCASTRADIO,请参阅常规播放 intent

    发现和服务连接

    PackageManager 可以直接找到提供广播电台树的 MediaBrowserService。为此,请使用 ACTION_PLAY_BROADCASTRADIO intent(参阅常规播放 intent)和 MATCH_SYSTEM_ONLY 标志调用 resolveService。如需查找所有提供电台的服务(可能有多个;例如单独的 AM/FM 和卫星),请使用 queryIntentServices

    已解析的服务也会处理 android.media.browse.MediaBrowserService 绑定 intent。此操作已通过 GTS 验证。

    如需连接到选定的 MediaBrowserService,请为给定服务组件创建 MediaBrowser 实例并执行 connect 操作。建立连接后,可以通过 getSessionToken 获取 MediaSession 的句柄。

    电台应用可以限制允许在其服务的 onGetRoot 实现中连接的客户端软件包。该应用应允许系统应用在没有加入白名单的情况下连接。如需了解有关加入白名单的详细信息,请参阅接受 Google 助理应用软件包和签名

    如果针对特定来源的应用(例如电台应用)安装在不支持此类来源的设备上,则该应用仍会将自己通告为会处理 ACTION_PLAY_BROADCASTRADIO intent,但其 MediaBrowser 树不会包含针对特定电台的标记。因此,如果客户端想要检查给定来源在设备上是否可用,其必须:

    1. 发现电台服务(为 ACTION_PLAY_BROADCASTRADIO 调用 resolveService)。
    2. 创建 MediaBrowser,然后与其连接。
    3. 通过 EXTRA_BCRADIO_FOLDER_TYPE extra 确定是否存在 MediaItem

    注意:在大多数情况下,客户端必须扫描所有可用的 MediaBrowser 树,以检测可在给定设备上使用的所有来源。

    频段名称

    频段列表由一组顶层目录表示,文件夹类型标记设置为 BCRADIO_FOLDER_TYPE_BAND。其 MediaItem 的标题是表示频段名称的本地化字符串。在大多数情况下,这与英语译文相同,但客户端不能依赖该假设。

    为提供查找特定频段的可靠机制,为频段文件夹添加了一个 extra 标记:EXTRA_BCRADIO_BAND_NAME_EN。该标记是频段的非本地化名称,只能接受以下某个预定义值:

    • AM
    • FM
    • DAB

    如果频段不在此列表中,则不应设置相应的频段名称标记。但是,如果频段在列表中,则必须设置标记。HD 电台不会列出单独的频段,因为它使用与 AM/FM 相同的底层媒体。

    常规播放 intent

    每个专用于播放给定来源(例如电台或 CD)的应用都必须处理一个常规播放 intent,才能开始播放一些可能来自非活跃状态的内容(例如启动后)。应用会决定如何选择要播放的内容,但这些内容通常是最近播放的电台节目或 CD 曲目。每个音频来源都定义了一个单独的 intent:

    • android.car.intent.action.PLAY_BROADCASTRADIO
    • android.car.intent.action.PLAY_AUDIOCD:CD-DA 或 CD-Text
    • android.car.intent.action.PLAY_DATADISC:诸如 CD/DVD 之类的光盘,但不是 CD-DA(可能是混合模式 CD)
    • android.car.intent.action.PLAY_AUX:不指定 AUX 端口
    • android.car.intent.action.PLAY_BLUETOOTH
    • android.car.intent.action.PLAY_USB:不指定 USB 设备
    • android.car.intent.action.PLAY_LOCAL:本地媒体存储(内置闪存)

    之所以选择将 intent 用于常规播放命令,是因为它们同时解决了两个问题:常规播放命令本身和服务发现。使用此类 intent 的另一个好处是,可以在不开启 MediaBrowser 会话的情况下执行此类简单操作。

    在使用这些 intent 解决的问题中,服务发现实际更为重要。通过这种方式,服务发现的过程简单而明确(请参阅发现和服务连接)。

    为了简化某些客户端实现,可以采用另一种方式来发出此类“播放”命令(也必须由电台应用实现):使用根节点的 rootId(用作 mediaId)发出 playFromMediaId。虽然根节点是不可播放的,但它的 rootId 是一个任意字符串,可作为 mediaId 使用。但是,客户端不需要了解这种细微差别。

    ProgramSelector

    虽然使用 mediaId 足以从 MediaBrowserService 中选择频道,但它会绑定到一个会话,并且不能在各提供程序之间保持一致。在某些情况下,客户端可能需要一个绝对指针(如绝对频率),以便在会话和设备之间保持 mediaId 不变。

    在数字电台广播时代,单靠频率不足以调到特定电台。因此,请使用 ProgramSelector 调到模拟频道或数字频道。ProgramSelector 由两部分组成:

    • 主要标识符。给定电台的唯一且稳定的标识符,它不会改变,但可能不足以调到该电台。例如,RDS PI 代码,在美国可以转换为呼号。
    • 辅助标识符。用于调到该电台的其他标识符(例如频率),可能包括来自其他电台技术的标识符。例如,DAB 电台可能具有模拟广播回退机制。

    为了使 ProgramSelector 适合于基于 MediaBrowserMediaSession 的解决方案,请定义一个 URI 架构来对其进行序列化。架构定义如下:

    broadcastradio://program/<primary ID type>/<primary ID>?
    <secondary ID type>=<secondary ID>&<secondary ID type>=<secondary ID>

    在此示例中,辅助标识符部分(位于问号 [?] 之后)是可选的,可以将其移除以提供稳定的标识符作为 mediaId 使用。例如:

    • broadcastradio://program/RDS_PI/1234?AMFM_FREQUENCY=88500&AMFM_FREQUENCY=103300
    • broadcastradio://program/AMFM_FREQUENCY/102100
    • broadcastradio://program/DAB_SID_EXT/14895264?RDS_PI=1234

    program 的 authority 部分(也就是 host)为将来扩展该架构提供了一些空间。标识符类型字符串在 IdentifierType 的 HAL 2.x 定义中被精确指定为其名称,值的格式为十进制或十六进制(带有 0x 前缀)数字。

    所有供应商专用标识符均由 VENDOR_ 前缀表示。例如,VENDOR_0 对应 VENDOR_STARTVENDOR_1 对应 VENDOR_START 加 1。此类 URI 只能在生成它们的电台硬件上使用,不能在其他 OEM 制造的设备之间传输。

    必须将这些 URI 分配给顶层电台文件夹下的每个 MediaItem。此外,MediaSession 必须同时支持 playFromMediaIdplayFromUri。但是,URI 主要用于电台元数据提取(例如 FM 频率)和永久性存储。不能保证 URI 可用于所有媒体内容(例如,当框架尚不支持主要 ID 类型时)。另一方面,媒体 ID 始终有效。不建议客户端使用 URI 从当前 MediaBrowser 会话中选择项目。相反,应使用 playFromMediaId。也就是说,它对于提供应用来说不是可选的,并且在合理情况下会保留缺失的 URI。

    最初的设计是使用一个冒号来代替 :// 序列,放在架构部分后面。但是,为实现绝对分层 URI 引用,android.net.Uri 不支持前者。

    其他来源类型

    其他音频来源也可以通过类似方式处理。例如,辅助输入和音频 CD 播放器。

    单个应用可以提供多种类型的来源。在这类情况下,建议为每种来源创建一个单独的 MediaBrowserService。即使在设置中提供了多个来源/MediaBrowserService,也强烈建议在一个应用中只使用一个 MediaSession。

    音频 CD

    与音频 CD 类似,提供此类磁盘的应用将提供具有一个(或多个,如果系统有 CD 换碟机)可浏览条目的 MediaBrowser,该 MediaBrowser 将包含给定 CD 的所有曲目。如果系统未读取到每张 CD 上的曲目(例如,一次性将所有磁盘插入磁盘盒,而系统未读取到所有磁盘的内容),则整个磁盘的 MediaItem 将只是 PLAYABLE,而不是 BROWSABLEPLAYABLE。如果指定槽位中没有磁盘,则相关内容既非 PLAYABLE,也非 BROWSABLE(但每个槽位必须始终存在于树中。)

    音频 CD 树结构
    图 3. 音频 CD 树结构

    这些条目将以类似于广播电台文件夹的方式进行标记;它们将包含 MediaDescription API 中定义的其他 extra 字段:

    • EXTRA_CD_TRACK:对于音频 CD 上的每个 MediaItem,从 1 开始的曲目编号。
    • EXTRA_CD_DISK:从 1 开始的磁盘编号。

    对于启用 CD-Text 的系统和兼容磁盘,顶层 MediaItem 将具有磁盘的标题。类似地,曲目的 MediaItem 将具有曲目的标题。

    辅助输入

    提供辅助输入的应用将提供一个 MediaBrowser 树,其中包含单个(或多个,如果存在多个端口)表示端口中的 AUX 的条目。相应的 MediaSession 会接受其 mediaId,并在收到 playFromMediaId 请求后切换到该来源。

    AUX 树结构
    图 4. AUX 树结构

    每个 AUX MediaItem 条目都有一个 extra 字段 EXTRA_AUX_PORT_NAME,设置为端口的非本地化名称(不包含“AUX”字样)。例如,“AUX 1”会设置为“1”,“AUX front”会设置为“front”,“AUX”会设置为空字符串。在非英语语言区域,名称标记将保留为英文字符串。与 EXTRA_BCRADIO_BAND_NAME_EN 不同,这些值由 OEM 定义,不受预定义列表的约束。

    如果硬件可以检测到连接到 AUX 端口的设备,则硬件仅在已连接输入的情况下才应将 MediaItem 标记为 PLAYABLE。如果没有任何输入连接到此端口,则仍应列出硬件(但不为 PLAYABLE)。如果硬件没有此类功能,则 MediaItem 必须始终设置为 PLAYABLE

    extra 字段

    定义以下字段:

    • EXTRA_CD_TRACK = "android.media.extra.CD_TRACK"
    • EXTRA_CD_DISK = "android.media.extra.CD_DISK"
    • EXTRA_AUX_PORT_NAME = "android.media.extra.AUX_PORT_NAME"

    客户端需要检查顶层 MediaItem,以获取设置了 EXTRA_CD_DISKEXTRA_AUX_PORT_NAME extra 字段的元素。

    详细示例

    以下示例说明了此设计中包含的来源类型的 MediaBrowser 树结构。

    广播电台 MediaBrowserService(处理 ACTION_PLAY_BROADCASTRADIO):

    • 电台(可浏览)EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_PROGRAMS
      • BBC One(可播放)URI: broadcastradio://program/RDS_PI/1234?AMFM_FREQUENCY=90500
      • ABC 88.1(可播放)URI: broadcastradio://program/RDS_PI/5678?AMFM_FREQUENCY=88100
      • ABC 88.1 HD1(可播放)URI: broadcastradio://program/HD_STATION_ID_EXT/158241DEADBEEF?AMFM_FREQUENCY=88100&RDS_PI=5678
      • ABC 88.1 HD2(可播放)URI: broadcastradio://program/HD_STATION_ID_EXT/158242DEADBEFE
      • 90.5 FM(可播放) - 没有 RDS 的 FM URI: broadcastradio://program/AMFM_FREQUENCY/90500
      • 620 AM(可播放)URI: broadcastradio://program/AMFM_FREQUENCY/620
      • BBC One(可播放)URI:broadcastradio://program/DAB_SID_EXT/1E24102?RDS_PI=1234
    • 收藏夹(可浏览、可播放)EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_FAVORITES
      • BBC One(可播放)URI:broadcastradio://program/RDS_PI/1234?AMFM_FREQUENCY=101300
      • BBC Two(不可播放)URI:broadcastradio://program/RDS_PI/1300?AMFM_FREQUENCY=102100
    • AM(可浏览、可播放): EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_BANDEXTRA_BCRADIO_BAND_NAME_EN="AM"
      • 530 AM(可播放)URI:broadcastradio://program/AMFM_FREQUENCY/530
      • 540 AM(可播放)URI:broadcastradio://program/AMFM_FREQUENCY/540
      • 550 AM(可播放)URI:broadcastradio://program/AMFM_FREQUENCY/550
    • FM(可浏览、可播放): EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_BANDEXTRA_BCRADIO_BAND_NAME_EN="FM"
      • 87.7 FM(可播放)URI:broadcastradio://program/AMFM_FREQUENCY/87700
      • 87.9 FM(可播放)URI:broadcastradio://program/AMFM_FREQUENCY/87900
      • 88.1 FM(可播放)URI:broadcastradio://program/AMFM_FREQUENCY/88100
    • DAB(可播放):EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_BANDEXTRA_BCRADIO_BAND_NAME_EN="DAB"

    音频 CD MediaBrowserService(处理 ACTION_PLAY_AUDIOCD):

    • 磁盘 1(可播放)EXTRA_CD_DISK=1
    • 磁盘 2(可浏览、可播放)EXTRA_CD_DISK=2
      • 曲目 1(可播放)EXTRA_CD_TRACK=1
      • 曲目 2(可播放)EXTRA_CD_TRACK=2
    • 我的音乐 CD(可浏览、可播放)EXTRA_CD_DISK=3
      • All By Myself(可播放)EXTRA_CD_TRACK=1
      • Reise,Reise(可播放)EXTRA_CD_TRACK=2
    • 空槽位 4(不可播放)EXTRA_CD_DISK=4

    AUX MediaBrowserService(处理 ACTION_PLAY_AUX):

    • AUX front(可播放)EXTRA_AUX_PORT_NAME="front"
    • AUX rear(可播放)EXTRA_AUX_PORT_NAME="rear"