Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playback performance improvements #615

Merged
merged 6 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -149,7 +149,7 @@ fun Player.prepareUsingTracks(
return
}

val mediaItems = tracks.toMediaItems()
val mediaItems = tracks.toMediaItemsFast()
runOnPlayerThread {
setMediaItems(mediaItems, startIndex, startPositionMs)
playWhenReady = play
Expand All @@ -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 ->
Expand All @@ -179,7 +181,7 @@ inline fun Player.maybePreparePlayer(context: Context, crossinline callback: (su
return@getQueuedTracksLazily
}

addRemainingMediaItems(tracks.toMediaItems(), startIndex)
addRemainingMediaItems(tracks.toMediaItemsFast(), startIndex)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -101,3 +102,9 @@ data class Track(
fun ArrayList<Track>.sortSafely(sorting: Int) = sortSafely(Track.getComparator(sorting))

fun Collection<Track>.toMediaItems() = map { it.toMediaItem() }

fun Collection<Track>.toMediaItemsFast() = map {
MediaItem.Builder()
.setMediaId(it.mediaStoreId.toString())
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,52 +166,44 @@ internal fun PlaybackService.getMediaSessionCallback() = object : MediaLibrarySe
mediaItems: MutableList<MediaItem>,
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<MediaSession.MediaItemsWithStartPosition> {
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<MediaItem>
) = 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<List<MediaItem>> {
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<MediaItem> = ArrayList()
Expand Down Expand Up @@ -89,7 +93,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)!!

Expand Down Expand Up @@ -145,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)
Expand All @@ -157,8 +170,7 @@ internal class MediaItemProvider(private val context: Context) {

fun reload() {
state = STATE_INITIALIZING

ensureBackgroundThread {
executor.execute {
buildRoot()

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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
}
}
Loading