Swich playback to an ExoPlayer based backend

This commit is contained in:
maia arson crimew 2021-12-15 23:46:40 +01:00
parent 13ea8dd04d
commit 4648e56b15
7 changed files with 246 additions and 55 deletions

View file

@ -161,6 +161,8 @@ dependencies {
implementation 'me.zhanghai.android.fastscroll:library:1.1.7' implementation 'me.zhanghai.android.fastscroll:library:1.1.7'
implementation 'cat.ereza:customactivityoncrash:2.3.0' implementation 'cat.ereza:customactivityoncrash:2.3.0'
debugImplementation 'com.github.amitshekhariitbhu:Android-Debug-Database:1.0.6' debugImplementation 'com.github.amitshekhariitbhu:Android-Debug-Database:1.0.6'
implementation 'com.google.android.exoplayer:exoplayer-core:2.16.1'
} }
apply from: '../spotless.gradle' apply from: '../spotless.gradle'

View file

@ -82,18 +82,18 @@ class RealSongRepository(private val context: Context) : SongRepository {
return listOf( return listOf(
Song( Song(
1, 1,
"example", "river-lake convergence",
1, 1,
2021, 2021,
23723478, 23723478,
"uuh idk what goes here yet, i think the file path or something", "https://versary.town/river-lake-convergence.mp3",
1639589623, 1639589623,
1, 1,
"example", "river-lake convergence",
1, 1,
"example", "annieversary",
"example", null,
"example" "annieversary"
) )
) )
} }

View file

@ -166,7 +166,7 @@ class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletion
player.setAudioAttributes( player.setAudioAttributes(
AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build() AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build()
) )
player.prepare() player.prepareAsync()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return false return false

View file

@ -23,10 +23,8 @@ import android.net.Uri;
import android.os.PowerManager; import android.os.PowerManager;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.R;
import code.name.monkey.retromusic.service.playback.Playback; import code.name.monkey.retromusic.service.playback.Playback;
import code.name.monkey.retromusic.util.PreferenceUtil; import code.name.monkey.retromusic.util.PreferenceUtil;
@ -83,7 +81,7 @@ public class MultiPlayer
player.setDataSource(path); player.setDataSource(path);
} }
player.setAudioStreamType(AudioManager.STREAM_MUSIC); player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.prepare(); player.prepareAsync();
} catch (Exception e) { } catch (Exception e) {
return false; return false;
} }

View file

@ -14,27 +14,11 @@
package code.name.monkey.retromusic.service; 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.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetManager;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver; import android.content.*;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@ -42,16 +26,10 @@ import android.graphics.Point;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.audiofx.AudioEffect; import android.media.audiofx.AudioEffect;
import android.os.Binder; import android.os.*;
import android.os.Build;
import android.os.Build.VERSION; 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.Process;
import android.os.Build.VERSION_CODES;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaDescriptionCompat;
@ -62,30 +40,14 @@ import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media.MediaBrowserServiceCompat; import androidx.media.MediaBrowserServiceCompat;
import androidx.preference.PreferenceManager; 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.appthemehelper.util.VersionUtils;
import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.R;
import code.name.monkey.retromusic.activities.LockScreenActivity; import code.name.monkey.retromusic.activities.LockScreenActivity;
import code.name.monkey.retromusic.appwidgets.AppWidgetBig; import code.name.monkey.retromusic.appwidgets.*;
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.auto.AutoMediaIDHelper; import code.name.monkey.retromusic.auto.AutoMediaIDHelper;
import code.name.monkey.retromusic.auto.AutoMusicProvider; import code.name.monkey.retromusic.auto.AutoMusicProvider;
import code.name.monkey.retromusic.glide.BlurTransformation; 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.util.RetroUtil;
import code.name.monkey.retromusic.volume.AudioVolumeObserver; import code.name.monkey.retromusic.volume.AudioVolumeObserver;
import code.name.monkey.retromusic.volume.OnAudioVolumeChangedListener; import code.name.monkey.retromusic.volume.OnAudioVolumeChangedListener;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.request.target.SimpleTarget;
import kotlin.Unit; 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 * @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 DUCK = 7;
public static final int UNDUCK = 8; public static final int UNDUCK = 8;
public static final int RESTORE_QUEUES = 9; public static final int RESTORE_QUEUES = 9;
public static final int SHUFFLE_MODE_NONE = 0; public static final int SHUFFLE_MODE_NONE = 0;
public static final int SHUFFLE_MODE_SHUFFLE = 1; public static final int SHUFFLE_MODE_SHUFFLE = 1;
public static final int REPEAT_MODE_NONE = 0; public static final int REPEAT_MODE_NONE = 0;
@ -384,7 +358,7 @@ public class MusicService extends MediaBrowserServiceCompat
} }
private static String getTrackUri(@NonNull Song song) { private static String getTrackUri(@NonNull Song song) {
return MusicUtil.INSTANCE.getSongFileUri(song.getId()).toString(); return song.getData();//MusicUtil.INSTANCE.getSongFileUri(song.getId()).toString();
} }
@Override @Override
@ -407,8 +381,9 @@ public class MusicService extends MediaBrowserServiceCompat
playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper()); playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper());
// Set MultiPlayer when crossfade duration is 0 i.e. off // 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) { if (PreferenceUtil.INSTANCE.getCrossFadeDuration() == 0) {
playback = new MultiPlayer(this); playback = new ExoPlayerPlayback(this);
} else { } else {
playback = new CrossFadePlayer(this); playback = new CrossFadePlayer(this);
} }
@ -904,7 +879,7 @@ public class MusicService extends MediaBrowserServiceCompat
playback.release(); playback.release();
} }
playback = null; playback = null;
playback = new MultiPlayer(this); playback = new ExoPlayerPlayback(this);
playback.setCallbacks(this); playback.setCallbacks(this);
if (openTrackAndPrepareNextAt(position)) { if (openTrackAndPrepareNextAt(position)) {
seek(progress); seek(progress);

View file

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

View file

@ -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 <T> ensurePlayerThread(block: () -> T): T =
if (player.playbackLooper.isCurrentThreadCompat) block() else runBlocking(dispatcher) { block() }
}