為 Android TV 接收器新增核心功能

本頁包含程式碼片段和自訂 Android TV 接收器應用程式的功能說明。

設定程式庫

如要為 Android TV 應用程式提供 Cast Connect API,請按照下列步驟操作:

Android
  1. 開啟應用程式模組目錄中的 build.gradle 檔案。
  2. 確認 google() 已包含在列出的 repositories 中。
      repositories {
        google()
      }
  3. 依據應用程式的目標裝置類型,將最新版程式庫加入依附元件:
    • Android 接收器應用程式:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast-tv:21.1.0'
          implementation 'com.google.android.gms:play-services-cast:21.5.0'
        }
    • Android 寄件者應用程式:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast:21.1.0'
          implementation 'com.google.android.gms:play-services-cast-framework:21.5.0'
        }
    每次服務更新時,請務必更新這組版本號碼。
  4. 儲存變更,然後按一下工具列中的 Sync Project with Gradle Files
iOS
  1. 請確定您的 Podfile 指定 google-cast-sdk 4.8.1 以上版本
  2. 指定 iOS 14 以上版本。詳情請參閱「版本資訊」。
      platform: ios, '14'
    
      def target_pods
         pod 'google-cast-sdk', '~>4.8.1'
      end
網頁
  1. 需要 Chromium 瀏覽器 M87 以上版本。
  2. 將 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 Content ID。
METADATA_KEY_ARTIST 藝人。
METADATA_KEY_ALBUM 專輯。

PlaybackStateCompat

必要方法 說明
setActions() 設定支援的媒體指令。
setState() 設定播放狀態和目前位置。

MediaSessionCompat

必要方法 說明
setRepeatMode() 設定重播模式。
setShuffleMode() 設定隨機播放模式。
setMetadata() 設定媒體中繼資料。
setPlaybackState() 設定播放狀態。
Kotlin
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)
}
Java
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() 停止目前的媒體
Kotlin
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() );
Java
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());

設定 Cast 支援

當傳送端應用程式送出啟動要求時,系統會建立包含應用程式命名空間的意圖。當電視應用程式啟動時,您的應用程式會負責處理該物件,並建立 CastReceiverContext 物件的執行個體。需要 CastReceiverContext 物件,才能在電視應用程式執行期間與 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

Kotlin
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
          .setStatusText("My App")
          .build()
    }
}
Java
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

Kotlin
override fun onCreate() {
  CastReceiverContext.initInstance(this)

  ...
}
Java
@Override
public void onCreate() {
  CastReceiverContext.initInstance(this);

  ...
}

在應用程式移至前景時啟動 CastReceiverContext

Kotlin
CastReceiverContext.getInstance().start()
Java
CastReceiverContext.getInstance().start();

當應用程式進入背景,或者不支援背景播放的應用程式時,在 CastReceiverContext 上呼叫 stop()

Kotlin
// Player has stopped.
CastReceiverContext.getInstance().stop()
Java
// Player has stopped.
CastReceiverContext.getInstance().stop();

此外,如果應用程式支援在背景播放,請在背景停止播放時,對 CastReceiverContext 呼叫 stop()

強烈建議您使用 androidx.lifecycle 程式庫中的 LifecycleObserver 來管理呼叫 CastReceiverContext.start()CastReceiverContext.stop(),尤其是當原生應用程式有多個活動時。從不同活動呼叫 start()stop() 時,這種做法可避免競爭狀況。

Kotlin
// 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())
  }
}
Java
// 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 播放時,才知道要傳送狀態和擷取媒體的位置:

Kotlin
val mediaManager: MediaManager = receiverContext.getMediaManager()
mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken())
Java
MediaManager mediaManager = receiverContext.getMediaManager();
mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());

當你因為播放無效而釋出 MediaSession 時,應在 MediaManager 上設定空值權杖:

Kotlin
myPlayer.stop()
mediaSession.release()
mediaManager.setSessionCompatToken(null)
Java
myPlayer.stop();
mediaSession.release();
mediaManager.setSessionCompatToken(null);

如果應用程式在背景播放媒體時支援播放媒體,則在應用程式傳送到背景時,不要呼叫CastReceiverContext.stop(),建議只在應用程式於背景執行且不再播放媒體時呼叫。例如:

Kotlin
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()
  }
}
Java
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 可用於處理 MediaButton 事件,方法是呼叫 setMediaButtonEventHandler(MediaButtonEventHandler),預設會由 MediaSessionCompat.Callback 處理。

如要在應用程式中整合 MediaSessionConnector,請將以下內容新增至玩家活動類別,或新增到您管理媒體工作階段的任何位置:

Kotlin
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)
    ...
  }
}
Java
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 支援功能更新傳送者應用程式後,您可以將 LaunchOptionsandroidReceiverCompatible 標記設為 true,宣告應用程式已準備就緒。

Android

需要 19.0.0 play-services-cast-framework 以上版本。

系統會在 LaunchOptions 中設定 androidReceiverCompatible 標記 (屬於 CastOptions 的一部分):

Kotlin
class CastOptionsProvider : OptionsProvider {
  override fun getCastOptions(context: Context?): CastOptions {
    val launchOptions: LaunchOptions = Builder()
          .setAndroidReceiverCompatible(true)
          .build()
    return CastOptions.Builder()
          .setLaunchOptions(launchOptions)
          ...
          .build()
    }
}
Java
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();
  }
}
iOS

需要 google-cast-sdk v4.4.8 以上版本。

系統會在 GCKLaunchOptions 中設定 androidReceiverCompatible 標記 (屬於 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);

設定 Cast 開發人員控制台

設定 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 來傳遞深層連結:

Kotlin
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)
Android 版
Java
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);
iOS
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);

系統會透過含有深層連結的意圖,以及您在 Play 管理中心定義的套件名稱,傳送載入指令。

為傳送者設定 ATV 憑證

您的 Web Receiver 應用程式和 Android TV 應用程式可能支援不同的深層連結和 credentials (舉例來說,假設您在兩個平台上處理驗證的方式不同)。如要解決這個問題,您可以提供 Android TV 的 entitycredentials 替代項目:

Android
Kotlin
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)
Java
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);
iOS
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 應用程式啟動,在載入要求中使用 entitycredentials。不過,如果您的 Android TV 應用程式已啟動,SDK 會使用您的 atvEntityatvCredentials (如有指定) 覆寫 entitycredentials

依 Content ID 或 MediaQueueData 載入

如果您使用的不是 entityatvEntity,且在媒體資訊中使用 Content 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 by entity,您可以建立內容資訊來建立載入要求,並呼叫 load()

Android
Kotlin
val mediaToLoad = MediaInfo.Builder("some-id").build()
val loadRequest = MediaLoadRequestData.Builder()
    .setMediaInfo(mediaToLoad)
    .setCredentials("user-credentials")
    ...
    .build()
remoteMediaClient.load(loadRequest)
Java
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id").build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
iOS
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);

處理載入要求

在活動中,如要處理這些載入要求,您需要在活動生命週期回呼中處理意圖:

Kotlin
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.
    ...
  }
}
Java
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() 之前必須先註冊回呼 (建議在活動或應用程式 onCreate() 方法上)。

Kotlin
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)
  }
Java
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 Task onLoad(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)。

支援媒體指令

支援基本播放控制項

基本整合指令包含與媒體工作階段相容的指令。這些指令會透過媒體工作階段回呼收到通知。您需要註冊媒體工作階段的回呼,才能支援這項功能 (您可能已經完成這項操作)。

Kotlin
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())
Java
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());

支援投放控制指令

有些 MediaSession 不提供投放指令,例如 skipAd()setActiveMediaTracks()。此外,您需要在這裡實作部分佇列指令,因為投放佇列與 MediaSession 佇列並不完全相容。

Kotlin
class MyMediaCommandCallback : MediaCommandCallback() {
    override fun onSkipAd(requestData: RequestData?): Task {
        // Skip your ad
        ...
        return Tasks.forResult(null)
    }
}

val mediaManager = CastReceiverContext.getInstance().getMediaManager()
mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
Java
public class MyMediaCommandCallback extends MediaCommandCallback {
  @Override
  public Task onSkipAd(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 中指定其他指令。

Kotlin
// 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)
Java
// 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 應用程式僅支援基本媒體控制項,但網路接收器應用程式支援更進階的控制項,則在將內容投放到 Android TV 應用程式時,您必須確認傳送端應用程式可以正常運作。舉例來說,如果您的 Android TV 應用程式無法在網路接收端應用程式支援的情況下變更播放率,您應該在每個平台上正確設定支援的動作,並確保傳送端應用程式能正確轉譯 UI。

修改 MediaStatus

如要支援測試群組、廣告、直播和待播清單等進階功能,Android TV 應用程式需要透過 MediaSession 提供無法確認的額外資訊。

為此,我們提供了 MediaStatusModifier 類別。MediaStatusModifier 一律會在您在 CastReceiverContext 中設定的 MediaSession 運作。

如何建立及播送 MediaStatus

Kotlin
val mediaManager: MediaManager = castReceiverContext.getMediaManager()
val statusModifier: MediaStatusModifier = mediaManager.getMediaStatusModifier()

statusModifier
    .setLiveSeekableRange(seekableRange)
    .setAdBreakStatus(adBreakStatus)
    .setCustomData(customData)

mediaManager.broadcastMediaStatus()
Java
MediaManager mediaManager = castReceiverContext.getMediaManager();
MediaStatusModifier statusModifier = mediaManager.getMediaStatusModifier();

statusModifier
    .setLiveSeekableRange(seekableRange)
    .setAdBreakStatus(adBreakStatus)
    .setCustomData(customData);

mediaManager.broadcastMediaStatus();

我們的用戶端程式庫會從 MediaSession 取得基礎 MediaStatus,您的 Android TV 應用程式可以透過 MediaStatus 修飾符指定其他狀態並覆寫狀態。

某些狀態和中繼資料可在 MediaSessionMediaStatusModifier 中設定。強烈建議您僅設定 MediaSession 中的這類參數。您仍然可以使用修飾符覆寫 MediaSession 中的狀態,建議您不要採取這種做法,因為修飾符中的狀態優先順序一律高於 MediaSession 提供的值。

在送出前攔截 MediaStatus

與 Web Receiver SDK 相同,如果您想在送出前完成最後的調整,可以指定 MediaStatusInterceptor 來處理要傳送的 MediaStatus。我們會傳入 MediaStatusWriter 來在傳送 MediaStatus 前加以控管。

Kotlin
mediaManager.setMediaStatusInterceptor(object : MediaStatusInterceptor {
    override fun intercept(mediaStatusWriter: MediaStatusWriter) {
      // Perform customization.
        mediaStatusWriter.setCustomData(JSONObject("{data: \"my Hello\"}"))
    }
})
Java
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 會改回啟動網路接收器。

寄件者應用程式啟動憑證資料

在傳送者端,您可以指定 CredentialsData 來代表加入工作階段的使用者。

credentials 是可以由使用者定義的字串,前提是您的 ATV 應用程式能夠理解。credentialsType 會定義 CredentialsData 的來源平台,也可以可以是自訂值。根據預設,它會設為送出它的來源平台。

CredentialsData 只會在啟動或加入期間傳遞至您的 Android TV 應用程式。如果在連線時再次設定設定檔,該設定檔並不會傳遞至 Android TV 應用程式。如果傳送者在連線時切換設定檔,您可以留在工作階段中;如果您認為新的設定檔與工作階段不相容,請呼叫 SessionManager.endCurrentCastSession(boolean stopCasting)

您可以使用 CastReceiverContext 上的 getSenders 擷取每個傳送端的 CredentialsData,取得 SenderInfogetCastLaunchRequest(),然後取得 CastLaunchRequest,然後getCredentialsData()

Android

需要 19.0.0 play-services-cast-framework 以上版本。

Kotlin
CastContext.getSharedInstance().setLaunchCredentialsData(
    CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
)
Java
CastContext.getSharedInstance().setLaunchCredentialsData(
    new CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build());
iOS

需要 google-cast-sdk v4.8.1 以上版本。

設定選項後,隨時可以呼叫: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 介面的類別:

Kotlin
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.
}
Java
public class MyLaunchRequestChecker
    implements CastReceiverOptions.LaunchRequestChecker {
  @Override
  public Task checkLaunchRequestSupported(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 中設定:

Kotlin
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
        ...
        .setLaunchRequestChecker(MyLaunchRequestChecker())
        .build()
  }
}
Java
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 中指定支援的命名空間:

Kotlin
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
        .setCustomNamespaces(
            Arrays.asList("urn:x-cast:com.example.cast.mynamespace")
        )
        .build()
  }
}
Java
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 - 傳送訊息

Kotlin
// If senderId is null, then the message is broadcasted to all senders.
CastReceiverContext.getInstance().sendMessage(
    "urn:x-cast:com.example.cast.mynamespace", senderId, customString)
Java
// 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 - 接收自訂命名空間訊息

Kotlin
class MyCustomMessageListener : MessageReceivedListener {
    override fun onMessageReceived(
        namespace: String, senderId: String?, message: String ) {
        ...
    }
}

CastReceiverContext.getInstance().setMessageReceivedListener(
    "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());
Java
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());