From 1a52f19b329e770fff984ffa4e9964599d077624 Mon Sep 17 00:00:00 2001 From: eagleeyetom Date: Thu, 27 Nov 2025 00:32:33 +0100 Subject: [PATCH] Android cast: stabilize MediaSession volume handling; simplify Google Cast volume --- .../audiobookshelf/app/player/CastPlayer.kt | 41 +++++++++++++++---- .../app/player/PlayerNotificationService.kt | 31 +++++--------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/CastPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/player/CastPlayer.kt index 9f7f59f9..a31bacf4 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/CastPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/CastPlayer.kt @@ -3,6 +3,7 @@ package com.audiobookshelf.app.player import android.annotation.SuppressLint import android.os.Looper import android.util.Log +import kotlin.math.roundToInt import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView @@ -53,6 +54,10 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() { private var myCurrentTrackGroups: TrackGroupArray private var myCurrentTrackSelections: TrackSelectionArray + // Cache for pending volume to handle async setVolume + private var pendingVolume: Int? = null + private var pendingVolumeSetAt: Long = 0L + var currentMediaItems:List = mutableListOf() var remoteMediaClient:RemoteMediaClient? = null var sessionAvailabilityListener:SessionAvailabilityListener? = null @@ -864,12 +869,24 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() { } override fun getDeviceVolume(): Int { + // Short-lived cache without even-value quantization + val now = System.currentTimeMillis() + if (pendingVolume != null && (now - pendingVolumeSetAt) < 100) { + return pendingVolume!! + } + val session = castContext.sessionManager.currentCastSession - return if (session != null && !session.isMute) { - (session.volume * 100).toInt() + val vol = if (session != null && !session.isMute) { + (session.volume * 100).roundToInt().coerceIn(0, 100) } else { 0 } + + if (pendingVolume != null && vol == pendingVolume) { + pendingVolume = null + } + + return vol } override fun isDeviceMuted(): Boolean { @@ -881,22 +898,28 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() { val session = castContext.sessionManager.currentCastSession try { val clamped = volume.coerceIn(0, 100) - session?.volume = clamped / 100.0 + val target = (clamped / 100.0).let { kotlin.math.round(it * 100.0) / 100.0 } + + // Cache for 100ms to return in getDeviceVolume during async period + pendingVolume = clamped + pendingVolumeSetAt = System.currentTimeMillis() + + session?.setVolume(target) } catch (e: Exception) { Log.e(tag, "Failed to set cast device volume", e) } } override fun increaseDeviceVolume() { - val currentVolume = getDeviceVolume() - val newVolume = (currentVolume + 1).coerceAtMost(100) - setDeviceVolume(newVolume) + val current = getDeviceVolume() + val next = current + 1 + setDeviceVolume(next.coerceAtMost(100)) } override fun decreaseDeviceVolume() { - val currentVolume = getDeviceVolume() - val newVolume = (currentVolume - 1).coerceAtLeast(0) - setDeviceVolume(newVolume) + val current = getDeviceVolume() + val next = current - 1 + setDeviceVolume(next.coerceAtLeast(0)) } override fun setDeviceMuted(muted: Boolean) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index a967345b..ab6d5c60 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -732,32 +732,21 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } private fun setMediaSessionToCastVolume() { - val currentVol = try { castPlayer?.getDeviceVolume() ?: 0 } catch (e: Exception) { 0 } + val currentVol = try { castPlayer?.getDeviceVolume() ?: 0 } catch (_: Exception) { 0 } val provider = object : VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, 100, currentVol) { override fun onSetVolumeTo(volume: Int) { + // Clamp, update UI immediately, then send to device val clamped = volume.coerceIn(0, 100) - try { - castPlayer?.setDeviceVolume(clamped) - val actual = castPlayer?.getDeviceVolume() ?: clamped - setCurrentVolume(actual) - } catch (_: Exception) {} + setCurrentVolume(clamped) + try { castPlayer?.setDeviceVolume(clamped) } catch (_: Exception) {} } + override fun onAdjustVolume(direction: Int) { - val current = try { castPlayer?.getDeviceVolume() ?: currentVolume } catch (_: Exception) { currentVolume } - val step = if (direction > 0) 1 else if (direction < 0) -1 else 0 - if (step == 0) return - var target = (current + step).coerceIn(0, 100) - try { - castPlayer?.setDeviceVolume(target) - var actual = castPlayer?.getDeviceVolume() ?: current - // If device ignored tiny step, try nudging up to 3 steps total. - if (actual == current) { - target = (current + step * 3).coerceIn(0, 100) - castPlayer?.setDeviceVolume(target) - actual = castPlayer?.getDeviceVolume() ?: current - } - setCurrentVolume(actual) - } catch (_: Exception) {} + // Use Android-provided step (−1, 0, +1). Clamp, update UI immediately, then send. + val current = currentVolume + val target = (current + direction).coerceIn(0, 100) + setCurrentVolume(target) + try { castPlayer?.setDeviceVolume(target) } catch (_: Exception) {} } } remoteVolumeProvider = provider