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

本页包含 Google Cloud 上的代码段, 自定义 Android TV 接收器应用。

配置库

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

<ph type="x-smartling-placeholder">
</ph>
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() 停止播放当前媒体内容
<ph type="x-smartling-placeholder">
</ph>
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 具有应用命名空间由您的应用负责处理 并创建一个 CastReceiverContext 对象。需要 CastReceiverContext 对象 在 TV 应用运行时与 Cast 进行交互。此对象可让您的 TV 应用接受来自任何已连接发送器的 Cast 媒体消息。

Android TV 设置

添加启动 intent 过滤器

向您要处理启动的 activity 添加新的 intent 过滤器 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 用于在以下情况下提供 CastReceiverOptions: 已初始化 CastReceiverContext

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

致电 stop()CastReceiverContext 应用进入后台后 后台播放:

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

此外,如果您的应用支持后台播放,请调用 stop()CastReceiverContext 在后台停止播放时触发。

我们强烈建议您使用 androidx.lifecycle 用于管理通话的库 CastReceiverContext.start()CastReceiverContext.stop(), 尤其是在您的原生应用具有多个 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, 您还需要将当前的 MediaSession 令牌提供给 CastReceiverContext 以便它知道发送命令并检索媒体播放状态的位置:

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

当您因播放处于非活动状态而释放 MediaSession 时,您应将 出现 null 标记, 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();
  }
}

将 Exoplayer 与 Cast Connect 搭配使用

如果您使用的是 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 后,您可以声明 通过设置 androidReceiverCompatible 开启举报 LaunchOptions 设为 true。

<ph type="x-smartling-placeholder">
</ph>
Android

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

androidReceiverCompatible 标志在 LaunchOptionsCastOptions 的一部分):

<ph type="x-smartling-placeholder">
</ph>
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();
  }
}
<ph type="x-smartling-placeholder">
</ph>
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 Developer Console 设置

配置 Android TV 应用

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

注册开发者设备

注册您要使用的 Android TV 设备的序列号 生成式 AI 的 投屏开发者控制台

如果未注册,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 来传递深层链接 加载请求的信息:

<ph type="x-smartling-placeholder">
</ph>
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
。 <ph type="x-smartling-placeholder">
</ph>
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);
<ph type="x-smartling-placeholder">
</ph>
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 接收器应用和 Android TV 应用支持的浏览器可能 深层链接和 credentials(例如,如果您要处理身份验证) 在两个平台上有所不同)。要解决此问题,您可以提供备用 entitycredentials(适用于 Android TV):

<ph type="x-smartling-placeholder">
</ph>
Android
。 <ph type="x-smartling-placeholder">
</ph>
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);
<ph type="x-smartling-placeholder">
</ph>
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 接收器应用启动,它会在 Web Receiver 应用中使用 entitycredentials 加载请求。不过,如果您的 Android TV 应用已启动,SDK 将替换 entitycredentials以及您的atvEntityatvCredentials (如果已指定)。

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

<ph type="x-smartling-placeholder">
</ph>
Android
。 <ph type="x-smartling-placeholder">
</ph>
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);
<ph type="x-smartling-placeholder">
</ph>
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 中,为了处理这些加载请求,您需要处理 intent 在 activity 生命周期回调中:

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,它会提取 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()。 此外,由于投放队列 与“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 应用应指定哪些命令 ,因此发送方可以启用或停用某些界面控件。对于 作为 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 应用不支持更改播放速率,而 Web 接收器应用则支持, 您应在每个平台上正确设置支持的操作,并确保 您的发送者应用能否正确呈现界面

修改 MediaStatus

为了支持曲目、广告、直播和队列等高级功能,您的 Android 设备 TV 应用需要提供无法通过 MediaSession

我们提供 MediaStatusModifier 类来实现这一点。MediaStatusModifier 将始终在 您在“MediaSession”中设置的 CastReceiverContext

创建并广播 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 应用 需要处理用户的凭据,以便 Google 可以 正确跟踪。

当发送方应用启动或加入您的 Android TV 应用时,您的发送方应用 应提供代表谁加入会话的凭据。

在发送者启动并加入您的 Android TV 应用之前,您可以指定 启动检查工具,看看发件人凭据是否允许使用。否则, Connect SDK 会回退到启动您的 Web 接收器。

发送者应用启动凭据数据

在发件人端,您可以指定 CredentialsData 来代表 加入会议。

credentials 是一个可由用户定义的字符串,只要您的 ATV 应用能够理解它。credentialsType 定义了 CredentialsData 来自自定义值或可以是自定义值。默认设置为 发送到目标平台

CredentialsData 仅在 Android TV 应用启动或 加入时间。如果您在联网时再次设置,系统不会将此代码传递给 Android TV 应用。如果发送者在关联期间切换个人资料,则你 可以继续参与会话,也可以调用 SessionManager.endCurrentCastSession(boolean stopCasting) (如果您认为新配置文件与此会话不兼容)。

通过 CredentialsData 可以使用 getSendersCastReceiverContext 以获取SenderInfogetCastLaunchRequest() 以获取 CastLaunchRequest, 接着点击 getCredentialsData()

<ph type="x-smartling-placeholder">
</ph>
Android

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

<ph type="x-smartling-placeholder">
</ph>
Kotlin
CastContext.getSharedInstance().setLaunchCredentialsData(
    CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
)
Java
CastContext.getSharedInstance().setLaunchCredentialsData(
    new CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build());
<ph type="x-smartling-placeholder">
</ph>
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。 以允许或拒绝此请求。

如果请求被拒绝,则加载网络接收器,而不是启动 直接嵌入到 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();
  }
}

解决 true 中的 LaunchRequestChecker 会启动 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());