fix(audio): Spotify resumed nach Mute — RNSound's haengenden Focus loesen

Logs zeigten: react-native-sound requestet beim Sound.play() einen
EIGENEN AudioFocus mit USAGE_MEDIA, released den aber bei Sound.stop()/
release() NICHT (bekanntes RN-sound-Bug). Spotify sieht den haengenden
Media-Focus → bleibt pausiert.

Workaround: Native-Methode kickReleaseMedia() macht einen request+abandon-
Cycle mit USAGE_MEDIA, das System raeumt damit den Focus-Stack auf und
Spotify bekommt sauberen GAIN-Event. stopPlayback ruft das jetzt nach
Sound.release() wenn vorher ein RNSound aktiv war.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 16:57:52 +02:00
parent fb9e5dcd10
commit b1eaf42fef
2 changed files with 57 additions and 0 deletions
@@ -131,6 +131,52 @@ class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBase
promise.resolve(true)
}
/** Den USAGE_MEDIA-Focus-Stack im System aufmischen, damit Spotify/YouTube
* resumen wenn ein anderer Player (z.B. react-native-sound) seinen Focus
* nicht ordnungsgemaess released hat. Strategie: kurz selbst USAGE_MEDIA
* GAIN beanspruchen — das System invalidiert dabei den haengenden Stack-
* Eintrag des anderen Players — und sofort wieder abandonen. Spotify
* bekommt den Focus-Gain und resumed.
*
* Workaround fuer das react-native-sound-Bug: Sound.stop()/release()
* laesst den AudioFocusRequest haengen.
*/
@ReactMethod
fun kickReleaseMedia(promise: Promise) {
val am = audioManager()
if (am == null) {
promise.resolve(false)
return
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val attrs = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
val kickListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
val kickReq = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(attrs)
.setOnAudioFocusChangeListener(kickListener)
.build()
am.requestAudioFocus(kickReq)
am.abandonAudioFocusRequest(kickReq)
} else {
@Suppress("DEPRECATION")
val kickListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
@Suppress("DEPRECATION")
am.requestAudioFocus(kickListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
@Suppress("DEPRECATION")
am.abandonAudioFocus(kickListener)
}
Log.i(TAG, "kickReleaseMedia: USAGE_MEDIA-Stack aufgemischt")
promise.resolve(true)
} catch (e: Exception) {
Log.w(TAG, "kickReleaseMedia failed: ${e.message}")
promise.resolve(false)
}
}
private fun release() {
val am = audioManager() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {