本頁面提供程式碼片段,並說明可用於自訂 Android TV Receiver 應用程式的功能。
設定程式庫
如要讓 Cast Connect API 可供 Android TV 應用程式使用,請按照下列步驟操作:
-
開啟應用程式模組目錄中的
build.gradle
檔案。 -
確認
google()
已納入repositories
清單。repositories { google() }
-
視應用程式的目標裝置類型而定,將最新版本的程式庫新增至依附元件:
-
適用於 Android 接收器應用程式:
dependencies { implementation 'com.google.android.gms:play-services-cast-tv:21.1.1' implementation 'com.google.android.gms:play-services-cast:22.0.0' }
-
Android 傳送端應用程式:
dependencies { implementation 'com.google.android.gms:play-services-cast:21.1.1' implementation 'com.google.android.gms:play-services-cast-framework:22.0.0' }
-
適用於 Android 接收器應用程式:
-
儲存變更後,按一下工具列中的
Sync Project with Gradle Files
。
-
請確認您的
Podfile
指定google-cast-sdk
4.8.3 以上版本 -
指定 iOS 14 以上版本。詳情請參閱版本資訊。
platform: ios, '14' def target_pods pod 'google-cast-sdk', '~>4.8.3' end
- 必須使用 Chromium 瀏覽器 M87 以上版本。
-
將 Web Sender API 程式庫新增至專案
<script src="//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>
AndroidX 需求
新版 Google Play 服務要求應用程式已更新至使用 androidx
命名空間。請按照遷移至 AndroidX 的操作說明操作。
Android TV 應用程式 - 必要條件
如要在 Android TV 應用程式中支援 Cast Connect,您必須透過媒體工作階段建立及支援事件。媒體工作階段提供的資料可提供媒體狀態的基本資訊,例如位置、播放狀態等。Cast Connect 程式庫也會使用您的媒體工作階段,在收到傳送端傳送的特定訊息 (例如暫停) 時發出訊號。
如要進一步瞭解媒體工作階段和如何初始化媒體工作階段,請參閱使用媒體工作階段指南。
媒體工作階段生命週期
應用程式應在播放開始時建立媒體工作階段,並在無法再控制時釋出。舉例來說,如果您的應用程式是影片應用程式,則應在使用者退出播放活動時釋放工作階段,方法是選取「返回」來瀏覽其他內容,或將應用程式設為背景執行。如果您的應用程式是音樂應用程式,則應在應用程式不再播放任何媒體時釋放工作階段。
更新工作階段狀態
媒體工作階段中的資料應與播放器狀態保持一致。舉例來說,當播放處於暫停狀態時,您應更新播放狀態以及支援的動作。下表列出您負責更新的狀態。
MediaMetadataCompat
中繼資料欄位 | 說明 |
---|---|
METADATA_KEY_TITLE (必要) | 媒體標題。 |
METADATA_KEY_DISPLAY_SUBTITLE | 副標題。 |
METADATA_KEY_DISPLAY_ICON_URI | 圖示網址。 |
METADATA_KEY_DURATION (必要) | 媒體時間長度。 |
METADATA_KEY_MEDIA_URI | 內容 ID。 |
METADATA_KEY_ARTIST | 藝人。 |
METADATA_KEY_ALBUM | 相簿。 |
PlaybackStateCompat
必要方法 | 說明 |
---|---|
setActions() | 設定支援的媒體指令。 |
setState() | 設定播放狀態和目前位置。 |
MediaSessionCompat
必要方法 | 說明 |
---|---|
setRepeatMode() | 設定重複播放模式。 |
setShuffleMode() | 設定隨機播放模式。 |
setMetadata() | 設定媒體中繼資料。 |
setPlaybackState() | 設定播放狀態。 |
private fun updateMediaSession() { val metadata = MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, mMovie.getCardImageUrl()) .build() val playbackState = PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, player.getPosition(), player.getPlaybackSpeed(), System.currentTimeMillis() ) .build() mediaSession.setMetadata(metadata) mediaSession.setPlaybackState(playbackState) }
private void updateMediaSession() { MediaMetadataCompat metadata = new MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,mMovie.getCardImageUrl()) .build(); PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, player.getPosition(), player.getPlaybackSpeed(), System.currentTimeMillis()) .build(); mediaSession.setMetadata(metadata); mediaSession.setPlaybackState(playbackState); }
處理傳輸控制
應用程式應實作媒體工作階段傳輸控制回呼。下表列出需要處理的傳輸控制項動作:
MediaSessionCompat.Callback
動作 | 說明 |
---|---|
onPlay() | 繼續 |
onPause() | 暫停 |
onSeekTo() | 跳轉至某個位置 |
onStop() | 停止播放目前的媒體 |
class MyMediaSessionCallback : MediaSessionCompat.Callback() { override fun onPause() { // Pause the player and update the play state. ... } override fun onPlay() { // Resume the player and update the play state. ... } override fun onSeekTo (long pos) { // Seek and update the play state. ... } ... } mediaSession.setCallback( MyMediaSessionCallback() );
public MyMediaSessionCallback extends MediaSessionCompat.Callback { public void onPause() { // Pause the player and update the play state. ... } public void onPlay() { // Resume the player and update the play state. ... } public void onSeekTo (long pos) { // Seek and update the play state. ... } ... } mediaSession.setCallback(new MyMediaSessionCallback());
設定投放支援
當傳送端應用程式傳送啟動要求時,系統會使用應用程式命名空間建立意圖。您的應用程式負責處理此問題,並在 TV 應用程式啟動時建立 CastReceiverContext
物件的例項。在電視應用程式執行期間,您需要使用 CastReceiverContext
物件與 Cast 互動。這個物件可讓電視應用程式接受來自任何已連結傳送端的 Cast 媒體訊息。
Android TV 設定
新增啟動意圖篩選器
將新的意圖篩選器新增至要處理傳送端應用程式啟動意圖的活動:
<activity android:name="com.example.activity">
<intent-filter>
<action android:name="com.google.android.gms.cast.tv.action.LAUNCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
指定接收者選項供應器
您必須實作 ReceiverOptionsProvider
才能提供 CastReceiverOptions
:
class MyReceiverOptionsProvider : ReceiverOptionsProvider { override fun getOptions(context: Context?): CastReceiverOptions { return CastReceiverOptions.Builder(context) .setStatusText("My App") .build() } }
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider { @Override public CastReceiverOptions getOptions(Context context) { return new CastReceiverOptions.Builder(context) .setStatusText("My App") .build(); } }
接著,請在 AndroidManifest
中指定選項供應器:
<meta-data
android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.example.mysimpleatvapplication.MyReceiverOptionsProvider" />
在 CastReceiverContext
初始化時,系統會使用 ReceiverOptionsProvider
提供 CastReceiverOptions
。
投放接收端內容
在建立應用程式時初始化 CastReceiverContext
:
override fun onCreate() { CastReceiverContext.initInstance(this) ... }
@Override public void onCreate() { CastReceiverContext.initInstance(this); ... }
在應用程式移至前景時啟動 CastReceiverContext
:
CastReceiverContext.getInstance().start()
CastReceiverContext.getInstance().start();
針對影片應用程式或不支援背景播放的應用程式,在應用程式進入背景後,請在 CastReceiverContext
上呼叫 stop()
:
// Player has stopped. CastReceiverContext.getInstance().stop()
// Player has stopped. CastReceiverContext.getInstance().stop();
此外,如果您的應用程式支援在背景播放,請在 CastReceiverContext
在背景停止播放時呼叫 stop()
。
強烈建議您使用 androidx.lifecycle
程式庫中的 LifecycleObserver,以便管理呼叫 CastReceiverContext.start()
和 CastReceiverContext.stop()
,特別是在原生應用程式有多個活動的情況下。這樣一來,當您從不同活動呼叫 start()
和 stop()
時,就不會發生競爭狀態。
// Create a LifecycleObserver class. class MyLifecycleObserver : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { // App prepares to enter foreground. CastReceiverContext.getInstance().start() } override fun onStop(owner: LifecycleOwner) { // App has moved to the background or has terminated. CastReceiverContext.getInstance().stop() } } // Add the observer when your application is being created. class MyApplication : Application() { fun onCreate() { super.onCreate() // Initialize CastReceiverContext. CastReceiverContext.initInstance(this /* android.content.Context */) // Register LifecycleObserver ProcessLifecycleOwner.get().lifecycle.addObserver( MyLifecycleObserver()) } }
// Create a LifecycleObserver class. public class MyLifecycleObserver implements DefaultLifecycleObserver { @Override public void onStart(LifecycleOwner owner) { // App prepares to enter foreground. CastReceiverContext.getInstance().start(); } @Override public void onStop(LifecycleOwner owner) { // App has moved to the background or has terminated. CastReceiverContext.getInstance().stop(); } } // Add the observer when your application is being created. public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // Initialize CastReceiverContext. CastReceiverContext.initInstance(this /* android.content.Context */); // Register LifecycleObserver ProcessLifecycleOwner.get().getLifecycle().addObserver( new MyLifecycleObserver()); } }
// In AndroidManifest.xml set MyApplication as the application class
<application
...
android:name=".MyApplication">
將 MediaSession 連結至 MediaManager
建立 MediaSession
時,您也需要將目前的 MediaSession
權杖提供給 CastReceiverContext
,讓它知道要將指令傳送至何處,並擷取媒體播放狀態:
val mediaManager: MediaManager = receiverContext.getMediaManager() mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken())
MediaManager mediaManager = receiverContext.getMediaManager(); mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());
當您因播放活動量不足而釋出 MediaSession
時,應在 MediaManager
上設定空值權杖:
myPlayer.stop() mediaSession.release() mediaManager.setSessionCompatToken(null)
myPlayer.stop(); mediaSession.release(); mediaManager.setSessionCompatToken(null);
如果應用程式支援在背景播放媒體,請在應用程式進入背景且不再播放媒體時,呼叫 CastReceiverContext.stop()
,而非在應用程式進入背景時呼叫。例如:
class MyLifecycleObserver : DefaultLifecycleObserver { ... // App has moved to the background. override fun onPause(owner: LifecycleOwner) { mIsBackground = true myStopCastReceiverContextIfNeeded() } } // Stop playback on the player. private fun myStopPlayback() { myPlayer.stop() myStopCastReceiverContextIfNeeded() } // Stop the CastReceiverContext when both the player has // stopped and the app has moved to the background. private fun myStopCastReceiverContextIfNeeded() { if (mIsBackground && myPlayer.isStopped()) { CastReceiverContext.getInstance().stop() } }
public class MyLifecycleObserver implements DefaultLifecycleObserver { ... // App has moved to the background. @Override public void onPause(LifecycleOwner owner) { mIsBackground = true; myStopCastReceiverContextIfNeeded(); } } // Stop playback on the player. private void myStopPlayback() { myPlayer.stop(); myStopCastReceiverContextIfNeeded(); } // Stop the CastReceiverContext when both the player has // stopped and the app has moved to the background. private void myStopCastReceiverContextIfNeeded() { if (mIsBackground && myPlayer.isStopped()) { CastReceiverContext.getInstance().stop(); } }
搭配 Cast Connect 使用 Exoplayer
如果您使用 Exoplayer
,可以使用 MediaSessionConnector
自動維護工作階段和所有相關資訊 (包括播放狀態),而非手動追蹤變更。
MediaSessionConnector.MediaButtonEventHandler
可用於呼叫 setMediaButtonEventHandler(MediaButtonEventHandler)
來處理 MediaButton 事件,否則這些事件會在預設情況下由 MediaSessionCompat.Callback
處理。
如要在應用程式中整合 MediaSessionConnector
,請將下列內容新增至播放器活動類別或管理媒體工作階段的任何位置:
class PlayerActivity : Activity() { private var mMediaSession: MediaSessionCompat? = null private var mMediaSessionConnector: MediaSessionConnector? = null private var mMediaManager: MediaManager? = null override fun onCreate(savedInstanceState: Bundle?) { ... mMediaSession = MediaSessionCompat(this, LOG_TAG) mMediaSessionConnector = MediaSessionConnector(mMediaSession!!) ... } override fun onStart() { ... mMediaManager = receiverContext.getMediaManager() mMediaManager!!.setSessionCompatToken(currentMediaSession.getSessionToken()) mMediaSessionConnector!!.setPlayer(mExoPlayer) mMediaSessionConnector!!.setMediaMetadataProvider(mMediaMetadataProvider) mMediaSession!!.isActive = true ... } override fun onStop() { ... mMediaSessionConnector!!.setPlayer(null) mMediaSession!!.release() mMediaManager!!.setSessionCompatToken(null) ... } }
public class PlayerActivity extends Activity { private MediaSessionCompat mMediaSession; private MediaSessionConnector mMediaSessionConnector; private MediaManager mMediaManager; @Override protected void onCreate(Bundle savedInstanceState) { ... mMediaSession = new MediaSessionCompat(this, LOG_TAG); mMediaSessionConnector = new MediaSessionConnector(mMediaSession); ... } @Override protected void onStart() { ... mMediaManager = receiverContext.getMediaManager(); mMediaManager.setSessionCompatToken(currentMediaSession.getSessionToken()); mMediaSessionConnector.setPlayer(mExoPlayer); mMediaSessionConnector.setMediaMetadataProvider(mMediaMetadataProvider); mMediaSession.setActive(true); ... } @Override protected void onStop() { ... mMediaSessionConnector.setPlayer(null); mMediaSession.release(); mMediaManager.setSessionCompatToken(null); ... } }
寄件端應用程式設定
啟用 Cast Connect 支援
更新傳送端應用程式以支援 Cast Connect 後,您可以將 LaunchOptions
上的 androidReceiverCompatible
旗標設為 true,宣告應用程式已就緒。
需要 play-services-cast-framework
19.0.0
以上版本。
androidReceiverCompatible
標記會在 LaunchOptions
(屬於 CastOptions
的一部分) 中設定:
class CastOptionsProvider : OptionsProvider { override fun getCastOptions(context: Context?): CastOptions { val launchOptions: LaunchOptions = Builder() .setAndroidReceiverCompatible(true) .build() return CastOptions.Builder() .setLaunchOptions(launchOptions) ... .build() } }
public class CastOptionsProvider implements OptionsProvider { @Override public CastOptions getCastOptions(Context context) { LaunchOptions launchOptions = new LaunchOptions.Builder() .setAndroidReceiverCompatible(true) .build(); return new CastOptions.Builder() .setLaunchOptions(launchOptions) ... .build(); } }
需要 google-cast-sdk
v4.4.8
以上版本。
androidReceiverCompatible
標記會在 GCKLaunchOptions
中設定 (GCKCastOptions
的一部分):
let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID)) ... let launchOptions = GCKLaunchOptions() launchOptions.androidReceiverCompatible = true options.launchOptions = launchOptions GCKCastContext.setSharedInstanceWith(options)
需要 Chromium 瀏覽器 M87
以上版本。
const context = cast.framework.CastContext.getInstance(); const castOptions = new cast.framework.CastOptions(); castOptions.receiverApplicationId = kReceiverAppID; castOptions.androidReceiverCompatible = true; context.setOptions(castOptions);
設定投放開發人員控制台
設定 Android TV 應用程式
在 Cast 開發人員控制台中新增 Android TV 應用程式的套件名稱,將其與 Cast 應用程式 ID 建立關聯。
註冊開發人員裝置
在 Cast 開發人員控制台中,註冊您要用於開發的 Android TV 裝置序號。
為確保安全性,未註冊的 Cast Connect 僅適用於從 Google Play 商店安裝的應用程式。
如要進一步瞭解如何註冊 Cast 或 Android TV 裝置,以便進行 Cast 開發作業,請參閱註冊頁面。
載入媒體
如果您已在 Android TV 應用程式中實作深層連結支援功能,則應在 Android TV 資訊清單中設定類似的定義:
<activity android:name="com.example.activity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https"/>
<data android:host="www.example.com"/>
<data android:pathPattern=".*"/>
</intent-filter>
</activity>
依寄件者實體載入
在傳送端,您可以在載入要求的媒體資訊中設定 entity
,藉此傳遞深層連結:
val mediaToLoad = MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") ... .build() val loadRequest = MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build() remoteMediaClient.load(loadRequest)
MediaInfo mediaToLoad = new MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") ... .build(); MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build(); remoteMediaClient.load(loadRequest);
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id") ... mediaInformation = mediaInfoBuilder.build() let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder() mediaLoadRequestDataBuilder.mediaInformation = mediaInformation mediaLoadRequestDataBuilder.credentials = "user-credentials" ... let mediaLoadRequestData = mediaLoadRequestDataBuilder.build() remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
需要 Chromium 瀏覽器 M87
以上版本。
let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4'); mediaInfo.entity = 'https://example.com/watch/some-id'; ... let request = new chrome.cast.media.LoadRequest(mediaInfo); request.credentials = 'user-credentials'; ... cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
載入指令會透過意圖傳送,其中包含您的深層連結和您在開發人員控制台中定義的套件名稱。
在傳送端設定 ATV 憑證
Web Receiver 應用程式和 Android TV 應用程式可能支援不同的深層連結和 credentials
(例如,如果您在兩個平台上以不同方式處理驗證)。為解決這個問題,您可以為 Android TV 提供替代 entity
和 credentials
:
val mediaToLoad = MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") .setAtvEntity("myscheme://example.com/atv/some-id") ... .build() val loadRequest = MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") .setAtvCredentials("atv-user-credentials") ... .build() remoteMediaClient.load(loadRequest)
MediaInfo mediaToLoad = new MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") .setAtvEntity("myscheme://example.com/atv/some-id") ... .build(); MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") .setAtvCredentials("atv-user-credentials") ... .build(); remoteMediaClient.load(loadRequest);
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id") mediaInfoBuilder.atvEntity = "myscheme://example.com/atv/some-id" ... mediaInformation = mediaInfoBuilder.build() let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder() mediaLoadRequestDataBuilder.mediaInformation = mediaInformation mediaLoadRequestDataBuilder.credentials = "user-credentials" mediaLoadRequestDataBuilder.atvCredentials = "atv-user-credentials" ... let mediaLoadRequestData = mediaLoadRequestDataBuilder.build() remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
需要 Chromium 瀏覽器 M87
以上版本。
let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4'); mediaInfo.entity = 'https://example.com/watch/some-id'; mediaInfo.atvEntity = 'myscheme://example.com/atv/some-id'; ... let request = new chrome.cast.media.LoadRequest(mediaInfo); request.credentials = 'user-credentials'; request.atvCredentials = 'atv-user-credentials'; ... cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
如果啟動 Web Receiver 應用程式,該應用程式會在載入要求中使用 entity
和 credentials
。不過,如果您啟動 Android TV 應用程式,SDK 會使用 atvEntity
和 atvCredentials
(如果有指定) 覆寫 entity
和 credentials
。
依 Content ID 或 MediaQueueData 載入
如果您未使用 entity
或 atvEntity
,但在媒體資訊中使用內容 ID 或內容網址,或是使用更詳細的媒體載入要求資料,則必須在 Android TV 應用程式中新增下列預先定義的意圖篩選器:
<activity android:name="com.example.activity">
<intent-filter>
<action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
在傳送端,您可以透過依實體載入的方式,建立含有內容資訊的載入要求,並呼叫 load()
。
val mediaToLoad = MediaInfo.Builder("some-id").build() val loadRequest = MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build() remoteMediaClient.load(loadRequest)
MediaInfo mediaToLoad = new MediaInfo.Builder("some-id").build(); MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build(); remoteMediaClient.load(loadRequest);
let mediaInfoBuilder = GCKMediaInformationBuilder(contentId: "some-id") ... mediaInformation = mediaInfoBuilder.build() let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder() mediaLoadRequestDataBuilder.mediaInformation = mediaInformation mediaLoadRequestDataBuilder.credentials = "user-credentials" ... let mediaLoadRequestData = mediaLoadRequestDataBuilder.build() remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
需要 Chromium 瀏覽器 M87
以上版本。
let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4'); ... let request = new chrome.cast.media.LoadRequest(mediaInfo); ... cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
處理載入要求
如要在活動中處理這些載入要求,您必須在活動生命週期回呼中處理意圖:
class MyActivity : Activity() { override fun onStart() { super.onStart() val mediaManager = CastReceiverContext.getInstance().getMediaManager() // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(intent)) { // If the SDK recognizes the intent, you should early return. return } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } // For some cases, a new load intent triggers onNewIntent() instead of // onStart(). override fun onNewIntent(intent: Intent) { val mediaManager = CastReceiverContext.getInstance().getMediaManager() // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(intent)) { // If the SDK recognizes the intent, you should early return. return } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } }
public class MyActivity extends Activity { @Override protected void onStart() { super.onStart(); MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(getIntent())) { // If the SDK recognizes the intent, you should early return. return; } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } // For some cases, a new load intent triggers onNewIntent() instead of // onStart(). @Override protected void onNewIntent(Intent intent) { MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(intent)) { // If the SDK recognizes the intent, you should early return. return; } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } }
如果 MediaManager
偵測到意圖是載入意圖,就會從意圖中擷取 MediaLoadRequestData
物件,並叫用 MediaLoadCommandCallback.onLoad()
。您需要覆寫這個方法來處理載入要求。回呼必須在呼叫 MediaManager.onNewIntent()
之前註冊 (建議在 Activity 或 Application onCreate()
方法中註冊)。
class MyActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.setMediaLoadCommandCallback(MyMediaLoadCommandCallback()) } } class MyMediaLoadCommandCallback : MediaLoadCommandCallback() { override fun onLoad( senderId: String?, loadRequestData: MediaLoadRequestData ): Task{ return Tasks.call { // Resolve the entity into your data structure and load media. val mediaInfo = loadRequestData.getMediaInfo() if (!checkMediaInfoSupported(mediaInfo)) { // Throw MediaException to indicate load failure. throw MediaException( MediaError.Builder() .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED) .setReason(MediaError.ERROR_REASON_INVALID_REQUEST) .build() ) } myFillMediaInfo(MediaInfoWriter(mediaInfo)) myPlayerLoad(mediaInfo.getContentUrl()) // Update media metadata and state (this clears all previous status // overrides). castReceiverContext.getMediaManager() .setDataFromLoad(loadRequestData) ... castReceiverContext.getMediaManager().broadcastMediaStatus() // Return the resolved MediaLoadRequestData to indicate load success. return loadRequestData } } private fun myPlayerLoad(contentURL: String) { myPlayer.load(contentURL) // Update the MediaSession state. val playbackState: PlaybackStateCompat = Builder() .setState( player.getState(), player.getPosition(), System.currentTimeMillis() ) ... .build() mediaSession.setPlaybackState(playbackState) }
public class MyActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback()); } } public class MyMediaLoadCommandCallback extends MediaLoadCommandCallback { @Override public TaskonLoad(String senderId, MediaLoadRequestData loadRequestData) { return Tasks.call(() -> { // Resolve the entity into your data structure and load media. MediaInfo mediaInfo = loadRequestData.getMediaInfo(); if (!checkMediaInfoSupported(mediaInfo)) { // Throw MediaException to indicate load failure. throw new MediaException( new MediaError.Builder() .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED) .setReason(MediaError.ERROR_REASON_INVALID_REQUEST) .build()); } myFillMediaInfo(new MediaInfoWriter(mediaInfo)); myPlayerLoad(mediaInfo.getContentUrl()); // Update media metadata and state (this clears all previous status // overrides). castReceiverContext.getMediaManager() .setDataFromLoad(loadRequestData); ... castReceiverContext.getMediaManager().broadcastMediaStatus(); // Return the resolved MediaLoadRequestData to indicate load success. return loadRequestData; }); } private void myPlayerLoad(String contentURL) { myPlayer.load(contentURL); // Update the MediaSession state. PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setState( player.getState(), player.getPosition(), System.currentTimeMillis()) ... .build(); mediaSession.setPlaybackState(playbackState); }
如要處理載入意圖,您可以將意圖剖析為我們定義的資料結構 (MediaLoadRequestData
用於載入要求)。
支援媒體指令
支援基本播放控制功能
基本整合指令包括與媒體工作階段相容的指令。這些指令會透過媒體工作階段回呼通知。您需要註冊媒體工作階段的回呼,才能支援這項功能 (您可能已經這麼做了)。
private class MyMediaSessionCallback : MediaSessionCompat.Callback() { override fun onPause() { // Pause the player and update the play state. myPlayer.pause() } override fun onPlay() { // Resume the player and update the play state. myPlayer.play() } override fun onSeekTo(pos: Long) { // Seek and update the play state. myPlayer.seekTo(pos) } ... } mediaSession.setCallback(MyMediaSessionCallback())
private class MyMediaSessionCallback extends MediaSessionCompat.Callback { @Override public void onPause() { // Pause the player and update the play state. myPlayer.pause(); } @Override public void onPlay() { // Resume the player and update the play state. myPlayer.play(); } @Override public void onSeekTo(long pos) { // Seek and update the play state. myPlayer.seekTo(pos); } ... } mediaSession.setCallback(new MyMediaSessionCallback());
支援 Cast 控制指令
有些投放指令無法在 MediaSession
中使用,例如 skipAd()
或 setActiveMediaTracks()
。此外,由於 Cast 佇列與 MediaSession
佇列不完全相容,因此需要在此實作部分佇列指令。
class MyMediaCommandCallback : MediaCommandCallback() { override fun onSkipAd(requestData: RequestData?): Task<Void?> { // Skip your ad ... return Tasks.forResult(null) } } val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
public class MyMediaCommandCallback extends MediaCommandCallback { @Override public TaskonSkipAd(RequestData requestData) { // Skip your ad ... return Tasks.forResult(null); } } MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());
指定支援的媒體指令
與 Cast 接收器一樣,Android TV 應用程式應指定支援哪些指令,以便發送端啟用或停用特定 UI 控制項。如要使用 MediaSession
中的指令,請在 PlaybackStateCompat
中指定指令。請在 MediaStatusModifier
中指定其他指令。
// Set media session supported commands val playbackState: PlaybackStateCompat = PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE) .setState(PlaybackStateCompat.STATE_PLAYING) .build() mediaSession.setPlaybackState(playbackState) // Set additional commands in MediaStatusModifier val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.getMediaStatusModifier() .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT)
// Set media session supported commands PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE) .setState(PlaybackStateCompat.STATE_PLAYING) .build(); mediaSession.setPlaybackState(playbackState); // Set additional commands in MediaStatusModifier MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.getMediaStatusModifier() .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT);
隱藏不支援的按鈕
如果 Android TV 應用程式只支援基本媒體控制功能,但 Web Receiver 應用程式支援更進階的控制功能,您應確保在投放至 Android TV 應用程式時,傳送端應用程式能正常運作。舉例來說,如果 Android TV 應用程式不支援變更播放速率,但 Web Receiver 應用程式支援,您應在各平台上正確設定支援的動作,並確保傳送端應用程式能正確轉譯 UI。
修改 MediaStatus
如要支援曲目、廣告、直播和排隊等進階功能,您的 Android TV 應用程式必須提供無法透過 MediaSession
確認的額外資訊。
我們提供 MediaStatusModifier
類別,協助您達成這項目標。MediaStatusModifier
一律會在您在 CastReceiverContext
中設定的 MediaSession
上運作。
如要建立並發布 MediaStatus
:
val mediaManager: MediaManager = castReceiverContext.getMediaManager() val statusModifier: MediaStatusModifier = mediaManager.getMediaStatusModifier() statusModifier .setLiveSeekableRange(seekableRange) .setAdBreakStatus(adBreakStatus) .setCustomData(customData) mediaManager.broadcastMediaStatus()
MediaManager mediaManager = castReceiverContext.getMediaManager(); MediaStatusModifier statusModifier = mediaManager.getMediaStatusModifier(); statusModifier .setLiveSeekableRange(seekableRange) .setAdBreakStatus(adBreakStatus) .setCustomData(customData); mediaManager.broadcastMediaStatus();
我們的用戶端程式庫會從 MediaSession
取得基礎 MediaStatus
,您的 Android TV 應用程式可以透過 MediaStatus
修飾符指定其他狀態,並覆寫狀態。
部分狀態和中繼資料可同時在 MediaSession
和 MediaStatusModifier
中設定。我們強烈建議您只在 MediaSession
中設定這些值。您仍可使用修飾符覆寫 MediaSession
中的狀態,但不建議這麼做,因為修飾符中的狀態一律會優先於 MediaSession
提供的值。
在傳送前攔截 MediaStatus
與 Web Receiver SDK 相同,如果您想在傳送前進行一些收尾動作,可以指定 MediaStatusInterceptor
來處理要傳送的 MediaStatus
。我們會傳入 MediaStatusWriter
,在 MediaStatus
傳送前進行操作。
mediaManager.setMediaStatusInterceptor(object : MediaStatusInterceptor { override fun intercept(mediaStatusWriter: MediaStatusWriter) { // Perform customization. mediaStatusWriter.setCustomData(JSONObject("{data: \"my Hello\"}")) } })
mediaManager.setMediaStatusInterceptor(new MediaStatusInterceptor() { @Override public void intercept(MediaStatusWriter mediaStatusWriter) { // Perform customization. mediaStatusWriter.setCustomData(new JSONObject("{data: \"my Hello\"}")); } });
處理使用者憑證
Android TV 應用程式可能只允許特定使用者啟動或加入應用程式工作階段。舉例來說,您可以只允許符合下列條件的傳送者啟動或加入會議:
- 傳送端應用程式已登入與 ATV 應用程式相同的帳戶和個人資料。
- 傳送端應用程式已登入相同帳戶,但與 ATV 應用程式使用不同的設定檔。
如果您的應用程式可以處理多位或匿名使用者,您可以允許其他使用者加入 ATV 工作階段。如果使用者提供憑證,您的 ATV 應用程式就需要處理這些憑證,才能正確追蹤使用者的進度和其他使用者資料。
當傳送端應用程式啟動或加入 Android TV 應用程式時,傳送端應用程式應提供代表加入工作階段的憑證。
在傳送端啟動並加入 Android TV 應用程式之前,您可以指定啟動檢查器,查看傳送端憑證是否允許。如果沒有,Cast Connect SDK 會改為啟動 Web Receiver。
寄件者應用程式啟動憑證資料
在傳送端,您可以指定 CredentialsData
來代表誰加入了工作階段。
credentials
是可由使用者定義的字串,只要 ATV 應用程式能瞭解即可。credentialsType
可定義 CredentialsData
來自哪個平台,或可為自訂值。根據預設,系統會將其設為傳送平台。
CredentialsData
僅會在啟動或加入時傳送至 Android TV 應用程式。如果您在連線時再次設定,系統不會將設定傳遞至 Android TV 應用程式。如果傳送端在連線時切換設定檔,您可以繼續使用工作階段,或是在認為新設定檔與工作階段不相容時呼叫 SessionManager.endCurrentCastSession(boolean stopCasting)
。
您可以使用 CastReceiverContext
上的 getSenders
來取得 SenderInfo
,然後使用 getCastLaunchRequest()
取得 CastLaunchRequest
,再使用 getCredentialsData()
來擷取每個寄件者的 CredentialsData
。
需要 play-services-cast-framework
19.0.0
以上版本。
CastContext.getSharedInstance().setLaunchCredentialsData( CredentialsData.Builder() .setCredentials("{\"userId\": \"abc\"}") .build() )
CastContext.getSharedInstance().setLaunchCredentialsData( new CredentialsData.Builder() .setCredentials("{\"userId\": \"abc\"}") .build());
需要 google-cast-sdk
v4.8.3
以上版本。
在設定選項後隨時可呼叫:GCKCastContext.setSharedInstanceWith(options)
。
GCKCastContext.sharedInstance().setLaunch( GCKCredentialsData(credentials: "{\"userId\": \"abc\"}")
需要 Chromium 瀏覽器 M87
以上版本。
在設定選項後隨時可呼叫:cast.framework.CastContext.getInstance().setOptions(options);
。
let credentialsData = new chrome.cast.CredentialsData("{\"userId\": \"abc\"}"); cast.framework.CastContext.getInstance().setLaunchCredentialsData(credentialsData);
實作 ATV 啟動要求檢查器
當傳送端嘗試啟動或加入時,系統會將 CredentialsData
傳遞至 Android TV 應用程式。您可以實作 LaunchRequestChecker
。允許或拒絕這項要求。
如果要求遭到拒絕,系統會載入 Web Receiver,而非原生啟動 ATV 應用程式。如果 ATV 無法處理使用者要求啟動或加入的情況,您應拒絕要求。例如,登入 ATV 應用程式的使用者與要求登入的使用者不同,而您的應用程式無法處理憑證切換作業,或是目前沒有使用者登入 ATV 應用程式。
如果要求獲得核准,系統就會啟動 ATV 應用程式。您可以視應用程式是否支援在使用者未登入 ATV 應用程式或使用者不相符時傳送載入要求,自訂這項行為。您可以在 LaunchRequestChecker
中完全自訂這項行為。
建立實作 CastReceiverOptions.LaunchRequestChecker
介面的類別:
class MyLaunchRequestChecker : LaunchRequestChecker { override fun checkLaunchRequestSupported(launchRequest: CastLaunchRequest): Task{ return Tasks.call { myCheckLaunchRequest( launchRequest ) } } } private fun myCheckLaunchRequest(launchRequest: CastLaunchRequest): Boolean { val credentialsData = launchRequest.getCredentialsData() ?: return false // or true if you allow anonymous users to join. // The request comes from a mobile device, e.g. checking user match. return if (credentialsData.credentialsType == CredentialsData.CREDENTIALS_TYPE_ANDROID) { myCheckMobileCredentialsAllowed(credentialsData.getCredentials()) } else false // Unrecognized credentials type. }
public class MyLaunchRequestChecker implements CastReceiverOptions.LaunchRequestChecker { @Override public TaskcheckLaunchRequestSupported(CastLaunchRequest launchRequest) { return Tasks.call(() -> myCheckLaunchRequest(launchRequest)); } } private boolean myCheckLaunchRequest(CastLaunchRequest launchRequest) { CredentialsData credentialsData = launchRequest.getCredentialsData(); if (credentialsData == null) { return false; // or true if you allow anonymous users to join. } // The request comes from a mobile device, e.g. checking user match. if (credentialsData.getCredentialsType().equals(CredentialsData.CREDENTIALS_TYPE_ANDROID)) { return myCheckMobileCredentialsAllowed(credentialsData.getCredentials()); } // Unrecognized credentials type. return false; }
然後在 ReceiverOptionsProvider
中設定:
class MyReceiverOptionsProvider : ReceiverOptionsProvider { override fun getOptions(context: Context?): CastReceiverOptions { return CastReceiverOptions.Builder(context) ... .setLaunchRequestChecker(MyLaunchRequestChecker()) .build() } }
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider { @Override public CastReceiverOptions getOptions(Context context) { return new CastReceiverOptions.Builder(context) ... .setLaunchRequestChecker(new MyLaunchRequestChecker()) .build(); } }
在 LaunchRequestChecker
中解析 true
會啟動 ATV 應用程式,而 false
會啟動 Web Receiver 應用程式。
傳送及接收自訂訊息
您可以使用 Cast 通訊協定,在傳送者和接收器應用程式之間傳送自訂字串訊息。您必須先註冊命名空間 (管道),才能在初始化 CastReceiverContext
之前傳送訊息。
Android TV:指定自訂命名空間
您必須在設定期間,在 CastReceiverOptions
中指定支援的命名空間:
class MyReceiverOptionsProvider : ReceiverOptionsProvider { override fun getOptions(context: Context?): CastReceiverOptions { return CastReceiverOptions.Builder(context) .setCustomNamespaces( Arrays.asList("urn:x-cast:com.example.cast.mynamespace") ) .build() } }
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider { @Override public CastReceiverOptions getOptions(Context context) { return new CastReceiverOptions.Builder(context) .setCustomNamespaces( Arrays.asList("urn:x-cast:com.example.cast.mynamespace")) .build(); } }
Android TV:傳送訊息
// If senderId is null, then the message is broadcasted to all senders. CastReceiverContext.getInstance().sendMessage( "urn:x-cast:com.example.cast.mynamespace", senderId, customString)
// If senderId is null, then the message is broadcasted to all senders. CastReceiverContext.getInstance().sendMessage( "urn:x-cast:com.example.cast.mynamespace", senderId, customString);
Android TV:接收自訂命名空間訊息
class MyCustomMessageListener : MessageReceivedListener { override fun onMessageReceived( namespace: String, senderId: String?, message: String ) { ... } } CastReceiverContext.getInstance().setMessageReceivedListener( "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());
class MyCustomMessageListener implements CastReceiverContext.MessageReceivedListener { @Override public void onMessageReceived( String namespace, String senderId, String message) { ... } } CastReceiverContext.getInstance().setMessageReceivedListener( "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());