O Google Assistente e os apps de mídia

O Google Assistente permite que você use comandos de voz para controlar vários dispositivos, como o Google Home, seu smartphone e muito mais. Ele tem um recurso integrado para entender os comandos de mídia ("tocar algo da Beyoncé") e é compatível com controles, como pause, pule, avance e gostei.

O Assistente se comunica com apps de mídia do Android usando uma sessão de mídia. Ele pode usar intents ou serviços para iniciar seu app e a reprodução. Para conseguir os melhores resultados, o app precisa implementar todos os recursos descritos nesta página.

Usar uma sessão de mídia

Cada app de áudio e vídeo precisa implementar uma sessão de mídiapara que o Assistente possa operar os controles de transporte quando a reprodução for iniciada. Ative os controles de mídia e transporte definindo essas sinalizações no objeto MediaSession do seu app:

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);
    

A sessão de mídia do app precisa declarar as ações compatíveis e implementar os callbacks de sessão de mídia correspondentes. Declare as ações compatíveis em setActions().

O projeto de amostra Universal Music Player (link em inglês) é um bom exemplo de como configurar uma sessão de mídia.

Ações de reprodução

Para iniciar a reprodução a partir de um serviço, uma sessão de mídia precisa ter as seguintes ações (PLAY) e callbacks correspondentes:

Ação Callback
ACTION_PLAY onPlay()
ACTION_PLAY_FROM_SEARCH onPlayFromSearch()
ACTION_PLAY_FROM_URI (*) onPlayFromUri()

A sessão também precisa implementar estas ações de preparação (PREPARE) e os callbacks correspondentes:

Ação Callback
ACTION_PREPARE onPrepare()
ACTION_PREPARE_FROM_SEARCH onPrepareFromSearch()
ACTION_PREPARE_FROM_URI (*) onPrepareFromUri()

(*) Ações baseadas em URIs do Google Assistente só funcionam para empresas que fornecem URIs ao Google. Para saber mais sobre como descrever o conteúdo de mídia para o Google, consulte Ações de mídia (link em inglês).

Ao implementar as APIs de preparação, a latência de reprodução após um comando de voz pode ser reduzida. Os apps de mídia que quiserem melhorar a latência de reprodução poderão usar o tempo extra para iniciar o armazenamento em cache do conteúdo e preparar a reprodução de mídia.

Embora o Assistente use somente as ações listadas nesta seção, a prática recomendada é implementar todas as APIs de preparação e reprodução para garantir compatibilidade com outros apps.

Analisar consultas de pesquisa

Quando um usuário pesquisa um item de mídia específico, como “Tocar jazz no [nome do app]” ou “Ouvir [título da música]”, o método de callback onPrepareFromSearch() ou onPlayFromSearch() recebe um parâmetro de consulta e um pacote de extras.

O app precisa analisar a consulta de pesquisa por voz e iniciar a reprodução seguindo estas etapas:

  1. Use o pacote de extras e a string de consulta de pesquisa retornada da pesquisa por voz para filtrar os resultados.
  2. Crie uma fila de reprodução com base nesses resultados.
  3. Toque o item de mídia mais relevante dos resultados.

O método onPlayFromSearch() usa um parâmetro para extras com informações mais detalhadas da pesquisa por voz. Esses extras ajudam a encontrar o conteúdo de áudio a ser tocado no app. Se os resultados da pesquisa não fornecerem esses dados, você poderá implementar uma lógica para analisar a consulta de pesquisa bruta e reproduzir as faixas adequadas com base na consulta.

Os seguintes extras são compatíveis com o Android Automotive OS e o Android Auto:

O snippet de código a seguir mostra como substituir o método onPlayFromSearch() na implementação de MediaSession.Callback para analisar a consulta de pesquisa por voz e iniciar a reprodução:

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

Para ver um exemplo mais detalhado de como implementar a pesquisa por voz para tocar conteúdo de áudio no app, consulte o exemplo do Universal Music Player (link em inglês).

Gerenciar consultas vazias

Se onPrepare(), onPlay(), onPrepareFromSearch() ou onPlayFromSearch() forem chamados sem uma consulta de pesquisa, o app precisará reproduzir a mídia atual. Se não houver mídia atual, o app precisará tentar tocar algo, como uma música da playlist mais recente ou uma lista aleatória. O Assistente usa essas APIs quando um usuário pede para “Tocar música no [nome do app]” sem informações adicionais.

Quando um usuário diz "Tocar música no [nome do app]", o Android Automotive OS ou o Android Auto tenta iniciar o app e tocar áudio chamando o método onPlayFromSearch(). No entanto, como o usuário não informou o nome do item de mídia, o método onPlayFromSearch() recebe um parâmetro de consulta vazio. Nesses casos, o app precisa responder imediatamente tocando áudio, como uma música da playlist mais recente ou de uma lista aleatória.

Declarar compatibilidade legada para ações de voz

Na maioria dos casos, gerenciar as ações de reprodução descritas acima dá ao app toda a funcionalidade de reprodução de que ele precisa. No entanto, alguns sistemas exigem que app contenha um filtro de intent para a pesquisa. Declare compatibilidade para este filtro de intent nos arquivos do manifesto do app.

Inclua este código no arquivo de manifesto de um app para smartphone e também para um módulo do Android Automotive OS, se houver:

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

Controles de transporte

Depois que a sessão de mídia do app estiver ativa, o Assistente poderá emitir comandos de voz para controlar a reprodução e atualizar os metadados de mídia. Para que isso funcione, seu código precisa ativar as seguintes ações e implementar os callbacks correspondentes:

Ação Callback Descrição
ACTION_SKIP_TO_NEXT onSkipToNext() Próximo vídeo
ACTION_SKIP_TO_PREVIOUS onSkipToPrevious() Música anterior
ACTION_PAUSE, ACTION_PLAY_PAUSE onPause() Pausar
ACTION_STOP onStop() Parar
ACTION_PLAY onPlay() Retomar
ACTION_SEEK_TO onSeekTo() Retroceder 10 segundos
ACTION_SET_RATING onSetRating(android.support.v4.media.RatingCompat) Sinal de Gostei/Não gostei
ACTION_SET_CAPTIONING_ENABLED onSetCaptioningEnabled(boolean) Ativar/desativar legendas

Observações:

  • Para que comandos de busca funcionem, o PlaybackState precisa estar atualizado com state, position, playback speed, and update time. O app precisa chamar setPlaybackState() quando o estado for modificado.
  • O app também precisa manter os metadados da sessão de mídia atualizados. Isso é compatível com perguntas como "qual música está tocando?" O app precisa chamar setMetadata() quando os campos aplicáveis (como título da faixa, artista e nome) são modificados.
  • MediaSession.setRatingType() precisa ser definido para indicar o tipo de avaliação compatível e o app precisa implementar onSetRating(). Se o app não for compatível com classificação, ele precisará definir o tipo como RATING_NONE.

Erros

Quando ocorrem erros, o Assistente os gerencia a partir de uma sessão de mídia e os informa aos usuários. Certifique-se de que sua sessão de mídia atualize o estado de transporte e o código de erro no PlaybackState corretamente, conforme descrito em Como trabalhar com uma sessão de mídia. O Assistente reconhece todos os códigos de erro retornados por getErrorCode().

Reprodução com um intent

O Assistente pode iniciar um app de áudio ou vídeo e iniciar a reprodução enviando um intent com um link direto.

O intent e o link direto podem ter origens diferentes:

  • Quando o Assistente inicia um app para dispositivos móveis, ele pode usar a Pesquisa Google para recuperar um conteúdo marcado que fornece uma ação de assistir com um link.
  • Quando o Assistente inicia um app de TV, o app precisa incluir um provedor de pesquisa de TV para expor URIs para conteúdo de mídia. O Assistente envia uma consulta ao provedor de conteúdo, que precisa retornar um intent contendo um URI para o link direto e uma ação opcional. Se a consulta retornar uma ação no intent, o Assistente enviará essa ação e o URI de volta ao app. Se o provedor não especificou uma ação, o Assistente adicionará ACTION_VIEW ao intent.

O Assistente adiciona o EXTRA_START_PLAYBACK extra com valor true ao intent que ele envia ao app. O app precisa iniciar a reprodução quando receber um intent com EXTRA_START_PLAYBACK.

Como gerenciar intents enquanto ativo

Os usuários podem pedir ao Assistente para reproduzir algo enquanto o app ainda está reproduzindo conteúdo de uma solicitação anterior. Isso significa que o app pode receber novos intents para iniciar a reprodução enquanto a atividade de reprodução já foi iniciada e está ativa.

As atividades compatíveis com intents com links diretos precisam substituir onNewIntent() para processar novas solicitações.

Ao iniciar a reprodução, o Assistente pode adicionar outras sinalizações ao intent que envia para o app. Em particular, ele pode adicionar FLAG_ACTIVITY_CLEAR_TOP ou FLAG_ACTIVITY_NEW_TASK ou ambos. Embora o código não precise gerenciar essas sinalizações, o sistema Android responde a elas. Isso pode afetar o comportamento do app quando uma segunda solicitação de reprodução com um novo URI chegar enquanto o URI anterior ainda estiver sendo reproduzido. É uma boa ideia testar como o app responde nesse caso. Você pode usar a ferramenta de linha de comando adb para simular a situação. A constante 0x14000000 é o bit a bit booleano OU das duas sinalizações:

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
    

Reprodução a partir de um serviço

Se o app tiver um media browser service que permite conexões do Assistente, o Assistente poderá iniciar o app comunicando-se com o media session do serviço. O serviço de navegador de mídia não pode iniciar uma atividade. O Assistente iniciará a atividade com base no PendingIntent que você definir com setSessionActivity().

Defina MediaSession.Token quando inicializar o serviço de navegador de mídia. Lembre-se de definir as ações de reprodução compatíveis em todos os momentos, inclusive durante a inicialização. O Assistente espera que o app de música defina as ações de reprodução antes que o Assistente envie o primeiro comando de reprodução.

Para iniciar a partir de um serviço, o Assistente implementa as APIs de cliente do navegador de mídia. Ele realiza chamadas TransportControls que acionam callbacks de ação PLAY na sessão de mídia do app.

O diagrama a seguir mostra a ordem das chamadas geradas pelo Assistente e os callbacks de sessão de mídia correspondentes. Os callbacks de preparação são enviados somente se o app é compatível com eles. Todas as chamadas são assíncronas. O Assistente não espera nenhuma resposta do app.

Iniciar a reprodução com uma sessão de mídia

Quando um usuário emite um comando de voz para reprodução, o Assistente responde com um breve aviso. Assim que o aviso é concluído, o Assistente emite uma ação PLAY. Ele não espera nenhum estado de reprodução específico.

Se o app é compatível com as ações ACTIONPREPARE*, o Assistente chama a ação PREPARE antes de iniciar o aviso.

Como se conectar a um MediaBrowserService

Para usar um serviço para iniciar o app, o Assistente precisa ser capaz de se conectar ao MediaBrowserService do app e recuperar o MediaSession.Token dele. As solicitações de conexão são processadas no método onGetRoot() do serviço. Há duas maneiras de processar solicitações:

  • Aceitar todas as solicitações de conexão
  • Aceitar solicitações de conexão apenas do app Assistente

Aceitar todas as solicitações de conexão

Você precisa retornar um BrowserRoot para permitir que o Assistente envie comandos para a sessão de mídia. A maneira mais fácil é permitir que todos os apps do MediaBrowser se conectem ao MediaBrowserService. Você precisa retornar um BrowserRoot não nulo. Veja o código aplicável do Universal Music Player (link em inglês):

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

Aceitar o pacote e a assinatura do app do Assistente

Você pode permitir explicitamente que o Assistente se conecte ao serviço de navegador de mídia verificando o nome e a assinatura do pacote. O app receberá o nome do pacote no método onGetRoot do MediaBrowserService. Você precisa retornar um BrowserRoot para permitir que o Assistente envie comandos para a sessão de mídia. A amostra Universal Music Player (link em inglês) mantém uma lista de nomes de pacotes e assinaturas conhecidas. Veja abaixo os nomes dos pacotes e as assinaturas usadas pelo Google Assistente.

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