TV 사용자 상호작용 관리

라이브 TV 환경���서 사용자가 채널을 변경하고 채널 및 프로그램 정보를 잠깐 동안 표시하는 것을 의미합니다. 다른 유형의 정보, (예: 'DO NOT ATTEMPT AT HOME'), 자막 또는 광고가 계속 표시될 수 있습니다. 여느 TV와 마찬가지 이러한 정보가 화면에서 재생되는 프로그램 콘텐츠를 방해하지 않아야 합니다.

그림 1. 실시간 TV 앱의 오버레이 메시지

또한 다음 사항을 고려하여 특정 프로그램 콘텐츠를 제시해야 하는지 콘텐츠 등급 및 자녀 보호 기능 설정, 언제 앱에서 어떻게 동작하고 사용자에게 알리는지 콘텐츠가 차단되었거나 사용할 수 없는 경우입니다. 이 과정에서는 TV 입력의 사용자를 개발하는 방법을 설명합니다. 이러한 고려사항을 더 잘 이해할 수 있습니다

TV 입력 서비스 샘플 앱을 다운로드합니다.

플레이어를 표면과 통합

TV 입력은 Surface 객체에 동영상을 렌더링해야 하며, 이 객체는 TvInputService.Session.onSetSurface() 메서드를 사용하여 축소하도록 요청합니다. 다음은 MediaPlayer 인스턴스를 사용하여 재생하는 방법을 보여주는 예입니다. 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
}

자바

@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:

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
}

자바

@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), 다음 예와 같이 됩니다.

Kotlin

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() 객체를 오버레이에 사용합니다.

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

자바

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

콘텐츠 제어

사용자가 채널을 선택하면 TV 입력이 TvInputService.Session 객체의 onTune() 콜백을 처리합니다. 시스템 TV 앱의 자녀 보호 기능은 콘텐츠 등급을 고려하여 표시되는 콘텐츠를 결정합니다. 다음 섹션에서는 TvInputService.Session notify 메서드를 시스템 TV 앱과 통신할 수 있습니다.

동영상을 시청 불가로 설정

사용자가 채널을 변경할 때 화면에 잘못된 부분이 표시되지 않는지 확인하려고 합니다. 동영상 아티팩트를 생성합니다. TvInputService.Session.onTune()를 호출하면 TvInputService.Session.notifyVideoUnavailable()를 호출하여 동영상이 표시되지 않도록 할 수 있습니다. 다음과 같이 VIDEO_UNAVAILABLE_REASON_TUNING 상수를 전달합니다. 다음 예에 나와 있습니다.

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
}

자바

@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() 다음과 같이 동영상이 표시되도록 합니다.

Kotlin

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

자바

@Override
public void onRenderedFirstFrame(Surface surface) {
    firstFrameDrawn = true;
    notifyVideoAvailable();
}

이 전환은 1초도 안 되는 시간에만 지속되지만 빈 화면이 표시되는 것은 이미지가 이상한 깜빡임과 흔들림을 주는 것보다 시각적으로 더 좋습니다.

작업에 관한 자세한 내용은 플레이어를 표면과 통합도 참고하세요. Surface로 동영상을 렌더링합니다.

자녀 보호 기능 제공

특정 콘텐츠가 자녀 보호 기능 및 콘텐츠 등급으로 인해 차단되는지 확인하려면 TvInputManager 클래스 메서드, isParentalControlsEnabled()isRatingBlocked(android.media.tv.TvContentRating) 나 콘텐츠의 TvContentRating이 콘텐츠 등급으로 구성됩니다. 이러한 고려 사항은 다음 샘플에 표시되어 있습니다.

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

자바

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

콘텐츠 차단 여부를 결정한 후 시스템 TV에 알립니다. 앱에서 TvInputService.Session 메서드 notifyContentAllowed() 또는 <ph type="x-smartling-placeholder">notifyContentBlocked()</ph> 로 설정합니다.

TvContentRating 클래스를 사용하여 COLUMN_CONTENT_RATING <ph type="x-smartling-placeholder">TvContentRating.createRating()</ph> 메서드를 사용할 수 있습니다.

Kotlin

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 클래스에는 다음과 같은 미디어 트랙에 관한 정보가 포함됩니다. 트랙 유형 (동영상, 오디오 또는 자막) 등을 선택할 수 있습니다.

TV 입력 세션에서 처음으로 트랙 정보를 얻을 수 있는 경우 TvInputService.Session.notifyTracksChanged()를 시스템 TV 앱을 업데이트할 모든 트랙 목록으로 바꿉니다. 도착했을 때 트랙 정보 변경이며, notifyTracksChanged() 다시 시도해야 시스템을 업데이트할 수 있습니다

시스템 TV 앱은 트랙이 2개 이상인 경우 사용자가 특정 트랙을 선택할 수 있는 인터페��스를 제공합니다. 사용할 수 있는 경우 다른 언어로 된 자막 등을 예로 들 수 있습니다 내 TV 입력 문장에 onSelectTrack() 호출 notifyTrackSelected() 를 사용해야 합니다. null가 이 트랙 ID로 전달되면 트랙이 선택 해제됩니다.

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