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:
parent
24de54c7e0
commit
d734953b54
|
@ -167,18 +167,18 @@ dependencies {
|
|||
implementation("com.google.android.material:material:1.6.1")
|
||||
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-ui:2.14.2")
|
||||
implementation("com.google.android.exoplayer:extension-mediasession:2.14.2")
|
||||
implementation("com.google.android.exoplayer:exoplayer-core:2.18.1")
|
||||
implementation("com.google.android.exoplayer:exoplayer-ui:2.18.1")
|
||||
implementation("com.google.android.exoplayer:extension-mediasession:2.18.1")
|
||||
|
||||
implementation("io.insert-koin:koin-core:3.1.2")
|
||||
implementation("io.insert-koin:koin-android: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
|
||||
}
|
||||
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:2.14.0") {
|
||||
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:789a4f83169cff5c7a91655bb828fde2cfde671a") {
|
||||
isTransitive = false
|
||||
}
|
||||
|
||||
|
|
|
@ -473,11 +473,9 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
binding.nowPlayingContainer?.nowPlayingTitle?.text = track.title
|
||||
binding.nowPlayingContainer?.nowPlayingAlbum?.text = track.artist.name
|
||||
binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsTitle?.text = track.title
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsArtist?.text = track.artist.name
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon = getDrawable(R.drawable.pause)
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))
|
||||
|
|
|
@ -41,7 +41,7 @@ class MediaSession(private val context: Context) {
|
|||
MediaSessionConnector(session).also {
|
||||
it.setQueueNavigator(FFAQueueNavigator())
|
||||
|
||||
it.setMediaButtonEventHandler { _, _, intent ->
|
||||
it.setMediaButtonEventHandler { _, intent ->
|
||||
if (!active) {
|
||||
Intent(context, PlayerService::class.java).let { player ->
|
||||
player.action = intent.action
|
||||
|
@ -65,13 +65,11 @@ class MediaSession(private val context: Context) {
|
|||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
override fun onCurrentWindowIndexChanged(player: Player) {}
|
||||
|
||||
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?) = true
|
||||
override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?) = true
|
||||
|
||||
override fun getSupportedQueueNavigatorActions(player: Player): Long {
|
||||
return PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||
|
@ -80,13 +78,13 @@ class FFAQueueNavigator : MediaSessionConnector.QueueNavigator {
|
|||
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||
}
|
||||
|
||||
override fun onSkipToNext(player: Player, controlDispatcher: ControlDispatcher) {
|
||||
override fun onSkipToNext(player: Player) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -80,14 +80,16 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
|||
|
||||
override fun getScheduler(): Scheduler? = null
|
||||
|
||||
override fun getForegroundNotification(downloads: MutableList<Download>): Notification {
|
||||
override fun getForegroundNotification(downloads: MutableList<Download>,
|
||||
notMetRequirements: Int): Notification {
|
||||
val description =
|
||||
resources.getQuantityString(R.plurals.downloads_description, downloads.size, downloads.size)
|
||||
|
||||
return DownloadNotificationHelper(
|
||||
this,
|
||||
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()
|
||||
|
|
|
@ -31,11 +31,10 @@ 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.ExoPlaybackException
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.PlaybackException
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
||||
import com.google.android.exoplayer2.Tracks
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
|
@ -65,7 +64,7 @@ class PlayerService : Service() {
|
|||
|
||||
private lateinit var queue: QueueManager
|
||||
private lateinit var mediaControlsManager: MediaControlsManager
|
||||
private lateinit var player: SimpleExoPlayer
|
||||
private lateinit var player: ExoPlayer
|
||||
|
||||
private val mediaMetadataBuilder = MediaMetadataCompat.Builder()
|
||||
|
||||
|
@ -132,12 +131,13 @@ class PlayerService : Service() {
|
|||
|
||||
mediaControlsManager = MediaControlsManager(this, scope, mediaSession.session)
|
||||
|
||||
player = SimpleExoPlayer.Builder(this).build().apply {
|
||||
player = ExoPlayer.Builder(this).build().apply {
|
||||
playWhenReady = false
|
||||
|
||||
playerEventListener = PlayerEventListener().also {
|
||||
addListener(it)
|
||||
}
|
||||
EventBus.send(Event.StateChanged(this.isPlaying()))
|
||||
}
|
||||
|
||||
mediaSession.active = true
|
||||
|
@ -151,7 +151,8 @@ class PlayerService : Service() {
|
|||
}
|
||||
|
||||
if (queue.current > -1) {
|
||||
player.prepare(queue.dataSources)
|
||||
player.setMediaSource(queue.dataSources)
|
||||
player.prepare()
|
||||
|
||||
FFACache.getLine(this, "progress")?.let {
|
||||
player.seekTo(queue.current, it.toLong())
|
||||
|
@ -180,7 +181,8 @@ class PlayerService : Service() {
|
|||
if (!command.fromRadio) radioPlayer.stop()
|
||||
|
||||
queue.replace(command.queue)
|
||||
player.prepare(queue.dataSources, true, true)
|
||||
player.setMediaSource(queue.dataSources)
|
||||
player.prepare()
|
||||
|
||||
setPlaybackState(true)
|
||||
|
||||
|
@ -307,7 +309,8 @@ class PlayerService : Service() {
|
|||
}
|
||||
|
||||
if (state && player.playbackState == Player.STATE_IDLE) {
|
||||
player.prepare(queue.dataSources)
|
||||
player.setMediaSource(queue.dataSources)
|
||||
player.prepare()
|
||||
}
|
||||
|
||||
if (hasAudioFocus(state)) {
|
||||
|
@ -318,7 +321,7 @@ class PlayerService : Service() {
|
|||
}
|
||||
|
||||
private fun togglePlayback() {
|
||||
setPlaybackState(!player.playWhenReady)
|
||||
setPlaybackState(!player.isPlaying)
|
||||
}
|
||||
|
||||
private fun skipToPreviousTrack() {
|
||||
|
@ -326,11 +329,11 @@ class PlayerService : Service() {
|
|||
return player.seekTo(0)
|
||||
}
|
||||
|
||||
player.previous()
|
||||
player.seekToPrevious()
|
||||
}
|
||||
|
||||
private fun skipToNextTrack() {
|
||||
player.next()
|
||||
player.seekToNext()
|
||||
|
||||
FFACache.set(this@PlayerService, "progress", "0")
|
||||
ProgressBus.send(0, 0, 0)
|
||||
|
@ -419,9 +422,14 @@ class PlayerService : Service() {
|
|||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
inner class PlayerEventListener : Player.EventListener {
|
||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||
super.onPlayerStateChanged(playWhenReady, playbackState)
|
||||
inner class PlayerEventListener : Player.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
super.onIsPlayingChanged(isPlaying)
|
||||
mediaControlsManager.updateNotification(queue.current(), isPlaying)
|
||||
}
|
||||
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
||||
|
||||
EventBus.send(Event.StateChanged(playWhenReady))
|
||||
|
||||
|
@ -429,55 +437,45 @@ class PlayerService : Service() {
|
|||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
}
|
||||
|
||||
when (playWhenReady) {
|
||||
true -> {
|
||||
when (playbackState) {
|
||||
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true)
|
||||
Player.STATE_BUFFERING -> EventBus.send(Event.Buffering(true))
|
||||
Player.STATE_ENDED -> {
|
||||
setPlaybackState(false)
|
||||
if (!playWhenReady) {
|
||||
Build.VERSION_CODES.N.onApi(
|
||||
{ stopForeground(STOP_FOREGROUND_DETACH) },
|
||||
{ stopForeground(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
queue.current = 0
|
||||
player.seekTo(0, C.TIME_UNSET)
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
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 -> {
|
||||
setPlaybackState(false)
|
||||
|
||||
return EventBus.send(Event.PlaybackStopped)
|
||||
}
|
||||
}
|
||||
|
||||
if (playbackState != Player.STATE_BUFFERING) EventBus.send(Event.Buffering(false))
|
||||
ProgressBus.send(0, 0, 0)
|
||||
}
|
||||
|
||||
false -> {
|
||||
EventBus.send(Event.Buffering(false))
|
||||
Player.STATE_IDLE -> {
|
||||
setPlaybackState(false)
|
||||
|
||||
Build.VERSION_CODES.N.onApi(
|
||||
{ stopForeground(STOP_FOREGROUND_DETACH) },
|
||||
{ stopForeground(false) }
|
||||
)
|
||||
EventBus.send(Event.PlaybackStopped)
|
||||
|
||||
when (playbackState) {
|
||||
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), false)
|
||||
Player.STATE_IDLE -> mediaControlsManager.remove()
|
||||
if (!player.playWhenReady) {
|
||||
mediaControlsManager.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTracksChanged(
|
||||
trackGroups: TrackGroupArray,
|
||||
trackSelections: TrackSelectionArray
|
||||
) {
|
||||
super.onTracksChanged(trackGroups, trackSelections)
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
super.onTracksChanged(tracks)
|
||||
|
||||
if (queue.current != player.currentWindowIndex) {
|
||||
queue.current = player.currentWindowIndex
|
||||
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
|
||||
if (queue.current != player.currentMediaItemIndex) {
|
||||
queue.current = player.currentMediaItemIndex
|
||||
mediaControlsManager.updateNotification(queue.current(), player.isPlaying)
|
||||
}
|
||||
|
||||
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)))
|
||||
|
||||
if (player.playWhenReady) {
|
||||
queue.current++
|
||||
player.prepare(queue.dataSources, true, true)
|
||||
player.setMediaSource(queue.dataSources, true)
|
||||
player.seekTo(queue.current, 0)
|
||||
player.prepare()
|
||||
|
||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import audio.funkwhale.ffa.utils.FFACache
|
|||
import audio.funkwhale.ffa.utils.log
|
||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
||||
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.ProgressiveMediaSource
|
||||
import com.google.gson.Gson
|
||||
|
@ -38,8 +39,8 @@ class QueueManager(val context: Context) {
|
|||
metadata.map { track ->
|
||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||
|
||||
ProgressiveMediaSource.Factory(factory).setTag(track.title)
|
||||
.createMediaSource(Uri.parse(url))
|
||||
val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
|
||||
ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -63,8 +64,8 @@ class QueueManager(val context: Context) {
|
|||
val factory = cacheDataSourceFactoryProvider.create(context)
|
||||
val sources = tracks.map { track ->
|
||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||
|
||||
ProgressiveMediaSource.Factory(factory).setTag(track.title).createMediaSource(Uri.parse(url))
|
||||
val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
|
||||
ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem)
|
||||
}
|
||||
|
||||
metadata = tracks.toMutableList()
|
||||
|
@ -84,7 +85,8 @@ class QueueManager(val context: Context) {
|
|||
val sources = missingTracks.map { track ->
|
||||
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)
|
||||
|
@ -101,7 +103,8 @@ class QueueManager(val context: Context) {
|
|||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||
|
||||
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)
|
||||
metadata.add(current + 1, track)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue