Skip to content

Commit 96c5bdf

Browse files
authored
Merge pull request #615 from naveensingh/performance_improvements
Playback performance improvements
2 parents 23c41bf + eba6185 commit 96c5bdf

File tree

9 files changed

+169
-155
lines changed

9 files changed

+169
-155
lines changed

app/src/main/kotlin/com/simplemobiletools/musicplayer/extensions/Player.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import androidx.media3.common.Player
77
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
88
import com.simplemobiletools.musicplayer.helpers.PlaybackSetting
99
import com.simplemobiletools.musicplayer.models.Track
10-
import com.simplemobiletools.musicplayer.models.toMediaItems
10+
import com.simplemobiletools.musicplayer.models.toMediaItemsFast
1111

1212
val Player.isReallyPlaying: Boolean
1313
get() = when (playbackState) {
@@ -149,7 +149,7 @@ fun Player.prepareUsingTracks(
149149
return
150150
}
151151

152-
val mediaItems = tracks.toMediaItems()
152+
val mediaItems = tracks.toMediaItemsFast()
153153
runOnPlayerThread {
154154
setMediaItems(mediaItems, startIndex, startPositionMs)
155155
playWhenReady = play
@@ -164,8 +164,10 @@ fun Player.prepareUsingTracks(
164164
* items are added using [addRemainingMediaItems]. This helps prevent delays, especially with large queues, and
165165
* avoids potential issues like [android.app.ForegroundServiceStartNotAllowedException] when starting from background.
166166
*/
167+
var prepareInProgress = false
167168
inline fun Player.maybePreparePlayer(context: Context, crossinline callback: (success: Boolean) -> Unit) {
168-
if (currentMediaItem == null) {
169+
if (!prepareInProgress && currentMediaItem == null) {
170+
prepareInProgress = true
169171
ensureBackgroundThread {
170172
var prepared = false
171173
context.audioHelper.getQueuedTracksLazily { tracks, startIndex, startPositionMs ->
@@ -179,7 +181,7 @@ inline fun Player.maybePreparePlayer(context: Context, crossinline callback: (su
179181
return@getQueuedTracksLazily
180182
}
181183

182-
addRemainingMediaItems(tracks.toMediaItems(), startIndex)
184+
addRemainingMediaItems(tracks.toMediaItemsFast(), startIndex)
183185
}
184186
}
185187
}

app/src/main/kotlin/com/simplemobiletools/musicplayer/models/Track.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.simplemobiletools.musicplayer.models
33
import android.content.ContentUris
44
import android.net.Uri
55
import android.provider.MediaStore
6+
import androidx.media3.common.MediaItem
67
import androidx.room.ColumnInfo
78
import androidx.room.Entity
89
import androidx.room.Index
@@ -101,3 +102,9 @@ data class Track(
101102
fun ArrayList<Track>.sortSafely(sorting: Int) = sortSafely(Track.getComparator(sorting))
102103

103104
fun Collection<Track>.toMediaItems() = map { it.toMediaItem() }
105+
106+
fun Collection<Track>.toMediaItemsFast() = map {
107+
MediaItem.Builder()
108+
.setMediaId(it.mediaStoreId.toString())
109+
.build()
110+
}

app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/MediaSessionCallback.kt

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -166,52 +166,44 @@ internal fun PlaybackService.getMediaSessionCallback() = object : MediaLibrarySe
166166
mediaItems: MutableList<MediaItem>,
167167
startIndex: Int,
168168
startPositionMs: Long
169-
) = if (controller.packageName == packageName) {
170-
Futures.immediateFuture(MediaSession.MediaItemsWithStartPosition(mediaItems, startIndex, startPositionMs))
171-
} else {
172-
callWhenSourceReady {
173-
// this is to avoid single items in the queue: https://github.com/androidx/media/issues/156
174-
var queueItems = mediaItems
175-
val startItemId = mediaItems[0].mediaId
176-
val currentItems = mediaItemProvider.getChildren(currentRoot).orEmpty()
169+
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
170+
if (controller.packageName == packageName) {
171+
return super.onSetMediaItems(mediaSession, controller, mediaItems, startIndex, startPositionMs)
172+
}
177173

178-
queueItems = if (currentItems.any { it.mediaId == startItemId }) {
179-
currentItems.toMutableList()
180-
} else {
181-
mediaItemProvider.getDefaultQueue()?.toMutableList() ?: queueItems
182-
}
174+
// this is to avoid single items in the queue: https://github.com/androidx/media/issues/156
175+
var queueItems = mediaItems
176+
val startItemId = mediaItems[0].mediaId
177+
val currentItems = mediaItemProvider.getChildren(currentRoot).orEmpty()
183178

184-
val startItemIndex = queueItems.indexOfFirst { it.mediaId == startItemId }
185-
super.onSetMediaItems(mediaSession, controller, queueItems, startItemIndex, startPositionMs).get()
179+
queueItems = if (currentItems.any { it.mediaId == startItemId }) {
180+
currentItems.toMutableList()
181+
} else {
182+
mediaItemProvider.getDefaultQueue()?.toMutableList() ?: queueItems
186183
}
184+
185+
val startItemIndex = queueItems.indexOfFirst { it.mediaId == startItemId }
186+
return super.onSetMediaItems(mediaSession, controller, queueItems, startItemIndex, startPositionMs)
187187
}
188188

189189
override fun onAddMediaItems(
190190
mediaSession: MediaSession,
191191
controller: MediaSession.ControllerInfo,
192192
mediaItems: List<MediaItem>
193-
) = if (controller.packageName == packageName) {
194-
Futures.immediateFuture(mediaItems)
195-
} else {
196-
callWhenSourceReady {
197-
mediaItems.map { mediaItem ->
198-
if (mediaItem.requestMetadata.searchQuery != null) {
199-
getMediaItemFromSearchQuery(mediaItem.requestMetadata.searchQuery!!)
200-
} else {
201-
mediaItemProvider[mediaItem.mediaId] ?: mediaItem
202-
}
193+
): ListenableFuture<List<MediaItem>> {
194+
val items = mediaItems.map { mediaItem ->
195+
if (mediaItem.requestMetadata.searchQuery != null) {
196+
getMediaItemFromSearchQuery(mediaItem.requestMetadata.searchQuery!!)
197+
} else {
198+
mediaItemProvider[mediaItem.mediaId] ?: mediaItem
203199
}
204200
}
201+
202+
return Futures.immediateFuture(items)
205203
}
206204

207205
private fun getMediaItemFromSearchQuery(query: String): MediaItem {
208-
val searchQuery = if (query.startsWith("play ", ignoreCase = true)) {
209-
query.drop(5).lowercase()
210-
} else {
211-
query.lowercase()
212-
}
213-
214-
return mediaItemProvider.getItemFromSearch(searchQuery) ?: mediaItemProvider.getRandomItem()
206+
return mediaItemProvider.getItemFromSearch(query.lowercase()) ?: mediaItemProvider.getRandomItem()
215207
}
216208

217209
private fun reloadContent() {

app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/library/MediaItemProvider.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import androidx.media3.common.MediaMetadata
1212
import androidx.media3.common.MediaMetadata.MediaType
1313
import androidx.media3.common.util.UnstableApi
1414
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition
15-
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
15+
import com.google.common.util.concurrent.MoreExecutors
1616
import com.simplemobiletools.musicplayer.R
1717
import com.simplemobiletools.musicplayer.extensions.*
1818
import com.simplemobiletools.musicplayer.helpers.TAB_ALBUMS
@@ -23,6 +23,7 @@ import com.simplemobiletools.musicplayer.helpers.TAB_PLAYLISTS
2323
import com.simplemobiletools.musicplayer.helpers.TAB_TRACKS
2424
import com.simplemobiletools.musicplayer.models.QueueItem
2525
import com.simplemobiletools.musicplayer.models.toMediaItems
26+
import java.util.concurrent.Executors
2627

2728
private const val STATE_CREATED = 1
2829
private const val STATE_INITIALIZING = 2
@@ -42,6 +43,9 @@ private const val SMP_GENRES_ROOT_ID = "__GENRES__"
4243
*/
4344
@UnstableApi
4445
internal class MediaItemProvider(private val context: Context) {
46+
private val executor by lazy {
47+
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor())
48+
}
4549

4650
inner class MediaItemNode(val item: MediaItem) {
4751
private val children: MutableList<MediaItem> = ArrayList()
@@ -89,7 +93,16 @@ internal class MediaItemProvider(private val context: Context) {
8993
}
9094
}
9195

92-
operator fun get(mediaId: String) = getNode(mediaId)?.item
96+
operator fun get(mediaId: String): MediaItem? {
97+
val mediaItem = getNode(mediaId)?.item
98+
if (mediaItem == null) {
99+
// assume it's a track
100+
val mediaStoreId = mediaId.toLongOrNull() ?: return null
101+
return audioHelper.getTrack(mediaStoreId)?.toMediaItem()
102+
}
103+
104+
return mediaItem
105+
}
93106

94107
fun getRootItem() = get(SMP_ROOT_ID)!!
95108

@@ -145,7 +158,7 @@ internal class MediaItemProvider(private val context: Context) {
145158
return
146159
}
147160

148-
ensureBackgroundThread {
161+
executor.execute {
149162
val trackId = current.mediaId.toLong()
150163
val queueItems = mediaItems.mapIndexed { index, mediaItem ->
151164
QueueItem(trackId = mediaItem.mediaId.toLong(), trackOrder = index, isCurrent = false, lastPosition = 0)
@@ -157,8 +170,7 @@ internal class MediaItemProvider(private val context: Context) {
157170

158171
fun reload() {
159172
state = STATE_INITIALIZING
160-
161-
ensureBackgroundThread {
173+
executor.execute {
162174
buildRoot()
163175

164176
try {

app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/AudioOnlyRenderersFactory.kt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ private const val SKIP_SILENCE_THRESHOLD_LEVEL = 16.toShort()
2222
@UnstableApi
2323
class AudioOnlyRenderersFactory(context: Context) : DefaultRenderersFactory(context) {
2424

25-
override fun buildAudioSink(context: Context, enableFloatOutput: Boolean, enableAudioTrackPlaybackParams: Boolean, enableOffload: Boolean): AudioSink? {
25+
override fun buildAudioSink(context: Context, enableFloatOutput: Boolean, enableAudioTrackPlaybackParams: Boolean): AudioSink {
2626
val silenceSkippingAudioProcessor = SilenceSkippingAudioProcessor(
2727
SKIP_SILENCE_MINIMUM_DURATION_US,
2828
DEFAULT_PADDING_SILENCE_US,
@@ -32,13 +32,6 @@ class AudioOnlyRenderersFactory(context: Context) : DefaultRenderersFactory(cont
3232
return DefaultAudioSink.Builder(context)
3333
.setEnableFloatOutput(enableFloatOutput)
3434
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
35-
.setOffloadMode(
36-
if (enableOffload) {
37-
DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED
38-
} else {
39-
DefaultAudioSink.OFFLOAD_MODE_DISABLED
40-
}
41-
)
4235
.setAudioProcessorChain(
4336
DefaultAudioSink.DefaultAudioProcessorChain(
4437
arrayOf(),

app/src/main/kotlin/com/simplemobiletools/musicplayer/playback/player/SimpleMusicPlayer.kt

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,19 @@ import androidx.media3.exoplayer.ExoPlayer
88
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
99
import com.simplemobiletools.musicplayer.extensions.*
1010
import com.simplemobiletools.musicplayer.inlines.indexOfFirstOrNull
11+
import kotlinx.coroutines.*
1112

1213
private const val DEFAULT_SHUFFLE_ORDER_SEED = 42L
1314

1415
@UnstableApi
1516
class SimpleMusicPlayer(private val exoPlayer: ExoPlayer) : ForwardingPlayer(exoPlayer) {
1617

18+
private var seekToNextCount = 0
19+
private var seekToPreviousCount = 0
20+
21+
private val scope = CoroutineScope(Dispatchers.Default)
22+
private var seekJob: Job? = null
23+
1724
/**
1825
* The default implementation only advertises the seek to next and previous item in the case
1926
* 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
5360
override fun seekToNext() {
5461
play()
5562
if (!maybeForceNext()) {
56-
super.seekToNext()
63+
seekToNextCount += 1
64+
seekWithDelay()
5765
}
5866
}
5967

6068
override fun seekToPrevious() {
6169
play()
6270
if (!maybeForcePrevious()) {
63-
super.seekToPrevious()
71+
seekToPreviousCount += 1
72+
seekWithDelay()
6473
}
6574
}
6675

6776
override fun seekToNextMediaItem() {
6877
play()
6978
if (!maybeForceNext()) {
70-
super.seekToNextMediaItem()
79+
seekToNextCount += 1
80+
seekWithDelay()
7181
}
7282
}
7383

7484
override fun seekToPreviousMediaItem() {
7585
play()
7686
if (!maybeForcePrevious()) {
77-
super.seekToPreviousMediaItem()
87+
seekToPreviousCount += 1
88+
seekWithDelay()
7889
}
7990
}
8091

@@ -122,4 +133,36 @@ class SimpleMusicPlayer(private val exoPlayer: ExoPlayer) : ForwardingPlayer(exo
122133
exoPlayer.setShuffleOrder(DefaultShuffleOrder(shuffledIndices.toIntArray(), DEFAULT_SHUFFLE_ORDER_SEED))
123134
}
124135
}
136+
137+
/**
138+
* This is here so the player can quickly seek next/previous without doing too much work.
139+
* It probably won't be needed once https://github.com/androidx/media/issues/81 is resolved.
140+
*/
141+
private fun seekWithDelay() {
142+
seekJob?.cancel()
143+
seekJob = scope.launch {
144+
delay(timeMillis = 400)
145+
if (seekToNextCount > 0 || seekToPreviousCount > 0) {
146+
runOnPlayerThread {
147+
if (currentMediaItem != null) {
148+
if (seekToNextCount > 0) {
149+
seekTo(rotateIndex(currentMediaItemIndex + seekToNextCount), 0)
150+
}
151+
152+
if (seekToPreviousCount > 0) {
153+
seekTo(rotateIndex(currentMediaItemIndex - seekToPreviousCount), 0)
154+
}
155+
156+
seekToNextCount = 0
157+
seekToPreviousCount = 0
158+
}
159+
}
160+
}
161+
}
162+
}
163+
164+
private fun rotateIndex(index: Int): Int {
165+
val count = mediaItemCount
166+
return (index % count + count) % count
167+
}
125168
}

0 commit comments

Comments
 (0)