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 do canal e do programa rapidamente antes que elas desapareçam. Outros tipos de informações, como mensagens ("NÃO TENTE EM CASA"), legendas ou anúncios, por exemplo. Como em qualquer TV aplicativo, essas informações não devem interferir com o 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 deve ser apresentado, dadas as classificação do conteúdo e configurações de controle da família, além de como seu app se comporta e informa o usuário quando está bloqueado ou indisponível. Esta lição descreve como desenvolver um conjunto de dados de usuário experiência do usuário para essas considerações.

Experimente o App de exemplo do serviço de entrada de TV (link em inglês).

Integrar o player com a superfície

Sua entrada de TV precisa renderizar o vídeo em um objeto Surface, que é transmitido TvInputService.Session.onSetSurface() . Confira um exemplo de como usar uma instância do MediaPlayer para abrir 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 ExoPlayer (em inglês):

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, o está desativada. É 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(), 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. A TV do sistema o controle da família do app determina qual conteúdo é exibido de acordo com a classificação dele. As seções a seguir descrevem como gerenciar a seleção de canais e programas usando o TvInputService.Session métodos notify que se comunicar com o app de TV do sistema.

Tornar o vídeo indisponível

Quando o usuário muda de canal, você precisa ter certeza de que a tela não exibe nada fora do comum. artefatos de vídeo antes que a entrada de TV renderize o conteúdo. Quando você chama TvInputService.Session.onTune(), você pode impedir que o vídeo seja apresentado chamando TvInputService.Session.notifyVideoUnavailable() e passando a constante VIDEO_UNAVAILABLE_REASON_TUNING, conforme 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 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 apenas frações de segundo, mas apresentar uma tela em branco é visualmente melhor do que permitir que a imagem mostre falhas e tremores estranhos.

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

Disponibilizar "controle dos pais"

Para determinar se um conteúdo específico está bloqueado pelo controle dos pais e pela classificação do conteúdo, verifique o Métodos da classe TvInputManager, isParentalControlsEnabled() e isRatingBlocked(android.media.tv.TvContentRating). Você verifique se o TvContentRating do conteúdo está incluído em um o 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 deve ou não ser bloqueado, notifique a TV do sistema. aplicativo chamando Método notifyContentAllowed() do TvInputService.Session 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 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 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 conseguir 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 é 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 caso haja mais de uma está disponível para um determinado tipo de faixa. como legendas em idiomas diferentes. Sua TV entrada responde à solicitação onSelectTrack() do app de TV do sistema chamando notifyTrackSelected() , como mostrado no exemplo a seguir. Quando null é transmitido como o ID da faixa, esta 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;
}