При просмотре телепередач в прямом эфире пользователь переключает каналы и на короткое время получает информацию о каналах и программах, прежде чем информация исчезает. Другие типы информации, такие как сообщения («НЕ ПЫТАЙТЕСЬ ДОМА»), субтитры или реклама, возможно, потребуется сохранить. Как и в случае с любым телевизионным приложением, такая информация не должна мешать воспроизведению контента программы на экране.
Также подумайте, следует ли отображать определенный контент программы, учитывая рейтинг контента и настройки родительского контроля, а также то, как ваше приложение ведет себя и информирует пользователя, когда контент заблокирован или недоступен. В этом уроке описывается, как улучшить пользовательский интерфейс вашего ТВ-входа с учетом этих соображений.
Попробуйте образец приложения TV Input Service .
Интеграция плеера с поверхностью
Ваш ТВ-вход должен отображать видео в объект Surface
, который передается методом TvInputService.Session.onSetSurface()
. Вот пример использования экземпляра MediaPlayer
для воспроизведения содержимого в объекте Surface
:
Котлин
override fun onSetSurface(surface: Surface?): Boolean { player?.setSurface(surface) mSurface = surface return true } override fun onSetStreamVolume(volume: Float) { player?.setVolume(volume, volume) mVolume = volume }
Ява
@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; }
Аналогично, вот как это сделать с помощью ExoPlayer :
Котлин
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 }
Ява
@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; }
Используйте наложение
Используйте наложение для отображения субтитров, сообщений, рекламы или трансляций данных MHEG-5. По умолчанию наложение отключено. Вы можете включить его при создании сеанса, вызвав TvInputService.Session.setOverlayViewEnabled(true)
, как в следующем примере:
Котлин
override fun onCreateSession(inputId: String): Session = onCreateSessionInternal(inputId).apply { setOverlayViewEnabled(true) sessions.add(this) }
Ява
@Override public final Session onCreateSession(String inputId) { BaseTvInputSessionImpl session = onCreateSessionInternal(inputId); session.setOverlayViewEnabled(true); sessions.add(session); return session; }
Используйте объект View
для наложения, возвращаемый из TvInputService.Session.onCreateOverlayView()
, как показано здесь:
Котлин
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) } } }
Ява
@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; }
Определение макета для наложения может выглядеть примерно так:
<?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>
Управление контентом
Когда пользователь выбирает канал, ваш ТВ-вход обрабатывает обратный вызов onTune()
в объекте TvInputService.Session
. Родительский контроль приложения системного ТВ определяет, какой контент будет отображаться, учитывая рейтинг контента. В следующих разделах описывается, как управлять выбором каналов и программ с помощью методов notify
TvInputService.Session
, которые взаимодействуют с системным ТВ-приложением.
Сделать видео недоступным
Когда пользователь меняет канал, вы должны убедиться, что на экране не отображаются какие-либо случайные видеоартефакты, прежде чем ваш вход телевизора будет отображать контент. При вызове TvInputService.Session.onTune()
вы можете запретить представление видео, вызвав TvInputService.Session.notifyVideoUnavailable()
и передав константу VIDEO_UNAVAILABLE_REASON_TUNING
, как показано в следующем примере.
Котлин
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 }
Ява
@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; }
Затем, когда содержимое отображается на Surface
, вы вызываете TvInputService.Session.notifyVideoAvailable()
чтобы разрешить отображение видео, например:
Котлин
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
Ява
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
Этот переход длится лишь доли секунды, но визуально лучше представить пустой экран, чем позволять изображению мигать и дрожать.
См. также раздел «Интеграция проигрывателя с Surface» для получения дополнительной информации о работе с Surface
для рендеринга видео.
Обеспечьте родительский контроль
Чтобы определить, заблокирован ли данный контент средствами родительского контроля и рейтингом контента, вы проверяете методы класса TvInputManager
isParentalControlsEnabled()
и isRatingBlocked(android.media.tv.TvContentRating)
. Вы также можете убедиться, что TvContentRating
контента включен в набор разрешенных в настоящее время рейтингов контента. Эти соображения показаны в следующем примере.
Котлин
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) }
Ява
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); }
Определив, следует или не следует блокировать контент, уведомите об этом системное ТВ-приложение, вызвав метод TvInputService.Session
notifyContentAllowed()
или notifyContentBlocked()
, как показано в предыдущем примере.
Используйте класс TvContentRating
для создания определяемой системой строки для COLUMN_CONTENT_RATING
с помощью метода TvContentRating.createRating()
, как показано здесь:
Котлин
val rating = TvContentRating.createRating( "com.android.tv", "US_TV", "US_TV_PG", "US_TV_D", "US_TV_L" )
Ява
TvContentRating rating = TvContentRating.createRating( "com.android.tv", "US_TV", "US_TV_PG", "US_TV_D", "US_TV_L");
Обработка выбора трека
Класс TvTrackInfo
содержит информацию о дорожках мультимедиа, например тип дорожки (видео, аудио или субтитры) и т. д.
Когда сеанс ТВ-входа впервые сможет получить информацию о дорожке, он должен вызвать TvInputService.Session.notifyTracksChanged()
со списком всех дорожек, чтобы обновить системное ТВ-приложение. При изменении информации о треке снова вызовите notifyTracksChanged()
чтобы обновить систему.
Приложение системного ТВ предоставляет пользователю интерфейс для выбора определенной дорожки, если для данного типа дорожки доступно более одной дорожки; например, субтитры на разных языках. Ваш ТВ-вход отвечает на вызов onSelectTrack()
из системного ТВ-приложения, вызывая notifyTrackSelected()
, как показано в следующем примере. Обратите внимание: если в качестве идентификатора дорожки передается null
, это отменяет выбор дорожки.
Котлин
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
Ява
@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; }