Skip to content

Commit

Permalink
Totally change how player works in backend, hopefully it will be smoo…
Browse files Browse the repository at this point in the history
…ther to use now (#15)
  • Loading branch information
brahmkshatriya authored Jul 20, 2024
1 parent c070c5c commit a1857ec
Show file tree
Hide file tree
Showing 48 changed files with 1,524 additions and 1,135 deletions.
2 changes: 0 additions & 2 deletions app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,11 @@ class MainActivity : AppCompatActivity() {
uiViewModel.fromNotification.value = true
return
}
println("Intent: $data")
val uri = data
if (uri != null) {
fun createSnack(id: Int) {
val snackbar by viewModels<SnackBar>()
val message = getString(id)
println("bruh : $message")
snackbar.create(SnackBar.Message(message))
}

Expand Down
90 changes: 52 additions & 38 deletions app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,50 @@ import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import dagger.hilt.android.AndroidEntryPoint
import dev.brahmkshatriya.echo.common.clients.LibraryClient
import dev.brahmkshatriya.echo.playback.Current
import dev.brahmkshatriya.echo.playback.PlayerBitmapLoader
import dev.brahmkshatriya.echo.playback.PlayerListener
import dev.brahmkshatriya.echo.playback.PlayerEventListener
import dev.brahmkshatriya.echo.playback.PlayerSessionCallback
import dev.brahmkshatriya.echo.playback.Queue
import dev.brahmkshatriya.echo.playback.Radio
import dev.brahmkshatriya.echo.playback.RenderersFactory
import dev.brahmkshatriya.echo.playback.ResumptionUtils
import dev.brahmkshatriya.echo.playback.StreamableDataSource
import dev.brahmkshatriya.echo.playback.TrackResolver
import dev.brahmkshatriya.echo.playback.getLikeButton
import dev.brahmkshatriya.echo.playback.getRepeatButton
import dev.brahmkshatriya.echo.playback.TrackingListener
import dev.brahmkshatriya.echo.plugger.MusicExtension
import dev.brahmkshatriya.echo.plugger.getExtension
import dev.brahmkshatriya.echo.plugger.TrackerExtension
import dev.brahmkshatriya.echo.ui.settings.AudioFragment.AudioPreference.Companion.CLOSE_PLAYER
import dev.brahmkshatriya.echo.ui.settings.AudioFragment.AudioPreference.Companion.KEEP_QUEUE
import dev.brahmkshatriya.echo.ui.settings.AudioFragment.AudioPreference.Companion.SKIP_SILENCE
import dev.brahmkshatriya.echo.viewmodels.SnackBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
@OptIn(UnstableApi::class)
class PlaybackService : MediaLibraryService() {
@Inject
lateinit var extensionFlow: MutableStateFlow<MusicExtension?>
lateinit var extFlow: MutableStateFlow<MusicExtension?>

@Inject
lateinit var extensionList: MutableStateFlow<List<MusicExtension>?>
lateinit var extListFlow: MutableStateFlow<List<MusicExtension>?>

@Inject
lateinit var global: Queue
lateinit var trackerList: MutableStateFlow<List<TrackerExtension>?>

@Inject
lateinit var throwFlow: MutableSharedFlow<Throwable>

@Inject
lateinit var messageFlow: MutableSharedFlow<SnackBar.Message>

@Inject
lateinit var stateFlow: MutableStateFlow<Radio.State>

@Inject
lateinit var settings: SharedPreferences
Expand All @@ -55,16 +68,17 @@ class PlaybackService : MediaLibraryService() {
lateinit var cache: SimpleCache

@Inject
lateinit var listener: PlayerListener
lateinit var current: MutableStateFlow<Current?>

private var mediaLibrarySession: MediaLibrarySession? = null
private val scope = CoroutineScope(Dispatchers.Main)

override fun onCreate() {
super.onCreate()

val player = createExoplayer()
listener.setup(player, scope)
val exoPlayer = createExoplayer()

exoPlayer.prepare()

val intent = Intent(this, MainActivity::class.java)
.putExtra("fromNotification", true)
Expand All @@ -73,12 +87,12 @@ class PlaybackService : MediaLibraryService() {
.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)

val callback = PlayerSessionCallback(
this, scope, global, extensionList, extensionFlow
this, settings, scope, extListFlow, extFlow, throwFlow, messageFlow, stateFlow
)

val mediaLibrarySession = MediaLibrarySession.Builder(this, player, callback)
val session = MediaLibrarySession.Builder(this, exoPlayer, callback)
.setSessionActivity(pendingIntent)
.setBitmapLoader(PlayerBitmapLoader(this, global, scope))
.setBitmapLoader(PlayerBitmapLoader(this, exoPlayer, scope))
.build()

val notificationProvider =
Expand All @@ -88,13 +102,29 @@ class PlaybackService : MediaLibraryService() {
notificationProvider.setSmallIcon(R.drawable.ic_mono)
setMediaNotificationProvider(notificationProvider)

global.listenToChanges(scope, mediaLibrarySession, ::updateLayout)
exoPlayer.addListener(PlayerEventListener(this, session, current, extListFlow))
exoPlayer.addListener(
Radio(session,this, settings, scope, extListFlow, throwFlow, messageFlow, stateFlow)
)
exoPlayer.addListener(
TrackingListener(session, scope, extListFlow, trackerList, throwFlow)
)
settings.registerOnSharedPreferenceChangeListener { prefs, key ->
when (key) {
SKIP_SILENCE -> player.skipSilenceEnabled = prefs.getBoolean(key, true)
SKIP_SILENCE -> exoPlayer.skipSilenceEnabled = prefs.getBoolean(key, true)
}
}
this.mediaLibrarySession = mediaLibrarySession

val keepQueue = settings.getBoolean(KEEP_QUEUE, true)
if (keepQueue) scope.launch {
extListFlow.first { it != null }
ResumptionUtils.recoverPlaylist(this@PlaybackService).apply {
exoPlayer.setMediaItems(mediaItems, startIndex, startPositionMs)
exoPlayer.prepare()
}
}

this.mediaLibrarySession = session
}


Expand All @@ -108,12 +138,10 @@ class PlaybackService : MediaLibraryService() {
val cacheFactory = CacheDataSource
.Factory().setCache(cache)
.setUpstreamDataSourceFactory(streamableFactory)
val dataSourceFactory =
ResolvingDataSource.Factory(
cacheFactory,
TrackResolver(this, global, extensionList, settings)
)

val trackResolver = TrackResolver(this, extListFlow, settings)

val dataSourceFactory = ResolvingDataSource.Factory(cacheFactory, trackResolver)
val factory = DefaultMediaSourceFactory(this)
.setDataSourceFactory(dataSourceFactory)

Expand All @@ -124,24 +152,10 @@ class PlaybackService : MediaLibraryService() {
.setSkipSilenceEnabled(settings.getBoolean(SKIP_SILENCE, true))
.setAudioAttributes(audioAttributes, true)
.build()
}

private fun updateLayout() {
val context = this@PlaybackService
val track = global.current ?: return
val mediaLibrarySession = mediaLibrarySession ?: return
val player = mediaLibrarySession.player
val supportsLike = extensionList.getExtension(track.clientId)?.client is LibraryClient

val commandButtons = listOfNotNull(
getRepeatButton(context, player.repeatMode),
getLikeButton(context, track.liked).takeIf { supportsLike }
)
mediaLibrarySession.setCustomLayout(commandButtons)
.also { trackResolver.player = it }
}

override fun onDestroy() {
scope.launch { global.clearQueue(false) }
mediaLibrarySession?.run {
player.release()
release()
Expand Down
40 changes: 14 additions & 26 deletions app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dev.brahmkshatriya.echo.EchoDatabase
import dev.brahmkshatriya.echo.db.models.UserEntity
import dev.brahmkshatriya.echo.playback.PlayerListener
import dev.brahmkshatriya.echo.playback.Queue
import dev.brahmkshatriya.echo.plugger.MusicExtension
import dev.brahmkshatriya.echo.plugger.TrackerExtension
import dev.brahmkshatriya.echo.playback.Current
import dev.brahmkshatriya.echo.playback.Radio
import dev.brahmkshatriya.echo.viewmodels.SnackBar
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -29,10 +27,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
class AppModule {

@Provides
@Singleton
fun provideGlobalQueue() = Queue()

@Provides
@Singleton
fun provideThrowableFlow() = MutableSharedFlow<Throwable>()
Expand All @@ -46,6 +40,16 @@ class AppModule {
fun provideSettingsPreferences(application: Application): SharedPreferences =
application.getSharedPreferences(application.packageName, Context.MODE_PRIVATE)

@Provides
@Singleton
fun provideDatabase(application: Application) = Room.databaseBuilder(
application, EchoDatabase::class.java, "echo-database"
).fallbackToDestructiveMigration().build()

@Provides
@Singleton
fun provideLoginUserFlow() = MutableSharedFlow<UserEntity?>()

@Provides
@Singleton
@UnstableApi
Expand All @@ -60,25 +64,9 @@ class AppModule {

@Provides
@Singleton
fun provideDatabase(application: Application) = Room.databaseBuilder(
application, EchoDatabase::class.java, "echo-database"
).fallbackToDestructiveMigration().build()

@Provides
@Singleton
fun provideLoginUserFlow() = MutableSharedFlow<UserEntity?>()
fun currentMediaItemFlow() = MutableStateFlow<Current?>(null)

@Provides
@Singleton
fun providePlayerListener(
application: Application,
extensionList: MutableStateFlow<List<MusicExtension>?>,
trackerListFlow: MutableStateFlow<List<TrackerExtension>?>,
global: Queue,
settings: SharedPreferences,
throwableFlow: MutableSharedFlow<Throwable>,
messageFlow: MutableSharedFlow<SnackBar.Message>,
) = PlayerListener(
application, extensionList, trackerListFlow, global, settings, throwableFlow, messageFlow
)
fun provideExtensionListFlow() = MutableStateFlow<Radio.State>(Radio.State.Empty)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem
import dev.brahmkshatriya.echo.common.models.StreamableAudio
import dev.brahmkshatriya.echo.common.models.Track
import dev.brahmkshatriya.echo.db.models.DownloadEntity
import dev.brahmkshatriya.echo.playback.TrackResolver
import dev.brahmkshatriya.echo.plugger.MusicExtension
import dev.brahmkshatriya.echo.plugger.getExtension
import dev.brahmkshatriya.echo.ui.settings.AudioFragment.AudioPreference.Companion.selectStream
import dev.brahmkshatriya.echo.utils.getFromCache
import dev.brahmkshatriya.echo.utils.saveToCache
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -80,7 +80,7 @@ class Downloader(
val track = loaded.copy(album = album)

val settings = getSharedPreferences(packageName, Context.MODE_PRIVATE)
val stream = TrackResolver.selectStream(settings, track.audioStreamables)
val stream = selectStream(settings, track.audioStreamables)
?: throw Exception("No Stream Found")
val audio = client.getStreamableAudio(stream)
val folder = "Echo${parent?.title?.let { "/$it" } ?: ""}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,6 @@ object MediaStoreUtils {

val playlistsFinal = playlists.map {
it.first.also { playlist ->
println("Playlist : ${playlist.title} with ${it.second.size} songs")
playlist.songList.addAll(it.second.map { value ->
idMap!![value]
?: if (DEBUG_MISSING_SONG) throw NullPointerException(
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/playback/Current.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.brahmkshatriya.echo.playback

import androidx.media3.common.MediaItem

data class Current(
val index: Int,
val mediaItem: MediaItem,
val isLoaded: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package dev.brahmkshatriya.echo.playback

import android.content.SharedPreferences
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.ThumbRating
import dev.brahmkshatriya.echo.common.models.EchoMediaItem
import dev.brahmkshatriya.echo.common.models.Track
import dev.brahmkshatriya.echo.ui.settings.AudioFragment.AudioPreference.Companion.selectStreamIndex
import dev.brahmkshatriya.echo.utils.getParcel

object MediaItemUtils {

fun build(
settings: SharedPreferences?,
track: Track,
clientId: String,
context: EchoMediaItem?,
): MediaItem {
val item = MediaItem.Builder()
item.setUri(track.id)
val metadata = track.toMetaData(settings, clientId, context)
item.setMediaMetadata(metadata)
item.setMediaId(track.id)
return item.build()
}

fun build(settings: SharedPreferences?, mediaItem: MediaItem, track: Track): MediaItem =
with(mediaItem) {
val item = buildUpon()
val metadata = track.toMetaData(settings, clientId, context, true)
item.setMediaMetadata(metadata)
return item.build()
}

private fun Track.toMetaData(
settings: SharedPreferences?,
clientId: String,
context: EchoMediaItem?,
loaded: Boolean = false,
audioStreamIndex: Int? = null
) = MediaMetadata.Builder()
.setTitle(title)
.setArtist(artists.joinToString(", ") { it.name })
.setArtworkUri(id.toUri())
.setUserRating(ThumbRating(liked))
.setIsPlayable(true)
.setIsBrowsable(false)
.setExtras(
bundleOf(
"track" to this,
"clientId" to clientId,
"context" to context,
"loaded" to loaded,
"audioStream" to selectStream(settings, loaded, audioStreamIndex)
)
)
.setIsPlayable(loaded)
.build()

private fun Track.selectStream(
settings: SharedPreferences?,
loaded: Boolean,
audioStreamIndex: Int?
): Int? {
if (!loaded) return null
if (settings == null) return audioStreamIndex
return audioStreamIndex ?: selectStreamIndex(settings, audioStreamables)
}

val MediaMetadata.isLoaded get() = extras?.getBoolean("loaded") ?: false
val MediaMetadata.track get() = requireNotNull(extras?.getParcel<Track>("track"))
val MediaMetadata.clientId get() = requireNotNull(extras?.getString("clientId"))
val MediaMetadata.context get() = extras?.getParcel<EchoMediaItem?>("context")
val MediaMetadata.audioStreamIndex get() = extras?.getInt("audioStream") ?: -1
val MediaMetadata.isLiked get() = (userRating as? ThumbRating)?.isThumbsUp == true

val MediaItem.track get() = mediaMetadata.track
val MediaItem.clientId get() = mediaMetadata.clientId
val MediaItem.context get() = mediaMetadata.context
val MediaItem.isLoaded get() = mediaMetadata.isLoaded
val MediaItem.audioStreamIndex get() = mediaMetadata.audioStreamIndex
val MediaItem.isLiked get() = mediaMetadata.isLiked

}
Loading

0 comments on commit a1857ec

Please sign in to comment.