Dans l'expérience de télévision en direct, l'utilisateur change de chaîne et reçoit brièvement des informations sur la chaîne et le programme avant qu'elles ne disparaissent. D'autres types d'informations, tels que les messages ("NE PAS TROUVER À LA MAISON"), les sous-titres ou les annonces peuvent avoir besoin de persister. Comme pour toute application TV, ces informations ne doivent pas interférer avec le contenu du programme diffusé à l'écran.
Déterminez également si certains contenus du programme doivent être présentés, compte tenu de leurs paramètres de classification et de contrôle parental, ainsi que du comportement de votre application et de l'information de l'utilisateur lorsque le contenu est bloqué ou indisponible. Cette leçon explique comment développer l'expérience utilisateur de votre entrée TV en fonction de ces considérations.
Essayez l'application exemple TV Input Service.
Intégrer le joueur à la surface
Votre entrée TV doit afficher la vidéo dans un objet Surface
, qui est transmis par la méthode TvInputService.Session.onSetSurface()
. Voici un exemple d'utilisation d'une instance MediaPlayer
pour lire du contenu dans l'objet 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; }
De même, voici comment procéder avec 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; }
Utiliser une superposition
Utilisez une superposition pour afficher des sous-titres, des messages, des annonces ou des diffusions de données MHEG-5. Par défaut, la superposition est désactivée. Vous pouvez l'activer lors de la création de la session en appelant TvInputService.Session.setOverlayViewEnabled(true)
, comme dans l'exemple suivant:
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; }
Utilisez un objet View
pour la superposition, renvoyé par TvInputService.Session.onCreateOverlayView()
, comme indiqué ci-dessous:
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 définition de la mise en page de la superposition peut se présenter comme suit:
<?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>
Contrôler le contenu
Lorsque l'utilisateur sélectionne une chaîne, votre entrée TV gère le rappel onTune()
dans l'objet TvInputService.Session
. Le contrôle parental de l'application TV du système détermine le contenu qui s'affiche en fonction de sa classification.
Les sections suivantes décrivent comment gérer la sélection des chaînes et des programmes à l'aide des méthodes TvInputService.Session
notify
qui communiquent avec l'application TV système.
Rendre la vidéo indisponible
Lorsque l'utilisateur change de chaîne, vous devez vous assurer que l'écran n'affiche aucun artefact vidéo parasite avant que l'entrée TV n'affiche le contenu. Lorsque vous appelez TvInputService.Session.onTune()
, vous pouvez empêcher la présentation de la vidéo en appelant TvInputService.Session.notifyVideoUnavailable()
et en transmettant la constante VIDEO_UNAVAILABLE_REASON_TUNING
, comme illustré dans l'exemple suivant.
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; }
Ensuite, lorsque le contenu est affiché dans Surface
, appelez TvInputService.Session.notifyVideoAvailable()
pour permettre l'affichage de la vidéo, comme ceci:
Kotlin
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
Java
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
Cette transition ne dure que quelques fractions de seconde, mais il est visuellement préférable de présenter un écran vide plutôt que de laisser l'image clignoter des gigues étranges.
Pour en savoir plus sur l'utilisation de Surface
pour le rendu vidéo, consultez également Intégrer le lecteur à la surface.
Activer le contrôle parental
Pour déterminer si un contenu donné est bloqué par le contrôle parental et la classification du contenu, vérifiez les méthodes de classe TvInputManager
, isParentalControlsEnabled()
et isRatingBlocked(android.media.tv.TvContentRating)
. Vous pouvez également vous assurer que l'TvContentRating
du contenu est inclus dans un ensemble de classifications de contenu actuellement autorisées. Ces considérations sont illustrées dans l'exemple suivant.
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); }
Une fois que vous avez déterminé si le contenu doit ou non être bloqué, informez l'application TV du système en appelant la méthode TvInputService.Session
notifyContentAllowed()
ou notifyContentBlocked()
, comme illustré dans l'exemple précédent.
Utilisez la classe TvContentRating
afin de générer la chaîne définie par le système pour COLUMN_CONTENT_RATING
avec la méthode TvContentRating.createRating()
, comme indiqué ci-dessous:
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");
Gérer la sélection de la piste
La classe TvTrackInfo
contient des informations sur les pistes multimédias, telles que le type de piste (vidéo, audio ou de sous-titres), etc.
La première fois que votre session d'entrée TV parvient à obtenir des informations sur les titres, elle doit appeler TvInputService.Session.notifyTracksChanged()
avec la liste de tous les titres pour mettre à jour l'application TV du système. En cas de modification des informations sur les titres, appelez à nouveau notifyTracksChanged()
pour mettre à jour le système.
L'application TV du système fournit une interface permettant à l'utilisateur de sélectionner une piste spécifique si plusieurs pistes sont disponibles pour un type de piste donné (par exemple, des sous-titres dans différentes langues). Votre entrée TV répond à l'appel onSelectTrack()
de l'application TV du système en appelant notifyTrackSelected()
, comme illustré dans l'exemple suivant. Notez que lorsque null
est transmis comme ID de canal, le titre est désélectionné.
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; }