Skip to content

Commit

Permalink
Rewrite player logic
Browse files Browse the repository at this point in the history
  • Loading branch information
siper committed Feb 8, 2025
1 parent e30f045 commit f01ba2b
Show file tree
Hide file tree
Showing 31 changed files with 622 additions and 605 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ dependencies {
implementation project(":core:api")
implementation project(":core:ui")
implementation project(":core:room")
implementation project(":core:utils")
implementation project(":core:properties")
implementation project(":shared:player")
implementation project(":shared:favorites")
Expand Down Expand Up @@ -108,4 +109,5 @@ dependencies {
implementation(libs.material)
implementation(libs.coil.compose)
implementation(libs.timber)
implementation(libs.kotlin.coroutines.guava)
}
24 changes: 24 additions & 0 deletions app/src/main/java/ru/stersh/youamp/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,40 @@ import coil.ImageLoader
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import ru.stersh.youamp.core.api.provider.ApiProvider
import ru.stersh.youamp.player.ApiSonicPlayQueueSyncer
import ru.stersh.youamp.player.PlayQueueSyncActivityCallback
import ru.stersh.youamp.player.ProgressSyncActivityCallback
import ru.stersh.youamp.shared.player.progress.PlayerProgressStore
import ru.stersh.youamp.shared.player.provider.PlayerProvider
import timber.log.Timber

class App : Application() {
private val apiProvider: ApiProvider by inject()
private val playerProgressStore: PlayerProgressStore by inject()
private val playerProvider: PlayerProvider by inject()
private val playQueueSyncer: ApiSonicPlayQueueSyncer by inject()

override fun onCreate() {
super.onCreate()
setupDi(this)
setupCoil()
setupTimber()
setupActivityCallbacks()
}

private fun setupActivityCallbacks() {
registerActivityLifecycleCallbacks(
ProgressSyncActivityCallback(
playerProgressStore = playerProgressStore,
playerProvider = playerProvider,
apiProvider = apiProvider
)
)
registerActivityLifecycleCallbacks(
PlayQueueSyncActivityCallback(playQueueSyncer)
)
}

private fun setupTimber() {
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/ru/stersh/youamp/Di.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import ru.stersh.youamp.main.data.ServerExistRepositoryImpl
import ru.stersh.youamp.main.domain.AvatarUrlRepository
import ru.stersh.youamp.main.domain.ServerExistRepository
import ru.stersh.youamp.main.ui.MainViewModel
import ru.stersh.youamp.player.ApiSonicPlayQueueSyncer
import ru.stersh.youamp.player.PlayerProviderImpl
import ru.stersh.youamp.shared.player.playerSharedModule
import ru.stersh.youamp.shared.player.provider.PlayerProvider
import ru.stresh.youamp.core.properties.app.AppProperties
import ru.stresh.youamp.core.propertiesModule
import ru.stresh.youamp.feature.about.aboutModule
Expand Down Expand Up @@ -96,6 +99,8 @@ private val impl = module {
)
}
single<ApiProvider> { ApiProviderImpl(get()) }
single<PlayerProvider> { PlayerProviderImpl(get()) }
single { ApiSonicPlayQueueSyncer(get(), get()) }
}

private val main = module {
Expand Down
109 changes: 109 additions & 0 deletions app/src/main/java/ru/stersh/youamp/player/ApiSonicPlayQueueSyncer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package ru.stersh.youamp.player

import androidx.media3.common.Player
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import ru.stersh.youamp.core.api.provider.ApiProvider
import ru.stersh.youamp.shared.player.provider.PlayerProvider
import ru.stersh.youamp.shared.player.utils.PlayerDispatcher
import ru.stersh.youamp.shared.player.utils.mediaItems
import ru.stersh.youamp.shared.player.utils.toMediaItem
import timber.log.Timber

internal class ApiSonicPlayQueueSyncer(
private val playerProvider: PlayerProvider,
private val apiProvider: ApiProvider
) {

suspend fun syncQueue() {
apiProvider
.flowApiId()
.filterNotNull()
.flatMapLatest {
flow<Nothing> {
val player = playerProvider.get()
loadPlayQueue(player)
var playQueue = getPlayQueue(player)
while (true) {
val currentPlayQueue = getPlayQueue(player)
if (currentPlayQueue != playQueue) {
syncPlayQueue(player)
playQueue = currentPlayQueue
}
delay(SYNC_QUEUE_PERIOD)
}
}
}
.collect()
}

private suspend fun loadPlayQueue(player: Player) {
val playQueue = withContext(Dispatchers.IO) {
runCatching {
apiProvider
.getApi()
.getPlayQueue()
.playQueue
}
.onFailure { Timber.w(it) }
.getOrNull()
} ?: return

val items = playQueue.entry.map { it.toMediaItem(apiProvider) }
val currentIndex = items.indexOfFirst { it.mediaId == playQueue.current }
val position = playQueue.position

withContext(PlayerDispatcher) {
if (currentIndex != -1) {
player.setMediaItems(items, currentIndex, position ?: 0)
} else {
player.setMediaItems(items)
}
player.prepare()
}
}

private suspend fun getPlayQueue(player: Player) = withContext(PlayerDispatcher) {
val items = player.mediaItems.map { it.mediaId }
val current = player.currentMediaItem?.mediaId
val position = player.currentPosition
return@withContext PlayQueue(
items = items,
current = current,
position = position
)
}

private suspend fun syncPlayQueue(player: Player) = withContext(PlayerDispatcher) {
if (!player.isPlaying) {
return@withContext
}

val items = player.mediaItems.map { it.mediaId }
val current = player.currentMediaItem?.mediaId
val position = player.currentPosition

withContext(Dispatchers.IO) {
runCatching {
apiProvider
.getApi()
.savePlayQueue(items, current, position)
}.onFailure { Timber.w(it) }
}
}

private data class PlayQueue(
val items: List<String>,
val current: String?,
val position: Long
)

companion object {
private const val SYNC_QUEUE_PERIOD = 5000L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ru.stersh.youamp.player

import android.app.Activity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import ru.stersh.youamp.core.utils.EmptyActivityLifecycleCallback
import ru.stersh.youamp.main.ui.MainActivity

internal class PlayQueueSyncActivityCallback(
private val apiSonicPlayQueueSyncer: ApiSonicPlayQueueSyncer
) : EmptyActivityLifecycleCallback() {

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity !is MainActivity) {
return
}
activity.lifecycleScope.launch {
apiSonicPlayQueueSyncer.syncQueue()
}
}
}
23 changes: 23 additions & 0 deletions app/src/main/java/ru/stersh/youamp/player/PlayerProviderImpl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ru.stersh.youamp.player

import android.content.ComponentName
import android.content.Context
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.withContext
import ru.stersh.youamp.shared.player.android.MusicService
import ru.stersh.youamp.shared.player.provider.PlayerProvider
import ru.stersh.youamp.shared.player.utils.PlayerDispatcher

internal class PlayerProviderImpl(private val context: Context) : PlayerProvider {

override suspend fun get(): Player = withContext(PlayerDispatcher) {
val sessionToken = SessionToken(context, ComponentName(context, MusicService::class.java))
return@withContext MediaController
.Builder(context, sessionToken)
.buildAsync()
.await()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package ru.stersh.youamp.player

import android.app.Activity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ru.stersh.youamp.core.api.provider.ApiProvider
import ru.stersh.youamp.core.utils.EmptyActivityLifecycleCallback
import ru.stersh.youamp.main.ui.MainActivity
import ru.stersh.youamp.shared.player.progress.PlayerProgressStore
import ru.stersh.youamp.shared.player.provider.PlayerProvider
import ru.stersh.youamp.shared.player.utils.PlayerDispatcher

internal class ProgressSyncActivityCallback(
private val playerProgressStore: PlayerProgressStore,
private val playerProvider: PlayerProvider,
private val apiProvider: ApiProvider
) : EmptyActivityLifecycleCallback() {
private var scrobbleSender: ScrobbleSender? = null

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity !is MainActivity) {
return
}
playerProgressStore
.playerProgress()
.filterNotNull()
.onEach { progress ->
val player = playerProvider.get()
val currentItemId = withContext(PlayerDispatcher) { player.currentMediaItem?.mediaId } ?: return@onEach
if (currentItemId != scrobbleSender?.id) {
scrobbleSender = ScrobbleSender(currentItemId, apiProvider)
}
val sender = scrobbleSender ?: return@onEach
if (progress.currentTimeMs > SEND_SCROBBLE_EVENT_TIME && !sender.scrobbleSent) {
activity.lifecycleScope.launch {
sender.trySendScrobble()
}
}
if ((progress.currentTimeMs + SEND_SCROBBLE_EVENT_TIME) >= progress.totalTimeMs && !sender.submissionSent) {
activity.lifecycleScope.launch {
sender.trySendSubmission()
}
}
}
.launchIn(activity.lifecycleScope)
}

companion object {
private const val SEND_SCROBBLE_EVENT_TIME = 5000L
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ru.stersh.youamp.shared.player.progress
package ru.stersh.youamp.player

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
Expand All @@ -9,8 +9,10 @@ internal class ScrobbleSender(
val id: String,
private val apiProvider: ApiProvider,
) {
private var scrobbleSent = false
private var submissionSent = false
var scrobbleSent = false
private set
var submissionSent = false
private set

private val sendMutex = Mutex()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ru.stersh.youamp.core.utils

import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle

abstract class EmptyActivityLifecycleCallback : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}

override fun onActivityStarted(activity: Activity) {}

override fun onActivityResumed(activity: Activity) {}

override fun onActivityPaused(activity: Activity) {}

override fun onActivityStopped(activity: Activity) {}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}

override fun onActivityDestroyed(activity: Activity) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
Expand Down Expand Up @@ -84,7 +85,11 @@ private fun PlayerQueueScreen(
}
) { padding ->
if (state.progress) {
Box(modifier = Modifier.padding(padding)) {
Box(
modifier = Modifier
.padding(padding)
.fillMaxSize()
) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
} else {
Expand Down
Loading

0 comments on commit f01ba2b

Please sign in to comment.