Replace deprecated SimpleExoPlayer with ExoPlayer.

This is part of an effort to resolve deprecation warnings.

Most of this is simple refactoring of interfaces that change between
the two Player implementations.  There are a few other changes that
deserve further explanation.

Testing indicated that the play/pause button was being reset to pause
in MainActivity:refreshCurrentTrack.  In the past this was likely
masked by the ordering of other callbacks.  We have removed the
nowPlayingToggle.icon update from MainActivity, leaving that UI update
to PlayerService.

One of the bigger refactorings in PlayerService was forced by the
deprecation of Player.EventListener.onPlayerStateChanged.  That forced
separation of handling playWhenReady and playbackState transitions.
In the SimpleExoPlayer implementations, where these transitions were
combined, the module attempted to work out playing state from a
combination of these two state variables.

In addition to separating the reaction to these state changes, we have
added a listener to onIsPlayingChanged, eliminating the need for some
of the earlier logic in Player.EventListener.onPlayerStateChanged.
This addition, along with the separation of state transition
processing, seems to provide a simpler implementation.  But it is,
certainly, a possible source of bugs.
This commit is contained in:
Hugh Daschbach 2022-08-27 00:21:03 -07:00 committed by Ryan Harg
parent 24de54c7e0
commit d734953b54
No known key found for this signature in database
GPG Key ID: 89106F3A84E6958C
6 changed files with 76 additions and 76 deletions

View File

@ -167,18 +167,18 @@ dependencies {
implementation("com.google.android.material:material:1.6.1") implementation("com.google.android.material:material:1.6.1")
implementation("com.android.support.constraint:constraint-layout:2.0.4") implementation("com.android.support.constraint:constraint-layout:2.0.4")
implementation("com.google.android.exoplayer:exoplayer-core:2.14.2") implementation("com.google.android.exoplayer:exoplayer-core:2.18.1")
implementation("com.google.android.exoplayer:exoplayer-ui:2.14.2") implementation("com.google.android.exoplayer:exoplayer-ui:2.18.1")
implementation("com.google.android.exoplayer:extension-mediasession:2.14.2") implementation("com.google.android.exoplayer:extension-mediasession:2.18.1")
implementation("io.insert-koin:koin-core:3.1.2") implementation("io.insert-koin:koin-core:3.1.2")
implementation("io.insert-koin:koin-android:3.1.2") implementation("io.insert-koin:koin-android:3.1.2")
testImplementation("io.insert-koin:koin-test:3.1.2") testImplementation("io.insert-koin:koin-test:3.1.2")
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:2.14.0") { implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:789a4f83169cff5c7a91655bb828fde2cfde671a") {
isTransitive = false isTransitive = false
} }
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:2.14.0") { implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:789a4f83169cff5c7a91655bb828fde2cfde671a") {
isTransitive = false isTransitive = false
} }

View File

@ -473,11 +473,9 @@ class MainActivity : AppCompatActivity() {
binding.nowPlayingContainer?.nowPlayingTitle?.text = track.title binding.nowPlayingContainer?.nowPlayingTitle?.text = track.title
binding.nowPlayingContainer?.nowPlayingAlbum?.text = track.artist.name binding.nowPlayingContainer?.nowPlayingAlbum?.text = track.artist.name
binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
binding.nowPlayingContainer?.nowPlayingDetailsTitle?.text = track.title binding.nowPlayingContainer?.nowPlayingDetailsTitle?.text = track.title
binding.nowPlayingContainer?.nowPlayingDetailsArtist?.text = track.artist.name binding.nowPlayingContainer?.nowPlayingDetailsArtist?.text = track.artist.name
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon = getDrawable(R.drawable.pause)
Picasso.get() Picasso.get()
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original)) .maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))

View File

@ -41,7 +41,7 @@ class MediaSession(private val context: Context) {
MediaSessionConnector(session).also { MediaSessionConnector(session).also {
it.setQueueNavigator(FFAQueueNavigator()) it.setQueueNavigator(FFAQueueNavigator())
it.setMediaButtonEventHandler { _, _, intent -> it.setMediaButtonEventHandler { _, intent ->
if (!active) { if (!active) {
Intent(context, PlayerService::class.java).let { player -> Intent(context, PlayerService::class.java).let { player ->
player.action = intent.action player.action = intent.action
@ -65,13 +65,11 @@ class MediaSession(private val context: Context) {
} }
class FFAQueueNavigator : MediaSessionConnector.QueueNavigator { class FFAQueueNavigator : MediaSessionConnector.QueueNavigator {
override fun onSkipToQueueItem(player: Player, controlDispatcher: ControlDispatcher, id: Long) { override fun onSkipToQueueItem(player: Player, id: Long) {
CommandBus.send(Command.PlayTrack(id.toInt())) CommandBus.send(Command.PlayTrack(id.toInt()))
} }
override fun onCurrentWindowIndexChanged(player: Player) {} override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?) = true
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?) = true
override fun getSupportedQueueNavigatorActions(player: Player): Long { override fun getSupportedQueueNavigatorActions(player: Player): Long {
return PlaybackStateCompat.ACTION_PLAY_PAUSE or return PlaybackStateCompat.ACTION_PLAY_PAUSE or
@ -80,13 +78,13 @@ class FFAQueueNavigator : MediaSessionConnector.QueueNavigator {
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
} }
override fun onSkipToNext(player: Player, controlDispatcher: ControlDispatcher) { override fun onSkipToNext(player: Player) {
CommandBus.send(Command.NextTrack) CommandBus.send(Command.NextTrack)
} }
override fun getActiveQueueItemId(player: Player?) = player?.currentWindowIndex?.toLong() ?: 0 override fun getActiveQueueItemId(player: Player?) = player?.currentMediaItemIndex?.toLong() ?: 0
override fun onSkipToPrevious(player: Player, controlDispatcher: ControlDispatcher) { override fun onSkipToPrevious(player: Player) {
CommandBus.send(Command.PreviousTrack) CommandBus.send(Command.PreviousTrack)
} }

View File

@ -80,14 +80,16 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
override fun getScheduler(): Scheduler? = null override fun getScheduler(): Scheduler? = null
override fun getForegroundNotification(downloads: MutableList<Download>): Notification { override fun getForegroundNotification(downloads: MutableList<Download>,
notMetRequirements: Int): Notification {
val description = val description =
resources.getQuantityString(R.plurals.downloads_description, downloads.size, downloads.size) resources.getQuantityString(R.plurals.downloads_description, downloads.size, downloads.size)
return DownloadNotificationHelper( return DownloadNotificationHelper(
this, this,
AppContext.NOTIFICATION_CHANNEL_DOWNLOADS AppContext.NOTIFICATION_CHANNEL_DOWNLOADS
).buildProgressNotification(this, R.drawable.downloads, null, description, downloads) ).buildProgressNotification(this, R.drawable.downloads, null, description,
downloads, notMetRequirements)
} }
private fun getDownloads() = downloadManager.downloadIndex.getDownloads() private fun getDownloads() = downloadManager.downloadIndex.getDownloads()

View File

@ -31,11 +31,10 @@ import audio.funkwhale.ffa.utils.log
import audio.funkwhale.ffa.utils.maybeNormalizeUrl import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.onApi import audio.funkwhale.ffa.utils.onApi
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlaybackException import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.Tracks
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -65,7 +64,7 @@ class PlayerService : Service() {
private lateinit var queue: QueueManager private lateinit var queue: QueueManager
private lateinit var mediaControlsManager: MediaControlsManager private lateinit var mediaControlsManager: MediaControlsManager
private lateinit var player: SimpleExoPlayer private lateinit var player: ExoPlayer
private val mediaMetadataBuilder = MediaMetadataCompat.Builder() private val mediaMetadataBuilder = MediaMetadataCompat.Builder()
@ -132,12 +131,13 @@ class PlayerService : Service() {
mediaControlsManager = MediaControlsManager(this, scope, mediaSession.session) mediaControlsManager = MediaControlsManager(this, scope, mediaSession.session)
player = SimpleExoPlayer.Builder(this).build().apply { player = ExoPlayer.Builder(this).build().apply {
playWhenReady = false playWhenReady = false
playerEventListener = PlayerEventListener().also { playerEventListener = PlayerEventListener().also {
addListener(it) addListener(it)
} }
EventBus.send(Event.StateChanged(this.isPlaying()))
} }
mediaSession.active = true mediaSession.active = true
@ -151,7 +151,8 @@ class PlayerService : Service() {
} }
if (queue.current > -1) { if (queue.current > -1) {
player.prepare(queue.dataSources) player.setMediaSource(queue.dataSources)
player.prepare()
FFACache.getLine(this, "progress")?.let { FFACache.getLine(this, "progress")?.let {
player.seekTo(queue.current, it.toLong()) player.seekTo(queue.current, it.toLong())
@ -180,7 +181,8 @@ class PlayerService : Service() {
if (!command.fromRadio) radioPlayer.stop() if (!command.fromRadio) radioPlayer.stop()
queue.replace(command.queue) queue.replace(command.queue)
player.prepare(queue.dataSources, true, true) player.setMediaSource(queue.dataSources)
player.prepare()
setPlaybackState(true) setPlaybackState(true)
@ -307,7 +309,8 @@ class PlayerService : Service() {
} }
if (state && player.playbackState == Player.STATE_IDLE) { if (state && player.playbackState == Player.STATE_IDLE) {
player.prepare(queue.dataSources) player.setMediaSource(queue.dataSources)
player.prepare()
} }
if (hasAudioFocus(state)) { if (hasAudioFocus(state)) {
@ -318,7 +321,7 @@ class PlayerService : Service() {
} }
private fun togglePlayback() { private fun togglePlayback() {
setPlaybackState(!player.playWhenReady) setPlaybackState(!player.isPlaying)
} }
private fun skipToPreviousTrack() { private fun skipToPreviousTrack() {
@ -326,11 +329,11 @@ class PlayerService : Service() {
return player.seekTo(0) return player.seekTo(0)
} }
player.previous() player.seekToPrevious()
} }
private fun skipToNextTrack() { private fun skipToNextTrack() {
player.next() player.seekToNext()
FFACache.set(this@PlayerService, "progress", "0") FFACache.set(this@PlayerService, "progress", "0")
ProgressBus.send(0, 0, 0) ProgressBus.send(0, 0, 0)
@ -419,9 +422,14 @@ class PlayerService : Service() {
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")
inner class PlayerEventListener : Player.EventListener { inner class PlayerEventListener : Player.Listener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onPlayerStateChanged(playWhenReady, playbackState) super.onIsPlayingChanged(isPlaying)
mediaControlsManager.updateNotification(queue.current(), isPlaying)
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
EventBus.send(Event.StateChanged(playWhenReady)) EventBus.send(Event.StateChanged(playWhenReady))
@ -429,55 +437,45 @@ class PlayerService : Service() {
CommandBus.send(Command.RefreshTrack(queue.current())) CommandBus.send(Command.RefreshTrack(queue.current()))
} }
when (playWhenReady) { if (!playWhenReady) {
true -> { Build.VERSION_CODES.N.onApi(
when (playbackState) { { stopForeground(STOP_FOREGROUND_DETACH) },
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true) { stopForeground(false) }
Player.STATE_BUFFERING -> EventBus.send(Event.Buffering(true)) )
Player.STATE_ENDED -> { }
setPlaybackState(false) }
queue.current = 0 override fun onPlaybackStateChanged(playbackState: Int) {
player.seekTo(0, C.TIME_UNSET) super.onPlaybackStateChanged(playbackState)
EventBus.send(Event.Buffering(playbackState == Player.STATE_BUFFERING))
when (playbackState) {
Player.STATE_ENDED -> {
setPlaybackState(false)
ProgressBus.send(0, 0, 0) queue.current = 0
} player.seekTo(0, C.TIME_UNSET)
Player.STATE_IDLE -> { ProgressBus.send(0, 0, 0)
setPlaybackState(false)
return EventBus.send(Event.PlaybackStopped)
}
}
if (playbackState != Player.STATE_BUFFERING) EventBus.send(Event.Buffering(false))
} }
false -> { Player.STATE_IDLE -> {
EventBus.send(Event.Buffering(false)) setPlaybackState(false)
Build.VERSION_CODES.N.onApi( EventBus.send(Event.PlaybackStopped)
{ stopForeground(STOP_FOREGROUND_DETACH) },
{ stopForeground(false) }
)
when (playbackState) { if (!player.playWhenReady) {
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), false) mediaControlsManager.remove()
Player.STATE_IDLE -> mediaControlsManager.remove()
} }
} }
} }
} }
override fun onTracksChanged( override fun onTracksChanged(tracks: Tracks) {
trackGroups: TrackGroupArray, super.onTracksChanged(tracks)
trackSelections: TrackSelectionArray
) {
super.onTracksChanged(trackGroups, trackSelections)
if (queue.current != player.currentWindowIndex) { if (queue.current != player.currentMediaItemIndex) {
queue.current = player.currentWindowIndex queue.current = player.currentMediaItemIndex
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady) mediaControlsManager.updateNotification(queue.current(), player.isPlaying)
} }
if (queue.get().isNotEmpty() && if (queue.get().isNotEmpty() &&
@ -510,13 +508,14 @@ class PlayerService : Service() {
} }
} }
override fun onPlayerError(error: ExoPlaybackException) { override fun onPlayerError(error: PlaybackException) {
EventBus.send(Event.PlaybackError(getString(R.string.error_playback))) EventBus.send(Event.PlaybackError(getString(R.string.error_playback)))
if (player.playWhenReady) { if (player.playWhenReady) {
queue.current++ queue.current++
player.prepare(queue.dataSources, true, true) player.setMediaSource(queue.dataSources, true)
player.seekTo(queue.current, 0) player.seekTo(queue.current, 0)
player.prepare()
CommandBus.send(Command.RefreshTrack(queue.current())) CommandBus.send(Command.RefreshTrack(queue.current()))
} }

View File

@ -12,6 +12,7 @@ import audio.funkwhale.ffa.utils.FFACache
import audio.funkwhale.ffa.utils.log import audio.funkwhale.ffa.utils.log
import audio.funkwhale.ffa.utils.mustNormalizeUrl import audio.funkwhale.ffa.utils.mustNormalizeUrl
import com.github.kittinunf.fuel.gson.gsonDeserializerOf import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.source.ConcatenatingMediaSource import com.google.android.exoplayer2.source.ConcatenatingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.gson.Gson import com.google.gson.Gson
@ -38,8 +39,8 @@ class QueueManager(val context: Context) {
metadata.map { track -> metadata.map { track ->
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "") val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
ProgressiveMediaSource.Factory(factory).setTag(track.title) val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
.createMediaSource(Uri.parse(url)) ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem)
} }
) )
} }
@ -63,8 +64,8 @@ class QueueManager(val context: Context) {
val factory = cacheDataSourceFactoryProvider.create(context) val factory = cacheDataSourceFactoryProvider.create(context)
val sources = tracks.map { track -> val sources = tracks.map { track ->
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "") val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
ProgressiveMediaSource.Factory(factory).setTag(track.title).createMediaSource(Uri.parse(url)) ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem)
} }
metadata = tracks.toMutableList() metadata = tracks.toMutableList()
@ -84,7 +85,8 @@ class QueueManager(val context: Context) {
val sources = missingTracks.map { track -> val sources = missingTracks.map { track ->
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "") val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url)) val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem)
} }
metadata.addAll(tracks) metadata.addAll(tracks)
@ -101,7 +103,8 @@ class QueueManager(val context: Context) {
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "") val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
if (metadata.indexOf(track) == -1) { if (metadata.indexOf(track) == -1) {
ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url)).let { val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem).let {
dataSources.addMediaSource(current + 1, it) dataSources.addMediaSource(current + 1, it)
metadata.add(current + 1, track) metadata.add(current + 1, track)
} }