funkwhale-app-android/app/src/main/java/audio/funkwhale/ffa/playback/PlayerService.kt

582 lines
17 KiB
Kotlin
Raw Normal View History

package audio.funkwhale.ffa.playback
2019-08-19 16:50:33 +02:00
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.media.MediaMetadata
2019-08-19 16:50:33 +02:00
import android.os.Build
2019-10-21 11:51:32 +02:00
import android.os.IBinder
import android.support.v4.media.MediaMetadataCompat
2023-10-02 20:30:09 +02:00
import android.util.Log
2020-07-12 20:46:33 +02:00
import android.view.KeyEvent
import androidx.core.app.NotificationManagerCompat
import androidx.media.session.MediaButtonReceiver
2021-07-21 09:11:07 +02:00
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.model.Track
2021-09-09 09:56:15 +02:00
import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.CoverArt
2021-09-09 09:56:15 +02:00
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.FFACache
import audio.funkwhale.ffa.utils.HeadphonesUnpluggedReceiver
import audio.funkwhale.ffa.utils.ProgressBus
import audio.funkwhale.ffa.utils.Request
import audio.funkwhale.ffa.utils.RequestBus
import audio.funkwhale.ffa.utils.Response
import audio.funkwhale.ffa.utils.log
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.onApi
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.IllegalSeekPositionException
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.Tracks
import com.preference.PowerPreference
2021-09-09 09:56:15 +02:00
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
2019-08-19 16:50:33 +02:00
import kotlinx.coroutines.Dispatchers.Main
2021-09-09 09:56:15 +02:00
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.koin.java.KoinJavaComponent.inject
2019-08-19 16:50:33 +02:00
class PlayerService : Service() {
companion object {
const val INITIAL_COMMAND_KEY = "start_command"
}
private val mediaSession: MediaSession by inject(MediaSession::class.java)
private var started = false
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
2019-08-19 16:50:33 +02:00
private lateinit var audioManager: AudioManager
private var audioFocusRequest: AudioFocusRequest? = null
private val audioFocusChangeListener = AudioFocusChange()
private var stateWhenLostFocus = false
private lateinit var queue: QueueManager
2019-08-19 16:50:33 +02:00
private lateinit var mediaControlsManager: MediaControlsManager
private lateinit var player: ExoPlayer
2019-08-19 16:50:33 +02:00
private val mediaMetadataBuilder = MediaMetadataCompat.Builder()
2019-08-19 16:50:33 +02:00
private lateinit var playerEventListener: PlayerEventListener
private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver()
private var progressCache = Triple(0, 0, 0)
private lateinit var radioPlayer: RadioPlayer
2019-08-19 16:50:33 +02:00
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.action?.let {
if (it == Intent.ACTION_MEDIA_BUTTON) {
2020-07-12 20:46:33 +02:00
intent.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
when (key.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
2021-07-02 13:55:49 +02:00
if (hasAudioFocus(true)) MediaButtonReceiver.handleIntent(
mediaSession.session,
2021-07-02 13:55:49 +02:00
intent
)
2020-07-12 20:46:33 +02:00
Unit
}
else -> MediaButtonReceiver.handleIntent(mediaSession.session, intent)
2020-07-12 20:46:33 +02:00
}
}
}
}
if (!started) {
watchEventBus()
}
started = true
2019-08-19 16:50:33 +02:00
return START_STICKY
}
2020-07-12 20:46:33 +02:00
@SuppressLint("NewApi")
2019-08-19 16:50:33 +02:00
override fun onCreate() {
super.onCreate()
queue = QueueManager(this)
radioPlayer = RadioPlayer(this, scope)
2019-08-19 16:50:33 +02:00
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
2020-07-12 20:46:33 +02:00
Build.VERSION_CODES.O.onApi {
2019-08-19 16:50:33 +02:00
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
2021-09-09 09:56:15 +02:00
setAudioAttributes(
AudioAttributes.Builder().run {
setUsage(AudioAttributes.USAGE_MEDIA)
setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
2019-08-19 16:50:33 +02:00
2021-09-09 09:56:15 +02:00
setAcceptsDelayedFocusGain(true)
setOnAudioFocusChangeListener(audioFocusChangeListener)
2019-08-19 16:50:33 +02:00
2021-09-09 09:56:15 +02:00
build()
}
)
2019-08-19 16:50:33 +02:00
build()
}
}
mediaControlsManager = MediaControlsManager(this, scope, mediaSession.session)
2019-08-19 16:50:33 +02:00
player = ExoPlayer.Builder(this).build().apply {
2019-08-19 16:50:33 +02:00
playWhenReady = false
playerEventListener = PlayerEventListener().also {
addListener(it)
}
EventBus.send(Event.StateChanged(this.isPlaying()))
}
2019-08-19 16:50:33 +02:00
mediaSession.active = true
mediaSession.connector.apply {
setPlayer(player)
2019-08-19 16:50:33 +02:00
setMediaMetadataProvider {
buildTrackMetadata(queue.current())
2019-08-19 16:50:33 +02:00
}
}
if (queue.current > -1) {
player.setMediaSource(queue.dataSources)
player.prepare()
FFACache.getLine(this, "progress")?.let {
try {
player.seekTo(queue.current, it.toLong())
val (current, duration, percent) = getProgress(true)
ProgressBus.send(current, duration, percent)
} catch (e: IllegalSeekPositionException) {
// The app remembered an incorrect position, let's reset it
FFACache.set(this, "current", "-1")
}
}
2019-08-19 16:50:33 +02:00
}
2021-07-02 13:55:49 +02:00
registerReceiver(
headphonesUnpluggedReceiver,
IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
)
2019-08-19 16:50:33 +02:00
}
private fun watchEventBus() {
scope.launch(Main) {
CommandBus.get().collect { command ->
2022-08-26 14:06:41 +02:00
if (command is Command.RefreshService) {
if (queue.metadata.isNotEmpty()) {
CommandBus.send(Command.RefreshTrack(queue.current()))
2022-08-26 14:06:41 +02:00
EventBus.send(Event.StateChanged(player.playWhenReady))
2019-08-19 16:50:33 +02:00
}
2022-08-26 14:06:41 +02:00
} else if (command is Command.ReplaceQueue) {
if (!command.fromRadio) radioPlayer.stop()
queue.replace(command.queue)
player.setMediaSource(queue.dataSources)
player.prepare()
2022-08-26 14:06:41 +02:00
setPlaybackState(true)
CommandBus.send(Command.RefreshTrack(queue.current()))
} else if (command is Command.AddToQueue) {
queue.append(command.tracks)
} else if (command is Command.PlayNext) {
queue.insertNext(command.track)
} else if (command is Command.RemoveFromQueue) {
queue.remove(command.track)
} else if (command is Command.MoveFromQueue) {
queue.move(command.oldPosition, command.newPosition)
} else if (command is Command.PlayTrack) {
queue.current = command.index
player.seekTo(command.index, C.TIME_UNSET)
setPlaybackState(true)
CommandBus.send(Command.RefreshTrack(queue.current()))
} else if (command is Command.ToggleState) {
togglePlayback()
} else if (command is Command.SetState) {
setPlaybackState(command.state)
} else if (command is Command.NextTrack) {
skipToNextTrack()
} else if (command is Command.PreviousTrack) {
skipToPreviousTrack()
} else if (command is Command.Seek) {
seek(command.progress)
} else if (command is Command.ClearQueue) {
queue.clear()
player.stop()
} else if (command is Command.ShuffleQueue) {
queue.shuffle()
} else if (command is Command.PlayRadio) {
queue.clear()
radioPlayer.play(command.radio)
} else if (command is Command.SetRepeatMode) {
player.repeatMode = command.mode
} else if (command is Command.PinTrack) {
PinService.download(this@PlayerService, command.track)
} else if (command is Command.PinTracks) {
command.tracks.forEach {
2021-07-02 13:55:49 +02:00
PinService.download(
this@PlayerService,
it
)
}
2019-08-19 16:50:33 +02:00
}
}
}
2019-08-19 16:50:33 +02:00
scope.launch(Main) {
2019-10-31 16:17:37 +01:00
RequestBus.get().collect { request ->
2022-08-26 14:06:41 +02:00
if (request is Request.GetCurrentTrack) {
request.channel?.trySend(Response.CurrentTrack(queue.current()))?.isSuccess
2023-01-04 14:28:44 +01:00
} else if (request is Request.GetCurrentTrackIndex) {
request.channel?.trySend(Response.CurrentTrackIndex(queue.currentIndex()))?.isSuccess
2022-08-26 14:06:41 +02:00
} else if (request is Request.GetState) {
request.channel?.trySend(Response.State(player.playWhenReady))?.isSuccess
} else if (request is Request.GetQueue) {
request.channel?.trySend(Response.Queue(queue.get()))?.isSuccess
2019-08-19 16:50:33 +02:00
}
}
}
2019-08-19 16:50:33 +02:00
scope.launch(Main) {
2019-08-19 16:50:33 +02:00
while (true) {
delay(1000)
val (current, duration, percent) = getProgress()
2019-08-19 16:50:33 +02:00
if (player.playWhenReady) {
ProgressBus.send(current, duration, percent)
}
}
}
2019-08-19 16:50:33 +02:00
}
2019-10-21 11:51:32 +02:00
override fun onBind(intent: Intent?): IBinder? = null
2019-08-19 16:50:33 +02:00
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if (!player.playWhenReady) {
NotificationManagerCompat.from(this).cancelAll()
stopSelf()
}
}
2019-08-19 16:50:33 +02:00
@SuppressLint("NewApi")
override fun onDestroy() {
scope.cancel()
2019-08-19 16:50:33 +02:00
try {
unregisterReceiver(headphonesUnpluggedReceiver)
} catch (_: Exception) {
}
Build.VERSION_CODES.O.onApi(
{
audioFocusRequest?.let {
audioManager.abandonAudioFocusRequest(it)
}
},
{
@Suppress("DEPRECATION")
audioManager.abandonAudioFocus(audioFocusChangeListener)
2021-09-09 09:56:15 +02:00
}
)
2019-08-19 16:50:33 +02:00
player.removeListener(playerEventListener)
setPlaybackState(false)
2019-08-19 16:50:33 +02:00
player.release()
mediaSession.active = false
2019-08-19 16:50:33 +02:00
super.onDestroy()
}
private fun setPlaybackState(state: Boolean) {
if (!state) {
val (progress, _, _) = getProgress()
FFACache.set(this@PlayerService, "progress", progress.toString())
}
2019-08-19 16:50:33 +02:00
if (state && player.playbackState == Player.STATE_IDLE) {
player.setMediaSource(queue.dataSources)
player.prepare()
2019-08-19 16:50:33 +02:00
}
2020-07-12 20:46:33 +02:00
if (hasAudioFocus(state)) {
2019-08-19 16:50:33 +02:00
player.playWhenReady = state
EventBus.send(Event.StateChanged(state))
}
}
private fun togglePlayback() {
setPlaybackState(!player.isPlaying)
2019-08-19 16:50:33 +02:00
}
private fun skipToPreviousTrack() {
2019-08-19 16:50:33 +02:00
if (player.currentPosition > 5000) {
return player.seekTo(0)
}
player.seekToPrevious()
2019-08-19 16:50:33 +02:00
}
private fun skipToNextTrack() {
player.seekToNext()
FFACache.set(this@PlayerService, "progress", "0")
ProgressBus.send(0, 0, 0)
}
private fun getProgress(force: Boolean = false): Triple<Int, Int, Int> {
if (!player.playWhenReady && !force) return progressCache
2019-08-19 16:50:33 +02:00
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 seek(value: Int) {
2019-08-19 16:50:33 +02:00
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())
}
private fun buildTrackMetadata(track: Track?): MediaMetadataCompat {
track?.let {
2020-08-08 14:51:39 +02:00
val coverUrl = maybeNormalizeUrl(track.album?.cover())
return mediaMetadataBuilder.apply {
putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.title)
putString(MediaMetadataCompat.METADATA_KEY_ARTIST, track.artist.name)
2021-07-02 13:55:49 +02:00
putLong(
MediaMetadata.METADATA_KEY_DURATION,
(track.bestUpload()?.duration?.toLong() ?: 0L) * 1000
)
try {
runBlocking(IO) {
2021-07-02 13:55:49 +02:00
this@apply.putBitmap(
MediaMetadataCompat.METADATA_KEY_ALBUM_ART,
CoverArt.withContext(this@PlayerService.applicationContext, coverUrl).get()
2021-07-02 13:55:49 +02:00
)
}
} catch (_: Exception) {
}
}.build()
}
return mediaMetadataBuilder.build()
}
2020-07-12 20:46:33 +02:00
@SuppressLint("NewApi")
private fun hasAudioFocus(state: Boolean): Boolean {
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")
2021-07-02 13:55:49 +02:00
audioManager.requestAudioFocus(
audioFocusChangeListener,
AudioAttributes.CONTENT_TYPE_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
).let {
2020-07-12 20:46:33 +02:00
allowed = when (it) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> true
else -> false
}
}
}
)
}
return allowed
}
private fun skipBackwardsAfterPause(): Int {
val deltaPref = PowerPreference.getDefaultFile().getString("auto_skip_backwards_on_pause")
val delta = deltaPref.toFloatOrNull()
return if (delta == null) 0 else (delta * 1000).toInt()
}
@SuppressLint("NewApi")
inner class PlayerEventListener : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
mediaControlsManager.updateNotification(queue.current(), isPlaying)
if (!isPlaying) {
val delta = skipBackwardsAfterPause()
val (current, duration, _) = getProgress(true)
val position = if (current > delta) current - delta else 0
player.seekTo(position.toLong())
ProgressBus.send(position, duration, ((position.toFloat()) / duration / 10).toInt())
}
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
2019-08-19 16:50:33 +02:00
EventBus.send(Event.StateChanged(playWhenReady))
2019-08-19 16:50:33 +02:00
if (queue.current == -1) {
CommandBus.send(Command.RefreshTrack(queue.current()))
2019-08-19 16:50:33 +02:00
}
if (!playWhenReady) {
Build.VERSION_CODES.N.onApi(
{ stopForeground(STOP_FOREGROUND_DETACH) },
{ stopForeground(false) }
)
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
2023-10-02 20:30:09 +02:00
when (playbackState) {
2023-10-02 20:30:09 +02:00
Player.STATE_BUFFERING -> {
EventBus.send(Event.Buffering(true))
}
Player.STATE_ENDED -> {
setPlaybackState(false)
queue.current = 0
player.seekTo(0, C.TIME_UNSET)
2019-08-19 16:50:33 +02:00
ProgressBus.send(0, 0, 0)
2019-08-19 16:50:33 +02:00
}
Player.STATE_IDLE -> {
setPlaybackState(false)
2019-08-19 16:50:33 +02:00
EventBus.send(Event.PlaybackStopped)
if (!player.playWhenReady) {
mediaControlsManager.remove()
2019-08-19 16:50:33 +02:00
}
}
2023-10-02 20:30:09 +02:00
Player.STATE_READY -> {
EventBus.send(Event.Buffering(false))
}
2019-08-19 16:50:33 +02:00
}
}
override fun onTracksChanged(tracks: Tracks) {
super.onTracksChanged(tracks)
2019-08-19 16:50:33 +02:00
if (queue.current != player.currentMediaItemIndex) {
queue.current = player.currentMediaItemIndex
mediaControlsManager.updateNotification(queue.current(), player.isPlaying)
}
2019-08-19 16:50:33 +02:00
2022-08-25 14:58:19 +02:00
if (queue.get().isNotEmpty() &&
queue.current() == queue.get().last() && radioPlayer.isActive()
2021-07-02 13:55:49 +02:00
) {
scope.launch(IO) {
if (radioPlayer.lock.tryAcquire()) {
radioPlayer.prepareNextTrack()
radioPlayer.lock.release()
}
}
}
FFACache.set(this@PlayerService, "current", queue.current.toString())
2019-08-19 16:50:33 +02:00
CommandBus.send(Command.RefreshTrack(queue.current()))
}
2021-08-29 15:41:50 +02:00
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
val currentTrack = queue.current().also {
it.log("Track finished")
}
EventBus.send(Event.TrackFinished(currentTrack))
}
2019-08-19 16:50:33 +02:00
}
override fun onPlayerError(error: PlaybackException) {
EventBus.send(Event.PlaybackError(getString(R.string.error_playback)))
2019-08-19 16:50:33 +02:00
if (player.playWhenReady) {
queue.current++
player.setMediaSource(queue.dataSources, true)
player.seekTo(queue.current, 0)
player.prepare()
CommandBus.send(Command.RefreshTrack(queue.current()))
}
2019-08-19 16:50:33 +02:00
}
}
inner class AudioFocusChange : AudioManager.OnAudioFocusChangeListener {
override fun onAudioFocusChange(focus: Int) {
when (focus) {
AudioManager.AUDIOFOCUS_GAIN -> {
player.volume = 1f
setPlaybackState(stateWhenLostFocus)
2019-08-19 16:50:33 +02:00
stateWhenLostFocus = false
}
AudioManager.AUDIOFOCUS_LOSS -> {
stateWhenLostFocus = false
setPlaybackState(false)
2019-08-19 16:50:33 +02:00
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
stateWhenLostFocus = player.playWhenReady
setPlaybackState(false)
2019-08-19 16:50:33 +02:00
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
stateWhenLostFocus = player.playWhenReady
player.volume = 0.3f
}
}
}
}
2021-07-02 13:55:49 +02:00
}