עבודה עם נתוני הערוץ

הקלט של הטלוויזיה צריך לספק נתונים של מדריך תוכניות אלקטרוני (EPG) לפחות לערוץ אחד בפעילות ההגדרה שלו. כדאי גם לעדכן את הנתונים האלה מדי פעם, תוך התחשבות בגודל העדכון ובשרשור העיבוד שמטפל בו. בנוסף, ניתן לספק קישורי אפליקציות לערוצים שמובילים את המשתמש לתכנים ופעילויות קשורים. בשיעור הזה נדון ביצירה ובעדכון של נתוני ערוצים ותוכניות במסד הנתונים של המערכת, תוך התחשבות בשיקולים האלה.

נסה את אפליקציית הדוגמה של שירות קלט הטלוויזיה.

קבל אישור

כדי שהקלט של הטלוויזיה יפעל עם נתוני EPG, צריך להצהיר על הרשאת הכתיבה בקובץ המניפסט של Android באופן הבא:

<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />

רישום ערוצים במסד הנתונים

במסד הנתונים של מערכת Android TV נשמרים רשומות של נתוני ערוצים עבור מקורות קלט של טלוויזיה. בפעילות ההגדרה, לכל אחד מהערוצים, צריך למפות את נתוני הערוץ לשדות הבאים של המחלקה TvContract.Channels:

למרות שמסגרת קלט הטלוויזיה כללית מספיק כדי לטפל הן בתכני שידור מסורתיים והן בתכני OTT (Over-the-Top) ללא כל הבחנה, ייתכן שתרצו להגדיר את העמודות הבאות בנוסף כדי לזהות טוב יותר ערוצי שידור מסורתיים:

אם ברצונך לספק פרטי קישור לאפליקציה עבור הערוצים שלך, עליך לעדכן כמה שדות נוספים. מידע נוסף על שדות של קישורים לאפליקציות זמין במאמר בנושא הוספת פרטים של קישורים לאפליקציות.

עבור כניסות טלוויזיה המבוססות על הזרמת אינטרנט, הקצה ערכים משלך בהתאם כך שניתן יהיה לזהות כל ערוץ באופן ייחודי.

שולפים את המטא-נתונים של הערוץ (בפורמט XML,‏ JSON או כל פורמט אחר) משרת הבק-אנד, ובפעילות ההגדרה ממפים את הערכים למסד הנתונים של המערכת באופן הבא:

Kotlin

val values = ContentValues().apply {
    put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.number)
    put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.name)
    put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId)
    put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId)
    put(TvContract.Channels.COLUMN_SERVICE_ID, channel.serviceId)
    put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.videoFormat)
}
val uri = context.contentResolver.insert(TvContract.Channels.CONTENT_URI, values)

Java

ContentValues values = new ContentValues();

values.put(Channels.COLUMN_DISPLAY_NUMBER, channel.number);
values.put(Channels.COLUMN_DISPLAY_NAME, channel.name);
values.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId);
values.put(Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId);
values.put(Channels.COLUMN_SERVICE_ID, channel.serviceId);
values.put(Channels.COLUMN_VIDEO_FORMAT, channel.videoFormat);

Uri uri = context.getContentResolver().insert(TvContract.Channels.CONTENT_URI, values);

בדוגמה הזו, channel הוא אובייקט שמכיל מטא-נתונים של הערוץ משרת הקצה העורפי.

מידע על ערוצים ותוכניות

אפליקציית הטלוויזיה של המערכת מציגה למשתמשים מידע על הערוצים והתוכניות כשהם עוברים בין הערוצים, כמו שמוצג באיור 1. כדי לוודא שפרטי הערוץ והתוכנית פועלים יחד עם מגיש פרטי הערוצים והתוכנית של אפליקציית הטלוויזיה של המערכת, יש לפעול לפי ההנחיות הבאות:

  1. מספר הערוץ (COLUMN_DISPLAY_NUMBER)
  2. אייקון (android:icon במניפסט של קלט הטלוויזיה)
  3. תיאור התוכנית (COLUMN_SHORT_DESCRIPTION)
  4. כותרת התוכנית (COLUMN_TITLE)
  5. הלוגו של הערוץ (TvContract.Channels.Logo)
    • שימוש בצבע ‎ #EEEEEE כדי להתאים לטקסט שמסביב
    • לא לכלול מרווח פנימי
  6. Poster art (COLUMN_POSTER_ART_URI)
    • יחס גובה-רוחב: בין 16:9 ל-4:3
איור 1. מגיש מידע על ערוץ אפליקציית הטלוויזיה של המערכת ותוכניתו.

אפליקציית הטלוויזיה של המערכת מספקת את אותו מידע דרך לוח השידורים, כולל תמונת הפוסטר, כמו שמוצג באיור 2.

איור 2. מדריך התוכניות של אפליקציית הטלוויזיה של המערכת.

עדכון נתוני הערוץ

כשמעדכנים נתונים קיימים של ערוץ, צריך להשתמש בשיטה update במקום למחוק את הנתונים ולהוסיף אותם מחדש. כדי לזהות את הגרסה הנוכחית של הנתונים, אפשר להשתמש ב-Channels.COLUMN_VERSION_NUMBER וב-Programs.COLUMN_VERSION_NUMBER כשבוחרים את הרשומות לעדכון.

הערה: יכול להיות שיעבור זמן עד שנתוני הערוץ יתווספו אל ContentProvider. מוסיפים תוכניות שמשודרות כרגע (בטווח של שעתיים מהשעה הנוכחית) רק כשמגדירים את EpgSyncJobService לעדכן את שאר נתוני הערוץ ברקע. דוגמה אפשר לראות ב אפליקציית הדוגמה של טלוויזיה בשידור חי ב-Android TV.

טעינה באצווה של נתוני ערוצים

כשמעדכנים את מסד הנתונים של המערכת עם כמות גדולה של נתוני ערוצים, משתמשים בשיטה ContentResolver applyBatch או בשיטה bulkInsert. דוגמה לשימוש ב-applyBatch:

Kotlin

val ops = ArrayList<ContentProviderOperation>()
val programsCount = channelInfo.mPrograms.size
channelInfo.mPrograms.forEachIndexed { index, program ->
    ops += ContentProviderOperation.newInsert(
            TvContract.Programs.CONTENT_URI).run {
        withValues(programs[index])
        withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, programStartSec * 1000)
        withValue(
                TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
                (programStartSec + program.durationSec) * 1000
        )
        build()
    }
    programStartSec += program.durationSec
    if (index % 100 == 99 || index == programsCount - 1) {
        try {
            contentResolver.applyBatch(TvContract.AUTHORITY, ops)
        } catch (e: RemoteException) {
            Log.e(TAG, "Failed to insert programs.", e)
            return
        } catch (e: OperationApplicationException) {
            Log.e(TAG, "Failed to insert programs.", e)
            return
        }
        ops.clear()
    }
}

Java

ArrayList<ContentProviderOperation> ops = new ArrayList<>();
int programsCount = channelInfo.mPrograms.size();
for (int j = 0; j < programsCount; ++j) {
    ProgramInfo program = channelInfo.mPrograms.get(j);
    ops.add(ContentProviderOperation.newInsert(
            TvContract.Programs.CONTENT_URI)
            .withValues(programs.get(j))
            .withValue(Programs.COLUMN_START_TIME_UTC_MILLIS,
                    programStartSec * 1000)
            .withValue(Programs.COLUMN_END_TIME_UTC_MILLIS,
                    (programStartSec + program.durationSec) * 1000)
            .build());
    programStartSec = programStartSec + program.durationSec;
    if (j % 100 == 99 || j == programsCount - 1) {
        try {
            getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
        } catch (RemoteException | OperationApplicationException e) {
            Log.e(TAG, "Failed to insert programs.", e);
            return;
        }
        ops.clear();
    }
}

עיבוד נתוני ערוצים באופן אסינכרוני

פעולות של מניפולציה של נתונים, כמו אחזור של זרם מהשרת או גישה למסד הנתונים, לא צריכות לחסום את שרשור ה-UI. אחת הדרכים לבצע עדכונים באופן אסינכרוני היא באמצעות AsyncTask. לדוגמה, כשמעלים פרטי ערוץ משרת backend, אפשר להשתמש ב-AsyncTask באופן הבא:

Kotlin

private class LoadTvInputTask(val context: Context) : AsyncTask<Uri, Unit, Unit>() {

    override fun doInBackground(vararg uris: Uri) {
        try {
            fetchUri(uris[0])
        } catch (e: IOException) {
            Log.d("LoadTvInputTask", "fetchUri error")
        }
    }

    @Throws(IOException::class)
    private fun fetchUri(videoUri: Uri) {
        context.contentResolver.openInputStream(videoUri).use { inputStream ->
            Xml.newPullParser().also { parser ->
                try {
                    parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
                    parser.setInput(inputStream, null)
                    sTvInput = ChannelXMLParser.parseTvInput(parser)
                    sSampleChannels = ChannelXMLParser.parseChannelXML(parser)
                } catch (e: XmlPullParserException) {
                    e.printStackTrace()
                }
            }
        }
    }
}

Java

private static class LoadTvInputTask extends AsyncTask<Uri, Void, Void> {

    private Context mContext;

    public LoadTvInputTask(Context context) {
        mContext = context;
    }

    @Override
    protected Void doInBackground(Uri... uris) {
        try {
            fetchUri(uris[0]);
        } catch (IOException e) {
          Log.d("LoadTvInputTask", "fetchUri error");
        }
        return null;
    }

    private void fetchUri(Uri videoUri) throws IOException {
        InputStream inputStream = null;
        try {
            inputStream = mContext.getContentResolver().openInputStream(videoUri);
            XmlPullParser parser = Xml.newPullParser();
            try {
                parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
                parser.setInput(inputStream, null);
                sTvInput = ChannelXMLParser.parseTvInput(parser);
                sSampleChannels = ChannelXMLParser.parseChannelXML(parser);
            } catch (XmlPullParserException e) {
                e.printStackTrace();
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }
}

אם אתם צריכים לעדכן את נתוני מדריך התוכניות באופן קבוע, כדאי להשתמש ב-WorkManager כדי להריץ את תהליך העדכון בזמן שהמכשיר בלי פעילות, למשל כל יום בשעה 3:00.

טכניקות נוספות להפרדה בין משימות עדכון הנתונים לבין שרשור UI כוללות שימוש במחלקה HandlerThread, או שאתם יכולים להטמיע משלכם באמצעות המחלקות Looper ו-Handler. מידע נוסף זמין במאמר תהליכים ושרשורים.

בערוצים אפשר להשתמש בקישורים לאפליקציות כדי לאפשר למשתמשים להפעיל פעילות קשורה בזמן שהם צופים בתוכן של הערוץ. אפליקציות של ערוצים משתמשות בקישורים לאפליקציות כדי להרחיב את האינטראקציה עם המשתמשים. הן מפעילות פעילויות שמציגות מידע שקשור לתוכן או תוכן נוסף. לדוגמה, אתם יכולים להשתמש בקישורים לאפליקציות כדי:

  • לעזור למשתמש לגלות ולקנות תוכן שקשור לתוכן שהוא צופה בו.
  • כאן אפשר לציין מידע נוסף על התוכן שמופעל כרגע.
  • בזמן צפייה בתוכן שמחולק לפרקים, אפשר להתחיל לצפות בפרק הבא בסדרה.
  • לאפשר למשתמש ליצור אינטראקציה עם התוכן – למשל, לדרג או לכתוב ביקורת על התוכן – בלי להפריע להפעלת התוכן.

קישורים לאפליקציות מוצגים כשהמשתמש לוחץ על Select כדי להציג את תפריט הטלוויזיה בזמן הצפייה בתוכן של הערוץ.

איור 1. דוגמה לקישור לאפליקציה שמוצג בשורה ערוצים בזמן שתוכן הערוץ מוצג.

כשהמשתמש בוחר את קישור האפליקציה, המערכת מתחילה פעילות באמצעות URI של intent שצוין על ידי אפליקציית הערוץ. התוכן של הערוץ ממשיך לפעול בזמן שהפעילות של קישור האפליקציה פעילה. המשתמש יכול לחזור לתוכן של הערוץ בלחיצה על הקודם.

הוספת נתוני ערוץ של קישור לאפליקציה

מערכת Android TV יוצרת באופן אוטומטי קישור לאפליקציה לכל ערוץ, על סמך מידע מנתוני הערוץ. כדי לספק פרטים על קישור לאפליקציה, מציינים את הפרטים הבאים בשדות TvContract.Channels:

  • COLUMN_APP_LINK_COLOR – צבע משני של קישור האפליקציה בערוץ הזה. דוגמה לצבע משני מופיעה באיור 2, הערה 3.
  • COLUMN_APP_LINK_ICON_URI - כתובת ה-URI של סמל התג של האפליקציה בקישור לאפליקציה בערוץ הזה. לדוגמה של סמל תג באפליקציה, אפשר לעיין באיור 2, הערה 2.
  • COLUMN_APP_LINK_INTENT_URI - כתובת ה-URI של הכוונה של קישור האפליקציה לערוץ הזה. אפשר ליצור את ה-URI באמצעות toUri(int) עם URI_INTENT_SCHEME ולהמיר את ה-URI בחזרה ל-Intent המקורי באמצעות parseUri.
  • COLUMN_APP_LINK_POSTER_ART_URI - ה-URI של עיצוב הפוסטר שמשמש כרקע לקישור לאפליקציה בערוץ הזה. דוגמה לתמונה של פוסטר מופיעה באיור 2, הערה 1.
  • COLUMN_APP_LINK_TEXT - הטקסט התיאורי של הקישור לאפליקציה בערוץ הזה. דוגמה לתיאור של קישור לאפליקציה מופיעה בטקסט באיור 2, בהערה 3.
איור 2. פרטי הקישור לאפליקציה.

אם בנתוני הערוץ לא מצוינים פרטים על קישור לאפליקציה, המערכת יוצרת קישור לאפליקציה כברירת מחדל. המערכת בוחרת את פרטי ברירת המחדל באופן הבא:

  • לגבי ה-URI של ה-Intent‏ (COLUMN_APP_LINK_INTENT_URI), המערכת משתמשת בפעילות ACTION_MAIN בקטגוריה CATEGORY_LEANBACK_LAUNCHER, שמוגדרת בדרך כלל בקובץ מניפסט של אפליקציה. אם פעילות זו אינה מוגדרת, יופיע קישור לאפליקציה שאינה מתפקדת - אם המשתמש לוחץ עליו, לא יקרה דבר.
  • לגבי הטקסט התיאורי (COLUMN_APP_LINK_TEXT), המערכת משתמשת בטקסט 'פתיחת app-name'. אם לא מוגדר URI של כוונת קישור לאפליקציה, המערכת משתמשת בטקסט 'אין קישור זמין'.
  • לגבי הצבע המשני (COLUMN_APP_LINK_COLOR), המערכת משתמשת בצבע ברירת המחדל של האפליקציה.
  • לתמונה של הפוסטר (COLUMN_APP_LINK_POSTER_ART_URI), המערכת משתמשת בבאנר של מסך הבית של האפליקציה. אם האפליקציה לא מספקת באנר, המערכת משתמשת בתמונה של אפליקציית טלוויזיה שמוגדרת כברירת מחדל.
  • עבור סמל התג (COLUMN_APP_LINK_ICON_URI), המערכת משתמשת בתג המציג את שם האפליקציה. אם המערכת משתמשת גם בבאנר האפליקציה או בתמונת ברירת המחדל של האפליקציה כתמונת הפוסטר, לא יוצג תג האפליקציה.

מציינים את פרטי הקישור לאפליקציה עבור הערוצים בפעילות ההגדרה של האפליקציה. אפשר לעדכן את פרטי הקישור לאפליקציה בכל שלב. לכן, אם צריך להתאים קישור לאפליקציה לשינויים בערוץ, צריך לעדכן את פרטי הקישור לאפליקציה ולהתקשר אל ContentResolver.update לפי הצורך. פרטים נוספים על עדכון נתוני הערוץ זמינים במאמר בנושא עדכון נתוני הערוץ.