Merge branch 'fixShuffle' into 'develop'

Fix shuffle

Closes #876 and #877

See merge request ultrasonic/ultrasonic!966
This commit is contained in:
birdbird 2023-04-20 11:25:25 +00:00
commit 08d3618eb3
10 changed files with 225 additions and 91 deletions

View File

@ -34,7 +34,7 @@ android {
minifyEnabled false minifyEnabled false
multiDexEnabled true multiDexEnabled true
testCoverageEnabled true testCoverageEnabled true
applicationIdSuffix ".debug" applicationIdSuffix '.debug'
} }
} }

View File

@ -62,8 +62,6 @@ class TrackViewBinder(
diffAdapter.isSelected(item.longId) diffAdapter.isSelected(item.longId)
) )
// Timber.v("Setting listeners")
holder.itemView.setOnLongClickListener { holder.itemView.setOnLongClickListener {
if (onContextMenuClick != null) { if (onContextMenuClick != null) {
val popup = createContextMenu(holder.itemView, track) val popup = createContextMenu(holder.itemView, track)
@ -116,8 +114,6 @@ class TrackViewBinder(
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
} }
// Timber.v("Setting listeners done")
} }
override fun onViewRecycled(holder: TrackViewHolder) { override fun onViewRecycled(holder: TrackViewHolder) {

View File

@ -131,7 +131,7 @@ class TrackViewHolder(val view: View) :
// Create new Disposable for the new Subscriptions // Create new Disposable for the new Subscriptions
rxBusSubscription = CompositeDisposable() rxBusSubscription = CompositeDisposable()
rxBusSubscription!! += RxBus.playerStateObservable.subscribe { rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
setPlayIcon(it.index == bindingAdapterPosition && it.track?.id == song.id) setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
} }
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe { rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {

View File

@ -148,10 +148,10 @@ class NowPlayingFragment : Fragment() {
if (abs(deltaX) > MIN_DISTANCE) { if (abs(deltaX) > MIN_DISTANCE) {
// left or right // left or right
if (deltaX < 0) { if (deltaX < 0) {
mediaPlayerController.previous() mediaPlayerController.seekToPrevious()
} }
if (deltaX > 0) { if (deltaX > 0) {
mediaPlayerController.next() mediaPlayerController.seekToNext()
} }
} else if (abs(deltaY) > MIN_DISTANCE) { } else if (abs(deltaY) > MIN_DISTANCE) {
if (deltaY < 0) { if (deltaY < 0) {

View File

@ -154,6 +154,8 @@ class PlayerFragment :
private lateinit var pauseButton: View private lateinit var pauseButton: View
private lateinit var stopButton: View private lateinit var stopButton: View
private lateinit var playButton: View private lateinit var playButton: View
private lateinit var previousButton: MaterialButton
private lateinit var nextButton: MaterialButton
private lateinit var shuffleButton: View private lateinit var shuffleButton: View
private lateinit var repeatButton: MaterialButton private lateinit var repeatButton: MaterialButton
private lateinit var progressBar: SeekBar private lateinit var progressBar: SeekBar
@ -196,6 +198,8 @@ class PlayerFragment :
pauseButton = view.findViewById(R.id.button_pause) pauseButton = view.findViewById(R.id.button_pause)
stopButton = view.findViewById(R.id.button_stop) stopButton = view.findViewById(R.id.button_stop)
playButton = view.findViewById(R.id.button_start) playButton = view.findViewById(R.id.button_start)
nextButton = view.findViewById(R.id.button_next)
previousButton = view.findViewById(R.id.button_previous)
repeatButton = view.findViewById(R.id.button_repeat) repeatButton = view.findViewById(R.id.button_repeat)
fiveStar1ImageView = view.findViewById(R.id.song_five_star_1) fiveStar1ImageView = view.findViewById(R.id.song_five_star_1)
fiveStar2ImageView = view.findViewById(R.id.song_five_star_2) fiveStar2ImageView = view.findViewById(R.id.song_five_star_2)
@ -259,9 +263,7 @@ class PlayerFragment :
previousButton.setOnClickListener { previousButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.previous() mediaPlayerController.seekToPrevious()
onCurrentChanged()
onSliderProgressChanged()
} }
} }
@ -272,9 +274,7 @@ class PlayerFragment :
nextButton.setOnClickListener { nextButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.next() mediaPlayerController.seekToNext()
onCurrentChanged()
onSliderProgressChanged()
} }
} }
@ -285,16 +285,12 @@ class PlayerFragment :
pauseButton.setOnClickListener { pauseButton.setOnClickListener {
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.pause() mediaPlayerController.pause()
onCurrentChanged()
onSliderProgressChanged()
} }
} }
stopButton.setOnClickListener { stopButton.setOnClickListener {
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.reset() mediaPlayerController.reset()
onCurrentChanged()
onSliderProgressChanged()
} }
} }
@ -304,8 +300,6 @@ class PlayerFragment :
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.play() mediaPlayerController.play()
onCurrentChanged()
onSliderProgressChanged()
} }
} }
@ -342,7 +336,6 @@ class PlayerFragment :
override fun onStopTrackingTouch(seekBar: SeekBar) { override fun onStopTrackingTouch(seekBar: SeekBar) {
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.seekTo(progressBar.progress) mediaPlayerController.seekTo(progressBar.progress)
onSliderProgressChanged()
} }
} }
@ -367,11 +360,13 @@ class PlayerFragment :
// Observe playlist changes and update the UI // Observe playlist changes and update the UI
rxBusSubscription += RxBus.playlistObservable.subscribe { rxBusSubscription += RxBus.playlistObservable.subscribe {
onPlaylistChanged() onPlaylistChanged()
onSliderProgressChanged() updateSeekBar()
} }
rxBusSubscription += RxBus.playerStateObservable.subscribe { rxBusSubscription += RxBus.playerStateObservable.subscribe {
update() update()
updateTitle(it.state)
updateButtonStates(it.state)
} }
// Query the Jukebox state in an IO Context // Query the Jukebox state in an IO Context
@ -432,7 +427,7 @@ class PlayerFragment :
} else { } else {
// Download list and Album art must be updated when resumed // Download list and Album art must be updated when resumed
onPlaylistChanged() onPlaylistChanged()
onCurrentChanged() onTrackChanged()
} }
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
@ -764,10 +759,9 @@ class PlayerFragment :
if (cancel?.isCancellationRequested == true) return if (cancel?.isCancellationRequested == true) return
val mediaPlayerController = mediaPlayerController val mediaPlayerController = mediaPlayerController
if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) { if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) {
onCurrentChanged() onTrackChanged()
} }
onSliderProgressChanged() updateSeekBar()
requireActivity().invalidateOptionsMenu()
} }
private fun savePlaylistInBackground(playlistName: String) { private fun savePlaylistInBackground(playlistName: String) {
@ -827,12 +821,9 @@ class PlayerFragment :
} }
// Create listener // Create listener
val clickHandler: ((Track, Int) -> Unit) = { _, pos -> val clickHandler: ((Track, Int) -> Unit) = { _, listPos ->
mediaPlayerController.seekTo(pos, 0) val mediaIndex = mediaPlayerController.getUnshuffledIndexOf(listPos)
mediaPlayerController.prepare() mediaPlayerController.play(mediaIndex)
mediaPlayerController.play()
onCurrentChanged()
onSliderProgressChanged()
} }
viewAdapter.register( viewAdapter.register(
@ -931,6 +922,7 @@ class PlayerFragment :
if (actionState == ACTION_STATE_IDLE && dragging) { if (actionState == ACTION_STATE_IDLE && dragging) {
dragging = false dragging = false
// Move the item in the playlist separately // Move the item in the playlist separately
Timber.i("Moving item %s to %s", startPosition, endPosition)
mediaPlayerController.moveItemInPlaylist(startPosition, endPosition) mediaPlayerController.moveItemInPlaylist(startPosition, endPosition)
} }
} }
@ -1010,7 +1002,8 @@ class PlayerFragment :
private fun onPlaylistChanged() { private fun onPlaylistChanged() {
val mediaPlayerController = mediaPlayerController val mediaPlayerController = mediaPlayerController
val list = mediaPlayerController.playlist // Try to display playlist in play order
val list = mediaPlayerController.playlistInPlayOrder
emptyTextView.setText(R.string.playlist_empty) emptyTextView.setText(R.string.playlist_empty)
viewAdapter.submitList(list.map(MediaItem::toTrack)) viewAdapter.submitList(list.map(MediaItem::toTrack))
@ -1020,7 +1013,7 @@ class PlayerFragment :
updateRepeatButtonState(mediaPlayerController.repeatMode) updateRepeatButtonState(mediaPlayerController.repeatMode)
} }
private fun onCurrentChanged() { private fun onTrackChanged() {
currentSong = mediaPlayerController.currentMediaItem?.toTrack() currentSong = mediaPlayerController.currentMediaItem?.toTrack()
scrollToCurrent() scrollToCurrent()
@ -1064,7 +1057,7 @@ class PlayerFragment :
it.loadImage(albumArtImageView, currentSong, true, 0) it.loadImage(albumArtImageView, currentSong, true, 0)
} }
displaySongRating() updateSongRating()
} else { } else {
currentSong = null currentSong = null
songTitleTextView.text = null songTitleTextView.text = null
@ -1078,24 +1071,27 @@ class PlayerFragment :
it.loadImage(albumArtImageView, null, true, 0) it.loadImage(albumArtImageView, null, true, 0)
} }
} }
// TODO: It would be a lot nicer if MediaPlayerController would send an event
// when this is necessary instead of updating every time
updateSongRating()
nextButton.isEnabled = mediaPlayerController.canSeekToNext()
previousButton.isEnabled = mediaPlayerController.canSeekToPrevious()
} }
@Suppress("LongMethod")
@Synchronized @Synchronized
private fun onSliderProgressChanged() { private fun updateSeekBar() {
Timber.i("Calling updateSeekBar")
val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled
val millisPlayed: Int = max(0, mediaPlayerController.playerPosition) val millisPlayed: Int = max(0, mediaPlayerController.playerPosition)
val duration: Int = mediaPlayerController.playerDuration val duration: Int = mediaPlayerController.playerDuration
val playbackState: Int = mediaPlayerController.playbackState val playbackState: Int = mediaPlayerController.playbackState
val isPlaying = mediaPlayerController.isPlaying
if (cancellationToken.isCancellationRequested) return
if (currentSong != null) { if (currentSong != null) {
positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true) positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true)
durationTextView.text = Util.formatTotalDuration(duration.toLong(), true) durationTextView.text = Util.formatTotalDuration(duration.toLong(), true)
progressBar.max = progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug.
if (duration == 0) 100 else duration // Work-around for apparent bug.
progressBar.progress = millisPlayed progressBar.progress = millisPlayed
progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled
} else { } else {
@ -1107,18 +1103,18 @@ class PlayerFragment :
} }
val progress = mediaPlayerController.bufferedPercentage val progress = mediaPlayerController.bufferedPercentage
updateBufferProgress(playbackState, progress)
}
private fun updateTitle(playbackState: Int) {
when (playbackState) { when (playbackState) {
Player.STATE_BUFFERING -> { Player.STATE_BUFFERING -> {
val downloadStatus = resources.getString( val downloadStatus = resources.getString(
R.string.download_playerstate_loading R.string.download_playerstate_loading
) )
progressBar.secondaryProgress = progress
setTitle(this@PlayerFragment, downloadStatus) setTitle(this@PlayerFragment, downloadStatus)
} }
Player.STATE_READY -> { Player.STATE_READY -> {
progressBar.secondaryProgress = progress
if (mediaPlayerController.isShufflePlayEnabled) { if (mediaPlayerController.isShufflePlayEnabled) {
setTitle( setTitle(
this@PlayerFragment, this@PlayerFragment,
@ -1128,13 +1124,22 @@ class PlayerFragment :
setTitle(this@PlayerFragment, R.string.common_appname) setTitle(this@PlayerFragment, R.string.common_appname)
} }
} }
Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_ENDED -> {}
Player.STATE_ENDED,
-> {
}
else -> setTitle(this@PlayerFragment, R.string.common_appname) else -> setTitle(this@PlayerFragment, R.string.common_appname)
} }
}
private fun updateBufferProgress(playbackState: Int, progress: Int) {
when (playbackState) {
Player.STATE_BUFFERING, Player.STATE_READY -> {
progressBar.secondaryProgress = progress
}
else -> { }
}
}
private fun updateButtonStates(playbackState: Int) {
val isPlaying = mediaPlayerController.isPlaying
when (playbackState) { when (playbackState) {
Player.STATE_READY -> { Player.STATE_READY -> {
pauseButton.isVisible = isPlaying pauseButton.isVisible = isPlaying
@ -1152,10 +1157,6 @@ class PlayerFragment :
playButton.isVisible = true playButton.isVisible = true
} }
} }
// TODO: It would be a lot nicer if MediaPlayerController would send an event
// when this is necessary instead of updating every time
displaySongRating()
} }
private fun seek(forward: Boolean) { private fun seek(forward: Boolean) {
@ -1189,18 +1190,14 @@ class PlayerFragment :
// Right to Left swipe // Right to Left swipe
if (e1X - e2X > swipeDistance && absX > swipeVelocity) { if (e1X - e2X > swipeDistance && absX > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.next() mediaPlayerController.seekToNext()
onCurrentChanged()
onSliderProgressChanged()
return true return true
} }
// Left to Right swipe // Left to Right swipe
if (e2X - e1X > swipeDistance && absX > swipeVelocity) { if (e2X - e1X > swipeDistance && absX > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.previous() mediaPlayerController.seekToPrevious()
onCurrentChanged()
onSliderProgressChanged()
return true return true
} }
@ -1208,7 +1205,6 @@ class PlayerFragment :
if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) { if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000) mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000)
onSliderProgressChanged()
return true return true
} }
@ -1216,7 +1212,6 @@ class PlayerFragment :
if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) { if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000) mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000)
onSliderProgressChanged()
return true return true
} }
return false return false
@ -1237,7 +1232,7 @@ class PlayerFragment :
return false return false
} }
private fun displaySongRating() { private fun updateSongRating() {
var rating = 0 var rating = 0
if (currentSong?.userRating != null) { if (currentSong?.userRating != null) {
@ -1253,7 +1248,7 @@ class PlayerFragment :
private fun setSongRating(rating: Int) { private fun setSongRating(rating: Int) {
if (currentSong == null) return if (currentSong == null) return
displaySongRating() updateSongRating()
mediaPlayerController.setSongRating(rating) mediaPlayerController.setSongRating(rating)
} }

View File

@ -26,9 +26,12 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import java.util.Random
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -175,6 +178,27 @@ class PlaybackService :
player.setWakeMode(getWakeModeFlag()) player.setWakeMode(getWakeModeFlag())
} }
// Set a listener to reset the ShuffleOrder
rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle ->
val len = player.currentTimeline.windowCount
Timber.i("Resetting shuffle order, isShuffled: %s", shuffle)
// If disabling Shuffle return early
if (!shuffle) {
return@subscribe player.setShuffleOrder(UnshuffledShuffleOrder(len))
}
// Get the position of the current track in the unshuffled order
val cur = player.currentMediaItemIndex
val seed = System.currentTimeMillis()
val random = Random(seed)
val list = createShuffleListFromCurrentIndex(cur, len, random)
Timber.i("New Shuffle order: %s", list.joinToString { it.toString() })
player.setShuffleOrder(DefaultShuffleOrder(list, seed))
}
// Listen to the shutdown command // Listen to the shutdown command
rxBusSubscription += RxBus.shutdownCommandObservable.subscribe { rxBusSubscription += RxBus.shutdownCommandObservable.subscribe {
Timber.i("Received destroy command via Rx") Timber.i("Received destroy command via Rx")
@ -185,6 +209,24 @@ class PlaybackService :
isStarted = true isStarted = true
} }
fun createShuffleListFromCurrentIndex(
currentIndex: Int,
length: Int,
random: Random
): IntArray {
val list = IntArray(length) { it }
// Shuffle the remaining items using a swapping algorithm
for (i in currentIndex + 1 until length) {
val swapIndex = (currentIndex + 1) + random.nextInt(i - currentIndex)
val swapItem = list[i]
list[i] = list[swapIndex]
list[swapIndex] = swapItem
}
return list
}
private val listener: Player.Listener = object : Player.Listener { private val listener: Player.Listener = object : Player.Listener {
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
cacheNextSongs() cacheNextSongs()

View File

@ -11,11 +11,14 @@ import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.widget.Toast import android.widget.Toast
import androidx.annotation.IntRange
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.HeartRating import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Player.REPEAT_MODE_OFF
import androidx.media3.common.Timeline import androidx.media3.common.Timeline
@ -30,6 +33,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
@ -60,6 +64,7 @@ class MediaPlayerController(
private val externalStorageMonitor: ExternalStorageMonitor, private val externalStorageMonitor: ExternalStorageMonitor,
val context: Context val context: Context
) : KoinComponent { ) : KoinComponent {
private val activeServerProvider: ActiveServerProvider by inject() private val activeServerProvider: ActiveServerProvider by inject()
private var created = false private var created = false
@ -96,6 +101,14 @@ class MediaPlayerController(
* We run the event through RxBus in order to throttle them * We run the event through RxBus in order to throttle them
*/ */
override fun onTimelineChanged(timeline: Timeline, reason: Int) { override fun onTimelineChanged(timeline: Timeline, reason: Int) {
val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled)
Timber.w("On timeline changed. First shuffle play at index: %s", start)
deferredPlay?.let {
Timber.w("Executing deferred shuffle play")
it()
deferredPlay = null
}
RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack)) RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack))
} }
@ -150,19 +163,21 @@ class MediaPlayerController(
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
val timeline: Timeline = controller!!.currentTimeline val timeline: Timeline = controller!!.currentTimeline
var windowIndex = timeline.getFirstWindowIndex( /* shuffleModeEnabled= */true) var windowIndex = timeline.getFirstWindowIndex(true)
var count = 0 var count = 0
Timber.d("Shuffle: windowIndex: $windowIndex, at: $count") Timber.d("Shuffle: windowIndex: $windowIndex, at: $count")
while (windowIndex != C.INDEX_UNSET) { while (windowIndex != C.INDEX_UNSET) {
count++ count++
windowIndex = timeline.getNextWindowIndex( windowIndex = timeline.getNextWindowIndex(
windowIndex, REPEAT_MODE_OFF, /* shuffleModeEnabled= */true windowIndex, REPEAT_MODE_OFF, true
) )
Timber.d("Shuffle: windowIndex: $windowIndex, at: $count") Timber.d("Shuffle: windowIndex: $windowIndex, at: $count")
} }
} }
} }
private var deferredPlay: (() -> Unit)? = null
private var cachedMediaItem: MediaItem? = null private var cachedMediaItem: MediaItem? = null
fun onCreate(onCreated: () -> Unit) { fun onCreate(onCreated: () -> Unit) {
@ -259,7 +274,7 @@ class MediaPlayerController(
private fun publishPlaybackState() { private fun publishPlaybackState() {
val newState = RxBus.StateWithTrack( val newState = RxBus.StateWithTrack(
track = currentMediaItem?.toTrack(), track = currentMediaItem?.toTrack(),
index = currentMediaItemIndex, index = if (isShufflePlayEnabled) getCurrentShuffleIndex() else currentMediaItemIndex,
isPlaying = isPlaying, isPlaying = isPlaying,
state = playbackState state = playbackState
) )
@ -316,6 +331,8 @@ class MediaPlayerController(
@Synchronized @Synchronized
fun play(index: Int) { fun play(index: Int) {
controller?.seekTo(index, 0L) controller?.seekTo(index, 0L)
// FIXME CHECK ITS NOT MAKING PROBLEMS
controller?.prepare()
controller?.play() controller?.play()
} }
@ -404,6 +421,7 @@ class MediaPlayerController(
} }
if (shuffle) isShufflePlayEnabled = true if (shuffle) isShufflePlayEnabled = true
Timber.w("Adding ${mediaItems.size} media items")
controller?.addMediaItems(insertAt, mediaItems) controller?.addMediaItems(insertAt, mediaItems)
prepare() prepare()
@ -411,10 +429,19 @@ class MediaPlayerController(
// Playback doesn't start correctly when the player is in STATE_ENDED. // Playback doesn't start correctly when the player is in STATE_ENDED.
// So we need to call seek before (this is what play(0,0)) does. // So we need to call seek before (this is what play(0,0)) does.
// We can't just use play(0,0) then all random playlists will start with the first track. // We can't just use play(0,0) then all random playlists will start with the first track.
// This means that we need to generate the random first track ourselves. // Additionally the shuffle order becomes clear on after some time, so we need to wait for
// the right event, and can start playback only then.
if (autoPlay) { if (autoPlay) {
val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled) ?: 0 if (isShufflePlayEnabled) {
play(start) deferredPlay = {
val start = controller?.currentTimeline
?.getFirstWindowIndex(isShufflePlayEnabled) ?: 0
Timber.i("Deferred shuffle play starting now at index: %s", start)
play(start)
}
} else {
play(0)
}
} }
} }
@ -422,6 +449,8 @@ class MediaPlayerController(
var isShufflePlayEnabled: Boolean var isShufflePlayEnabled: Boolean
get() = controller?.shuffleModeEnabled == true get() = controller?.shuffleModeEnabled == true
set(enabled) { set(enabled) {
Timber.i("Shuffle is now enabled: %s", enabled)
RxBus.shufflePlayPublisher.onNext(enabled)
controller?.shuffleModeEnabled = enabled controller?.shuffleModeEnabled = enabled
} }
@ -431,11 +460,17 @@ class MediaPlayerController(
return isShufflePlayEnabled return isShufflePlayEnabled
} }
/**
* Returns an estimate of the percentage in the current content up to which data is
* buffered, or 0 if no estimate is available.
*/
@get:IntRange(from = 0, to = 100)
val bufferedPercentage: Int val bufferedPercentage: Int
get() = controller?.bufferedPercentage ?: 0 get() = controller?.bufferedPercentage ?: 0
@Synchronized @Synchronized
fun moveItemInPlaylist(oldPos: Int, newPos: Int) { fun moveItemInPlaylist(oldPos: Int, newPos: Int) {
// TODO: This currently does not care about shuffle position.
controller?.moveMediaItem(oldPos, newPos) controller?.moveMediaItem(oldPos, newPos)
} }
@ -494,15 +529,25 @@ class MediaPlayerController(
} }
@Synchronized @Synchronized
fun previous() { fun seekToPrevious() {
controller?.seekToPrevious() controller?.seekToPrevious()
} }
@Synchronized @Synchronized
operator fun next() { fun canSeekToPrevious(): Boolean {
return controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true
}
@Synchronized
fun seekToNext() {
controller?.seekToNext() controller?.seekToNext()
} }
@Synchronized
fun canSeekToNext(): Boolean {
return controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true
}
@Synchronized @Synchronized
fun reset() { fun reset() {
controller?.clearMediaItems() controller?.clearMediaItems()
@ -693,15 +738,15 @@ class MediaPlayerController(
if (currentMediaItem == null) return if (currentMediaItem == null) return
val song = currentMediaItem!!.toTrack() val song = currentMediaItem!!.toTrack()
song.userRating = rating song.userRating = rating
Thread { mainScope.launch {
try { withContext(Dispatchers.IO) {
getMusicService().setRating(song.id, rating) try {
} catch (e: Exception) { getMusicService().setRating(song.id, rating)
Timber.e(e) } catch (e: Exception) {
Timber.e(e)
}
} }
}.start() }
// TODO this would be better handled with a Rx command
// updateNotification()
} }
val currentMediaItem: MediaItem? val currentMediaItem: MediaItem?
@ -710,9 +755,65 @@ class MediaPlayerController(
val currentMediaItemIndex: Int val currentMediaItemIndex: Int
get() = controller?.currentMediaItemIndex ?: -1 get() = controller?.currentMediaItemIndex ?: -1
fun getCurrentShuffleIndex(): Int {
val currentMediaItemIndex = controller?.currentMediaItemIndex ?: return -1
return getShuffledIndexOf(currentMediaItemIndex)
}
/**
* Loops over the timeline windows to find the entry which matches the given closure.
*
* @param searchClosure Determines the condition which the searched for window needs to match.
* @param timeline the timeline to search in.
* @return the index of the window that satisfies the search condition,
* or [C.INDEX_UNSET] if not found.
*/
private fun getWindowIndexWhere(searchClosure: (Int, Int) -> Boolean): Int {
val timeline = controller?.currentTimeline!!
var windowIndex = timeline.getFirstWindowIndex(true)
var count = 0
while (windowIndex != C.INDEX_UNSET) {
if (searchClosure(count, windowIndex)) return count
count++
windowIndex = timeline.getNextWindowIndex(
windowIndex, REPEAT_MODE_OFF, true
)
}
return C.INDEX_UNSET
}
/**
* Returns the index of the shuffled position of the current playback item given its original
* position in the unshuffled timeline.
*
* @param searchPosition The index of the item in the unshuffled timeline to search for
* in the shuffled timeline.
* @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found.
*/
fun getShuffledIndexOf(searchPosition: Int): Int {
return getWindowIndexWhere { _, windowIndex -> windowIndex == searchPosition }
}
/**
* Returns the index of the unshuffled position of the current playback item given its shuffled
* position in the shuffled timeline.
*
* @param shufflePosition the index of the item in the shuffled timeline to search for in the
* unshuffled timeline.
* @return the index of the item in the unshuffled timeline, or [C.INDEX_UNSET] if not found.
*/
fun getUnshuffledIndexOf(shufflePosition: Int): Int {
return getWindowIndexWhere { count, _ -> count == shufflePosition }
}
val mediaItemCount: Int val mediaItemCount: Int
get() = controller?.mediaItemCount ?: 0 get() = controller?.mediaItemCount ?: 0
fun getMediaItemAt(index: Int): MediaItem? {
return controller?.getMediaItemAt(index)
}
val playlistSize: Int val playlistSize: Int
get() = controller?.currentTimeline?.windowCount ?: 0 get() = controller?.currentTimeline?.windowCount ?: 0
@ -721,10 +822,6 @@ class MediaPlayerController(
return Util.getPlayListFromTimeline(controller?.currentTimeline, false) return Util.getPlayListFromTimeline(controller?.currentTimeline, false)
} }
fun getMediaItemAt(index: Int): MediaItem? {
return controller?.getMediaItemAt(index)
}
val playlistInPlayOrder: List<MediaItem> val playlistInPlayOrder: List<MediaItem>
get() { get() {
return Util.getPlayListFromTimeline( return Util.getPlayListFromTimeline(

View File

@ -182,8 +182,8 @@ class MediaPlayerLifecycleSupport : KoinComponent {
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause() KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause()
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.previous() KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.seekToPrevious()
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.next() KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.seekToNext()
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop() KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop()
KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play() KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play()
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause()
@ -226,8 +226,8 @@ class MediaPlayerLifecycleSupport : KoinComponent {
// no need to call anything // no need to call anything
if (isRunning) mediaPlayerController.resumeOrPlay() if (isRunning) mediaPlayerController.resumeOrPlay()
Constants.CMD_NEXT -> mediaPlayerController.next() Constants.CMD_NEXT -> mediaPlayerController.seekToNext()
Constants.CMD_PREVIOUS -> mediaPlayerController.previous() Constants.CMD_PREVIOUS -> mediaPlayerController.seekToPrevious()
Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause() Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause()
Constants.CMD_STOP -> mediaPlayerController.stop() Constants.CMD_STOP -> mediaPlayerController.stop()
Constants.CMD_PAUSE -> mediaPlayerController.pause() Constants.CMD_PAUSE -> mediaPlayerController.pause()

View File

@ -20,9 +20,13 @@ class RxBus {
private fun mainThread() = AndroidSchedulers.from(Looper.getMainLooper()) private fun mainThread() = AndroidSchedulers.from(Looper.getMainLooper())
val shufflePlayPublisher: PublishSubject<Boolean> =
PublishSubject.create()
val shufflePlayObservable: Observable<Boolean> =
shufflePlayPublisher
var activeServerChangingPublisher: PublishSubject<Int> = var activeServerChangingPublisher: PublishSubject<Int> =
PublishSubject.create() PublishSubject.create()
// Subscribers should be called synchronously, not on another thread // Subscribers should be called synchronously, not on another thread
var activeServerChangingObservable: Observable<Int> = var activeServerChangingObservable: Observable<Int> =
activeServerChangingPublisher activeServerChangingPublisher

View File

@ -757,7 +757,7 @@ object Util {
fun getPlayListFromTimeline( fun getPlayListFromTimeline(
timeline: Timeline?, timeline: Timeline?,
shuffle: Boolean, isShuffled: Boolean,
firstIndex: Int? = null, firstIndex: Int? = null,
count: Int? = null count: Int? = null
): List<MediaItem> { ): List<MediaItem> {
@ -765,13 +765,13 @@ object Util {
if (timeline.windowCount < 1) return emptyList() if (timeline.windowCount < 1) return emptyList()
val playlist: MutableList<MediaItem> = mutableListOf() val playlist: MutableList<MediaItem> = mutableListOf()
var i = firstIndex ?: timeline.getFirstWindowIndex(false) var i = firstIndex ?: timeline.getFirstWindowIndex(isShuffled)
if (i == C.INDEX_UNSET) return emptyList() if (i == C.INDEX_UNSET) return emptyList()
while (i != C.INDEX_UNSET && (count != playlist.count())) { while (i != C.INDEX_UNSET && (count != playlist.count())) {
val window = timeline.getWindow(i, Timeline.Window()) val window = timeline.getWindow(i, Timeline.Window())
playlist.add(window.mediaItem) playlist.add(window.mediaItem)
i = timeline.getNextWindowIndex(i, Player.REPEAT_MODE_OFF, shuffle) i = timeline.getNextWindowIndex(i, Player.REPEAT_MODE_OFF, isShuffled)
} }
return playlist return playlist
} }