添加 Android TV 接收器的核心功能

本页包含可用于自定义 Android TV 接收器应用的功能的代码段和说明。

配置库

如需向 Android TV 应用提供 Cast Connect API,请执行以下操作:

Android
  1. 打开应用模块目录中的 build.gradle 文件。
  2. 验证所列 repositories 中是否包含 google()
      repositories {
        google()
      }
  3. 根据应用的目标设备类型,将最新版本的库添加到依赖项:
    • 对于 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'
        }
    请务必在每次更新服务时更新此版本号。
  4. 保存更改,然后点击工具栏中的 Sync Project with Gradle Files
iOS
  1. 确保您的 Podfilegoogle-cast-sdk 4.8.3 或更高版本为目标平台
  2. 以 iOS 14 或更高版本为目标平台。如需了解详情,请参阅版本说明
      platform: ios, '14'
    
      def target_pods
         pod 'google-cast-sdk', '~>4.8.3'
      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 库还会使用媒体会话,在收到发送器发送来的特定信息(例如暂停)时发出信号。

如需详细了解媒体会话以及如何初始化媒体会话,请参阅使用媒体会话指南

媒体会话生命周期

您的应用应在开始播放时创建媒体会话,并在无法再控制媒体会话时释放它。例如,如果您的应用是视频应用,则应在用户退出播放 activity(通过选择“返回”以浏览其他内容或将应用切换到后台)时释放会话。如果您的应用是音乐应用,则应在应用不再播放任何媒体时释放会话。

更新会话状态

媒体会话中的数据应与播放器的状态保持最新状态。例如,在暂停播放时,您应更新播放状态以及受支持的操作。下表列出了您负责及时更新的状态。

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() 设置播放状态。
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 支持

当发送方应用发出启动请求时,系统会创建一个包含应用命名空间的 intent。您的应用负责处理它,并在 TV 应用启动时创建 CastReceiverContext 对象的实例。在 TV 应用运行时,需要使用 CastReceiverContext 对象与 Cast 进行交互。借助此对象,您的 TV 应用可以接受来自任何已连接发送方的 Cast 媒体消息。

Android TV 设置

添加启动 intent 过滤器

向您要处理发送器应用中的启动 intent 的 activity 添加新的 intent 过滤器:

<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" />

ReceiverOptionsProvider 用于在初始化 CastReceiverContext 时提供 CastReceiverOptions

Cast 接收器上下文

在创建应用时初始化 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(),尤其是当您的原生应用包含多个 activity 时。这样做可避免从不同 activity 调用 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 时,您还需要向 CastReceiverContext 提供当前的 MediaSession 令牌,以便它知道要在何处发送命令以及检索媒体播放状态:

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

在因播放挂起而释放 MediaSession 时,您应在 MediaManager 上设置 null 令牌:

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();
  }
}

将 Exoplayer 与 Cast Connect 搭配使用

如果您使用 Exoplayer,则可以使用 MediaSessionConnector 自动维护会话和所有相关信息(包括播放状态),而不是手动跟踪更改。

MediaSessionConnector.MediaButtonEventHandler 可用于通过调用 setMediaButtonEventHandler(MediaButtonEventHandler) 来处理 MediaButton 事件,默认情况下,这些事件由 MediaSessionCompat.Callback 处理。

如需在应用中集成 MediaSessionConnector,请将以下代码添加到播放器 activity 类或您管理媒体会话的任何位置:

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 后,您可以通过将 LaunchOptions 上的 androidReceiverCompatible 标志设置为 true 来声明其准备就绪。

Android

需要 play-services-cast-framework 19.0.0 版或更高版本。

androidReceiverCompatible 标志在 LaunchOptions(属于 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 版或更高版本。

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);

Cast 开发者控制台设置

配置 Android TV 应用

Cast Developer Console 中添加 Android TV 应用的软件包名称,并将其与 Cast 应用 ID 相关联。

注册开发者设备

Cast Developer Console 中注册您要用于开发的 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);

通过 intent,使用您的深层链接和您在开发者控制台中定义的软件包名称发送加载命令。

在发件人处设置 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,而是在媒体信息中使用内容 ID 或内容网址,或者使用更详细的媒体加载请求数据,则需要在 Android TV 应用中添加以下预定义 intent 过滤器:

<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()

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);

处理加载请求

在 activity 中,如需处理这些加载请求,您需要在 activity 生命周期回调中处理 intent:

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 检测到 intent 是加载 intent,则会从此 intent 中提取 MediaLoadRequestData 对象,并调用 MediaLoadCommandCallback.onLoad()。您需要替换此方法以处理加载请求。回调必须在调用 MediaManager.onNewIntent() 之前注册(建议在 activity 或应用 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);
}

如需处理加载 intent,您可以将 intent 解析为我们定义的数据结构(对于加载请求,为 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());

支持 Cast 控件命令

有些 Cast 命令在 MediaSession 中不可用,例如 skipAd()setActiveMediaTracks()。此外,由于 Cast 队列与 MediaSession 队列不完全兼容,因此需要在此处实现一些队列命令。

Kotlin
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())
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 应用应指定支持哪些命令,以便发送器可以启用或停用某些界面控件。对于 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 应用仅支持基本媒体控件,而您的 Web 接收器应用支持更高级的控件,您应确保发送器应用在投放到 Android TV 应用时正常运行。例如,如果您的 Android TV 应用不支持更改播放速率,而您的 Web 接收器应用支持,您应在每个平台上正确设置支持的操作,并确保发送器应用正确呈现界面。

修改 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 会回退到启动 Web 接收器。

发件人应用启动凭据数据

在发送方,您可以指定 CredentialsData 来表示谁将加入会话。

credentials 是一个可由用户定义的字符串,只要您的 ATV 应用可以理解它即可。credentialsType 用于定义 CredentialsData 来自哪个平台,也可以是自定义值。默认情况下,此字段设置为发送内容时所用的平台。

CredentialsData 仅在启动或加入时传递给 Android TV 应用。如果您在连接期间再次进行设置,系统不会将其传递给 Android TV 应用。如果发送方在连接期间切换了配置文件,您可以选择保留会话,或者如果您认为新配置文件与会话不兼容,则可以调用 SessionManager.endCurrentCastSession(boolean stopCasting)

您可以使用 getSendersCastReceiverContext 执行查询以获取 SenderInfo,然后使用 getCastLaunchRequest() 获取 CastLaunchRequest,最后使用 getCredentialsData() 获取每个发件人的 CredentialsData

Android

需要 play-services-cast-framework 19.0.0 版或更高版本。

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.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 接收器,而不是原生启动到 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 接收器应用。

发送和接收自定义消息

借助 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());