Google 助理和媒体应用

借助 Google 助理,您可以使用语音指令控制许多设备,例如 Google Home、手机等。它内置了理解媒体命令(“播放碧昂丝的歌曲”)并支持媒体控件(例如暂停、跳过、快进、点赞)的功能。

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()

(*) 基于 URI 的 Google 助理操作仅适用于向 Google 提供 URI 的公司。如需详细了解如何向 Google 描述您的媒体内容,请参阅媒体操作

通过实现准备 API,可以缩短发出语音指令后的播放延迟。希望缩短播放延迟时间的媒体应用可以利用额外的时间来开始缓存内容并准备媒体播放。

解析搜索查询

当用户搜索特定媒体项(例如“在 [您的应用名称] 上播放爵士乐”或“聆听 [歌曲名称]”)时,onPrepareFromSearch()onPlayFromSearch() 回调方法会收到一个查询参数和一个 extra 包。

您的应用应按照以下步骤解析语音搜索查询并开始播放:

  1. 使用语音搜索返回的 extra 包和搜索查询字符串来过滤结果。
  2. 根据这些结果构建一个播放队列。
  3. 从结果中播放最相关的媒体项。

onPlayFromSearch() 方法接受 extras 参数,其中包含来自语音搜索的更多详细信息。这些 extra 可帮助您在应用中找到要播放的音频内容。 如果搜索结果无法提供此数据,您可以实现相应的逻辑来解析原始搜索查询,并根据查询播放相应的曲目。

Android Automotive OS 和 Android Auto 支持以下 extra:

以下代码段展示了如何替换 MediaSession.Callback 实现中的 onPlayFromSearch() 方法,以解析语音搜索查询并开始播放:

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(),您的媒体应用应播放“当前”媒体。如果没有当前媒体,应用应尝试播放一些内容,例如最近的播放列表或随机队列中的歌曲。当用户请求“在 [您的应用名称] 上播放音乐”而不提供额外信息时,智能助理会使用这些 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) 开启/关闭字幕。

请注意:

你支持的语音操作可能会因内容类型而异。

内容类型 所需操作
音乐

必须支持:播放、暂停、停止、跳到下一个和跳到上一个

强烈建议支持:跳转至

播客

必须支持:播放、暂停、停止和跳转

建议支持:“跳至下一个”和“跳至上一个”

有声读物 必须支持:播放、暂停、停止和跳转
收音机 必须支持:播放、暂停和停止
新闻 必须支持:播放、暂停、停止、跳到下一个和跳到上一个
视频

必须支持:播放、暂停、停止、跳转、快退和快进

强烈建议支持:“跳至下一个”和“跳到上一个”

您必须在产品允许的范围内支持上面列出的许多操作,同时仍能优雅地响应任何其他操作。例如,如果只有高级用户能够返回到上一项内容,而免费层级用户要求 Google 助理返回上一项内容,您可能会引发错误。请参阅“错误处理”部分,获取更多指导。

可尝试的语音查询示例

下表概述了您在测试实现时应使用的一些示例查询:

MediaSession 回调 要使用的“Hey Google”指令
onPlay()

“播放。”

“继续。”

onPlayFromSearch()
onPlayFromUri()
音乐

“用(应用名称)播放音乐或歌曲。”查询内容为空。

“在(应用名称)<br class="ph-3">上播放(歌曲 | 音乐人 | 专辑 | 流派 | 播放列表)

电台 “在(应用名称)上播放(频率 | 电台)”。
Audiobook

“使用(应用名称)朗读我的有声读物。”

“在‘(app name)’上阅读(有声读物)”。

播客 “在(应用名称)上播放(播客)。”
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
    • 设置 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 助理向 content provider 发送查询,该查询应返回一个 intent,其中包含深层链接的 URI 和可选操作。如果查询在 intent 中返回操作,Google 助理会将该操作和 URI 发送回您的应用。如果提供程序未指定操作,Google 助理会将 ACTION_VIEW 添加到 intent。

Google 助理将值为 true 的额外 EXTRA_START_PLAYBACK 添加到其发送给应用的 intent。您的应用应在收到包含 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 助理将根据您使用 setSessionActivity() 定义的 PendingIntent 启动您的 activity。

请务必在初始化媒体浏览器服务时设置 MediaSession.Token。请务必在任何时候设置支持的播放操作,包括在初始化期间。Google 助理希望您的媒体应用在 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 助理向您的媒体会话发送命令。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>