Google 어시스턴트 및 미디어 앱

Google 어시스턴트는 음성 명령을 사용하여 Google Home, 스마트폰 등 다양한 기기를 제어하도록 지원합니다. 또한 미디어 명령어('비욘세의 곡 재생')를 이해하는 내장된 기능을 제공하며 미디어 컨트롤(예: 일시중지, 건너뛰기, 앞으로, 좋아요)을 지원합니다.

어시스턴트는 미디어 세션을 사용하여 Android 미디어 앱과 통신합니다. 인텐트 또는 서비스를 사용하여 앱을 실행하고 재생을 시작할 수 있습니다. 최상의 결과를 얻으려면 앱에서 이 페이지에 설명된 모든 기능을 구현해야 합니다.

미디어 세션 사용

재생이 시작되면 어시스턴트가 전송 컨트롤을 조작할 수 있도록 모든 오디오 및 동영상 앱은 미디어 세션을 구현해야 합니다. 앱의 MediaSession 객체에서 이러한 플래그를 설정하여 미디어 및 전송 컨트롤을 사용 설정합니다.

Kotlin

    session.setFlags(
            MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
            MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
    )
    

자바

    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를 구현하면 음성 명령 이후의 재생 지연 시간을 줄일 수 있습니다. 재생 지연 시간을 개선하려는 미디어 앱은 별도의 시간을 들여 콘텐츠 캐시 및 미디어 재생 준비를 시작할 수 있습니다.

어시스턴트는 이 섹션에 나열된 작업만 사용하지만, 다른 애플리케이션과의 호환성을 보장하려면 모든 준비 및 재생 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
        }
    }
    

자바

    @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
        }
    }
    

앱에서 오디오 콘텐츠를 재생하기 위해 음성 검색을 구현하는 방법에 관한 더 자세한 예는 범용 미디어 플레이어 샘플을 참조하세요.

빈 쿼리 처리

검색어 없이 onPrepare(), onPlay(), onPrepareFromSearch() 또는 onPlayFromSearch()가 호출되면 미디어 앱에서 '현재' 미디어를 재생해야 합니다. 현재 미디어가 없는 경우 앱은 최신 재생목록의 곡 또는 임의 대기열의 곡 등을 재생하려고 해야 합니다. 사용자가 추가 정보 없이 '[앱 이름]에서 음악 재생'을 요청하면 어시스턴트는 이러한 API를 사용합니다.

사용자가 '[앱 이름]에서 음악 재생'이라고 말하면 Android Automotive OS 또는 Android Auto에서는 앱을 실행하고 앱의 onPlayFromSearch() 메서드를 호출하여 오디오를 재생하려고 시도합니다. 하지만 사용자가 미디어 항목의 이름을 말하지 않았으므로 onPlayFromSearch() 메서드는 빈 쿼리 매개변수를 받습니다. 이 경우 앱은 최신 재생목록 또는 무작위 대기열의 노래와 같은 오디오를 즉시 재생하여 응답해야 합니다.

음성 작업의 레거시 지원 선언

대부분의 경우 위에서 설명한 재생 작업을 처리하면 앱에 필요한 모든 재생 기능이 제공됩니다. 그러나 일부 시스템에서는 검색용 인텐트 필터를 앱에 포함해야 합니다. 앱의 manifest 파일에서 이 인텐트 필터에 관한 지원을 선언해야 합니다.

전화 앱과 Android Automotive OS 모듈(있는 경우)의 manifest 파일에 이 코드를 포함합니다.

<activity>
        <intent-filter>
            <action android:name=
                 "android.media.action.MEDIA_PLAY_FROM_SEARCH" />
            <category android:name=
                 "android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
    

전송 컨트롤

앱 미디어 세션이 활성화되면 어시스턴트가 음성 명령을 실행하여 재생을 제어하고 미디어 메타데이터를 업데이트할 수 있습니다. 이렇게 하려면 코드에서 다음 작업을 사용 설정하고 해당하는 콜백을 구현해야 합니다.

작업 콜백 설명
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 명령어가 작동하려면 PlaybackStatestate, position, playback speed, and update time이 최신 상태여야 합니다. 상태가 변경되면 앱에서 setPlaybackState()를 호출해야 합니다.
  • 또한 미디어 앱은 미디어 세션 메타데이터를 최신 상태로 유지해야 합니다. "재생 중인 곡이 무엇인가요?"와 같은 질문을 지원합니다. 트랙 제목, 아티스트, 이름 등 관련 필드가 변경되면 앱에서 setMetadata()를 호출해야 합니다.
  • 앱이 지원하는 평점의 유형을 나타내도록 MediaSession.setRatingType()을 설정해야 하며 앱은 onSetRating()을 구현해야 합니다. 앱이 평점을 지원하지 않는 경우 평점 유형을 RATING_NONE으로 설정해야 합니다.

오류

어시스턴트는 발생하는 미디어 세션의 오류를 처리하고 사용자에게 보고합니다. 미디어 세션 작업에 설명된 대로 미디어 세션이 PlaybackState에서 전송 상태 및 오류 코드를 올바르게 업데이트해야 합니다. 어시스턴트는 getErrorCode()에 의해 반환되는 모든 오류 코드를 인식합니다.

인텐트를 사용한 재생

어시스턴트는 오디오 또는 동영상 앱을 실행하고 딥 링크와 함께 인텐트를 전송하여 재생을 시작할 수 있습니다.

인텐트와 딥 링크는 다음과 같이 다양한 소스에서 가져올 수 있습니다.

  • 어시스턴트는 모바일 앱을 시작하면 Google 검색을 사용하여 링크가 있는 시청 작업을 제공하는 마크업된 콘텐츠를 검색할 수 있습니다.
  • 어시스턴트가 TV 앱을 시작할 때 미디어 콘텐츠의 URI를 노출하기 위한 TV 검색 공급자가 앱에 포함되어 있어야 합니다. 어시스턴트는 딥 링크용 URI와 선택적 작업이 포함된 인텐트를 반환해야 하는 쿼리를 콘텐츠 제공업체에 보냅니다. 쿼리가 인텐트에서 작업을 반환하면 어시스턴트가 이 작업과 URI를 다시 앱으로 보냅니다. 제공업체가 작업을 지정하지 않은 경우 어시스턴트는 ACTION_VIEW를 인텐트에 추가합니다.

어시스턴트는 앱에 전송하는 인텐트에 값이 true인 별도의 EXTRA_START_PLAYBACK을 추가합니다. 앱은 EXTRA_START_PLAYBACK의 인텐트를 수신하면 재생을 시작해야 합니다.

활성 상태에서 인텐트 처리

앱이 이전 요청의 콘텐츠를 여전히 재생하는 동안 사용자가 다른 것을 재생하도록 어시스턴트에 요청할 수 있습니다. 즉, 재생 활동이 이미 시작되어 활성 상태인 동안 앱에서 재생을 시작하기 위한 새로운 인텐트를 수신할 수 있습니다.

딥 링크가 포함된 인텐트를 지원하는 활동은 새 요청을 처리하기 위해 onNewIntent()를 재정의해야 합니다.

재생이 시작되면 어시스턴트가 앱에 전송하는 인텐트에 플래그를 추가할 수 있습니다. 특히 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
    

서비스에서 재생

앱에 어시스턴트에서의 연결을 허용하는 media browser service가 있는 경우 어시스턴트는 서비스의 media session과 통신하여 앱을 시작할 수 있습니다. 미디어 브라우저 서비스는 활동을 시작해서는 안 됩니다. 어시스턴트는 setSessionActivity()로 정의한 PendingIntent에 따라 활동을 시작합니다.

미디어 브라우저 서비스를 초기화할 때 MediaSession.Token을 설정해야 합니다. 초기화하는 동안을 포함하여 지원되는 재생 작업을 항상 설정해야 합니다. 어시스턴트는 첫 번째 재생 명령어를 전송하기 전에 미디어 앱이 재생 작업을 설정할 것이라고 기대합니다.

서비스에서 재생을 시작하기 위해 어시스턴트는 미디어 브라우저 클라이언트 API를 구현합니다. 앱의 미디어 세션에서 PLAY 작업 콜백을 트리거하는 TransportControls를 호출합니다.

다음 다이어그램은 어시스턴트 및 해당하는 미디어 세션 콜백에서 생성된 호출의 순서를 보여줍니다. (앱에서 지원하는 경우에만 준비 콜백이 전송됩니다.) 모든 호출은 비동기입니다. 어시스턴트는 앱의 응답을 기다리지 않습니다.

미디어 세션과 함께 재생 시작

사용자가 재생을 위한 음성 명령을 실행하면 어시스턴트가 짧은 알림으로 응답합니다. 알림이 완료되는 즉시 어시스턴트가 PLAY 작업을 실행합니다. 특정 재생 상태를 기다리지 않습니다.

앱에서 ACTIONPREPARE* 작업을 지원하는 경우 어시스턴트는 알림을 시작하기 전에 PREPARE 작업을 호출합니다.

MediaBrowserService에 연결

서비스를 사용하여 앱을 시작하려면 어시스턴트가 앱의 MediaBrowserService에 연결하여 MediaSession.Token을 검색할 수 있어야 합니다. 연결 요청은 서비스의 onGetRoot() 메서드에서 처리됩니다. 요청을 처리하는 방법에는 두 가지가 있습니다.

  • 모든 연결 요청 수락
  • 어시스턴트 앱에서만 연결 요청 수락

모든 연결 요청 수락

어시스턴트가 미디어 세션으로 명령어를 보낼 수 있도록 하려면 BrowserRoot를 반환해야 합니다. 가장 쉬운 방법은 모든 MediaBrowser 앱이 MediaBrowserService에 연결되도록 허용하는 것입니다. Null이 아닌 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...
    }
    

자바

    @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...
    }
    

어시스턴트 앱 패키지 및 서명 수락

패키지 이름과 서명을 확인하여 어시스턴트가 미디어 브라우저 서비스에 연결하도록 명시적으로 허용할 수 있습니다. 앱은 MediaBrowserService의 onGetRoot 메서드에서 패키지 이름을 수신합니다. 어시스턴트가 미디어 세션으로 명령어를 보낼 수 있도록 하려면 BrowserRoot를 반환해야 합니다. 유니버설 음악 플레이어 샘플은 알려진 패키지 이름 및 서명의 목록을 유지 관리합니다. 다음은 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>