From 4648e56b1549f191ea312b760495e4e3c498c550 Mon Sep 17 00:00:00 2001 From: maia tillie arson crimew Date: Wed, 15 Dec 2021 23:46:40 +0100 Subject: [PATCH] Swich playback to an ExoPlayer based backend --- app/build.gradle | 2 + .../retromusic/repository/SongRepository.kt | 12 +- .../retromusic/service/CrossFadePlayer.kt | 2 +- .../retromusic/service/MultiPlayer.java | 4 +- .../retromusic/service/MusicService.java | 65 ++---- .../lavender/music/extensions/looper.kt | 7 + .../music/player/ExoPlayerPlayback.kt | 209 ++++++++++++++++++ 7 files changed, 246 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/software/lavender/music/extensions/looper.kt create mode 100644 app/src/main/java/software/lavender/music/player/ExoPlayerPlayback.kt diff --git a/app/build.gradle b/app/build.gradle index 4a6e2f99..f102bc10 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,6 +161,8 @@ dependencies { implementation 'me.zhanghai.android.fastscroll:library:1.1.7' implementation 'cat.ereza:customactivityoncrash:2.3.0' debugImplementation 'com.github.amitshekhariitbhu:Android-Debug-Database:1.0.6' + + implementation 'com.google.android.exoplayer:exoplayer-core:2.16.1' } apply from: '../spotless.gradle' diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt index 5ad0eb98..d2a66c1d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt @@ -82,18 +82,18 @@ class RealSongRepository(private val context: Context) : SongRepository { return listOf( Song( 1, - "example", + "river-lake convergence", 1, 2021, 23723478, - "uuh idk what goes here yet, i think the file path or something", + "https://versary.town/river-lake-convergence.mp3", 1639589623, 1, - "example", + "river-lake convergence", 1, - "example", - "example", - "example" + "annieversary", + null, + "annieversary" ) ) } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt index 27eb8676..1659f839 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt @@ -166,7 +166,7 @@ class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletion player.setAudioAttributes( AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build() ) - player.prepare() + player.prepareAsync() } catch (e: Exception) { e.printStackTrace() return false diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.java b/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.java index f8e89f11..0982d30f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.java @@ -23,10 +23,8 @@ import android.net.Uri; import android.os.PowerManager; import android.util.Log; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.service.playback.Playback; import code.name.monkey.retromusic.util.PreferenceUtil; @@ -83,7 +81,7 @@ public class MultiPlayer player.setDataSource(path); } player.setAudioStreamType(AudioManager.STREAM_MUSIC); - player.prepare(); + player.prepareAsync(); } catch (Exception e) { return false; } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java index feed70d8..52054e57 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java @@ -14,27 +14,11 @@ package code.name.monkey.retromusic.service; -import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE; -import static androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT; -import static org.koin.java.KoinJavaComponent.get; -import static code.name.monkey.retromusic.ConstantsKt.ALBUM_ART_ON_LOCK_SCREEN; -import static code.name.monkey.retromusic.ConstantsKt.BLURRED_ALBUM_ART; -import static code.name.monkey.retromusic.ConstantsKt.CLASSIC_NOTIFICATION; -import static code.name.monkey.retromusic.ConstantsKt.COLORED_NOTIFICATION; -import static code.name.monkey.retromusic.ConstantsKt.CROSS_FADE_DURATION; -import static code.name.monkey.retromusic.ConstantsKt.TOGGLE_HEADSET; -import static code.name.monkey.retromusic.service.AudioFader.startFadeAnimator; - import android.app.NotificationManager; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; +import android.content.*; import android.content.pm.ServiceInfo; import android.database.ContentObserver; import android.graphics.Bitmap; @@ -42,16 +26,10 @@ import android.graphics.Point; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.media.audiofx.AudioEffect; -import android.os.Binder; -import android.os.Build; +import android.os.*; import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.PowerManager; import android.os.Process; +import android.os.Build.VERSION_CODES; import android.provider.MediaStore; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaDescriptionCompat; @@ -62,30 +40,14 @@ import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media.MediaBrowserServiceCompat; import androidx.preference.PreferenceManager; - -import com.bumptech.glide.RequestBuilder; -import com.bumptech.glide.request.target.SimpleTarget; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Random; - import code.name.monkey.appthemehelper.util.VersionUtils; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.activities.LockScreenActivity; -import code.name.monkey.retromusic.appwidgets.AppWidgetBig; -import code.name.monkey.retromusic.appwidgets.AppWidgetCard; -import code.name.monkey.retromusic.appwidgets.AppWidgetClassic; -import code.name.monkey.retromusic.appwidgets.AppWidgetMD3; -import code.name.monkey.retromusic.appwidgets.AppWidgetSmall; -import code.name.monkey.retromusic.appwidgets.AppWidgetText; +import code.name.monkey.retromusic.appwidgets.*; import code.name.monkey.retromusic.auto.AutoMediaIDHelper; import code.name.monkey.retromusic.auto.AutoMusicProvider; import code.name.monkey.retromusic.glide.BlurTransformation; @@ -108,7 +70,18 @@ import code.name.monkey.retromusic.util.PreferenceUtil; import code.name.monkey.retromusic.util.RetroUtil; import code.name.monkey.retromusic.volume.AudioVolumeObserver; import code.name.monkey.retromusic.volume.OnAudioVolumeChangedListener; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.request.target.SimpleTarget; import kotlin.Unit; +import software.lavender.music.player.ExoPlayerPlayback; + +import java.util.*; + +import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE; +import static androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT; +import static code.name.monkey.retromusic.ConstantsKt.*; +import static code.name.monkey.retromusic.service.AudioFader.startFadeAnimator; +import static org.koin.java.KoinJavaComponent.get; /** * @author Karim Abou Zeid (kabouzeid), Andrew Neal @@ -162,6 +135,7 @@ public class MusicService extends MediaBrowserServiceCompat public static final int DUCK = 7; public static final int UNDUCK = 8; public static final int RESTORE_QUEUES = 9; + public static final int SHUFFLE_MODE_NONE = 0; public static final int SHUFFLE_MODE_SHUFFLE = 1; public static final int REPEAT_MODE_NONE = 0; @@ -384,7 +358,7 @@ public class MusicService extends MediaBrowserServiceCompat } private static String getTrackUri(@NonNull Song song) { - return MusicUtil.INSTANCE.getSongFileUri(song.getId()).toString(); + return song.getData();//MusicUtil.INSTANCE.getSongFileUri(song.getId()).toString(); } @Override @@ -407,8 +381,9 @@ public class MusicService extends MediaBrowserServiceCompat playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper()); // Set MultiPlayer when crossfade duration is 0 i.e. off + // TODO: do crossfading in exoplayer or remove the feature entirely for now, crossfadeplayer will NOT work right now if (PreferenceUtil.INSTANCE.getCrossFadeDuration() == 0) { - playback = new MultiPlayer(this); + playback = new ExoPlayerPlayback(this); } else { playback = new CrossFadePlayer(this); } @@ -904,7 +879,7 @@ public class MusicService extends MediaBrowserServiceCompat playback.release(); } playback = null; - playback = new MultiPlayer(this); + playback = new ExoPlayerPlayback(this); playback.setCallbacks(this); if (openTrackAndPrepareNextAt(position)) { seek(progress); diff --git a/app/src/main/java/software/lavender/music/extensions/looper.kt b/app/src/main/java/software/lavender/music/extensions/looper.kt new file mode 100644 index 00000000..ea3f3d99 --- /dev/null +++ b/app/src/main/java/software/lavender/music/extensions/looper.kt @@ -0,0 +1,7 @@ +package software.lavender.music.extensions + +import android.os.Build +import android.os.Looper + +inline val Looper.isCurrentThreadCompat: Boolean + get() = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) isCurrentThread else Thread.currentThread() == thread \ No newline at end of file diff --git a/app/src/main/java/software/lavender/music/player/ExoPlayerPlayback.kt b/app/src/main/java/software/lavender/music/player/ExoPlayerPlayback.kt new file mode 100644 index 00000000..e4abe646 --- /dev/null +++ b/app/src/main/java/software/lavender/music/player/ExoPlayerPlayback.kt @@ -0,0 +1,209 @@ +package software.lavender.music.player + +import android.content.Context +import android.content.Intent +import android.media.audiofx.AudioEffect +import android.os.Handler +import android.os.HandlerThread +import android.os.PowerManager +import android.widget.Toast +import code.name.monkey.retromusic.service.playback.Playback +import com.google.android.exoplayer2.ExoPlayer +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.PlaybackException +import com.google.android.exoplayer2.Player +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.runBlocking +import software.lavender.music.extensions.isCurrentThreadCompat + +// TODO: implement audio offloading, see: https://exoplayer.dev/battery-consumption.html +class ExoPlayerPlayback(private val context: Context) : Playback, Player.Listener { + private val handler = Handler(HandlerThread("ExoPlayerHandler").apply { start() }.looper) + private val dispatcher = handler.asCoroutineDispatcher("ExoPlayerDispatcher") + private val player = + ExoPlayer.Builder(context).setWakeMode(PowerManager.PARTIAL_WAKE_LOCK).setLooper(handler.looper).build() + + private var initialized: Boolean = false + override val isInitialized: Boolean + get() = initialized + private var playing = false + override val isPlaying: Boolean + get() = isInitialized && ensurePlayerThread { player.isPlaying } + private var sessionId = -1 + override val audioSessionId: Int + get() = ensurePlayerThread { player.audioSessionId } + + private var callbacks: Playback.PlaybackCallbacks? = null + private var hasNext = false + + override fun setDataSource(path: String): Boolean { + Toast.makeText( + context, + path, + Toast.LENGTH_SHORT + ).show() + val success = ensurePlayerThread { + initialized = false + try { + player.stop() + player.clearMediaItems() + player.setMediaItem(MediaItem.fromUri(path)) + player.prepare() + true + } catch (e: Exception) { + Toast.makeText( + context, + e.message, + Toast.LENGTH_SHORT + ).show() + e.printStackTrace() + initialized = false + false + } + } + if (!success) { + return initialized + } + // TODO: listeners? + ensurePlayerThread { + player.addListener(this) + } + val intent = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION) + intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId) + intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) + intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) + context.sendBroadcast(intent) + initialized = true + return initialized + } + + override fun setNextDataSource(path: String?) = ensurePlayerThread { + if (path == null) { + return@ensurePlayerThread + } + player.addMediaItem(MediaItem.fromUri(path)) + hasNext = true + + // TODO: i think we need to do more stuff here + // TODO: gapless? + } + + override fun setCallbacks(callbacks: Playback.PlaybackCallbacks) { + this.callbacks = callbacks + } + + override fun start() = ensurePlayerThread { + try { + player.play() + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + override fun stop() = ensurePlayerThread { + player.stop() + player.clearMediaItems() + initialized = false + } + + override fun release() = ensurePlayerThread { + player.stop() + player.clearMediaItems() + initialized = false + player.release() + } + + override fun pause() = ensurePlayerThread { + try { + player.pause() + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + override fun duration() = if (!initialized) -1 else ensurePlayerThread { + try { + player.duration.toInt() + } catch (e: Exception) { + e.printStackTrace() + -1 + } + } + + override fun position() = if (!initialized) -1 else ensurePlayerThread { + try { + player.currentPosition.toInt() + } catch (e: Exception) { + e.printStackTrace() + -1 + } + } + + // TODO: do seeking via + override fun seek(whereto: Int) = ensurePlayerThread { + try { + player.seekTo(whereto.toLong()) + player.currentPosition.toInt() + } catch (e: Exception) { + e.printStackTrace() + -1 + } + } + + override fun setVolume(vol: Float) = ensurePlayerThread { + try { + player.volume = vol + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + override fun setAudioSessionId(sessionId: Int) = ensurePlayerThread { + try { + player.audioSessionId = sessionId + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + override fun setCrossFadeDuration(duration: Int) { + // TODO: can we do crossfading in exoplayer directly?? + } + + override fun onPlayerError(error: PlaybackException) { + Toast.makeText( + context, + error.message, + Toast.LENGTH_SHORT + ).show() + } + + // TODO: tbh with first class playlist support in exoplayer we should probably get rid of the weird track switching behaviour asap + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + if (hasNext) { + hasNext = false + callbacks?.onTrackWentToNext() + } else { + callbacks?.onTrackEnded() + } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + playing = isPlaying + } + + override fun onAudioSessionIdChanged(audioSessionId: Int) { + sessionId = audioSessionId + } + + private fun ensurePlayerThread(block: () -> T): T = + if (player.playbackLooper.isCurrentThreadCompat) block() else runBlocking(dispatcher) { block() } +} \ No newline at end of file