mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Fix theme song playing over video (#1442)
## Description There was an issue if when opening a page with theme music and quickly changing pages, usually playback, the theme music might play on the new page. This is because the call to stop the theme music could potentially happen _before_ the song was loaded or had started playing. This PR fixes this race condition by tracking the job that's fetching the theme song and by adding a mutex to ensure the play function completes before the stop function can begin. ### Related issues Fixes #938 ### Testing Emulator w/ fake delay to increase the odds of the issue occurring ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
cb704991f0
commit
086badf2e2
8 changed files with 105 additions and 97 deletions
|
|
@ -2,17 +2,30 @@ package com.github.damontecres.wholphin.services
|
|||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||
import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.util.profile.Codec
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
|
|
@ -34,7 +47,12 @@ class ThemeSongPlayer
|
|||
@param:ApplicationContext private val context: Context,
|
||||
@param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient,
|
||||
private val api: ApiClient,
|
||||
@param:IoCoroutineScope private val scope: CoroutineScope,
|
||||
private val preferences: DataStore<AppPreferences>,
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
private val state = MutableStateFlow(ThemeSongState())
|
||||
|
||||
private val player: Player by lazy {
|
||||
ExoPlayer
|
||||
.Builder(context)
|
||||
|
|
@ -45,68 +63,86 @@ class ThemeSongPlayer
|
|||
).build()
|
||||
}
|
||||
|
||||
suspend fun playThemeFor(
|
||||
itemId: UUID,
|
||||
volume: ThemeSongVolume,
|
||||
): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (volume == ThemeSongVolume.DISABLED || volume == ThemeSongVolume.UNRECOGNIZED) {
|
||||
return@withContext false
|
||||
}
|
||||
val themeSongs by api.libraryApi.getThemeSongs(itemId)
|
||||
return@withContext themeSongs.items.randomOrNull()?.let { theme ->
|
||||
val url =
|
||||
api.universalAudioApi.getUniversalAudioStreamUrl(
|
||||
theme.id,
|
||||
container =
|
||||
listOf(
|
||||
Codec.Audio.OPUS,
|
||||
Codec.Audio.MP3,
|
||||
Codec.Audio.AAC,
|
||||
Codec.Audio.FLAC,
|
||||
),
|
||||
fun playThemeFor(itemId: UUID) {
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
state.value.job?.join()
|
||||
val volumeLevel =
|
||||
when (
|
||||
preferences.data
|
||||
.first()
|
||||
.interfacePreferences.playThemeSongs
|
||||
) {
|
||||
ThemeSongVolume.UNRECOGNIZED,
|
||||
ThemeSongVolume.DISABLED,
|
||||
-> return@withLock
|
||||
|
||||
ThemeSongVolume.LOWEST -> .05f
|
||||
|
||||
ThemeSongVolume.LOW -> .1f
|
||||
|
||||
ThemeSongVolume.MEDIUM -> .25f
|
||||
|
||||
ThemeSongVolume.HIGH -> .5f
|
||||
|
||||
ThemeSongVolume.HIGHEST -> 75f
|
||||
}
|
||||
val job =
|
||||
scope.launchIO {
|
||||
val themeSongs by api.libraryApi.getThemeSongs(itemId)
|
||||
themeSongs.items.randomOrNull()?.let { theme ->
|
||||
val url =
|
||||
api.universalAudioApi.getUniversalAudioStreamUrl(
|
||||
theme.id,
|
||||
container =
|
||||
listOf(
|
||||
Codec.Audio.OPUS,
|
||||
Codec.Audio.MP3,
|
||||
Codec.Audio.AAC,
|
||||
Codec.Audio.FLAC,
|
||||
),
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
player.apply {
|
||||
stop()
|
||||
volume = volumeLevel
|
||||
setMediaItem(MediaItem.fromUri(url))
|
||||
prepare()
|
||||
play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
state.update {
|
||||
it.copy(
|
||||
itemId = itemId,
|
||||
job = job,
|
||||
)
|
||||
Timber.v("Found theme song for $itemId")
|
||||
withContext(Dispatchers.Main) {
|
||||
play(volume, url)
|
||||
}
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun play(
|
||||
volumeLevel: ThemeSongVolume,
|
||||
url: String,
|
||||
) {
|
||||
val volumeLevel =
|
||||
when (volumeLevel) {
|
||||
ThemeSongVolume.UNRECOGNIZED,
|
||||
ThemeSongVolume.DISABLED,
|
||||
-> return
|
||||
|
||||
ThemeSongVolume.LOWEST -> .05f
|
||||
|
||||
ThemeSongVolume.LOW -> .1f
|
||||
|
||||
ThemeSongVolume.MEDIUM -> .25f
|
||||
|
||||
ThemeSongVolume.HIGH -> .5f
|
||||
|
||||
ThemeSongVolume.HIGHEST -> 75f
|
||||
}
|
||||
player.apply {
|
||||
stop()
|
||||
volume = volumeLevel
|
||||
setMediaItem(MediaItem.fromUri(url))
|
||||
prepare()
|
||||
play()
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (player.isPlaying) {
|
||||
Timber.v("Stopping theme song")
|
||||
player.stop()
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
state.value.job?.cancelAndJoin()
|
||||
withContext(Dispatchers.Main) {
|
||||
Timber.v("Stopping theme song")
|
||||
player.stop()
|
||||
}
|
||||
state.update {
|
||||
it.copy(
|
||||
itemId = null,
|
||||
job = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ThemeSongState(
|
||||
val itemId: UUID? = null,
|
||||
val job: Job? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -509,15 +509,11 @@ class CollectionFolderViewModel
|
|||
}
|
||||
|
||||
fun onResumePage() {
|
||||
viewModelScope.launchIO {
|
||||
viewModelScope.launchDefault {
|
||||
item.value?.let {
|
||||
Timber.v("onResumePage: %s", loading.value!!::class)
|
||||
if (it.type == BaseItemKind.BOX_SET && loading.value !is DataLoadingState.Error) {
|
||||
val volume =
|
||||
userPreferencesService
|
||||
.getCurrent()
|
||||
.appPreferences.interfacePreferences.playThemeSongs
|
||||
themeSongPlayer.playThemeFor(it.id, volume)
|
||||
themeSongPlayer.playThemeFor(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,12 +156,7 @@ class CollectionViewModel
|
|||
)
|
||||
}
|
||||
listenForStateUpdates()
|
||||
themeSongPlayer.playThemeFor(
|
||||
itemId,
|
||||
preferencesService
|
||||
.getCurrent()
|
||||
.appPreferences.interfacePreferences.playThemeSongs,
|
||||
)
|
||||
themeSongPlayer.playThemeFor(itemId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,11 +132,10 @@ fun EpisodeDetails(
|
|||
|
||||
LoadingState.Success -> {
|
||||
item?.let { ep ->
|
||||
LifecycleResumeEffect(destination.itemId) {
|
||||
viewModel.maybePlayThemeSong(
|
||||
destination.itemId,
|
||||
preferences.appPreferences.interfacePreferences.playThemeSongs,
|
||||
)
|
||||
LifecycleResumeEffect(ep) {
|
||||
ep.data.seriesId?.let { seriesId ->
|
||||
viewModel.maybePlayThemeSong(seriesId)
|
||||
}
|
||||
onPauseOrDispose {
|
||||
viewModel.release()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
|||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.MediaManagementService
|
||||
|
|
@ -178,15 +177,9 @@ class EpisodeViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun maybePlayThemeSong(
|
||||
seriesId: UUID,
|
||||
playThemeSongs: ThemeSongVolume,
|
||||
) {
|
||||
fun maybePlayThemeSong(seriesId: UUID) {
|
||||
viewModelScope.launchIO {
|
||||
themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
|
||||
addCloseable {
|
||||
themeSongPlayer.stop()
|
||||
}
|
||||
themeSongPlayer.playThemeFor(seriesId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -152,10 +152,7 @@ fun MovieDetails(
|
|||
val movie by rememberUpdatedState(s.data)
|
||||
val chosenStreams by rememberUpdatedState(state.chosenStreams)
|
||||
LifecycleResumeEffect(destination.itemId) {
|
||||
viewModel.maybePlayThemeSong(
|
||||
destination.itemId,
|
||||
preferences.appPreferences.interfacePreferences.playThemeSongs,
|
||||
)
|
||||
viewModel.maybePlayThemeSong(destination.itemId)
|
||||
onPauseOrDispose {
|
||||
viewModel.release()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.github.damontecres.wholphin.data.model.DiscoverItem
|
|||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.Person
|
||||
import com.github.damontecres.wholphin.data.model.Trailer
|
||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.ExtrasService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
|
|
@ -258,12 +257,9 @@ class MovieViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun maybePlayThemeSong(
|
||||
seriesId: UUID,
|
||||
playThemeSongs: ThemeSongVolume,
|
||||
) {
|
||||
fun maybePlayThemeSong(itemId: UUID) {
|
||||
viewModelScope.launchIO {
|
||||
themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
|
||||
themeSongPlayer.playThemeFor(itemId)
|
||||
addCloseable {
|
||||
themeSongPlayer.stop()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -282,12 +282,8 @@ class SeriesViewModel
|
|||
fun onResumePage() {
|
||||
item.value?.let { item ->
|
||||
viewModelScope.launchDefault { backdropService.submit(item) }
|
||||
viewModelScope.launchIO {
|
||||
val playThemeSongs =
|
||||
userPreferencesService
|
||||
.getCurrent()
|
||||
.appPreferences.interfacePreferences.playThemeSongs
|
||||
themeSongPlayer.playThemeFor(seriesId, playThemeSongs)
|
||||
viewModelScope.launchDefault {
|
||||
themeSongPlayer.playThemeFor(seriesId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue