Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {

// 支持通过 -PVERSION_NAME / -PVERSION_CODE 显式传入版本信息。
// 未显式传入 VERSION_NAME 时,默认把最后一段 patch 替换为 BUILD_NUMBER。
val baseAppVersionName = "0.10.0"
val baseAppVersionName = "0.11.0"

fun String?.nonBlankOrNull(): String? =
this?.trim()?.takeIf { it.isNotBlank() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ fun pictureSettingsDescriptionLabel(): String =
"按 SDR、HDR10、HDR10+ 和 Dolby Vision 分别配置默认画面规则。"

fun pictureOsdMenuTitleLabel(): String =
"Picture"
"画面"

fun pictureSaveDefaultForFormatLabel(): String =
"保存为该格式默认"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
Expand Down Expand Up @@ -90,6 +91,8 @@ class ExoPlaybackController @Inject constructor(

private val availableSubtitles = mutableListOf<SubtitleTrack>()
private val availableAudioTracks = mutableListOf<AudioTrack>()
private var selectedSubtitleTrackIndex: Int? = null
private var selectedAudioTrackIndex: Int? = null
private var currentSource: PlaybackSource? = null
private var autoResumeSeekCalled = false
private var playbackPreferences = FormatAwareToneMappingPreferences()
Expand Down Expand Up @@ -360,6 +363,8 @@ class ExoPlaybackController @Inject constructor(
autoResumeSeekCalled = false
availableSubtitles.clear()
availableAudioTracks.clear()
selectedSubtitleTrackIndex = null
selectedAudioTrackIndex = null
containerSignalDescriptor = null
_currentVideoSignalDescriptor.value = null
PlaybackCodecSelectionState.decoderPreference = PlaybackDecoderPreference.DEFAULT
Expand Down Expand Up @@ -388,17 +393,21 @@ class ExoPlaybackController @Inject constructor(
}

override suspend fun setSubtitleTrack(trackIndex: Int) {
updateAvailableTracks()
selectTrackGroup(C.TRACK_TYPE_TEXT, trackIndex)
}

override suspend fun setAudioTrack(trackIndex: Int) {
updateAvailableTracks()
selectTrackGroup(C.TRACK_TYPE_AUDIO, trackIndex)
}

override fun getAvailableSubtitles(): List<SubtitleTrack> = availableSubtitles.toList()

override fun getAvailableAudioTracks(): List<AudioTrack> = availableAudioTracks.toList()

override fun getSelectedSubtitleTrackIndex(): Int? = selectedSubtitleTrackIndex

override fun getSelectedAudioTrackIndex(): Int? = selectedAudioTrackIndex

override suspend fun getCurrentPosition(): Long = withContext(Dispatchers.Main) {
when (_activeRenderBackend.value) {
PlaybackRenderBackend.EXPERIMENTAL_MPV_ANDROID -> externalMpvPositionMs
Expand Down Expand Up @@ -852,16 +861,36 @@ class ExoPlaybackController @Inject constructor(
}
}

private suspend fun selectTrackGroup(trackType: Int, trackIndex: Int) = withContext(Dispatchers.Main) {
val player = activeExoPlayer()
val group = player.currentTracks.groups.filter { it.type == trackType }.getOrNull(trackIndex)
?: return@withContext
player.trackSelectionParameters = player.trackSelectionParameters
.buildUpon()
.setTrackTypeDisabled(trackType, false)
.clearOverridesOfType(trackType)
.addOverride(TrackSelectionOverride(group.mediaTrackGroup, 0))
.build()
if (trackType == C.TRACK_TYPE_TEXT) {
selectedSubtitleTrackIndex = trackIndex
} else {
selectedAudioTrackIndex = trackIndex
}
}

private fun updateAvailableTracks() {
availableSubtitles.clear()
availableAudioTracks.clear()
selectedSubtitleTrackIndex = null
selectedAudioTrackIndex = null

try {
val tracks = activeExoPlayer().currentTracks
for (i in 0 until tracks.groups.size) {
val group = tracks.groups[i]
when (group.type) {
C.TRACK_TYPE_TEXT -> {
val trackIndex = availableSubtitles.size
val format = group.getTrackFormat(0)
availableSubtitles.add(
SubtitleTrack(
Expand All @@ -872,18 +901,21 @@ class ExoPlaybackController @Inject constructor(
format = SubtitleFormat.SRT,
),
)
if (group.isTrackSelected(0)) selectedSubtitleTrackIndex = trackIndex

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'androidx\.media3|media3-(common|exoplayer)|media3Version' \
  --glob '*.gradle' --glob '*.gradle.kts' --glob 'libs.versions.toml' .

rg -n 'isTrackSelected|\.isSelected' player-core ui-tv --glob '*.kt'

Repository: ModerRAS/MiruPlay

Length of output: 1589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Media3 version ==="
sed -n '1,90p' gradle/libs.versions.toml | nl -ba | sed -n '1,90p'

echo
echo "=== ExoPlaybackController selection mapping ==="
sed -n '880,930p' player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt | nl -ba

echo
echo "=== Any other uses of group selection APIs ==="
rg -n 'isTrackSelected\(|group\.isSelected|selectedSubtitleTrackIndex|selectedAudioTrackIndex' player-core ui-tv --glob '*.kt'

Repository: ModerRAS/MiruPlay

Length of output: 218


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== gradle/libs.versions.toml media3 entries ==="
sed -n '1,80p' gradle/libs.versions.toml | awk 'NR>=1 && NR<=80 {print NR ":" $0}' | grep -n 'media3'

echo
echo "=== ExoPlaybackController relevant lines ==="
sed -n '896,922p' player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt | awk '{print NR ":" $0}'

echo
echo "=== Local usages of group selection APIs ==="
rg -n 'isTrackSelected\(|group\.isSelected|selectedSubtitleTrackIndex|selectedAudioTrackIndex' player-core ui-tv --glob '*.kt'

Repository: ModerRAS/MiruPlay

Length of output: 7590


🌐 Web query:

androidx.media3 Tracks.Group isSelected API docs version media3-exoplayer media3-common

💡 Result:

In the androidx.media3.common library, the Tracks.Group class provides information about a single group of tracks [1][2]. The isSelected API is used to determine the selection status of tracks within that group [3][4]. There are two primary methods related to selection in this class: 1. isSelected: This method returns a boolean indicating whether at least one track in the group is currently selected for playback [1][2]. 2. isTrackSelected(int trackIndex): This method returns a boolean indicating whether a specific track, identified by its index within the group, is selected for playback [1][2]. Important context regarding track selection: - Multiple tracks in a single group can be selected simultaneously [1]. This is common in adaptive streaming scenarios where the player selects multiple tracks of varying quality to switch between based on network conditions [1][3]. - A track is considered selected if it has been chosen for playback based on the current TrackSelectionParameters [3][4]. - While this class identifies which tracks are selected, it does not explicitly indicate which of the selected tracks is currently active (the one actually being played) [1]. Some player implementations, such as ExoPlayer, provide mechanisms to retrieve this, for example via ExoTrackSelection.getSelectedFormat [1].

Citations:


Use the group’s selected state here. Tracks.Group exposes isSelected, and checking only track 0 can leave the UI index unset when ExoPlayer selects a different track in the same group. Use group.isSelected for both subtitle and audio mapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt`
at line 904, Update the subtitle and audio track mapping logic in
ExoPlaybackController to use each Tracks.Group’s isSelected state instead of
checking isTrackSelected(0). Ensure selectedSubtitleTrackIndex and the
corresponding audio index are set whenever the group is selected, regardless of
which track within the group ExoPlayer chose.

}

C.TRACK_TYPE_AUDIO -> {
val trackIndex = availableAudioTracks.size
val format = group.getTrackFormat(0)
availableAudioTracks.add(
AudioTrack(
index = availableAudioTracks.size,
index = trackIndex,
language = format.language ?: "und",
title = format.label,
codec = format.codecs,
),
)
if (group.isTrackSelected(0)) selectedAudioTrackIndex = trackIndex
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ interface PlaybackController {
*/
fun getAvailableAudioTracks(): List<AudioTrack>

fun getSelectedSubtitleTrackIndex(): Int? = null

fun getSelectedAudioTrackIndex(): Int? = null

/**
* Get current playback position in milliseconds
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ fun FeatureAnimeCard(
onClick: () -> Unit,
modifier: Modifier = Modifier,
subtitle: String? = null,
onDirectionUp: (() -> Unit)? = null,
backdropPlaceholder: @Composable () -> Unit = { ImagePlaceholder() },
posterPlaceholder: @Composable () -> Unit = { ImagePlaceholder() },
) {
Expand All @@ -172,6 +173,7 @@ fun FeatureAnimeCard(
)
.tvFocusableClickable(
interactionSource = interactionSource,
onDirectionUp = onDirectionUp,
onClick = onClick
)
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class InitialFocusHandle internal constructor(
private val markPlaced: () -> Unit,
private val updateFocused: (Boolean) -> Unit,
) {
fun requester(): FocusRequester = focusRequester

fun modifier(): Modifier =
Modifier
.focusRequester(focusRequester)
Expand Down
23 changes: 17 additions & 6 deletions ui-tv/src/main/kotlin/com/miruplay/tv/ui/components/TvKeyEvents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ internal inline fun tvActivateIntentEvent(
internal fun Modifier.tvFocusableClickable(
interactionSource: MutableInteractionSource,
enabled: Boolean = true,
onDirectionUp: (() -> Unit)? = null,
onClick: () -> Unit,
): Modifier = this
.clickable(
Expand All @@ -79,12 +80,22 @@ internal fun Modifier.tvFocusableClickable(
onClick = onClick,
)
.onKeyEvent { event ->
tvActivateKeyEvent(
key = event.key,
type = event.type,
enabled = enabled,
onActivate = onClick,
)
if (
enabled &&
onDirectionUp != null &&
event.type == KeyEventType.KeyDown &&
event.key.toMiruPlayInputIntent() == MiruPlayInputIntent.DirectionUp
) {
onDirectionUp()
true
} else {
tvActivateKeyEvent(
key = event.key,
type = event.type,
enabled = enabled,
onActivate = onClick,
)
}
}
.focusable(
enabled = enabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
Expand All @@ -25,9 +27,17 @@ fun TvTextField(
onValueChange: (String) -> Unit,
label: String,
modifier: Modifier = Modifier,
isPassword: Boolean = false
isPassword: Boolean = false,
requestInitialFocus: Boolean = false,
) {
var isFocused by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(requestInitialFocus) {
if (requestInitialFocus) {
kotlinx.coroutines.delay(120)
focusRequester.requestFocus()
}
}
val borderWidth by animateFloatAsState(
targetValue = if (isFocused) 3f else 1f,
label = "borderW"
Expand Down Expand Up @@ -59,6 +69,7 @@ fun TvTextField(
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
.onFocusChanged { isFocused = it.isFocused },
textStyle = TextStyle(
color = TextPrimary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ fun AnimeDetailScreen(
LaunchedEffect(animeId) {
viewModel.loadAnime(animeId)
}
BackHandler(enabled = manualMatch.isOpen, onBack = viewModel::closeRescrapeMatcher)
BackHandler(enabled = manualMatch.isOpen) {
if (!manualMatch.isSearching && !manualMatch.isApplying) {
viewModel.closeRescrapeMatcher()
}
}
BackHandler(enabled = !manualMatch.isOpen, onBack = onNavigateBack)

Box(modifier = Modifier.fillMaxSize()) {
Expand Down Expand Up @@ -319,7 +323,8 @@ private fun DetailContent(
TvButton(
text = detailSeasonLabel(season),
onClick = { onSelectSeason(season) },
modifier = Modifier.width(132.dp)
modifier = Modifier.width(132.dp),
secondary = selectedSeason != season,
)
}
}
Expand Down Expand Up @@ -354,7 +359,7 @@ private fun BangumiManualMatchDialog(
val busy = state.isSearching || state.isApplying
val dialogMaxHeight = (LocalConfiguration.current.screenHeightDp.dp * 0.9f).coerceAtLeast(420.dp)

Dialog(onDismissRequest = onDismiss) {
Dialog(onDismissRequest = { if (!busy) onDismiss() }) {
Column(
modifier = Modifier
.fillMaxWidth(0.9f)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ fun DramaDetailScreen(
LaunchedEffect(seriesId) {
viewModel.loadSeries(seriesId)
}
BackHandler(enabled = manualMatch.isOpen, onBack = viewModel::closeManualMatch)
BackHandler(enabled = manualMatch.isOpen) {
if (!manualMatch.isSearching && !manualMatch.isApplying) {
viewModel.closeManualMatch()
}
}
BackHandler(enabled = !manualMatch.isOpen, onBack = onNavigateBack)

Box(modifier = Modifier.fillMaxSize()) {
Expand Down Expand Up @@ -421,7 +425,7 @@ private fun DramaManualMatchDialog(
val busy = state.isSearching || state.isApplying
val dialogMaxHeight = (LocalConfiguration.current.screenHeightDp.dp * 0.9f).coerceAtLeast(420.dp)

Dialog(onDismissRequest = onDismiss) {
Dialog(onDismissRequest = { if (!busy) onDismiss() }) {
Column(
modifier = Modifier
.fillMaxWidth(0.9f)
Expand Down
Loading
Loading