Minor playback improvements

Improve the code design and fix some bugs with PlaybackService and friends.
This commit is contained in:
OxygenCobalt 2020-10-30 11:39:55 -06:00
parent 2ded706445
commit 6fc034e376
6 changed files with 109 additions and 62 deletions

View file

@ -105,9 +105,13 @@ class MainFragment : Fragment() {
}
}
// Start the playback service.
Intent(requireContext(), PlaybackService::class.java).also {
requireContext().startService(it)
// Start the playback service [If not already]
if (!playbackModel.serviceStarted) {
Intent(requireContext(), PlaybackService::class.java).also {
requireContext().startService(it)
}
playbackModel.setServiceStatus(true)
}
Log.d(this::class.simpleName, "Fragment Created.")

View file

@ -72,10 +72,16 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
// --- VIEWMODEL SETUP --
playbackModel.song.observe(viewLifecycleOwner) {
Log.d(this::class.simpleName, "Updating song display to ${it.name}.")
if (it != null) {
Log.d(this::class.simpleName, "Updating song display to ${it.name}.")
binding.song = it
binding.playbackSeekBar.max = it.seconds.toInt()
binding.song = it
binding.playbackSeekBar.max = it.seconds.toInt()
} else {
Log.d(this::class.simpleName, "No song played anymore, leaving.")
findNavController().navigateUp()
}
}
playbackModel.index.observe(viewLifecycleOwner) {

View file

@ -39,6 +39,7 @@ private const val NOTIF_ID = 0xA0A0
// A Service that manages the single ExoPlayer instance and [attempts] to keep
// persistence if the app closes.
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
// TODO: Use the ExoPlayer queue functionality [To an extent]? Could make things faster.
private val player: SimpleExoPlayer by lazy {
val p = SimpleExoPlayer.Builder(applicationContext).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@ -50,13 +51,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
private val playbackManager = PlaybackStateManager.getInstance()
private lateinit var mediaSession: MediaSessionCompat
private val buttonMediaCallback = object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
Log.d(this::class.simpleName, "Hello?")
return true
}
}
private val serviceJob = Job()
private val serviceScope = CoroutineScope(
@ -67,19 +61,20 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
private lateinit var notification: Notification
// --- SERVICE OVERRIDES ---
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(this::class.simpleName, "Service is active.")
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onBind(intent: Intent): IBinder? = null
override fun onCreate() {
super.onCreate()
// Set up the media button callbacks
mediaSession = MediaSessionCompat(this, packageName).apply {
isActive = true
}
@ -87,30 +82,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
val connector = MediaSessionConnector(mediaSession)
connector.setPlayer(player)
connector.setMediaButtonEventHandler { _, _, mediaButtonEvent ->
val item = mediaButtonEvent
.getParcelableExtra<Parcelable>(Intent.EXTRA_KEY_EVENT) as KeyEvent
if (item.action == KeyEvent.ACTION_DOWN) {
when (item.keyCode) {
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
playbackManager.next()
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playbackManager.prev()
}
// TODO: Implement the other callbacks for
// CLOSE/STOP & REWIND
}
}
true
handleMediaButtonEvent(mediaButtonEvent)
}
notification = createNotification()
@ -121,14 +93,19 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
override fun onDestroy() {
super.onDestroy()
stopForeground(true)
// Release everything that could cause a memory leak if left around
player.release()
mediaSession.release()
serviceJob.cancel()
playbackManager.removeCallback(this)
stopForeground(true)
Log.d(this::class.simpleName, "Service destroyed.")
}
// --- PLAYER EVENT LISTENER OVERRIDES ---
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_ENDED) {
playbackManager.next()
@ -137,28 +114,31 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
}
}
// --- PLAYBACK STATE CALLBACK OVERRIDES ---
override fun onSongUpdate(song: Song?) {
song?.let {
if (!isForeground) {
startForeground(NOTIF_ID, notification)
isForeground = true
}
val item = MediaItem.fromUri(it.id.toURI())
player.setMediaItem(item)
player.prepare()
player.play()
return
}
player.stop()
}
override fun onPlayingUpdate(isPlaying: Boolean) {
if (isPlaying) {
if (isPlaying && !player.isPlaying) {
player.play()
startForeground(NOTIF_ID, notification)
startPollingPosition()
} else {
player.pause()
stopForeground(false)
}
}
@ -166,25 +146,63 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
player.seekTo(position * 1000)
}
// --- OTHER FUNCTIONS ---
// Awful Hack to get position polling to work, as exoplayer does not provide any
// onPositionChanged callback for some inane reason.
// FIXME: Don't be surprised if this causes problems.
// FIXME: There has to be a better way of polling positions.
private fun pollCurrentPosition() = flow {
while (player.currentPosition <= player.duration) {
while (player.isPlaying) {
emit(player.currentPosition)
delay(500)
delay(250)
}
}.conflate()
private fun startPollingPosition() {
serviceScope.launch {
pollCurrentPosition().takeWhile { true }.collect {
pollCurrentPosition().takeWhile { player.isPlaying }.collect {
playbackManager.setPosition(it / 1000)
}
}
}
// Handle a media button event.
private fun handleMediaButtonEvent(event: Intent): Boolean {
val item = event
.getParcelableExtra<Parcelable>(Intent.EXTRA_KEY_EVENT) as KeyEvent
if (item.action == KeyEvent.ACTION_DOWN) {
return when (item.keyCode) {
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
true
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
playbackManager.next()
true
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playbackManager.prev()
true
}
// TODO: Implement the other callbacks for
// CLOSE/STOP & REWIND
else -> false
}
}
return false
}
// Create a notification
// TODO: Spin this off into its own object!
private fun createNotification(): Notification {
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -197,7 +215,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
)
notificationManager.createNotificationChannel(channel)
}
// TODO: Placeholder, implement proper media controls :)
// TODO: Placeholder, implement proper media controls.
val notif = NotificationCompat.Builder(
applicationContext,
CHANNEL_ID

View file

@ -18,10 +18,10 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
// TODO: Implement Looping Modes
// TODO: Implement User Queue
// TODO: Implement Persistence through Bundles/Databases/Idk
class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
// Playback
private val mSong = MutableLiveData<Song>()
val song: LiveData<Song> get() = mSong
private val mSong = MutableLiveData<Song?>()
val song: LiveData<Song?> get() = mSong
private val mPosition = MutableLiveData(0L)
val position: LiveData<Long> get() = mPosition
@ -44,6 +44,9 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
private val mIsSeeking = MutableLiveData(false)
val isSeeking: LiveData<Boolean> get() = mIsSeeking
private var mServiceStarted = false
val serviceStarted: Boolean get() = mServiceStarted
val formattedPosition = Transformations.map(mPosition) {
it.toDuration()
}
@ -179,6 +182,10 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
mIsSeeking.value = value
}
fun setServiceStatus(value: Boolean) {
mServiceStarted = value
}
// --- OVERRIDES ---
override fun onCleared() {
@ -186,9 +193,7 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback {
}
override fun onSongUpdate(song: Song?) {
song?.let {
mSong.value = it
}
mSong.value = song
}
override fun onPositionUpdate(position: Long) {

View file

@ -9,5 +9,7 @@ interface PlaybackStateCallback {
fun onPlayingUpdate(isPlaying: Boolean) {}
fun onShuffleUpdate(isShuffling: Boolean) {}
fun onIndexUpdate(index: Int) {}
// Service callbacks
fun onSeekConfirm(position: Long) {}
}

View file

@ -165,7 +165,13 @@ class PlaybackStateManager {
}
fun setPosition(position: Long) {
mPosition = position
// Due to the hacky way I poll ExoPlayer positions, don't accept any bugged positions
// that are over the duration of the song.
mSong?.let {
if (position <= it.seconds) {
mPosition = position
}
}
}
fun seekTo(position: Long) {
@ -177,8 +183,14 @@ class PlaybackStateManager {
// --- QUEUE FUNCTIONS ---
fun next() {
if (mIndex < mQueue.size) {
if (mIndex < mQueue.lastIndex) {
mIndex = mIndex.inc()
} else {
// TODO: Implement option so that the playlist loops instead of stops
mQueue = mutableListOf()
mSong = null
return
}
updatePlayback(mQueue[mIndex])