package de.waypointaudio.service import android.content.Context import android.media.MediaPlayer import android.net.Uri import android.util.Log /** * Einfacher Audio-Player für Wegpunkt-Sounds (GPS-Dienst). * Nutzt Android MediaPlayer; spielt Content-URIs oder File-URIs ab. * * Completion- und Error-Callbacks erlauben dem Aufrufer, exakt nach dem * Ende der Wiedergabe (oder bei einem Fehler) auf das Ereignis zu reagieren – * z. B. um die Begleitmusik korrekt wiederherzustellen. * * Thread-Sicherheit: Die Callbacks werden auf dem MediaPlayer-internen Thread * aufgerufen. Der Aufrufer ist dafür verantwortlich, ggf. in den richtigen * Dispatcher zu wechseln (z. B. Dispatchers.Main für ExoPlayer-Zugriffe). */ class AudioPlayer { private var mediaPlayer: MediaPlayer? = null /** * Spielt eine Audiodatei ab. * * @param context Android-Context * @param soundUri URI-String (content:// oder file://) * @param onCompletion Wird aufgerufen, wenn die Wiedergabe natürlich endet. * Wird auf dem MediaPlayer-Thread aufgerufen. * @param onError Wird aufgerufen, wenn die Wiedergabe nicht gestartet * werden kann oder ein MediaPlayer-Fehler auftritt. * Wird auf dem MediaPlayer-Thread (oder dem Aufruf-Thread * bei Initialisierungsfehlern) aufgerufen. */ fun play( context: Context, soundUri: String, onCompletion: (() -> Unit)? = null, onError: ((Exception?) -> Unit)? = null ) { if (soundUri.isBlank()) { Log.w(TAG, "Keine Audiodatei angegeben.") onError?.invoke(IllegalArgumentException("soundUri ist leer")) return } stop() // vorherige Wiedergabe stoppen // callbackFired verhindert Doppel-Callbacks: completion und error // schließen sich gegenseitig aus. var callbackFired = false runCatching { val uri = Uri.parse(soundUri) val mp = MediaPlayer() mediaPlayer = mp mp.setDataSource(context, uri) mp.prepare() mp.setOnCompletionListener { finishedMp -> if (!callbackFired) { callbackFired = true finishedMp.release() if (mediaPlayer === finishedMp) mediaPlayer = null Log.d(TAG, "Wiedergabe beendet: $soundUri") onCompletion?.invoke() } } mp.setOnErrorListener { errorMp, what, extra -> if (!callbackFired) { callbackFired = true errorMp.release() if (mediaPlayer === errorMp) mediaPlayer = null val msg = "MediaPlayer-Fehler: what=$what extra=$extra" Log.e(TAG, msg) onError?.invoke(RuntimeException(msg)) } true // Fehler wurde behandelt → kein zusätzlicher onCompletion-Aufruf } mp.start() }.onFailure { e -> Log.e(TAG, "Fehler beim Abspielen von $soundUri", e) // Ressource freigeben falls teilweise initialisiert runCatching { mediaPlayer?.release() } mediaPlayer = null if (!callbackFired) { callbackFired = true onError?.invoke(e as? Exception ?: RuntimeException(e)) } } } /** Stoppt die aktuelle Wiedergabe und gibt Ressourcen frei. */ fun stop() { runCatching { mediaPlayer?.let { if (it.isPlaying) it.stop() it.release() } } mediaPlayer = null } /** * Pausiert die aktuelle Wiedergabe ohne den MediaPlayer freizugeben. * Anschließend kann [resume] aufgerufen werden, um an derselben Position * weiterzuspielen. Wird verwendet, wenn PTT-Sprechen einsetzt und das * Wegpunkt-Audio während dieser Zeit nicht hörbar sein soll. */ fun pause() { runCatching { mediaPlayer?.let { if (it.isPlaying) it.pause() } }.onFailure { e -> Log.w(TAG, "Pause fehlgeschlagen", e) } } /** * Setzt eine zuvor mit [pause] angehaltene Wiedergabe fort. Wenn der * MediaPlayer bereits freigegeben wurde oder nicht im Pause-Zustand ist, * passiert nichts. */ fun resume() { runCatching { val mp = mediaPlayer ?: return if (!mp.isPlaying) mp.start() }.onFailure { e -> Log.w(TAG, "Resume fehlgeschlagen", e) } } /** True, wenn aktuell eine Wiedergabe aktiv ist (nicht pausiert/gestoppt). */ val isPlaying: Boolean get() = runCatching { mediaPlayer?.isPlaying == true }.getOrDefault(false) /** True, wenn ein MediaPlayer existiert (spielt oder pausiert). */ val hasContent: Boolean get() = mediaPlayer != null companion object { private const val TAG = "AudioPlayer" } }