Merge pull request #615 from naveensingh/performance_improvements

Playback performance improvements
This commit is contained in:
Tibor Kaputa 2023-10-15 21:15:10 +02:00 committed by GitHub
commit 96c5bdfe6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 171 additions and 157 deletions

View file

@ -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
@ -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 ->
@ -179,7 +181,7 @@ inline fun Player.maybePreparePlayer(context: Context, crossinline callback: (su
return@getQueuedTracksLazily
}
addRemainingMediaItems(tracks.toMediaItems(), startIndex)
addRemainingMediaItems(tracks.toMediaItemsFast(), startIndex)
}
}
}

View file

@ -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<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()
}

View file

@ -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()
queueItems = if (currentItems.any { it.mediaId == startItemId }) {
currentItems.toMutableList()
} else {
mediaItemProvider.getDefaultQueue()?.toMutableList() ?: queueItems
}
val startItemIndex = queueItems.indexOfFirst { it.mediaId == startItemId }
super.onSetMediaItems(mediaSession, controller, queueItems, startItemIndex, startPositionMs).get()
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
if (controller.packageName == packageName) {
return super.onSetMediaItems(mediaSession, controller, mediaItems, startIndex, startPositionMs)
}
// 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()
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() {

View file

@ -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<MediaItem> = ArrayList()
@ -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)!!
@ -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)
@ -157,8 +170,7 @@ internal class MediaItemProvider(private val context: Context) {
fun reload() {
state = STATE_INITIALIZING
ensureBackgroundThread {
executor.execute {
buildRoot()
try {

View file

@ -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(),

View file

@ -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
}
}

View file

@ -1,108 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/track_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/tiny_margin"
android:paddingStart="@dimen/medium_margin"
android:orientation="horizontal"
android:paddingStart="@dimen/normal_margin"
android:paddingTop="@dimen/activity_margin"
android:paddingEnd="@dimen/medium_margin"
android:paddingBottom="@dimen/activity_margin">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/small_margin"
app:layout_constraintEnd_toEndOf="parent">
<ImageView
android:id="@+id/track_image"
android:layout_width="@dimen/song_image_size"
android:layout_height="@dimen/song_image_size"
android:visibility="gone" />
<ImageView
android:id="@+id/track_image"
android:layout_width="@dimen/song_image_size"
android:layout_height="@dimen/song_image_size"
android:layout_centerVertical="true"
android:visibility="gone" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/track_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ems="2"
android:gravity="end"
android:paddingEnd="@dimen/small_margin"
android:textSize="@dimen/bigger_text_size"
tools:text="1" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:orientation="vertical"
android:paddingHorizontal="@dimen/normal_margin">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/track_id"
android:id="@+id/track_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/track_image"
android:ems="2"
android:gravity="end"
android:paddingEnd="@dimen/small_margin"
android:ellipsize="end"
android:maxLines="2"
android:textSize="@dimen/bigger_text_size"
tools:text="1" />
tools:text="Track title" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/layout_duration_handle"
android:layout_toEndOf="@id/track_id"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/normal_margin">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/track_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintBottom_toTopOf="@id/track_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Track title" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/track_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:alpha="0.6"
android:ellipsize="end"
android:maxLines="1"
android:textSize="@dimen/normal_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/track_title"
tools:text="Track artist" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_duration_handle"
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/track_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true">
android:alpha="0.6"
android:ellipsize="end"
android:maxLines="1"
android:textSize="@dimen/normal_text_size"
tools:text="Track artist" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/track_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/medium_margin"
android:textSize="@dimen/bigger_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/track_drag_handle"
app:layout_constraintTop_toTopOf="parent"
tools:text="3:45" />
</LinearLayout>
<ImageView
android:id="@+id/track_drag_handle"
android:layout_width="@dimen/song_image_size"
android:layout_height="@dimen/song_image_size"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_drag_handle_vector"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/track_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/medium_margin"
android:textSize="@dimen/bigger_text_size"
tools:text="3:45" />
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/track_drag_handle"
android:layout_width="@dimen/song_image_size"
android:layout_height="@dimen/song_image_size"
android:layout_gravity="center|end"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_drag_handle_vector"
android:visibility="gone" />
</LinearLayout>

View file

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/track_queue_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/tiny_margin"
android:orientation="horizontal"
android:paddingStart="@dimen/normal_margin"
android:paddingTop="@dimen/activity_margin"
android:paddingEnd="@dimen/medium_margin"
@ -23,7 +24,8 @@
android:id="@+id/track_queue_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_gravity="center"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:paddingStart="@dimen/normal_margin"
@ -38,27 +40,19 @@
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/track_queue_duration"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="center"
android:layout_marginEnd="@dimen/medium_margin"
android:gravity="end"
android:textSize="@dimen/bigger_text_size"
app:flow_verticalAlign="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/track_queue_drag_handle"
app:layout_constraintTop_toTopOf="parent"
tools:text="3:45" />
<ImageView
android:id="@+id/track_queue_drag_handle"
android:layout_width="@dimen/song_image_size"
android:layout_height="@dimen/song_image_size"
android:layout_gravity="center"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_drag_handle_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:src="@drawable/ic_drag_handle_vector" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

@ -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"