Files
GPS2Audio/app/src/main/kotlin/de/waypointaudio/service/AudioPlayer.kt
T
2026-05-30 05:42:44 +00:00

149 lines
5.1 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
}
}