PlayerAndroid/app/src/main/java/software/lavender/music/player/ExoPlayerPlayback.kt

209 lines
6.2 KiB
Kotlin

package software.lavender.music.player
import android.content.Context
import android.content.Intent
import android.media.audiofx.AudioEffect
import android.os.Handler
import android.os.HandlerThread
import android.os.PowerManager
import android.widget.Toast
import code.name.monkey.retromusic.service.playback.Playback
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import software.lavender.music.extensions.isCurrentThreadCompat
// TODO: implement audio offloading, see: https://exoplayer.dev/battery-consumption.html
class ExoPlayerPlayback(private val context: Context) : Playback, Player.Listener {
private val handler = Handler(HandlerThread("ExoPlayerHandler").apply { start() }.looper)
private val dispatcher = handler.asCoroutineDispatcher("ExoPlayerDispatcher")
private val player =
ExoPlayer.Builder(context).setWakeMode(PowerManager.PARTIAL_WAKE_LOCK).setLooper(handler.looper).build()
private var initialized: Boolean = false
override val isInitialized: Boolean
get() = initialized
private var playing = false
override val isPlaying: Boolean
get() = isInitialized && ensurePlayerThread { player.isPlaying }
private var sessionId = -1
override val audioSessionId: Int
get() = ensurePlayerThread { player.audioSessionId }
private var callbacks: Playback.PlaybackCallbacks? = null
private var hasNext = false
override fun setDataSource(path: String): Boolean {
Toast.makeText(
context,
path,
Toast.LENGTH_SHORT
).show()
val success = ensurePlayerThread {
initialized = false
try {
player.stop()
player.clearMediaItems()
player.setMediaItem(MediaItem.fromUri(path))
player.prepare()
true
} catch (e: Exception) {
Toast.makeText(
context,
e.message,
Toast.LENGTH_SHORT
).show()
e.printStackTrace()
initialized = false
false
}
}
if (!success) {
return initialized
}
// TODO: listeners?
ensurePlayerThread {
player.addListener(this)
}
val intent = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
context.sendBroadcast(intent)
initialized = true
return initialized
}
override fun setNextDataSource(path: String?) = ensurePlayerThread {
if (path == null) {
return@ensurePlayerThread
}
player.addMediaItem(MediaItem.fromUri(path))
hasNext = true
// TODO: i think we need to do more stuff here
// TODO: gapless?
}
override fun setCallbacks(callbacks: Playback.PlaybackCallbacks) {
this.callbacks = callbacks
}
override fun start() = ensurePlayerThread {
try {
player.play()
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
override fun stop() = ensurePlayerThread {
player.stop()
player.clearMediaItems()
initialized = false
}
override fun release() = ensurePlayerThread {
player.stop()
player.clearMediaItems()
initialized = false
player.release()
}
override fun pause() = ensurePlayerThread {
try {
player.pause()
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
override fun duration() = if (!initialized) -1 else ensurePlayerThread {
try {
player.duration.toInt()
} catch (e: Exception) {
e.printStackTrace()
-1
}
}
override fun position() = if (!initialized) -1 else ensurePlayerThread {
try {
player.currentPosition.toInt()
} catch (e: Exception) {
e.printStackTrace()
-1
}
}
// TODO: do seeking via
override fun seek(whereto: Int) = ensurePlayerThread {
try {
player.seekTo(whereto.toLong())
player.currentPosition.toInt()
} catch (e: Exception) {
e.printStackTrace()
-1
}
}
override fun setVolume(vol: Float) = ensurePlayerThread {
try {
player.volume = vol
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
override fun setAudioSessionId(sessionId: Int) = ensurePlayerThread {
try {
player.audioSessionId = sessionId
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
override fun setCrossFadeDuration(duration: Int) {
// TODO: can we do crossfading in exoplayer directly??
}
override fun onPlayerError(error: PlaybackException) {
Toast.makeText(
context,
error.message,
Toast.LENGTH_SHORT
).show()
}
// TODO: tbh with first class playlist support in exoplayer we should probably get rid of the weird track switching behaviour asap
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (hasNext) {
hasNext = false
callbacks?.onTrackWentToNext()
} else {
callbacks?.onTrackEnded()
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
playing = isPlaying
}
override fun onAudioSessionIdChanged(audioSessionId: Int) {
sessionId = audioSessionId
}
private fun <T> ensurePlayerThread(block: () -> T): T =
if (player.playbackLooper.isCurrentThreadCompat) block() else runBlocking(dispatcher) { block() }
}