Nell'esperienza TV in diretta, l'utente cambia canale e gli vengono presentate brevemente informazioni sul canale e sui programmi prima che queste scompaiano. Altri tipi di informazioni, come i messaggi ("NON TENTARE A CASA"), i sottotitoli o gli annunci potrebbero dover essere mantenuti. Come per qualsiasi app per TV, queste informazioni non devono interferire con la riproduzione dei contenuti del programma sullo schermo.
Considera inoltre se alcuni contenuti del programma devono essere presentati, in base alla classificazione dei contenuti e alle impostazioni del Controllo genitori, nonché al comportamento dell'app e al modo in cui informa l'utente quando i contenuti sono bloccati o non disponibili. In questa lezione viene descritto come sviluppare l'esperienza utente dell'input TV sulla base di queste considerazioni.
Prova l'app di esempio TV Input Service.
Integra player con Surface
L'ingresso della TV deve eseguire il rendering del video su un oggetto Surface
, trasmesso dal
metodo TvInputService.Session.onSetSurface()
. Ecco un esempio di come utilizzare un'istanza MediaPlayer
per riprodurre contenuti nell'oggetto 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; }
Analogamente, ecco come fare utilizzando 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; }
Utilizzare un overlay
Utilizza un overlay per visualizzare sottotitoli, messaggi, annunci o trasmissioni di dati MHEG-5. Per impostazione predefinita, l'overlay è disattivato. Puoi abilitarlo quando crei la sessione chiamando
TvInputService.Session.setOverlayViewEnabled(true)
,
come nell'esempio seguente:
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; }
Utilizza un oggetto View
per l'overlay, restituito da TvInputService.Session.onCreateOverlayView()
, come mostrato qui:
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; }
La definizione del layout per l'overlay potrebbe avere il seguente aspetto:
<?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>
Controllare i contenuti
Quando l'utente seleziona un canale, l'ingresso TV gestisce il callback onTune()
nell'oggetto TvInputService.Session
. Il Controllo genitori dell'app per TV di sistema determina quali contenuti mostrare in base alla loro classificazione.
Le seguenti sezioni descrivono come gestire la selezione di canali e programmi utilizzando i metodi TvInputService.Session
notify
che comunicano con l'app TV di sistema.
Rendi il video non disponibile
Quando l'utente cambia canale, vuoi assicurarti che lo schermo non mostri elementi video discontinui prima che l'input TV esegua il rendering dei contenuti. Quando chiami TvInputService.Session.onTune()
,
puoi impedire che il video venga visualizzato chiamando TvInputService.Session.notifyVideoUnavailable()
e trasmettendo la costante VIDEO_UNAVAILABLE_REASON_TUNING
, come
mostrato nell'esempio seguente.
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; }
Quando i contenuti vengono visualizzati in Surface
, chiami
TvInputService.Session.notifyVideoAvailable()
per consentire la visualizzazione del video, in questo modo:
Kotlin
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
Java
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
Questa transizione dura solo per frazioni di secondo, ma presentare uno schermo vuoto è visivamente meglio che consentire all'immagine di lampeggiare blip e tremolio dispari.
Consulta anche Integrazione del player con la piattaforma per ulteriori informazioni sull'utilizzo di Surface
per il rendering del video.
Offri il Controllo genitori
Per stabilire se determinati contenuti sono bloccati dal Controllo genitori e dalla classificazione dei contenuti, controlla i
TvInputManager
metodi dei corsi, isParentalControlsEnabled()
e isRatingBlocked(android.media.tv.TvContentRating)
. Ti consigliamo inoltre di assicurarti che TvContentRating
dei contenuti sia incluso in un insieme di classificazioni dei contenuti attualmente consentite. Queste considerazioni sono mostrate nel seguente esempio.
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); }
Dopo aver stabilito se i contenuti devono o non devono essere bloccati, invia una notifica all'app per TV di sistema chiamando il metodo TvInputService.Session
notifyContentAllowed()
o notifyContentBlocked()
, come mostrato nell'esempio precedente.
Utilizza la classe TvContentRating
per generare la stringa definita dal sistema per COLUMN_CONTENT_RATING
con il metodo TvContentRating.createRating()
, come mostrato qui:
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");
Gestisci la selezione delle tracce
La classe TvTrackInfo
contiene informazioni sulle tracce multimediali, come il tipo di traccia (video, audio o sottotitoli) e così via.
La prima volta che la sessione con ingresso TV riesce a recuperare informazioni sulle tracce, dovrebbe chiamare
TvInputService.Session.notifyTracksChanged()
con un elenco di tutti i brani per aggiornare l'app TV di sistema. In caso di
modifica delle informazioni sulle tracce, chiama di nuovo
notifyTracksChanged()
per aggiornare il sistema.
L'app di sistema per TV fornisce all'utente un'interfaccia per selezionare una traccia specifica se sono disponibili più tracce per un determinato tipo di traccia, ad esempio sottotitoli in diverse lingue. L'ingresso della TV risponde alla chiamata onSelectTrack()
dall'app per TV di sistema chiamando notifyTrackSelected()
, come mostrato nell'esempio che segue. Tieni presente che quando null
viene trasmesso come ID canale, il canale deseleziona il canale.
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; }