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

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

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

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

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

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

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

پس از اینکه مشخص کردید محتوا باید مسدود شود یا خیر، با فراخوانی متد notifyContentAllowed() یا notifyContentBlocked() از TvInputService.Session ، همانطور که در مثال قبلی نشان داده شده است، به برنامه تلویزیون سیستمی اطلاع دهید.

از کلاس 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() 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;
}
،

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

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

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

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

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

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

پس از اینکه مشخص کردید محتوا باید مسدود شود یا خیر، با فراخوانی متد notifyContentAllowed() یا notifyContentBlocked() از TvInputService.Session ، همانطور که در مثال قبلی نشان داده شده است، به برنامه تلویزیون سیستمی اطلاع دهید.

از کلاس 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() 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;
}
،

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

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

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

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

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

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

پس از اینکه مشخص کردید محتوا باید مسدود شود یا خیر، با فراخوانی متد notifyContentAllowed() یا notifyContentBlocked() از TvInputService.Session ، همانطور که در مثال قبلی نشان داده شده است، به برنامه تلویزیون سیستمی اطلاع دهید.

از کلاس 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() 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;
}