TV 사용자 상호작용 관리

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

실시간 TV 환경에서 사용자가 채널을 변경하면 정보가 사라지기 전에 채널 및 프로그램 정보가 잠시 표시됩니다. 메시지(예: 'DO NOT ATTEMPT AT HOME'), 자막 또는 광고와 같은 다른 유형의 정보는 계속 표시되어야 할 수 있습니다. 다른 TV 앱과 마찬가지로 이러한 정보는 화면에서 재생되는 프로그램 콘텐츠를 방해하지 않아야 합니다.

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

또한 콘텐츠 등급 및 자녀 보호 기능 설정을 감안할 때 특정 프로그램 콘텐츠가 제공되어야 하는지 여부와 콘텐츠가 차단되거나 사용 불가능한 경우에 앱이 어떻게 작동하고 사용자에게 알리는지도 고려하세요. 이 강의에서는 이러한 고려 사항에 맞게 TV 입력의 사용자 환경을 개발하는 방법을 설명합니다.

TV 입력 서비스 샘플 앱을 사용해 보세요.

플레이어를 표면과 통합

TV 입력은 TvInputService.Session.onSetSurface() 메서드에 의해 전달된 Surface 객체에 동영상을 렌더링해야 합니다. 다음은 Surface 객체에서 콘텐츠를 재생하기 위해 MediaPlayer 인스턴스를 사용하는 방법을 보여주는 예입니다.

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 앱의 자녀 보호 기능은 콘텐츠 등급을 고려하여 표시되는 콘텐츠를 결정합니다. 다음 섹션에서는 시스템 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);
    }
    

콘텐츠의 차단 여부를 결정했으면 이전 예에 나와 있는 것처럼 TvInputService.Session 메서드인 notifyContentAllowed() 또는 notifyContentBlocked()를 호출하여 시스템 TV 앱에 알립니다.

아래에 나와 있는 것처럼 TvContentRating 클래스를 사용하여 TvContentRating.createRating() 메서드로 COLUMN_CONTENT_RATING에 관한 시스템 정의 문자열을 생성합니다.

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 앱은 지정된 트랙 유형에 둘 이상의 트랙(예: 여러 언어로 된 자막)이 사용 가능한 경우 사용자가 특정 트랙을 선택할 수 있는 인터페이스를 제공합니다. 다음 예에 나와 있는 것처럼 TV 입력은 notifyTrackSelected()를 호출하여 시스템 TV 앱에서 onSelectTrack() 호출에 응답합니다. 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
    

자바

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