feat: Add 200% in-player volume boost for Android and iOS

Implemented in-player volume boost support up to 200% across Android and iOS playback engines.

Changes include:
- Added player-level volume control APIs to PlayerEngineController.
- Routed volume gestures through the active player engine instead of system volume when available.
- Added Android support by mapping boosted volume to ExoPlayer volume from 0.0 to 2.0.
- Added iOS support by exposing volume control through NuvioPlayerBridge.
- Configured MPV volume-max to 200 and mapped shared volume values to MPV volume 0–200.
- Updated player volume feedback to display boosted levels up to 200%.
- Preserved fallback behavior to system volume when the player controller is unavailable.

This keeps brightness and system volume behavior intact while allowing playback-specific volume amplification during video playback.
This commit is contained in:
Luqman Fadlli 2026-06-03 17:41:39 +07:00
parent 17168387d8
commit 123549a1a5
6 changed files with 69 additions and 5 deletions

View file

@ -393,6 +393,23 @@ actual fun PlatformPlayerSurface(
exoPlayer.setPlaybackSpeed(speed)
}
override fun currentPlayerVolume(): PlayerAudioLevel {
val current = exoPlayer.volume.coerceIn(0f, 2f)
return PlayerAudioLevel(
fraction = current,
isMuted = current <= 0.001f,
)
}
override fun setPlayerVolume(level: Float): PlayerAudioLevel {
val target = level.coerceIn(0f, 2f)
exoPlayer.volume = target
return PlayerAudioLevel(
fraction = target,
isMuted = target <= 0.001f,
)
}
override fun getAudioTracks(): List<AudioTrack> =
exoPlayer.extractAudioTracks(context)

View file

@ -10,6 +10,14 @@ interface PlayerEngineController {
fun seekBy(offsetMs: Long)
fun retry()
fun setPlaybackSpeed(speed: Float)
fun currentPlayerVolume(): PlayerAudioLevel = PlayerAudioLevel(
fraction = 1f,
isMuted = false,
)
fun setPlayerVolume(level: Float): PlayerAudioLevel = PlayerAudioLevel(
fraction = 1f,
isMuted = false,
)
fun getAudioTracks(): List<AudioTrack>
fun getSubtitleTracks(): List<SubtitleTrack>
fun selectAudioTrack(index: Int)

View file

@ -105,6 +105,7 @@ private const val PlayerLockedOverlayDurationMs = 2_000L
private const val PlayerLeftGestureBoundary = 0.4f
private const val PlayerRightGestureBoundary = 0.6f
private const val PlayerVerticalGestureSensitivity = 1f
private const val PlayerMaxVolumeBoost = 2f
private const val PlayerSeekProgressSyncDebounceMs = 700L
private const val P2pInitialPreloadTargetBytes = 5_242_880L
/** Hard ceiling for next-episode stream search to prevent hanging forever. */
@ -993,7 +994,7 @@ fun PlayerScreen(
}
fun showVolumeFeedback(level: PlayerAudioLevel) {
val percentage = (level.fraction.coerceIn(0f, 1f) * 100f).roundToInt()
val percentage = (level.fraction.coerceIn(0f, PlayerMaxVolumeBoost) * 100f).roundToInt()
showGestureFeedback(
GestureFeedbackState(
messageRes = if (level.isMuted) {
@ -2344,7 +2345,7 @@ fun PlayerScreen(
},
)
}
.pointerInput(gestureController, layoutSize) {
.pointerInput(gestureController, playerController, layoutSize) {
awaitEachGesture {
val down = awaitFirstDown()
if (playerControlsLockedState.value) {
@ -2371,7 +2372,7 @@ fun PlayerScreen(
null
}
val initialVolume = if (region == PlayerSideGesture.Volume) {
controller?.currentVolume()
playerController?.currentPlayerVolume() ?: controller?.currentVolume()
} else {
null
}
@ -2456,8 +2457,11 @@ fun PlayerScreen(
PlayerGestureMode.Volume -> {
val gestureDeltaFraction =
(-totalDy / height) * PlayerVerticalGestureSensitivity
controller?.setVolume((initialVolume?.fraction ?: 0f) + gestureDeltaFraction)
?.let(showVolumeFeedbackState.value)
val target = ((initialVolume?.fraction ?: 1f) + gestureDeltaFraction)
.coerceIn(0f, PlayerMaxVolumeBoost)
val level = playerController?.setPlayerVolume(target)
?: controller?.setVolume(target.coerceIn(0f, 1f))
level?.let(showVolumeFeedbackState.value)
}
null -> Unit

View file

@ -31,6 +31,8 @@ interface NuvioPlayerBridge {
gamma: Int,
)
fun setPlaybackSpeed(speed: Float)
fun getVolume(): Float
fun setVolume(volume: Float)
fun setResizeMode(mode: Int) // 0=Fit, 1=Fill, 2=Zoom
fun getAudioTrackCount(): Int
fun getAudioTrackIndex(at: Int): Int

View file

@ -87,6 +87,23 @@ actual fun PlatformPlayerSurface(
bridge.setPlaybackSpeed(speed)
}
override fun currentPlayerVolume(): PlayerAudioLevel {
val current = bridge.getVolume().coerceIn(0f, 2f)
return PlayerAudioLevel(
fraction = current,
isMuted = current <= 0.001f,
)
}
override fun setPlayerVolume(level: Float): PlayerAudioLevel {
val target = level.coerceIn(0f, 2f)
bridge.setVolume(target)
return PlayerAudioLevel(
fraction = target,
isMuted = target <= 0.001f,
)
}
override fun getAudioTracks(): List<AudioTrack> {
val count = bridge.getAudioTrackCount()
return (0 until count).map { i ->

View file

@ -60,6 +60,8 @@ final class MPVPlayerBridgeImpl: NSObject, NuvioPlayerBridge {
)
}
func setPlaybackSpeed(speed: Float) { playerVC?.setSpeed(speed) }
func getVolume() -> Float { playerVC?.getVolume() ?? 1.0 }
func setVolume(volume: Float) { playerVC?.setVolume(volume) }
func setResizeMode(mode: Int32) { playerVC?.setResize(Int(mode)) }
// Audio tracks
@ -319,6 +321,8 @@ final class MPVPlayerViewController: UIViewController {
checkError(mpv_set_option_string(mpv, "hwdec", "auto"))
checkError(mpv_set_option_string(mpv, "audio-channels", "stereo"))
checkError(mpv_set_option_string(mpv, "audio-fallback-to-null", "yes"))
checkError(mpv_set_option_string(mpv, "volume-max", "200"))
checkError(mpv_set_option_string(mpv, "volume", "100"))
checkError(mpv_set_option_string(mpv, "vulkan-swap-mode", "fifo"))
checkError(mpv_set_option_string(mpv, "vulkan-queue-count", "1"))
checkError(mpv_set_option_string(mpv, "vulkan-async-compute", "no"))
@ -517,6 +521,18 @@ final class MPVPlayerViewController: UIViewController {
mpv_set_property(mpv, "speed", MPV_FORMAT_DOUBLE, &s)
}
func getVolume() -> Float {
guard mpv != nil else { return 1.0 }
let volume = getDouble("volume") / 100.0
return Float(max(0.0, min(2.0, volume)))
}
func setVolume(_ volume: Float) {
guard mpv != nil else { return }
var value = Double(max(0.0, min(2.0, volume)) * 100.0)
checkError(mpv_set_property(mpv, "volume", MPV_FORMAT_DOUBLE, &value))
}
func setResize(_ mode: Int) {
guard mpv != nil else { return }
switch mode {