Cómo desarrollar un servicio de entrada de TV

Un servicio de entrada de TV representa una fuente de transmisión multimedia y te permite presentar tu contenido multimedia como se transmite en TV como canales y programas. Con un servicio de entrada de TV, puedes proporcionar controles parentales, información de la guía de programas y clasificaciones de contenido. El servicio de entrada de TV funciona con la app de TV del sistema Android. En última instancia, esta app controla y presenta contenido de canales en la TV. La app de TV del sistema se desarrolla específicamente para el dispositivo y es inmutable por apps de terceros. Para obtener más información sobre la arquitectura del marco de trabajo de entrada de TV (TIF) y sus componentes, consulta Marco de trabajo de entrada de TV.

Cómo crear un servicio de entrada de TV usando la Biblioteca complementaria del TIF

La Biblioteca complementaria del TIF es un framework que proporciona implementaciones extensibles de funciones comunes del servicio de entrada de TV. Está destinado a que los OEM lo usen a fin de compilar canales para Android 5.0 (nivel de API 21) hasta Android 7.1 (nivel de API 25).

Cómo actualizar tu proyecto

La Biblioteca complementaria del TIF está disponible para que los OEM la usen de forma heredada en el repositorio androidtv-sample-inputs. Consulta ese repositorio para ver un ejemplo de cómo incluir la biblioteca en una app.

Cómo declarar tu servicio de entrada de TV en el manifiesto

Tu app debe proporcionar un servicio compatible con TvInputService que el sistema use para acceder a ella. La Biblioteca complementaria del TIF proporciona la clase BaseTvInputService, que brinda una implementación predeterminada de TvInputService que puedes personalizar. Crea una subclase de BaseTvInputService y declárala en tu manifiesto como servicio.

En la declaración del manifiesto, especifica el permiso BIND_TV_INPUT para permitir que el servicio conecte la entrada de TV al sistema. Un servicio del sistema realiza la vinculación y tiene el permiso BIND_TV_INPUT. La app de TV del sistema envía solicitudes a los servicios de entrada de TV a través de la interfaz TvInputManager.

En tu declaración de servicio, incluye un filtro de intents que especifique TvInputService como la acción a realizar con el intent. También declara los metadatos del servicio como recurso XML independiente. La declaración del servicio, el filtro de intents y la declaración de metadatos del servicio se muestran en el siguiente ejemplo:

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

Define los metadatos del servicio en un archivo XML independiente. El archivo en formato XML de metadatos del servicio debe incluir una interfaz de configuración que describa la configuración inicial de la entrada de TV y la búsqueda de canales. El archivo de metadatos también debe contener una marca que indique si los usuarios pueden grabar contenido o no. Si quieres obtener más información para admitir la grabación de contenido en tu app, consulta Cómo admitir la grabación de contenido.

El archivo de metadatos del servicio se encuentra en el directorio de recursos XML de tu app y debe coincidir con el nombre del recurso que declaraste en el manifiesto. Con las entradas de manifiesto del ejemplo anterior, puedes crear el archivo en formato XML en res/xml/richtvinputservice.xml, con el siguiente contenido:

<?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" />

Cómo definir canales y crear tu actividad de configuración

El servicio de entrada de TV debe definir al menos un canal al que los usuarios accedan a través de la app de TV del sistema. Debes registrar los canales en la base de datos del sistema y proporcionar una actividad de configuración que el sistema invoque cuando no encuentre un canal para tu app.

Primero, permite que tu app lea y escriba en la Guía de programación electrónica (EPG) del sistema, cuyos datos incluyen canales y programas disponibles para el usuario. Para permitir que tu app realice estas acciones y sincronizar con el EG después de reiniciar el dispositivo, agrega los siguientes elementos al manifiesto de la app:

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

Agrega el siguiente elemento para asegurarte de que tu app aparezca en Google Play Store como una app que proporciona canales de contenido en Android TV:

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

A continuación, crea una clase que extienda la clase EpgSyncJobService. Esta clase abstracta facilita la creación de un servicio de trabajo que crea y actualiza canales en la base de datos del sistema.

En la subclase, crea y muestra la lista completa de canales en getChannels(). Si tus canales provienen de un archivo XMLTV, usa la clase XmlTvParser. De lo contrario, genera canales de manera programática con la clase Channel.Builder.

Para cada canal, el sistema llama a getProgramsForChannel() cuando necesita una lista de programas que se puedan ver dentro de un período determinado en el canal. Muestra una lista de objetos Program para el canal. Usa la clase XmlTvParser para obtener programas desde un archivo XMLTV o genéralos de manera programática con la clase Program.Builder.

Por cada objeto Program, usa un objeto InternalProviderData para establecer información del programa, como el tipo de video del programa. Si solo quieres que el canal repita indefinidamente una cantidad limitada de programas, usa el método InternalProviderData.setRepeatable() con un valor de true cuando configures la información sobre el programa.

Después de implementar el servicio de trabajo, agrégalo al manifiesto de tu app:

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

Finalmente, crea una actividad de configuración. Tu actividad de configuración debe proporcionar una manera de sincronizar los datos del canal y del programa. Una forma de hacerlo es que el usuario lo haga a través de la IU en la actividad. También puedes hacer que la app lo haga automáticamente cuando comience la actividad. Cuando la actividad de configuración necesita sincronizar la información del canal y del programa, la app debe iniciar el servicio de trabajo:

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

Usa el método requestImmediateSync() para sincronizar el servicio de trabajo. El usuario debe esperar a que finalice la sincronización, por lo que debes mantener un período de solicitud relativamente corto.

Usa el método setUpPeriodicSync() para que el servicio de trabajo sincronice de forma periódica los datos del canal y del programa en segundo plano:

Kotlin

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

Java

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

La Biblioteca complementaria del TIF proporciona un método adicional de requestImmediateSync() sobrecargado que te permite especificar la duración en milisegundos de los datos del canal que se sincronizarán. El método predeterminado sincroniza una hora de datos del canal.

La Biblioteca complementaria del TIF proporciona un método adicional de setUpPeriodicSync() sobrecargado que te permite especificar la duración de los datos del canal que se sincronizarán y la frecuencia con la que se realizará la sincronización periódica. El método predeterminado sincroniza 48 horas de datos del canal cada 12 horas.

Para obtener más detalles sobre los datos del canal y la EPG, consulta Cómo trabajar con datos del canal.

Cómo manejar solicitudes de sintonización y reproducción de contenido multimedia

Cuando un usuario selecciona un canal específico, la app de TV del sistema usa un Session, creado por tu app, para sintonizar el canal solicitado y reproducir contenido. La Biblioteca complementaria del TIF proporciona varias clases que puedes extender para controlar las llamadas de canal y sesión desde el sistema.

La subclase BaseTvInputService crea sesiones que controlan solicitudes de ajuste. Anula el método onCreateSession(), crea una sesión extendida desde la clase BaseTvInputService.Session y llama a super.sessionCreated() con tu sesión nueva. En el siguiente ejemplo, onCreateSession() muestra un objeto RichTvInputSessionImpl que extiende 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;
}

Cuando el usuario utiliza la app de TV del sistema para comenzar a ver uno de tus canales, el sistema llama al método onPlayChannel() de tu sesión. Anula este método si necesitas realizar una inicialización de canal especial antes de que se comience a reproducir el programa.

Luego, el sistema obtiene el programa programado actualmente, llama al método onPlayProgram() de tu sesión y especifica la información del programa y la hora de inicio en milisegundos. Usa la interfaz TvPlayer para comenzar a reproducir el programa.

El código de tu reproductor multimedia debería implementar TvPlayer para controlar eventos de reproducción específicos. La clase TvPlayer controla funciones como los controles de pausa en directo sin agregar complejidad a tu implementación de BaseTvInputService.

En el método getTvPlayer() de tu sesión, muestra el reproductor multimedia que implementa TvPlayer. La app de ejemplo del servicio de entrada de TV implementa un reproductor multimedia que usa ExoPlayer.

Cómo crear un servicio de entrada de TV con el marco de trabajo de entrada de TV

Si tu servicio de entrada de TV no puede usar la Biblioteca complementaria del TIF, debes implementar los siguientes componentes:

  • TvInputService, que proporciona disponibilidad de larga duración y en segundo plano para la entrada de TV
  • TvInputService.Session: Mantiene el estado de la entrada de TV y se comunica con la app host.
  • TvContract describe los canales y programas disponibles para la entrada de TV.
  • TvContract.Channels, que representa información acerca de un canal de TV.
  • TvContract.Programs describe un programa de TV con datos como el título y la hora de inicio del programa.
  • TvTrackInfo, que representa una pista de audio, video o subtítulo.
  • TvContentRating describe una clasificación del contenido y permite esquemas personalizados de clasificación.
  • TvInputManager proporciona una API a la app de TV del sistema y administra la interacción con las entradas y apps de TV.

También debes realizar lo siguiente:

  1. Declara tu servicio de entrada de TV en el manifiesto, como se describe en Cómo declarar tu servicio de entrada de TV en el manifiesto.
  2. Crea el archivo de metadatos del servicio.
  3. Crea y registra la información del canal y el programa.
  4. Crea la actividad de configuración.

Cómo definir tu servicio de entrada de TV

Para tu servicio, extiende la clase TvInputService. Una implementación de TvInputService es un servicio vinculado en el que el servicio del sistema es el cliente que lo vincula. Los métodos de ciclo de vida de servicio que debes implementar se ilustran en la Figura 1.

El método onCreate() inicializa e inicia el HandlerThread, que proporciona un subproceso independiente del subproceso de IU para controlar acciones impulsadas por el sistema. En el siguiente ejemplo, el método onCreate() inicializa el CaptioningManager y se prepara para controlar las acciones ACTION_BLOCKED_RATINGS_CHANGED y ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED. Estas acciones describen los intents del sistema que se activan cuando el usuario cambia la configuración de controles parentales y cuando hay un cambio en la lista de calificaciones 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 de TvInputService

Consulta Contenido de control para obtener más información sobre cómo trabajar con contenido bloqueado y proporcionar control parental. Consulta TvInputManager para ver más acciones impulsadas por el sistema que puedes controlar en tu servicio de entrada de TV.

El TvInputService crea un TvInputService.Session que implementa Handler.Callback para controlar los cambios de estado del reproductor. Con onSetSurface(), TvInputService.Session establece el objeto Surface con el contenido de video. Consulta Cómo integrar el reproductor con Surface para obtener más información sobre cómo trabajar con Surface en el procesamiento de videos.

El TvInputService.Session controla el evento onTune() cuando el usuario selecciona un canal y notifica a la app de TV del sistema cuando hay cambios en el contenido y los metadatos de contenido. Estos métodos notify() se describen en Control de contenido y Cómo controlar la selección de pistas más adelante en esta capacitación.

Cómo definir la actividad de configuración

La app de TV del sistema funciona con la actividad de configuración que defines para tu entrada de TV. La actividad de configuración es obligatoria y debe proporcionar al menos un registro de canal para la base de datos del sistema. La app de TV del sistema invoca la actividad de configuración cuando no puede encontrar un canal para la entrada de TV.

La actividad de configuración describe a la app de TV los canales disponibles a través de la entrada de TV, como se demuestra en la siguiente lección, Cómo crear y actualizar los datos de canales.

Referencias adicionales