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 動作及其回呼:

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

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(),您的媒體應用程式應播放「目前」媒體。如果目前沒有媒體,應用程式應嘗試播放其他媒體內容,例如最新播放清單中的歌曲或隨機待播清單。當使用者要求「在 [您的應用程式名稱] 上播放音樂」時,Google 助理會使用這些 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) 開啟或關閉字幕。

注意事項:

  • 為了讓搜尋指令正常運作,PlaybackState 必須與 state, 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 錯誤代碼。舉例來說,如果動作不支援該動作,請將 PlaybackState 設為 ERROR_CODE_NOT_SUPPORTED;如果動作受到登入保護,則請將 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 的意圖,以及選用的動作。如果查詢在意圖中傳回動作,Google 助理會將該動作和 URI 傳回應用程式。如果供應器未指定動作,Google 助理會在意圖中新增 ACTION_VIEW

Google 助理會將含有 true 值的額外 EXTRA_START_PLAYBACK 新增至其傳送至應用程式的意圖。您的應用程式會在收到帶有 EXTRA_START_PLAYBACK 的意圖時開始播放。

在啟用狀態下處理意圖

應用程式仍播放先前要求的內容時,使用者可以要求 Google 助理播放某些內容。這表示您的應用程式可以接收新的意圖,在活動啟動且啟用時開始播放。

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

開始播放時,Google 助理可能會在傳送至應用程式的意圖中加入其他標記。特別是,可能會新增 FLAG_ACTIVITY_CLEAR_TOPFLAG_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 通訊來啟動應用程式。媒體瀏覽器服務一律不應啟動活動。Google 助理會根據您使用 setSessionActivity() 定義的 PendingIntent 啟動活動。

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

為了從服務開始,Google 助理會實作媒體瀏覽器用戶端 API。這個 API 會執行 TransportControl 呼叫,在應用程式媒體工作階段觸發 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>