您的電視輸入資料至少必須提供電子節目指南 (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
:電視輸入服務的 IDCOLUMN_SERVICE_TYPE
:頻道的服務類型COLUMN_TYPE
- 頻道的放送標準 類型COLUMN_VIDEO_FORMAT
- 預設影片格式 頻道
雖然電視輸入架構相當通用,可同時處理傳統廣播和電視 over-the-top (OTT) 內容沒有任何差異,建議您在 以及上述的傳統廣播頻道:
COLUMN_ORIGINAL_NETWORK_ID
- 電視 聯播網 IDCOLUMN_SERVICE_ID
:服務 IDCOLUMN_TRANSPORT_STREAM_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 所示。為了確保頻道和節目資訊能與系統 TV 應用程式的 頻道和計劃資訊的畫面,請遵循下列規範。
- 頻道編號 (
COLUMN_DISPLAY_NUMBER
) - 圖示
(
android:icon
的 電視輸入裝置的資訊清單) - 方案說明 (
COLUMN_SHORT_DESCRIPTION
) - 方案名稱 (
COLUMN_TITLE
) - 頻道標誌 (
TvContract.Channels.Logo
)- 使用 #EEEEEE 顏色代表與周圍文字相符
- 不加入邊框間距
- 海報圖片 (
COLUMN_POSTER_ART_URI
)- 顯示比例介於 16:9 和 4:3
![](https://developer.android.google.cn/static/images/tv/channel-info.png?authuser=9&hl=zh-tw)
圖 1. 系統電視應用程式頻道和節目資訊顯示器。
系統 TV 應用程式透過節目指南提供相同的資訊,包括海報圖片 如圖 2 所示。
![](https://developer.android.google.cn/static/images/tv/prog-guide.png?authuser=9&hl=zh-tw)
圖 2. 系統 TV 應用程式節目指南。
更新頻道資料
更新現有頻道資料時,請使用
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(); } } } }
如果您需要定期更新電子節目表資料,建議您使用
WorkManager
敬上
在閒置時間執行更新程序,例如每天凌晨 3 點。
其他將資料更新工作與 UI 執行緒分離的技術包括:
HandlerThread
類別,也可以使用 Looper
自行實作
和 Handler
類別。詳情請參閱
程序和執行緒。
新增應用程式連結資訊
頻道可使用應用程式連結,讓使用者輕鬆啟動相關的 觀看頻道內容時的活動。管道應用程式會使用 應用程式連結,然後啟動會顯示 相關資訊或其他內容舉例來說,您可以使用應用程式連結 :
- 引導使用者探索及購買相關內容。
- 針對目前播放的內容提供額外資訊。
- 觀看系列內容時, 播放下一集 這是 Gemini 版 Google Workspace 系列課程之一
- 讓使用者與內容互動,例如評分或評論 完全不必中斷內容播放。
使用者按下 Select 後 觀看頻道內容時顯示的電視選單。
![](https://developer.android.google.cn/static/images/training/tv/tif/app-link.png?authuser=9&hl=zh-tw)
圖 1. 應用程式連結範例 頻道內容顯示時,則會顯示在「頻道」列中。
當使用者選取應用程式連結時,系統會透過 頻道應用程式指定的意圖 URI。頻道內容繼續播放 在應用程式連結活動啟用的狀態下進行。使用者可以返回頻道 按下「返回」圖示。
提供應用程式連結管道資料
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 轉換回原始意圖parseUri()
。COLUMN_APP_LINK_POSTER_ART_URI
- 應用程式連結背景的海報圖片 URI 。如需海報圖片範例,請參閱圖 2,圖說 1。COLUMN_APP_LINK_TEXT
: 這個頻道的應用程式連結的描述性連結文字。範例說明 應用程式連結說明,請參閱圖 2 中的文字,摘要 3。
![](https://developer.android.google.cn/static/images/training/tv/tif/app-link-diagram.png?authuser=9&hl=zh-tw)
圖 2. 應用程式連結詳細資料。
如果管道資料未指定應用程式連結資訊,系統會 建立預設的應用程式連結系統會選擇下列預設詳細資料:
- 適用於意圖 URI
(
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
), 系統會使用應用程式的主畫面橫幅。如果應用程式未提供 橫幅,系統會使用預設的 TV 應用程式圖片。 - 徽章圖示
(
COLUMN_APP_LINK_ICON_URI
), 系統會使用顯示應用程式名稱的標記如果系統也使用 代表海報圖片的應用程式橫幅或預設的應用程式圖片,不會顯示應用程式徽章。
您可以在應用程式的
設定活動。您隨時都可更新這些應用程式連結詳細資料,例如
如果應用程式連結需要與頻道變更一致,請更新應用程式
連結詳細資料和通話
ContentResolver.update()
。進一步瞭解如何更新
頻道資料,請參閱「更新頻道資料」一文。