Google 助理和媒體應用程式

你可以透過 Google 助理下達語音指令,控制許多裝置,例如 Google Home、手機等。這項功能內建媒體指令理解能力 (例如「播放碧昂絲的歌曲」),並支援媒體控制項 (例如暫停、略過、快轉、按讚)。

Google 助理會使用媒體工作階段與 Android 媒體應用程式通訊。它可以透過意圖服務啟動應用程式並開始播放內容。為獲得最佳效果,應用程式應實作本頁所述的所有功能。

使用媒體工作階段

所有音訊和影片應用程式都必須導入媒體工作階段,這樣一來,助理就能在播放開始後操作傳輸控制項。

請注意,雖然 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 動作和回呼:

動作 Callback
ACTION_PLAY onPlay()
ACTION_PLAY_FROM_SEARCH onPlayFromSearch()
ACTION_PLAY_FROM_URI (*) onPlayFromUri()

工作階段也應實作下列 PREPARE 動作和回呼:

動作 Callback
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() 方法會採用 extras 參數,其中包含語音搜尋的詳細資訊。這些額外資訊可協助你在應用程式中尋找音訊內容以供播放。 如果搜尋結果無法提供這項資料,您可以實作邏輯來剖析原始搜尋查詢,並根據查詢播放適當的曲目。

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() 時未提供搜尋查詢,媒體應用程式應播放「目前」媒體。如果目前沒有媒體,應用程式應嘗試播放內容,例如最近播放清單中的歌曲或隨機佇列。當使用者要求「在『[你的應用程式名稱]』上播放音樂」,但未提供其他資訊時,助理就會使用這些 API。

當使用者說出「在『[應用程式名稱]』上播放音樂」時,Android Automotive OS 或 Android Auto 會嘗試啟動應用程式,並呼叫應用程式的 onPlayFromSearch() 方法來播放音訊。不過,由於使用者未說出媒體項目的名稱,onPlayFromSearch() 方法會收到空白的查詢參數。在這種情況下,應用程式應立即播放音訊做為回應,例如最近播放清單中的歌曲或隨機佇列。

宣告支援舊版語音指令

在大多數情況下,處理上述播放動作可為應用程式提供所有必要的播放功能。不過,部分系統會要求應用程式包含搜尋的意圖篩選器。您應在應用程式的資訊清單檔案中,宣告支援這項 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 助理就能發出語音指令來控制播放作業及更新媒體中繼資料。如要讓這項功能正常運作,程式碼應啟用下列動作,並實作對應的回呼:

動作 Callback 說明
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) 開啟/關閉字幕。

注意事項:

支援的語音動作可能會因內容類型而異。

內容類型 必要行動
音樂

必須支援:播放、暫停、停止、跳至下一首及跳至上一首

強烈建議支援:Seek To

Podcast

必須支援:播放、暫停、停止和搜尋至

建議支援:跳至下一個和跳至上一個

有聲書 必須支援:播放、暫停、停止和搜尋至
電台 必須支援:播放、暫停和停止
新聞 必須支援:播放、暫停、停止、跳至下一首及跳至上一首
影片

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

強烈建議支援:跳至下一個和跳至上一個

您必須支援產品供應項目允許的上述所有動作,但仍須妥善回應任何其他動作。舉例來說,如果只有進階版使用者可以返回上一個項目,當免費方案使用者要求 Google 助理返回上一個項目時,您可能會引發錯誤。如需更多指引,請參閱錯誤處理部分

語音查詢範例

下表列出一些範例查詢,供您在測試實作項目時使用:

MediaSession 回呼 要使用的「Ok Google」啟動字詞
onPlay()

「播放」。

「繼續播放。」

onPlayFromSearch()
onPlayFromUri()
音樂

「在『(應用程式名稱)』上播放音樂或歌曲。」這是空白查詢。

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

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

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

「在『(應用程式名稱)』上閱讀『(有聲書)』。」

Podcast 「在『(應用程式名稱)』上播放『(Podcast)』。」
onPause() 「暫停」。
onStop() 「停止。」
onSkipToNext() 「下一首(歌曲 | 劇集 | 樂曲)。」
onSkipToPrevious() 「上一個(歌曲 | 集數 | 軌道)。」
onSeekTo()

「重新啟動」。

「Skip ahead ## seconds」(快轉 ## 秒)。

「Go back ## minutes」(倒回 ## 分鐘)。

不適用 (請隨時掌握最新消息)MediaMetadata 「現在播的是什麼?」

錯誤

當媒體工作階段發生錯誤時,Google 助理會處理這些錯誤,並向使用者回報。請務必按照「使用媒體工作階段」一文所述,讓媒體工作階段正確更新 PlaybackState 中的傳輸狀態和錯誤碼。助理會辨識 getErrorCode() 傳回的所有錯誤代碼。

常見的處理不當案例

以下列舉幾個錯誤案例,請務必正確處理:

  • 使用者必須登入
    • PlaybackState 錯誤代碼設為 ERROR_CODE_AUTHENTICATION_EXPIRED
    • 設定 PlaybackState 錯誤訊息。
    • 如果需要播放,請將 PlaybackState 狀態設為 STATE_ERROR,否則請保留其餘 PlaybackState 不變。
  • 使用者要求執行無法執行的動作
    • 適當設定 PlaybackState 錯誤代碼。舉例來說,如果系統不支援該動作,請將 PlaybackState 設為 ERROR_CODE_NOT_SUPPORTED;如果該動作受到登入保護,請將 PlaybackState 設為 ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED
    • 設定 PlaybackState 錯誤訊息。
    • 其餘 PlaybackState 則維持不變。
  • 使用者要求應用程式中未提供的內容
    • 適當設定 PlaybackState 錯誤代碼。例如,使用 ERROR_CODE_NOT_AVAILABLE_IN_REGION
    • 設定 PlaybackState 錯誤訊息。
    • PlaybackSate 狀態設為 STATE_ERROR,中斷播放,否則保留其餘 PlaybackState 不變。
  • 使用者要求內容,但系統找不到完全相符的結果。舉例來說,免費層級使用者要求提供僅限付費層級使用者存取的內容。
    • 我們建議您不要傳回錯誤,而是優先尋找類似的內容。Google 助理會先說出最相關的語音回覆,再開始播放。

根據意圖播放

Google 助理可以傳送含有深層連結的意圖,啟動音訊或影片應用程式並開始播放內容。

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

  • 當 Google 助理啟動行動應用程式時,可以使用 Google 搜尋擷取標記的內容,提供含有連結的手錶動作
  • 當 Google 助理啟動電視應用程式時,應用程式應加入 TV Search Provider,公開媒體內容的 URI。Google 助理會將查詢傳送給內容供應商,後者應傳回包含深層連結 URI 和選用動作的意圖。如果查詢在 Intent 中傳回動作,Google 助理會將該動作和 URI 傳回應用程式。如果供應商未指定動作,Google 助理會在 Intent 中加入 ACTION_VIEW

Google 助理會將額外的 EXTRA_START_PLAYBACK (值為 true) 新增至傳送給應用程式的意圖。應用程式收到含有 EXTRA_START_PLAYBACK 的意圖時,應開始播放內容。

在活動期間處理意圖

使用者可以要求 Google 助理播放其他內容,而應用程式仍會繼續播放先前要求播放的內容。也就是說,應用程式的播放活動啟動並處於有效狀態時,仍可接收啟動播放的新意圖。

支援意圖和深層連結的活動應覆寫 onNewIntent(),以處理新要求。

開始播放時,Google 助理可能會在傳送至應用程式的意圖中加入其他標記,特別是 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。 助理會根據您使用 setSessionActivity() 定義的 PendingIntent 啟動活動。

請務必在初始化媒體瀏覽器服務時設定 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。您必須傳回非空值的 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>