Merge pull request #1738 from eagleeyetom/chromecast-volume-fix-updated

Android cast: stabilize MediaSession volume handling; simplify Google…
This commit is contained in:
advplyr
2026-02-08 16:37:15 -06:00
committed by GitHub
2 changed files with 42 additions and 30 deletions
@@ -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<MediaItem> = 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) {
@@ -736,32 +736,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