电台控制的实现基于 MediaSession
和 MediaBrowse
,媒体和语音助理应用可以利用这两个方法实现对电台的控制。如需了解详情,请参阅 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
)接口提供了适用于当前正在播放的电台节目的控制机制:
onPlay
、onStop
。将电台播放(取消)静音。onPause
。时移暂停(如果支持)。onPlayFromMediaId
。播放顶层文件夹中的任何内容。例如“播放 FM”或“播放电台”。onPlayFromUri
。播放特定频率。例如,“播放 88.5 FM”。onSkipToNext
、onSkipToPrevious
。调到下一个或上一个电台。onSetRating
。向收藏夹中添加或从收藏夹中移除。
MediaBrowser 在三种类型的顶层目录上提供可收听的 MediaItem:
- (可选)节目(电台)。这种模式通常供双调谐器电台使用,以指示用户位置的所有可用的可收听电台。
- 收藏夹。添加到“收藏夹”列表的电台节目,有些节目可能无法收到(超出接收范围)。
- 频段频道。当前区域中所有可实际收到的频道(例如 87.9、88.1、88.3、88.5、88.7、88.9、89.1 等)。每个频段都有一个单独的顶层目录。
![MediaBrowserService 树结构](https://source.android.google.cn/static/docs/devices/automotive/images/radio_01.png?hl=zh-tw)
其中每个文件夹 (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 MHz 至 108.0 MHz 的频率范围内(间隔为 0.2MHz),有 101 个 FM 频道;在 530 kHz 至 1700 kHz 的频率范围内(间隔为 10kHz),有 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 与找到的节目的 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 树不会包含特定于电台的标记。因此,如果客户端想要检查给定来源在设备上是否可用,其必须:- 发现电台服务(为
ACTION_PLAY_BROADCASTRADIO
调用resolveService
)。 - 为该服务创建
MediaBrowser
,然后连接到该 MediaBrowser。 - 使用
EXTRA_BCRADIO_FOLDER_TYPE
extra 确定MediaItem
是否存在。
注意:在大多数情况下,客户端必须扫描所有可用的 MediaBrowser 树,以检测可在给定设备上使用的所有来源。
频段名称
频段列表由一组顶层目录表示,文件夹类型标记设置为
BCRADIO_FOLDER_TYPE_BAND
。其MediaItem
的标题是表示频段名称的本地化字符串。在大多数情况下,这与英语译文相同,但客户端不能依赖该假设。为提供查找特定频段的可靠机制,为频段文件夹添加了一个 extra 标记:
EXTRA_BCRADIO_BAND_NAME_EN
。该标记是频段的非本地化名称,只能接受以下某个预定义值:AM
FM
DAB
SXM
如果频段不在此列表中,则不应设置相应的频段名称标记。而如果频段在列表中,则必须设置标记。HD 电台不会列出单独的频段,因为它使用与 AM/FM 相同的底层媒体。
常规播放 intent
每个专用于播放给定来源(例如电台或 CD)的应用都必须处理一个常规播放 intent,才能开始播放一些可能来自非活动状态的内容(例如,启动后)。如何选择要播放的内容由应用决定,但通常会选择最近播放过的电台节目或 CD 曲目。
每个音频来源都定义了一个单独的 intent:android.car.intent.action.PLAY_BROADCASTRADIO
android.car.intent.action.PLAY_AUDIOCD
:CD-DA 或 CD-Textandroid.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 中选择频道,但它会绑定到一个会话,并且在各 provider 之间不一致。在某些情况下,客户端可能需要一个绝对指针(如绝对频率)以在会话和设备之间保持 mediaId 不变。
在数字电台广播时代,单靠频率不足以调到特定电台。因此,请使用
ProgramSelector
调到模拟频道或数字频道。ProgramSelector
由两部分组成:- 主要标识符。给定电台的唯一且稳定的标识符,它不会改变,但可能不足以调到该电台。例如,RDS PI 代码,在美国可以转换为呼号。
- 辅助标识符。用于调到该电台的其他标识符(例如频率),可能包括来自其他电台技术的标识符。例如,DAB 电台可能具有模拟广播回退。
如需使 ProgramSelector 适合于基于 MediaBrowser/MediaSession 的解决方案,请定义一个 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=103300broadcastradio://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_START
,VENDOR_1
对应VENDOR_START + 1
。此类 URI 只能在生成它们的电台硬件上使用,不能在其他 OEM 制造的设备之间转移。必须将这些 URI 分配给顶层电台文件夹下的每个 MediaItem。此外,MediaSession 必须同时支持
playFromMediaId
和playFromUri
。但是,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
,而不是BROWSABLE+PLAYABLE
。如果指定槽位中没有磁盘,则相关内容既非PLAYABLE
,也非BROWSABLE
(但每个槽位必须始终存在于树中。)图 2. 音频 CD 树结构 这些条目将以类似于广播电台文件夹的方式进行标记,它们将包含 MediaDescription API 中定义的其他 extra 字段:
EXTRA_CD_TRACK
:对于音频 CD 上的每个MediaItem
,从 1 开始的曲目编号。EXTRA_CD_DISK
:从 1 开始的磁盘编号。
对于启用 CD-Text 的系统和兼容磁盘,顶层 MediaItem 将具有磁盘的标题。类似地,曲目的 MediaItem 将具有曲目的标题。
辅助输入
提供辅助输入的应用将提供一个 MediaBrowser 树,其中包含单个(或多个,如果存在多个端口)表示端口中的 AUX 的条目。相应的 MediaSession 会接受其 mediaId,并在收到
playFromMediaId
请求后切换到该来源。图 3. 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 键:
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_DISK
或EXTRA_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
- BBC One(可播放)
- 收藏夹(可浏览、可播放)
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
- BBC One(可播放)
- AM(可浏览、可播放)
EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_BAND
EXTRA_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
- 530 AM(可播放)
- FM(可浏览、可播放)
EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_BAND
EXTRA_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
- 87.7 FM(可播放)
- DAB(可播放)
EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_BAND
EXTRA_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
- 曲目 1(可播放)
- 我的音乐 CD(可浏览、可播放)
EXTRA_CD_DISK=3
- All By Myself(可播放)
EXTRA_CD_TRACK=1
- Reise, Reise(可播放)
EXTRA_CD_TRACK=2
- All By Myself(可播放)
- 空槽位 4(不可播放)
EXTRA_CD_DISK=4
AUX MediaBrowserService(处理
ACTION_PLAY_AUX
):- 前置 AUX(可播放)
EXTRA_AUX_PORT_NAME="front"
- 后置 AUX(可播放)
EXTRA_AUX_PORT_NAME="rear"