149 lines
5.1 KiB
Kotlin
149 lines
5.1 KiB
Kotlin
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"
|
||
}
|
||
}
|