Google 助理和媒体应用

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

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

使用媒体会话

每个音频和视频应用都必须实现 媒体会话 以便 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()

通过 通用 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() 回调方法会接收一个查询参数和一个 extra 捆绑包。

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

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

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

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

以下代码段展示了如何替换 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() 无需搜索查询即可调用,您的媒体应用应播放“当前” 媒体。如果没有当前媒体,应用应尝试播放内容,如 最新播放列表或随机队列中的歌曲。Google 助理会使用 当用户在不使用应用名称的情况下询问“在 [您的应用名称] 上播放音乐”时, 更多信息。

当用户说出“在 [您的应用名称]上播放音乐”时,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()
音乐

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

“在(应用名称)上播放(歌曲 | 音乐人 | 专辑 | 流派 | 播放列表)”。”

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

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

“通过(应用名称)阅读(有声读物)”。”

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

“重启。”

“快进 ## 秒。”

“后退 ## 分钟。”

不适用(保留您的 MediaMetadata 已更新) “现在播放的是什么?”

错误

Google 助理会处理媒体会话中出现的错误,并报告 提供给用户请确保您的媒体会话更新了传输状态, PlaybackState 中正确设置了错误代码,如使用 媒体会话。Google 助理 并识别由外部 API 服务器返回的所有错误代码 getErrorCode()

经常处理不当的情况

下面列举了一些示例来说明您应该确保能够妥善处理 正确:

  • 用户需要登录 <ph type="x-smartling-placeholder">
      </ph>
    • PlaybackState 错误代码设置为 ERROR_CODE_AUTHENTICATION_EXPIRED
    • 设置 PlaybackState 错误消息。
    • 如果需要播放,请将 PlaybackState 状态设置为 STATE_ERROR。 否则,按原样保留 PlaybackState 的其余部分。
  • 用户请求无法执行的操作 <ph type="x-smartling-placeholder">
  • 用户请求在应用中未提供的内容 <ph type="x-smartling-placeholder">
      </ph>
    • 适当设置 PlaybackState 错误代码。例如,使用 ERROR_CODE_NOT_AVAILABLE_IN_REGION
    • 设置 PlaybackState 错误消息。
    • PlaybackSate 状态设置为 STATE_ERROR 即可中断播放。 否则,按原样保留 PlaybackState 的其余部分。
  • 用户请求没有完全匹配的内容。例如, 免费层级用户询问仅限高级层级用户观看的内容。
    • 我们建议您不要返回错误,而应优先考虑 才能找到类似的玩法Google 助理最擅长说话 相关语音回应。

通过 Intent 进行播放

Google 助理可以启动音频或视频应用,并通过以下方式开始播放: intent 和深层链接

Intent 与其深层链接可以来自不同的来源:

  • 当 Google 助理处于以下状态时 启动移动应用时,它可以使用 Google 搜索来检索 用于提供带有链接的观看操作
  • 当 Google 助理启动 TV 应用时,您的应用应包含 电视搜索提供程序 公开媒体内容的 URI。Google 助理会将查询发送到 content provider,它应返回包含深层链接 URI 的 intent,以及 执行可选操作。 如果查询在 intent 中返回操作, Google 助理会将该操作和 URI 发送回您的应用。 如果提供商未指定 操作时,Google 助理会将 ACTION_VIEW 添加到 intent。

Google 助理添加了值为 true 的额外 EXTRA_START_PLAYBACK 传递给应用的 intent。您的应用应该在满足以下条件时开始播放 使用 EXTRA_START_PLAYBACK 接收 intent。

在处于活动状态时处理 Intent

用户可以让 Google 助理在应用还播放内容时播放一些内容 请求的内容这意味着您的应用可以接收 当其播放 Activity 已启动且处于活动状态时开始播放。

支持带有深层链接的 intent 的 activity 应该替换 onNewIntent() 处理新的请求

开始播放时,Google 助理可能会添加其他 标志 传递给应用的 intent。具体来说,它可能会增加 FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK 或两者。虽然您的代码 不需要处理这些标记,Android 系统会对其进行响应。 当收到包含新 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

从服务进行播放

如果您的应用具有 media browser service 允许通过“Google 助理”进行连接 Google 助理可以通过与该服务的通信服务来启动应用 media session。 媒体浏览器服务绝不应启动 Activity。 Google 助理将根据您定义的 PendingIntent 启动您的 activity (使用 setSessionActivity() 即可)。

请务必在登录时设置 MediaSession.Token 初始化媒体浏览器服务。 请务必设置支持的播放操作 任何时候(包括在初始化期间)。Google 助理需要你的媒体内容 应用,以便在 Google 助理发送第一次播放之前设置播放操作 命令。

为了从服务启动,Google 助理会实现媒体浏览器客户端 API。 它会执行 TransportControls 调用,以在您的 应用的媒体会话。

下图显示了 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 助理向您的媒体会话发送命令。通过 通用音乐播放器 示例维护了一个已知软件包名称和签名的列表。以下是 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>