Nell'esperienza di TV in diretta, l'utente cambia canale e le informazioni sul canale e sul programma vengono visualizzate brevemente prima di scomparire. Altri tipi di informazioni, come messaggi ("NON TENTARE A CASA"), sottotitoli o annunci, potrebbero dover rimanere visibili. Come per qualsiasi app TV, queste informazioni non devono interferire con i contenuti del programma riprodotti sullo schermo.
Valuta anche se determinati contenuti del programma devono essere presentati, in base alla classificazione dei contenuti e alle impostazioni del Controllo genitori, e come si comporta la tua app e informa l'utente quando i contenuti sono bloccati o non disponibili. Questa lezione descrive come sviluppare l'esperienza utente dell'input TV per questi aspetti.
Prova l'app di esempio del servizio di input TV .
Integrare il player con la superficie
L'input TV deve eseguire il rendering del video su un oggetto Surface, che viene passato da
il TvInputService.Session.onSetSurface()
metodo. 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; }
Allo stesso modo, ecco come farlo 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 attivarlo quando crei la sessione chiamando TvInputService.Session.setOverlayViewEnabled(true), come nel seguente esempio:
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 di seguito:
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 essere simile alla seguente:
<?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'input TV gestisce il callback onTune() nell'oggetto TvInputService.Session. I controlli genitori dell'app TV di sistema determinano quali contenuti vengono visualizzati, in base alla classificazione dei contenuti.
Le sezioni seguenti descrivono come gestire la selezione di canali e programmi utilizzando i metodi notify di TvInputService.Session che comunicano con l'app TV di sistema.
Rendere il video non disponibile
Quando l'utente cambia canale, devi assicurarti che sullo schermo non vengano visualizzati artefatti video prima che l'input TV esegua il rendering dei contenuti. Quando chiami TvInputService.Session.onTune(), puoi impedire la presentazione del video chiamando TvInputService.Session.notifyVideoUnavailable() e passando la costante VIDEO_UNAVAILABLE_REASON_TUNING, come mostrato nel seguente esempio.
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; }
Poi, quando i contenuti vengono sottoposti a rendering su Surface, chiama TvInputService.Session.notifyVideoAvailable() per consentire la visualizzazione del video, come segue:
Kotlin
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
Java
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
Questa transizione dura solo frazioni di secondo, ma la presentazione di una schermata vuota è visivamente migliore rispetto alla visualizzazione di strani artefatti e tremolii.
Per ulteriori informazioni sull'utilizzo di Surface per il rendering dei video, consulta anche Integrare il player con la superficie.
Fornire il Controllo genitori
Per determinare se un determinato contenuto è bloccato dal Controllo genitori e dalla classificazione dei contenuti, controlla i metodi della classe TvInputManager, isParentalControlsEnabled() e isRatingBlocked(android.media.tv.TvContentRating). Potresti anche voler assicurarti che TvContentRating dei contenuti sia incluso in un insieme di classificazioni dei contenuti attualmente consentite. Questi aspetti sono mostrati 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 essere bloccati o meno, notifica l'app 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
il COLUMN_CONTENT_RATING con il
TvContentRating.createRating()
metodo, come mostrato di seguito:
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");
Gestire 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 di input TV è in grado di ottenere informazioni sulle tracce, deve chiamare
TvInputService.Session.notifyTracksChanged() con un elenco di tutte le tracce per aggiornare l'app TV di sistema. Quando le informazioni sulle tracce cambiano, chiama
notifyTracksChanged()
di nuovo per aggiornare il sistema.
L'app TV di sistema fornisce un'interfaccia per consentire all'utente di selezionare una traccia specifica se sono disponibili più tracce per un determinato tipo di traccia, ad esempio sottotitoli in lingue diverse. L'input TV risponde alla chiamata onSelectTrack() dall'app TV di sistema chiamando notifyTrackSelected() , come mostrato nel seguente esempio. Tieni presente che quando viene passato null come ID traccia, la traccia viene deselezionata.
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; }