fix(audio): AudioFocus erst beim NATIVEN Playback-Finished-Event released
Logcat-Befund:
12:22:54.860 — final-Chunk + Cache geschrieben
12:22:55.402 — abandonAudioFocus (~0.5s spaeter)
12:22:55 — Spotify resumed (Atlas: TotalTime 93s)
12:23:27.064 — Playback fertig (32s spaeter!)
→ ARIA spricht 32s parallel zu Spotify weil end() viel zu frueh
returnt. Stefans 'Spotify resumed obwohl ARIA noch redet'.
Fix:
- PcmStreamPlayerModule emittiert 'PcmPlaybackFinished' RN-Event nach
dem finally{}-Block im Writer-Thread (= AudioTrack hat alle Samples
wirklich durchgespielt)
- audioService subscribed im constructor → ruft erst dann
_releaseFocusDeferred()
- _handlePcmChunkImpl bei isFinal triggert NICHT mehr direkt das
Release — nur die playbackFinished-Listener (UI-Logic)
So bleibt Spotify pausiert bis ARIA tatsaechlich fertig ist, egal
wie viel Audio im AudioTrack-Buffer wartet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,12 @@ import android.media.AudioManager
|
|||||||
import android.media.AudioTrack
|
import android.media.AudioTrack
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.facebook.react.bridge.Arguments
|
||||||
import com.facebook.react.bridge.Promise
|
import com.facebook.react.bridge.Promise
|
||||||
import com.facebook.react.bridge.ReactApplicationContext
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
import com.facebook.react.bridge.ReactMethod
|
import com.facebook.react.bridge.ReactMethod
|
||||||
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -255,6 +257,17 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
|||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
try { t.stop() } catch (_: Exception) {}
|
try { t.stop() } catch (_: Exception) {}
|
||||||
try { t.release() } catch (_: Exception) {}
|
try { t.release() } catch (_: Exception) {}
|
||||||
|
// RN-Event: AudioTrack ist wirklich durch (alle Samples gespielt).
|
||||||
|
// JS released erst JETZT den AudioFocus — sonst spielt Spotify
|
||||||
|
// beim end()-Cap waehrend ARIA noch redet (15s+ je nach Buffer).
|
||||||
|
try {
|
||||||
|
val params = Arguments.createMap()
|
||||||
|
reactApplicationContext
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||||
|
.emit("PcmPlaybackFinished", params)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "PlaybackFinished emit failed: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, "PcmStreamWriter").apply { start() }
|
}, "PcmStreamWriter").apply { start() }
|
||||||
|
|
||||||
@@ -311,6 +324,9 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
|||||||
promise.resolve(true)
|
promise.resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactMethod fun addListener(eventName: String) {}
|
||||||
|
@ReactMethod fun removeListeners(count: Int) {}
|
||||||
|
|
||||||
private fun stopInternal() {
|
private fun stopInternal() {
|
||||||
writerShouldStop = true
|
writerShouldStop = true
|
||||||
endRequested = true
|
endRequested = true
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Nutzt react-native-audio-recorder-player fuer Aufnahme.
|
* Nutzt react-native-audio-recorder-player fuer Aufnahme.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Platform, PermissionsAndroid, NativeModules, ToastAndroid } from 'react-native';
|
import { Platform, PermissionsAndroid, NativeModules, ToastAndroid, NativeEventEmitter } from 'react-native';
|
||||||
import Sound from 'react-native-sound';
|
import Sound from 'react-native-sound';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
@@ -272,6 +272,21 @@ class AudioService {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.recorder = new AudioRecorderPlayer();
|
this.recorder = new AudioRecorderPlayer();
|
||||||
this.recorder.setSubscriptionDuration(0.1); // 100ms Metering-Updates
|
this.recorder.setSubscriptionDuration(0.1); // 100ms Metering-Updates
|
||||||
|
// Native Event: AudioTrack hat alle Samples wirklich durchgespielt (nach
|
||||||
|
// dem finally{}-Block im Writer-Thread). ERST jetzt darf AudioFocus
|
||||||
|
// freigegeben werden — sonst spielt Spotify schon waehrend ARIA noch
|
||||||
|
// redet (PcmStreamPlayer.end() returnt mit 15s-Cap viel zu frueh).
|
||||||
|
if (PcmStreamPlayer) {
|
||||||
|
try {
|
||||||
|
const emitter = new NativeEventEmitter(NativeModules.PcmStreamPlayer as any);
|
||||||
|
emitter.addListener('PcmPlaybackFinished', () => {
|
||||||
|
console.log('[Audio] PcmPlaybackFinished — Focus jetzt freigeben');
|
||||||
|
this._releaseFocusDeferred();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Audio] PcmPlaybackFinished-Subscription fehlgeschlagen:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** AudioFocus mit kleiner Verzoegerung freigeben — Spotify/YouTube
|
/** AudioFocus mit kleiner Verzoegerung freigeben — Spotify/YouTube
|
||||||
@@ -741,13 +756,16 @@ class AudioService {
|
|||||||
|
|
||||||
if (isFinal) {
|
if (isFinal) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
// end() resolved jetzt erst wenn der native Writer-Thread fertig
|
// end() signalisiert dem Writer "keine weiteren Chunks". Aber WIR
|
||||||
// ist (alle Samples ausgespielt) — danach AudioFocus verzoegert
|
// releasen den AudioFocus NICHT hier — der writer braucht u.U. noch
|
||||||
// freigeben, damit Spotify/YouTube nicht im Mikro-Gap zwischen zwei
|
// 30+ Sekunden bis der Buffer wirklich abgespielt ist. Den release
|
||||||
// ARIA-Antworten wieder hochdrehen. Wenn ein neuer Stream innerhalb
|
// triggert das native Event "PcmPlaybackFinished" wenn AudioTrack
|
||||||
// FOCUS_RELEASE_DELAY_MS startet, wird das Release abgebrochen.
|
// wirklich am Ende ist (siehe ensurePlaybackFinishedListener).
|
||||||
try { await PcmStreamPlayer!.end(); } catch {}
|
try { await PcmStreamPlayer!.end(); } catch {}
|
||||||
this._releaseFocusDeferred();
|
// playbackFinished-Listener informieren (UI-Logik)
|
||||||
|
this.playbackFinishedListeners.forEach(cb => {
|
||||||
|
try { cb(); } catch (e) { console.warn('[Audio] playbackFinished cb err:', e); }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.pcmStreamActive = false;
|
this.pcmStreamActive = false;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user