本页包含可用于自定义 Android TV 接收器应用的功能的代码段和说明。
配置库
如需使 Cast Connect API 可供 Android TV 应用使用,请执行以下操作:
-
打开应用模块目录中的
build.gradle
文件。 -
验证
google()
是否包含在列出的repositories
中。repositories { google() }
-
根据应用的目标设备类型,将最新版本的库添加到您的依赖项中:
-
对于 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' }
-
对于 Android 接收器应用:
-
保存更改,然后点击工具栏中的
Sync Project with Gradle Files
。
-
确保您的
Podfile
以google-cast-sdk
4.8.1 或更高版本为目标平台 -
以 iOS 14 或更高版本为目标平台。如需了解详情,请参阅版本说明。
platform: ios, '14' def target_pods pod 'google-cast-sdk', '~>4.8.1' end
- 需要 Chromium 浏览器 M87 或更高版本。
-
将 Web Sender API 库添加到您的项目
<script src="//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>
AndroidX 要求
新版 Google Play 服务要求必须更新应用才能使用 androidx
命名空间。按照相关说明迁移到 AndroidX。
Android TV 应用 - 前提条件
为了在 Android TV 应用中支持 Cast Connect,您必须创建和支持来自媒体会话的事件。媒体会话提供的数据提供了媒体状态的基本信息,例如位置、播放状态等。Cast Connect 库还会使用您的媒体会话在收到来自发送器的特定消息(例如暂停)时发出信号。
如需详细了解媒体会话以及如何初始化媒体会话,请参阅使用媒体会话指南。
媒体会话生命周期
您的应用应在播放开始时创建媒体会话,并在无法再控制播放时释放媒体会话。例如,如果您的应用是视频应用,则应在用户退出播放 activity 时释放会话 - 通过选择“返回”浏览其他内容,或将应用设为后台运行。如果您的应用是音乐应用,则应在应用不再播放任何媒体时释放会话。
正在更新会话状态
媒体会话中的数据应与播放器状态保持同步。例如,播放暂停时,您应更新播放状态以及支持的操作。下表列出了您负责及时更新的状态。
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() | 设置播放状态。 |
private fun updateMediaSession() { val metadata = MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, mMovie.getCardImageUrl()) .build() val playbackState = PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, player.getPosition(), player.getPlaybackSpeed(), System.currentTimeMillis() ) .build() mediaSession.setMetadata(metadata) mediaSession.setPlaybackState(playbackState) }
private void updateMediaSession() { MediaMetadataCompat metadata = new MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,mMovie.getCardImageUrl()) .build(); PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, player.getPosition(), player.getPlaybackSpeed(), System.currentTimeMillis()) .build(); mediaSession.setMetadata(metadata); mediaSession.setPlaybackState(playbackState); }
处理传输控制
您的应用应实现媒体会话传输控制回调。下表显示了它们需要处理的传输控制操作:
MediaSessionCompat.Callback
Action | 说明 |
---|---|
onPlay() | 恢复 |
onPause() | 暂停 |
onSeekTo() | 跳转至某个位置 |
onStop() | 停止播放当前媒体内容 |
class MyMediaSessionCallback : MediaSessionCompat.Callback() { override fun onPause() { // Pause the player and update the play state. ... } override fun onPlay() { // Resume the player and update the play state. ... } override fun onSeekTo (long pos) { // Seek and update the play state. ... } ... } mediaSession.setCallback( MyMediaSessionCallback() );
public MyMediaSessionCallback extends MediaSessionCompat.Callback { public void onPause() { // Pause the player and update the play state. ... } public void onPlay() { // Resume the player and update the play state. ... } public void onSeekTo (long pos) { // Seek and update the play state. ... } ... } mediaSession.setCallback(new MyMediaSessionCallback());
配置 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
:
class MyReceiverOptionsProvider : ReceiverOptionsProvider { override fun getOptions(context: Context?): CastReceiverOptions { return CastReceiverOptions.Builder(context) .setStatusText("My App") .build() } }
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider { @Override public CastReceiverOptions getOptions(Context context) { return new CastReceiverOptions.Builder(context) .setStatusText("My App") .build(); } }
然后在 AndroidManifest
中指定选项提供程序:
<meta-data
android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.example.mysimpleatvapplication.MyReceiverOptionsProvider" />
ReceiverOptionsProvider
用于在 CastReceiverContext
初始化时提供 CastReceiverOptions
。
Cast 接收器上下文
在创建应用时初始化 CastReceiverContext
:
override fun onCreate() { CastReceiverContext.initInstance(this) ... }
@Override public void onCreate() { CastReceiverContext.initInstance(this); ... }
当应用进入前台时启动 CastReceiverContext
:
CastReceiverContext.getInstance().start()
CastReceiverContext.getInstance().start();
对于视频应用或不支持后台播放的应用,请在应用进入后台后,对 CastReceiverContext
调用 stop()
:
// Player has stopped. CastReceiverContext.getInstance().stop()
// Player has stopped. CastReceiverContext.getInstance().stop();
此外,如果您的应用支持后台播放,请在应用在后台停止播放时对 CastReceiverContext
调用 stop()
。
强烈建议您使用 androidx.lifecycle
库中的 LifecycleObserver 管理对 CastReceiverContext.start()
和 CastReceiverContext.stop()
的调用,尤其是在原生应用具有多个 activity 时。这可以避免在不同 activity 中调用 start()
和 stop()
时出现竞态条件。
// Create a LifecycleObserver class. class MyLifecycleObserver : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { // App prepares to enter foreground. CastReceiverContext.getInstance().start() } override fun onStop(owner: LifecycleOwner) { // App has moved to the background or has terminated. CastReceiverContext.getInstance().stop() } } // Add the observer when your application is being created. class MyApplication : Application() { fun onCreate() { super.onCreate() // Initialize CastReceiverContext. CastReceiverContext.initInstance(this /* android.content.Context */) // Register LifecycleObserver ProcessLifecycleOwner.get().lifecycle.addObserver( MyLifecycleObserver()) } }
// Create a LifecycleObserver class. public class MyLifecycleObserver implements DefaultLifecycleObserver { @Override public void onStart(LifecycleOwner owner) { // App prepares to enter foreground. CastReceiverContext.getInstance().start(); } @Override public void onStop(LifecycleOwner owner) { // App has moved to the background or has terminated. CastReceiverContext.getInstance().stop(); } } // Add the observer when your application is being created. public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // Initialize CastReceiverContext. CastReceiverContext.initInstance(this /* android.content.Context */); // Register LifecycleObserver ProcessLifecycleOwner.get().getLifecycle().addObserver( new MyLifecycleObserver()); } }
// In AndroidManifest.xml set MyApplication as the application class
<application
...
android:name=".MyApplication">
将 MediaSession 连接到 MediaManager
创建 MediaSession
时,您还需要向 CastReceiverContext
提供当前的 MediaSession
令牌,使其知道将命令发送到何处并检索媒体播放状态:
val mediaManager: MediaManager = receiverContext.getMediaManager() mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken())
MediaManager mediaManager = receiverContext.getMediaManager(); mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());
当您因播放处于非活动状态而释放 MediaSession
时,应在 MediaManager
上设置一个 null 令牌:
myPlayer.stop() mediaSession.release() mediaManager.setSessionCompatToken(null)
myPlayer.stop(); mediaSession.release(); mediaManager.setSessionCompatToken(null);
如果您的应用支持在后台时播放媒体内容,则您应仅在应用在后台运行且不再播放媒体内容时才调用CastReceiverContext.stop()
,而不是在转到后台时调用。例如:
class MyLifecycleObserver : DefaultLifecycleObserver { ... // App has moved to the background. override fun onPause(owner: LifecycleOwner) { mIsBackground = true myStopCastReceiverContextIfNeeded() } } // Stop playback on the player. private fun myStopPlayback() { myPlayer.stop() myStopCastReceiverContextIfNeeded() } // Stop the CastReceiverContext when both the player has // stopped and the app has moved to the background. private fun myStopCastReceiverContextIfNeeded() { if (mIsBackground && myPlayer.isStopped()) { CastReceiverContext.getInstance().stop() } }
public class MyLifecycleObserver implements DefaultLifecycleObserver { ... // App has moved to the background. @Override public void onPause(LifecycleOwner owner) { mIsBackground = true; myStopCastReceiverContextIfNeeded(); } } // Stop playback on the player. private void myStopPlayback() { myPlayer.stop(); myStopCastReceiverContextIfNeeded(); } // Stop the CastReceiverContext when both the player has // stopped and the app has moved to the background. private void myStopCastReceiverContextIfNeeded() { if (mIsBackground && myPlayer.isStopped()) { CastReceiverContext.getInstance().stop(); } }
将 Exoplayer 与 Cast Connect 搭配使用
如果您使用的是 Exoplayer
,则可以使用 MediaSessionConnector
自动维护会话和所有相关信息(包括播放状态),而不是手动跟踪更改。
MediaSessionConnector.MediaButtonEventHandler
可用于通过调用 setMediaButtonEventHandler(MediaButtonEventHandler)
来处理 MediaButton 事件,而这些事件默认由 MediaSessionCompat.Callback
处理。
如需在应用中集成 MediaSessionConnector
,请将以下内容添加到您的播放器 activity 类或您管理媒体会话的任何位置:
class PlayerActivity : Activity() { private var mMediaSession: MediaSessionCompat? = null private var mMediaSessionConnector: MediaSessionConnector? = null private var mMediaManager: MediaManager? = null override fun onCreate(savedInstanceState: Bundle?) { ... mMediaSession = MediaSessionCompat(this, LOG_TAG) mMediaSessionConnector = MediaSessionConnector(mMediaSession!!) ... } override fun onStart() { ... mMediaManager = receiverContext.getMediaManager() mMediaManager!!.setSessionCompatToken(currentMediaSession.getSessionToken()) mMediaSessionConnector!!.setPlayer(mExoPlayer) mMediaSessionConnector!!.setMediaMetadataProvider(mMediaMetadataProvider) mMediaSession!!.isActive = true ... } override fun onStop() { ... mMediaSessionConnector!!.setPlayer(null) mMediaSession!!.release() mMediaManager!!.setSessionCompatToken(null) ... } }
public class PlayerActivity extends Activity { private MediaSessionCompat mMediaSession; private MediaSessionConnector mMediaSessionConnector; private MediaManager mMediaManager; @Override protected void onCreate(Bundle savedInstanceState) { ... mMediaSession = new MediaSessionCompat(this, LOG_TAG); mMediaSessionConnector = new MediaSessionConnector(mMediaSession); ... } @Override protected void onStart() { ... mMediaManager = receiverContext.getMediaManager(); mMediaManager.setSessionCompatToken(currentMediaSession.getSessionToken()); mMediaSessionConnector.setPlayer(mExoPlayer); mMediaSessionConnector.setMediaMetadataProvider(mMediaMetadataProvider); mMediaSession.setActive(true); ... } @Override protected void onStop() { ... mMediaSessionConnector.setPlayer(null); mMediaSession.release(); mMediaManager.setSessionCompatToken(null); ... } }
发件人应用设置
启用 Cast Connect 支持
通过支持 Cast Connect 来更新发送器应用后,您可以通过将 LaunchOptions
上的 androidReceiverCompatible
标志设置为 true 来声明其就绪情况。
需要 play-services-cast-framework
19.0.0
或更高版本。
androidReceiverCompatible
标志在 LaunchOptions
(CastOptions
的一部分)中设置:
class CastOptionsProvider : OptionsProvider { override fun getCastOptions(context: Context?): CastOptions { val launchOptions: LaunchOptions = Builder() .setAndroidReceiverCompatible(true) .build() return CastOptions.Builder() .setLaunchOptions(launchOptions) ... .build() } }
public class CastOptionsProvider implements OptionsProvider { @Override public CastOptions getCastOptions(Context context) { LaunchOptions launchOptions = new LaunchOptions.Builder() .setAndroidReceiverCompatible(true) .build(); return new CastOptions.Builder() .setLaunchOptions(launchOptions) ... .build(); } }
需要 google-cast-sdk
v4.4.8
或更高版本。
androidReceiverCompatible
标志在 GCKLaunchOptions
(GCKCastOptions
的一部分)中设置:
let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID)) ... let launchOptions = GCKLaunchOptions() launchOptions.androidReceiverCompatible = true options.launchOptions = launchOptions GCKCastContext.setSharedInstanceWith(options)
需要 Chromium 浏览器 M87
或更高版本。
const context = cast.framework.CastContext.getInstance(); const castOptions = new cast.framework.CastOptions(); castOptions.receiverApplicationId = kReceiverAppID; castOptions.androidReceiverCompatible = true; context.setOptions(castOptions);
Cast Developer Console 设置
配置 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
来传递深层链接:
val mediaToLoad = MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") ... .build() val loadRequest = MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build() remoteMediaClient.load(loadRequest)
MediaInfo mediaToLoad = new MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") ... .build(); MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build(); remoteMediaClient.load(loadRequest);
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id") ... mediaInformation = mediaInfoBuilder.build() let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder() mediaLoadRequestDataBuilder.mediaInformation = mediaInformation mediaLoadRequestDataBuilder.credentials = "user-credentials" ... let mediaLoadRequestData = mediaLoadRequestDataBuilder.build() remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
需要 Chromium 浏览器 M87
或更高版本。
let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4'); mediaInfo.entity = 'https://example.com/watch/some-id'; ... let request = new chrome.cast.media.LoadRequest(mediaInfo); request.credentials = 'user-credentials'; ... cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
系统会通过 intent 发送该加载命令,其中包含您在开发者控制台中定义的深层链接和软件包名称。
为发送者设置 ATV 凭据
您的 Web 接收器应用和 Android TV 应用可能支持不同的深层链接和 credentials
(例如,如果您在这两个平台上以不同方式处理身份验证)。为了解决此问题,您可以为 Android TV 提供备用的 entity
和 credentials
:
val mediaToLoad = MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") .setAtvEntity("myscheme://example.com/atv/some-id") ... .build() val loadRequest = MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") .setAtvCredentials("atv-user-credentials") ... .build() remoteMediaClient.load(loadRequest)
MediaInfo mediaToLoad = new MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") .setAtvEntity("myscheme://example.com/atv/some-id") ... .build(); MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") .setAtvCredentials("atv-user-credentials") ... .build(); remoteMediaClient.load(loadRequest);
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id") mediaInfoBuilder.atvEntity = "myscheme://example.com/atv/some-id" ... mediaInformation = mediaInfoBuilder.build() let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder() mediaLoadRequestDataBuilder.mediaInformation = mediaInformation mediaLoadRequestDataBuilder.credentials = "user-credentials" mediaLoadRequestDataBuilder.atvCredentials = "atv-user-credentials" ... let mediaLoadRequestData = mediaLoadRequestDataBuilder.build() remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
需要 Chromium 浏览器 M87
或更高版本。
let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4'); mediaInfo.entity = 'https://example.com/watch/some-id'; mediaInfo.atvEntity = 'myscheme://example.com/atv/some-id'; ... let request = new chrome.cast.media.LoadRequest(mediaInfo); request.credentials = 'user-credentials'; request.atvCredentials = 'atv-user-credentials'; ... cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
如果 Web 接收器应用已启动,则会在加载请求中使用 entity
和 credentials
。不过,如果您的 Android TV 应用已启动,SDK 会将 entity
和 credentials
替换为您的 atvEntity
和 atvCredentials
(如果已指定)。
通过 Content ID 或 MediaQueueData 加载
如果您未使用 entity
或 atvEntity
,而是在媒体信息中使用了 Content 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()
。
val mediaToLoad = MediaInfo.Builder("some-id").build() val loadRequest = MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build() remoteMediaClient.load(loadRequest)
MediaInfo mediaToLoad = new MediaInfo.Builder("some-id").build(); MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build(); remoteMediaClient.load(loadRequest);
let mediaInfoBuilder = GCKMediaInformationBuilder(contentId: "some-id") ... mediaInformation = mediaInfoBuilder.build() let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder() mediaLoadRequestDataBuilder.mediaInformation = mediaInformation mediaLoadRequestDataBuilder.credentials = "user-credentials" ... let mediaLoadRequestData = mediaLoadRequestDataBuilder.build() remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
需要 Chromium 浏览器 M87
或更高版本。
let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4'); ... let request = new chrome.cast.media.LoadRequest(mediaInfo); ... cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
处理加载请求
在您的 activity 中,如需处理这些加载请求,您需要处理 activity 生命周期回调中的 intent:
class MyActivity : Activity() { override fun onStart() { super.onStart() val mediaManager = CastReceiverContext.getInstance().getMediaManager() // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(intent)) { // If the SDK recognizes the intent, you should early return. return } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } // For some cases, a new load intent triggers onNewIntent() instead of // onStart(). override fun onNewIntent(intent: Intent) { val mediaManager = CastReceiverContext.getInstance().getMediaManager() // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(intent)) { // If the SDK recognizes the intent, you should early return. return } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } }
public class MyActivity extends Activity { @Override protected void onStart() { super.onStart(); MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(getIntent())) { // If the SDK recognizes the intent, you should early return. return; } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } // For some cases, a new load intent triggers onNewIntent() instead of // onStart(). @Override protected void onNewIntent(Intent intent) { MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(intent)) { // If the SDK recognizes the intent, you should early return. return; } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } }
如果 MediaManager
检测到 intent 是加载 intent,则会从该 intent 中提取 MediaLoadRequestData
对象,并调用 MediaLoadCommandCallback.onLoad()
。您需要替换此方法以处理加载请求。回调必须在调用 MediaManager.onNewIntent()
之前注册(建议针对 Activity 或应用 onCreate()
方法)。
class MyActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.setMediaLoadCommandCallback(MyMediaLoadCommandCallback()) } } class MyMediaLoadCommandCallback : MediaLoadCommandCallback() { override fun onLoad( senderId: String?, loadRequestData: MediaLoadRequestData ): Task{ return Tasks.call { // Resolve the entity into your data structure and load media. val mediaInfo = loadRequestData.getMediaInfo() if (!checkMediaInfoSupported(mediaInfo)) { // Throw MediaException to indicate load failure. throw MediaException( MediaError.Builder() .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED) .setReason(MediaError.ERROR_REASON_INVALID_REQUEST) .build() ) } myFillMediaInfo(MediaInfoWriter(mediaInfo)) myPlayerLoad(mediaInfo.getContentUrl()) // Update media metadata and state (this clears all previous status // overrides). castReceiverContext.getMediaManager() .setDataFromLoad(loadRequestData) ... castReceiverContext.getMediaManager().broadcastMediaStatus() // Return the resolved MediaLoadRequestData to indicate load success. return loadRequestData } } private fun myPlayerLoad(contentURL: String) { myPlayer.load(contentURL) // Update the MediaSession state. val playbackState: PlaybackStateCompat = Builder() .setState( player.getState(), player.getPosition(), System.currentTimeMillis() ) ... .build() mediaSession.setPlaybackState(playbackState) }
public class MyActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback()); } } public class MyMediaLoadCommandCallback extends MediaLoadCommandCallback { @Override public TaskonLoad(String senderId, MediaLoadRequestData loadRequestData) { return Tasks.call(() -> { // Resolve the entity into your data structure and load media. MediaInfo mediaInfo = loadRequestData.getMediaInfo(); if (!checkMediaInfoSupported(mediaInfo)) { // Throw MediaException to indicate load failure. throw new MediaException( new MediaError.Builder() .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED) .setReason(MediaError.ERROR_REASON_INVALID_REQUEST) .build()); } myFillMediaInfo(new MediaInfoWriter(mediaInfo)); myPlayerLoad(mediaInfo.getContentUrl()); // Update media metadata and state (this clears all previous status // overrides). castReceiverContext.getMediaManager() .setDataFromLoad(loadRequestData); ... castReceiverContext.getMediaManager().broadcastMediaStatus(); // Return the resolved MediaLoadRequestData to indicate load success. return loadRequestData; }); } private void myPlayerLoad(String contentURL) { myPlayer.load(contentURL); // Update the MediaSession state. PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setState( player.getState(), player.getPosition(), System.currentTimeMillis()) ... .build(); mediaSession.setPlaybackState(playbackState); }
为了处理加载 intent,您可以将 intent 解析到我们定义的数据结构中(对于加载请求,请使用 MediaLoadRequestData
)。
支持媒体命令
基本播放控制支持
基本集成命令包括与媒体会话兼容的命令。这些命令通过媒体会话回调获得通知。您需要注册一个对媒体会话的回调来支持此操作(您可能已经在这样做)。
private class MyMediaSessionCallback : MediaSessionCompat.Callback() { override fun onPause() { // Pause the player and update the play state. myPlayer.pause() } override fun onPlay() { // Resume the player and update the play state. myPlayer.play() } override fun onSeekTo(pos: Long) { // Seek and update the play state. myPlayer.seekTo(pos) } ... } mediaSession.setCallback(MyMediaSessionCallback())
private class MyMediaSessionCallback extends MediaSessionCompat.Callback { @Override public void onPause() { // Pause the player and update the play state. myPlayer.pause(); } @Override public void onPlay() { // Resume the player and update the play state. myPlayer.play(); } @Override public void onSeekTo(long pos) { // Seek and update the play state. myPlayer.seekTo(pos); } ... } mediaSession.setCallback(new MyMediaSessionCallback());
支持 Cast 控制命令
有一些 Cast 命令在 MediaSession
中不可用,例如 skipAd()
或 setActiveMediaTracks()
。此外,由于 Cast 队列与 MediaSession
队列不完全兼容,因此需要在此处实现某些队列命令。
class MyMediaCommandCallback : MediaCommandCallback() { override fun onSkipAd(requestData: RequestData?): Task{ // Skip your ad ... return Tasks.forResult(null) } } val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
public class MyMediaCommandCallback extends MediaCommandCallback { @Override public TaskonSkipAd(RequestData requestData) { // Skip your ad ... return Tasks.forResult(null); } } MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());
指定支持的媒体命令
与 Cast 接收器一样,您的 Android TV 应用应指定支持的命令,以便发送器可以启用或停用某些界面控件。对于属于 MediaSession
的命令,请在 PlaybackStateCompat
中指定这些命令。应在 MediaStatusModifier
中指定其他命令。
// Set media session supported commands val playbackState: PlaybackStateCompat = PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE) .setState(PlaybackStateCompat.STATE_PLAYING) .build() mediaSession.setPlaybackState(playbackState) // Set additional commands in MediaStatusModifier val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.getMediaStatusModifier() .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT)
// Set media session supported commands PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE) .setState(PlaybackStateCompat.STATE_PLAYING) .build(); mediaSession.setPlaybackState(playbackState); // Set additional commands in MediaStatusModifier MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.getMediaStatusModifier() .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT);
隐藏不支持的按钮
如果您的 Android TV 应用仅支持基本媒体控制,但网络接收器应用支持更高级的控制,则应确保发送器应用在投射到 Android TV 应用时能够正常运行。例如,如果 Android TV 应用不支持更改播放速率,而网络接收器应用支持更改播放速率,则您应在每个平台上正确设置支持的操作,并确保发送器应用能够正确呈现界面。
修改 MediaStatus
为了支持轨道、广告、直播和队列等高级功能,您的 Android TV 应用需要提供无法通过 MediaSession
确定的其他信息。
我们为您提供了 MediaStatusModifier
类来实现这一点。MediaStatusModifier
将始终对您在 CastReceiverContext
中设置的 MediaSession
执行操作。
如需创建并广播 MediaStatus
,请执行以下操作:
val mediaManager: MediaManager = castReceiverContext.getMediaManager() val statusModifier: MediaStatusModifier = mediaManager.getMediaStatusModifier() statusModifier .setLiveSeekableRange(seekableRange) .setAdBreakStatus(adBreakStatus) .setCustomData(customData) mediaManager.broadcastMediaStatus()
MediaManager mediaManager = castReceiverContext.getMediaManager(); MediaStatusModifier statusModifier = mediaManager.getMediaStatusModifier(); statusModifier .setLiveSeekableRange(seekableRange) .setAdBreakStatus(adBreakStatus) .setCustomData(customData); mediaManager.broadcastMediaStatus();
我们的客户端库将从 MediaSession
获取基本 MediaStatus
,您的 Android TV 应用可以通过 MediaStatus
修饰符指定其他状态和替换状态。
某些状态和元数据可以在 MediaSession
和 MediaStatusModifier
中同时设置。我们强烈建议您仅在 MediaSession
中设置它们。您仍然可以使用该修饰符替换 MediaSession
中的状态,不建议这样做,因为修饰符中的状态的优先级始终高于 MediaSession
提供的值。
在发送之前拦截 MediaStatus
与 Web Receiver SDK 相同,如果您想在发送之前进行最后的润色,可以指定 MediaStatusInterceptor
来处理要发送的 MediaStatus
。我们传入 MediaStatusWriter
,以在发送 MediaStatus
之前对其进行操作。
mediaManager.setMediaStatusInterceptor(object : MediaStatusInterceptor { override fun intercept(mediaStatusWriter: MediaStatusWriter) { // Perform customization. mediaStatusWriter.setCustomData(JSONObject("{data: \"my Hello\"}")) } })
mediaManager.setMediaStatusInterceptor(new MediaStatusInterceptor() { @Override public void intercept(MediaStatusWriter mediaStatusWriter) { // Perform customization. mediaStatusWriter.setCustomData(new JSONObject("{data: \"my Hello\"}")); } });
处理用户凭据
您的 Android TV 应用可能仅允许特定用户启动或加入应用会话。例如,仅当满足以下条件时,才允许发送者发起或加入群组:
- 发送者应用登录的帐号和个人资料与 ATV 应用相同。
- 发送者应用登录的帐号与 ATV 应用不同。
如果您的应用可以处理多位用户或匿名用户,您可以允许其他任何其他用户加入 ATV 会话。如果用户提供凭据,您的 ATV 应用需要处理其凭据,以便正确跟踪其进度和其他用户数据。
当发送器应用启动或加入 Android TV 应用时,发送器应用应提供代表谁加入会话的凭据。
在发送者启动并加入您的 Android TV 应用之前,您可以指定启动检查工具,以查看是否允许发送发送者凭据。否则,Cast Connect SDK 会回退到启动您的网络接收器。
发送者应用启动凭据数据
在发送者端,您可以指定 CredentialsData
来表示谁将加入会话。
credentials
是一个可由用户定义的字符串,只要您的 ATV 应用能够理解即可。credentialsType
定义了 CredentialsData
来自哪个平台,或者可以是自定义值。默认情况下,它被设置为发送该消息的平台。
CredentialsData
仅在启动或加入期间传递给 Android TV 应用。如果您在连接时再次设置,它不会传递给 Android TV 应用。如果发送器在连接期间切换配置文件,您可以留在会话中,或者,如果您认为新配置文件与会话不兼容,可以调用 SessionManager.endCurrentCastSession(boolean stopCasting)
。
可以使用 CastReceiverContext
上的 getSenders
检索每个发送者的 CredentialsData
,获取 SenderInfo
,getCastLaunchRequest()
获取 CastLaunchRequest
,然后获取 getCredentialsData()
。
需要 play-services-cast-framework
19.0.0
或更高版本。
CastContext.getSharedInstance().setLaunchCredentialsData( CredentialsData.Builder() .setCredentials("{\"userId\": \"abc\"}") .build() )
CastContext.getSharedInstance().setLaunchCredentialsData( new CredentialsData.Builder() .setCredentials("{\"userId\": \"abc\"}") .build());
需要 google-cast-sdk
v4.8.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 应用。您可以根据您的应用是否支持在用户未登录 ATV 应用时发送加载请求或者用户不匹配的情况来自定义此行为。此行为完全可以在 LaunchRequestChecker
中自定义。
创建一个实现 CastReceiverOptions.LaunchRequestChecker
接口的类:
class MyLaunchRequestChecker : LaunchRequestChecker { override fun checkLaunchRequestSupported(launchRequest: CastLaunchRequest): Task{ return Tasks.call { myCheckLaunchRequest( launchRequest ) } } } private fun myCheckLaunchRequest(launchRequest: CastLaunchRequest): Boolean { val credentialsData = launchRequest.getCredentialsData() ?: return false // or true if you allow anonymous users to join. // The request comes from a mobile device, e.g. checking user match. return if (credentialsData.credentialsType == CredentialsData.CREDENTIALS_TYPE_ANDROID) { myCheckMobileCredentialsAllowed(credentialsData.getCredentials()) } else false // Unrecognized credentials type. }
public class MyLaunchRequestChecker implements CastReceiverOptions.LaunchRequestChecker { @Override public TaskcheckLaunchRequestSupported(CastLaunchRequest launchRequest) { return Tasks.call(() -> myCheckLaunchRequest(launchRequest)); } } private boolean myCheckLaunchRequest(CastLaunchRequest launchRequest) { CredentialsData credentialsData = launchRequest.getCredentialsData(); if (credentialsData == null) { return false; // or true if you allow anonymous users to join. } // The request comes from a mobile device, e.g. checking user match. if (credentialsData.getCredentialsType().equals(CredentialsData.CREDENTIALS_TYPE_ANDROID)) { return myCheckMobileCredentialsAllowed(credentialsData.getCredentials()); } // Unrecognized credentials type. return false; }
然后在 ReceiverOptionsProvider
中进行设置:
class MyReceiverOptionsProvider : ReceiverOptionsProvider { override fun getOptions(context: Context?): CastReceiverOptions { return CastReceiverOptions.Builder(context) ... .setLaunchRequestChecker(MyLaunchRequestChecker()) .build() } }
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider { @Override public CastReceiverOptions getOptions(Context context) { return new CastReceiverOptions.Builder(context) ... .setLaunchRequestChecker(new MyLaunchRequestChecker()) .build(); } }
在 LaunchRequestChecker
中解析 true
会启动 ATV 应用,并启动 false
您的 Web 接收器应用。
发送和接收自定义消息
借助 Cast 协议,您可以在发送器和接收器应用之间发送自定义字符串消息。在初始化 CastReceiverContext
之前,您必须注册一个要发送消息的命名空间(通道)。
Android TV - 指定自定义命名空间
在设置过程中,您需要在 CastReceiverOptions
中指定支持的命名空间:
class MyReceiverOptionsProvider : ReceiverOptionsProvider { override fun getOptions(context: Context?): CastReceiverOptions { return CastReceiverOptions.Builder(context) .setCustomNamespaces( Arrays.asList("urn:x-cast:com.example.cast.mynamespace") ) .build() } }
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider { @Override public CastReceiverOptions getOptions(Context context) { return new CastReceiverOptions.Builder(context) .setCustomNamespaces( Arrays.asList("urn:x-cast:com.example.cast.mynamespace")) .build(); } }
Android TV - 发送消息
// If senderId is null, then the message is broadcasted to all senders. CastReceiverContext.getInstance().sendMessage( "urn:x-cast:com.example.cast.mynamespace", senderId, customString)
// If senderId is null, then the message is broadcasted to all senders. CastReceiverContext.getInstance().sendMessage( "urn:x-cast:com.example.cast.mynamespace", senderId, customString);
Android TV - 接收自定义命名空间消息
class MyCustomMessageListener : MessageReceivedListener { override fun onMessageReceived( namespace: String, senderId: String?, message: String ) { ... } } CastReceiverContext.getInstance().setMessageReceivedListener( "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());
class MyCustomMessageListener implements CastReceiverContext.MessageReceivedListener { @Override public void onMessageReceived( String namespace, String senderId, String message) { ... } } CastReceiverContext.getInstance().setMessageReceivedListener( "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());