Làm việc với dữ liệu kênh

Đầu vào của TV phải cung cấp dữ liệu Hướng dẫn chương trình điện tử (EPG) cho ít nhất một kênh trong hoạt động thiết lập. Bạn cũng nên cập nhật dữ liệu đó theo định kỳ, có tính đến quy mô của bản cập nhật và luồng xử lý xử lý bản cập nhật đó. Ngoài ra, bạn có thể cung cấp đường liên kết đến ứng dụng cho các kênh hướng dẫn người dùng đến nội dung và hoạt động liên quan. Bài học này thảo luận về việc tạo và cập nhật dữ liệu kênh và chương trình trên cơ sở dữ liệu hệ thống, có tính đến những điểm cần cân nhắc này.

Hãy dùng thử ứng dụng mẫu Dịch vụ đầu vào TV.

Xin cấp quyền

Để đầu vào TV hoạt động với dữ liệu EPG, đầu vào đó phải khai báo quyền ghi trong tệp kê khai Android như sau:

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

Đăng ký các kênh trong cơ sở dữ liệu

Cơ sở dữ liệu hệ thống Android TV lưu giữ các bản ghi dữ liệu kênh cho các nguồn đầu vào của TV. Trong hoạt động thiết lập, đối với mỗi kênh, bạn phải liên kết dữ liệu kênh với các trường sau của lớp TvContract.Channels:

Mặc dù khung đầu vào TV đủ chung chung để xử lý cả nội dung phát sóng truyền thống và nội dung phát trực tuyến (OTT) mà không có sự phân biệt nào, nhưng bạn có thể muốn xác định các cột sau ngoài việc xác định rõ hơn các kênh phát sóng truyền thống:

Nếu muốn cung cấp thông tin chi tiết về đường liên kết đến ứng dụng cho các kênh truyền hình, bạn cần cập nhật thêm một số trường. Để biết thêm thông tin về các trường liên kết ứng dụng, hãy xem Thêm thông tin liên kết ứng dụng.

Đối với các đầu vào TV dựa trên tính năng phát trực tuyến trên Internet, hãy chỉ định các giá trị của riêng bạn cho phù hợp để mỗi kênh có thể được xác định một cách riêng biệt.

Lấy siêu dữ liệu kênh (ở định dạng XML, JSON hoặc bất kỳ định dạng nào) từ máy chủ phụ trợ, rồi trong hoạt động thiết lập, hãy liên kết các giá trị với cơ sở dữ liệu hệ thống như sau:

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);

Trong ví dụ này, channel là một đối tượng lưu giữ siêu dữ liệu kênh từ máy chủ phụ trợ.

Trình bày thông tin về kênh và chương trình

Ứng dụng TV hệ thống trình bày thông tin về kênh và chương trình cho người dùng khi họ chuyển kênh, như minh hoạ trong hình 1. Để đảm bảo thông tin về kênh và chương trình hoạt động với trình bày thông tin về kênh và chương trình của ứng dụng TV hệ thống, hãy làm theo các nguyên tắc sau:

  1. Số kênh (COLUMN_DISPLAY_NUMBER)
  2. Biểu tượng (android:icon trong tệp kê khai đầu vào TV)
  3. Nội dung mô tả chương trình (COLUMN_SHORT_DESCRIPTION)
  4. Tên chương trình (COLUMN_TITLE)
  5. Biểu trưng của kênh (TvContract.Channels.Logo)
    • Sử dụng màu #EEEEEE để khớp với văn bản xung quanh
    • Không thêm khoảng đệm
  6. Hình áp phích (COLUMN_POSTER_ART_URI)
    • Tỷ lệ khung hình từ 16:9 đến 4:3
Hình 1. Trình trình bày thông tin kênh và chương trình của ứng dụng truyền hình hệ thống.

Ứng dụng truyền hình hệ thống cung cấp thông tin tương tự thông qua hướng dẫn chương trình, bao gồm cả ảnh áp phích, như minh hoạ trong hình 2.

Hình 2. Hướng dẫn chương trình của ứng dụng truyền hình hệ thống.

Cập nhật dữ liệu kênh

Khi cập nhật dữ liệu kênh hiện có, hãy sử dụng phương thức update thay vì xoá rồi thêm lại dữ liệu. Bạn có thể xác định phiên bản hiện tại của dữ liệu bằng cách sử dụng Channels.COLUMN_VERSION_NUMBERPrograms.COLUMN_VERSION_NUMBER khi chọn bản ghi để cập nhật.

Lưu ý: Việc thêm dữ liệu kênh vào ContentProvider có thể mất thời gian. Chỉ thêm các chương trình hiện tại (những chương trình diễn ra trong vòng 2 giờ tính từ thời điểm hiện tại) khi bạn định cấu hình EpgSyncJobService để cập nhật phần còn lại của dữ liệu kênh ở chế độ nền. Hãy xem Ứng dụng mẫu truyền hình trực tiếp trên Android TV để biết ví dụ.

Tải dữ liệu kênh theo lô

Khi cập nhật cơ sở dữ liệu hệ thống bằng một lượng lớn dữ liệu kênh, hãy sử dụng phương thức ContentResolver applyBatch hoặc bulkInsert. Sau đây là một ví dụ về cách dùng 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();
    }
}

Xử lý dữ liệu kênh một cách không đồng bộ

Việc thao tác dữ liệu, chẳng hạn như lấy luồng dữ liệu từ máy chủ hoặc truy cập cơ sở dữ liệu, không nên làm tắc nghẽn luồng giao diện người dùng. Sử dụng AsyncTask là một cách để thực hiện cập nhật bất đồng bộ. Ví dụ, khi tải thông tin kênh từ máy chủ phụ trợ, bạn có thể sử dụng AsyncTask như sau:

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();
            }
        }
    }
}

Nếu bạn cần cập nhật dữ liệu EPG thường xuyên, hãy cân nhắc sử dụng WorkManager để chạy quy trình cập nhật trong thời gian rảnh, chẳng hạn như mỗi ngày lúc 3:00.

Các kỹ thuật khác để tách các tác vụ cập nhật dữ liệu khỏi luồng giao diện người dùng bao gồm việc sử dụng lớp HandlerThread hoặc bạn có thể triển khai lớp của riêng mình bằng cách sử dụng các lớp LooperHandler. Hãy xem phần Quy trình và luồng để biết thêm thông tin.

Các kênh có thể sử dụng đường liên kết đến ứng dụng để cho phép người dùng chạy một hoạt động có liên quan trong khi họ đang xem nội dung của kênh. Ứng dụng kênh sử dụng đường liên kết đến ứng dụng để tăng mức độ tương tác của người dùng bằng cách khởi chạy các hoạt động hiển thị thông tin liên quan hoặc nội dung bổ sung. Ví dụ: bạn có thể sử dụng đường liên kết đến ứng dụng để làm những việc sau:

  • Hướng dẫn người dùng khám phá và mua nội dung có liên quan.
  • Cung cấp thêm thông tin về nội dung đang phát.
  • Khi xem nội dung nhiều tập, hãy bắt đầu xem tập tiếp theo trong một loạt nội dung.
  • Cho phép người dùng tương tác với nội dung (ví dụ: đánh giá hoặc nhận xét nội dung) mà không làm gián đoạn quá trình phát nội dung.

Đường liên kết đến ứng dụng sẽ xuất hiện khi người dùng nhấn nút Chọn để hiện trình đơn TV trong khi xem nội dung của kênh.

Hình 1. Ví dụ về đường liên kết đến ứng dụng xuất hiện trên hàng Kênh trong khi nội dung của kênh đang hiển thị.

Khi người dùng chọn đường liên kết đến ứng dụng, hệ thống sẽ bắt đầu một hoạt động bằng cách sử dụng URI ý định do ứng dụng kênh chỉ định. Nội dung trên kênh sẽ tiếp tục phát trong khi hoạt động của đường liên kết đến ứng dụng đang hoạt động. Người dùng có thể quay lại nội dung kênh bằng cách nhấn Quay lại.

Cung cấp dữ liệu kênh đường liên kết đến ứng dụng

Android TV tự động tạo một đường liên kết đến ứng dụng cho mỗi kênh bằng cách sử dụng thông tin từ dữ liệu kênh. Để cung cấp thông tin về đường liên kết đến ứng dụng, hãy chỉ định các thông tin sau trong các trường TvContract.Channels:

  • COLUMN_APP_LINK_COLOR – Màu nhấn của đường liên kết đến ứng dụng cho kênh này. Ví dụ về màu nhấn, xem hình 2, chú thích 3.
  • COLUMN_APP_LINK_ICON_URI – URI cho biểu tượng huy hiệu ứng dụng của đường liên kết đến ứng dụng cho kênh này. Ví dụ về biểu tượng huy hiệu ứng dụng, xem hình 2, chú thích 2.
  • COLUMN_APP_LINK_INTENT_URI - URI ý định của liên kết ứng dụng cho kênh này. Bạn có thể tạo URI bằng toUri(int) với URI_INTENT_SCHEME và chuyển đổi URI trở lại ý định ban đầu bằng parseUri.
  • COLUMN_APP_LINK_POSTER_ART_URI - URI cho ảnh áp phích được dùng làm nền của đường liên kết đến ứng dụng cho kênh này. Để xem ví dụ về hình ảnh áp phích, hãy xem hình 2, chú thích 1.
  • COLUMN_APP_LINK_TEXT – Văn bản mô tả của đường liên kết đến ứng dụng cho kênh này. Để xem ví dụ về mô tả liên kết ứng dụng, hãy xem văn bản trong hình 2, chú thích 3.
Hình 2. Thông tin chi tiết về đường liên kết đến ứng dụng.

Nếu dữ liệu kênh không chỉ định thông tin về đường liên kết đến ứng dụng, thì hệ thống sẽ tạo một đường liên kết mặc định đến ứng dụng. Hệ thống sẽ chọn thông tin chi tiết mặc định như sau:

  • Đối với URI ý định (COLUMN_APP_LINK_INTENT_URI), hệ thống sẽ sử dụng hoạt động ACTION_MAIN cho danh mục CATEGORY_LEANBACK_LAUNCHER, thường được xác định trong tệp kê khai ứng dụng. Nếu bạn không xác định hoạt động này, thì một đường liên kết đến ứng dụng không hoạt động sẽ xuất hiện. Nếu người dùng nhấp vào đường liên kết đó, sẽ không có gì xảy ra.
  • Đối với văn bản mô tả (COLUMN_APP_LINK_TEXT), hệ thống sẽ sử dụng "Mở app-name". Nếu không có URI ý định liên kết đến ứng dụng nào có thể sử dụng, hệ thống sẽ sử dụng "Không có đường liên kết".
  • Đối với màu nhấn (COLUMN_APP_LINK_COLOR), hệ thống sẽ dùng màu mặc định của ứng dụng.
  • Đối với hình ảnh áp phích (COLUMN_APP_LINK_POSTER_ART_URI), hệ thống sẽ dùng biểu ngữ trên màn hình chính của ứng dụng. Nếu ứng dụng không cung cấp biểu ngữ, hệ thống sẽ sử dụng hình ảnh mặc định của ứng dụng truyền hình.
  • Đối với biểu tượng huy hiệu (COLUMN_APP_LINK_ICON_URI), hệ thống sẽ dùng một huy hiệu cho biết tên ứng dụng. Nếu hệ thống cũng đang sử dụng biểu ngữ ứng dụng hoặc hình ảnh ứng dụng mặc định cho hình ảnh áp phích, thì sẽ không có huy hiệu ứng dụng nào xuất hiện.

Bạn chỉ định thông tin chi tiết về đường liên kết đến ứng dụng cho các kênh trong hoạt động thiết lập của ứng dụng. Bạn có thể cập nhật những thông tin này về đường liên kết đến ứng dụng bất cứ lúc nào. Vì vậy, nếu một đường liên kết đến ứng dụng cần phải khớp với các thay đổi của kênh, hãy cập nhật thông tin về đường liên kết đến ứng dụng và gọi ContentResolver.update nếu cần. Để biết thêm thông tin về cách cập nhật dữ liệu kênh, hãy xem bài viết Cập nhật dữ liệu kênh.