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.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(),
)
}

View file

@ -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 {

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

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.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,

View file

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

View file

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

View file

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

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.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<List<Cue>>(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
},

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