Manage TV user interaction
Stay organized with collections
Save and categorize content based on your preferences.
In the live TV experience, the user changes channels and is presented with channel and program information briefly before the information disappears. Other types of information, such as messages ("DO NOT ATTEMPT AT HOME"), subtitles, or ads may need to persist. As with any TV app, such information should not interfere with the program content playing on the screen.
Figure 1. An overlay message in a live TV app.
Also consider whether certain program content should be presented, given the content's rating and parental control settings, and how your app behaves and informs the user when content is blocked or unavailable. This lesson describes how to develop your TV input's user experience for these considerations.
Try the TV Input Service sample app.
Integrate player with surface
Your TV input must render video onto a Surface object, which is passed by
the TvInputService.Session.onSetSurface()
method. Here's an example of how to use a MediaPlayer instance for playing
content in the Surface object:
Kotlin
overridefunonSetSurface(surface:Surface?):Boolean{ player?.setSurface(surface) mSurface=surface returntrue } overridefunonSetStreamVolume(volume:Float){ player?.setVolume(volume,volume) mVolume=volume }
Java
@Override publicbooleanonSetSurface(Surfacesurface){ if(player!=null){ player.setSurface(surface); } mSurface=surface; returntrue; } @Override publicvoidonSetStreamVolume(floatvolume){ if(player!=null){ player.setVolume(volume,volume); } mVolume=volume; }
Similarly, here's how to do it using ExoPlayer:
Kotlin
overridefunonSetSurface(surface:Surface?):Boolean{ player?.createMessage(videoRenderer)?.apply{ type=MSG_SET_SURFACE payload=surface send() } mSurface=surface returntrue } overridefunonSetStreamVolume(volume:Float){ player?.createMessage(audioRenderer)?.apply{ type=MSG_SET_VOLUME payload=volume send() } mVolume=volume }
Java
@Override publicbooleanonSetSurface(@NullableSurfacesurface){ if(player!=null){ player.createMessage(videoRenderer) .setType(MSG_SET_SURFACE) .setPayload(surface) .send(); } mSurface=surface; returntrue; } @Override publicvoidonSetStreamVolume(floatvolume){ if(player!=null){ player.createMessage(videoRenderer) .setType(MSG_SET_VOLUME) .setPayload(volume) .send(); } mVolume=volume; }
Use an overlay
Use an overlay to display subtitles, messages, ads or MHEG-5 data broadcasts. By default, the
overlay is disabled. You can enable it when you create the session by calling
TvInputService.Session.setOverlayViewEnabled(true) ,
as in the following example:
Kotlin
overridefunonCreateSession(inputId:String):Session= onCreateSessionInternal(inputId).apply{ setOverlayViewEnabled(true) sessions.add(this) }
Java
@Override publicfinalSessiononCreateSession(StringinputId){ BaseTvInputSessionImplsession=onCreateSessionInternal(inputId); session.setOverlayViewEnabled(true); sessions.add(session); returnsession; }
Use a View object for the overlay, returned from TvInputService.Session.onCreateOverlayView() , as shown here:
Kotlin
overridefunonCreateOverlayView():View= (context.getSystemService(LAYOUT_INFLATER_SERVICE)asLayoutInflater).run{ inflate(R.layout.overlayview,null).apply{ subtitleView=findViewById<SubtitleView>(R.id.subtitles).apply{ // Configure the subtitle view. valcaptionStyle:CaptionStyleCompat= CaptionStyleCompat.createFromCaptionStyle(captioningManager.userStyle) setStyle(captionStyle) setFractionalTextSize(captioningManager.fontScale) } } }
Java
@Override publicViewonCreateOverlayView(){ LayoutInflaterinflater=(LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE); Viewview=inflater.inflate(R.layout.overlayview,null); subtitleView=(SubtitleView)view.findViewById(R.id.subtitles); // Configure the subtitle view. CaptionStyleCompatcaptionStyle; captionStyle=CaptionStyleCompat.createFromCaptionStyle( captioningManager.getUserStyle()); subtitleView.setStyle(captionStyle); subtitleView.setFractionalTextSize(captioningManager.fontScale); returnview; }
The layout definition for the overlay might look something like this:
<?xmlversion="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>
Control content
When the user selects a channel, your TV input handles the onTune() callback in the TvInputService.Session object. The system TV
app's parental controls determine what content displays, given the content rating.
The following sections describe how to manage channel and program selection using the
TvInputService.Session notify methods that
communicate with the system TV app.
Make video unavailable
When the user changes the channel, you want to make sure the screen doesn't display any stray
video artifacts before your TV input renders the content. When you call TvInputService.Session.onTune() ,
you can prevent the video from being presented by calling TvInputService.Session.notifyVideoUnavailable()
and passing the VIDEO_UNAVAILABLE_REASON_TUNING constant, as
shown in the following example.
Kotlin
overridefunonTune(channelUri:Uri):Boolean{ subtitleView?.visibility=View.INVISIBLE notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING) unblockedRatingSet.clear() dbHandler.apply{ removeCallbacks(playCurrentProgramRunnable) playCurrentProgramRunnable=PlayCurrentProgramRunnable(channelUri) post(playCurrentProgramRunnable) } returntrue }
Java
@Override publicbooleanonTune(UrichannelUri){ if(subtitleView!=null){ subtitleView.setVisibility(View.INVISIBLE); } notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); unblockedRatingSet.clear(); dbHandler.removeCallbacks(playCurrentProgramRunnable); playCurrentProgramRunnable=newPlayCurrentProgramRunnable(channelUri); dbHandler.post(playCurrentProgramRunnable); returntrue; }
Then, when the content is rendered to the Surface , you call
TvInputService.Session.notifyVideoAvailable()
to allow the video to display, like so:
Kotlin
funonRenderedFirstFrame(surface:Surface){ firstFrameDrawn=true notifyVideoAvailable() }
Java
@Override publicvoidonRenderedFirstFrame(Surfacesurface){ firstFrameDrawn=true; notifyVideoAvailable(); }
This transition lasts only for fractions of a second, but presenting a blank screen is visually better than allowing the picture to flash odd blips and jitters.
See also, Integrate player with surface for more information about working
with Surface to render video.
Provide parental control
To determine if a given content is blocked by parental controls and content rating, you check the
TvInputManager class methods, isParentalControlsEnabled()
and isRatingBlocked(android.media.tv.TvContentRating) . You
might also want to make sure the content's TvContentRating is included in a
set of currently allowed content ratings. These considerations are shown in the following sample.
Kotlin
privatefuncheckContentBlockNeeded(){ 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
privatevoidcheckContentBlockNeeded(){ 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); }
Once you have determined if the content should or should not be blocked, notify the system TV
app by calling the
TvInputService.Session method notifyContentAllowed()
or
notifyContentBlocked()
, as shown in the previous example.
Use the TvContentRating class to generate the system-defined string for
the COLUMN_CONTENT_RATING with the
TvContentRating.createRating()
method, as shown here:
Kotlin
valrating=TvContentRating.createRating( "com.android.tv", "US_TV", "US_TV_PG", "US_TV_D","US_TV_L" )
Java
TvContentRatingrating=TvContentRating.createRating( "com.android.tv", "US_TV", "US_TV_PG", "US_TV_D","US_TV_L");
Handle track selection
The TvTrackInfo class holds information about media tracks such
as the track type (video, audio, or subtitle) and so forth.
The first time your TV input session is able to get track information, it should call
TvInputService.Session.notifyTracksChanged() with a list of all tracks to update the system TV app. When there
is a change in track information, call
notifyTracksChanged()
again to update the system.
The system TV app provides an interface for the user to select a specific track if more than one
track is available for a given track type; for example, subtitles in different languages. Your TV
input responds to the
onSelectTrack()
call from the system TV app by calling
notifyTrackSelected()
, as shown in the following example. Note that when null
is passed as the track ID, this deselects the track.
Kotlin
overridefunonSelectTrack(type:Int,trackId:String?):Boolean= mPlayer?.let{player-> if(type==TvTrackInfo.TYPE_SUBTITLE){ if(!captionEnabled && trackId!=null)returnfalse selectedSubtitleTrackId=trackId subtitleView.visibility=if(trackId==null)View.INVISIBLEelseView.VISIBLE } player.trackInfo.indexOfFirst{it.trackType==type}.let{trackIndex-> if(trackIndex>=0){ player.selectTrack(trackIndex) notifyTrackSelected(type,trackId) true }elsefalse } }?:false
Java
@Override publicbooleanonSelectTrack(inttype,StringtrackId){ if(player!=null){ if(type==TvTrackInfo.TYPE_SUBTITLE){ if(!captionEnabled && trackId!=null){ returnfalse; } selectedSubtitleTrackId=trackId; if(trackId==null){ subtitleView.setVisibility(View.INVISIBLE); } } inttrackIndex=-1; MediaPlayer.TrackInfo[]trackInfos=player.getTrackInfo(); for(intindex=0;index < trackInfos.length;index++){ MediaPlayer.TrackInfotrackInfo=trackInfos[index]; if(trackInfo.getTrackType()==type){ trackIndex=index; break; } } if(trackIndex>=0){ player.selectTrack(trackIndex); notifyTrackSelected(type,trackId); returntrue; } } returnfalse; }