refactors design to flutter driven service core.
This commit is contained in:
+183
-158
@@ -3,36 +3,28 @@ package com.example.kryz_go_flutter
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaControllerCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import com.ryanheise.audioservice.AudioService
|
||||
|
||||
/**
|
||||
* MediaBrowserService for Android Auto.
|
||||
*
|
||||
* Android Auto connects to this service to browse content and control playback.
|
||||
*
|
||||
* Two playback modes:
|
||||
* 1. Cold start (Flutter not running): the service plays the stream directly via
|
||||
* MediaPlayer. This avoids the unreliable startActivity → Flutter bridge.
|
||||
* 2. Warm start (Flutter running): MainActivity registers play/stop callbacks so
|
||||
* Auto commands are forwarded through the method channel to just_audio.
|
||||
*
|
||||
* When the Flutter app opens while the service is playing, MainActivity stops the
|
||||
* service's MediaPlayer and Flutter takes over seamlessly.
|
||||
* Android Auto connects to this service to browse content and send transport
|
||||
* controls. Playback execution is delegated to Flutter's audio_service session.
|
||||
*
|
||||
* Brand color (#E36A18) is declared on this service via KryzAutoTheme in styles.xml
|
||||
* so Android Auto accents its media UI with the KRYZ orange.
|
||||
@@ -46,45 +38,51 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
private const val STREAM_URL = "https://kryz.out.airtime.pro/kryz_a"
|
||||
private const val NOTIFICATION_CHANNEL_ID = "com.kryzgoflutter.auto.channel"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
|
||||
private var instance: KryzAutoMediaBrowserService? = null
|
||||
|
||||
// Called by MainActivity to mirror Flutter playback state into the MediaSession.
|
||||
fun updatePlaybackState(isPlaying: Boolean, isBuffering: Boolean) {
|
||||
instance?.applyPlaybackState(isPlaying, isBuffering)
|
||||
}
|
||||
|
||||
// Called by MainActivity when Flutter is alive and taking over from the service player.
|
||||
fun stopServicePlayer() {
|
||||
instance?.stopServicePlayback()
|
||||
}
|
||||
|
||||
fun isServicePlaying(): Boolean {
|
||||
return try {
|
||||
instance?.serviceMediaPlayer?.isPlaying == true
|
||||
} catch (_: IllegalStateException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Flutter-side play/stop callbacks registered by MainActivity when Flutter is ready.
|
||||
private var directPlayCallback: (() -> Unit)? = null
|
||||
private var directStopCallback: (() -> Unit)? = null
|
||||
|
||||
fun registerDirectCallbacks(play: () -> Unit, stop: () -> Unit) {
|
||||
directPlayCallback = play
|
||||
directStopCallback = stop
|
||||
}
|
||||
|
||||
fun unregisterDirectCallbacks() {
|
||||
directPlayCallback = null
|
||||
directStopCallback = null
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var mediaSession: MediaSessionCompat
|
||||
private var serviceMediaPlayer: MediaPlayer? = null
|
||||
private var audioFocusRequest: AudioFocusRequest? = null
|
||||
private var audioServiceBrowser: MediaBrowserCompat? = null
|
||||
private var audioServiceController: MediaControllerCompat? = null
|
||||
private var pendingPlayRequest: Boolean = false
|
||||
private var pendingStopRequest: Boolean = false
|
||||
|
||||
private val mediaControllerCallback = object : MediaControllerCompat.Callback() {
|
||||
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
|
||||
applyPlaybackStateFromFlutter(state)
|
||||
}
|
||||
|
||||
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
|
||||
applyNowPlayingMetadataFromFlutter(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
private val browserConnectionCallback = object : MediaBrowserCompat.ConnectionCallback() {
|
||||
override fun onConnected() {
|
||||
val browser = audioServiceBrowser ?: return
|
||||
try {
|
||||
val controller = MediaControllerCompat(this@KryzAutoMediaBrowserService, browser.sessionToken)
|
||||
audioServiceController?.unregisterCallback(mediaControllerCallback)
|
||||
audioServiceController = controller
|
||||
controller.registerCallback(mediaControllerCallback)
|
||||
applyPlaybackStateFromFlutter(controller.playbackState)
|
||||
applyNowPlayingMetadataFromFlutter(controller.metadata)
|
||||
consumePendingTransportCommands()
|
||||
Log.d(TAG, "Connected to Flutter audio_service session")
|
||||
} catch (error: Exception) {
|
||||
Log.e(TAG, "Unable to bind media controller: ${error.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnectionSuspended() {
|
||||
Log.w(TAG, "audio_service browser connection suspended")
|
||||
disconnectFromFlutterAudioService()
|
||||
}
|
||||
|
||||
override fun onConnectionFailed() {
|
||||
Log.e(TAG, "audio_service browser connection failed")
|
||||
disconnectFromFlutterAudioService()
|
||||
}
|
||||
}
|
||||
|
||||
private val mediaSessionCallback = object : MediaSessionCompat.Callback() {
|
||||
override fun onPlay() {
|
||||
@@ -94,12 +92,7 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
|
||||
override fun onStop() {
|
||||
Log.d(TAG, "onStop()")
|
||||
val stop = directStopCallback
|
||||
if (stop != null) {
|
||||
stop()
|
||||
} else {
|
||||
stopServicePlayback()
|
||||
}
|
||||
dispatchStopCommand()
|
||||
}
|
||||
|
||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||
@@ -114,20 +107,11 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
|
||||
private fun triggerPlay() {
|
||||
val play = directPlayCallback
|
||||
if (play != null) {
|
||||
// Flutter is running — signal buffering then let Flutter handle audio.
|
||||
applyPlaybackState(isPlaying = false, isBuffering = true)
|
||||
play()
|
||||
} else {
|
||||
// Flutter is not running — play directly via MediaPlayer.
|
||||
startServicePlayback()
|
||||
}
|
||||
dispatchPlayCommand()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
Log.d(TAG, "onCreate()")
|
||||
createNotificationChannel()
|
||||
|
||||
@@ -154,6 +138,7 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
sessionToken = mediaSession.sessionToken
|
||||
Log.d(TAG, "MediaSession created, sessionToken set")
|
||||
connectToFlutterAudioService()
|
||||
}
|
||||
|
||||
override fun onGetRoot(
|
||||
@@ -187,78 +172,11 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "onDestroy()")
|
||||
stopServicePlayback()
|
||||
instance = null
|
||||
disconnectFromFlutterAudioService()
|
||||
if (::mediaSession.isInitialized) mediaSession.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
// Direct MediaPlayer playback used when Flutter is not running (cold Auto start).
|
||||
private fun startServicePlayback() {
|
||||
Log.d(TAG, "startServicePlayback()")
|
||||
stopServicePlayback()
|
||||
applyPlaybackState(isPlaying = false, isBuffering = true)
|
||||
startForeground(NOTIFICATION_ID, buildNotification(isPlaying = false))
|
||||
requestAudioFocus()
|
||||
|
||||
val player = MediaPlayer()
|
||||
try {
|
||||
player.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
)
|
||||
player.setDataSource(STREAM_URL)
|
||||
player.setOnPreparedListener { mp ->
|
||||
Log.d(TAG, "MediaPlayer prepared → starting")
|
||||
mp.start()
|
||||
applyPlaybackState(isPlaying = true, isBuffering = false)
|
||||
startForeground(NOTIFICATION_ID, buildNotification(isPlaying = true))
|
||||
}
|
||||
player.setOnInfoListener { _, what, _ ->
|
||||
when (what) {
|
||||
MediaPlayer.MEDIA_INFO_BUFFERING_START ->
|
||||
applyPlaybackState(isPlaying = false, isBuffering = true)
|
||||
MediaPlayer.MEDIA_INFO_BUFFERING_END ->
|
||||
applyPlaybackState(isPlaying = true, isBuffering = false)
|
||||
}
|
||||
true
|
||||
}
|
||||
player.setOnErrorListener { _, what, extra ->
|
||||
Log.e(TAG, "MediaPlayer error what=$what extra=$extra")
|
||||
applyPlaybackState(isPlaying = false, isBuffering = false)
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
true
|
||||
}
|
||||
player.prepareAsync()
|
||||
serviceMediaPlayer = player
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start service playback: ${e.message}")
|
||||
player.release()
|
||||
applyPlaybackState(isPlaying = false, isBuffering = false)
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopServicePlayback() {
|
||||
val player = serviceMediaPlayer ?: return
|
||||
Log.d(TAG, "stopServicePlayback()")
|
||||
serviceMediaPlayer = null
|
||||
try {
|
||||
if (player.isPlaying) player.stop()
|
||||
player.release()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error releasing MediaPlayer: ${e.message}")
|
||||
}
|
||||
abandonAudioFocus()
|
||||
applyPlaybackState(isPlaying = false, isBuffering = false)
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
private fun applyPlaybackState(isPlaying: Boolean, isBuffering: Boolean) {
|
||||
if (!::mediaSession.isInitialized) return
|
||||
val state = when {
|
||||
@@ -279,34 +197,66 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1f)
|
||||
.build()
|
||||
)
|
||||
startForeground(NOTIFICATION_ID, buildNotification(isPlaying = isPlaying, isBuffering = isBuffering))
|
||||
Log.d(TAG, "applyPlaybackState: playing=$isPlaying buffering=$isBuffering")
|
||||
}
|
||||
|
||||
private fun requestAudioFocus() {
|
||||
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val req = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
audioFocusRequest = req
|
||||
audioManager.requestAudioFocus(req)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
|
||||
private fun dispatchPlayCommand() {
|
||||
val controller = audioServiceController
|
||||
if (controller != null) {
|
||||
pendingPlayRequest = false
|
||||
pendingStopRequest = false
|
||||
applyPlaybackState(isPlaying = false, isBuffering = true)
|
||||
controller.transportControls.play()
|
||||
return
|
||||
}
|
||||
|
||||
pendingStopRequest = false
|
||||
pendingPlayRequest = true
|
||||
applyPlaybackState(isPlaying = false, isBuffering = true)
|
||||
connectToFlutterAudioService()
|
||||
}
|
||||
|
||||
private fun dispatchStopCommand() {
|
||||
val controller = audioServiceController
|
||||
if (controller != null) {
|
||||
pendingPlayRequest = false
|
||||
pendingStopRequest = false
|
||||
controller.transportControls.stop()
|
||||
return
|
||||
}
|
||||
|
||||
pendingPlayRequest = false
|
||||
pendingStopRequest = true
|
||||
applyPlaybackState(isPlaying = false, isBuffering = false)
|
||||
connectToFlutterAudioService()
|
||||
}
|
||||
|
||||
private fun consumePendingTransportCommands() {
|
||||
val controller = audioServiceController ?: return
|
||||
|
||||
if (pendingStopRequest) {
|
||||
pendingStopRequest = false
|
||||
controller.transportControls.stop()
|
||||
}
|
||||
|
||||
if (pendingPlayRequest) {
|
||||
pendingPlayRequest = false
|
||||
controller.transportControls.play()
|
||||
}
|
||||
}
|
||||
|
||||
private fun abandonAudioFocus() {
|
||||
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
audioFocusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
|
||||
audioFocusRequest = null
|
||||
private fun applyPlaybackStateFromFlutter(state: PlaybackStateCompat?) {
|
||||
if (state == null) {
|
||||
applyPlaybackState(isPlaying = false, isBuffering = false)
|
||||
return
|
||||
}
|
||||
|
||||
val isPlaying = state.state == PlaybackStateCompat.STATE_PLAYING
|
||||
val isBuffering = state.state == PlaybackStateCompat.STATE_BUFFERING ||
|
||||
state.state == PlaybackStateCompat.STATE_CONNECTING
|
||||
|
||||
applyPlaybackState(isPlaying = isPlaying, isBuffering = isBuffering)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
@@ -324,16 +274,91 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(isPlaying: Boolean): Notification {
|
||||
private fun buildNotification(isPlaying: Boolean, isBuffering: Boolean): Notification {
|
||||
val statusText = when {
|
||||
isPlaying -> "Playing from Flutter audio handler"
|
||||
isBuffering -> "Connecting via Flutter audio handler..."
|
||||
else -> "Stopped"
|
||||
}
|
||||
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle("KRYZ Radio")
|
||||
.setContentText(if (isPlaying) "Playing via Android Auto" else "Connecting…")
|
||||
.setContentText(statusText)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setOngoing(isPlaying)
|
||||
.setOngoing(isPlaying || isBuffering)
|
||||
.setSilent(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun applyNowPlayingMetadata(
|
||||
title: String?,
|
||||
subtitle: String?,
|
||||
album: String?,
|
||||
mediaUri: String?,
|
||||
) {
|
||||
if (!::mediaSession.isInitialized) return
|
||||
|
||||
val iconUri = Uri.parse("android.resource://${packageName}/mipmap/ic_launcher")
|
||||
mediaSession.setMetadata(
|
||||
MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title ?: "KRYZ Live Stream")
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, subtitle ?: "KRYZ Radio")
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album ?: "Live")
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, MEDIA_ID_LIVE)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, mediaUri ?: STREAM_URL)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, iconUri.toString())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, iconUri.toString())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyNowPlayingMetadataFromFlutter(metadata: MediaMetadataCompat?) {
|
||||
if (metadata == null) {
|
||||
applyNowPlayingMetadata(
|
||||
title = "KRYZ Live Stream",
|
||||
subtitle = "KRYZ Radio",
|
||||
album = "Live",
|
||||
mediaUri = STREAM_URL,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
applyNowPlayingMetadata(
|
||||
title = metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE),
|
||||
subtitle = metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST),
|
||||
album = metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM),
|
||||
mediaUri = metadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI),
|
||||
)
|
||||
}
|
||||
|
||||
private fun connectToFlutterAudioService() {
|
||||
val existing = audioServiceBrowser
|
||||
if (existing?.isConnected == true) {
|
||||
return
|
||||
}
|
||||
|
||||
val browser = MediaBrowserCompat(
|
||||
this,
|
||||
ComponentName(this, AudioService::class.java),
|
||||
browserConnectionCallback,
|
||||
null,
|
||||
)
|
||||
|
||||
audioServiceBrowser = browser
|
||||
browser.connect()
|
||||
}
|
||||
|
||||
private fun disconnectFromFlutterAudioService() {
|
||||
audioServiceController?.unregisterCallback(mediaControllerCallback)
|
||||
audioServiceController = null
|
||||
|
||||
audioServiceBrowser?.let { browser ->
|
||||
if (browser.isConnected) {
|
||||
browser.disconnect()
|
||||
}
|
||||
}
|
||||
audioServiceBrowser = null
|
||||
}
|
||||
|
||||
private fun buildLiveStreamMetadata(): MediaMetadataCompat {
|
||||
val iconUri = Uri.parse("android.resource://${packageName}/mipmap/ic_launcher")
|
||||
return MediaMetadataCompat.Builder()
|
||||
|
||||
@@ -27,7 +27,6 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
companion object {
|
||||
private const val METHOD_CHANNEL = "kryz_go_flutter/casting/methods"
|
||||
private const val EVENT_CHANNEL = "kryz_go_flutter/casting/events"
|
||||
private const val PLAYBACK_METHOD_CHANNEL = "kryz_go_flutter/playback/methods"
|
||||
}
|
||||
|
||||
private var castContext: CastContext? = null
|
||||
@@ -35,9 +34,6 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
private var chooserDialog: MediaRouteChooserDialog? = null
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var activeRemoteMediaClient: RemoteMediaClient? = null
|
||||
private var playbackMethodChannel: MethodChannel? = null
|
||||
private var flutterReady = false
|
||||
private var pendingAutoPlay = false
|
||||
|
||||
private var statusPayload: MutableMap<String, Any?> = mutableMapOf(
|
||||
"status" to "idle",
|
||||
@@ -106,38 +102,6 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
|
||||
.setStreamHandler(this)
|
||||
|
||||
playbackMethodChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
PLAYBACK_METHOD_CHANNEL,
|
||||
)
|
||||
|
||||
playbackMethodChannel?.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"flutterReady" -> {
|
||||
flutterReady = true
|
||||
// Stop the service's MediaPlayer and hand off to Flutter.
|
||||
val wasServicePlaying = KryzAutoMediaBrowserService.isServicePlaying()
|
||||
KryzAutoMediaBrowserService.stopServicePlayer()
|
||||
KryzAutoMediaBrowserService.registerDirectCallbacks(
|
||||
play = { mainHandler.post { invokeFlutterPlay() } },
|
||||
stop = { mainHandler.post { playbackMethodChannel?.invokeMethod("stop", null) } },
|
||||
)
|
||||
if (pendingAutoPlay || wasServicePlaying) {
|
||||
pendingAutoPlay = false
|
||||
invokeFlutterPlay()
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"updatePlaybackState" -> {
|
||||
val isPlaying = call.argument<Boolean>("isPlaying") ?: false
|
||||
val isBuffering = call.argument<Boolean>("isBuffering") ?: false
|
||||
KryzAutoMediaBrowserService.updatePlaybackState(isPlaying, isBuffering)
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
initializeCastContext()
|
||||
}
|
||||
|
||||
@@ -181,8 +145,6 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
chooserDialog?.dismiss()
|
||||
chooserDialog = null
|
||||
|
||||
KryzAutoMediaBrowserService.unregisterDirectCallbacks()
|
||||
|
||||
castContext?.sessionManager?.removeSessionManagerListener(
|
||||
castSessionManagerListener,
|
||||
CastSession::class.java,
|
||||
@@ -192,16 +154,6 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun invokeFlutterPlay() {
|
||||
if (!flutterReady) {
|
||||
pendingAutoPlay = true
|
||||
return
|
||||
}
|
||||
mainHandler.post {
|
||||
playbackMethodChannel?.invokeMethod("play", null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeCastContext() {
|
||||
try {
|
||||
castContext = CastContext.getSharedInstance(this)
|
||||
|
||||
Reference in New Issue
Block a user