Google アシスタントとメディアアプリ

Google アシスタントを使用すると、Google Home やスマートフォンなど、さまざまなデバイスを音声コマンドで操作できます。メディア関連コマンド(「ビヨンセの曲をかけて」など)を理解する機能が組み込まれており、一時停止、スキップ、早送り、評価などのメディア コントロールが可能です。

Google アシスタントでは、Android メディアアプリとの通信にメディア セッションが使用されます。また、インテントサービスを使用してアプリの起動、再生開始を行えます。アプリの実装にあたっては、このページで説明する機能をすべて含めることをおすすめします。

メディア セッションの使用

再生を開始したら 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 と再生 API をすべて実装して他のアプリとの互換性を確保することをおすすめします。

検索クエリの解析

ユーザーが特定のメディア アイテム(「<アプリ名> でジャズを再生して」や「<曲名> を聴く」など)を検索すると、コールバック メソッド onPrepareFromSearch() または onPlayFromSearch() がクエリ パラメータと extras バンドルを受け取ります。

アプリでは音声検索クエリを解析し、次の手順で再生を開始します。

  1. 音声検索から返された extras バンドルと検索クエリ文字列を使用して結果をフィルタする。
  2. その結果に基づいて再生キューを作成する。
  3. 結果の中から最も関連性の高いメディア アイテムを再生する。

onPlayFromSearch() メソッドは、音声検索からのより詳細な情報を含む extras パラメータを受け取ります。これらの extras は、再生するアプリ内の音声コンテンツを見つけるのに役立ちます。検索結果から再生するデータが見つからない場合は、生の検索クエリを解析するロジックを実装することで、クエリに沿った曲を再生できます。

Android Automotive OS と Android Auto では、以下の extras がサポートされています。

次のコード スニペットは、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() メソッドには空のクエリ パラメータが渡されます。このような場合、アプリではこれを受けてすぐに音楽(最新の再生リストやランダムキューの曲など)を再生するようにします。

以前の音声操作サポートの宣言

ほとんどの場合、再生アクションを上記のように処理することで、アプリでは必要な再生動作を行えます。ただし、システムによっては、アプリに検索用のインテント フィルタを含める必要があります。このインテント フィルタのサポートは、アプリのマニフェスト ファイルで宣言します。

以下のコードを、スマートフォン アプリと 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) 字幕オン / オフ

注意事項:

  • 移動コマンドを機能させるには、PlaybackStatestate, position, playback speed, and update time が最新状態でなければなりません。アプリでは、状態が変化したときに setPlaybackState() を呼び出す必要があります。
  • メディアアプリでは、メディア セッション メタデータも最新状態に保つ必要があります。これにより、「今かかっている曲は何?」などの質問がサポートされます。アプリでは、該当の項目(トラックのタイトル、アーティスト、名前など)が変わった時点で setMetadata() を呼び出す必要があります。
  • MediaSession.setRatingType() は、アプリがサポートする評価のタイプを示すように設定する必要があり、アプリでは onSetRating() を実装する必要があります。アプリが評価をサポートしていない場合は、評価タイプをRATING_NONEに設定します。

エラー

メディア セッションで発生したエラーは、Google アシスタントにより処理されてユーザーに報告されます。メディア セッションの操作で説明されているように、メディア セッションでは PlaybackState のトランスポート状態とエラーコードを正しく更新するようにしてください。getErrorCode() から返されるエラーコードはすべて、Google アシスタントにより認識されます。

インテントを使用した再生

Google アシスタントは、ディープリンクを含むインテントを送信することで、オーディオ アプリや動画アプリを起動して再生を開始できます。

インテントとそのディープリンクは、以下のさまざまなソースから取得できます。

  • Google アシスタントは、モバイルアプリを起動する場合、Google 検索を使用して、視聴アクションにリンクを提供するマークアップ コンテンツを取得できます。
  • Google アシスタントで TV アプリを起動する場合、アプリに TV 検索プロバイダを含めてメディア コンテンツの URI を公開するようにします。Google アシスタントからは、ディープリンクの URI とオプションのアクションを含むインテントを返すよう、コンテンツ プロバイダにクエリが送信されます。クエリによりアクションを含むインテントが返された場合は、そのアクションと URI が Google アシスタントからアプリに送られます。プロバイダからアクションが指定されなかった場合は、Google アシスタントによりそのインテントに ACTION_VIEW が追加されます。

Google アシスタントからアプリに送信されるインテントには、Google アシスタントにより値が trueEXTRA_START_PLAYBACK が追加されます。アプリが EXTRA_START_PLAYBACK を含むインテントを受信すると、再生が開始されます。

アクティブ時のインテントの処理

ユーザーは、アプリで前回リクエストされたコンテンツが再生されている間も、別のものを再生するよう Google アシスタントにリクエストできます。つまり、再生アクティビティがすでに起動されアクティブになっている間も、アプリでは再生を開始するための新たなインテントを受け取る可能性があるということです。

ディープリンクを含むインテントをサポートするアクティビティでは、新しいリクエストを処理するために、onNewIntent() をオーバーライドします。

再生を開始するとき、Google アシスタントによりインテントにフラグが追加されてアプリに送信される場合があります。特に、FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK のいずれか、または両方が追加される場合があります。これらのフラグに対しては、コードで何かをする必要はありませんが、Android システム側で応答がなされます。これにより、前の URI の再生中に、アプリが 2 番目の再生リクエストを新たな URI で受け取った場合に、アプリの動作に影響が及ぶ可能性があります。この場合にアプリがどのように反応するか、テストすることをおすすめします。たとえば、adb コマンドライン ツールを使用して、状況をシミュレートできます(定数 0x14000000 は、2 つのフラグのビット単位論理和です)。

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 と通信することでアプリの起動を行えます。メディア ブラウザ サービスからはアクティビティを起動すべきではありません。setSessionActivity() で定義した PendingIntent に基づいて、Google アシスタントによりアクティビティが起動されます。

メディア ブラウザ サービスを初期化するときは、必ず MediaSession.Token を設定してください。初期化中も含め、サポートする再生アクションを常に設定しておいてください。Google アシスタントによる最初の再生コマンドの送信は、すでにメディアアプリで再生アクションの設定がなされているものとして行われます。

サービスからの起動のために、Google アシスタントによりメディア ブラウザ クライアント API が実装されます。これにより、TransportControls 呼び出しが行われ、アプリのメディア セッションで PLAY アクション コールバックがトリガーされます。

次の図は、Google アシスタントによって生成される呼び出しの順序と、対応するメディア セッション コールバックを示しています(準備コールバックは、アプリがサポートしている場合にのみ送信されます)。すべての呼び出しは非同期です。Google アシスタントがアプリからの応答を待つことはありません。

メディア セッションを使用した再生の開始

ユーザーが再生のための音声コマンドを発行すると、Google アシスタントから短いアナウンスが返されます。アナウンスが終わるとすぐに、Google アシスタントから PLAY アクションが発行されます。特定の再生状態を待つことはありません。

アプリが ACTIONPREPARE* アクションをサポートしている場合は、Google アシスタントではアナウンス開始前に PREPARE アクションが呼び出されます。

MediaBrowserService への接続

サービスを使用してアプリを起動するには、Google アシスタントからアプリの MediaBrowserService に接続して MediaSession.Token を取得できる必要があります。接続リクエストは、このサービスの onGetRoot() メソッドで処理されます。リクエストの処理には、次の 2 つの方法があります。

  • すべての接続リクエストを受け入れる
  • Google アシスタント アプリからの接続リクエストのみを受け入れる

すべての接続リクエストを受け入れる場合

Google アシスタントからメディア セッションにコマンドを送信できるようにするには、BrowserRoot を返す必要があります。最も簡単な方法は、すべての 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 メソッドでパッケージ名を受け取ります。Google アシスタントからメディア セッションにコマンドを送信できるようにするには、BrowserRoot を返す必要があります。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>