From db1f61407f7798d9fd0b15e5d5e58f154509b1c1 Mon Sep 17 00:00:00 2001 From: Naveen Date: Sat, 14 Oct 2023 12:21:18 +0530 Subject: [PATCH 1/6] Improve recyclerview performance for large number of items The previous ConstraintLayout + RelativeLayout + double nested ConstraintLayouts for each item was too slow and led to laggy scroll experience. --- app/src/main/res/layout/item_track.xml | 143 ++++++++----------- app/src/main/res/layout/item_track_queue.xml | 24 ++-- 2 files changed, 66 insertions(+), 101 deletions(-) diff --git a/app/src/main/res/layout/item_track.xml b/app/src/main/res/layout/item_track.xml index 8799df458..21c9a2c64 100644 --- a/app/src/main/res/layout/item_track.xml +++ b/app/src/main/res/layout/item_track.xml @@ -1,108 +1,79 @@ - - + - + + + - - - - + tools:text="Track title" /> - - - - - + android:alpha="0.6" + android:ellipsize="end" + android:maxLines="1" + android:textSize="@dimen/normal_text_size" + tools:text="Track artist" /> + + - + - + - - - + diff --git a/app/src/main/res/layout/item_track_queue.xml b/app/src/main/res/layout/item_track_queue.xml index fe4b5aeb0..5a6f0afd3 100644 --- a/app/src/main/res/layout/item_track_queue.xml +++ b/app/src/main/res/layout/item_track_queue.xml @@ -1,11 +1,12 @@ - + android:src="@drawable/ic_drag_handle_vector" /> - + From 7a9c5a16124e397a4a02fa4ca0b18a5f5b61dca6 Mon Sep 17 00:00:00 2001 From: Naveen Date: Sun, 15 Oct 2023 20:50:05 +0530 Subject: [PATCH 2/6] Update media3 to 1.2.0-alpha02 --- .../playback/player/AudioOnlyRenderersFactory.kt | 9 +-------- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/AudioOnlyRenderersFactory.kt b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/AudioOnlyRenderersFactory.kt index b7e72beea..39d318979 100644 --- a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/AudioOnlyRenderersFactory.kt +++ b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/AudioOnlyRenderersFactory.kt @@ -22,7 +22,7 @@ private const val SKIP_SILENCE_THRESHOLD_LEVEL = 16.toShort() @UnstableApi class AudioOnlyRenderersFactory(context: Context) : DefaultRenderersFactory(context) { - override fun buildAudioSink(context: Context, enableFloatOutput: Boolean, enableAudioTrackPlaybackParams: Boolean, enableOffload: Boolean): AudioSink? { + override fun buildAudioSink(context: Context, enableFloatOutput: Boolean, enableAudioTrackPlaybackParams: Boolean): AudioSink { val silenceSkippingAudioProcessor = SilenceSkippingAudioProcessor( SKIP_SILENCE_MINIMUM_DURATION_US, DEFAULT_PADDING_SILENCE_US, @@ -32,13 +32,6 @@ class AudioOnlyRenderersFactory(context: Context) : DefaultRenderersFactory(cont return DefaultAudioSink.Builder(context) .setEnableFloatOutput(enableFloatOutput) .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) - .setOffloadMode( - if (enableOffload) { - DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED - } else { - DefaultAudioSink.OFFLOAD_MODE_DISABLED - } - ) .setAudioProcessorChain( DefaultAudioSink.DefaultAudioProcessorChain( arrayOf(), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4912c189c..7993f3c75 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ eventbus = "3.3.1" lottie = "6.1.0" m3uParser = "1.3.0" media = "1.6.0" -media3 = "1.1.1" +media3 = "1.2.0-alpha02" room = "2.5.2" #Simple Mobile Tools simple-commons = "e1603ee2d6" From a742ac461693f805299069b1ffb5bc5907fa43be Mon Sep 17 00:00:00 2001 From: Naveen Date: Sun, 15 Oct 2023 20:54:19 +0530 Subject: [PATCH 3/6] Only set mediaId on MediaItems in media controller This makes the media controller more responsive, see https://github.com/androidx/media/issues/81 --- .../musicplayer/extensions/Player.kt | 6 +- .../musicplayer/models/Track.kt | 7 +++ .../playback/MediaSessionCallback.kt | 56 ++++++++----------- .../playback/library/MediaItemProvider.kt | 11 +++- 4 files changed, 44 insertions(+), 36 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/musicplayer/extensions/Player.kt b/app/src/main/kotlin/com/simplemobiletools/musicplayer/extensions/Player.kt index 7c2f82d3d..b6b573f38 100644 --- a/app/src/main/kotlin/com/simplemobiletools/musicplayer/extensions/Player.kt +++ b/app/src/main/kotlin/com/simplemobiletools/musicplayer/extensions/Player.kt @@ -7,7 +7,7 @@ import androidx.media3.common.Player import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.musicplayer.helpers.PlaybackSetting import com.simplemobiletools.musicplayer.models.Track -import com.simplemobiletools.musicplayer.models.toMediaItems +import com.simplemobiletools.musicplayer.models.toMediaItemsFast val Player.isReallyPlaying: Boolean get() = when (playbackState) { @@ -149,7 +149,7 @@ fun Player.prepareUsingTracks( return } - val mediaItems = tracks.toMediaItems() + val mediaItems = tracks.toMediaItemsFast() runOnPlayerThread { setMediaItems(mediaItems, startIndex, startPositionMs) playWhenReady = play @@ -179,7 +179,7 @@ inline fun Player.maybePreparePlayer(context: Context, crossinline callback: (su return@getQueuedTracksLazily } - addRemainingMediaItems(tracks.toMediaItems(), startIndex) + addRemainingMediaItems(tracks.toMediaItemsFast(), startIndex) } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/musicplayer/models/Track.kt b/app/src/main/kotlin/com/simplemobiletools/musicplayer/models/Track.kt index caf05a766..8be6e02c6 100644 --- a/app/src/main/kotlin/com/simplemobiletools/musicplayer/models/Track.kt +++ b/app/src/main/kotlin/com/simplemobiletools/musicplayer/models/Track.kt @@ -3,6 +3,7 @@ package com.simplemobiletools.musicplayer.models import android.content.ContentUris import android.net.Uri import android.provider.MediaStore +import androidx.media3.common.MediaItem import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Index @@ -101,3 +102,9 @@ data class Track( fun ArrayList.sortSafely(sorting: Int) = sortSafely(Track.getComparator(sorting)) fun Collection.toMediaItems() = map { it.toMediaItem() } + +fun Collection.toMediaItemsFast() = map { + MediaItem.Builder() + .setMediaId(it.mediaStoreId.toString()) + .build() +} diff --git a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/MediaSessionCallback.kt b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/MediaSessionCallback.kt index 279130068..f3d3b99ce 100644 --- a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/MediaSessionCallback.kt +++ b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/MediaSessionCallback.kt @@ -166,52 +166,44 @@ internal fun PlaybackService.getMediaSessionCallback() = object : MediaLibrarySe mediaItems: MutableList, startIndex: Int, startPositionMs: Long - ) = if (controller.packageName == packageName) { - Futures.immediateFuture(MediaSession.MediaItemsWithStartPosition(mediaItems, startIndex, startPositionMs)) - } else { - callWhenSourceReady { - // this is to avoid single items in the queue: https://github.com/androidx/media/issues/156 - var queueItems = mediaItems - val startItemId = mediaItems[0].mediaId - val currentItems = mediaItemProvider.getChildren(currentRoot).orEmpty() + ): ListenableFuture { + if (controller.packageName == packageName) { + return super.onSetMediaItems(mediaSession, controller, mediaItems, startIndex, startPositionMs) + } - queueItems = if (currentItems.any { it.mediaId == startItemId }) { - currentItems.toMutableList() - } else { - mediaItemProvider.getDefaultQueue()?.toMutableList() ?: queueItems - } + // this is to avoid single items in the queue: https://github.com/androidx/media/issues/156 + var queueItems = mediaItems + val startItemId = mediaItems[0].mediaId + val currentItems = mediaItemProvider.getChildren(currentRoot).orEmpty() - val startItemIndex = queueItems.indexOfFirst { it.mediaId == startItemId } - super.onSetMediaItems(mediaSession, controller, queueItems, startItemIndex, startPositionMs).get() + queueItems = if (currentItems.any { it.mediaId == startItemId }) { + currentItems.toMutableList() + } else { + mediaItemProvider.getDefaultQueue()?.toMutableList() ?: queueItems } + + val startItemIndex = queueItems.indexOfFirst { it.mediaId == startItemId } + return super.onSetMediaItems(mediaSession, controller, queueItems, startItemIndex, startPositionMs) } override fun onAddMediaItems( mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: List - ) = if (controller.packageName == packageName) { - Futures.immediateFuture(mediaItems) - } else { - callWhenSourceReady { - mediaItems.map { mediaItem -> - if (mediaItem.requestMetadata.searchQuery != null) { - getMediaItemFromSearchQuery(mediaItem.requestMetadata.searchQuery!!) - } else { - mediaItemProvider[mediaItem.mediaId] ?: mediaItem - } + ): ListenableFuture> { + val items = mediaItems.map { mediaItem -> + if (mediaItem.requestMetadata.searchQuery != null) { + getMediaItemFromSearchQuery(mediaItem.requestMetadata.searchQuery!!) + } else { + mediaItemProvider[mediaItem.mediaId] ?: mediaItem } } + + return Futures.immediateFuture(items) } private fun getMediaItemFromSearchQuery(query: String): MediaItem { - val searchQuery = if (query.startsWith("play ", ignoreCase = true)) { - query.drop(5).lowercase() - } else { - query.lowercase() - } - - return mediaItemProvider.getItemFromSearch(searchQuery) ?: mediaItemProvider.getRandomItem() + return mediaItemProvider.getItemFromSearch(query.lowercase()) ?: mediaItemProvider.getRandomItem() } private fun reloadContent() { diff --git a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/library/MediaItemProvider.kt b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/library/MediaItemProvider.kt index af437673f..5f2720c20 100644 --- a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/library/MediaItemProvider.kt +++ b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/library/MediaItemProvider.kt @@ -89,7 +89,16 @@ internal class MediaItemProvider(private val context: Context) { } } - operator fun get(mediaId: String) = getNode(mediaId)?.item + operator fun get(mediaId: String): MediaItem? { + val mediaItem = getNode(mediaId)?.item + if (mediaItem == null) { + // assume it's a track + val mediaStoreId = mediaId.toLongOrNull() ?: return null + return audioHelper.getTrack(mediaStoreId)?.toMediaItem() + } + + return mediaItem + } fun getRootItem() = get(SMP_ROOT_ID)!! From 81e6f47f56f025196cf2e687c43e23c4acffdd98 Mon Sep 17 00:00:00 2001 From: Naveen Date: Sun, 15 Oct 2023 20:54:45 +0530 Subject: [PATCH 4/6] Ensure operation on a background thread --- .../musicplayer/playback/library/MediaItemProvider.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/library/MediaItemProvider.kt b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/library/MediaItemProvider.kt index 5f2720c20..f2e0cb605 100644 --- a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/library/MediaItemProvider.kt +++ b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/library/MediaItemProvider.kt @@ -12,7 +12,7 @@ import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata.MediaType import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaSession.MediaItemsWithStartPosition -import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import com.google.common.util.concurrent.MoreExecutors import com.simplemobiletools.musicplayer.R import com.simplemobiletools.musicplayer.extensions.* import com.simplemobiletools.musicplayer.helpers.TAB_ALBUMS @@ -23,6 +23,7 @@ import com.simplemobiletools.musicplayer.helpers.TAB_PLAYLISTS import com.simplemobiletools.musicplayer.helpers.TAB_TRACKS import com.simplemobiletools.musicplayer.models.QueueItem import com.simplemobiletools.musicplayer.models.toMediaItems +import java.util.concurrent.Executors private const val STATE_CREATED = 1 private const val STATE_INITIALIZING = 2 @@ -42,6 +43,9 @@ private const val SMP_GENRES_ROOT_ID = "__GENRES__" */ @UnstableApi internal class MediaItemProvider(private val context: Context) { + private val executor by lazy { + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()) + } inner class MediaItemNode(val item: MediaItem) { private val children: MutableList = ArrayList() @@ -154,7 +158,7 @@ internal class MediaItemProvider(private val context: Context) { return } - ensureBackgroundThread { + executor.execute { val trackId = current.mediaId.toLong() val queueItems = mediaItems.mapIndexed { index, mediaItem -> QueueItem(trackId = mediaItem.mediaId.toLong(), trackOrder = index, isCurrent = false, lastPosition = 0) @@ -166,8 +170,7 @@ internal class MediaItemProvider(private val context: Context) { fun reload() { state = STATE_INITIALIZING - - ensureBackgroundThread { + executor.execute { buildRoot() try { From 56ada154106c02b34c2e5d400d00cebcc5b7cecf Mon Sep 17 00:00:00 2001 From: Naveen Date: Sun, 15 Oct 2023 21:47:15 +0530 Subject: [PATCH 5/6] Improve seek/next previous performance with large playlists --- .../playback/player/SimpleMusicPlayer.kt | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/SimpleMusicPlayer.kt b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/SimpleMusicPlayer.kt index c1ddfdbe0..200b279ab 100644 --- a/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/SimpleMusicPlayer.kt +++ b/app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/SimpleMusicPlayer.kt @@ -8,12 +8,19 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder import com.simplemobiletools.musicplayer.extensions.* import com.simplemobiletools.musicplayer.inlines.indexOfFirstOrNull +import kotlinx.coroutines.* private const val DEFAULT_SHUFFLE_ORDER_SEED = 42L @UnstableApi class SimpleMusicPlayer(private val exoPlayer: ExoPlayer) : ForwardingPlayer(exoPlayer) { + private var seekToNextCount = 0 + private var seekToPreviousCount = 0 + + private val scope = CoroutineScope(Dispatchers.Default) + private var seekJob: Job? = null + /** * The default implementation only advertises the seek to next and previous item in the case * that it's not the first or last track. We manually advertise that these @@ -53,28 +60,32 @@ class SimpleMusicPlayer(private val exoPlayer: ExoPlayer) : ForwardingPlayer(exo override fun seekToNext() { play() if (!maybeForceNext()) { - super.seekToNext() + seekToNextCount += 1 + seekWithDelay() } } override fun seekToPrevious() { play() if (!maybeForcePrevious()) { - super.seekToPrevious() + seekToPreviousCount += 1 + seekWithDelay() } } override fun seekToNextMediaItem() { play() if (!maybeForceNext()) { - super.seekToNextMediaItem() + seekToNextCount += 1 + seekWithDelay() } } override fun seekToPreviousMediaItem() { play() if (!maybeForcePrevious()) { - super.seekToPreviousMediaItem() + seekToPreviousCount += 1 + seekWithDelay() } } @@ -122,4 +133,36 @@ class SimpleMusicPlayer(private val exoPlayer: ExoPlayer) : ForwardingPlayer(exo exoPlayer.setShuffleOrder(DefaultShuffleOrder(shuffledIndices.toIntArray(), DEFAULT_SHUFFLE_ORDER_SEED)) } } + + /** + * This is here so the player can quickly seek next/previous without doing too much work. + * It probably won't be needed once https://github.com/androidx/media/issues/81 is resolved. + */ + private fun seekWithDelay() { + seekJob?.cancel() + seekJob = scope.launch { + delay(timeMillis = 400) + if (seekToNextCount > 0 || seekToPreviousCount > 0) { + runOnPlayerThread { + if (currentMediaItem != null) { + if (seekToNextCount > 0) { + seekTo(rotateIndex(currentMediaItemIndex + seekToNextCount), 0) + } + + if (seekToPreviousCount > 0) { + seekTo(rotateIndex(currentMediaItemIndex - seekToPreviousCount), 0) + } + + seekToNextCount = 0 + seekToPreviousCount = 0 + } + } + } + } + } + + private fun rotateIndex(index: Int): Int { + val count = mediaItemCount + return (index % count + count) % count + } } From eba618556155e041421554498223114346d86275 Mon Sep 17 00:00:00 2001 From: Naveen Date: Sun, 15 Oct 2023 22:01:34 +0530 Subject: [PATCH 6/6] Avoid preparing the player twice It's costly for large playlists --- .../com/simplemobiletools/musicplayer/extensions/Player.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/musicplayer/extensions/Player.kt b/app/src/main/kotlin/com/simplemobiletools/musicplayer/extensions/Player.kt index b6b573f38..d2822bb47 100644 --- a/app/src/main/kotlin/com/simplemobiletools/musicplayer/extensions/Player.kt +++ b/app/src/main/kotlin/com/simplemobiletools/musicplayer/extensions/Player.kt @@ -164,8 +164,10 @@ fun Player.prepareUsingTracks( * items are added using [addRemainingMediaItems]. This helps prevent delays, especially with large queues, and * avoids potential issues like [android.app.ForegroundServiceStartNotAllowedException] when starting from background. */ +var prepareInProgress = false inline fun Player.maybePreparePlayer(context: Context, crossinline callback: (success: Boolean) -> Unit) { - if (currentMediaItem == null) { + if (!prepareInProgress && currentMediaItem == null) { + prepareInProgress = true ensureBackgroundThread { var prepared = false context.audioHelper.getQueuedTracksLazily { tracks, startIndex, startPositionMs ->