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 el contenido multimedia como se emite en la TV, es decir, de forma lineal, con 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 contenidos. 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 desarrolló específicamente para el dispositivo y no puede ser reemplazada 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 marco de trabajo que proporciona implementaciones extensibles de funciones comunes de servicio de entrada de TV. Usa la Biblioteca complementaria del TIF para crear de manera rápida y sencilla tu propio servicio de entrada de TV que siga las prácticas recomendadas para Android TV.

Cómo actualizar tu proyecto

Para comenzar a usar la Biblioteca complementaria del TIF, agrega lo siguiente al archivo build.gradle de tu app:

    compile 'com.google.android.libraries.tv:companionlibrary:0.2'
    

Por el momento, la Biblioteca complementaria del TIF no forma parte del marco de trabajo de Android. Se distribuye como una dependencia de Gradle mediante jcenter, no con el SDK de Android. Consulta jcenter para encontrar la versión más reciente de la Biblioteca complementaria del TIF.

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

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

Dentro de 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 cuenta con el permiso BIND_TV_INPUT. La app de TV del sistema envía solicitudes a los servicios de entrada de TV mediante la interfaz de 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 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. Además, el archivo de metadatos debe incluir una marca que indica si los usuarios pueden grabar el contenido o no. Para obtener más información sobre cómo admitir la grabación de contenido en tu app, consulta Grabación de TV.

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

    <?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 puedan acceder mediante la app de TV del sistema. Deberías registrar los canales en la base de datos del sistema y proporcionar una actividad de configuración que el sistema pueda invocar cuando no encuentre un canal para tu app.

Primero, habilita tu app para lectura y escritura en la Guía de programación electrónica (EPG) del sistema, cuyos datos incluyen canales y programas disponibles para el usuario. Si quieres habilitar la app para que lleve a cabo estas acciones y sincronizarla con la EPG después del reinicio del dispositivo, agrega los siguientes elementos en el 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 la app aparezca en Google Play Store como app que proporciona canales de contenido en Android TV:

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

Luego, crea una clase que extienda la clase EpgSyncJobService. Esta clase abstracta permite crear fácilmente 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 usando 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 usando 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 establezcas 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. Esta debería proporcionar una manera de sincronizar los datos del canal y los del programa. El usuario puede hacerlo mediante la IU en la actividad. También puedes hacer que la app lo realice automáticamente cuando se inicia la actividad. Cuando la actividad de configuración necesite sincronizar la información del canal y del programa, la app deberá 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 que finalice la sincronización, de modo que debes mantener un período de solicitud relativamente corto.

Usa el método setUpPeriodicSync() para hacer que el servicio de trabajo sincronice de manera regular 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 van a sincronizar. 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 van a sincronizar y con qué frecuencia se realizará la sincronización regular. 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 una Session, creada por tu app, para sintonizar el canal solicitado y reproducir contenido. La Biblioteca complementaria del TIF proporciona varias clases que puedes extender para manejar las llamadas de canal y sesión desde el sistema.

La subclase BaseTvInputService crea sesiones que manejan solicitudes de sintonización. Anula el método onCreateSession(), crea una sesión extendida desde la clase BaseTvInputService.Session y llama a super.sessionCreated() con tu nueva sesión. 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 el programa comience a reproducir.

Luego, el sistema obtiene el programa agendado, llama al método onPlayProgram() de tu sesión y especifica la información del programa y el tiempo de inicio en milisegundos. Usa la interfaz de 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 maneja funciones como controles con 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 complementos:

  • TvInputService, que proporciona disponibilidad de larga duración y en segundo plano para la entrada de TV.
  • TvInputService.Session, que mantiene el estado de la entrada de TV y se comunica con la app host.
  • TvContract, que 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, que 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, que describe una clasificación de contenido y permite usar esquemas personalizados de clasificación de contenido.
  • TvInputManager, que 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 enlazado en el que el servicio del sistema es el cliente que lo vincula. En la figura 1, se enumeran los métodos de ciclo de vida de servicio que debes implementar.

El método onCreate() inicializa e inicia el HandlerThread, que proporciona un subproceso separado del subproceso de la IU a fin de 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 a los intents del sistema que se activan cuando el usuario cambia la configuración de control parental y cuando hay un cambio en la lista de clasificaciones 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 TvInputService

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

El TvInputService crea una TvInputService.Session que implementa Handler.Callback para controlar los cambios de estado del reproductor. Con onSetSurface(), la TvInputService.Session establece la Surface con el contenido del 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.

La 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 del contenido. Más adelante en esta capacitación, se ofrece una descripción de estos métodos notify() en Cómo controlar el contenido y Cómo controlar la selección de pistas.

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 seguridad 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 datos del canal.

Referencias adicionales