ทำงานกับข้อมูลช่อง

อินพุตทีวีต้องระบุข้อมูลคู่มือรายการอิเล็กทรอนิกส์ (EPG) สำหรับช่องอย่างน้อย 1 ช่องในกิจกรรมการตั้งค่า นอกจากนี้ คุณควรจะอัปเดตข้อมูลดังกล่าวเป็นระยะๆ โดยคำนึงถึงขนาดของการอัปเดตและเธรดการประมวลผลที่จัดการข้อมูลนั้น อีกทั้งยังระบุลิงก์แอปสำหรับช่องที่จะแนะนำผู้ใช้ไปยังเนื้อหาและกิจกรรมที่เกี่ยวข้องได้ด้วย บทเรียนนี้จะกล่าวถึงการสร้างและอัปเดตข้อมูลช่องและข้อมูลรายการในฐานข้อมูลระบบโดยคำนึงถึงข้อควรพิจารณาเหล่านี้

ลองใช้แอปตัวอย่าง บริการอินพุตทีวี

ขอสิทธิ์

อินพุตทีวีจะต้องประกาศสิทธิ์ เขียนในไฟล์ Manifest ของ Android ดังนี้จึงจะทำงานกับข้อมูล EPG ได้

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

ลงทะเบียนช่องในฐานข้อมูล

ฐานข้อมูลระบบ Android TV จะเก็บรักษาบันทึกข้อมูลช่องสำหรับอินพุตทีวี ในกิจกรรมการตั้งค่า คุณต้องแมปข้อมูลช่องกับช่องต่อไปนี้ของคลาส TvContract.Channels สำหรับแต่ละช่อง

  • COLUMN_DISPLAY_NAME - ชื่อที่แสดงของ ช่อง
  • COLUMN_DISPLAY_NUMBER - หมายเลขช่องที่แสดง
  • COLUMN_INPUT_ID - รหัสของบริการอินพุตทีวี
  • COLUMN_SERVICE_TYPE - ประเภทบริการของช่อง
  • COLUMN_TYPE - ประเภทมาตรฐานการออกอากาศของช่อง
  • COLUMN_VIDEO_FORMAT - รูปแบบวิดีโอเริ่มต้น ของช่อง

แม้ว่าเฟรมเวิร์กอินพุตทีวีจะมีความทั่วไปมากพอที่จะจัดการเนื้อหาการออกอากาศแบบดั้งเดิมและเนื้อหา Over-the-Top (OTT) ได้โดยไม่มีความแตกต่างกัน แต่คุณอาจต้องการกำหนดคอลัมน์ต่อไปนี้เพิ่มเติมเพื่อระบุช่องการออกอากาศแบบดั้งเดิมได้ดียิ่งขึ้น

หากต้องการระบุรายละเอียดลิงก์แอปสำหรับช่อง คุณต้องอัปเดตช่องเพิ่มเติมบางช่อง ดูข้อมูลเพิ่มเติมเกี่ยวกับช่องลิงก์แอปได้ที่ เพิ่มข้อมูลลิงก์แอป

สำหรับอินพุตทีวีที่ใช้การสตรีมทางอินเทอร์เน็ต ให้กำหนดค่าของคุณเองตามความเหมาะสมเพื่อให้ระบุแต่ละช่องได้อย่างไม่ซ้ำกัน

ดึงข้อมูลเมตาของช่อง (ในรูปแบบ 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 ในไฟล์ Manifest ของอินพุตทีวี)
  3. คำอธิบายรายการ (COLUMN_SHORT_DESCRIPTION)
  4. ชื่อรายการ (COLUMN_TITLE)
  5. โลโก้ช่อง (TvContract.Channels.Logo)
    • ใช้สี #EEEEEE ให้ตรงกับข้อความโดยรอบ
    • ไม่ใส่ระยะขอบ
  6. ภาพโปสเตอร์ (COLUMN_POSTER_ART_URI)
    • สัดส่วนภาพระหว่าง 16:9 ถึง 4:3
รูปที่ 1 ตัวแสดงข้อมูลช่องและข้อมูลรายการของแอป TV ของระบบ

แอป TV ของระบบจะแสดงข้อมูลเดียวกันผ่านคู่มือรายการทีวี รวมถึงภาพโปสเตอร์ ดังที่แสดงในรูปที่ 2

รูปที่ 2 คู่มือรายการทีวีของแอป TV ของระบบ

อัปเดตข้อมูลช่อง

เมื่ออัปเดตข้อมูลช่องที่มีอยู่ ให้ใช้วิธี update แทนการลบและเพิ่มข้อมูลอีกครั้ง คุณสามารถระบุข้อมูลเวอร์ชันปัจจุบันได้โดยใช้ Channels.COLUMN_VERSION_NUMBER และ Programs.COLUMN_VERSION_NUMBER เมื่อเลือกบันทึกที่จะอัปเดต

หมายเหตุ: การเพิ่มข้อมูลช่องลงใน ContentProvider อาจใช้เวลา ให้เพิ่มรายการปัจจุบัน (รายการที่ออกอากาศภายใน 2 ชั่วโมงจากเวลาปัจจุบัน) ก็ต่อเมื่อคุณกำหนดค่า EpgSyncJobService ให้อัปเดตข้อมูลช่องที่เหลือในเบื้องหลัง ดูตัวอย่างได้ที่ แอปตัวอย่างรายการทีวีสดของ Android TV

การโหลดข้อมูลช่องแบบกลุ่ม

เมื่ออัปเดตฐานข้อมูลระบบด้วยข้อมูลช่องจำนวนมาก ให้ใช้วิธี applyBatch หรือ bulkInsert ของ ContentResolver ตัวอย่างการใช้ 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 เป็นวิธีหนึ่งในการอัปเดตแบบไม่พร้อมกัน ตัวอย่างเช่น เมื่อโหลดข้อมูลช่องจากเซิร์ฟเวอร์แบ็กเอนด์ คุณสามารถใช้ 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();
            }
        }
    }
}

หากต้องการอัปเดตข้อมูล EPG เป็นประจำ ให้พิจารณาใช้ WorkManager เพื่อเรียกใช้กระบวนการอัปเดตในช่วงเวลาที่ไม่มีการใช้งาน เช่น ทุกวันเวลา 03:00 น.

เทคนิคอื่นๆ ในการแยกงานอัปเดตข้อมูลออกจากเธรด UI ได้แก่ การใช้คลาส HandlerThread หรือคุณอาจใช้คลาส Looper และ Handler เพื่อสร้างเทคนิคของคุณเองก็ได้ ดูข้อมูลเพิ่มเติมได้ที่ กระบวนการและเธรด

ช่องสามารถใช้ ลิงก์แอป เพื่อให้ผู้ใช้เปิดกิจกรรมที่เกี่ยวข้องขณะรับชมเนื้อหาของช่องได้ แอปช่องจะใช้ลิงก์แอปเพื่อเพิ่มการมีส่วนร่วมของผู้ใช้โดยการเปิดกิจกรรมที่แสดงข้อมูลที่เกี่ยวข้องหรือเนื้อหาเพิ่มเติม ตัวอย่างเช่น คุณสามารถใช้ลิงก์แอปเพื่อทำสิ่งต่อไปนี้

  • แนะนำผู้ใช้ให้ค้นพบและซื้อเนื้อหาที่เกี่ยวข้อง
  • ให้ข้อมูลเพิ่มเติมเกี่ยวกับเนื้อหาที่กำลังเล่น
  • ขณะดูเนื้อหาแบบเป็นตอน ให้เริ่มดูตอนถัดไปในซีรีส์
  • อนุญาตให้ผู้ใช้โต้ตอบกับเนื้อหา เช่น ให้คะแนนหรือเขียนรีวิวเนื้อหา โดยไม่ขัดจังหวะการเล่นเนื้อหา

ลิงก์แอปจะแสดงขึ้นเมื่อผู้ใช้กดเลือก เพื่อแสดงเมนูทีวีขณะรับชมเนื้อหาของช่อง

รูปที่ 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 ของ Intent ของลิงก์แอปสำหรับช่องนี้ คุณสามารถสร้าง 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 ซึ่งโดยทั่วไปจะกำหนดไว้ในไฟล์ Manifest ของแอป หากไม่ได้กำหนดกิจกรรมนี้ไว้ ลิงก์แอปที่ไม่ทำงานจะปรากฏขึ้น และหากผู้ใช้คลิกลิงก์ดังกล่าว ก็จะไม่มีอะไรเกิดขึ้น
  • สำหรับข้อความอธิบาย (COLUMN_APP_LINK_TEXT) ระบบ จะใช้ "เปิดapp-name" หากไม่ได้กำหนด URI ของ Intent ของลิงก์แอปที่ใช้งานได้ ระบบจะใช้ "ไม่มีลิงก์"
  • สำหรับสีเน้น (COLUMN_APP_LINK_COLOR) ระบบจะใช้สีเริ่มต้นของแอป
  • สำหรับภาพโปสเตอร์ (COLUMN_APP_LINK_POSTER_ART_URI) ระบบจะใช้แบนเนอร์หน้าจอหลักของแอป หากแอปไม่มีแบนเนอร์ ระบบจะใช้รูปภาพเริ่มต้นของแอป TV
  • สำหรับไอคอนป้าย (COLUMN_APP_LINK_ICON_URI) ระบบจะใช้ป้ายที่แสดงชื่อแอป หากระบบใช้แบนเนอร์แอปหรือรูปภาพเริ่มต้นของแอปสำหรับภาพโปสเตอร์ด้วย ระบบจะไม่แสดงป้ายแอป

คุณระบุรายละเอียดลิงก์แอปสำหรับช่องในกิจกรรมการตั้งค่าของแอป คุณสามารถอัปเดตรายละเอียดลิงก์แอปเหล่านี้ได้ทุกเมื่อ ดังนั้นหากลิงก์แอปต้องตรงกับการเปลี่ยนแปลงของช่อง ให้อัปเดตรายละเอียดลิงก์แอปและเรียกใช้ ContentResolver.update ตามที่จำเป็น ดูรายละเอียดเพิ่มเติมเกี่ยวกับการอัปเดต ข้อมูลช่องได้ที่อัปเดตข้อมูลช่อง