Управление взаимодействием с пользователем телевизора

В режиме просмотра телепередач в прямом эфире пользователь переключает каналы, и ему на короткое время отображается информация о канале и программе, после чего эта информация исчезает. Другие типы информации, такие как сообщения («НЕ ПЫТАЙТЕСЬ ПОВТОРИТЬ ДОМА»), субтитры или реклама, могут оставаться видимыми. Как и в любом приложении для просмотра телепередач, такая информация не должна мешать воспроизведению контента на экране.

Рисунок 1. Наложенное сообщение в приложении для просмотра телепередач в прямом эфире.

Также следует учитывать, следует ли показывать определенный контент, принимая во внимание его рейтинг и настройки родительского контроля, а также то, как ваше приложение информирует пользователя о блокировке или недоступности контента. В этом уроке описывается, как разработать пользовательский интерфейс для вашего телевизионного входа с учетом этих соображений.

Попробуйте демонстрационное приложение 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
}

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;
}

Аналогичным образом это можно сделать с помощью 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
}

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;
}

Используйте наложение

Используйте наложение для отображения субтитров, сообщений, рекламы или трансляций данных MHEG-5. По умолчанию наложение отключено. Вы можете включить его при создании сессии, вызвав метод TvInputService.Session.setOverlayViewEnabled(true) , как показано в следующем примере:

Котлин

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;
}

Для наложения используйте объект 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)
                }
            }
        }

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;
}

Определение макета для наложения может выглядеть примерно так:

<?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
}

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;
}

Затем, когда контент отобразится на Surface , вы вызываете TvInputService.Session.notifyVideoAvailable() чтобы разрешить отображение видео, следующим образом:

Котлин

fun onRenderedFirstFrame(surface:Surface) {
    firstFrameDrawn = true
    notifyVideoAvailable()
}

Java

@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)
}

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);
}

После того, как вы определите, следует ли блокировать контент или нет, уведомите системное ТВ-приложение, вызвав метод 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"
)

Java

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

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;
}
,

В режиме просмотра телепередач в прямом эфире пользователь переключает каналы, и ему на короткое время отображается информация о канале и программе, после чего эта информация исчезает. Другие типы информации, такие как сообщения («НЕ ПЫТАЙТЕСЬ ПОВТОРИТЬ ДОМА»), субтитры или реклама, могут оставаться видимыми. Как и в любом приложении для просмотра телепередач, такая информация не должна мешать воспроизведению контента на экране.

Рисунок 1. Наложенное сообщение в приложении для просмотра телепередач в прямом эфире.

Также следует учитывать, следует ли показывать определенный контент, принимая во внимание его рейтинг и настройки родительского контроля, а также то, как ваше приложение информирует пользователя о блокировке или недоступности контента. В этом уроке описывается, как разработать пользовательский интерфейс для вашего телевизионного входа с учетом этих соображений.

Попробуйте демонстрационное приложение 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
}

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;
}

Аналогичным образом это можно сделать с помощью 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
}

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;
}

Используйте наложение

Используйте наложение для отображения субтитров, сообщений, рекламы или трансляций данных MHEG-5. По умолчанию наложение отключено. Вы можете включить его при создании сессии, вызвав метод TvInputService.Session.setOverlayViewEnabled(true) , как показано в следующем примере:

Котлин

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;
}

Для наложения используйте объект 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)
                }
            }
        }

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;
}

Определение макета для наложения может выглядеть примерно так:

<?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
}

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;
}

Затем, когда контент отобразится на Surface , вы вызываете TvInputService.Session.notifyVideoAvailable() чтобы разрешить отображение видео, следующим образом:

Котлин

fun onRenderedFirstFrame(surface:Surface) {
    firstFrameDrawn = true
    notifyVideoAvailable()
}

Java

@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)
}

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);
}

После того, как вы определите, следует ли блокировать контент или нет, уведомите системное ТВ-приложение, вызвав метод 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"
)

Java

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

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;
}
,

В режиме просмотра телепередач в прямом эфире пользователь переключает каналы, и ему на короткое время отображается информация о канале и программе, после чего эта информация исчезает. Другие типы информации, такие как сообщения («НЕ ПЫТАЙТЕСЬ ПОВТОРИТЬ ДОМА»), субтитры или реклама, могут оставаться видимыми. Как и в любом приложении для просмотра телепередач, такая информация не должна мешать воспроизведению контента на экране.

Рисунок 1. Наложенное сообщение в приложении для просмотра телепередач в прямом эфире.

Также следует учитывать, следует ли показывать определенный контент, принимая во внимание его рейтинг и настройки родительского контроля, а также то, как ваше приложение информирует пользователя о блокировке или недоступности контента. В этом уроке описывается, как разработать пользовательский интерфейс для вашего телевизионного входа с учетом этих соображений.

Попробуйте демонстрационное приложение 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
}

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;
}

Аналогичным образом это можно сделать с помощью 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
}

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;
}

Используйте наложение

Используйте наложение для отображения субтитров, сообщений, рекламы или трансляций данных MHEG-5. По умолчанию наложение отключено. Вы можете включить его при создании сессии, вызвав метод TvInputService.Session.setOverlayViewEnabled(true) , как показано в следующем примере:

Котлин

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;
}

Для наложения используйте объект 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)
                }
            }
        }

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;
}

Определение макета для наложения может выглядеть примерно так:

<?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
}

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;
}

Затем, когда контент отобразится на Surface , вы вызываете TvInputService.Session.notifyVideoAvailable() чтобы разрешить отображение видео, следующим образом:

Котлин

fun onRenderedFirstFrame(surface:Surface) {
    firstFrameDrawn = true
    notifyVideoAvailable()
}

Java

@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)
}

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);
}

После того, как вы определите, следует ли блокировать контент или нет, уведомите системное ТВ-приложение, вызвав метод 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"
)

Java

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

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;
}