Merge pull request #1176 from halibiram/feat/player-aspect-mode

player aspect mode
This commit is contained in:
Nayif 2026-04-01 00:22:27 +05:30 committed by GitHub
commit 3fdfb341d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 147 additions and 11 deletions

View file

@ -0,0 +1,112 @@
package com.nuvio.tv.ui.screens.player
import android.view.SurfaceView
import android.view.TextureView
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.media3.ui.PlayerView
import com.nuvio.tv.R
enum class AspectMode(@StringRes val labelResId: Int) {
ORIGINAL(R.string.player_aspect_fit),
FULL_SCREEN(R.string.player_aspect_crop),
SLIGHT_ZOOM(R.string.player_aspect_mode_slight_zoom),
CINEMA_ZOOM(R.string.player_aspect_mode_cinema_zoom),
VERTICAL_STRETCH(R.string.player_aspect_fit_height),
HORIZONTAL_STRETCH(R.string.player_aspect_fit_width)
}
internal fun nextAspectMode(current: AspectMode): AspectMode {
val modes = AspectMode.entries
val nextIndex = (modes.indexOf(current) + 1) % modes.size
return modes[nextIndex]
}
internal fun aspectModeLabel(mode: AspectMode, getString: (Int) -> String): String =
getString(mode.labelResId)
internal fun applyAspectMode(playerView: PlayerView, mode: AspectMode) {
val targetView = resolveVideoSurfaceView(playerView) ?: playerView
playerView.scaleX = 1.0f
playerView.scaleY = 1.0f
when (mode) {
AspectMode.ORIGINAL -> {
targetView.scaleX = 1.0f
targetView.scaleY = 1.0f
}
AspectMode.FULL_SCREEN -> applyCoverAspectScale(playerView, targetView)
AspectMode.SLIGHT_ZOOM -> {
targetView.scaleX = 1.15f
targetView.scaleY = 1.15f
}
AspectMode.CINEMA_ZOOM -> {
targetView.scaleX = 1.33f
targetView.scaleY = 1.33f
}
AspectMode.VERTICAL_STRETCH -> {
targetView.scaleX = 1.0f
targetView.scaleY = 1.33f
}
AspectMode.HORIZONTAL_STRETCH -> {
targetView.scaleX = 1.3333f
targetView.scaleY = 1.0f
}
}
}
private fun applyCoverAspectScale(playerView: PlayerView, targetView: View) {
val videoSize = playerView.player?.videoSize
val videoAspect = if ((videoSize?.height ?: 0) > 0) {
((videoSize?.width ?: 0).toFloat() * (videoSize?.pixelWidthHeightRatio ?: 1f)) /
videoSize!!.height.toFloat()
} else {
0f
}
val viewAspect = if (playerView.width > 0 && playerView.height > 0) {
playerView.width.toFloat() / playerView.height.toFloat()
} else {
0f
}
if (videoAspect > 0f && viewAspect > 0f) {
if (videoAspect > viewAspect) {
targetView.scaleX = 1.0f
targetView.scaleY = videoAspect / viewAspect
} else {
targetView.scaleX = viewAspect / videoAspect
targetView.scaleY = 1.0f
}
} else {
targetView.scaleX = 1.0f
targetView.scaleY = 1.0f
}
}
private fun resolveVideoSurfaceView(playerView: PlayerView): View? {
return findVideoSurfaceView(playerView)
}
private fun findVideoSurfaceView(view: View): View? {
return when (view) {
is SurfaceView, is TextureView -> view
is ViewGroup -> {
for (index in 0 until view.childCount) {
val child = findVideoSurfaceView(view.getChildAt(index))
if (child != null) {
return child
}
}
null
}
else -> null
}
}

View file

@ -79,7 +79,8 @@ internal fun PlayerRuntimeController.initializePlayer(url: String, headers: Map<
_uiState.update {
it.copy(
frameRateMatchingMode = playerSettings.frameRateMatchingMode,
resizeMode = playerSettings.resizeMode
resizeMode = playerSettings.resizeMode,
tunnelingEnabled = playerSettings.tunnelingEnabled
)
}
val afrJob = async {

View file

@ -191,6 +191,7 @@ internal fun PlayerRuntimeController.observeSubtitleSettings() {
pauseOverlayEnabled = settings.pauseOverlayEnabled,
osdClockEnabled = settings.osdClockEnabled,
frameRateMatchingMode = settings.frameRateMatchingMode,
tunnelingEnabled = settings.tunnelingEnabled,
persistAudioAmplification = settings.persistAudioAmplification,
audioAmplificationDb = resolvedAudioAmplificationDb
)

View file

@ -813,18 +813,31 @@ fun PlayerRuntimeController.onEvent(event: PlayerEvent) {
}
}
PlayerEvent.OnToggleAspectRatio -> {
val currentMode = _uiState.value.resizeMode
val newMode = PlayerDisplayModeUtils.nextResizeMode(currentMode)
val modeText = PlayerDisplayModeUtils.resizeModeLabel(newMode, context)
Log.d("PlayerViewModel", "Aspect ratio toggled: $currentMode -> $newMode")
val state = _uiState.value
if (state.tunnelingEnabled) {
_uiState.update {
it.copy(
showAspectRatioIndicator = true,
aspectRatioIndicatorText = context.getString(R.string.player_aspect_tunneling_unavailable)
)
}
hideAspectRatioIndicatorJob?.cancel()
hideAspectRatioIndicatorJob = scope.launch {
delay(1500)
_uiState.update { it.copy(showAspectRatioIndicator = false) }
}
return
}
val newMode = nextAspectMode(state.aspectMode)
val label = aspectModeLabel(newMode, context::getString)
Log.d("PlayerViewModel", "Aspect mode toggled: ${state.aspectMode} -> $newMode ($label)")
_uiState.update {
it.copy(
resizeMode = newMode,
aspectMode = newMode,
showAspectRatioIndicator = true,
aspectRatioIndicatorText = modeText
aspectRatioIndicatorText = label
)
}
scope.launch { playerSettingsDataStore.setResizeMode(newMode) }
hideAspectRatioIndicatorJob?.cancel()
hideAspectRatioIndicatorJob = scope.launch {
delay(1500)

View file

@ -87,6 +87,7 @@ import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@ -491,7 +492,7 @@ fun PlayerScreen(
// Video Player
viewModel.exoPlayer?.let { player ->
val subtitleStyle = uiState.subtitleStyle
val resizeMode = uiState.resizeMode
val aspectMode = uiState.aspectMode
AndroidView(
factory = { context ->
@ -505,8 +506,8 @@ fun PlayerScreen(
update = { playerView ->
// Keep device awake only while playback is active (or buffering), not when paused.
playerView.keepScreenOn = uiState.isPlaying || uiState.isBuffering
Log.d("PlayerScreen", "Applying resizeMode: $resizeMode")
playerView.resizeMode = resizeMode
playerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
applyAspectMode(playerView, aspectMode)
playerView.subtitleView?.apply {
// Calculate font size based on percentage (100% = 24sp base)
val baseFontSize = 24f

View file

@ -125,6 +125,8 @@ data class PlayerUiState(
val showDisplayModeInfo: Boolean = false,
// Aspect ratio / resize mode
val resizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_FIT,
val aspectMode: AspectMode = AspectMode.ORIGINAL,
val tunnelingEnabled: Boolean = false,
val showAspectRatioIndicator: Boolean = false,
val aspectRatioIndicatorText: String = "",
// Stream info overlay

View file

@ -847,6 +847,9 @@
<string name="player_aspect_fit_width">Genişliğe Sığdır</string>
<string name="player_aspect_fit_height">Yüksekliğe Sığdır</string>
<string name="player_aspect_crop">Kırp</string>
<string name="player_aspect_tunneling_unavailable">Tünellenmiş oynatım sırasında kullanılamaz</string>
<string name="player_aspect_mode_slight_zoom">Hafif Yakınlaştırma</string>
<string name="player_aspect_mode_cinema_zoom">Sinema Yakınlaştırma</string>
<!-- AudioDialog -->
<string name="audio_dialog_title">Ses</string>

View file

@ -851,6 +851,9 @@
<string name="player_aspect_fit_width">Fit Width</string>
<string name="player_aspect_fit_height">Fit Height</string>
<string name="player_aspect_crop">Crop</string>
<string name="player_aspect_tunneling_unavailable">Unavailable during tunneled playback</string>
<string name="player_aspect_mode_slight_zoom">Slight Zoom</string>
<string name="player_aspect_mode_cinema_zoom">Cinema Zoom</string>
<!-- AudioDialog -->
<string name="audio_dialog_title">Audio</string>