diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt index 74cf6a8a..0afaccd2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt @@ -77,7 +77,7 @@ class LegacyPlaylistManager : KoinComponent { // Public facing playlist (immutable) val playlist: List - get() = _playlist.toList() + get() = _playlist @get:Synchronized val playlistDuration: Long diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 9681e4aa..f16a2d39 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -150,18 +150,20 @@ class Downloader( // Store the result in a flag to know if changes have occurred var listChanged = cleanupActiveDownloads() + val playlist = legacyPlaylistManager.playlist + // Check if need to preload more from playlist val preloadCount = Settings.preloadCount // Start preloading at the current playing song var start = mediaController.currentMediaItemIndex - if (start == -1) start = 0 + if (start == -1 || start > playlist.size) start = 0 - val end = (start + preloadCount).coerceAtMost(mediaController.mediaItemCount) + val end = (start + preloadCount).coerceAtMost(playlist.size) for (i in start until end) { - val download = legacyPlaylistManager.playlist[i] + val download = playlist[i] // Set correct priority (the lower the number, the higher the priority) download.priority = i diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 13d9dda7..1ff9aa0b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -18,6 +18,9 @@ import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import com.google.common.util.concurrent.MoreExecutors import io.reactivex.rxjava3.disposables.CompositeDisposable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.app.UApp @@ -61,6 +64,8 @@ class MediaPlayerController( private val rxBusSubscription: CompositeDisposable = CompositeDisposable() + private var mainScope = CoroutineScope(Dispatchers.Main) + private var sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java)) @@ -94,10 +99,10 @@ class MediaPlayerController( /* * This will be called everytime the playlist has changed. + * We run the event through RxBus in order to throttle them */ override fun onTimelineChanged(timeline: Timeline, reason: Int) { legacyPlaylistManager.rebuildPlaylist(controller!!) - serializeCurrentSession() } override fun onPlaybackStateChanged(playbackState: Int) { @@ -143,6 +148,20 @@ class MediaPlayerController( isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault } + rxBusSubscription += RxBus.throttledPlaylistObservable.subscribe { + // Even though Rx should launch on the main thread it doesn't always :( + mainScope.launch { + serializeCurrentSession() + } + } + + rxBusSubscription += RxBus.throttledPlayerStateObservable.subscribe { + // Even though Rx should launch on the main thread it doesn't always :( + mainScope.launch { + serializeCurrentSession() + } + } + created = true Timber.i("MediaPlayerController started") } @@ -162,9 +181,6 @@ class MediaPlayerController( } } - // Save playback state - serializeCurrentSession() - // Update widget if (currentPlaying != null) { updateWidget(currentPlaying.track) @@ -367,8 +383,6 @@ class MediaPlayerController( if (songs == null) return val filteredSongs = songs.filterNotNull() downloader.downloadBackground(filteredSongs, save) - - serializeCurrentSession() } fun stopJukeboxService() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index 6d015cb5..552c399c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -9,8 +9,6 @@ package org.moire.ultrasonic.service import android.content.Context import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.locks.Lock -import java.util.concurrent.locks.ReentrantLock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -30,9 +28,6 @@ class PlaybackStateSerializer : KoinComponent { private val context by inject() - private val lock: Lock = ReentrantLock() - private val setup = AtomicBoolean(false) - private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) @@ -41,25 +36,24 @@ class PlaybackStateSerializer : KoinComponent { currentPlayingIndex: Int, currentPlayingPosition: Int ) { - if (!setup.get()) return + if (isSerializing.get() || !isSetup.get()) return - if (lock.tryLock()) { - ioScope.launch { - try { - serializeNow(songs, currentPlayingIndex, currentPlayingPosition) - } finally { - lock.unlock() - } - } + isSerializing.set(true) + + ioScope.launch { + serializeNow(songs, currentPlayingIndex, currentPlayingPosition) + }.invokeOnCompletion { + isSerializing.set(false) } } fun serializeNow( - songs: Iterable, + referencedList: Iterable, currentPlayingIndex: Int, currentPlayingPosition: Int ) { val state = State() + val songs = referencedList.toList() for (downloadFile in songs) { state.songs.add(downloadFile.track) @@ -78,16 +72,15 @@ class PlaybackStateSerializer : KoinComponent { } fun deserialize(afterDeserialized: (State?) -> Unit?) { - + if (isDeserializing.get()) return ioScope.launch { try { - lock.lock() deserializeNow(afterDeserialized) - setup.set(true) + isSetup.set(true) } catch (all: Exception) { Timber.e(all, "Had a problem deserializing:") } finally { - lock.unlock() + isDeserializing.set(false) } } } @@ -108,4 +101,10 @@ class PlaybackStateSerializer : KoinComponent { afterDeserialized(state) } } + + companion object { + private val isSetup = AtomicBoolean(false) + private val isSerializing = AtomicBoolean(false) + private val isDeserializing = AtomicBoolean(false) + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 3f792ca9..03859659 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -1,5 +1,6 @@ package org.moire.ultrasonic.service +import android.os.Looper import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -8,42 +9,55 @@ import io.reactivex.rxjava3.subjects.PublishSubject import java.util.concurrent.TimeUnit class RxBus { + companion object { + private fun mainThread() = AndroidSchedulers.from(Looper.getMainLooper()) + var activeServerChangePublisher: PublishSubject = PublishSubject.create() var activeServerChangeObservable: Observable = - activeServerChangePublisher.observeOn(AndroidSchedulers.mainThread()) + activeServerChangePublisher.observeOn(mainThread()) val themeChangedEventPublisher: PublishSubject = PublishSubject.create() val themeChangedEventObservable: Observable = - themeChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) + themeChangedEventPublisher.observeOn(mainThread()) val musicFolderChangedEventPublisher: PublishSubject = PublishSubject.create() val musicFolderChangedEventObservable: Observable = - musicFolderChangedEventPublisher.observeOn(AndroidSchedulers.mainThread()) + musicFolderChangedEventPublisher.observeOn(mainThread()) val playerStatePublisher: PublishSubject = PublishSubject.create() val playerStateObservable: Observable = - playerStatePublisher.observeOn(AndroidSchedulers.mainThread()) + playerStatePublisher.observeOn(mainThread()) .replay(1) .autoConnect(0) + val throttledPlayerStateObservable: Observable = + playerStatePublisher.observeOn(mainThread()) + .replay(1) + .autoConnect(0) + .throttleLatest(300, TimeUnit.MILLISECONDS) val playlistPublisher: PublishSubject> = PublishSubject.create() val playlistObservable: Observable> = - playlistPublisher.observeOn(AndroidSchedulers.mainThread()) + playlistPublisher.observeOn(mainThread()) .replay(1) .autoConnect(0) + val throttledPlaylistObservable: Observable> = + playlistPublisher.observeOn(mainThread()) + .replay(1) + .autoConnect(0) + .throttleLatest(300, TimeUnit.MILLISECONDS) // Commands val dismissNowPlayingCommandPublisher: PublishSubject = PublishSubject.create() val dismissNowPlayingCommandObservable: Observable = - dismissNowPlayingCommandPublisher.observeOn(AndroidSchedulers.mainThread()) + dismissNowPlayingCommandPublisher.observeOn(mainThread()) } data class StateWithTrack(