Desenvolver um serviço de entrada para TV

Um serviço de entrada de TV representa uma fonte de streaming de mídia e permite que você apresente seu conteúdo de mídia de maneira linear, como canais e programas. Com um serviço de entrada de TV, é possível fornecer controle da família, informações sobre o guia de programação e classificações de conteúdo. O serviço de entrada de TV funciona com o app de TV do sistema Android. Esse app controla e apresenta o conteúdo de canais na TV. O app de TV do sistema foi desenvolvido especificamente para o dispositivo e não pode ser modificado por apps de terceiros. Para saber mais sobre a arquitetura do TV Input Framework (TIF) e os componentes dela, consulte TV Input Framework (link em inglês).

Criar um serviço de entrada de TV usando a TIF Companion Library

A TIF Companion Library é um framework que oferece implementações extensíveis de recursos comuns de serviços de entrada de TV. Ela precisa ser usada por OEMs para criar canais do Android 5.0 (nível 21 da API) ao Android 7.1 (nível 25 da API).

Atualizar seu projeto

A TIF Companion Library está disponível para uso legado por OEMs no repositório androidtv-sample-inputs. Confira nesse repositório um exemplo de como incluir a biblioteca em um app.

Declarar seu serviço de entrada de TV no manifesto

Seu app precisa oferecer um serviço compatível com TvInputService que o sistema use para acessar o app. A Biblioteca complementar TIF fornece a classe BaseTvInputService, que fornece uma implementação padrão de TvInputService que pode ser personalizada. Crie uma subclasse de BaseTvInputService e declare-a no seu manifesto como um serviço.

Na declaração do manifesto, especifique a permissão BIND_TV_INPUT para que o serviço conecte a entrada de TV ao sistema. Um serviço do sistema executa a vinculação e tem a permissão BIND_TV_INPUT. O app de TV do sistema envia solicitações aos serviços de entrada de TV pela interface TvInputManager.

Na declaração de serviço, inclua um filtro de intent que especifique TvInputService como a ação a ser executada com a intent. Além disso, declare os metadados de serviço com um recurso XML separado. A declaração de serviço, o filtro de intent e a declaração de metadados de serviço são mostrados neste exemplo:

<service android:name=".rich.RichTvInputService"
    android:label="@string/rich_input_label"
    android:permission="android.permission.BIND_TV_INPUT">
    <!-- Required filter used by the system to launch our account service. -->
    <intent-filter>
        <action android:name="android.media.tv.TvInputService" />
    </intent-filter>
    <!-- An XML file which describes this input. This provides pointers to
    the RichTvInputSetupActivity to the system/TV app. -->
    <meta-data
        android:name="android.media.tv.input"
        android:resource="@xml/richtvinputservice" />
</service>

Defina os metadados de serviço em um arquivo XML separado. O arquivo XML de metadados do serviço precisa incluir uma interface de configuração que descreva a configuração inicial e a busca de canais da entrada de TV. O arquivo de metadados também precisa conter uma sinalização que indique se os usuários podem ou não registrar conteúdo. Para mais informações sobre como oferecer suporte à gravação de conteúdo no seu app, consulte Suporte à gravação de conteúdo.

O arquivo de metadados do serviço está localizado no diretório de recursos XML do seu app e precisa corresponder ao nome do recurso que você declarou no manifesto. Usando as entradas de manifesto do exemplo anterior, você criaria o arquivo XML em res/xml/richtvinputservice.xml, com o seguinte conteúdo:

<?xml version="1.0" encoding="utf-8"?>
<tv-input xmlns:android="http://schemas.android.com/apk/res/android"
  android:canRecord="true"
  android:setupActivity="com.example.android.sampletvinput.rich.RichTvInputSetupActivity" />

Definir canais e criar atividades de configuração

O serviço de entrada de TV precisa definir pelo menos um canal que os usuários acessem pelo app de TV do sistema. Registre seus canais no banco de dados do sistema e forneça uma atividade de configuração que o sistema invoca quando não consegue encontrar um canal para o app.

Primeiro, permita que o app leia e grave no Guia Eletrônico de programação (EPG, na sigla em inglês) do sistema, cujos dados incluem canais e programas disponíveis para o usuário. Para permitir que seu app execute essas ações e sincronize com o EPG após a reinicialização do dispositivo, adicione os seguintes elementos ao manifesto do app:

<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED "/>

Adicione o elemento abaixo para garantir que seu app apareça na Google Play Store como um app que oferece canais de conteúdo no Android TV:

<uses-feature
    android:name="android.software.live_tv"
    android:required="true" />

Em seguida, crie uma classe que estenda a classe EpgSyncJobService. Essa classe abstrata facilita a criação de um serviço de job que cria e atualiza canais no banco de dados do sistema.

Na subclasse, crie e retorne sua lista completa de canais em getChannels(). Caso seus canais venham de um arquivo XMLTV, use a classe XmlTvParser. Caso contrário, gere canais de forma programática usando a classe Channel.Builder.

Para cada canal, o sistema chama getProgramsForChannel() quando precisa de uma lista de programas que podem ser visualizados em uma determinada janela de tempo no canal. Retorne uma lista de objetos Program para o canal. Use a classe XmlTvParser para extrair programas de um arquivo XMLTV ou gere-os de forma programática usando a classe Program.Builder.

Para cada objeto Program, use um objeto InternalProviderData para definir informações do programa, como o tipo de vídeo dele. Se você tiver apenas um número limitado de programas que você quer que o canal repita em loop, use o método InternalProviderData.setRepeatable() com o valor de true ao definir informações sobre seu programa.

Depois de implementar o serviço de tarefas, adicione-o ao manifesto do seu app:

<service
    android:name=".sync.SampleJobService"
    android:permission="android.permission.BIND_JOB_SERVICE"
    android:exported="true" />

Para terminar, crie uma atividade de configuração. Sua atividade de configuração precisa oferecer uma maneira de sincronizar os dados dos canais e programas. Uma maneira de fazer isso é o usuário fazer isso usando a IU na atividade. O app também pode fazer isso automaticamente quando a atividade começar. Quando a atividade de configuração precisa sincronizar informações do canal e do programa, o app precisa iniciar o serviço de tarefas:

Kotlin

val inputId = getActivity().intent.getStringExtra(TvInputInfo.EXTRA_INPUT_ID)
EpgSyncJobService.cancelAllSyncRequests(getActivity())
EpgSyncJobService.requestImmediateSync(
        getActivity(),
        inputId,
        ComponentName(getActivity(), SampleJobService::class.java)
)

Java

String inputId = getActivity().getIntent().getStringExtra(TvInputInfo.EXTRA_INPUT_ID);
EpgSyncJobService.cancelAllSyncRequests(getActivity());
EpgSyncJobService.requestImmediateSync(getActivity(), inputId,
        new ComponentName(getActivity(), SampleJobService.class));

Use o método requestImmediateSync() para sincronizar o serviço de tarefas. O usuário precisa esperar a sincronização terminar. Portanto, mantenha o período de solicitação relativamente curto.

Use o método setUpPeriodicSync() para que o serviço de tarefas sincronize periodicamente os dados dos canais e programas em segundo plano:

Kotlin

EpgSyncJobService.setUpPeriodicSync(
        context,
        inputId,
        ComponentName(context, SampleJobService::class.java)
)

Java

EpgSyncJobService.setUpPeriodicSync(context, inputId,
        new ComponentName(context, SampleJobService.class));

A TIF Companion Library oferece outro método sobrecarregado de requestImmediateSync(), que permite especificar a duração da sincronização dos dados do canal em milissegundos. O método padrão sincroniza o equivalente a uma hora de dados de canais.

A biblioteca TIF Companion também oferece outro método sobrecarregado de setUpPeriodicSync(), que permite especificar a duração da sincronização dos dados do canal e a frequência com que a sincronização periódica vai ocorrer. O método padrão sincroniza 48 horas de dados de canais a cada 12 horas.

Para ver mais detalhes sobre dados de canais e o EPG, consulte Trabalhar com dados de canais.

Processar solicitações de sintonização e reprodução de mídia

Quando um usuário seleciona um canal específico, o app de TV do sistema usa um Session, criado pelo app, para sintonizar o canal solicitado e abrir conteúdo. A TIF Companion Library oferece várias classes que você pode estender para lidar com chamadas de canais e sessões do sistema.

A subclasse BaseTvInputService cria sessões que processam solicitações de ajuste. Modifique o método onCreateSession(), crie uma sessão estendida da classe BaseTvInputService.Session e chame super.sessionCreated() com a nova sessão. No exemplo a seguir, onCreateSession() retorna um objeto RichTvInputSessionImpl que estende BaseTvInputService.Session:

Kotlin

override fun onCreateSession(inputId: String): Session =
        RichTvInputSessionImpl(this, inputId).apply {
            setOverlayViewEnabled(true)
        }

Java

@Override
public final Session onCreateSession(String inputId) {
    RichTvInputSessionImpl session = new RichTvInputSessionImpl(this, inputId);
    session.setOverlayViewEnabled(true);
    return session;
}

Quando o usuário usa o app de TV do sistema para começar a visualizar um dos seus canais, o sistema chama o método onPlayChannel() da sessão. Modifique esse método se você precisar fazer qualquer inicialização de canal especial antes que o programa comece a ser reproduzido.

Em seguida, o sistema recebe o programa que está agendado e chama o método onPlayProgram() da sua sessão, especificando as informações do programa e o horário de início em milissegundos. Use a interface TvPlayer para começar a reproduzir o programa.

O código do seu player de mídia precisa implementar TvPlayer para processar eventos de reprodução específicos. A classe TvPlayer processa recursos como controles de time-shifting sem aumentar a complexidade da sua implementação de BaseTvInputService.

No método getTvPlayer() da sua sessão, retorne o player de mídia que implementa TvPlayer. O app de exemplo Serviço de entrada de TV (link em inglês) implementa um player de mídia que usa o ExoPlayer.

Criar um serviço de entrada de TV usando o framework de entrada de TV

Se o serviço de entrada de TV não puder usar a TIF Companion Library, você precisará implementar os seguintes componentes:

  • TvInputService oferece disponibilidade de longa duração e em segundo plano para a entrada de TV.
  • TvInputService.Session mantém o estado da entrada de TV e se comunica com o app host.
  • TvContract descreve os canais e programas disponíveis para a entrada de TV
  • TvContract.Channels representa informações sobre um canal de TV.
  • TvContract.Programs descreve um programa de TV com dados como título do programa e horário de início.
  • TvTrackInfo representa uma faixa de áudio, vídeo ou legendas.
  • O TvContentRating descreve uma classificação de conteúdo e permite esquemas personalizados de classificações de conteúdo.
  • TvInputManager fornece uma API para o app de TV do sistema e gerencia a interação com entradas e apps de TV.

Você também precisa fazer o seguinte:

  1. Declare o serviço de entrada de TV no manifesto, conforme descrito em Declarar o serviço de entrada de TV no manifesto.
  2. Crie o arquivo de metadados do serviço.
  3. Crie e registre suas informações de canais e programas.
  4. Crie suas atividades de configuração.

Definir o serviço de entrada de TV

Estenda a classe TvInputService para seu serviço. Uma implementação de TvInputService é um serviço vinculado em que o serviço do sistema é o cliente que se vincula a ele. Os métodos de ciclo de vida do serviço que você precisa implementar estão ilustrados na Figura 1.

O método onCreate() inicializa e inicia a HandlerThread, que fornece uma linha de execução do processo separada da linha de execução de IU para processar ações do sistema. No exemplo a seguir, o método onCreate() inicializa o CaptioningManager e se prepara para processar as ações ACTION_BLOCKED_RATINGS_CHANGED e ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED. Essas ações descrevem as intents do sistema disparadas quando o usuário muda as configurações do controle da família e quando há uma mudança na lista de classificações bloqueadas.

Kotlin

override fun onCreate() {
    super.onCreate()
    handlerThread = HandlerThread(javaClass.simpleName).apply {
        start()
    }
    dbHandler = Handler(handlerThread.looper)
    handler = Handler()
    captioningManager = getSystemService(Context.CAPTIONING_SERVICE) as CaptioningManager

    setTheme(android.R.style.Theme_Holo_Light_NoActionBar)

    sessions = mutableListOf<BaseTvInputSessionImpl>()
    val intentFilter = IntentFilter().apply {
        addAction(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED)
        addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED)
    }
    registerReceiver(broadcastReceiver, intentFilter)
}

Java

@Override
public void onCreate() {
    super.onCreate();
    handlerThread = new HandlerThread(getClass()
      .getSimpleName());
    handlerThread.start();
    dbHandler = new Handler(handlerThread.getLooper());
    handler = new Handler();
    captioningManager = (CaptioningManager)
      getSystemService(Context.CAPTIONING_SERVICE);

    setTheme(android.R.style.Theme_Holo_Light_NoActionBar);

    sessions = new ArrayList<BaseTvInputSessionImpl>();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(TvInputManager
      .ACTION_BLOCKED_RATINGS_CHANGED);
    intentFilter.addAction(TvInputManager
      .ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
    registerReceiver(broadcastReceiver, intentFilter);
}

Figura 1. Ciclo de vida da classe TvInputService.

Consulte Controlar conteúdo para mais informações sobre como trabalhar com conteúdo bloqueado e oferecer controle dos pais. Consulte TvInputManager para conhecer mais ações do sistema que você pode querer processar no seu serviço de entrada de TV.

O TvInputService cria um TvInputService.Session que implementa Handler.Callback para processar as mudanças de estado do player. Com onSetSurface(), o TvInputService.Session define Surface com o conteúdo de vídeo. Consulte Integrar o player com a superfície para ver mais informações sobre como trabalhar com Surface para renderizar vídeos.

O TvInputService.Session processa o evento onTune() quando o usuário seleciona um canal e notifica o app de TV do sistema sobre mudanças no conteúdo e nos metadados de conteúdo. Esses métodos notify() são descritos em Controlar conteúdo e Processar a seleção de faixas mais adiante neste treinamento.

Definir atividades de configuração

O app de TV do sistema trabalha com as atividades de configuração definidas para sua entrada de TV. A atividade de configuração é necessária e precisa fornecer pelo menos um registro de canal para o banco de dados do sistema. O app de TV do sistema invoca a atividade de configuração quando não consegue encontrar um canal para a entrada de TV.

A atividade de configuração descreve ao app de TV do sistema os canais disponibilizados pela entrada de TV, conforme demonstrado na próxima lição, Criar e atualizar dados de canais.

Outras referências