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 動作僅適用於公司 提供 URI 給 Google進一步瞭解如何向 Google 描述媒體內容 請參閱「媒體動作」。

實作準備 API 後,系統會在語音指令後延遲播放 可能可以減少如要改善播放延遲的媒體應用程式,可以使用 額外時間開始快取內容及準備播放媒體。

剖析搜尋查詢

使用者搜尋特定媒體項目時,例如「在以下位置播放爵士樂: [your app name]”「收聽 <歌曲名稱>」onPrepareFromSearch()onPlayFromSearch() 回呼方法會收到查詢參數與額外套件。

您的應用程式應剖析下列搜尋查詢並開始播放 步驟:

  1. 使用語音搜尋傳回的額外套件和搜尋查詢字串 篩選結果。
  2. 根據這些結果建立播放佇列。
  3. 播放搜尋結果中最相關的媒體項目。
,瞭解如何調查及移除這項存取權。

onPlayFromSearch() 方法會使用 extras 參數,以從語音取得更詳細的資訊 搜尋。這些額外項目可協助您尋找應用程式中的音訊內容,以便播放。 如果搜尋結果無法提供這類資料,您可以實作邏輯 剖析原始搜尋查詢,並根據結果播放適當的曲目。 。

Android Automotive OS 和 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() 稱為沒有搜尋查詢,您的媒體應用程式應播放「目前」 媒體。如果沒有目前媒體,應用程式應該嘗試播放內容,例如 例如最新播放清單中的歌曲,或隨機待播清單。Google 助理會使用 當使用者要求「在 <您的應用程式名稱> 上播放音樂」, 其他資訊

使用者說出「透過 <您的應用程式名稱> 播放音樂」時,Android Automotive OS 或 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>

傳輸控制項

應用程式的媒體工作階段啟用後,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) 開啟/關閉字幕。

請注意:

  • 如要執行搜尋指令,必須將 PlaybackStatestate, position, playback speed, and update time 更新至最新版本。當狀態變更時,應用程式必須呼叫 setPlaybackState()
  • 此外,媒體應用程式也必須隨時更新媒體工作階段中繼資料。方便觀眾說出「現在播放的是哪首歌?」等問題。當適用欄位 (例如曲名、演出者和姓名) 變更時,應用程式必須呼叫 setMetadata()
  • 您必須設定 MediaSession.setRatingType() 來表示應用程式支援的分級類型,而且應用程式必須實作 onSetRating()。如果應用程式不支援分級,則應將分級類型設為 RATING_NONE

你支援的語音操作可能會因內容類型而有所不同。

內容類型 必要動作
音樂

必須支援:「播放」、「暫停」、「停止」、「跳至下一個」和「跳至上一個」。

強烈建議支援下列項目:跳轉至

Podcast

必須使用:「播放」、「暫停」、「停止」和「跳轉」

建議支援以下項目:跳到下一個和跳至上一個

有聲書 必須使用:「播放」、「暫停」、「停止」和「跳轉」
電台 必須使用:「播放」、「暫停」和「停止」
新消息 必須支援:「播放」、「暫停」、「停止」、「跳至下一個」和「跳至上一個」。
影片

必須支援:播放、暫停、停止、跳轉、倒轉和快轉

強烈建議支援以下項目:跳到下一個和跳至上一個

您必須為產品資訊支援上述各項動作 但您仍能妥善回應任何其他行動。舉例來說 付費使用者能夠返回上一個商品, 免費方案使用者要求 Google 助理返回上一個項目時,會出現錯誤。 詳情請參閱錯誤處理一節

可嘗試的語音查詢範例

下表概述了一些應使用的查詢範例 測試實作:

MediaSession 回呼 使用「Ok Google」詞組
onPlay()

「播放。」

「繼續播放。」

onPlayFromSearch()
onPlayFromUri()
音樂

「透過 (應用程式名稱) 播放音樂或歌曲。」這是空白的查詢。

「在 (應用程式名稱) 上播放 (歌曲 | 藝人 | 專輯 | 類型 | 播放清單)」。

電台 「透過 (應用程式名稱) 播放 (頻率 | 電台)」。
Audiobook

「透過 (應用程式名稱) 朗讀我的有聲書。」

「朗讀 (應用程式名稱) 上的 (有聲書)。」

Podcast 「透過 (應用程式名稱) 播放 (Podcast)」。
onPause() 「暫停。」
onStop() 「停止。」
onSkipToNext() 「下一首 (歌曲 | 劇集 | 曲目)。」
onSkipToPrevious() 「播放上一集 (歌曲 | 劇集 | 曲目)。」
onSeekTo()

「重新啟動。」

「快轉 ## 秒。」

「倒轉 ## 分鐘。」

不適用 (請保留 MediaMetadata敬上 已更新) 「現在播放的是什麼?」

錯誤

Google 助理會處理媒體工作階段中的錯誤,並回報這些錯誤 向使用者傳達相關資訊請確認媒體工作階段會更新傳輸狀態,並 正確記錄在 PlaybackState 中的錯誤代碼,如使用 媒體工作階段。Google 助理 識別出 getErrorCode()

常見誤解情況

以下列舉幾個應確認的錯誤情況 正確:

  • 使用者需要登入
    • PlaybackState 錯誤代碼設為 ERROR_CODE_AUTHENTICATION_EXPIRED
    • 設定 PlaybackState 錯誤訊息。
    • 如果需要播放,請將 PlaybackState 狀態設為 STATE_ERROR。 否則 PlaybackState 的其餘部分則依原樣保留。
  • 使用者要求無法執行的操作
  • 使用者要求的內容未在應用程式中顯示
    • 妥善設定 PlaybackState 錯誤代碼。舉例來說,請使用 ERROR_CODE_NOT_AVAILABLE_IN_REGION
    • 設定 PlaybackState 錯誤訊息。
    • PlaybackSate 狀態設為 STATE_ERROR,即可中斷播放。 否則 PlaybackState 的其餘部分則依原樣保留。
  • 使用者要求的內容沒有完全相符的項目。舉例來說 免費方案使用者要求提供內容僅適用於進階方案使用者。
    • 建議您不要傳回錯誤,而是優先處理 尋找與遊戲類似的內容Google 助理會盡可能幫你處理語音 事先取得相關語音回應。

使用意圖播放

Google 助理只要傳送 包含深層連結的意圖

意圖及其深層連結可能來自不同來源:

  • Google 助理啟動狀態 啟動行動應用程式,可透過 Google 搜尋擷取已加上標記的內容 會提供包含連結的觀看動作
  • 當 Google 助理啟動 TV 應用程式時,應用程式應提供 電視搜尋引擎 揭露媒體內容的 URI。Google 助理傳送查詢到 內容供應器,應傳回包含深層連結 URI 的意圖,以及 選擇性的動作 如果查詢在意圖中傳回動作, Google 助理會將該動作和 URI 傳回應用程式。 如果供應商未指定 Google 助理會在意圖中新增 ACTION_VIEW

Google 助理會額外加入值為 trueEXTRA_START_PLAYBACK 傳送到應用程式的意圖您的應用程式應會在發生時開始播放 收到含有 EXTRA_START_PLAYBACK 的意圖。

啟用時處理意圖

使用者可以要求 Google 助理在應用程式仍在播放時播放內容 先前要求提供的內容這表示應用程式可接收新的意圖: 在已啟動並啟動其播放活動的情況下開始播放。

支援含有深層連結意圖的活動應覆寫 onNewIntent() 來處理新的要求

開始播放時,Google 助理可能會新增 旗標 傳送到應用程式的意圖特別是,它可能會 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

透過服務播放內容

如果你的應用程式有 media browser service 允許 Google 助理連線 Google 助理會透過與服務的 media session。 媒體瀏覽器服務不應啟動活動。 Google 助理會根據您定義的 PendingIntent 啟動活動 呼叫 setSessionActivity()

設定媒體工作階段時,請務必設定 MediaSession.Token 初始化媒體瀏覽器服務。 記得設定支援的播放動作 包括在初始化期間Google 助理已要求你播放媒體 應用程式,指定 Google 助理送出第一個播放前的播放動作 指令

為了從服務開始,Google 助理會實作媒體瀏覽器用戶端 API。 它會執行 TransportControl 呼叫,在 存取應用程式媒體工作階段

下圖顯示 Google 助理和 對應的媒體工作階段回呼。(Prepare 回呼只會傳送 )。所有呼叫皆為非同步。「Google 助理」不會 等候應用程式回應。

透過媒體工作階段開始播放

當使用者發出語音指令來播放內容時,Google 助理會回應簡短的公告。 公告結束後,Google 助理就會發出 Google Play 動作。不會等待任何特定的播放狀態。

如果您的應用程式支援 ACTION_PREPARE_* 動作,Google 助理會在開始朗讀前呼叫 PREPARE 動作。

連線至 MediaBrowserService

如要使用服務啟動應用程式,Google 助理必須連線至應用程式的 MediaBrowserService,並且 擷取 MediaSession.Token系統會在服務的 onGetRoot() 方法。處理要求的方式有兩種:

  • 接受所有連結要求
  • 僅接受來自 Google 助理應用程式的連結要求
,瞭解如何調查及移除這項存取權。

接受所有連結要求

您必須傳回 BrowserRoot 允許 Google 助理傳送指令給媒體工作階段。最簡單的方法是允許所有 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...
}

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