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 entrada de TV com essas considerações em mente.

Teste o serviço de entrada de TV app de exemplo.

Integrar o player com a superfície

Sua entrada de TV precisa renderizar o vídeo em um Surface objeto, que é transmitido pelo método TvInputService.Session.onSetSurface(). Veja um exemplo de como usar uma instância 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. A sobreposição é desativada por padrão. Você pode 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(), 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 dos pais do app de TV do sistema determina o conteúdo a ser exibido de acordo com a classificação do conteúdo. As seções a seguir descrevem como gerenciar a seleção de canais e programas usando os métodos notify de TvInputService.Session 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(), você pode impedir que o vídeo seja apresentado chamando TvInputService.Session.notifyVideoUnavailable() e transmitindo a constante VIDEO_UNAVAILABLE_REASON_TUNING, conforme 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 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 também Integrar o player à superfície para 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 dos pais 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(), conforme mostrado no exemplo anterior.

Use a classe TvContentRating para gerar a string definida pelo sistema para o COLUMN_CONTENT_RATING com o TvContentRating.createRating() método, conforme 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 a sessão de entrada de TV conseguir informações sobre a faixa, 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 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. A entrada de TV responde à chamada onSelectTrack() do app de TV do sistema chamando notifyTrackSelected(), conforme mostrado no exemplo a seguir. Note que quando null é transmitido como o ID da faixa, isso 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;
}