Skip to content

Commit

Permalink
Merge pull request #213 from Dark25/feat(cast)-Improve-expanded-contr…
Browse files Browse the repository at this point in the history
…ols-style-and-queue-handling-

feat(cast): Improve expanded controls style and queue handling
  • Loading branch information
Dark25 authored Jan 31, 2025
2 parents 6afd643 + 0dfc8ac commit 20c59b4
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 68 deletions.
10 changes: 9 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,15 @@
</activity>


<activity android:name=".ui.player.cast.ExpandedControlsActivity"/>
<activity android:name=".ui.player.cast.ExpandedControlsActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>


<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
Expand Down
46 changes: 34 additions & 12 deletions app/src/main/java/eu/kanade/tachiyomi/ui/player/CastManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.ui.player

import android.annotation.SuppressLint
import android.content.Context
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.viewModelScope
import com.google.android.gms.cast.MediaLoadRequestData
import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.common.ConnectionResult
Expand Down Expand Up @@ -33,7 +35,7 @@ class CastManager(
})
private val player by lazy { activity.player }

private var castContext: CastContext? = null
var castContext: CastContext? = null
private var castSession: CastSession? = null
private var sessionListener: CastSessionListener? = null
private val mediaBuilder = CastMediaBuilder(viewModel, activity)
Expand Down Expand Up @@ -90,7 +92,11 @@ class CastManager(
dialog.dismiss()
loadRemoteMediaWithState()
}
.setOnCancelListener { endSession() }
.setCancelable(false)
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
endSession()
}
.show()
}
}
Expand All @@ -103,22 +109,38 @@ class CastManager(
@SuppressLint("SuspiciousIndentation")
private fun loadRemoteMedia() {
if (!isCastApiAvailable) return

val remoteMediaClient = castSession?.remoteMediaClient ?: return

try {
val mediaInfo = mediaBuilder.buildMediaInfo()
remoteMediaClient.load(
MediaLoadRequestData.Builder()
.setMediaInfo(mediaInfo)
.setAutoplay(autoplayEnabled)
.setCurrentTime((player.timePos ?: 0).toLong() * 1000)
.build(),
)
_castState.value = CastState.CONNECTED
val selectedIndex = viewModel.selectedVideoIndex.value
val mediaInfo = mediaBuilder.buildMediaInfo(selectedIndex)
// si ya hay un video colocado entonce agregar a la cola
if (remoteMediaClient.mediaQueue.itemCount > 0) {
remoteMediaClient.queueAppendItem(
MediaQueueItem.Builder(mediaInfo).build(),
null,
)

activity.runOnUiThread {
Toast.makeText(context, "Video agregado a la cola", Toast.LENGTH_SHORT).show()
}
} else {
// Iniciar nueva reproducción
remoteMediaClient.load(
MediaLoadRequestData.Builder()
.setMediaInfo(mediaInfo)
.setAutoplay(autoplayEnabled)
.setCurrentTime((player.timePos ?: 0).toLong() * 1000)
.build(),
)
_castState.value = CastState.CONNECTED
}
} catch (e: Exception) {
_castState.value = CastState.DISCONNECTED
logcat(LogPriority.ERROR, e)
activity.runOnUiThread {
Toast.makeText(context, "Error al cargar el video", Toast.LENGTH_SHORT).show()
}
}
}

Expand Down
24 changes: 11 additions & 13 deletions app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ class PlayerActivity : BaseActivity() {
private val storageManager: StorageManager = Injekt.get()

// Cast -->
private lateinit var castManager: CastManager
val castManager: CastManager by lazy {
CastManager(context = this, activity = this)
}
// <-- Cast

private var audioFocusRequest: AudioFocusRequestCompat? = null
private var restoreAudioFocus: () -> Unit = {}
Expand Down Expand Up @@ -260,6 +263,9 @@ class PlayerActivity : BaseActivity() {

// <-- AM (DISCORD)
}
// Cast -->
castManager
// <-- Cast

binding.controls.setContent {
TachiyomiTheme {
Expand Down Expand Up @@ -287,10 +293,6 @@ class PlayerActivity : BaseActivity() {
}
}

// Cast -->
castManager = CastManager(this, this)
// <-- Cast

onNewIntent(this.intent)
}

Expand Down Expand Up @@ -398,14 +400,10 @@ class PlayerActivity : BaseActivity() {
updateDiscordRPC(exitingPlayer = false)

castManager.apply {
// Registrar listener de sesión Cast
registerSessionListener()

// Actualizar estado actual de Cast
if (castState.value == CastManager.CastState.CONNECTED) {
updateCastState(CastManager.CastState.CONNECTED)
}
// Sincronizar estado inicial con ViewModel
viewModel.isCasting.value = castState.value == CastManager.CastState.CONNECTED
}
}
Expand Down Expand Up @@ -622,15 +620,14 @@ class PlayerActivity : BaseActivity() {
}

override fun onResume() {
// Cast -->
castManager.apply {
// Actualizar contexto Cast después de pausas cortas
refreshCastContext()

// Si está en modo Cast, sincronizar controles UI
if (castState.value == CastManager.CastState.CONNECTED) {
updateCastState(CastManager.CastState.CONNECTED)
}
}
// <-- Cast
super.onResume()

viewModel.currentVolume.update {
Expand Down Expand Up @@ -896,8 +893,8 @@ class PlayerActivity : BaseActivity() {
}

override fun onPause() {
// Cast -->
castManager.apply {
// Liberar recursos solo si no está en PiP
if (!isInPictureInPictureMode) {
unregisterSessionListener()
}
Expand All @@ -907,6 +904,7 @@ class PlayerActivity : BaseActivity() {
maintainCastSessionBackground()
}
}
//
when (playAction) {
SingleActionGesture.None -> {}
SingleActionGesture.Seek -> {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import eu.kanade.tachiyomi.ui.player.PlayerViewModel
class CastMediaBuilder(private val viewModel: PlayerViewModel, private val activity: PlayerActivity) {
private val player by lazy { activity.player }

fun buildMediaInfo(): MediaInfo {
val currentVideo = viewModel.videoList.value.getOrNull(viewModel.selectedVideoIndex.value)
?: throw IllegalStateException("Invalid video selection")
fun buildMediaInfo(index: Int): MediaInfo {
val video = viewModel.videoList.value.getOrNull(index)
?: throw IllegalStateException("Invalid video index: $index")

return MediaInfo.Builder(currentVideo.videoUrl!!)
return MediaInfo.Builder(video.videoUrl!!)
.setContentType("video/mp4")
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.apply { addMetadata() }
.apply { addTracks() }
.apply { addTracks(index) }
.setStreamDuration((player.duration ?: 0).toLong() * 1000)
.build()
}
Expand All @@ -33,44 +33,32 @@ class CastMediaBuilder(private val viewModel: PlayerViewModel, private val activ
return setMetadata(metadata)
}

private fun MediaInfo.Builder.addTracks(): MediaInfo.Builder {
addSubtitlesToCast(this)
buildAudioTracks(this)
return this
}

private fun addSubtitlesToCast(mediaInfoBuilder: MediaInfo.Builder) {
val subtitleTracks = viewModel.videoList.value
.getOrNull(viewModel.selectedVideoIndex.value)
?.subtitleTracks
?.takeIf { it.isNotEmpty() }
private fun addSubtitlesToCast(index: Int): List<MediaTrack> {
val subtitleTracks = viewModel.videoList.value[index].subtitleTracks

subtitleTracks?.let { subs ->
val mediaTracks = subs.mapIndexed { index, sub ->
MediaTrack.Builder(index.toLong(), MediaTrack.TYPE_TEXT)
.setContentId(sub.url)
.setSubtype(MediaTrack.SUBTYPE_SUBTITLES)
.setName(sub.lang)
.build()
}
mediaInfoBuilder.setMediaTracks(mediaTracks)
return subtitleTracks.mapIndexed { trackIndex, sub ->
MediaTrack.Builder(trackIndex.toLong(), MediaTrack.TYPE_TEXT)
.setContentId(sub.url)
.setSubtype(MediaTrack.SUBTYPE_SUBTITLES)
.setName(sub.lang)
.build()
}
}

private fun buildAudioTracks(mediaInfoBuilder: MediaInfo.Builder) {
val audioTracks = viewModel.videoList.value
.getOrNull(viewModel.selectedVideoIndex.value)
?.audioTracks
?.takeIf { it.isNotEmpty() }
private fun buildAudioTracks(index: Int): List<MediaTrack> {
val audioTracks = viewModel.videoList.value[index].audioTracks

audioTracks?.let { tracks ->
val mediaTracks = tracks.mapIndexed { index, audio ->
MediaTrack.Builder(index.toLong(), MediaTrack.TYPE_AUDIO)
.setContentId(audio.url)
.setName(audio.lang)
.build()
}
mediaInfoBuilder.setMediaTracks(mediaTracks)
return audioTracks.mapIndexed { trackIndex, audio ->
MediaTrack.Builder(trackIndex.toLong(), MediaTrack.TYPE_AUDIO)
.setContentId(audio.url)
.setName(audio.lang)
.build()
}
}

private fun MediaInfo.Builder.addTracks(index: Int): MediaInfo.Builder {
val subtitleTracks = addSubtitlesToCast(index)
val audioTracks = buildAudioTracks(index)
return setMediaTracks(subtitleTracks + audioTracks)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package eu.kanade.tachiyomi.ui.player.cast

import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.media.widget.MiniControllerFragment
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.PlayerActivity

@Composable
fun CastMiniController(
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
var castState by remember { mutableIntStateOf(CastState.NO_DEVICES_AVAILABLE) }
val playerActivity = context as? PlayerActivity

LaunchedEffect(playerActivity) {
val castContext = playerActivity?.castManager?.castContext ?: return@LaunchedEffect
// Update state with current value and listen for changes
castState = castContext.castState
castContext.addCastStateListener { state ->
castState = state
}
}

if (castState == CastState.CONNECTED) {
AndroidView(
factory = { context ->
FragmentContainerView(context).apply {
id = R.id.castMiniController
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
}
},
update = { view ->
val fragment = MiniControllerFragment()
val fragmentManager = (context as FragmentActivity).supportFragmentManager
fragmentManager.beginTransaction()
.replace(view.id, fragment)
.commitNowAllowingStateLoss()
},
modifier = modifier,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import com.google.android.gms.cast.framework.media.MediaIntentReceiver
import com.google.android.gms.cast.framework.media.NotificationAction
import com.google.android.gms.cast.framework.media.NotificationActionsProvider
import com.google.android.gms.cast.framework.media.NotificationOptions
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
import com.google.android.gms.common.images.WebImage
import eu.kanade.tachiyomi.R

Expand Down Expand Up @@ -101,7 +100,7 @@ open class CastOptionsProvider : OptionsProvider {
val mediaOptions = CastMediaOptions.Builder()
.setImagePicker(ImagePickerImpl())
.setNotificationOptions(notificationOptions)
.setExpandedControllerActivityClassName(ExpandedControllerActivity::class.java.name)
.setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
.build()

return CastOptions.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package eu.kanade.tachiyomi.ui.player.controls

import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.FiniteAnimationSpec
Expand Down Expand Up @@ -57,6 +58,7 @@ import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import eu.kanade.presentation.more.settings.screen.player.custombutton.getButtons
import eu.kanade.presentation.theme.playerRippleConfiguration
import eu.kanade.tachiyomi.ui.player.CastManager
import eu.kanade.tachiyomi.ui.player.Dialogs
import eu.kanade.tachiyomi.ui.player.Panels
import eu.kanade.tachiyomi.ui.player.PlayerActivity
Expand Down Expand Up @@ -99,7 +101,6 @@ fun PlayerControls(
val audioPreferences = remember { Injekt.get<AudioPreferences>() }
val subtitlePreferences = remember { Injekt.get<SubtitlePreferences>() }
val interactionSource = remember { MutableInteractionSource() }

val controlsShown by viewModel.controlsShown.collectAsState()
val areControlsLocked by viewModel.areControlsLocked.collectAsState()
val seekBarShown by viewModel.seekBarShown.collectAsState()
Expand Down Expand Up @@ -438,6 +439,7 @@ fun PlayerControls(
end.linkTo(parent.end)
},
) {
val activity = LocalContext.current as PlayerActivity
TopRightPlayerControls(
autoPlayEnabled = autoPlayEnabled,
onToggleAutoPlay = { viewModel.setAutoPlay(it) },
Expand All @@ -454,6 +456,13 @@ fun PlayerControls(
onMoreClick = { viewModel.showSheet(Sheets.More) },
onMoreLongClick = { viewModel.showPanel(Panels.VideoFilters) },
isCastEnabled = { playerPreferences.enableCast().get() },
onCastLongClick = {
if (activity.castManager.castState.value == CastManager.CastState.CONNECTED) {
activity.castManager.handleQualitySelection()
} else {
Toast.makeText(activity, "Cast is not connected", Toast.LENGTH_SHORT).show()
}
},
)
}
// Bottom right controls
Expand Down
Loading

0 comments on commit 20c59b4

Please sign in to comment.