TV kullanıcı etkileşimini yönetme

Canlı TV deneyiminde kullanıcı kanal değiştirir ve bilgiler kaybolmadan önce kısa süreliğine kanal ve program bilgileri sunulur. Mesajlar veya altyazılar gibi diğer bilgi türlerinin (ör. mesajlar ("ANA EVDE TEŞVİK ETMEYİN") veya reklamlar) kalıcı olması gerekebilir. TV uygulamalarında olduğu gibi, bu tür bilgiler ekranda oynatılan program içeriğini engellememelidir.

Şekil 1. Canlı TV uygulamasında yer paylaşımlı mesaj.

Ayrıca, içeriğin derecelendirme ve ebeveyn denetimi ayarları ve uygulamanızın içerik engellendiğinde veya kullanılamadığında nasıl davrandığı ve kullanıcıyı nasıl bilgilendirdiği göz önünde bulundurulduğunda belirli program içeriklerinin sunulması gerekip gerekmediğini de göz önünde bulundurun. Bu derste, bu hususlar için TV girişinizin kullanıcı deneyimini nasıl geliştireceğiniz açıklanmaktadır.

TV Giriş Hizmeti örnek uygulamasını deneyin.

Oynatıcıyı yüzeyle entegre edin

TV girişiniz, videoyu TvInputService.Session.onSetSurface() yöntemi tarafından geçirilen bir Surface nesnesinde oluşturmalıdır. Surface nesnesindeki içeriği oynatmak için MediaPlayer örneğinin nasıl kullanılacağına dair bir örneği burada bulabilirsiniz:

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
}

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

Benzer bir şekilde, ExoPlayer kullanarak bunu şu şekilde yapabilirsiniz:

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
}

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

Yer paylaşımı kullanma

Altyazıları, mesajları, reklamları veya MHEG-5 veri yayınlarını görüntülemek için bir yer paylaşımı kullanın. Yer paylaşımı varsayılan olarak devre dışıdır. Aşağıdaki örnekte olduğu gibi, TvInputService.Session.setOverlayViewEnabled(true) yöntemini çağırarak oturumu oluştururken bunu etkinleştirebilirsiniz:

Kotlin

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

Yer paylaşımı için, burada gösterildiği gibi TvInputService.Session.onCreateOverlayView() öğesinden döndürülen bir View nesnesi kullanın:

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

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

Yer paylaşımının düzen tanımı aşağıdaki gibi görünebilir:

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

İçeriği kontrol edin

Kullanıcı bir kanal seçtiğinde TV girişiniz, TvInputService.Session nesnesindeki onTune() geri çağırmasını işler. Sistem TV uygulamasının ebeveyn denetimleri, içerik derecelendirmesine göre hangi içeriğin gösterileceğini belirler. Aşağıdaki bölümlerde sistem TV uygulamasıyla iletişim kuran TvInputService.Session notify yöntemlerini kullanarak kanal ve program seçiminin nasıl yönetileceği açıklanmaktadır.

Videoyu kullanılamaz yap

Kullanıcı kanalı değiştirdiğinde, TV girişiniz içeriği oluşturmadan önce ekranda kayıp video yapılarının görüntülenmediğinden emin olmak istersiniz. TvInputService.Session.onTune() yöntemini çağırdığınızda aşağıdaki örnekte gösterildiği gibi TvInputService.Session.notifyVideoUnavailable() yöntemini çağırıp VIDEO_UNAVAILABLE_REASON_TUNING sabitini ileterek videonun sunulmasını engelleyebilirsiniz.

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
}

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

İçerik Surface için oluşturulduğunda, videonun görüntülenmesine izin vermek için TvInputService.Session.notifyVideoAvailable() yöntemini çağırırsınız. Örneğin:

Kotlin

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

Java

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

Bu geçiş, saniyenin yalnızca kısa bir bölümü için sürer ancak boş ekran göstermek, resmin tuhaf yankılar ve titremelerin yanıp sönmesine izin vermekten görsel olarak daha iyidir.

Video oluşturmak için Surface ile çalışma hakkında daha fazla bilgi için Oynatıcıyı yüzeyle entegre etme bölümüne de bakın.

Ebeveyn denetimi sağlayın

Belirli bir içeriğin ebeveyn denetimleri ve içerik derecelendirmesi tarafından engellenip engellenmediğini belirlemek için TvInputManager sınıf yöntemlerini ve isParentalControlsEnabled() ve isRatingBlocked(android.media.tv.TvContentRating) öğelerini kontrol edersiniz. İçeriğin TvContentRating değerinin şu anda izin verilen içerik derecelendirmelerine dahil edildiğinden emin olmak da isteyebilirsiniz. Dikkat edilmesi gerekenler aşağıdaki örnekte gösterilmektedir.

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

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

İçeriğin engellenip engellenmemesi gerektiğini belirledikten sonra, önceki örnekte gösterildiği gibi TvInputService.Session yöntemini notifyContentAllowed() veya notifyContentBlocked() yöntemini çağırarak sistem TV uygulamasını bilgilendirin.

Burada gösterildiği gibi, COLUMN_CONTENT_RATING için TvContentRating.createRating() yöntemi ile sistem tanımlı dizeyi oluşturmak üzere TvContentRating sınıfını kullanın:

Kotlin

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

Kanal seçimini yönet

TvTrackInfo sınıfı, medya kanallarıyla ilgili bilgileri (ör. parça türü (video, ses veya altyazı) vb.) içerir.

TV giriş oturumunuz ilk kez parça bilgilerini alabildiğinde, sistem TV uygulamasını güncellemek için tüm parçaların bir listesiyle birlikte TvInputService.Session.notifyTracksChanged() çağrısını yapacaktır. Kanal bilgilerinde bir değişiklik olduğunda sistemi güncellemek için notifyTracksChanged() numaralı telefonu tekrar arayın.

Sistem TV uygulaması, belirli bir kanal türü için birden fazla parça varsa (örneğin, farklı dillerdeki altyazılar) kullanıcının belirli bir parçayı seçebileceği bir arayüz sağlar. TV girişiniz, aşağıdaki örnekte gösterildiği gibi notifyTrackSelected() çağrısı yaparak sistem TV uygulamasından gelen onSelectTrack() çağrısına yanıt verir. Parça kimliği olarak null iletildiğinde, bu işlemin kanalın seçimini kaldırdığını unutmayın.

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