电台控制实现

电台控制的实现基于 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" />

此 widget 会启动 AppSelectionFragment,以显示可切换到的媒体来源列表。除了提供的界面之外,如果还需要其他界面,则可以创建自定义 widget,以在应显示切换器时启动 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,请为给定服务组件create 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"