Google 助理和媒体应用

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

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

使用媒体会话

每个音频和视频应用都必须实现一个媒体会话,以便 Google 助理可以在播放开始后操作传输控件。要启用媒体和传输控件,可以在应用的 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 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 助理基于 URI 的操作仅适用于向 Google 提供 URI 的公司。要详细了解如何向 Google 描述您的媒体内容,请参阅媒体操作

通过实现准备 API,可以改善发出语音指令后的播放延迟。想要改善播放延迟的媒体应用,可以使用额外的时间来开始缓存内容和准备媒体播放。

请注意,尽管 Google 助理仅使用本部分中列出的操作,但最佳做法是实现所有准备和播放 API,以确保与其他应用兼容。

解析搜索查询

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

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

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

onPlayFromSearch() 方法采用 extra 参数,该参数包含了来自语音搜索的更详细的信息。这些 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 Media Player 示例。

处理空查询

如果在没有搜索查询的情况下调用 onPrepare()onPlay()onPrepareFromSearch()onPlayFromSearch(),您的媒体应用应播放“当前”媒体。如果没有当前媒体,应用应尝试播放一些内容,例如,播放最近播放列表或随机队列中的歌曲。当用户要求“在[您的应用名称]上播放音乐”而没有提供额外的信息时,Google 助理就会使用这些 API。

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

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

在大多数情况下,处理上述播放操作可使应用具备所需的全部播放功能。不过,某些系统还会要求应用包含针对搜索的 Intent 过滤器。您应该在应用的清单文件中声明对此 Intent 过滤器的支持。

将以下代码包含在手机应用以及 Android Automotive OS 模块(如果存在)的清单文件中:

<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 助理会处理媒体会话中出现的错误并向用户报告。请确保您的媒体会话在其 PlaybackState 中正确更新传输状态和错误代码,如使用媒体会话中所述。Google 助理可以识别 getErrorCode() 返回的所有错误代码。

通过 Intent 进行播放

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

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

  • 当 Google 助理启动移动应用时,它可以使用 Google 搜索来检索为观看操作提供链接的标记内容。
  • 当 Google 助理启动 TV 应用时,您的应用应包含 TV 搜索提供器,以提供媒体内容的 URI。Google 助理向内容提供器发送查询,该查询应返回一个 Intent,其中包含深层链接的 URI 和一个可选操作。 如果查询在 Intent 中返回了操作,Google 助理会将该操作和 URI 发回给您的应用。如果提供器未指定操作,Google 助理会在 Intent 中添加 ACTION_VIEW

Google 助理会在发送给您应用的 Intent 中添加值为 true 的 extra EXTRA_START_PLAYBACK。当您的应用收到包含 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 助理会实现媒体浏览器客户端 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>