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 o canal e o programa brevemente 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 mantidos. Como acontece com qualquer app de TV, essas informações não podem interferir no conteúdo do programa exibido na tela.

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

Considere também se determinado conteúdo do programa precisa ser apresentado, de acordo com as configurações de classificação e controle da família do conteúdo, além de como o app se comporta e informa ao usuário quando o 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 base nessas considerações.

Teste o app de exemplo Serviço de entrada de TV (link em inglês).

Integrar o player com a plataforma

Sua entrada de TV precisa renderizar o vídeo em um objeto Surface, que é transmitido pelo método TvInputService.Session.onSetSurface(). Confira um exemplo de como usar uma instância do MediaPlayer para reproduzir 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. Por padrão, a sobreposição está desativada. É possível ativá-lo 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(), como 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, a entrada da TV manipula o callback onTune() no objeto TvInputService.Session. O controle da família do app de TV do sistema determina qual conteúdo é exibido, de acordo com a classificação do conteúdo. As seções abaixo descrevem como gerenciar a seleção de canais e programas usando os métodos 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 mostre artefatos de vídeo desaparecidos antes que a entrada de TV renderize o conteúdo. Ao chamar TvInputService.Session.onTune(), você pode impedir 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;
}

Em seguida, quando o conteúdo for renderizado para o Surface, chame TvInputService.Session.notifyVideoAvailable() para permitir que o vídeo seja exibido, desta forma:

Kotlin

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

Java

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

Essa transição dura apenas frações de segundo, mas apresentar uma tela em branco é visualmente melhor do que permitir que a imagem pisque em mensagens estranhas e instabilidades.

Consulte também 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 está bloqueado pelo controle da família e pela classificação do conteúdo, verifique os métodos da classe TvInputManager, isParentalControlsEnabled() e isRatingBlocked(android.media.tv.TvContentRating). Verifique também se o TvContentRating do conteúdo está incluído em um conjunto de classificações de conteúdo permitidas no momento. Essas considerações são mostradas na amostra 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 precisa ou não ser bloqueado, notifique o app de TV do sistema chamando o método TvInputService.Session notifyContentAllowed() ou notifyContentBlocked(), como mostrado no exemplo anterior.

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

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 a sessão de entrada de TV conseguir informações sobre as faixas, ela precisará chamar TvInputService.Session.notifyTracksChanged() com uma lista de todas as faixas para atualizar o app de TV do sistema. Quando houver uma mudança nas informações da faixa, chame notifyTracksChanged() novamente para atualizar o sistema.

O app de TV do sistema oferece uma interface para o usuário selecionar uma faixa específica se mais de uma estiver disponível para um determinado tipo. Por exemplo, legendas em diferentes idiomas. A entrada de TV responde à chamada onSelectTrack() do app de TV do sistema chamando notifyTrackSelected(), conforme mostrado no exemplo abaixo. Quando null é transmitido como o ID 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;
}