209 lines
6.2 KiB
Kotlin
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() }
|
|
} |