本页包含 Google Cloud 上的代码段, 自定义 Android TV 接收器应用。
配置库
如需使 Cast Connect API 可供 Android TV 应用使用,请执行以下操作:
<ph type="x-smartling-placeholder">-
打开应用模块目录中的
build.gradle
文件。 -
验证
google()
是否包含在列出的repositories
中。repositories { google() }
-
根据应用的目标设备类型,添加最新版本
添加到您的依赖项中:
<ph type="x-smartling-placeholder">
- </ph>
-
对于 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 库使用 在收到来自发件人的某些邮件时发出信号(如暂停)。
如需详细了解媒体会话以及如何初始化媒体会话, 请参阅 使用媒体会话指南。
媒体会话生命周期
您的应用应在播放开始时创建一个媒体会话,并在播放完毕后释放该媒体会话。 就不能再对其进行控制了例如,如果您的应用是视频应用, 应在用户退出播放活动时释放会话,具体方法可能是 选择“返回”以浏览其他内容,或将应用置于后台。如果您的 应用是一款音乐应用,请在应用不再播放任何 媒体。
正在更新会话状态
媒体会话中的数据应根据 。例如,当播放暂停时,您应更新播放 以及支持的操作下表列出了 您需要负责保持最新状态。
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
操作 | 说明 |
---|---|
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
具有应用命名空间由您的应用负责处理
并创建一个
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
:
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
用于在以下情况下提供 CastReceiverOptions
:
已初始化 CastReceiverContext
。
Cast 接收器上下文
初始化
CastReceiverContext
您的应用创建完毕时:
override fun onCreate() { CastReceiverContext.initInstance(this) ... }
@Override public void onCreate() { CastReceiverContext.initInstance(this); ... }
当应用进入前台时启动 CastReceiverContext
:
CastReceiverContext.getInstance().start()
CastReceiverContext.getInstance().start();
致电
stop()
在
CastReceiverContext
应用进入后台后
后台播放:
// Player has stopped. CastReceiverContext.getInstance().stop()
// Player has stopped. CastReceiverContext.getInstance().stop();
此外,如果您的应用支持后台播放,请调用 stop()
在 CastReceiverContext
在后台停止播放时触发。
我们强烈建议您使用
androidx.lifecycle
用于管理通话的库
CastReceiverContext.start()
和
CastReceiverContext.stop()
,
尤其是在您的原生应用具有多个 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
、
您还需要将当前的 MediaSession
令牌提供给
CastReceiverContext
以便它知道发送命令并检索媒体播放状态的位置:
val mediaManager: MediaManager = receiverContext.getMediaManager() mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken())
MediaManager mediaManager = receiverContext.getMediaManager(); mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());
当您因播放处于非活动状态而释放 MediaSession
时,您应将
出现 null 标记,
MediaManager
:
myPlayer.stop() mediaSession.release() mediaManager.setSessionCompatToken(null)
myPlayer.stop(); mediaSession.release(); mediaManager.setSessionCompatToken(null);
如果您的应用支持在后台运行时播放媒体
调用
CastReceiverContext.stop()
当您的应用被发送到后台时,您应仅在您的应用
位于后台,不再播放媒体内容。例如:
class MyLifecycleObserver : DefaultLifecycleObserver { ... // App has moved to the background. override fun onPause(owner: LifecycleOwner) { mIsBackground = true myStopCastReceiverContextIfNeeded() } } // Stop playback on the player. private fun myStopPlayback() { myPlayer.stop() myStopCastReceiverContextIfNeeded() } // Stop the CastReceiverContext when both the player has // stopped and the app has moved to the background. private fun myStopCastReceiverContextIfNeeded() { if (mIsBackground && myPlayer.isStopped()) { CastReceiverContext.getInstance().stop() } }
public class MyLifecycleObserver implements DefaultLifecycleObserver { ... // App has moved to the background. @Override public void onPause(LifecycleOwner owner) { mIsBackground = true; myStopCastReceiverContextIfNeeded(); } } // Stop playback on the player. private void myStopPlayback() { myPlayer.stop(); myStopCastReceiverContextIfNeeded(); } // Stop the CastReceiverContext when both the player has // stopped and the app has moved to the background. private void myStopCastReceiverContextIfNeeded() { if (mIsBackground && myPlayer.isStopped()) { CastReceiverContext.getInstance().stop(); } }
将 Exoplayer 与 Cast Connect 搭配使用
如果您使用的是
Exoplayer
,您可以使用
MediaSessionConnector
以自动维护会话和所有相关信息,包括
而不必手动跟踪更改
MediaSessionConnector.MediaButtonEventHandler
可用于处理 MediaButton 事件,方法是调用
setMediaButtonEventHandler(MediaButtonEventHandler)
这些代码由
MediaSessionCompat.Callback
默认情况。
为了集成
MediaSessionConnector
将以下代码添加到您的玩家活动类中,或添加到
管理媒体会话:
class PlayerActivity : Activity() { private var mMediaSession: MediaSessionCompat? = null private var mMediaSessionConnector: MediaSessionConnector? = null private var mMediaManager: MediaManager? = null override fun onCreate(savedInstanceState: Bundle?) { ... mMediaSession = MediaSessionCompat(this, LOG_TAG) mMediaSessionConnector = MediaSessionConnector(mMediaSession!!) ... } override fun onStart() { ... mMediaManager = receiverContext.getMediaManager() mMediaManager!!.setSessionCompatToken(currentMediaSession.getSessionToken()) mMediaSessionConnector!!.setPlayer(mExoPlayer) mMediaSessionConnector!!.setMediaMetadataProvider(mMediaMetadataProvider) mMediaSession!!.isActive = true ... } override fun onStop() { ... mMediaSessionConnector!!.setPlayer(null) mMediaSession!!.release() mMediaManager!!.setSessionCompatToken(null) ... } }
public class PlayerActivity extends Activity { private MediaSessionCompat mMediaSession; private MediaSessionConnector mMediaSessionConnector; private MediaManager mMediaManager; @Override protected void onCreate(Bundle savedInstanceState) { ... mMediaSession = new MediaSessionCompat(this, LOG_TAG); mMediaSessionConnector = new MediaSessionConnector(mMediaSession); ... } @Override protected void onStart() { ... mMediaManager = receiverContext.getMediaManager(); mMediaManager.setSessionCompatToken(currentMediaSession.getSessionToken()); mMediaSessionConnector.setPlayer(mExoPlayer); mMediaSessionConnector.setMediaMetadataProvider(mMediaMetadataProvider); mMediaSession.setActive(true); ... } @Override protected void onStop() { ... mMediaSessionConnector.setPlayer(null); mMediaSession.release(); mMediaManager.setSessionCompatToken(null); ... } }
发件人应用设置
启用 Cast Connect 支持
将发送器应用更新为支持 Cast Connect 后,您可以声明
通过设置
androidReceiverCompatible
开启举报
LaunchOptions
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 应用
将 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
来传递深层链接
加载请求的信息:
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
(例如,如果您要处理身份验证)
在两个平台上有所不同)。要解决此问题,您可以提供备用
entity
和 credentials
(适用于 Android TV):
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 接收器应用启动,它会在 Web Receiver 应用中使用 entity
和 credentials
加载请求。不过,如果您的 Android TV 应用已启动,SDK 将替换
entity
、credentials
以及您的atvEntity
和atvCredentials
(如果已指定)。
通过 Content ID 或 MediaQueueData 加载
如果您没有使用 entity
或 atvEntity
,但使用 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()
。
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 中,为了处理这些加载请求,您需要处理 intent 在 activity 生命周期回调中:
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,它会提取
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()
。
此外,由于投放队列
与“MediaSession
”队列不完全兼容。
class MyMediaCommandCallback : MediaCommandCallback() { override fun onSkipAd(requestData: RequestData?): Task<Void?> { // Skip your ad ... return Tasks.forResult(null) } } val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
public class MyMediaCommandCallback extends MediaCommandCallback { @Override public TaskonSkipAd(RequestData requestData) { // Skip your ad ... return Tasks.forResult(null); } } MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());
指定支持的媒体命令
与 Cast 接收器一样,您的 Android TV 应用应指定哪些命令
,因此发送方可以启用或停用某些界面控件。对于
作为
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 应用不支持更改播放速率,而 Web 接收器应用则支持, 您应在每个平台上正确设置支持的操作,并确保 您的发送者应用能否正确呈现界面
修改 MediaStatus
为了支持曲目、广告、直播和队列等高级功能,您的 Android 设备
TV 应用需要提供无法通过
MediaSession
。
我们提供
MediaStatusModifier
类来实现这一点。MediaStatusModifier
将始终在
您在“MediaSession
”中设置的
CastReceiverContext
。
创建并广播
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 应用 需要处理用户的凭据,以便 Google 可以 正确跟踪。
当发送方应用启动或加入您的 Android TV 应用时,您的发送方应用 应提供代表谁加入会话的凭据。
在发送者启动并加入您的 Android TV 应用之前,您可以指定 启动检查工具,看看发件人凭据是否允许使用。否则, Connect SDK 会回退到启动您的 Web 接收器。
发送者应用启动凭据数据
在发件人端,您可以指定 CredentialsData
来代表
加入会议。
credentials
是一个可由用户定义的字符串,只要您的 ATV
应用能够理解它。credentialsType
定义了
CredentialsData
来自自定义值或可以是自定义值。默认设置为
发送到目标平台
CredentialsData
仅在 Android TV 应用启动或
加入时间。如果您在联网时再次设置,系统不会将此代码传递给
Android TV 应用。如果发送者在关联期间切换个人资料,则你
可以继续参与会话,也可以调用
SessionManager.endCurrentCastSession(boolean stopCasting)
(如果您认为新配置文件与此会话不兼容)。
通过
CredentialsData
可以使用
getSenders
在
CastReceiverContext
以获取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 应用,或者用户信息不匹配。这种行为
在 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; }
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(); } }
解决 true
中的
LaunchRequestChecker
启动 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());