Ich schaue und höre, wo ich will. Integration von Chromecast in eine Android App


Auf der Straße höre ich oft Hörbücher und Podcasts von einem Smartphone. Wenn ich nach Hause komme, möchte ich sie weiterhin auf Android TV oder Google Home hören. Aber nicht alle Anwendungen unterstützen Chromecast. Und es wäre bequem.


Laut Google- Statistiken hat sich die Anzahl der Geräte auf Android TV in den letzten drei Jahren vervierfacht, und die Anzahl der Herstellungspartner hat bereits hundert überschritten: „intelligente“ Fernseher, Lautsprecher, Set-Top-Boxen. Alle unterstützen Chromecast. Es gibt jedoch immer noch viele Anwendungen auf dem Markt, die offensichtlich nicht integriert sind.


In diesem Artikel möchte ich meine Erfahrungen mit der Integration von Chromecast in eine Android-Anwendung zum Abspielen von Medieninhalten teilen.


Wie funktioniert es?


Wenn Sie das Wort "Chromecast" zum ersten Mal hören, werde ich versuchen, es Ihnen kurz zu sagen. In Bezug auf die Nutzung sieht es ungefähr so ​​aus:


  1. Der Benutzer hört Musik oder sieht sich Videos über eine Anwendung oder Website an.
  2. Ein Chromecast-Gerät wird im lokalen Netzwerk angezeigt.
  3. Eine entsprechende Schaltfläche sollte in der Benutzeroberfläche des Players angezeigt werden.
  4. Durch Klicken darauf wählt der Benutzer das gewünschte Gerät aus der Liste aus. Dies kann ein Nexus Player, Android TV oder ein intelligenter Lautsprecher sein.
  5. Die weitere Wiedergabe wird mit diesem Gerät fortgesetzt.


Technisch gesehen passiert so etwas wie das Folgende:


  1. Google Services überwacht das Vorhandensein von Chromecast-Geräten im lokalen Netzwerk über Broadcasting.
  2. Wenn MediaRouter mit Ihrer Anwendung verbunden ist, erhalten Sie eine entsprechende Veranstaltung.
  3. Wenn ein Benutzer ein Cast-Gerät auswählt und eine Verbindung zu diesem herstellt, wird eine neue Mediensitzung (CastSession) geöffnet.
  4. Bereits in der erstellten Sitzung übertragen wir Inhalte zur Wiedergabe.
    Es klingt sehr einfach.

Integration


Google verfügt über ein eigenes Chromecast SDK , das jedoch nur unzureichend dokumentiert ist und dessen Code verschleiert ist. Daher mussten viele Dinge durch Eingabe überprüft werden. Lassen Sie uns alles in Ordnung bringen.


Initialisierung


Zuerst müssen wir das Cast Application Framework und MediaRouter verbinden:


implementation "com.google.android.gms:play-services-cast-framework:16.1.0" implementation "androidx.mediarouter:mediarouter:1.0.0" 

Dann sollte das Cast Framework die Anwendungskennung (dazu später mehr) und die Arten des unterstützten Medieninhalts erhalten. Wenn unsere Anwendung nur Videos wiedergibt, ist ein Casting in die Google Home-Spalte nicht möglich und nicht in der Liste der Geräte enthalten. Erstellen Sie dazu eine Implementierung von 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 } } 

Und erkläre es im Manifest:


 <meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="your.app.package.CastOptionsProvider" /> 

Registrieren Sie die Anwendung


Damit Chromecast mit unserer Anwendung funktioniert, muss sie in der Google Cast SDK Developers Console registriert sein. Dies erfordert ein Chromecast-Entwicklerkonto (nicht zu verwechseln mit einem Google Play-Entwicklerkonto). Bei der Registrierung müssen Sie eine einmalige Gebühr von 5 USD zahlen. Nach dem Veröffentlichen der ChromeCast-Anwendung müssen Sie etwas warten.
In der Konsole können Sie das Erscheinungsbild des Cast-Players für Geräte mit einem Bildschirm ändern und die Casting-Analyse in der Anwendung anzeigen.


Medienrouter


MediaRouteFramework ist ein Mechanismus, mit dem Sie alle Remote-Wiedergabegeräte in der Nähe des Benutzers finden können. Dies kann nicht nur Chromecast sein, sondern auch Remote-Displays und Lautsprecher, die Protokolle von Drittanbietern verwenden. Was uns aber interessiert, ist Chromecast.



MediaRouteFramework verfügt über eine Ansicht, die den Status des Media Scooters widerspiegelt. Es gibt zwei Möglichkeiten, es zu verbinden:


1) Über das Menü:


 <?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) Über Layout:


 <androidx.mediarouter.app.MediaRouteButton android:id="@+id/mediaRouteButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:mediaRouteTypes="user"/> 

Und vom Code aus müssen Sie nur die Schaltfläche in CastButtonFactory registrieren. dann wird der aktuelle Status des Medienrollers hineingeworfen:


 CastButtonFactory.setUpMediaRouteButton(applicationContext, view.mediaRouteButton) 

Nachdem die Anwendung registriert und der MediaRouter konfiguriert wurde, können Sie eine Verbindung zu ChromeCast-Geräten herstellen und Sitzungen für diese öffnen.


Casting von Medieninhalten


ChromeCast unterstützt drei Haupttypen von Inhalten:


  • Audio
  • Video
  • Foto

Abhängig von den Einstellungen des Players, z. B. Medieninhalt und Besetzungsgerät, kann die Benutzeroberfläche des Players variieren.


Castsession


Also wählte der Benutzer das gewünschte Gerät aus, CastFramework eröffnete eine neue Sitzung. Jetzt besteht unsere Aufgabe darin, darauf zu reagieren und Informationen zur Wiedergabe an das Gerät weiterzuleiten.
Verwenden Sie das SessionManager- Objekt, um den aktuellen Status der Sitzung zu ermitteln und sich anzumelden , um diesen Status zu aktualisieren:


 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) 

Und wir können auch herausfinden, ob es momentan eine offene Sitzung gibt:


 val currentSession: CastSession? = sessionManager.currentCastSession 

Wir haben zwei Hauptbedingungen, unter denen wir mit dem Casting beginnen können:


  1. Die Sitzung ist bereits geöffnet.
  2. Es gibt Inhalte zum Casting.

Bei jedem dieser beiden Events können wir den Status überprüfen. Wenn alles in Ordnung ist, beginnen wir mit dem Casting.


Casting


Jetzt, da wir wissen, was wir besetzen sollen und wo wir es besetzen sollen, können wir zum Wichtigsten übergehen. CastSession verfügt unter anderem über ein RemoteMediaClient- Objekt, das für den Wiedergabestatus von Medieninhalten verantwortlich ist. Wir werden mit ihm arbeiten.


Lassen Sie uns MediaMetadata erstellen, in denen Informationen über den Autor, das Album usw. gespeichert werden. Es ist sehr ähnlich zu dem, was wir zu MediaSession übertragen, wenn wir die lokale Wiedergabe starten.


 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 hat viele Parameter, und es ist besser, sie in der Dokumentation zu sehen. Ich war angenehm überrascht, dass Sie ein Bild nicht über eine Bitmap, sondern einfach über einen Link in WebImage hinzufügen können.


Das MediaInfo- Objekt enthält Informationen zu den Inhaltsmetadaten und gibt Auskunft darüber, woher der Medieninhalt stammt, welcher Typ er ist und wie er abgespielt wird:


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

Ich möchte Sie daran erinnern, dass contentType eine Art von Inhalt gemäß der MIME- Spezifikation ist.
Sie können Werbebeilagen auch an MediaInfo übertragen:


  • setAdBreakClips - Akzeptiert eine Liste von AdBreakClipInfo- Werbespots mit Links zu Inhalt, Titel, Timing und der Zeit, in der die Anzeige übersprungen wird.
  • setAdBreaks - Informationen zum Layout und Timing von Werbebeilagen.

In MediaLoadOptions beschreiben wir, wie wir den Medienstrom verarbeiten (Geschwindigkeit, Startposition). In der Dokumentation heißt es außerdem, dass Sie über setCredentials den Anforderungsheader zur Autorisierung übergeben können, meine Anforderungen von Chromecast jedoch nicht die angeforderten Autorisierungsfelder enthielten.


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

Sobald alles fertig ist, können wir alle Daten an RemoteMediaClient weitergeben, und Chromecast wird abgespielt. Es ist wichtig, die lokale Wiedergabe anzuhalten.


 val remoteMediaClient = currentSession!!.remoteMediaClient remoteMediaClient.load(mediaInfo, mediaLoadOptions) 

Ereignisbehandlung


Das Video wurde abgespielt und was dann? Was ist, wenn der Benutzer den Fernseher anhält? Um mehr über Ereignisse von der Seite von Chromecast zu erfahren, verfügt RemoteMediaClient über Rückrufe:


 private val castStatusCallback = object : RemoteMediaClient.Callback() { override fun onStatusUpdated() { // check and update current state } } remoteMediaClient.registerCallback(castStatusCallback) 

Den aktuellen Fortschritt zu kennen ist auch einfach:


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

Erfahrung in der Integration mit einem vorhandenen Spieler


Die Anwendung, an der ich arbeitete, hatte bereits einen vorgefertigten Media Player. Ziel war es, die Chromecast-Unterstützung zu integrieren. Der Media Player basierte auf der State Machine, und der erste Gedanke war, einen neuen Status hinzuzufügen: „CastingState“. Diese Idee wurde jedoch sofort abgelehnt, da jeder Player-Status den Wiedergabestatus widerspiegelt und es keine Rolle spielt, was als Implementierung von ExoPlayer oder ChromeCast dient.
Dann kam die Idee auf, ein bestimmtes System von Delegierten mit Priorisierung und Verarbeitung des „Lebenszyklus“ des Spielers zu schaffen. Alle Teilnehmer können Spielerstatusereignisse empfangen: Wiedergabe, Pause usw. - aber nur der Hauptdelegierte spielt den Medieninhalt ab.



Wir haben so etwas wie diese Player-Oberfläche:


 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) } } 

Im Inneren wird es eine Zustandsmaschine mit so vielen Zuständen geben:


  • Leer - Ausgangszustand vor der Initialisierung.
  • Vorbereiten - Der Player startet die Wiedergabe von Medieninhalten.
  • Vorbereitet - Medien werden hochgeladen und können abgespielt werden.
  • Spielen
  • Pause
  • Warten auf Netzwerk
  • Fehler


Zuvor gab jeder Status während der Initialisierung einen Befehl in ExoPlayer aus. Jetzt wird der Befehl an die Liste der spielenden Delegierten ausgegeben, und der "Lead" -Delegierte kann ihn verarbeiten. Da der Delegat alle Funktionen des Players implementiert, kann er auch von der Benutzeroberfläche des Players geerbt und bei Bedarf separat verwendet werden. Dann sieht der abstrakte Delegierte folgendermaßen aus:


 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 } 

Zum Beispiel habe ich die Schnittstellen vereinfacht. In Wirklichkeit gibt es ein bisschen mehr Ereignisse.
Es kann so viele Delegierte geben, wie es Reproduktionsquellen gibt. Ein Chromecast-Delegat könnte ungefähr so ​​aussehen:


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 } } 

Bevor wir einen Befehl zur Reproduktion erteilen, müssen wir uns für den führenden Delegierten entscheiden. Dazu werden sie dem Spieler in der Reihenfolge ihrer Priorität hinzugefügt, und jeder von ihnen kann seinen Bereitschaftszustand in der readyForLeading () -Methode angeben. Der vollständige Beispielcode ist auf GitHub zu sehen.


Gibt es ein Leben nach ChromeCast?



Nachdem ich die Chromecast-Unterstützung in die Anwendung integriert hatte, war ich mehr erfreut, nach Hause zu kommen und Hörbücher nicht nur über die Kopfhörer, sondern auch über Google Home zu genießen. In Bezug auf die Architektur kann die Implementierung von Playern in verschiedenen Anwendungen variieren, sodass dieser Ansatz nicht überall angemessen ist. Aber für unsere Architektur kam es auf. Ich hoffe, dieser Artikel war nützlich und in naher Zukunft wird es weitere Anwendungen geben, die sich in die digitale Umgebung integrieren lassen!

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


All Articles