Merge pull request #1176 from halibiram/feat/player-aspect-mode
player aspect mode
This commit is contained in:
commit
3fdfb341d7
8 changed files with 147 additions and 11 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue