Gerenciar a interação do usuário com a TV

Na experiência de TV ao vivo, o usuário muda de canal e recebe informações sobre canais e programas pouco antes que elas desapareçam. Outros tipos de informações, como mensagens ("NÃO TENTE ISSO EM CASA"), legendas ou anúncios, podem precisar ser persistidas. Como para qualquer app de TV, essas informações não podem interferir no conteúdo da programação na tela.

Figura 1. Mensagem sobreposta em um app de TV ao vivo.

Analise também se determinados conteúdos de programação devem ser apresentados de acordo com a classificação do conteúdo e com as configurações de controle dos pais. Essa decisão também depende de como seu app se comporta e informa o usuário quando um conteúdo está bloqueado ou indisponível. Esta lição descreve como desenvolver a experiência do usuário da sua entrada de TV com essas considerações em mente.

Conheça o app de exemplo Serviço de entrada de TV (em inglês).

Integrar o player com a superfície

Sua entrada de TV precisa renderizar vídeo em um objeto Surface, que é transmitido pelo método TvInputService.Session.onSetSurface(). Veja um exemplo de como usar uma instância MediaPlayer para exibir conteúdo no objeto Surface:

Kotlin

    override fun onSetSurface(surface: Surface?): Boolean {
        player?.setSurface(surface)
        mSurface = surface
        return true
    }

    override fun onSetStreamVolume(volume: Float) {
        player?.setVolume(volume, volume)
        mVolume = volume
    }
    

Java

    @Override
    public boolean onSetSurface(Surface surface) {
        if (player != null) {
            player.setSurface(surface);
        }
        mSurface = surface;
        return true;
    }

    @Override
    public void onSetStreamVolume(float volume) {
        if (player != null) {
            player.setVolume(volume, volume);
        }
        mVolume = volume;
    }
    

Da mesma forma, veja como fazer isso usando o ExoPlayer:

Kotlin

    override fun onSetSurface(surface: Surface?): Boolean {
        player?.createMessage(videoRenderer)?.apply {
            type = MSG_SET_SURFACE
            payload = surface
            send()
        }
        mSurface = surface
        return true
    }

    override fun onSetStreamVolume(volume: Float) {
        player?.createMessage(audioRenderer)?.apply {
            type = MSG_SET_VOLUME
            payload = volume
            send()
        }
        mVolume = volume
    }
    

Java

    @Override
    public boolean onSetSurface(@Nullable Surface surface) {
        if (player != null) {
            player.createMessage(videoRenderer)
                    .setType(MSG_SET_SURFACE)
                    .setPayload(surface)
                    .send();
        }
        mSurface = surface;
        return true;
    }

    @Override
    public void onSetStreamVolume(float volume) {
        if (player != null) {
            player.createMessage(videoRenderer)
                    .setType(MSG_SET_VOLUME)
                    .setPayload(volume)
                    .send();
        }
        mVolume = volume;
    }
    

Usar uma sobreposição

Use uma sobreposição para exibir legendas, mensagens, anúncios ou transmissões de dados MHEG-5. A sobreposição é desativada por padrão. É possível ativá-la ao criar a sessão chamando TvInputService.Session.setOverlayViewEnabled(true), como no exemplo a seguir:

Kotlin

    override fun onCreateSession(inputId: String): Session =
            onCreateSessionInternal(inputId).apply {
                setOverlayViewEnabled(true)
                sessions.add(this)
            }
    

Java

    @Override
    public final Session onCreateSession(String inputId) {
        BaseTvInputSessionImpl session = onCreateSessionInternal(inputId);
        session.setOverlayViewEnabled(true);
        sessions.add(session);
        return session;
    }
    

Use um objeto View para a sobreposição, retornado de TvInputService.Session.onCreateOverlayView(), conforme mostrado aqui:

Kotlin

    override fun onCreateOverlayView(): View =
            (context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater).run {
                inflate(R.layout.overlayview, null).apply {
                    subtitleView = findViewById<SubtitleView>(R.id.subtitles).apply {
                        // Configure the subtitle view.
                        val captionStyle: CaptionStyleCompat =
                                CaptionStyleCompat.createFromCaptionStyle(captioningManager.userStyle)
                        setStyle(captionStyle)
                        setFractionalTextSize(captioningManager.fontScale)
                    }
                }
            }
    

Java

    @Override
    public View onCreateOverlayView() {
        LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.overlayview, null);
        subtitleView = (SubtitleView) view.findViewById(R.id.subtitles);

        // Configure the subtitle view.
        CaptionStyleCompat captionStyle;
        captionStyle = CaptionStyleCompat.createFromCaptionStyle(
                captioningManager.getUserStyle());
        subtitleView.setStyle(captionStyle);
        subtitleView.setFractionalTextSize(captioningManager.fontScale);
        return view;
    }
    

A definição de layout para a sobreposição pode ser semelhante a esta:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"

        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.google.android.exoplayer.text.SubtitleView
            android:id="@+id/subtitles"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|center_horizontal"
            android:layout_marginLeft="16dp"
            android:layout_marginRight="16dp"
            android:layout_marginBottom="32dp"
            android:visibility="invisible"/>
    </FrameLayout>
    

Controlar o conteúdo

Quando o usuário seleciona um canal, sua entrada de TV processa o callback onTune() no objeto TvInputService.Session. O controle dos pais do app de TV do sistema determina o conteúdo a ser exibido de acordo com a classificação. As seções a seguir descrevem como gerenciar a seleção de canais e programas usando os métodos de TvInputService.Session notify que se comunicam com o app de TV do sistema.

Tornar o vídeo indisponível

Quando o usuário muda de canal, é importante garantir que a tela não exiba nenhum artefato de vídeo perdido antes que sua entrada de TV renderize o conteúdo. Ao chamar TvInputService.Session.onTune(), é possível evitar que o vídeo seja apresentado chamando TvInputService.Session.notifyVideoUnavailable() e transmitindo a constante VIDEO_UNAVAILABLE_REASON_TUNING, como mostrado no exemplo a seguir.

Kotlin

    override fun onTune(channelUri: Uri): Boolean {
        subtitleView?.visibility = View.INVISIBLE
        notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING)
        unblockedRatingSet.clear()

        dbHandler.apply {
            removeCallbacks(playCurrentProgramRunnable)
            playCurrentProgramRunnable = PlayCurrentProgramRunnable(channelUri)
            post(playCurrentProgramRunnable)
        }
        return true
    }
    

Java

    @Override
    public boolean onTune(Uri channelUri) {
        if (subtitleView != null) {
            subtitleView.setVisibility(View.INVISIBLE);
        }
        notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
        unblockedRatingSet.clear();

        dbHandler.removeCallbacks(playCurrentProgramRunnable);
        playCurrentProgramRunnable = new PlayCurrentProgramRunnable(channelUri);
        dbHandler.post(playCurrentProgramRunnable);
        return true;
    }
    

Então, quando o conteúdo for renderizado para a Surface, chame TvInputService.Session.notifyVideoAvailable() para permitir a exibição do vídeo, da seguinte forma:

Kotlin

    fun onRenderedFirstFrame(surface:Surface) {
        firstFrameDrawn = true
        notifyVideoAvailable()
    }
    

Java

    @Override
    public void onRenderedFirstFrame(Surface surface) {
        firstFrameDrawn = true;
        notifyVideoAvailable();
    }
    

Essa transição dura frações de segundo, mas a apresentação de uma tela vazia é visualmente melhor do que permitir imagens com linhas estranhas e instáveis.

Consulte Integrar o player com a superfície para ver mais informações sobre como trabalhar com Surface para renderizar vídeos.

Disponibilizar "controle dos pais"

Para determinar se um determinado conteúdo foi bloqueado pelo "controle dos pais" e pela classificação do conteúdo, verifique os métodos isParentalControlsEnabled() e isRatingBlocked(android.media.tv.TvContentRating) da classe TvInputManager. Verifique também se a TvContentRating do conteúdo está incluída em um conjunto de classificações de conteúdo permitidas no momento. Essas considerações são mostradas no exemplo a seguir:

Kotlin

    private fun checkContentBlockNeeded() {
        currentContentRating?.also { rating ->
            if (!tvInputManager.isParentalControlsEnabled
                    || !tvInputManager.isRatingBlocked(rating)
                    || unblockedRatingSet.contains(rating)) {
                // Content rating is changed so we don't need to block anymore.
                // Unblock content here explicitly to resume playback.
                unblockContent(null)
                return
            }
        }
        lastBlockedRating = currentContentRating
        player?.run {
            // Children restricted content might be blocked by TV app as well,
            // but TIF should do its best not to show any single frame of blocked content.
            releasePlayer()
        }

        notifyContentBlocked(currentContentRating)
    }
    

Java

    private void checkContentBlockNeeded() {
        if (currentContentRating == null || !tvInputManager.isParentalControlsEnabled()
                || !tvInputManager.isRatingBlocked(currentContentRating)
                || unblockedRatingSet.contains(currentContentRating)) {
            // Content rating is changed so we don't need to block anymore.
            // Unblock content here explicitly to resume playback.
            unblockContent(null);
            return;
        }

        lastBlockedRating = currentContentRating;
        if (player != null) {
            // Children restricted content might be blocked by TV app as well,
            // but TIF should do its best not to show any single frame of blocked content.
            releasePlayer();
        }

        notifyContentBlocked(currentContentRating);
    }
    

Depois de determinar se o conteúdo deve ou não ser bloqueado, notifique o app de TV do sistema chamando um dos métodos de TvInputService.Session: notifyContentAllowed() ou notifyContentBlocked().

Use a classe TvContentRating para gerar uma string definida pelo sistema para a COLUMN_CONTENT_RATING com o método TvContentRating.createRating(), como mostrado aqui:

Kotlin

    val rating = TvContentRating.createRating(
            "com.android.tv",
            "US_TV",
            "US_TV_PG",
            "US_TV_D", "US_TV_L"
    )
    

Java

    TvContentRating rating = TvContentRating.createRating(
        "com.android.tv",
        "US_TV",
        "US_TV_PG",
        "US_TV_D", "US_TV_L");
    

Processar a seleção de faixas

A classe TvTrackInfo contém informações sobre faixas de mídia, como o tipo de faixa (vídeo, áudio ou legenda) e assim por diante.

Na primeira vez que sua sessão de entrada de TV acessar informações de faixa, ela deverá chamar TvInputService.Session.notifyTracksChanged() com uma lista de todas as faixas, para atualizar o app de TV do sistema. Quando houver alterações nas informações de faixas, chame notifyTracksChanged() novamente para atualizar o sistema.

O app de TV do sistema oferece uma interface para que o usuário selecione uma faixa específica quando houver mais de uma disponível para um determinado tipo de faixa. Por exemplo, legendas em vários idiomas. Sua entrada de TV responde à chamada de onSelectTrack() do app de TV do sistema chamando notifyTrackSelected(), como mostrado no exemplo a seguir. Observe que, quando null é transmitido como código de faixa, essa ação desmarca a faixa.

Kotlin

    override fun onSelectTrack(type: Int, trackId: String?): Boolean =
            mPlayer?.let { player ->
                if (type == TvTrackInfo.TYPE_SUBTITLE) {
                    if (!captionEnabled && trackId != null) return false
                    selectedSubtitleTrackId = trackId
                    subtitleView.visibility = if (trackId == null) View.INVISIBLE else View.VISIBLE
                }
                player.trackInfo.indexOfFirst { it.trackType == type }.let { trackIndex ->
                    if( trackIndex >= 0) {
                        player.selectTrack(trackIndex)
                        notifyTrackSelected(type, trackId)
                        true
                    } else false
                }
            } ?: false
    

Java

    @Override
    public boolean onSelectTrack(int type, String trackId) {
        if (player != null) {
            if (type == TvTrackInfo.TYPE_SUBTITLE) {
                if (!captionEnabled && trackId != null) {
                    return false;
                }
                selectedSubtitleTrackId = trackId;
                if (trackId == null) {
                    subtitleView.setVisibility(View.INVISIBLE);
                }
            }
            int trackIndex = -1;
            MediaPlayer.TrackInfo[] trackInfos = player.getTrackInfo();
            for (int index = 0; index < trackInfos.length; index++) {
                MediaPlayer.TrackInfo trackInfo = trackInfos[index];
                if (trackInfo.getTrackType() == type) {
                    trackIndex = index;
                    break;
                }
            }
            if (trackIndex >= 0) {
                player.selectTrack(trackIndex);
                notifyTrackSelected(type, trackId);
                return true;
            }
        }
        return false;
    }