mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-16 01:10:36 +03:00
Merge branch 'fixShuffle' into 'develop'
Fix shuffle Closes #876 and #877 See merge request ultrasonic/ultrasonic!966
This commit is contained in:
commit
08d3618eb3
@ -34,7 +34,7 @@ android {
|
||||
minifyEnabled false
|
||||
multiDexEnabled true
|
||||
testCoverageEnabled true
|
||||
applicationIdSuffix ".debug"
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,8 +62,6 @@ class TrackViewBinder(
|
||||
diffAdapter.isSelected(item.longId)
|
||||
)
|
||||
|
||||
// Timber.v("Setting listeners")
|
||||
|
||||
holder.itemView.setOnLongClickListener {
|
||||
if (onContextMenuClick != null) {
|
||||
val popup = createContextMenu(holder.itemView, track)
|
||||
@ -116,8 +114,6 @@ class TrackViewBinder(
|
||||
|
||||
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
|
||||
}
|
||||
|
||||
// Timber.v("Setting listeners done")
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: TrackViewHolder) {
|
||||
|
@ -131,7 +131,7 @@ class TrackViewHolder(val view: View) :
|
||||
// Create new Disposable for the new Subscriptions
|
||||
rxBusSubscription = CompositeDisposable()
|
||||
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 {
|
||||
|
@ -148,10 +148,10 @@ class NowPlayingFragment : Fragment() {
|
||||
if (abs(deltaX) > MIN_DISTANCE) {
|
||||
// left or right
|
||||
if (deltaX < 0) {
|
||||
mediaPlayerController.previous()
|
||||
mediaPlayerController.seekToPrevious()
|
||||
}
|
||||
if (deltaX > 0) {
|
||||
mediaPlayerController.next()
|
||||
mediaPlayerController.seekToNext()
|
||||
}
|
||||
} else if (abs(deltaY) > MIN_DISTANCE) {
|
||||
if (deltaY < 0) {
|
||||
|
@ -154,6 +154,8 @@ class PlayerFragment :
|
||||
private lateinit var pauseButton: View
|
||||
private lateinit var stopButton: View
|
||||
private lateinit var playButton: View
|
||||
private lateinit var previousButton: MaterialButton
|
||||
private lateinit var nextButton: MaterialButton
|
||||
private lateinit var shuffleButton: View
|
||||
private lateinit var repeatButton: MaterialButton
|
||||
private lateinit var progressBar: SeekBar
|
||||
@ -196,6 +198,8 @@ class PlayerFragment :
|
||||
pauseButton = view.findViewById(R.id.button_pause)
|
||||
stopButton = view.findViewById(R.id.button_stop)
|
||||
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)
|
||||
fiveStar1ImageView = view.findViewById(R.id.song_five_star_1)
|
||||
fiveStar2ImageView = view.findViewById(R.id.song_five_star_2)
|
||||
@ -259,9 +263,7 @@ class PlayerFragment :
|
||||
previousButton.setOnClickListener {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.previous()
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
mediaPlayerController.seekToPrevious()
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,9 +274,7 @@ class PlayerFragment :
|
||||
nextButton.setOnClickListener {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.next()
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
mediaPlayerController.seekToNext()
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,16 +285,12 @@ class PlayerFragment :
|
||||
pauseButton.setOnClickListener {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.pause()
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}
|
||||
|
||||
stopButton.setOnClickListener {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.reset()
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,8 +300,6 @@ class PlayerFragment :
|
||||
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.play()
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,7 +336,6 @@ class PlayerFragment :
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.seekTo(progressBar.progress)
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ -367,11 +360,13 @@ class PlayerFragment :
|
||||
// Observe playlist changes and update the UI
|
||||
rxBusSubscription += RxBus.playlistObservable.subscribe {
|
||||
onPlaylistChanged()
|
||||
onSliderProgressChanged()
|
||||
updateSeekBar()
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
||||
update()
|
||||
updateTitle(it.state)
|
||||
updateButtonStates(it.state)
|
||||
}
|
||||
|
||||
// Query the Jukebox state in an IO Context
|
||||
@ -432,7 +427,7 @@ class PlayerFragment :
|
||||
} else {
|
||||
// Download list and Album art must be updated when resumed
|
||||
onPlaylistChanged()
|
||||
onCurrentChanged()
|
||||
onTrackChanged()
|
||||
}
|
||||
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
@ -764,10 +759,9 @@ class PlayerFragment :
|
||||
if (cancel?.isCancellationRequested == true) return
|
||||
val mediaPlayerController = mediaPlayerController
|
||||
if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) {
|
||||
onCurrentChanged()
|
||||
onTrackChanged()
|
||||
}
|
||||
onSliderProgressChanged()
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
updateSeekBar()
|
||||
}
|
||||
|
||||
private fun savePlaylistInBackground(playlistName: String) {
|
||||
@ -827,12 +821,9 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
// Create listener
|
||||
val clickHandler: ((Track, Int) -> Unit) = { _, pos ->
|
||||
mediaPlayerController.seekTo(pos, 0)
|
||||
mediaPlayerController.prepare()
|
||||
mediaPlayerController.play()
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
val clickHandler: ((Track, Int) -> Unit) = { _, listPos ->
|
||||
val mediaIndex = mediaPlayerController.getUnshuffledIndexOf(listPos)
|
||||
mediaPlayerController.play(mediaIndex)
|
||||
}
|
||||
|
||||
viewAdapter.register(
|
||||
@ -931,6 +922,7 @@ class PlayerFragment :
|
||||
if (actionState == ACTION_STATE_IDLE && dragging) {
|
||||
dragging = false
|
||||
// Move the item in the playlist separately
|
||||
Timber.i("Moving item %s to %s", startPosition, endPosition)
|
||||
mediaPlayerController.moveItemInPlaylist(startPosition, endPosition)
|
||||
}
|
||||
}
|
||||
@ -1010,7 +1002,8 @@ class PlayerFragment :
|
||||
|
||||
private fun onPlaylistChanged() {
|
||||
val mediaPlayerController = mediaPlayerController
|
||||
val list = mediaPlayerController.playlist
|
||||
// Try to display playlist in play order
|
||||
val list = mediaPlayerController.playlistInPlayOrder
|
||||
emptyTextView.setText(R.string.playlist_empty)
|
||||
|
||||
viewAdapter.submitList(list.map(MediaItem::toTrack))
|
||||
@ -1020,7 +1013,7 @@ class PlayerFragment :
|
||||
updateRepeatButtonState(mediaPlayerController.repeatMode)
|
||||
}
|
||||
|
||||
private fun onCurrentChanged() {
|
||||
private fun onTrackChanged() {
|
||||
currentSong = mediaPlayerController.currentMediaItem?.toTrack()
|
||||
|
||||
scrollToCurrent()
|
||||
@ -1064,7 +1057,7 @@ class PlayerFragment :
|
||||
it.loadImage(albumArtImageView, currentSong, true, 0)
|
||||
}
|
||||
|
||||
displaySongRating()
|
||||
updateSongRating()
|
||||
} else {
|
||||
currentSong = null
|
||||
songTitleTextView.text = null
|
||||
@ -1078,24 +1071,27 @@ class PlayerFragment :
|
||||
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
|
||||
private fun onSliderProgressChanged() {
|
||||
|
||||
private fun updateSeekBar() {
|
||||
Timber.i("Calling updateSeekBar")
|
||||
val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled
|
||||
val millisPlayed: Int = max(0, mediaPlayerController.playerPosition)
|
||||
val duration: Int = mediaPlayerController.playerDuration
|
||||
val playbackState: Int = mediaPlayerController.playbackState
|
||||
val isPlaying = mediaPlayerController.isPlaying
|
||||
|
||||
if (cancellationToken.isCancellationRequested) return
|
||||
if (currentSong != null) {
|
||||
positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true)
|
||||
durationTextView.text = Util.formatTotalDuration(duration.toLong(), true)
|
||||
progressBar.max =
|
||||
if (duration == 0) 100 else duration // Work-around for apparent bug.
|
||||
progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug.
|
||||
progressBar.progress = millisPlayed
|
||||
progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled
|
||||
} else {
|
||||
@ -1107,18 +1103,18 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
val progress = mediaPlayerController.bufferedPercentage
|
||||
updateBufferProgress(playbackState, progress)
|
||||
}
|
||||
|
||||
private fun updateTitle(playbackState: Int) {
|
||||
when (playbackState) {
|
||||
Player.STATE_BUFFERING -> {
|
||||
|
||||
val downloadStatus = resources.getString(
|
||||
R.string.download_playerstate_loading
|
||||
)
|
||||
progressBar.secondaryProgress = progress
|
||||
setTitle(this@PlayerFragment, downloadStatus)
|
||||
}
|
||||
Player.STATE_READY -> {
|
||||
progressBar.secondaryProgress = progress
|
||||
if (mediaPlayerController.isShufflePlayEnabled) {
|
||||
setTitle(
|
||||
this@PlayerFragment,
|
||||
@ -1128,13 +1124,22 @@ class PlayerFragment :
|
||||
setTitle(this@PlayerFragment, R.string.common_appname)
|
||||
}
|
||||
}
|
||||
Player.STATE_IDLE,
|
||||
Player.STATE_ENDED,
|
||||
-> {
|
||||
}
|
||||
Player.STATE_IDLE, Player.STATE_ENDED -> {}
|
||||
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) {
|
||||
Player.STATE_READY -> {
|
||||
pauseButton.isVisible = isPlaying
|
||||
@ -1152,10 +1157,6 @@ class PlayerFragment :
|
||||
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) {
|
||||
@ -1189,18 +1190,14 @@ class PlayerFragment :
|
||||
// Right to Left swipe
|
||||
if (e1X - e2X > swipeDistance && absX > swipeVelocity) {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.next()
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
mediaPlayerController.seekToNext()
|
||||
return true
|
||||
}
|
||||
|
||||
// Left to Right swipe
|
||||
if (e2X - e1X > swipeDistance && absX > swipeVelocity) {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.previous()
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
mediaPlayerController.seekToPrevious()
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1208,7 +1205,6 @@ class PlayerFragment :
|
||||
if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000)
|
||||
onSliderProgressChanged()
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1216,7 +1212,6 @@ class PlayerFragment :
|
||||
if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000)
|
||||
onSliderProgressChanged()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -1237,7 +1232,7 @@ class PlayerFragment :
|
||||
return false
|
||||
}
|
||||
|
||||
private fun displaySongRating() {
|
||||
private fun updateSongRating() {
|
||||
var rating = 0
|
||||
|
||||
if (currentSong?.userRating != null) {
|
||||
@ -1253,7 +1248,7 @@ class PlayerFragment :
|
||||
|
||||
private fun setSongRating(rating: Int) {
|
||||
if (currentSong == null) return
|
||||
displaySongRating()
|
||||
updateSongRating()
|
||||
mediaPlayerController.setSongRating(rating)
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,12 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
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.MediaSession
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import java.util.Random
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -175,6 +178,27 @@ class PlaybackService :
|
||||
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
|
||||
rxBusSubscription += RxBus.shutdownCommandObservable.subscribe {
|
||||
Timber.i("Received destroy command via Rx")
|
||||
@ -185,6 +209,24 @@ class PlaybackService :
|
||||
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 {
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
cacheNextSongs()
|
||||
|
@ -11,11 +11,14 @@ import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
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.REPEAT_MODE_OFF
|
||||
import androidx.media3.common.Timeline
|
||||
@ -30,6 +33,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
@ -60,6 +64,7 @@ class MediaPlayerController(
|
||||
private val externalStorageMonitor: ExternalStorageMonitor,
|
||||
val context: Context
|
||||
) : KoinComponent {
|
||||
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private var created = false
|
||||
@ -96,6 +101,14 @@ class MediaPlayerController(
|
||||
* We run the event through RxBus in order to throttle them
|
||||
*/
|
||||
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))
|
||||
}
|
||||
|
||||
@ -150,19 +163,21 @@ class MediaPlayerController(
|
||||
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
val timeline: Timeline = controller!!.currentTimeline
|
||||
var windowIndex = timeline.getFirstWindowIndex( /* shuffleModeEnabled= */true)
|
||||
var windowIndex = timeline.getFirstWindowIndex(true)
|
||||
var count = 0
|
||||
Timber.d("Shuffle: windowIndex: $windowIndex, at: $count")
|
||||
while (windowIndex != C.INDEX_UNSET) {
|
||||
count++
|
||||
windowIndex = timeline.getNextWindowIndex(
|
||||
windowIndex, REPEAT_MODE_OFF, /* shuffleModeEnabled= */true
|
||||
windowIndex, REPEAT_MODE_OFF, true
|
||||
)
|
||||
Timber.d("Shuffle: windowIndex: $windowIndex, at: $count")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var deferredPlay: (() -> Unit)? = null
|
||||
|
||||
private var cachedMediaItem: MediaItem? = null
|
||||
|
||||
fun onCreate(onCreated: () -> Unit) {
|
||||
@ -259,7 +274,7 @@ class MediaPlayerController(
|
||||
private fun publishPlaybackState() {
|
||||
val newState = RxBus.StateWithTrack(
|
||||
track = currentMediaItem?.toTrack(),
|
||||
index = currentMediaItemIndex,
|
||||
index = if (isShufflePlayEnabled) getCurrentShuffleIndex() else currentMediaItemIndex,
|
||||
isPlaying = isPlaying,
|
||||
state = playbackState
|
||||
)
|
||||
@ -316,6 +331,8 @@ class MediaPlayerController(
|
||||
@Synchronized
|
||||
fun play(index: Int) {
|
||||
controller?.seekTo(index, 0L)
|
||||
// FIXME CHECK ITS NOT MAKING PROBLEMS
|
||||
controller?.prepare()
|
||||
controller?.play()
|
||||
}
|
||||
|
||||
@ -404,6 +421,7 @@ class MediaPlayerController(
|
||||
}
|
||||
|
||||
if (shuffle) isShufflePlayEnabled = true
|
||||
Timber.w("Adding ${mediaItems.size} media items")
|
||||
controller?.addMediaItems(insertAt, mediaItems)
|
||||
|
||||
prepare()
|
||||
@ -411,10 +429,19 @@ class MediaPlayerController(
|
||||
// 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.
|
||||
// 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) {
|
||||
val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled) ?: 0
|
||||
play(start)
|
||||
if (isShufflePlayEnabled) {
|
||||
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
|
||||
get() = controller?.shuffleModeEnabled == true
|
||||
set(enabled) {
|
||||
Timber.i("Shuffle is now enabled: %s", enabled)
|
||||
RxBus.shufflePlayPublisher.onNext(enabled)
|
||||
controller?.shuffleModeEnabled = enabled
|
||||
}
|
||||
|
||||
@ -431,11 +460,17 @@ class MediaPlayerController(
|
||||
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
|
||||
get() = controller?.bufferedPercentage ?: 0
|
||||
|
||||
@Synchronized
|
||||
fun moveItemInPlaylist(oldPos: Int, newPos: Int) {
|
||||
// TODO: This currently does not care about shuffle position.
|
||||
controller?.moveMediaItem(oldPos, newPos)
|
||||
}
|
||||
|
||||
@ -494,15 +529,25 @@ class MediaPlayerController(
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun previous() {
|
||||
fun seekToPrevious() {
|
||||
controller?.seekToPrevious()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
operator fun next() {
|
||||
fun canSeekToPrevious(): Boolean {
|
||||
return controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun seekToNext() {
|
||||
controller?.seekToNext()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun canSeekToNext(): Boolean {
|
||||
return controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun reset() {
|
||||
controller?.clearMediaItems()
|
||||
@ -693,15 +738,15 @@ class MediaPlayerController(
|
||||
if (currentMediaItem == null) return
|
||||
val song = currentMediaItem!!.toTrack()
|
||||
song.userRating = rating
|
||||
Thread {
|
||||
try {
|
||||
getMusicService().setRating(song.id, rating)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
mainScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
getMusicService().setRating(song.id, rating)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
// TODO this would be better handled with a Rx command
|
||||
// updateNotification()
|
||||
}
|
||||
}
|
||||
|
||||
val currentMediaItem: MediaItem?
|
||||
@ -710,9 +755,65 @@ class MediaPlayerController(
|
||||
val currentMediaItemIndex: Int
|
||||
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
|
||||
get() = controller?.mediaItemCount ?: 0
|
||||
|
||||
fun getMediaItemAt(index: Int): MediaItem? {
|
||||
return controller?.getMediaItemAt(index)
|
||||
}
|
||||
|
||||
val playlistSize: Int
|
||||
get() = controller?.currentTimeline?.windowCount ?: 0
|
||||
|
||||
@ -721,10 +822,6 @@ class MediaPlayerController(
|
||||
return Util.getPlayListFromTimeline(controller?.currentTimeline, false)
|
||||
}
|
||||
|
||||
fun getMediaItemAt(index: Int): MediaItem? {
|
||||
return controller?.getMediaItemAt(index)
|
||||
}
|
||||
|
||||
val playlistInPlayOrder: List<MediaItem>
|
||||
get() {
|
||||
return Util.getPlayListFromTimeline(
|
||||
|
@ -182,8 +182,8 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
|
||||
KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.previous()
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.next()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.seekToPrevious()
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.seekToNext()
|
||||
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop()
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play()
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause()
|
||||
@ -226,8 +226,8 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
// no need to call anything
|
||||
if (isRunning) mediaPlayerController.resumeOrPlay()
|
||||
|
||||
Constants.CMD_NEXT -> mediaPlayerController.next()
|
||||
Constants.CMD_PREVIOUS -> mediaPlayerController.previous()
|
||||
Constants.CMD_NEXT -> mediaPlayerController.seekToNext()
|
||||
Constants.CMD_PREVIOUS -> mediaPlayerController.seekToPrevious()
|
||||
Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause()
|
||||
Constants.CMD_STOP -> mediaPlayerController.stop()
|
||||
Constants.CMD_PAUSE -> mediaPlayerController.pause()
|
||||
|
@ -20,9 +20,13 @@ class RxBus {
|
||||
|
||||
private fun mainThread() = AndroidSchedulers.from(Looper.getMainLooper())
|
||||
|
||||
val shufflePlayPublisher: PublishSubject<Boolean> =
|
||||
PublishSubject.create()
|
||||
val shufflePlayObservable: Observable<Boolean> =
|
||||
shufflePlayPublisher
|
||||
|
||||
var activeServerChangingPublisher: PublishSubject<Int> =
|
||||
PublishSubject.create()
|
||||
|
||||
// Subscribers should be called synchronously, not on another thread
|
||||
var activeServerChangingObservable: Observable<Int> =
|
||||
activeServerChangingPublisher
|
||||
|
@ -757,7 +757,7 @@ object Util {
|
||||
|
||||
fun getPlayListFromTimeline(
|
||||
timeline: Timeline?,
|
||||
shuffle: Boolean,
|
||||
isShuffled: Boolean,
|
||||
firstIndex: Int? = null,
|
||||
count: Int? = null
|
||||
): List<MediaItem> {
|
||||
@ -765,13 +765,13 @@ object Util {
|
||||
if (timeline.windowCount < 1) return emptyList()
|
||||
|
||||
val playlist: MutableList<MediaItem> = mutableListOf()
|
||||
var i = firstIndex ?: timeline.getFirstWindowIndex(false)
|
||||
var i = firstIndex ?: timeline.getFirstWindowIndex(isShuffled)
|
||||
if (i == C.INDEX_UNSET) return emptyList()
|
||||
|
||||
while (i != C.INDEX_UNSET && (count != playlist.count())) {
|
||||
val window = timeline.getWindow(i, Timeline.Window())
|
||||
playlist.add(window.mediaItem)
|
||||
i = timeline.getNextWindowIndex(i, Player.REPEAT_MODE_OFF, shuffle)
|
||||
i = timeline.getNextWindowIndex(i, Player.REPEAT_MODE_OFF, isShuffled)
|
||||
}
|
||||
return playlist
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user