Otter-App-Android-Funkwhale/app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt

461 lines
14 KiB
Kotlin

package com.github.apognu.otter.playback
import android.annotation.SuppressLint
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.os.Build
import android.os.IBinder
import android.support.v4.media.session.MediaSessionCompat
import android.view.KeyEvent
import com.github.apognu.otter.Cast
import com.github.apognu.otter.R
import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class PlayerService : Service() {
interface OnPlayerSwitchListener {
fun switchToLocal()
fun switchToRemote()
}
private lateinit var queue: QueueManager
private val jobs = mutableListOf<Job>()
private lateinit var audioManager: AudioManager
private var audioFocusRequest: AudioFocusRequest? = null
private val audioFocusChangeListener = AudioFocusChange()
private var stateWhenLostFocus = false
private lateinit var mediaControlsManager: MediaControlsManager
private lateinit var mediaSession: MediaSessionCompat
private lateinit var player: Player
private lateinit var localPlayer: SimpleExoPlayer
private var cast: Cast? = null
private lateinit var playerEventListener: PlayerEventListener
private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver()
private var progressCache = Triple(0, 0, 0)
private lateinit var radioPlayer: RadioPlayer
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (jobs.isEmpty()) watchEventBus()
return START_STICKY
}
override fun onCreate() {
super.onCreate()
radioPlayer = RadioPlayer(this)
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
setAudioAttributes(AudioAttributes.Builder().run {
setUsage(AudioAttributes.USAGE_MEDIA)
setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
setAcceptsDelayedFocusGain(true)
setOnAudioFocusChangeListener(audioFocusChangeListener)
build()
})
build()
}
}
mediaSession = MediaSessionCompat(this, applicationContext.packageName).apply {
isActive = true
}
mediaControlsManager = MediaControlsManager(this, mediaSession)
localPlayer = SimpleExoPlayer.Builder(this).build().apply {
playWhenReady = false
playerEventListener = PlayerEventListener().also {
addListener(it)
}
cast = Cast.get(this@PlayerService, PlayerSwitchListener(), playerEventListener)
MediaSessionConnector(mediaSession).also {
it.setPlayer(this)
it.setMediaButtonEventHandler { player, _, mediaButtonEvent ->
mediaButtonEvent.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
if (key.action == KeyEvent.ACTION_UP) {
when (key.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY -> state(true)
KeyEvent.KEYCODE_MEDIA_PAUSE -> state(false)
KeyEvent.KEYCODE_MEDIA_NEXT -> player.next()
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> previousTrack()
}
}
}
true
}
}
}
player = if (cast?.isCastSessionAvailable() == true) cast!!.getPlayer(this) else localPlayer
queue = QueueManager(this, cast)
if (queue.current > -1) {
player.onLocal()?.prepare(queue.datasources, true, true)
Cache.get(this, "progress")?.let { progress ->
player.seekTo(queue.current, progress.readLine().toLong())
val (current, duration, percent) = progress(true)
ProgressBus.send(current, duration, percent)
}
}
registerReceiver(headphonesUnpluggedReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
}
private fun watchEventBus() {
jobs.add(GlobalScope.launch(Main) {
CommandBus.get().collect { command ->
when (command) {
is Command.RefreshService -> {
EventBus.send(Event.QueueChanged)
if (queue.metadata.isNotEmpty()) {
CommandBus.send(Command.RefreshTrack(queue.current()))
EventBus.send(Event.StateChanged(player.playWhenReady))
}
}
is Command.ReplaceQueue -> {
if (!command.fromRadio) radioPlayer.stop()
queue.replace(command.queue)
player.onLocal()?.prepare(queue.datasources, true, true)
state(true)
CommandBus.send(Command.RefreshTrack(queue.current()))
}
is Command.AddToQueue -> queue.append(command.tracks)
is Command.PlayNext -> queue.insertNext(command.track)
is Command.RemoveFromQueue -> queue.remove(command.track)
is Command.MoveFromQueue -> queue.move(command.oldPosition, command.newPosition)
is Command.PlayTrack -> {
queue.current = command.index
player.seekTo(command.index, C.TIME_UNSET)
state(true)
CommandBus.send(Command.RefreshTrack(queue.current()))
}
is Command.ToggleState -> toggle()
is Command.SetState -> state(command.state)
is Command.NextTrack -> {
player.next()
Cache.set(this@PlayerService, "progress", "0".toByteArray())
ProgressBus.send(0, 0, 0)
}
is Command.PreviousTrack -> previousTrack()
is Command.Seek -> progress(command.progress)
is Command.ClearQueue -> queue.clear()
is Command.PlayRadio -> {
queue.clear()
radioPlayer.play(command.radio)
}
is Command.SetRepeatMode -> player.repeatMode = command.mode
is Command.PinTrack -> PinService.download(this@PlayerService, command.track)
is Command.PinTracks -> command.tracks.forEach { PinService.download(this@PlayerService, it) }
}
if (player.playWhenReady) {
mediaControlsManager.tick()
}
}
})
jobs.add(GlobalScope.launch(Main) {
RequestBus.get().collect { request ->
when (request) {
is Request.GetCurrentTrack -> request.channel?.offer(Response.CurrentTrack(queue.current()))
is Request.GetState -> request.channel?.offer(Response.State(player.playWhenReady))
is Request.GetQueue -> request.channel?.offer(Response.Queue(queue.get()))
}
}
})
jobs.add(GlobalScope.launch(Main) {
while (true) {
delay(1000)
val (current, duration, percent) = progress()
if (player.playWhenReady) {
ProgressBus.send(current, duration, percent)
}
}
})
}
override fun onBind(intent: Intent?): IBinder? = null
@SuppressLint("NewApi")
override fun onDestroy() {
jobs.forEach {
it.cancel()
jobs.remove(it)
}
try {
unregisterReceiver(headphonesUnpluggedReceiver)
} catch (_: Exception) {
}
Build.VERSION_CODES.O.onApi(
{
audioFocusRequest?.let {
audioManager.abandonAudioFocusRequest(it)
}
},
{
@Suppress("DEPRECATION")
audioManager.abandonAudioFocus(audioFocusChangeListener)
})
mediaSession.isActive = false
mediaSession.release()
player.removeListener(playerEventListener)
state(false)
player.release()
stopForeground(true)
stopSelf()
super.onDestroy()
}
@SuppressLint("NewApi")
private fun state(state: Boolean) {
if (!state) {
val (progress, _, _) = progress()
Cache.set(this@PlayerService, "progress", progress.toString().toByteArray())
}
if (state && player.playbackState == Player.STATE_IDLE) {
player.onLocal()?.prepare(queue.datasources)
}
var allowed = !state
if (!allowed) {
Build.VERSION_CODES.O.onApi(
{
audioFocusRequest?.let {
allowed = when (audioManager.requestAudioFocus(it)) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> true
else -> false
}
}
},
{
@Suppress("DEPRECATION")
audioManager.requestAudioFocus(audioFocusChangeListener, AudioAttributes.CONTENT_TYPE_MUSIC, AudioManager.AUDIOFOCUS_GAIN).let {
allowed = when (it) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> true
else -> false
}
}
}
)
}
if (allowed) {
player.playWhenReady = state
EventBus.send(Event.StateChanged(state))
}
}
private fun toggle() {
state(!player.playWhenReady)
}
private fun previousTrack() {
if (player.currentPosition > 5000) {
return player.seekTo(0)
}
player.previous()
}
private fun progress(force: Boolean = false): Triple<Int, Int, Int> {
if (!player.playWhenReady && !force) return progressCache
return queue.current()?.bestUpload()?.let { upload ->
val current = player.currentPosition
val duration = upload.duration.toFloat()
val percent = ((current / (duration * 1000)) * 100).toInt()
progressCache = Triple(current.toInt(), duration.toInt(), percent)
progressCache
} ?: Triple(0, 0, 0)
}
private fun progress(value: Int) {
val duration = ((queue.current()?.bestUpload()?.duration ?: 0) * (value.toFloat() / 100)) * 1000
progressCache = Triple(duration.toInt(), queue.current()?.bestUpload()?.duration ?: 0, value)
player.seekTo(duration.toLong())
}
inner class PlayerEventListener : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
super.onPlayerStateChanged(playWhenReady, playbackState)
EventBus.send(Event.StateChanged(playWhenReady))
if (queue.current == -1) {
CommandBus.send(Command.RefreshTrack(queue.current()))
}
when (playWhenReady) {
true -> {
when (playbackState) {
Player.STATE_READY -> player.onLocal()?.also { mediaControlsManager.updateNotification(queue.current(), true) }
Player.STATE_BUFFERING -> EventBus.send(Event.Buffering(true))
Player.STATE_ENDED -> EventBus.send(Event.PlaybackStopped)
}
if (playbackState != Player.STATE_BUFFERING) EventBus.send(Event.Buffering(false))
}
false -> {
EventBus.send(Event.Buffering(false))
if (playbackState == Player.STATE_READY) {
mediaControlsManager.updateNotification(queue.current(), false)
stopForeground(false)
}
}
}
}
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
super.onTracksChanged(trackGroups, trackSelections)
queue.current = player.currentWindowIndex
player.onLocal()?.also { mediaControlsManager.updateNotification(queue.current(), player.playWhenReady) }
if (queue.get().isNotEmpty() && queue.current() == queue.get().last() && radioPlayer.isActive()) {
GlobalScope.launch(IO) {
if (radioPlayer.lock.tryAcquire()) {
radioPlayer.prepareNextTrack()
radioPlayer.lock.release()
}
}
}
Cache.set(this@PlayerService, "current", queue.current.toString().toByteArray())
CommandBus.send(Command.RefreshTrack(queue.current()))
}
override fun onPositionDiscontinuity(reason: Int) {
super.onPositionDiscontinuity(reason)
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
EventBus.send(Event.TrackFinished(queue.current()))
}
}
override fun onPlayerError(error: ExoPlaybackException) {
EventBus.send(Event.PlaybackError(getString(R.string.error_playback)))
queue.current++
player.onLocal()?.prepare(queue.datasources, true, true)
player.seekTo(queue.current, 0)
player.playWhenReady = true
CommandBus.send(Command.RefreshTrack(queue.current()))
}
}
inner class AudioFocusChange : AudioManager.OnAudioFocusChangeListener {
override fun onAudioFocusChange(focus: Int) {
when (focus) {
AudioManager.AUDIOFOCUS_GAIN -> {
player.onLocal()?.volume = 1f
state(stateWhenLostFocus)
stateWhenLostFocus = false
}
AudioManager.AUDIOFOCUS_LOSS -> {
stateWhenLostFocus = false
state(false)
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
stateWhenLostFocus = player.playWhenReady
state(false)
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
stateWhenLostFocus = player.playWhenReady
player.onLocal()?.volume = 0.3f
}
}
}
}
inner class PlayerSwitchListener : OnPlayerSwitchListener {
override fun switchToLocal() {
player = localPlayer
}
override fun switchToRemote() {
cast?.let { cast ->
player = cast.getPlayer(this@PlayerService)
}
}
}
}