利用 Google 助理,您可以使用语音指令控制许多设备,例如 Google Home、手机等。Google 助理有内置功能可以理解媒体指令(“播放碧昂斯的歌曲”)并支持媒体控件(如暂停、跳过、快进、点赞)。
Google 助理使用媒体 会话与 Android 媒体应用通信。它可以使用 Intent或服务来 启动您的应用并开始播放。为了获得最佳效果,您的应用应实现此页面上描述的所有功能。
使用媒体会话
每个音频和视频应用都必须实现一 个媒体会话 ,以便 Google 助理可以在播放开始后操作 传输控件。
请注意,尽管 Google 助理仅使用本部分中列出的操作,但最佳做法是实现所有准备和播放 API,以确保与其他应用兼容。对于您不支持的任何操作,
媒体会话回调只需使用
ERROR_CODE_NOT_SUPPORTED 返回错误即可。
要启用媒体和传输控件,可以在应用的 MediaSession 对象中设置以下标记:
Kotlin
session.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS )
Java
session.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
应用的媒体会话必须声明其支持的操作,并实现相应的媒体会话回调。在
setActions()中声明您支持的操作。
例如, Universal Android Music Player 项目示例就很好地说明了如何设置媒体会话。
播放操作
为了从服务进行播放,媒体会话必须包含以下PLAY操作及其回调:
| 操作 | 回调 |
|---|---|
ACTION_PLAY |
onPlay() |
ACTION_PLAY_FROM_SEARCH |
onPlayFromSearch() |
ACTION_PLAY_FROM_URI (*) |
onPlayFromUri() |
您的会话还应实现以下 PREPARE 操作及其回调:
| 操作 | 回调 |
|---|---|
ACTION_PREPARE |
onPrepare() |
ACTION_PREPARE_FROM_SEARCH |
onPrepareFromSearch() |
ACTION_PREPARE_FROM_URI (*) |
onPrepareFromUri() |
(*) Google 助理基于 URI 的操作仅适用于向 Google 提供 URI 的公司。要详细了解如何向 Google 描述您的媒体内容,请参阅媒体操作 。
通过实现准备 API,可以改善发出语音指令后的播放延迟。想要改善播放延迟的媒体应用,可以使用额外的时间来开始缓存内容和准备媒体播放。
解析搜索查询
当用户搜索特定的媒体项(例如“在
[您的应用名称]”上播放爵士乐”或“听 [歌曲名称]”)时,
onPrepareFromSearch()或
onPlayFromSearch()
回调方法会收到查询参数和 extra bundle。
您的应用应按照以下步骤解析语音搜索查询并开始播放:
- 使用语音搜索返回的 extra bundle 和搜索查询字符串来过滤结果。
- 根据这些结果构建一个播放队列。
- 从结果中播放最相关的媒体项。
The onPlayFromSearch()
方法采用 extra 参数,该参数包含了来自语音
搜索的更详细的信息。这些 extra
可帮助您在应用中找到要播放的音频内容。
如果搜索结果无法提供此数据,则可以实现相应的逻辑来解析原始搜索查询并根据查询来播放适当的曲目。
Android Automotive OS 和 Android Auto 支持以下 extras:
以下代码段展示了如何替换 onPlayFromSearch()
方法在您的 MediaSession.Callback
实现来解析语音搜索查询并开始播放:
Kotlin
override fun onPlayFromSearch(query: String?, extras: Bundle?) { if (query.isNullOrEmpty()) { // The user provided generic string e.g. 'Play music' // Build appropriate playlist queue } else { // Build a queue based on songs that match "query" or "extras" param val mediaFocus: String? = extras?.getString(MediaStore.EXTRA_MEDIA_FOCUS) if (mediaFocus == MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE) { isArtistFocus = true artist = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST) } else if (mediaFocus == MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE) { isAlbumFocus = true album = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM) } // Implement additional "extras" param filtering } // Implement your logic to retrieve the queue var result: String? = when { isArtistFocus -> artist?.also { searchMusicByArtist(it) } isAlbumFocus -> album?.also { searchMusicByAlbum(it) } else -> null } result = result ?: run { // No focus found, search by query for song title query?.also { searchMusicBySongTitle(it) } } if (result?.isNotEmpty() == true) { // Immediately start playing from the beginning of the search results // Implement your logic to start playing music playMusic(result) } else { // Handle no queue found. Stop playing if the app // is currently playing a song } }
Java
@Override public void onPlayFromSearch(String query, Bundle extras) { if (TextUtils.isEmpty(query)) { // The user provided generic string e.g. 'Play music' // Build appropriate playlist queue } else { // Build a queue based on songs that match "query" or "extras" param String mediaFocus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS); if (TextUtils.equals(mediaFocus, MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE)) { isArtistFocus = true; artist = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST); } else if (TextUtils.equals(mediaFocus, MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE)) { isAlbumFocus = true; album = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM); } // Implement additional "extras" param filtering } // Implement your logic to retrieve the queue if (isArtistFocus) { result = searchMusicByArtist(artist); } else if (isAlbumFocus) { result = searchMusicByAlbum(album); } if (result == null) { // No focus found, search by query for song title result = searchMusicBySongTitle(query); } if (result != null && !result.isEmpty()) { // Immediately start playing from the beginning of the search results // Implement your logic to start playing music playMusic(result); } else { // Handle no queue found. Stop playing if the app // is currently playing a song } }
如需通过更详细的示例了解如何在应用中实现语音搜索来播放音频 内容,请参见 Universal Android Music Player 示例。
处理空查询
如果在没有搜索查询的情况下调用 onPrepare()、onPlay()、onPrepareFromSearch() 或 onPlayFromSearch()
,您的媒体应用应播放“当前”
媒体。如果没有当前媒体,应用应尝试播放一些内容,例如,播放最近播放列表或随机队列中的歌曲。当用户要求“在 [您的应用名称]上播放音乐”而没有提供额外的信息时,Google
助理就会使用这些 API。
当用户说“在 [您的应用名称]上播放音乐”时,Android Automotive OS 或
Android Auto 会尝试启动您的应用,并通过调用应用的onPlayFromSearch()
方法来并播放音频。但是,由于用户没有说出媒体项的名称,onPlayFromSearch()
方法会收到空的查询参数。在这些情况下,您的应用应该立即播放音频,比如最近的播放列表或随机队列中的歌曲。
声明对语音操作的旧版支持
在大多数情况下,处理上述播放操作可使应用具备所需的全部播放功能。不过,某些系统还会要求应用包含针对搜索的 Intent 过滤器。您应该在应用的清单文件中声明对此 Intent 过滤器的支持。
将以下代码包含在手机应用的清单文件中:
<activity>
<intent-filter>
<action android:name=
"android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<category android:name=
"android.intent.category.DEFAULT" />
</intent-filter>
</activity>
传输控件
应用的媒体会话激活后,Google 助理就可以发出语音指令来控制播放和更新媒体元数据。为了实现此功能,您的代码应启用以下操作并实现相应的回调:
| 操作 | 回调 | 说明 |
|---|---|---|
ACTION_SKIP_TO_NEXT |
onSkipToNext() |
下一个视频 |
ACTION_SKIP_TO_PREVIOUS |
onSkipToPrevious() |
上一曲 |
ACTION_PAUSE, ACTION_PLAY_PAUSE |
onPause() |
暂停 |
ACTION_STOP |
onStop() |
停止 |
ACTION_PLAY |
onPlay() |
继续 |
ACTION_SEEK_TO |
onSeekTo() |
快退 30 秒 |
ACTION_SET_RATING |
onSetRating(android.support.v4.media.RatingCompat) |
喜欢/不喜欢。 |
ACTION_SET_CAPTIONING_ENABLED |
onSetCaptioningEnabled(boolean) |
开启/关闭字幕。 |
请注意:
- 要使跳转命令起作用,
PlaybackState需要与state, position, playback speed, and update time保持同步。当状态改变时,应用必须调用setPlaybackState()。 - 媒体应用还必须使媒体会话元数据保持最新状态,以便回答“现在播放的是什么歌曲?”这样的问题。应用必须在相关字段(如曲目名称、音乐人和名称)发生变化时调用
setMetadata()。 - 必须设置
MediaSession.setRatingType()来指示应用支持的评分类型,并且应用必须实现onSetRating()。如果应用不支持评分,则应将评分类型设置为RATING_NONE。
您支持的语音操作可能会因内容类型而异。
| 内容类型 | 所需操作 |
|---|---|
| 音乐 |
必须支持:播放、暂停、停止、跳至下一首和跳至上一首 强烈建议支持:跳转到 |
| 播客 |
必须支持:播放、暂停、停止和跳转到 建议支持:跳至下一首和跳至上一首 |
| 有声读物 | 必须支持:播放、暂停、停止和跳转到 |
| 电台 | 必须支持:播放、暂停和停止 |
| 新闻 | 必须支持:播放、暂停、停止、跳至下一首和跳至上一首 |
| 视频 |
必须支持:播放、暂停、停止、跳转到、快退和快进 强烈建议支持:跳至下一首和跳至上一首 |
您必须支持尽可能多的上述操作,但仍需妥善响应任何其他操作。例如,如果只有高级用户才能返回到上一个项目,那么当免费用户要求 Google 助理返回到上一个项目时,您可能会引发错误。 如需更多指导,请参阅错误处理部分。
尝试使用示例语音查询
下表列出了一些示例查询,您在测试实现时应使用这些查询:
| MediaSession 回调 | 要使用的“Hey Google”短语 | |
|---|---|---|
onPlay() |
“播放。” “继续。” |
|
onPlayFromSearch()
onPlayFromUri() |
音乐 |
“在(应用名称) 上播放音乐或歌曲。”这是一个空查询。 “在(应用名称) 上播放(歌曲 | 音乐人 | 专辑 | 类型 | 播放列表) 。” |
| 电台 | “在(应用名称) 上播放(频率 | 电台) 。” | |
| 有声读物 |
“在(应用名称) 上阅读我的有声读物。” “在(应用名称) 上阅读(有声读物) 。” |
|
| 播客 | “在(应用名称) 上播放(播客) 。” | |
onPause() |
“暂停。” | |
onStop() |
“停止。” | |
onSkipToNext() |
“下一首(歌曲 | 剧集 | 曲目) 。” | |
onSkipToPrevious() |
“上一首(歌曲 | 剧集 | 曲目) 。” | |
onSeekTo() |
“重新开始。” “快进 ## 秒。” “返回 ## 分钟。” |
|
不适用(保持
MediaMetadata
更新) |
“正在播放什么?” | |
错误
Google 助理会处理媒体会话中出现的错误并向用户报告。请确保您的媒体会话在其 PlaybackState 中正确更新传输状态和
错误代码,如使用媒体会话中所述。Google 助理
可以识别
getErrorCode()返回的所有错误代码。
常见处理不当的情况
以下是一些错误情况示例,您应确保正确处理这些情况:
- 用户需要登录
- 将
PlaybackState错误代码设置为ERROR_CODE_AUTHENTICATION_EXPIRED。 - 设置
PlaybackState错误消息。 - 如果播放需要,请将
PlaybackState状态设置为STATE_ERROR,否则使PlaybackState的其余部分保持不变。
- 将
- 用户请求不可用的操作
- 相应地设置
PlaybackState错误代码。例如,如果操作不受支持,请将PlaybackState设置为ERROR_CODE_NOT_SUPPORTED;如果操作受登录保护,请将ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED设置为ERROR_CODE_NOT_SUPPORTED。 - 设置
PlaybackState错误消息。 - 使
PlaybackState的其余部分保持不变。
- 相应地设置
- 用户请求应用中没有的内容
- 相应地设置
PlaybackState错误代码。例如,使用ERROR_CODE_NOT_AVAILABLE_IN_REGION。 - 设置
PlaybackState错误消息。 - 将
PlaybackSate状态设置为STATE_ERROR以中断播放,否则使PlaybackState的其余部分保持不变。
- 相应地设置
- 用户请求的内容没有完全匹配项。例如,免费用户请求的内容仅适用于高级用户。
- 我们建议您不要返回错误,而应优先查找类似的内容进行播放。Google 助理会在播放开始前处理最相关的语音响应。
通过 Intent 进行播放
Google 助理可以通过发送包含 深层链接的 Intent 来启动音频或视频应用并开始播放。
Intent 与其深层链接可以来自不同的来源:
- 当 Google 助理 启动移动应用时,它可以使用 Google 搜索来检索为 观看操作提供链接的标记内容。
- 当 Google 助理启动 TV 应用时,您的应用应包含
TV 搜索提供器
以提供媒体内容的 URI。Google 助理向内容提供器发送查询,该查询应返回一个 Intent,其中包含深层链接的 URI 和一个可选操作。
如果查询在 intent 中返回了操作,Google 助理会将该操作和 URI 发回给您的应用。如果提供方未指定操作,Google 助理会在 Intent 中添加
ACTION_VIEW。
Google 助理会在发送给您应用的 Intent 中添加值为 true 的 extra EXTRA_START_PLAYBACK。当您的应用收到包含 EXTRA_START_PLAYBACK 的 Intent 时,就应该开始播放。
在处于活动状态时处理 Intent
用户可以在您的应用仍在播放之前请求的内容时,要求 Google 助理播放一些内容。这意味着您的应用会在其播放 Activity 已启动并处于活动状态时收到要求开始播放的新 Intent。
支持包含深层链接的 Intent 的 Activity 应替换 onNewIntent() 来处理新请求。
开始播放时,Google 助理可能会在发送给您应用的 Intent 中添加其他
标记
。具体而言,它可能会添加
FLAG_ACTIVITY_CLEAR_TOP 或
FLAG_ACTIVITY_NEW_TASK。尽管您的代码不需要处理这些标记,但
Android 系统会对这些标记做出响应。
这在以下情况下可能会影响应用的行为:当应用收到第二个包含新 URI 的播放请求时,前一个 URI
仍在播放。建议您测试一下应用在这种情况下如何响应。您可以使用 adb 命令行工具来模拟这种情况(常量
0x14000000 是两个标记的按位布尔值 OR):
adb shell 'am start -a android.intent.action.VIEW --ez android.intent.extra.START_PLAYBACK true -d "<first_uri>"' -f 0x14000000
adb shell 'am start -a android.intent.action.VIEW --ez android.intent.extra.START_PLAYBACK true -d "<second_uri>"' -f 0x14000000
从服务进行播放
如果您的应用具有允许 Google 助理进行连接的
media browser service
,
Google 助理就可以通过与该服务的
media session进行通信来启动应用。
该媒体浏览器服务永不应该启动 Activity。
Google 助理将根据您定义
的 PendingIntent来启动您的 Activitywith setSessionActivity()。
请务必在初始化媒体浏览器服务时设置 MediaSession.Token。请确保在任何时候都要设置受支持的播放操作 ,包括在初始化期间。Google 助理希望您的媒体应用在其发送第一个播放命令之前设置播放操作。
为了从服务启动播放,Google 助理会实现媒体浏览器客户端 API。 它会执行 TransportControls 调用,以在应用的媒体会话上触发 PLAY 操作回调。
下图显示了 Google 助理生成的调用的顺序以及相应的媒体会话回调。(准备回调仅在您的应用支持时才会发送。) 所有调用都是异步的。Google 助理不会等待您的应用做出任何响应。

当用户发出要求播放的语音指令时,Google 助理会发出简短的通知来做出响应。 通知完成后,Google 助理就会发出 PLAY 操作。它不会等待任何特定的播放状态。
如果您的应用支持 ACTION_PREPARE_* 操作,则 Google 助理会在发出通知之前调用 PREPARE 操作。
连接到 MediaBrowserService
为了使用服务来启动您的应用,Google 助理必须能够连接到应用的 MediaBrowserService 并检索其
MediaSession.Token。连接请求在服务的 onGetRoot() 方法中处理。有两种方式来处理请求:
- 接受所有连接请求
- 仅接受来自 Google 助理应用的连接请求
接受所有连接请求
您必须返回 BrowserRoot,才能允许 Google 助理向您的媒体会话发送命令。最简单的方式是允许所有 MediaBrowser 应用连接到您的 MediaBrowserService。您必须返回非 null 的 BrowserRoot。以下是来自Universal Music Player的相关代码:
Kotlin
override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): BrowserRoot? { // To ensure you are not allowing any arbitrary app to browse your app's contents, you // need to check the origin: if (!packageValidator.isCallerAllowed(this, clientPackageName, clientUid)) { // If the request comes from an untrusted package, return an empty browser root. // If you return null, then the media browser will not be able to connect and // no further calls will be made to other media browsing methods. Log.i(TAG, "OnGetRoot: Browsing NOT ALLOWED for unknown caller. Returning empty " + "browser root so all apps can use MediaController. $clientPackageName") return MediaBrowserServiceCompat.BrowserRoot(MEDIA_ID_EMPTY_ROOT, null) } // Return browser roots for browsing... }
Java
@Override public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) { // To ensure you are not allowing any arbitrary app to browse your app's contents, you // need to check the origin: if (!packageValidator.isCallerAllowed(this, clientPackageName, clientUid)) { // If the request comes from an untrusted package, return an empty browser root. // If you return null, then the media browser will not be able to connect and // no further calls will be made to other media browsing methods. LogHelper.i(TAG, "OnGetRoot: Browsing NOT ALLOWED for unknown caller. " + "Returning empty browser root so all apps can use MediaController." + clientPackageName); return new MediaBrowserServiceCompat.BrowserRoot(MEDIA_ID_EMPTY_ROOT, null); } // Return browser roots for browsing... }
接受 Google 助理应用包和签名
您可以通过检查 Google 助理的软件包名称和签名来明确允许其连接到您的媒体浏览器服务。您的应用会在 MediaBrowserService 的 onGetRoot 方法中收到软件包名称。您必须返回 BrowserRoot,才能允许 Google 助理向您的媒体会话发送命令。The Universal Music Player 示例维护了一个已知软件包名称和签名的列表。以下是 Google 助理使用的软件包名称和签名。
<signature name="Google" package="com.google.android.googlequicksearchbox">
<key release="false">19:75:b2:f1:71:77:bc:89:a5:df:f3:1f:9e:64:a6:ca:e2:81:a5:3d:c1:d1:d5:9b:1d:14:7f:e1:c8:2a:fa:00</key>
<key release="true">f0:fd:6c:5b:41:0f:25:cb:25:c3:b5:33:46:c8:97:2f:ae:30:f8:ee:74:11:df:91:04:80:ad:6b:2d:60:db:83</key>
</signature>
<signature name="Google Assistant on Android Automotive OS" package="com.google.android.carassistant">
<key release="false">17:E2:81:11:06:2F:97:A8:60:79:7A:83:70:5B:F8:2C:7C:C0:29:35:56:6D:46:22:BC:4E:CF:EE:1B:EB:F8:15</key>
<key release="true">74:B6:FB:F7:10:E8:D9:0D:44:D3:40:12:58:89:B4:23:06:A6:2C:43:79:D0:E5:A6:62:20:E3:A6:8A:BF:90:E2</key>
</signature>