Google 助理和媒体应用

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

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

使用媒体会话

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

请注意,虽然助手只使用本节中列出的操作,但最佳实践是实现所有准备和播放 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() 中声明您支持的操作。

通用 Android 音乐播放器 示例项目是设置媒体会话的良好示例。

播放操作

为了 从服务启动播放,媒体会话必须具有这些 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() 回调方法将接收查询参数和额外信息包。

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

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

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

以下额外信息在 Android 汽车操作系统和 Android Auto 中受支持。

以下代码片段显示了如何在 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
    }
}

有关如何在应用程序中实现语音搜索以播放音频内容的更详细示例,请参阅 通用 Android 音乐播放器 示例。

处理空查询

如果 onPrepare()onPlay()onPrepareFromSearch()onPlayFromSearch() 在没有搜索查询的情况下被调用,您的媒体应用程序应播放“当前”媒体。如果没有当前媒体,应用程序应尝试播放某些内容,例如来自最近播放列表的歌曲或随机队列。

当用户说“在 [您的应用程序名称] 上播放音乐”时,Android 汽车操作系统或 Android Auto 会尝试启动您的应用程序并通过调用应用程序的 onPlayFromSearch() 方法播放音频。但是,由于用户没有说出媒体项目的名称,因此 onPlayFromSearch() 方法将接收一个空查询参数。在这种情况下,您的应用程序应立即开始播放音频,例如来自最近播放列表的歌曲或随机队列。

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

在大多数情况下,处理上面描述的播放操作会使您的应用程序拥有所需的全部播放功能。但是,某些系统要求您的应用程序包含用于搜索的意图过滤器。您应在应用程序的清单文件中声明对该意图过滤器的支持。

将此代码包含在手机应用程序的清单文件中。

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

传输控件

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

操作 回调 描述
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 需要与 状态、位置、播放速度和更新时间 保持一致。当状态发生变化时,应用程序必须调用 setPlaybackState()
  • 媒体应用程序还必须使媒体会话元数据保持最新。这支持诸如“正在播放什么歌曲?”之类的询问。当相关字段(例如曲目标题、艺术家和名称)发生变化时,应用程序必须调用 setMetadata()
  • MediaSession.setRatingType() 必须设置为指示应用程序支持的评级类型,并且应用程序必须实现 onSetRating()。如果应用程序不支持评级,则应将评级类型设置为 RATING_NONE

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

内容类型 必需的操作
音乐

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

强烈建议支持:搜索

播客

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

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

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

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

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

您必须根据产品提供支持尽可能多的上面列出的操作,但仍然对任何其他操作做出良好响应。例如,如果只有高级用户才能返回到上一项,那么如果免费层用户要求助手返回到上一项,您可能会引发错误。有关更多指导,请参阅 错误处理部分

要尝试的示例语音查询

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

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

“播放。”

“恢复。”

onPlayFromSearch()
onPlayFromUri()
音乐

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

“在 (应用程序名称) 上播放 (歌曲 | 艺术家 | 专辑 | 类型 | 播放列表)。”

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

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

“在 (应用程序名称) 上阅读 (有声读物)。”

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

“重新启动。”

“快进 ## 秒。”

“倒退 ## 分钟。”

N/A(保持您的 MediaMetadata 更新) “正在播放什么?”

错误

当媒体会话发生错误时,助手会处理这些错误并将其报告给用户。请确保您的媒体会话在 PlaybackState 中正确更新了传输状态和错误代码,如 使用媒体会话 中所述。助手会识别 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 搜索来检索带有 观看操作 链接的标记内容。
  • 当助手启动电视应用程序时,您的应用程序应包含 电视搜索提供程序,以公开媒体内容的 URI。助手将查询发送到内容提供程序,内容提供程序应返回包含深层链接 URI 和可选操作的意图。如果查询在意图中返回操作,助手将该操作和 URI 发送回您的应用程序。如果提供程序未指定操作,助手将向意图添加 ACTION_VIEW

助手将带有值 true 的额外 EXTRA_START_PLAYBACK 添加到它发送到您的应用程序的意图中。您的应用程序应该在收到带有 EXTRA_START_PLAYBACK 的意图时开始播放。

在活动状态下处理意图

用户可以在您的应用程序仍在播放来自先前请求的内容时,要求助手播放其他内容。这意味着您的应用程序可以在其播放活动已启动并处于活动状态时,接收启动播放的新意图。

支持带有深层链接的意图的活动应覆盖 onNewIntent() 来处理新的请求。

在开始播放时,助手可能会向它发送到您的应用程序的意图添加 其他标志。特别是,它可能会添加 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

从服务播放

如果您的应用程序具有允许助手连接的 媒体浏览器服务,助手可以通过与服务的 媒体会话 通信来启动应用程序。媒体浏览器服务永远不应该启动活动。助手将根据您使用 setSessionActivity() 定义的 PendingIntent 来启动您的活动。

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

为了从服务开始,助手实现了媒体浏览器客户端 API。它执行 TransportControls 调用,这些调用会触发您应用程序媒体会话上的 PLAY 操作回调。

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

Starting playback with a media session

当用户发出语音命令播放时,助手将以简短的公告进行响应。一旦公告完成,助手就会发出 PLAY 操作。它不会等待任何特定的播放状态。

如果您的应用程序支持 ACTION_PREPARE_* 操作,助手将在开始公告之前调用 PREPARE 操作。

连接到 MediaBrowserService

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

  • 接受所有连接请求
  • 仅接受来自助手应用程序的连接请求

接受所有连接请求

您必须返回 BrowserRoot 以允许助手向您的媒体会话发送命令。最简单的方法是允许所有 MediaBrowser 应用程序连接到您的 MediaBrowserService。您必须返回非空 BrowserRoot。以下来自 通用音乐播放器 的适用代码

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

接受助手应用程序包和签名

您可以通过检查助手的包名称和签名来明确允许助手连接到您的媒体浏览器服务。您的应用程序将在您的 MediaBrowserService 的 onGetRoot 方法中接收到包名称。您必须返回 BrowserRoot 以允许助手向您的媒体会话发送命令。通用音乐播放器 示例维护一个已知包名称和签名的列表。以下是 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>