Google Assistant 和媒体应用

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

Assistant 使用媒体会话与 Android 媒体应用通信。它可以使用 intent服务启动您的应用并开始播放。为了获得最佳结果,您的应用应实现此页面上描述的所有功能。

使用媒体会话

每个音频和视频应用都必须实现媒体会话,以便 Assistant 在播放开始后可以操作传输控件。

请注意,尽管 Assistant 只使用本节中列出的操作,但最佳实践是实现所有准备和播放 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 Assistant 基于 URI 的操作仅适用于向 Google 提供 URI 的公司。要了解有关向 Google 描述您的媒体内容的更多信息,请参阅媒体操作

通过实现准备 API,可以减少语音命令后的播放延迟。希望改善播放延迟的媒体应用可以使用额外的时间开始缓存内容并准备媒体播放。

解析搜索查询

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

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

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

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

Android Automotive OS 和 Android Auto 中支持以下 extras

以下代码片段展示了如何在 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() 时没有搜索查询,您的媒体应用应播放“当前”媒体。如果没有当前媒体,应用应尝试播放一些内容,例如最近播放列表中的歌曲或随机队列中的歌曲。当用户在没有提供额外信息的情况下要求“在 [您的应用名称] 上播放音乐”时,Assistant 会使用这些 API。

当用户说“在 [您的应用名称] 上播放音乐”时,Android Automotive OS 或 Android Auto 会尝试启动您的应用并调用应用的 onPlayFromSearch() 方法来播放音频。但是,由于用户没有说出媒体项的名称,因此 onPlayFromSearch() 方法会收到一个空查询参数。在这些情况下,您的应用应立即播放音频,例如最近播放列表中的歌曲或随机队列中的歌曲。

声明对语音操作的旧版支持

在大多数情况下,处理上述播放操作可为您的应用提供所需的所有播放功能。但是,某些系统要求您的应用包含用于搜索的 Intent 过滤器。您应该在应用的 manifest 文件中声明对此 Intent 过滤器的支持。

在手机应用的 manifest 文件中包含此代码

<activity>
    <intent-filter>
        <action android:name=
             "android.media.action.MEDIA_PLAY_FROM_SEARCH" />
        <category android:name=
             "android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

传输控件

在您的应用媒体会话处于活动状态后,Assistant 可以发出语音命令来控制播放和更新媒体元数据。为此,您的代码应启用以下操作并实现相应的回调

操作 回调 描述
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) 打开/关闭字幕。

请注意

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

内容类型 所需操作
音乐

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

强烈建议支持:搜寻

播客

必须支持:播放、暂停、停止和搜寻

建议支持:跳到下一首和跳到上一首

有声读物 必须支持:播放、暂停、停止和搜寻
电台 必须支持:播放、暂停和停止
新闻 必须支持:播放、暂停、停止、跳到下一首和跳到上一首
视频

必须支持:播放、暂停、停止、搜寻、倒回和快进

强烈建议支持:跳到下一段和跳到上一段

您必须支持产品所允许的尽可能多的上述操作,但仍要优雅地响应任何其他操作。例如,如果只有高级用户才能返回到上一个项目,那么如果免费用户要求 Assistant 返回到上一个项目,您可能会引发错误。有关更多指导,请参阅错误处理部分

可尝试的语音查询示例

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

MediaSession 回调 要使用的“Hey Google”短语
onPlay()

“播放。”

“恢复。”

onPlayFromSearch()
onPlayFromUri()
音乐

“在(应用名称)上播放音乐或歌曲。”这是一个空查询。

“在(应用名称)上播放(歌曲 | 艺术家 | 专辑 | 流派 | 播放列表)。”

电台 “在(应用名称)上播放(频率 | 电台)。”
有声读物

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

“在(应用名称)上朗读(有声读物)。”

播客 “在(应用名称)上播放(播客)。”
onPause() “暂停。”
onStop() “停止。”
onSkipToNext() “下一(歌曲 | 剧集 | 曲目)。”
onSkipToPrevious() “上一(歌曲 | 剧集 | 曲目)。”
onSeekTo()

“重新开始。”

“快进##秒。”

“后退##分钟。”

不适用(保持您的 MediaMetadata 更新) “正在播放什么?”

错误

Assistant 会处理媒体会话中发生的错误,并将其报告给用户。请确保您的媒体会话在 PlaybackState 中正确更新传输状态和错误代码,如使用媒体会话中所述。Assistant 识别 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 不变。
  • 用户请求的内容没有完全匹配。例如,免费用户请求只有高级用户才能使用的内容。
    • 我们建议您不要返回错误,而应优先查找类似的内容进行播放。在播放开始之前,Assistant 会处理说出最相关的语音响应。

通过 intent 播放

Assistant 可以通过发送带有深层链接的 intent 来启动音频或视频应用并开始播放。

intent 及其深层链接可以来自不同的来源

  • 当 Assistant 启动移动应用时,它可以利用 Google 搜索检索已标记的内容,这些内容提供带有链接的观看操作
  • 当 Assistant 启动电视应用时,您的应用应包含一个电视搜索提供程序来公开媒体内容的 URI。Assistant 会向内容提供程序发送查询,内容提供程序应返回一个包含深层链接 URI 和可选操作的 intent。如果查询在 intent 中返回一个操作,Assistant 会将该操作和 URI 发送回您的应用。如果提供程序未指定操作,Assistant 将向 Intent 添加ACTION_VIEW

Assistant 会在发送到您应用的 intent 中添加值为 true 的额外参数 EXTRA_START_PLAYBACK。您的应用在收到带有 EXTRA_START_PLAYBACK 的 intent 时应开始播放。

在活动时处理 intent

用户可以在您的应用仍在播放先前请求的内容时,要求 Assistant 播放其他内容。这意味着您的应用可以在其播放 Activity 已经启动并活动时,接收新的 intent 以开始播放。

支持带有深层链接 intent 的 Activity 应该重写 onNewIntent() 来处理新请求。

开始播放时,Assistant 可能会向其发送给您应用的 intent 添加附加标志。特别是,它可能会添加 FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK 或两者。尽管您的代码不需要处理这些标志,但 Android 系统会响应它们。这可能会影响您的应用在第二个播放请求(带有新 URI)到达而前一个 URI 仍在播放时的行为。最好测试您的应用在这种情况下如何响应。您可以使用 adb 命令行工具来模拟这种情况(常量 0x14000000 是这两个标志的布尔位或)

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

从服务播放

如果您的应用有一个允许 Assistant 连接的 媒体浏览器服务,Assistant 可以通过与该服务的 媒体会话通信来启动应用。媒体浏览器服务不应启动 Activity。Assistant 将根据您使用 setSessionActivity() 定义的 PendingIntent 启动您的 Activity。

初始化媒体浏览器服务时,请务必设置 MediaSession.Token。请记住始终设置支持的播放操作,包括在初始化期间。Assistant 希望您的媒体应用在 Assistant 发送第一个播放命令之前设置播放操作。

要从服务启动,Assistant 会实现媒体浏览器客户端 API。它会执行 TransportControls 调用,这些调用会在您的应用的媒体会话上触发 PLAY 操作回调。

下图显示了 Assistant 生成的调用顺序和相应的媒体会话回调。(准备回调仅在您的应用支持它们时才发送。)所有调用都是异步的。Assistant 不会等待您应用的任何响应。

Starting playback with a media session

当用户发出语音命令播放时,Assistant 会以简短的通知进行响应。通知完成后,Assistant 会立即发出 PLAY 操作。它不会等待任何特定的播放状态。

如果您的应用支持 ACTION_PREPARE_* 操作,Assistant 会在开始通知之前调用 PREPARE 操作。

连接到 MediaBrowserService

为了使用服务启动您的应用,Assistant 必须能够连接到应用的 MediaBrowserService 并检索其 MediaSession.Token。连接请求在服务的 onGetRoot() 方法中处理。有两种处理请求的方法

  • 接受所有连接请求
  • 仅接受来自 Assistant 应用的连接请求

接受所有连接请求

您必须返回一个 BrowserRoot 以允许 Assistant 向您的媒体会话发送命令。最简单的方法是允许所有 MediaBrowser 应用连接到您的 MediaBrowserService。您必须返回一个非空的 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...
}

接受 Assistant 应用包和签名

您可以通过检查 Assistant 的包名和签名来明确允许 Assistant 连接到您的媒体浏览器服务。您的应用将在 MediaBrowserService 的 onGetRoot 方法中收到包名。您必须返回一个 BrowserRoot 以允许 Assistant 向您的媒体会话发送命令。通用音乐播放器示例维护了一个已知包名和签名的列表。以下是 Google Assistant 使用的包名和签名。

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