From f2e643d1fb1af940719cfbcc92b11e43813c4fe3 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 26 Apr 2026 13:18:25 +0200 Subject: [PATCH] =?UTF-8?q?fix(app):=20Underrun-Schutz=20im=20PcmStreamPla?= =?UTF-8?q?yer=20=E2=80=94=20Spotify=20resumed=20nicht=20mehr=20nach=2010s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wenn die Bridge zwischen zwei Saetzen rendert (1-2s pro Satz auf der Gamebox-RTX 3060), kommen keine neuen PCM-Chunks rein und der AudioTrack- Buffer laeuft leer. Spotify hat eine eigene Heuristik die nach ~10s "stummer Lücke" eigenmaechtig die Wiedergabe wiederaufnimmt — auch wenn wir den AudioFocus formal noch halten. Fix: Writer-Thread fuettert Stille rein wenn der Puffer unter ~100ms faellt (~50ms pro Refill-Tick alle 50ms). AudioTrack bleibt damit durchgehend aktiv, andere Apps respektieren weiterhin den Fokus. Bonus: 30s-Idle-Cutoff falls die Bridge crashed und kein final-Marker mehr kommt — sonst wuerde der Writer-Thread ewig Stille fuettern. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/ariacockpit/PcmStreamPlayerModule.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt b/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt index f84a32d..db189a1 100644 --- a/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt +++ b/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt @@ -137,6 +137,17 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex Log.w(TAG, "play() sofort failed: ${e.message}") } } + // Idle-Cutoff: wenn endRequested NICHT kam aber 30s nichts mehr + // reinkommt, brechen wir ab (Bridge-Crash, verlorener final). + var idleMs = 0L + val maxIdleMs = 30_000L + // Zielpufferfuellung — unter diesem Wasserstand fuettern wir + // Stille rein damit AudioTrack nicht underrunt waehrend die + // Bridge den naechsten Satz rendert. Spotify/YouTube reagieren + // sonst mit eigenmaechtiger Wiederaufnahme nach ~10s Stille. + val underrunGuardFrames = sampleRate / 10 // ~100ms + val silenceFillFrames = sampleRate / 20 // ~50ms pro Refill + mainLoop@ while (!writerShouldStop) { val data = queue.poll(50, java.util.concurrent.TimeUnit.MILLISECONDS) if (data == null) { @@ -153,8 +164,33 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex } break@mainLoop } + // Underrun-Schutz: Stille reinfuettern wenn der AudioTrack- + // Puffer leerzulaufen droht. Spotify resumed sonst nach + // ~10s Pause auf eigene Faust, obwohl wir den Fokus halten. + if (playbackStarted) { + val framesWritten = bytesBuffered / streamBytesPerFrame + val framesPlayed = t.playbackHeadPosition.toLong() + val framesInBuffer = framesWritten - framesPlayed + if (framesInBuffer < underrunGuardFrames) { + val fillBytes = silenceFillFrames * streamBytesPerFrame + val silence = ByteArray(fillBytes) + var silOff = 0 + while (silOff < silence.size && !writerShouldStop) { + val w = t.write(silence, silOff, silence.size - silOff) + if (w <= 0) break + silOff += w + } + bytesBuffered += silence.size + } + } + idleMs += 50L + if (idleMs >= maxIdleMs) { + Log.w(TAG, "Idle-Cutoff: ${maxIdleMs}ms keine Daten — Stream wird beendet") + break@mainLoop + } continue@mainLoop } + idleMs = 0L // Pre-Roll Check: play() erst wenn genug gepuffert if (!playbackStarted && bytesBuffered + data.size >= prerollBytes) {