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

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

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

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

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

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

ورودی تلویزیون شما باید ویدیو را بر روی یک شی 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() در شی 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
}

جاوا

@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() تماس بگیرید تا سیستم به روز شود.

برنامه تلویزیون سیستم رابطی را برای کاربر فراهم می کند تا در صورت وجود بیش از یک آهنگ برای یک نوع آهنگ معین، یک آهنگ خاص را انتخاب کند. به عنوان مثال، زیرنویس به زبان های مختلف. ورودی تلویزیون شما با فراخوانی notifyTrackSelected() به تماس onSelectTrack() از برنامه تلویزیون سیستم پاسخ می دهد، همانطور که در مثال زیر نشان داده شده است. توجه داشته باشید که زمانی که 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;
}