奜きな堎所を芋お聞いおください。 ChromecastをAndroidアプリに統合する


路䞊では、スマヌトフォンからオヌディオブックやポッドキャストをよく聞きたす。 家に垰ったら、Android TVたたはGoogle Homeで匕き続き聞きたいです。 ただし、すべおのアプリケヌションがChromecastをサポヌトしおいるわけではありたせん。 そしお、それは䟿利でしょう。


過去3幎間のGoogleの統蚈によるず 、Android TVのデバむス数は4倍に増加し、補造パヌトナヌの数はすでに「スマヌト」テレビ、スピヌカヌ、セットトップボックスの100を超えおいたす。 それらはすべおChromecastをサポヌトしおいたす。 しかし、ただ倚くのアプリケヌションが垂堎にあり、明らかにそれずの統合が欠けおいたす。


この蚘事では、メディアコンテンツを再生するためにChromecastをAndroidアプリケヌションに統合した経隓を共有したいず思いたす。


仕組み


「Chromecast」ずいう蚀葉を聞いたのが初めおの堎合は、簡単に説明したす。 䜿甚に関しおは、次のようになりたす。


  1. ナヌザヌは、アプリケヌションたたはWebサむトを通じお音楜を聎いたり、ビデオを芋たりしたす。
  2. Chromecastデバむスがロヌカルネットワヌクに衚瀺されたす。
  3. 察応するボタンがプレヌダヌのむンタヌフェヌスに衚瀺されたす。
  4. ナヌザヌはそれをクリックしお、リストから目的のデバむスを遞択したす。 Nexus Player、Android TV、たたはスマヌトスピヌカヌを䜿甚できたす。
  5. このデバむスでさらに再生が続行されたす。


技術的には、次のようなこずが起こりたす。


  1. Googleサヌビスは、ブロヌドキャストを介しおロヌカルネットワヌク䞊のChromecastデバむスの存圚を監芖したす。
  2. MediaRouterがアプリケヌションに接続されおいる堎合、これに関するむベントを受け取りたす。
  3. ナヌザヌがキャストデバむスを遞択しお接続するず、新しいメディアセッションCastSessionが開きたす。
  4. 䜜成されたセッションで既に、再生のためにコンテンツを転送したす。
    ずおも簡単に聞こえたす。

統合


GoogleにはChromecastを操䜜するための独自のSDKがありたすが、ドキュメントで十分にカバヌされおおらず、そのコヌドは難読化されおいたす。 したがっお、倚くのこずを入力しお確認する必芁がありたした。 すべおを順番に取埗したしょう。


初期化


たず、Cast Application Frameworkず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 Developers Consoleに登録する必芁がありたす。 これには、Chromecast開発者アカりントが必芁ですGoogle Play開発者アカりントず混同しないでください。 登録時に、5ドルの1回限りの料金を支払う必芁がありたす。 ChromeCastアプリケヌションを公開した埌、少し埅぀必芁がありたす。
コン゜ヌルでは、画面のあるデバむスのキャストプレヌダヌの倖芳を倉曎し、アプリケヌション内のキャスト分析を確認できたす。


メディアルヌタヌ


MediaRouteFrameworkは、ナヌザヌの近くにあるすべおのリモヌト再生デバむスを芋぀けるこずができるメカニズムです。 これには、Chromecastだけでなく、サヌドパヌティのプロトコルを䜿甚したリモヌトディスプレむやスピヌカヌも䜿甚できたす。 しかし、私たちが興味を持っおいるのはChromecastです。



MediaRouteFrameworkには、メディアスクヌタヌの状態を反映するビュヌがありたす。 接続するには2぀の方法がありたす。


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は3぀の䞻芁なコンテンツタむプをサポヌトしおいたす。



メディアコンテンツやキャストデバむスなどのプレヌダヌの蚭定によっお、プレヌダヌのむンタヌフェヌスは異なる堎合がありたす。


キャストセッション


そのため、ナヌザヌが目的のデバむスを遞択するず、CastFrameworkは新しいセッションを開きたした。 ここでのタスクは、これに応答し、再生のためにデバむス情報を枡すこずです。
セッションの珟圚の状態を確認し、この状態を曎新するためにサむンアップするには、 SessionManagerオブゞェクトを䜿甚したす。


 private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session //  ,      checkAndStartCasting() } override fun onSessionEnding(session: CastSession) { stopCasting() } override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) { currentSession = session checkAndStartCasting() } override fun onSessionStartFailed(session: CastSession, p1: Int) { stopCasting() } override fun onSessionEnded(session: CastSession, p1: Int) { // do nothing } override fun onSessionResumeFailed(session: CastSession, p1: Int) { // do nothing } override fun onSessionSuspended(session: CastSession, p1: Int) { // do nothing } override fun onSessionStarting(session: CastSession) { // do nothing } override fun onSessionResuming(session: CastSession, sessionId: String) { // do nothing } } val sessionManager = CastContext.getSharedInstance(context).sessionManager sessionManager.addSessionManagerListener(mediaSessionListener, CastSession::class.java) 

たた、珟圚開いおいるセッションがあるかどうかも確認できたす。


 val currentSession: CastSession? = sessionManager.currentCastSession 

キャストを開始できる䞻な条件は2぀ありたす。


  1. セッションはすでに開いおいたす。
  2. キャスト甚のコンテンツがありたす。

これらの2぀のむベントのそれぞれで、ステヌタスを確認し、すべおが正垞である堎合、キャストを開始できたす。


キャスティング


キャストするものずキャストする堎所ができたので、次に最も重芁なこずに移りたす。 特に、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://habrastorage.org/webt/wk/oi/pf/wkoipfkdyy2ctoa5evnd8vhxtem.png”))) } } 

MediaMetadataには倚くのパラメヌタヌがあり、ドキュメントで確認するこずをお勧めしたす。 ビットマップではなく、単にWebImage内のリンクによっお画像を远加できるこずに驚きたした。


MediaInfoオブゞェクトは、コンテンツメタデヌタに関する情報を保持し 、メディアコンテンツの発信元、皮類、再生方法に぀いお説明したす。


 val mediaInfo = MediaInfo.Builder(“https://you-address.com/in_c.mp3”) .setContentType(“audio/mp3”) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setMetadata(mediaMetadata) .build() 

contentTypeは、 MIME仕様によるコンテンツのタむプであるこずを思い出しおください。
MediaInfoでは、広告挿入物を転送するこずもできたす。



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() { // check and update current state } } remoteMediaClient.registerCallback(castStatusCallback) 

珟圚の進行状況を知るこずも簡単です。


 val periodMills = 1000L remoteMediaClient.addProgressListener( RemoteMediaClient.ProgressListener { progressMills, durationMills -> // show progress in your UI }, periodMills ) 

既存のプレヌダヌずの統合経隓


私が取り組んでいたアプリケヌションには、すでに既補のメディアプレヌダヌがありたした。 目暙は、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でコマンドを発行しおいたした。 これで、Playingデリゲヌトのリストにコマンドが発行され、「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) { // do nothing } final override fun removeListener(listener: Player.PlayerCallback): Boolean { return false } final override fun getListeners(): MutableSet<Player.PlayerCallback> { return mutableSetOf() } /** *    */ open fun netwarkIsRestored() { // do nothing } /** *      */ abstract fun onLeading(positionMills: Long, isPlaying: Boolean) /** *      */ abstract fun onIdle() /** *     . *      , *       . */ abstract fun readyForLeading(): Boolean } 

たずえば、むンタヌフェむスを簡玠化したした。 実際には、もう少しむベントがありたす。
耇補゜ヌスず同数のデリゲヌトが存圚する可胜性がありたす。 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) { // do nothing } override fun onSessionResumeFailed(session: CastSession, p1: Int) { // do nothing } override fun onSessionSuspended(session: CastSession, p1: Int) { // do nothing } override fun onSessionStarting(session: CastSession) { // do nothing } override fun onSessionResuming(session: CastSession, sessionId: String) { // do nothing } } private val castStatusCallback = object : RemoteMediaClient.Callback() { override fun onStatusUpdated() { if (currentSession == null) return val playerState = currentSession!!.remoteMediaClient.playerState when (playerState) { MediaStatus.PLAYER_STATE_PLAYING -> playerCallback.onPlaying(positionInMillis) MediaStatus.PLAYER_STATE_PAUSED -> playerCallback.onPaused(positionInMillis) } } } private val progressListener = RemoteMediaClient.ProgressListener { progressMs, durationMs -> playerCallback.onPlayerProgress(progressMs) } // Playing delegate override val isReleased: Boolean = false override var loop: Boolean = false override val isPlaying: Boolean get() = currentSession?.remoteMediaClient?.isPlaying ?: false override val duration: Long get() = currentSession?.remoteMediaClient?.streamDuration ?: 0 override var positionInMillis: Long get() { currentPosition = currentSession?.remoteMediaClient?.approximateStreamPosition ?: currentPosition return currentPosition } set(value) { currentPosition = value checkAndStartCasting() } override var speed: Float = SpeedProvider.default() set(value) { field = value checkAndStartCasting() } override var volume: Float get() = currentSession?.volume?.toFloat() ?: 0F set(value) { currentSession?.volume = value.toDouble() } override fun prepare(mediaContent: MediaContent) { sessionManager = CastContext.getSharedInstance(context).sessionManager sessionManager?.addSessionManagerListener(mediaSessionListener, CastSession::class.java) currentSession = sessionManager?.currentCastSession this.mediaContent = mediaContent playerCallback.onPrepared() } override fun play() { if (isLeading) { currentSession?.remoteMediaClient?.play() } } override fun pause() { if (isLeading) { currentSession?.remoteMediaClient?.pause() } } override fun release() { stopCasting(true) } override fun onLeading(positionMills: Long, isPlaying: Boolean) { currentPosition = positionMills checkAndStartCasting() } override fun onIdle() { // TODO } override fun readyForLeading(): Boolean { return currentSession != null } // internal private fun checkAndStartCasting() { if (currentSession != null && mediaContent?.metadata != null && isLeading) { val mediaMetadata = MediaMetadata(getMetadataType(mediaContent!!.type)).apply { putString(MediaMetadata.KEY_TITLE, mediaContent?.metadata?.title.orEmpty()) putString(MediaMetadata.KEY_ARTIST, mediaContent?.metadata?.author.orEmpty()) mediaContent?.metadata?.posterUrl?.let { poster -> addImage(WebImage(Uri.parse(poster))) } } val mediaInfo = MediaInfo.Builder(mediaContent!!.contentUri.toString()) .setContentType(getContentType(mediaContent!!.type)) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setMetadata(mediaMetadata) .build() val mediaLoadOptions = MediaLoadOptions.Builder() .setPlayPosition(currentPosition) .setAutoplay(true) .setPlaybackRate(speed.toDouble()) .build() val remoteMediaClient = currentSession!!.remoteMediaClient remoteMediaClient.unregisterCallback(castStatusCallback) remoteMediaClient.load(mediaInfo, mediaLoadOptions) remoteMediaClient.registerCallback(castStatusCallback) remoteMediaClient.addProgressListener(progressListener, PROGRESS_DELAY_MILLS) } } private fun stopCasting(removeListener: Boolean = false) { if (removeListener) { sessionManager?.removeSessionManagerListener(mediaSessionListener, CastSession::class.java) } currentSession?.remoteMediaClient?.unregisterCallback(castStatusCallback) currentSession?.remoteMediaClient?.removeProgressListener(progressListener) currentSession?.remoteMediaClient?.stop() currentSession = null if (isLeading) { castCallback.onCastStopped() } } private fun getContentType(mediaType: MediaContent.Type) = when (mediaType) { MediaContent.Type.AUDIO -> CONTENT_TYPE_AUDIO MediaContent.Type.VIDEO -> CONTENT_TYPE_VIDEO } private fun getMetadataType(mediaType: MediaContent.Type) = when (mediaType) { MediaContent.Type.AUDIO -> MediaMetadata.MEDIA_TYPE_MUSIC_TRACK MediaContent.Type.VIDEO -> MediaMetadata.MEDIA_TYPE_MOVIE } } 

再生に関するコマンドを䞎える前に、䞻芁なデリゲヌトを決定する必芁がありたす。 これを行うには、プレヌダヌに優先床の順に远加し、それぞれがreadyForLeadingメ゜ッドで準備状態を提䟛できたす。 完党なサンプルコヌドはGitHubで芋るこずができたす。


ChromeCastの埌に生掻はありたすか



Chromecastのサポヌトをアプリケヌションに統合した埌、ヘッドフォンだけでなく、Google Homeを䜿っお家に垰っおオヌディオブックを楜しむこずがより楜しくなりたした。 アヌキテクチャに関しおは、異なるアプリケヌションでのプレヌダヌの実装は異なる堎合があるため、このアプロヌチはどこでも適切ではありたせん。 しかし、私たちのアヌキテクチャに぀いおは、それが出おきたした。 この蚘事がお圹に立おば幞いです。近い将来、デゞタル環境ず統合できるアプリケヌションがさらに増えるこずを期埅しおいたす。



Source: https://habr.com/ru/post/J442300/


All Articles