您的電視輸入裝置必須在設定活動中至少提供一個頻道的電子節目指南 (EPG) 資料。您也應該定期更新資料,並將更新大小和處理資料的處理執行緒納入考量。此外,您也可以為頻道提供應用程式連結,引導使用者前往相關內容和活動。本課程將討論在系統資料庫中建立及更新管道和計畫資料,並將上述注意事項納入考量。
歡迎試用 TV 輸入服務範例應用程式。
授予權限
為了讓電視輸入裝置與 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
- 頻道的預設影片格式
雖然電視輸入架構十分通用,可處理傳統廣播和 over-the-top (OTT) 內容而無差異,但除了上述欄位之外,您可能需要定義下列資料欄,以便更準確地識別傳統廣播頻道:
COLUMN_ORIGINAL_NETWORK_ID
:電視聯播網 IDCOLUMN_SERVICE_ID
:服務 IDCOLUMN_TRANSPORT_STREAM_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 之間
系統電視應用程式會透過節目指南 (包括海報圖片) 提供相同的資訊,如圖 2 所示。
更新頻道資料
更新現有管道資料時,請使用 update()
方法,而不要刪除並重新新增資料。您可以在選擇要更新的記錄時,使用 Channels.COLUMN_VERSION_NUMBER
和 Programs.COLUMN_VERSION_NUMBER
來識別資料的目前版本。
注意:將管道資料新增至 ContentProvider
可能需要一些時間。請僅在您設定 EpgSyncJobService
以在背景更新其餘管道資料時,新增目前的節目 (在目前時間的兩小時內)。如需範例,請參閱「
Android TV Live 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
類別自行實作。詳情請參閱「
程序和執行緒」。
新增應用程式連結資訊
頻道可使用應用程式連結,讓使用者在觀看頻道內容時輕鬆啟動相關活動。管道應用程式會使用應用程式連結來啟動顯示相關資訊或其他內容的活動,藉此提升使用者參與度。舉例來說,您可以使用應用程式連結執行以下操作:
- 引導使用者發掘並購買相關內容。
- 針對目前播放的內容提供額外資訊。
- 觀看劇集內容時,開始觀看系列中的下一集。
- 讓使用者與內容互動 (例如評分或評論內容),而不中斷內容播放。
當使用者在觀看頻道內容時,按下「Select」可顯示電視選單,系統就會顯示應用程式連結。
當使用者選取應用程式連結時,系統會使用頻道應用程式指定的意圖 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,系統就會使用「No link available」。 - 如果是強調色 (
COLUMN_APP_LINK_COLOR
),系統會使用預設的應用程式顏色。 - 如果是海報圖片 (
COLUMN_APP_LINK_POSTER_ART_URI
),系統會使用應用程式的主畫面橫幅。如果應用程式未提供橫幅,系統會使用預設的電視應用程式圖片。 - 如果是標記圖示 (
COLUMN_APP_LINK_ICON_URI
),系統會使用標記圖示來顯示應用程式名稱。如果系統在海報圖片中使用應用程式橫幅或預設應用程式圖片,系統就不會顯示應用程式徽章。
您可以在應用程式的設定活動中指定管道的應用程式連結詳細資料。您隨時可以更新這些應用程式連結詳細資料,因此如果應用程式連結需要與管道變更相符,請更新應用程式連結詳細資料,並視需要呼叫 ContentResolver.update()
。如要進一步瞭解如何更新頻道資料,請參閱「更新管道資料」。