Initial release of GPS2Audio

This commit is contained in:
Marcel Mayer
2026-05-05 20:54:05 +02:00
commit b2aa1c167c
48 changed files with 11570 additions and 0 deletions
@@ -0,0 +1,110 @@
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
}
companion object {
private const val TAG = "AudioPlayer"
}
}