مدیریت تعامل کاربر تلویزیون

در تجربه تلویزیون زنده، کاربر کانال‌ها را تغییر می‌دهد و اطلاعات کانال و برنامه به‌طور خلاصه قبل‌از ناپدید شدن اطلاعات به او ارائه می‌شود. انواع دیگر اطلاعات، مانند پیام‌ها («در خانه امتحان نکنید»)، زیرنویس‌ها، یا آگهی‌ها ممکن است نیاز به ماندگاری داشته باشند. مانند هر برنامه تلویزیونی، چنین اطلاعاتی نباید با محتوای برنامه درحال پخش روی صفحه تداخل داشته باشد.

شکل ۱. پیام هم‌پوشانی در برنامه تلویزیون زنده.

همچنین با درنظر گرفتن رده‌بندی محتوا و تنظیمات کنترل‌های والدین، و نحوه رفتار برنامه شما و اطلاع‌رسانی به کاربر درصورت مسدود شدن یا دردسترس نبودن محتوا، بررسی کنید که آیا محتوای برنامه خاصی باید ارائه شود یا نه. این درس نحوه توسعه تجربه کاربری ورودی تلویزیون را برای این ملاحظات شرح می‌دهد.

برنامه نمونه سرویس ورودی تلویزیون را امتحان کنید.

ادغام پخش‌کننده با سطح

ورودی تلویزیون شما باید ویدیو را روی شیء 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() callback را در شیء TvInputService.Session مدیریت می‌کند. کنترل‌های والدین برنامه تلویزیون سیستم تعیین می‌کند که چه محتوایی، باتوجه‌به رده‌بندی محتوا، نمایش داده شود. بخش‌های زیر نحوه مدیریت انتخاب کانال و برنامه را بااستفاده از روش‌های TvInputService.Session notify که با برنامه تلویزیون سیستم ارتباط برقرار می‌کنند شرح می‌دهد.

ویدیو دردسترس نباشد

وقتی کاربر کانال را تغییر می‌دهد، باید مطمئن شوید که صفحه‌نمایش هیچ‌گونه مصنوعات ویدیویی سرگردانی را قبل‌از اینکه ورودی تلویزیون محتوا را رندر کند، نمایش ندهد. وقتی 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 برای رندر کردن ویدیو، ادغام پخش‌کننده با سطح را ببینید.

ارائه کنترل والدین

برای تعیین اینکه آیا محتوای خاصی توسط کنترل‌های والدین و رده‌بندی محتوا مسدود شده است یا نه، روش‌های کلاس 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;
}