From bddf51e07696eb0a39b2c0bcad17dffd9756ead7 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:17:28 -0500 Subject: [PATCH] Fix some threading issues (#364) Fixes thread used by changes from #359 Also create the device direct play profile lazily-ish off of the main thread because this can take some time to create. Should help address some of the "too much work" reported in #363, and also fixes the network-on-main exception in the logs on #363 --- .../damontecres/wholphin/MainActivity.kt | 18 +++-- .../wholphin/WholphinApplication.kt | 12 +++ .../wholphin/services/DeviceProfileService.kt | 76 +++++++++++++++++++ .../ui/components/RecommendedTvShow.kt | 4 +- .../wholphin/ui/nav/ApplicationContent.kt | 4 - .../wholphin/ui/nav/DestinationContent.kt | 3 - .../damontecres/wholphin/ui/nav/NavDrawer.kt | 3 - .../wholphin/ui/playback/PlaybackPage.kt | 4 +- .../wholphin/ui/playback/PlaybackViewModel.kt | 11 +-- .../util/profile/DeviceProfileUtils.kt | 33 ++++---- 10 files changed, 123 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index b7c6a696..849ee3c5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -34,6 +34,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.AppUpgradeHandler +import com.github.damontecres.wholphin.services.DeviceProfileService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver import com.github.damontecres.wholphin.services.ServerEventListener @@ -45,8 +46,9 @@ import com.github.damontecres.wholphin.ui.nav.ApplicationContent import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.util.DebugLogTree -import com.github.damontecres.wholphin.util.profile.createDeviceProfile import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.jellyfin.sdk.model.serializer.toUUIDOrNull import timber.log.Timber @@ -79,6 +81,9 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver + @Inject + lateinit var deviceProfileService: DeviceProfileService + @OptIn(ExperimentalTvMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -176,20 +181,19 @@ class MainActivity : AppCompatActivity() { } } } - val deviceProfile = - remember(current, preferences) { - createDeviceProfile( - this@MainActivity, - preferences, + LaunchedEffect(current, preferences) { + withContext(Dispatchers.IO) { + deviceProfileService.getOrCreateDeviceProfile( + preferences.appPreferences.playbackPreferences, current?.server?.serverVersion, ) } + } ApplicationContent( user = current?.user, server = current?.server, navigationManager = navigationManager, preferences = preferences, - deviceProfile = deviceProfile, modifier = Modifier.fillMaxSize(), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt b/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt index 7418dbc5..3368091a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/WholphinApplication.kt @@ -2,6 +2,8 @@ package com.github.damontecres.wholphin import android.app.Application import android.os.Build +import android.os.StrictMode +import android.os.StrictMode.ThreadPolicy import android.util.Log import androidx.compose.runtime.Composer import androidx.compose.runtime.ExperimentalComposeRuntimeApi @@ -19,6 +21,16 @@ class WholphinApplication : Application() { init { instance = this + if (BuildConfig.DEBUG) { + StrictMode.setThreadPolicy( + ThreadPolicy + .Builder() + .detectNetwork() + .penaltyLog() + .build(), + ) + } + if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } else { diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt new file mode 100644 index 00000000..ed04c3f2 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt @@ -0,0 +1,76 @@ +package com.github.damontecres.wholphin.services + +import android.content.Context +import com.github.damontecres.wholphin.preferences.PlaybackPreferences +import com.github.damontecres.wholphin.util.profile.MediaCodecCapabilitiesTest +import com.github.damontecres.wholphin.util.profile.createDeviceProfile +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.jellyfin.sdk.model.ServerVersion +import org.jellyfin.sdk.model.api.DeviceProfile +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DeviceProfileService + @Inject + constructor( + @param:ApplicationContext private val context: Context, + ) { + private val mediaCodecCapabilitiesTest by lazy { + // Created lazily below on the IO thread since it cn take time + MediaCodecCapabilitiesTest(context) + } + private val mutex = Mutex() + + private var configuration: DeviceProfileConfiguration? = null + private var deviceProfile: DeviceProfile? = null + + suspend fun getOrCreateDeviceProfile( + prefs: PlaybackPreferences, + serverVersion: ServerVersion?, + ): DeviceProfile = + withContext(Dispatchers.IO) { + mutex.withLock { + val newConfig = + DeviceProfileConfiguration( + maxBitrate = prefs.maxBitrate.toInt(), + isAC3Enabled = prefs.overrides.ac3Supported, + downMixAudio = prefs.overrides.downmixStereo, + assDirectPlay = prefs.overrides.directPlayAss, + pgsDirectPlay = prefs.overrides.directPlayPgs, + jellyfinTenEleven = + serverVersion != null && serverVersion >= ServerVersion(10, 11, 0), + ) + if (deviceProfile == null || this@DeviceProfileService.configuration != newConfig) { + this@DeviceProfileService.configuration = newConfig + this@DeviceProfileService.deviceProfile = + createDeviceProfile( + mediaTest = mediaCodecCapabilitiesTest, + maxBitrate = newConfig.maxBitrate, + isAC3Enabled = newConfig.isAC3Enabled, + downMixAudio = newConfig.downMixAudio, + assDirectPlay = newConfig.assDirectPlay, + pgsDirectPlay = newConfig.pgsDirectPlay, + jellyfinTenEleven = newConfig.jellyfinTenEleven, + ) + } + this@DeviceProfileService.deviceProfile!! + } + } + } + +/** + * The configuration used in [createDeviceProfile] + */ +data class DeviceProfileConfiguration( + val maxBitrate: Int, + val isAC3Enabled: Boolean, + val downMixAudio: Boolean, + val assDirectPlay: Boolean, + val pgsDirectPlay: Boolean, + val jellyfinTenEleven: Boolean, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt index a97ff0b9..80e8aa72 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt @@ -84,7 +84,7 @@ class RecommendedTvShowViewModel ?: AppPreference.HomePageItems.defaultValue.toInt() try { val resumeItemsDeferred = - viewModelScope.async { + viewModelScope.async(Dispatchers.IO) { val resumeItemsRequest = GetResumeItemsRequest( parentId = parentId, @@ -101,7 +101,7 @@ class RecommendedTvShowViewModel } val nextUpItemsDeferred = - viewModelScope.async { + viewModelScope.async(Dispatchers.IO) { val nextUpRequest = GetNextUpRequest( parentId = parentId, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt index ddd881b5..8e546fdb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt @@ -12,7 +12,6 @@ import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.components.ErrorMessage -import org.jellyfin.sdk.model.api.DeviceProfile /** * This is generally the root composable of the of the app @@ -25,7 +24,6 @@ fun ApplicationContent( user: JellyfinUser?, navigationManager: NavigationManager, preferences: UserPreferences, - deviceProfile: DeviceProfile, modifier: Modifier = Modifier, ) { NavDisplay( @@ -44,14 +42,12 @@ fun ApplicationContent( DestinationContent( destination = key, preferences = preferences, - deviceProfile = deviceProfile, modifier = modifier.fillMaxSize(), ) } else if (user != null && server != null) { NavDrawer( destination = key, preferences = preferences, - deviceProfile = deviceProfile, user = user, server = server, modifier = modifier, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 80b6de75..19bd999e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -32,7 +32,6 @@ import com.github.damontecres.wholphin.ui.setup.SwitchServerContent import com.github.damontecres.wholphin.ui.setup.SwitchUserContent import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.DeviceProfile import timber.log.Timber /** @@ -42,7 +41,6 @@ import timber.log.Timber fun DestinationContent( destination: Destination, preferences: UserPreferences, - deviceProfile: DeviceProfile, modifier: Modifier = Modifier, ) { when (destination) { @@ -56,7 +54,6 @@ fun DestinationContent( -> PlaybackPage( preferences = preferences, - deviceProfile = deviceProfile, destination = destination, modifier = modifier, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index 4b135a15..af2d3796 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -95,7 +95,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.DeviceProfile import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -211,7 +210,6 @@ fun NavDrawer( preferences: UserPreferences, user: JellyfinUser, server: JellyfinServer, - deviceProfile: DeviceProfile, modifier: Modifier = Modifier, viewModel: NavDrawerViewModel = hiltViewModel( @@ -526,7 +524,6 @@ fun NavDrawer( DestinationContent( destination = destination, preferences = preferences, - deviceProfile = deviceProfile, modifier = Modifier .fillMaxSize(), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index 2979b947..8dc28e7d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -81,7 +81,6 @@ import com.github.damontecres.wholphin.util.LoadingState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.extensions.ticks import java.util.UUID import kotlin.time.Duration.Companion.milliseconds @@ -94,7 +93,6 @@ import kotlin.time.Duration.Companion.seconds @Composable fun PlaybackPage( preferences: UserPreferences, - deviceProfile: DeviceProfile, destination: Destination, modifier: Modifier = Modifier, viewModel: PlaybackViewModel = hiltViewModel(), @@ -105,7 +103,7 @@ fun PlaybackPage( } } LaunchedEffect(destination) { - viewModel.init(destination, deviceProfile, preferences) + viewModel.init(destination, preferences) } val loading by viewModel.loading.observeAsState(LoadingState.Loading) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index b6477f40..0ae3b2b2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -37,6 +37,7 @@ import com.github.damontecres.wholphin.preferences.ShowNextUpWhen import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.DatePlayedService +import com.github.damontecres.wholphin.services.DeviceProfileService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.PlayerFactory import com.github.damontecres.wholphin.services.PlaylistCreationResult @@ -84,7 +85,6 @@ import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.api.sockets.subscribe import org.jellyfin.sdk.model.DeviceInfo import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.MediaSegmentDto import org.jellyfin.sdk.model.api.MediaSegmentType import org.jellyfin.sdk.model.api.MediaStreamType @@ -120,6 +120,7 @@ class PlaybackViewModel private val playerFactory: PlayerFactory, private val datePlayedService: DatePlayedService, private val deviceInfo: DeviceInfo, + private val deviceProfileService: DeviceProfileService, ) : ViewModel(), Player.Listener, AnalyticsListener { @@ -145,7 +146,6 @@ class PlaybackViewModel val subtitleCues = MutableLiveData>(listOf()) private lateinit var preferences: UserPreferences - private lateinit var deviceProfile: DeviceProfile internal lateinit var itemId: UUID internal lateinit var item: BaseItem internal var forceTranscoding: Boolean = false @@ -180,14 +180,12 @@ class PlaybackViewModel */ fun init( destination: Destination, - deviceProfile: DeviceProfile, preferences: UserPreferences, ) { nextUp.value = null this.preferences = preferences controllerViewState.hideMilliseconds = preferences.appPreferences.playbackPreferences.controllerTimeoutMs - this.deviceProfile = deviceProfile this.forceTranscoding = (destination as? Destination.Playback)?.forceTranscoding ?: false val positionMs: Long @@ -500,7 +498,10 @@ class PlaybackViewModel startTimeTicks = null, deviceProfile = if (playerBackend == PlayerBackend.EXO_PLAYER) { - deviceProfile + deviceProfileService.getOrCreateDeviceProfile( + preferences.appPreferences.playbackPreferences, + serverRepository.currentServer.value?.serverVersion, + ) } else { mpvDeviceProfile }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt index 4fc74cf1..e47cef56 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt @@ -2,10 +2,7 @@ package com.github.damontecres.wholphin.util.profile // Adapted from https://github.com/jellyfin/jellyfin-androidtv/blob/v0.19.4/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt -import android.content.Context import androidx.media3.common.MimeTypes -import com.github.damontecres.wholphin.preferences.UserPreferences -import org.jellyfin.sdk.model.ServerVersion import org.jellyfin.sdk.model.api.CodecType import org.jellyfin.sdk.model.api.DlnaProfileType import org.jellyfin.sdk.model.api.EncodingContext @@ -46,21 +43,21 @@ val supportedAudioCodecs = Codec.Audio.VORBIS, ) -fun createDeviceProfile( - context: Context, - userPreferences: UserPreferences, - serverVersion: ServerVersion?, -) = userPreferences.appPreferences.playbackPreferences.let { prefs -> - createDeviceProfile( - mediaTest = MediaCodecCapabilitiesTest(context), - maxBitrate = prefs.maxBitrate.toInt(), - isAC3Enabled = prefs.overrides.ac3Supported, - downMixAudio = prefs.overrides.downmixStereo, - assDirectPlay = prefs.overrides.directPlayAss, - pgsDirectPlay = prefs.overrides.directPlayPgs, - jellyfinTenEleven = serverVersion != null && serverVersion >= ServerVersion(10, 11, 0), - ) -} +// fun createDeviceProfile( +// context: Context, +// userPreferences: UserPreferences, +// serverVersion: ServerVersion?, +// ) = userPreferences.appPreferences.playbackPreferences.let { prefs -> +// createDeviceProfile( +// mediaTest = MediaCodecCapabilitiesTest(context), +// maxBitrate = prefs.maxBitrate.toInt(), +// isAC3Enabled = prefs.overrides.ac3Supported, +// downMixAudio = prefs.overrides.downmixStereo, +// assDirectPlay = prefs.overrides.directPlayAss, +// pgsDirectPlay = prefs.overrides.directPlayPgs, +// jellyfinTenEleven = serverVersion != null && serverVersion >= ServerVersion(10, 11, 0), +// ) +// } fun createDeviceProfile( mediaTest: MediaCodecCapabilitiesTest,