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
This commit is contained in:
damontecres 2025-12-02 19:17:28 -05:00 committed by GitHub
parent 56bf3cb7a0
commit bddf51e076
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 123 additions and 45 deletions

View file

@ -34,6 +34,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.AppUpgradeHandler 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.NavigationManager
import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver
import com.github.damontecres.wholphin.services.ServerEventListener 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.nav.Destination
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.util.DebugLogTree import com.github.damontecres.wholphin.util.DebugLogTree
import com.github.damontecres.wholphin.util.profile.createDeviceProfile
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber import timber.log.Timber
@ -79,6 +81,9 @@ class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver
@Inject
lateinit var deviceProfileService: DeviceProfileService
@OptIn(ExperimentalTvMaterial3Api::class) @OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -176,20 +181,19 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
val deviceProfile = LaunchedEffect(current, preferences) {
remember(current, preferences) { withContext(Dispatchers.IO) {
createDeviceProfile( deviceProfileService.getOrCreateDeviceProfile(
this@MainActivity, preferences.appPreferences.playbackPreferences,
preferences,
current?.server?.serverVersion, current?.server?.serverVersion,
) )
} }
}
ApplicationContent( ApplicationContent(
user = current?.user, user = current?.user,
server = current?.server, server = current?.server,
navigationManager = navigationManager, navigationManager = navigationManager,
preferences = preferences, preferences = preferences,
deviceProfile = deviceProfile,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }

View file

@ -2,6 +2,8 @@ package com.github.damontecres.wholphin
import android.app.Application import android.app.Application
import android.os.Build import android.os.Build
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.util.Log import android.util.Log
import androidx.compose.runtime.Composer import androidx.compose.runtime.Composer
import androidx.compose.runtime.ExperimentalComposeRuntimeApi import androidx.compose.runtime.ExperimentalComposeRuntimeApi
@ -19,6 +21,16 @@ class WholphinApplication : Application() {
init { init {
instance = this instance = this
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
ThreadPolicy
.Builder()
.detectNetwork()
.penaltyLog()
.build(),
)
}
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
} else { } else {

View file

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

View file

@ -84,7 +84,7 @@ class RecommendedTvShowViewModel
?: AppPreference.HomePageItems.defaultValue.toInt() ?: AppPreference.HomePageItems.defaultValue.toInt()
try { try {
val resumeItemsDeferred = val resumeItemsDeferred =
viewModelScope.async { viewModelScope.async(Dispatchers.IO) {
val resumeItemsRequest = val resumeItemsRequest =
GetResumeItemsRequest( GetResumeItemsRequest(
parentId = parentId, parentId = parentId,
@ -101,7 +101,7 @@ class RecommendedTvShowViewModel
} }
val nextUpItemsDeferred = val nextUpItemsDeferred =
viewModelScope.async { viewModelScope.async(Dispatchers.IO) {
val nextUpRequest = val nextUpRequest =
GetNextUpRequest( GetNextUpRequest(
parentId = parentId, parentId = parentId,

View file

@ -12,7 +12,6 @@ import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.components.ErrorMessage 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 * This is generally the root composable of the of the app
@ -25,7 +24,6 @@ fun ApplicationContent(
user: JellyfinUser?, user: JellyfinUser?,
navigationManager: NavigationManager, navigationManager: NavigationManager,
preferences: UserPreferences, preferences: UserPreferences,
deviceProfile: DeviceProfile,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
NavDisplay( NavDisplay(
@ -44,14 +42,12 @@ fun ApplicationContent(
DestinationContent( DestinationContent(
destination = key, destination = key,
preferences = preferences, preferences = preferences,
deviceProfile = deviceProfile,
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) )
} else if (user != null && server != null) { } else if (user != null && server != null) {
NavDrawer( NavDrawer(
destination = key, destination = key,
preferences = preferences, preferences = preferences,
deviceProfile = deviceProfile,
user = user, user = user,
server = server, server = server,
modifier = modifier, modifier = modifier,

View file

@ -32,7 +32,6 @@ import com.github.damontecres.wholphin.ui.setup.SwitchServerContent
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber import timber.log.Timber
/** /**
@ -42,7 +41,6 @@ import timber.log.Timber
fun DestinationContent( fun DestinationContent(
destination: Destination, destination: Destination,
preferences: UserPreferences, preferences: UserPreferences,
deviceProfile: DeviceProfile,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (destination) { when (destination) {
@ -56,7 +54,6 @@ fun DestinationContent(
-> ->
PlaybackPage( PlaybackPage(
preferences = preferences, preferences = preferences,
deviceProfile = deviceProfile,
destination = destination, destination = destination,
modifier = modifier, modifier = modifier,
) )

View file

@ -95,7 +95,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -211,7 +210,6 @@ fun NavDrawer(
preferences: UserPreferences, preferences: UserPreferences,
user: JellyfinUser, user: JellyfinUser,
server: JellyfinServer, server: JellyfinServer,
deviceProfile: DeviceProfile,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: NavDrawerViewModel = viewModel: NavDrawerViewModel =
hiltViewModel( hiltViewModel(
@ -526,7 +524,6 @@ fun NavDrawer(
DestinationContent( DestinationContent(
destination = destination, destination = destination,
preferences = preferences, preferences = preferences,
deviceProfile = deviceProfile,
modifier = modifier =
Modifier Modifier
.fillMaxSize(), .fillMaxSize(),

View file

@ -81,7 +81,6 @@ import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import java.util.UUID import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -94,7 +93,6 @@ import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun PlaybackPage( fun PlaybackPage(
preferences: UserPreferences, preferences: UserPreferences,
deviceProfile: DeviceProfile,
destination: Destination, destination: Destination,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: PlaybackViewModel = hiltViewModel(), viewModel: PlaybackViewModel = hiltViewModel(),
@ -105,7 +103,7 @@ fun PlaybackPage(
} }
} }
LaunchedEffect(destination) { LaunchedEffect(destination) {
viewModel.init(destination, deviceProfile, preferences) viewModel.init(destination, preferences)
} }
val loading by viewModel.loading.observeAsState(LoadingState.Loading) val loading by viewModel.loading.observeAsState(LoadingState.Loading)

View file

@ -37,6 +37,7 @@ import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.DatePlayedService 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.NavigationManager
import com.github.damontecres.wholphin.services.PlayerFactory import com.github.damontecres.wholphin.services.PlayerFactory
import com.github.damontecres.wholphin.services.PlaylistCreationResult 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.api.sockets.subscribe
import org.jellyfin.sdk.model.DeviceInfo import org.jellyfin.sdk.model.DeviceInfo
import org.jellyfin.sdk.model.api.BaseItemKind 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.MediaSegmentDto
import org.jellyfin.sdk.model.api.MediaSegmentType import org.jellyfin.sdk.model.api.MediaSegmentType
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
@ -120,6 +120,7 @@ class PlaybackViewModel
private val playerFactory: PlayerFactory, private val playerFactory: PlayerFactory,
private val datePlayedService: DatePlayedService, private val datePlayedService: DatePlayedService,
private val deviceInfo: DeviceInfo, private val deviceInfo: DeviceInfo,
private val deviceProfileService: DeviceProfileService,
) : ViewModel(), ) : ViewModel(),
Player.Listener, Player.Listener,
AnalyticsListener { AnalyticsListener {
@ -145,7 +146,6 @@ class PlaybackViewModel
val subtitleCues = MutableLiveData<List<Cue>>(listOf()) val subtitleCues = MutableLiveData<List<Cue>>(listOf())
private lateinit var preferences: UserPreferences private lateinit var preferences: UserPreferences
private lateinit var deviceProfile: DeviceProfile
internal lateinit var itemId: UUID internal lateinit var itemId: UUID
internal lateinit var item: BaseItem internal lateinit var item: BaseItem
internal var forceTranscoding: Boolean = false internal var forceTranscoding: Boolean = false
@ -180,14 +180,12 @@ class PlaybackViewModel
*/ */
fun init( fun init(
destination: Destination, destination: Destination,
deviceProfile: DeviceProfile,
preferences: UserPreferences, preferences: UserPreferences,
) { ) {
nextUp.value = null nextUp.value = null
this.preferences = preferences this.preferences = preferences
controllerViewState.hideMilliseconds = controllerViewState.hideMilliseconds =
preferences.appPreferences.playbackPreferences.controllerTimeoutMs preferences.appPreferences.playbackPreferences.controllerTimeoutMs
this.deviceProfile = deviceProfile
this.forceTranscoding = this.forceTranscoding =
(destination as? Destination.Playback)?.forceTranscoding ?: false (destination as? Destination.Playback)?.forceTranscoding ?: false
val positionMs: Long val positionMs: Long
@ -500,7 +498,10 @@ class PlaybackViewModel
startTimeTicks = null, startTimeTicks = null,
deviceProfile = deviceProfile =
if (playerBackend == PlayerBackend.EXO_PLAYER) { if (playerBackend == PlayerBackend.EXO_PLAYER) {
deviceProfile deviceProfileService.getOrCreateDeviceProfile(
preferences.appPreferences.playbackPreferences,
serverRepository.currentServer.value?.serverVersion,
)
} else { } else {
mpvDeviceProfile mpvDeviceProfile
}, },

View file

@ -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 // 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 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.CodecType
import org.jellyfin.sdk.model.api.DlnaProfileType import org.jellyfin.sdk.model.api.DlnaProfileType
import org.jellyfin.sdk.model.api.EncodingContext import org.jellyfin.sdk.model.api.EncodingContext
@ -46,21 +43,21 @@ val supportedAudioCodecs =
Codec.Audio.VORBIS, Codec.Audio.VORBIS,
) )
fun createDeviceProfile( // fun createDeviceProfile(
context: Context, // context: Context,
userPreferences: UserPreferences, // userPreferences: UserPreferences,
serverVersion: ServerVersion?, // serverVersion: ServerVersion?,
) = userPreferences.appPreferences.playbackPreferences.let { prefs -> // ) = userPreferences.appPreferences.playbackPreferences.let { prefs ->
createDeviceProfile( // createDeviceProfile(
mediaTest = MediaCodecCapabilitiesTest(context), // mediaTest = MediaCodecCapabilitiesTest(context),
maxBitrate = prefs.maxBitrate.toInt(), // maxBitrate = prefs.maxBitrate.toInt(),
isAC3Enabled = prefs.overrides.ac3Supported, // isAC3Enabled = prefs.overrides.ac3Supported,
downMixAudio = prefs.overrides.downmixStereo, // downMixAudio = prefs.overrides.downmixStereo,
assDirectPlay = prefs.overrides.directPlayAss, // assDirectPlay = prefs.overrides.directPlayAss,
pgsDirectPlay = prefs.overrides.directPlayPgs, // pgsDirectPlay = prefs.overrides.directPlayPgs,
jellyfinTenEleven = serverVersion != null && serverVersion >= ServerVersion(10, 11, 0), // jellyfinTenEleven = serverVersion != null && serverVersion >= ServerVersion(10, 11, 0),
) // )
} // }
fun createDeviceProfile( fun createDeviceProfile(
mediaTest: MediaCodecCapabilitiesTest, mediaTest: MediaCodecCapabilitiesTest,