
在大街上,我经常听智能手机上的有声读物和播客。 到家后,我想继续在Android TV或Google Home上收听它们。 但并非所有应用程序都支持Chromecast。 这样会很方便。
根据Google过去3年的统计 ,Android TV上的设备数量增加了4倍,制造合作伙伴的数量已经超过了100:“智能”电视,扬声器,机顶盒。 它们都支持Chromecast。 但是市场上仍然有许多应用显然缺乏与之集成。
在本文中,我想分享一下我将Chromecast集成到用于播放媒体内容的Android应用程序中的经验。
如何运作
如果这是您第一次听到“ Chromecast”一词,我会尝试简单地告诉您。 在用法方面,它看起来像这样:
- 用户通过应用程序或网站收听音乐或观看视频。
- Chromecast设备出现在本地网络上。
- 相应的按钮应出现在播放器的界面中。
- 通过单击它,用户可以从列表中选择所需的设备。 它可以是Nexus Player,Android TV或智能扬声器。
- 此设备继续播放。

从技术上讲,会发生以下情况:
- Google服务通过广播监视本地网络上Chromecast设备的存在。
- 如果MediaRouter已连接到您的应用程序,则您将收到有关此事件。
- 当用户选择Cast设备并连接到它时,将打开一个新的媒体会话(CastSession)。
- 在创建的会话中,我们将传输内容以进行回放。
听起来很简单。
整合性
Google拥有自己的Chromecast SDK ,但其文档覆盖率很低,并且其代码被混淆了。 因此,许多事情必须通过键入检查。 让我们整理一切。
初始化
首先,我们需要连接Cast应用程序框架和MediaRouter:
implementation "com.google.android.gms:play-services-cast-framework:16.1.0" implementation "androidx.mediarouter:mediarouter:1.0.0"
然后,Cast Framework应该获取应用程序标识符(稍后会详细介绍)以及支持的媒体内容的类型。 也就是说,如果我们的应用程序仅播放视频,则将无法投射到Google Home列,并且不在设备列表中。 为此,请创建OptionsProvider的实现:
class CastOptionsProvider: OptionsProvider { override fun getCastOptions(context: Context): CastOptions { return CastOptions.Builder() .setReceiverApplicationId(BuildConfig.CHROMECAST_APP_ID) .build() } override fun getAdditionalSessionProviders(context: Context): MutableList<SessionProvider>? { return null } }
并在清单中声明:
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="your.app.package.CastOptionsProvider" />
注册申请
为了使Chromecast与我们的应用一起使用,必须在Google Cast SDK开发者控制台中注册它。 这需要一个Chromecast开发者帐户(不要与Google Play开发者帐户混淆)。 注册时,您将需要支付5美元的一次性费用。 发布ChromeCast应用程序后,您需要稍等片刻。
在控制台中,您可以使用屏幕更改设备的 Cast播放器的外观,并查看应用程序中的 Cast分析。
MediaRouteFramework是一种允许您查找用户附近的所有远程播放设备的机制。 这不仅可以是Chromecast,也可以是使用第三方协议的远程显示器和扬声器。 但是让我们感兴趣的是Chromecast。

MediaRouteFramework具有一个可反映媒体滑板车状态的视图。 有两种连接方法:
1)通过菜单:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> ... <item android:id="@+id/menu_media_route" android:title="@string/cast" app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider" app:showAsAction="always"/> ... </menu>
2)通过布局:
<androidx.mediarouter.app.MediaRouteButton android:id="@+id/mediaRouteButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:mediaRouteTypes="user"/>
从代码中,您只需要在CastButtonFactory中注册按钮。 则媒体滑板车的当前状态将被抛出:
CastButtonFactory.setUpMediaRouteButton(applicationContext, view.mediaRouteButton)
现在已经注册了应用程序并配置了MediaRouter,您可以连接到ChromeCast设备并打开与它们的会话。
ChromeCast支持三种主要的内容类型:
根据播放器的设置(例如媒体内容和投射设备),播放器的界面可能会有所不同。
演员表
因此,用户选择了所需的设备,CastFramework打开了一个新会话。 现在,我们的任务是对此做出响应,并将信息传递给设备进行播放。
要查找会话的当前状态并注册以更新此状态,请使用SessionManager对象:
private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session
我们还可以了解当前是否有公开会议:
val currentSession: CastSession? = sessionManager.currentCastSession
我们有两个主要条件可以开始投射:
- 会话已经打开。
- 有铸造的内容。
在这两个事件的每一个中,我们都可以检查状态,如果一切正常,则可以开始投射。
铸件
现在我们有了要投射的东西和要投射的地方,我们可以继续进行最重要的事情。 除其他事项外,CastSession具有一个RemoteMediaClient对象,该对象负责媒体内容的播放状态。 我们将与他合作。
让我们创建MediaMetadata ,其中将存储有关作者,专辑等的信息,这与开始本地回放时传输到MediaSession的信息非常相似。
val mediaMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK ).apply { putString(MediaMetadata.KEY_TITLE, “In C”) putString(MediaMetadata.KEY_ARTIST, “Terry Riley”) mediaContent?.metadata?.posterUrl?.let { poster -> addImage(WebImage(Uri.parse(“https:
MediaMetadata有很多参数,最好在文档中查看它们。 我很惊讶您可以不通过位图而是仅通过WebImage内的链接来添加图像。
MediaInfo对象携带有关内容元数据的信息,并将讨论媒体内容来自何处,它是什么类型,如何播放:
val mediaInfo = MediaInfo.Builder(“https:
让我提醒您,根据MIME规范,contentType是一种内容类型。
您还可以将广告插入内容转移到MediaInfo:
- setAdBreakClips-接受AdBreakClipInfo广告的列表, 其中包含指向内容,标题,时间以及跳过广告的时间的链接。
- setAdBreaks-有关广告插入的布局和时间安排的信息。
在MediaLoadOptions中,我们描述了如何处理媒体流(速度,起始位置)。 该文档还说,您可以通过setCredentials传递请求标头以进行授权,但是我从Chromecast发出的请求中并未包含所请求的授权字段。
val mediaLoadOptions = MediaLoadOptions.Builder() .setPlayPosition(position!!) .setAutoplay(true) .setPlaybackRate(playbackSpeed) .setCredentials(context.getString(R.string.bearer_token, authGateway.authState.accessToken!!)) .setCredentialsType(context.getString(R.string.authorization_header_key)) .build()
一切准备就绪后,我们可以将所有数据提供给RemoteMediaClient,然后Chromecast将开始播放。 暂停本地播放很重要。
val remoteMediaClient = currentSession!!.remoteMediaClient remoteMediaClient.load(mediaInfo, mediaLoadOptions)
事件处理
视频开始播放,然后呢? 如果用户暂停电视怎么办? 要从Chromecast侧面了解事件,RemoteMediaClient具有回调:
private val castStatusCallback = object : RemoteMediaClient.Callback() { override fun onStatusUpdated() {
要知道当前的进度也很简单:
val periodMills = 1000L remoteMediaClient.addProgressListener( RemoteMediaClient.ProgressListener { progressMills, durationMills ->
与现有播放器集成的经验
我正在处理的应用程序已经有现成的媒体播放器。 目标是将Chromecast支持集成到其中。 媒体播放器基于状态机,首先想到的是添加一个新状态:“ CastingState”。 但是这个想法立即被拒绝了,因为每个播放器状态都反映了播放状态,并且什么用作ExoPlayer或ChromeCast的实现都无关紧要。
然后,提出了创建具有一定优先级并处理玩家“生命周期”的代表系统的想法。 所有代表都可以接收播放器状态事件:播放,暂停等。 -但只有首席代表才能播放媒体内容。

我们有这样的播放器界面:
interface Player { val isPlaying: Boolean val isReleased: Boolean val duration: Long var positionInMillis: Long var speed: Float var volume: Float var loop: Boolean fun addListener(listener: PlayerCallback) fun removeListener(listener: PlayerCallback): Boolean fun getListeners(): MutableSet<PlayerCallback> fun prepare(mediaContent: MediaContent) fun play() fun pause() fun release() interface PlayerCallback { fun onPlaying(currentPosition: Long) fun onPaused(currentPosition: Long) fun onPreparing() fun onPrepared() fun onLoadingChanged(isLoading: Boolean) fun onDurationChanged(duration: Long) fun onSetSpeed(speed: Float) fun onSeekTo(fromTimeInMillis: Long, toTimeInMillis: Long) fun onWaitingForNetwork() fun onError(error: String?) fun onReleased() fun onPlayerProgress(currentPosition: Long) } }
它的内部将是一个具有许多状态的状态机:
- 空-初始化之前的初始状态。
- 准备-播放器启动媒体内容的播放。
- 已准备-媒体已上传并可以播放。
- 玩
- 已暂停
- 等待网络
- 失误

以前,初始化期间的每个状态都会在ExoPlayer中发布命令。 现在,它将命令发布到“播放”委托人列表中,“ Lead”委托人将能够对其进行处理。 由于委托实现了播放器的所有功能,因此它也可以从播放器的界面继承,并在必要时单独使用。 然后,抽象委托将如下所示:
abstract class PlayingDelegate( protected val playerCallback: Player.PlayerCallback, var isLeading: Boolean = false ) : Player { fun setIsLeading(isLeading: Boolean, positionMills: Long, isPlaying: Boolean) { this.isLeading = isLeading if (isLeading) { onLeading(positionMills, isPlaying) } else { onDormant() } } final override fun addListener(listener: Player.PlayerCallback) {
例如,我简化了接口。 实际上,还有更多事件。
可以有与复制源一样多的代表。 Chromecast代表可能看起来像这样:
ChromeCastDelegate.kt class ChromeCastDelegate( private val context: Context, private val castCallback: ChromeCastListener, playerCallback: Player.PlayerCallback ) : PlayingDelegate(playerCallback) { companion object { private const val CONTENT_TYPE_VIDEO = "videos/mp4" private const val CONTENT_TYPE_AUDIO = "audio/mp3" private const val PROGRESS_DELAY_MILLS = 500L } interface ChromeCastListener { fun onCastStarted() fun onCastStopped() } private var sessionManager: SessionManager? = null private var currentSession: CastSession? = null private var mediaContent: MediaContent? = null private var currentPosition: Long = 0 private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session castCallback.onCastStarted() } override fun onSessionEnding(session: CastSession) { currentPosition = session.remoteMediaClient?.approximateStreamPosition ?: currentPosition stopCasting() } override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) { currentSession = session castCallback.onCastStarted() } override fun onSessionStartFailed(session: CastSession, p1: Int) { stopCasting() } override fun onSessionEnded(session: CastSession, p1: Int) {
在发出有关复制的命令之前,我们需要确定主要代表。 为此,将它们按优先级添加到播放器,并且它们每个都可以在readyForLeading()方法中给出其准备状态。 完整的示例代码可以在GitHub上找到 。
ChromeCast之后有生命吗

将Chromecast支持集成到应用程序后,我很高兴回到家,不仅可以通过耳机,而且可以通过Google Home欣赏有声读物。 至于体系结构,播放器在不同应用程序中的实现可能会有所不同,因此这种方法并不适合所有地方。 但是对于我们的体系结构,它来了。 我希望本文对您有所帮助,并且在不久的将来,将会有更多可以与数字环境集成的应用程序!