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 Automotive OS 和 Android Auto 支持以下额外信息

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

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

处理空查询

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

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

传输控件

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

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

请注意

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

内容类型 必需操作
音乐

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

强烈建议支持:跳转到

播客

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

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

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

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

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

您必须根据您的产品供应情况支持尽可能多的上述操作,但仍应优雅地响应任何其他操作。例如,如果只有高级用户才能返回到上一项,则如果免费层用户要求助手返回到上一项,则可能会引发错误。有关更多指导,请参阅 错误处理部分

示例语音查询

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

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错误代码。例如,使用ERROR_CODE_NOT_AVAILABLE_IN_REGION
    • 设置PlaybackState错误消息。
    • PlaybackSate状态设置为STATE_ERROR以中断播放,否则保留其余的PlaybackState
  • 用户请求的内容没有完全匹配项。例如,免费用户请求只有高级用户才能访问的内容。
    • 我们建议您不要返回错误,而应优先查找类似内容进行播放。助手会在播放开始前处理最相关的语音回复。

使用 Intent 进行播放

助手可以通过发送包含深度链接的 Intent 来启动音频或视频应用并开始播放。

Intent 及其深度链接可以来自不同的来源

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

助手会添加额外的EXTRA_START_PLAYBACK,其值为true,添加到它发送到您应用的 Intent 中。您的应用在接收到包含EXTRA_START_PLAYBACK的 Intent 时应开始播放。

处理活动期间的 Intent

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

支持带有深度链接的 Intent 的活动应重写onNewIntent()以处理新请求。

启动播放时,助手可能会向发送到您应用的 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

从服务进行播放

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

当您初始化媒体浏览器服务时,请务必设置 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>