1. 總覽
本程式碼研究室會說明如何修改現有的 Android TV 應用程式,以支援從現有的 Cast 發送端應用程式投放和通訊。
什麼是 Google Cast 和 Cast Connect?
Google Cast 可讓使用者將行動裝置上的內容投放到電視。一般 Google Cast 工作階段包含兩個元件:傳送端和接收端應用程式。傳送者應用程式 (例如行動應用程式或網站 (例如 YouTube.com)) 會啟動及控制 Cast 接收器應用程式的播放作業。投放接收端應用程式是在 Chromecast 和 Android TV 裝置上執行的 HTML 5 應用程式。
Cast 工作階段中的所有狀態幾乎都會儲存在接收器應用程式中。狀態更新後 (例如載入新媒體項目),系統就會向所有傳送者播送媒體狀態。這些廣播訊息包含投放工作階段的目前狀態。傳送端應用程式使用此媒體狀態,在其 UI 中顯示播放資訊。
以這個基礎架構為基礎的 Cast Connect 功能,並將 Android TV 應用程式當做接收器。Cast Connect 程式庫可讓 Android TV 應用程式接收訊息和廣播媒體狀態,就像是投放接收器應用程式一樣。
我們要建構什麼?
完成本程式碼研究室後,您就能使用投放傳送者應用程式將影片投放到 Android TV 應用程式。Android TV 應用程式也可以透過 Cast 通訊協定與傳送者應用程式進行通訊。
課程內容
- 如何將 Cast Connect 程式庫新增至範例 ATV 應用程式。
- 如何連線 Cast 發送端並啟動 ATV 應用程式。
- 如何透過 Cast 傳送端應用程式,在 ATV 應用程式中播放媒體。
- 如何將媒體狀態從 ATV 應用程式傳送至 Cast 發送端應用程式。
軟硬體需求
- 最新版 Android SDK
- 最新版 Android Studio具體來說,是
Chipmunk | 2021.2.1
以上版本。 - 已啟用開發人員選項和 USB 偵錯的 Android TV 裝置。
- 已啟用開發人員選項和 USB 偵錯的 Android 手機。
- USB 資料傳輸線,可連接 Android 手機和 Android TV 裝置與開發電腦。
- 使用 Kotlin 開發 Android 應用程式的基本知識。
2. 取得程式碼範例
您可以將所有程式碼範例下載至電腦...
將下載的 ZIP 檔案解壓縮。
3. 執行範例應用程式
首先,我們來看看完成的範例應用程式是什麼樣子。Android TV 應用程式使用 Leanback UI 和基本影片播放器。使用者可以從清單中選取影片,使用者選取影片後,就會在電視上播放。有了隨附的行動傳送應用程式,使用者也可以將影片投放到 Android TV 應用程式。
註冊開發人員裝置
如要啟用 Cast Connect 功能以進行應用程式開發,您必須前往 Cast 開發人員控制台,註冊要使用的 Android TV 裝置內建 Chromecast 序號。如要找到序號,請依序前往 [設定] 裝置偏好設定 >內建 Chromecast >Android TV 上的序號。請注意,這組號碼不同於實體裝置的序號,必須以上述方式取得。
基於安全考量,Cast Connect 僅適用於從 Google Play 商店安裝的應用程式。在開始註冊程序的 15 分鐘後,重新啟動裝置。
安裝 Android 寄件者應用程式
為了測試來自行動裝置的要求,我們在原始碼的 ZIP 下載檔案中,提供了一個名為「投放影片」的簡易傳送端應用程式,做為 mobile-sender-0629.apk
檔案。我們會使用 ADB 安裝 APK。如果你已安裝其他版本的「投放影片」,請先從裝置中的所有設定檔解除安裝該版本,再繼續操作。
- 在 Android 手機上啟用開發人員選項和 USB 偵錯功能。
- 插入 USB 資料傳輸線,將 Android 手機和開發電腦連接。
- 在 Android 手機上安裝
mobile-sender-0629.apk
。
- 您可以在 Android 手機上找到「投放影片」傳送端應用程式。
安裝 Android TV 應用程式
以下說明如何在 Android Studio 中開啟及執行完整的範例應用程式:
- 在歡迎畫面中選取「Import Project」,或是依序選取「File」>「File」新增 >匯入專案...選單選項。
- 從範例程式碼資料夾中選取
app-done
目錄,然後按一下 [OK]。 - 按一下「檔案」> 將專案與 Gradle 檔案同步處理。
- 在 Android TV 裝置上啟用開發人員選項和 USB 偵錯功能。
- ADB 與 Android TV 裝置連線後,該裝置應該會顯示在 Android Studio 中。
- 按一下 「Run」按鈕,幾秒後應該就會顯示名為「Cast Connect Codelab」的 ATV 應用程式。
使用 Cast Connect 搭配 ATV 應用程式播放內容
- 前往 Android TV 主畫面。
- 在 Android 手機上開啟「投放影片傳送者」應用程式。按一下「投放」按鈕 ,然後選取你的 ATV 裝置。
- 系統會在你的 ATV 上啟動 Cast Connect Codelab ATV 應用程式,而傳送端中的「投放」按鈕會指出已連上 。
- 從 ATV 應用程式中選取影片,影片就會在 ATV 上播放。
- 現在,手機上的「傳送者」應用程式的底部會顯示迷你控制器。您可以使用播放/暫停按鈕控製播放。
- 從行動電話中選取影片並播放。影片會開始在您的 ATV 上播放,且展開的控制器會顯示在行動裝置中。
- 鎖定手機後,你就能在解鎖手機時,在螢幕鎖定畫面上顯示通知,控制媒體播放或停止投放。
4. 準備 start 專案
我們確認已完成應用程式的 Cast Connect 整合程序,現在需要為您下載的啟動應用程式新增 Cast Connect 相關支援。您現在可以使用 Android Studio,以範例專案為基礎進行建構了:
- 在歡迎畫面中選取「Import Project」,或是依序選取「File」>「File」新增 >匯入專案...選單選項。
- 從範例程式碼資料夾中選取
app-start
目錄,然後按一下 [OK]。 - 按一下「檔案」> 將專案與 Gradle 檔案同步處理。
- 選取 ATV 裝置,然後按一下 「Run」按鈕,執行應用程式並探索 UI。
應用程式設計
應用程式提供影片清單讓使用者瀏覽。使用者可以選取要在 Android TV 上播放的影片。這個應用程式由兩個主要活動組成:MainActivity
和 PlaybackActivity
。
MainActivity
這個活動含有片段 (MainFragment
)。影片清單及相關中繼資料會在 MovieList
類別中設定,呼叫 setupMovies()
方法即可建構 Movie
物件清單。
Movie
物件代表影片實體,包括標題、說明、圖片縮圖和影片網址。每個 Movie
物件都會繫結至 CardPresenter
,以顯示含有標題和工作室的影片縮圖,並傳遞至 ArrayObjectAdapter
。
選取項目後,系統會將對應的 Movie
物件傳遞至 PlaybackActivity
。
PlaybackActivity
這項活動包含片段 (PlaybackVideoFragment
),其中含有 ExoPlayer
的 VideoView
、一些媒體控制項,以及顯示所選影片說明的文字區域,可讓使用者在 Android TV 上播放影片。使用者可以使用遙控器播放/暫停影片或尋找播放影片。
Cast Connect 必備條件
Cast Connect 使用的新版 Google Play 服務必須更新 ATV 應用程式才能使用 AndroidX 命名空間。
如要在 Android TV 應用程式中支援 Cast Connect,您必須在媒體工作階段中建立和支援事件。Cast Connect 程式庫會根據媒體工作階段的狀態產生媒體狀態。Cast Connect 程式庫也會使用您的媒體工作階段,在收到傳送者傳送特定訊息 (例如暫停) 時發出信號。
5. 設定 Cast 支援
依附元件
更新應用程式 build.gradle
檔案,納入必要的程式庫依附元件:
dependencies {
....
// Cast Connect libraries
implementation 'com.google.android.gms:play-services-cast-tv:20.0.0'
implementation 'com.google.android.gms:play-services-cast:21.1.0'
}
同步處理專案,確認專案版本是否正確無誤。
初始化
CastReceiverContext
是一個單例模式物件,用於協調所有 Cast 互動。您必須實作 ReceiverOptionsProvider
介面,才能在 CastReceiverContext
初始化時提供 CastReceiverOptions
。
建立 CastReceiverOptionsProvider.kt
檔案,然後在專案中加入以下類別:
package com.google.sample.cast.castconnect
import android.content.Context
import com.google.android.gms.cast.tv.ReceiverOptionsProvider
import com.google.android.gms.cast.tv.CastReceiverOptions
class CastReceiverOptionsProvider : ReceiverOptionsProvider {
override fun getOptions(context: Context): CastReceiverOptions {
return CastReceiverOptions.Builder(context)
.setStatusText("Cast Connect Codelab")
.build()
}
}
接著,請在應用程式 AndroidManifest.xml
檔案的 <application>
標記中指定接收器選項供應器:
<application>
...
<meta-data
android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.sample.cast.castconnect.CastReceiverOptionsProvider" />
</application>
如要透過 Cast 傳送端與 ATV 應用程式連線,請選取您要啟動的活動。在本程式碼研究室中,我們會在投放工作階段時啟動應用程式的 MainActivity
。在 AndroidManifest.xml
檔案的 MainActivity
中新增啟動意圖篩選器。
<activity android:name=".MainActivity">
...
<intent-filter>
<action android:name="com.google.android.gms.cast.tv.action.LAUNCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
Cast 接收器內容生命週期
您應該在應用程式啟動時啟動 CastReceiverContext
,並在應用程式移至背景時停止 CastReceiverContext
。建議您使用 androidx.lifecycle 程式庫中的 LifecycleObserver
管理呼叫 CastReceiverContext.start()
和 CastReceiverContext.stop()
開啟 MyApplication.kt
檔案,在應用程式的 onCreate
方法中呼叫 initInstance()
,即可初始化投放內容。在 AppLifeCycleObserver
類別 start()
中,應用程式繼續執行時為 CastReceiverContext
,在應用程式暫停時則為 stop()
:
package com.google.sample.cast.castconnect
import com.google.android.gms.cast.tv.CastReceiverContext
...
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
CastReceiverContext.initInstance(this)
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
}
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
Log.d(LOG_TAG, "onResume")
CastReceiverContext.getInstance().start()
}
override fun onPause(owner: LifecycleOwner) {
Log.d(LOG_TAG, "onPause")
CastReceiverContext.getInstance().stop()
}
}
}
將 MediaSession 連結至 MediaManager
MediaManager
是 CastReceiverContext
單例模式的屬性,可管理媒體狀態、處理載入意圖、將來自傳送方的媒體命名空間訊息轉譯為媒體指令,以及將媒體狀態傳回給傳送者。
建立 MediaSession
時,您也必須將目前的 MediaSession
權杖提供給 MediaManager
,讓它知道要在哪裡傳送指令及擷取媒體播放狀態。在 PlaybackVideoFragment.kt
檔案中,先確認 MediaSession
已初始化,然後再將符記設為 MediaManager
。
import com.google.android.gms.cast.tv.CastReceiverContext
import com.google.android.gms.cast.tv.media.MediaManager
...
class PlaybackVideoFragment : VideoSupportFragment() {
private var castReceiverContext: CastReceiverContext? = null
...
private fun initializePlayer() {
if (mPlayer == null) {
...
mMediaSession = MediaSessionCompat(getContext(), LOG_TAG)
...
castReceiverContext = CastReceiverContext.getInstance()
if (castReceiverContext != null) {
val mediaManager: MediaManager = castReceiverContext!!.getMediaManager()
mediaManager.setSessionCompatToken(mMediaSession!!.getSessionToken())
}
}
}
}
當你因為播放無效而釋出 MediaSession
時,應在 MediaManager
上設定空值權杖:
private fun releasePlayer() {
mMediaSession?.release()
castReceiverContext?.mediaManager?.setSessionCompatToken(null)
...
}
執行範例應用程式
按一下 「Run」按鈕,在 ATV 裝置上部署應用程式、關閉應用程式並返回 ATV 主畫面。在傳送者中,按一下「投放」按鈕 ,然後選取你的 ATV 裝置。ATV 應用程式將在 ATV 裝置上啟動,且投放按鈕狀態已連結。
6. 正在載入媒體
系統會透過意圖 (即您在 Play 管理中心中定義的套件名稱) 傳送載入指令。您必須在 Android TV 應用程式中新增下列預先定義的意圖篩選器,才能指定會收到此意圖的目標活動。在 AndroidManifest.xml
檔案中,將載入意圖篩選器新增至 PlayerActivity
:
<activity android:name="com.google.sample.cast.castconnect.PlaybackActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
處理 Android TV 上的載入要求
現在活動已設為接收此意圖 (內含載入要求),我們需要處理該要求。
活動啟動時,應用程式會呼叫名為 processIntent
的不公開方法。這個方法包含處理傳入意圖的邏輯。為了處理載入要求,我們會修改這個方法,並呼叫 MediaManager
執行個體的 onNewIntent
方法,傳送要進一步處理的意圖。如果 MediaManager
偵測到意圖是載入要求,就會從意圖中擷取 MediaLoadRequestData
物件,並叫用 MediaLoadCommandCallback.onLoad()
。修改 PlaybackVideoFragment.kt
檔案中的 processIntent
方法,以處理包含載入要求的意圖:
fun processIntent(intent: Intent?) {
val mediaManager: MediaManager = CastReceiverContext.getInstance().getMediaManager()
// Pass intent to Cast SDK
if (mediaManager.onNewIntent(intent)) {
return
}
// Clears all overrides in the modifier.
mediaManager.getMediaStatusModifier().clear()
// If the SDK doesn't recognize the intent, handle the intent with your own logic.
...
}
接下來,我們會擴充抽象類別 MediaLoadCommandCallback
,覆寫由 MediaManager
呼叫的 onLoad()
方法。這個方法會接收載入要求的資料,並將其轉換為 Movie
物件。轉換完成後,本機播放器會播放電影。接著,MediaManager
會使用 MediaLoadRequest
更新,並將 MediaStatus
播送給已連結的傳送者。在 PlaybackVideoFragment.kt
檔案中建立名為 MyMediaLoadCommandCallback
的巢狀私人類別:
import com.google.android.gms.cast.MediaLoadRequestData
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.cast.MediaError
import com.google.android.gms.cast.tv.media.MediaException
import com.google.android.gms.cast.tv.media.MediaCommandCallback
import com.google.android.gms.cast.tv.media.QueueUpdateRequestData
import com.google.android.gms.cast.tv.media.MediaLoadCommandCallback
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import android.widget.Toast
...
private inner class MyMediaLoadCommandCallback : MediaLoadCommandCallback() {
override fun onLoad(
senderId: String?, mediaLoadRequestData: MediaLoadRequestData): Task<MediaLoadRequestData> {
Toast.makeText(activity, "onLoad()", Toast.LENGTH_SHORT).show()
return if (mediaLoadRequestData == null) {
// Throw MediaException to indicate load failure.
Tasks.forException(MediaException(
MediaError.Builder()
.setDetailedErrorCode(MediaError.DetailedErrorCode.LOAD_FAILED)
.setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
.build()))
} else Tasks.call {
play(convertLoadRequestToMovie(mediaLoadRequestData)!!)
// Update media metadata and state
val mediaManager = castReceiverContext!!.mediaManager
mediaManager.setDataFromLoad(mediaLoadRequestData)
mediaLoadRequestData
}
}
}
private fun convertLoadRequestToMovie(mediaLoadRequestData: MediaLoadRequestData?): Movie? {
if (mediaLoadRequestData == null) {
return null
}
val mediaInfo: MediaInfo = mediaLoadRequestData.getMediaInfo() ?: return null
var videoUrl: String = mediaInfo.getContentId()
if (mediaInfo.getContentUrl() != null) {
videoUrl = mediaInfo.getContentUrl()
}
val metadata: MediaMetadata = mediaInfo.getMetadata()
val movie = Movie()
movie.videoUrl = videoUrl
movie.title = metadata?.getString(MediaMetadata.KEY_TITLE)
movie.description = metadata?.getString(MediaMetadata.KEY_SUBTITLE)
if(metadata?.hasImages() == true) {
movie.cardImageUrl = metadata.images[0].url.toString()
}
return movie
}
現在已定義回呼,我們需要將其註冊至 MediaManager
。呼叫 MediaManager.onNewIntent()
前,必須先註冊回呼。在玩家初始化時新增 setMediaLoadCommandCallback
:
private fun initializePlayer() {
if (mPlayer == null) {
...
mMediaSession = MediaSessionCompat(getContext(), LOG_TAG)
...
castReceiverContext = CastReceiverContext.getInstance()
if (castReceiverContext != null) {
val mediaManager: MediaManager = castReceiverContext.getMediaManager()
mediaManager.setSessionCompatToken(mMediaSession.getSessionToken())
mediaManager.setMediaLoadCommandCallback(MyMediaLoadCommandCallback())
}
}
}
執行範例應用程式
按一下 「Run」按鈕,在 ATV 裝置上部署應用程式。在傳送者中,按一下「投放」按鈕 ,然後選取你的 ATV 裝置。ATV 應用程式將在 ATV 裝置上啟動。在行動裝置上選取影片,影片就會在 ATV 上播放。檢查手機上是否有播放控制項的手機通知。嘗試使用暫停等控制選項,觀眾應該在 ATV 裝置上暫停播放影片。
7. 支援投放控制指令
目前應用程式支援與媒體工作階段相容的基本指令,例如播放、暫停和跳轉。不過,部分投放控制指令不適用於媒體工作階段。您需要註冊 MediaCommandCallback
,才能支援這些 Cast 控制指令。
在玩家初始化時,使用 setMediaCommandCallback
將 MyMediaCommandCallback
新增至 MediaManager
例項:
private fun initializePlayer() {
...
castReceiverContext = CastReceiverContext.getInstance()
if (castReceiverContext != null) {
val mediaManager = castReceiverContext!!.mediaManager
...
mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
}
}
建立 MyMediaCommandCallback
類別以覆寫方法,例如 onQueueUpdate()
以支援這些 Cast 控制指令:
private inner class MyMediaCommandCallback : MediaCommandCallback() {
override fun onQueueUpdate(
senderId: String?,
queueUpdateRequestData: QueueUpdateRequestData
): Task<Void> {
Toast.makeText(getActivity(), "onQueueUpdate()", Toast.LENGTH_SHORT).show()
// Queue Prev / Next
if (queueUpdateRequestData.getJump() != null) {
Toast.makeText(
getActivity(),
"onQueueUpdate(): Jump = " + queueUpdateRequestData.getJump(),
Toast.LENGTH_SHORT
).show()
}
return super.onQueueUpdate(senderId, queueUpdateRequestData)
}
}
8. 使用媒體狀態
修改媒體狀態
Cast Connect 會從媒體工作階段取得基本媒體狀態。如要支援進階功能,Android TV 應用程式可透過 MediaStatusModifier
指定及覆寫其他狀態屬性。MediaStatusModifier
一律會在您在 CastReceiverContext
中設定的 MediaSession
運作。
舉例來說,如要在觸發 onLoad
回呼時指定 setMediaCommandSupported
:
import com.google.android.gms.cast.MediaStatus
...
private class MyMediaLoadCommandCallback : MediaLoadCommandCallback() {
fun onLoad(
senderId: String?,
mediaLoadRequestData: MediaLoadRequestData
): Task<MediaLoadRequestData> {
Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show()
...
return Tasks.call({
play(convertLoadRequestToMovie(mediaLoadRequestData)!!)
...
// Use MediaStatusModifier to provide additional information for Cast senders.
mediaManager.getMediaStatusModifier()
.setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT, true)
.setIsPlayingAd(false)
mediaManager.broadcastMediaStatus()
// Return the resolved MediaLoadRequestData to indicate load success.
mediaLoadRequestData
})
}
}
送出前攔截 MediaStatus
與網路接收器 SDK 的 MessageInterceptor
類似,您可以在 MediaManager
中指定 MediaStatusWriter
,在 MediaStatus
播送給連結的寄件者之前,對其進行其他修改。
舉例來說,您可以先在 MediaStatus
中設定自訂資料,再傳送給行動寄件者:
import com.google.android.gms.cast.tv.media.MediaManager.MediaStatusInterceptor
import com.google.android.gms.cast.tv.media.MediaStatusWriter
import org.json.JSONObject
import org.json.JSONException
...
private fun initializePlayer() {
if (mPlayer == null) {
...
if (castReceiverContext != null) {
...
val mediaManager: MediaManager = castReceiverContext.getMediaManager()
...
// Use MediaStatusInterceptor to process the MediaStatus before sending out.
mediaManager.setMediaStatusInterceptor(
MediaStatusInterceptor { mediaStatusWriter: MediaStatusWriter ->
try {
mediaStatusWriter.setCustomData(JSONObject("{myData: 'CustomData'}"))
} catch (e: JSONException) {
Log.e(LOG_TAG,e.message,e);
}
})
}
}
}
9. 恭喜
您現已瞭解如何使用 Cast Connect 程式庫啟用 Android TV 應用程式投放功能。
詳情請參閱開發人員指南:/cast/docs/android_tv_receiver。