電視輸入裝置必須在設定活動中,為至少一個頻道提供電子節目表 (EPG) 資料。您也應定期更新該資料,並考量更新大小和處理該資料的處理程序執行緒。此外,您也可以提供頻道應用程式連結,引導使用者前往相關內容和活動。本課程將討論如何根據上述考量,在系統資料庫中建立及更新頻道和節目資料。
試用 TV Input Service 範例應用程式。
取得權限
如要讓電視輸入與 EPG 資料搭配運作,必須在 Android 資訊清單檔案中宣告寫入權限,如下所示:
<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- 電視輸入服務的 IDCOLUMN_SERVICE_TYPE- 管道的服務類型COLUMN_TYPE- 頻道的廣播標準類型COLUMN_VIDEO_FORMAT- 頻道的預設影片格式
雖然電視輸入架構夠通用,可處理傳統廣播和 OTT 內容,但您可能想定義下列資料欄,以便更清楚識別傳統廣播頻道:
COLUMN_ORIGINAL_NETWORK_ID- 電視聯播網 IDCOLUMN_SERVICE_ID:服務 IDCOLUMN_TRANSPORT_STREAM_ID- 傳輸串流 ID
如要為頻道提供應用程式連結詳細資料,請更新一些額外欄位。如要進一步瞭解應用程式連結欄位,請參閱「新增應用程式連結資訊」。
如果是以網際網路串流為基礎的電視輸入內容,請視情況指派值,確保每個頻道都能獲得專屬 ID。
從後端伺服器擷取頻道中繼資料 (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 所示,使用者切換頻道時,系統電視應用程式會顯示頻道和節目資訊。為確保頻道和節目資訊能與系統電視應用程式的頻道和節目資訊呈現工具搭配運作,請遵守下列規範:
- 頻道號碼 (
COLUMN_DISPLAY_NUMBER) - 圖示
(電視輸入來源資訊清單中的
android:icon) - 計畫說明 (
COLUMN_SHORT_DESCRIPTION) - 節目名稱 (
COLUMN_TITLE) - 頻道標誌 (
TvContract.Channels.Logo)- 使用 #EEEEEE 顏色,與周圍文字相符
- 不要加入邊框
- 海報圖片 (
COLUMN_POSTER_ART_URI) - 顯示比例介於 16:9 和 4:3 之間
系統 TV 應用程式會透過節目指南提供相同資訊,包括海報圖片,如圖 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 是以非同步方式執行更新作業的方法之一。舉例來說,從後端伺服器載入頻道資訊時,您可以使用 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 在閒置時間執行更新程序,例如每天凌晨 3 點。
如要將資料更新工作與 UI 執行緒分開,其他方法包括使用 HandlerThread 類別,或是使用 Looper 和 Handler 類別自行實作。詳情請參閱「
處理程序和執行緒」。
新增應用程式連結資訊
頻道可使用應用程式連結,讓使用者在觀看頻道內容時啟動相關活動。頻道應用程式會使用應用程式連結啟動活動,顯示相關資訊或額外內容,藉此延長使用者參與度。例如,您可以使用應用程式連結執行以下操作:
- 引導使用者探索及購買相關內容。
- 提供目前播放內容的其他資訊。
- 觀看系列內容時,開始觀看下一集。
- 讓使用者與內容互動 (例如評分或評論內容),不必中斷內容播放。
當使用者在觀看頻道內容時按下 選擇 顯示電視選單時,會顯示應用程式連結。
圖 1. 顯示頻道內容時,應用程式連結會顯示在「頻道」列中。
使用者選取應用程式連結後,系統會使用頻道應用程式指定的意圖 URI 啟動活動。應用程式連結活動啟動時,頻道內容會繼續播放。使用者可以按 Back 返回頻道內容。
提供應用程式連結通道數據
Android TV 會使用頻道資料中的資訊,自動為每個頻道建立應用程式連結。如要提供應用程式連結資訊,請在 TvContract.Channels 欄位中指定下列詳細資料:
COLUMN_APP_LINK_COLOR- 此頻道應用連結的強調色。如要查看強調色範例,請參閱圖 2 的標註 3。COLUMN_APP_LINK_ICON_URI- 此頻道應用連結的應用程式徽章圖示的 URI。如需應用程式徽章圖示範例,請參閱圖 2 的標註 2。COLUMN_APP_LINK_INTENT_URI- 此頻道的應用程式連結的意圖 URI。您可以使用toUri(int)和URI_INTENT_SCHEME建立 URI,並使用parseUri將 URI 轉換回原始意圖。COLUMN_APP_LINK_POSTER_ART_URI- 用於做為這個頻道應用程式連結背景的海報圖片 URI。海報圖片範例見圖 2,標註 1。COLUMN_APP_LINK_TEXT- 這個管道的應用程式連結說明文字。如需應用程式連結說明的範例,請參閱圖 2 的標註 3。
如果通道資料未指定應用連結訊息,系統將建立預設應用連結。系統會選擇預設詳細資料,如下所示:
- 如果是意圖 URI (
COLUMN_APP_LINK_INTENT_URI),系統會使用CATEGORY_LEANBACK_LAUNCHER類別的ACTION_MAIN活動,通常是在應用程式資訊清單中定義。如果未定義這項活動,系統會顯示無法運作的應用程式連結,使用者點按後不會有任何反應。 - 對於描述性文字(
COLUMN_APP_LINK_TEXT),系統使用「開啟app-name」。如果沒有定義有效的應用程式連結意圖 URI,系統則使用「無可用連結」。 - 如果是強調色 (
COLUMN_APP_LINK_COLOR),系統會使用預設應用程式顏色。 - 如果是海報圖片 (
COLUMN_APP_LINK_POSTER_ART_URI),系統會使用應用程式的主畫面橫幅。如果應用程式未提供橫幅,系統會使用預設的 TV 應用程式圖片。 - 如果是徽章圖示 (
COLUMN_APP_LINK_ICON_URI),系統會使用顯示應用程式名稱的徽章。如果系統同時使用套用橫幅或預設應用程式圖片作為海報圖片,則不會顯示套用徽章。
您可以在應用程式的設定活動中,為管道指定應用程式連結詳細資料。您可以隨時更新這些應用程式連結詳細資料,因此如果應用程式連結需要配合管道變更,請視需要更新應用程式連結詳細資料並呼叫 ContentResolver.update。如要進一步瞭解如何更新頻道資料,請參閱「更新頻道資料」。