Simplify how display mode is changed (#823)

## Description
Simplifies the refresh rate/display mode change code by removing the
live data go between.

Also, instead of a `CountDownLatch` that blocks the thread, use a
`Deferred` to suspend the coroutine instead.

### Related issues
Might help with #769

### Testing
Tested on nvidia shield
This commit is contained in:
Ray 2026-02-03 15:30:24 -05:00 committed by GitHub
parent 045b689994
commit 733213d6a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 27 deletions

View file

@ -66,9 +66,12 @@ import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
import com.github.damontecres.wholphin.util.DebugLogTree
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@ -126,19 +129,12 @@ class MainActivity : AppCompatActivity() {
@OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
instance = this
Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}")
lifecycle.addObserver(playbackLifecycleObserver)
if (savedInstanceState == null) {
appUpgradeHandler.copySubfont(false)
}
refreshRateService.refreshRateMode.observe(this) { modeId ->
// Listen for refresh rate changes
val attrs = window.attributes
if (attrs.preferredDisplayModeId != modeId) {
Timber.d("Switch preferredDisplayModeId to %s", modeId)
window.attributes = attrs.apply { preferredDisplayModeId = modeId }
}
}
viewModel.serverRepository.currentUser.observe(this) { user ->
if (user?.hasPin == true) {
window?.setFlags(
@ -384,6 +380,16 @@ class MainActivity : AppCompatActivity() {
}
}
fun changeDisplayMode(modeId: Int) {
lifecycleScope.launch(Dispatchers.Main + ExceptionHandler()) {
val attrs = window.attributes
if (attrs.preferredDisplayModeId != modeId) {
Timber.d("Switch preferredDisplayModeId to %s", modeId)
window.attributes = attrs.apply { preferredDisplayModeId = modeId }
}
}
}
companion object {
const val INTENT_ITEM_ID = "itemId"
const val INTENT_ITEM_TYPE = "itemType"
@ -391,6 +397,9 @@ class MainActivity : AppCompatActivity() {
const val INTENT_EPISODE_NUMBER = "epNum"
const val INTENT_SEASON_NUMBER = "seaNum"
const val INTENT_SEASON_ID = "seaId"
lateinit var instance: MainActivity
private set
}
}

View file

@ -6,17 +6,17 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.Display
import androidx.lifecycle.LiveData
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.MainActivity
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.EqualityMutableLiveData
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.jellyfin.sdk.model.api.MediaStream
import org.jellyfin.sdk.model.api.MediaStreamType
import timber.log.Timber
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.roundToInt
@ -30,7 +30,6 @@ class RefreshRateService
) {
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
private val originalMode = display.mode
val supportedDisplayModes get() = display.supportedModes.orEmpty()
@ -44,9 +43,6 @@ class RefreshRateService
)
}
private val _refreshRateMode = EqualityMutableLiveData<Int>(originalMode.modeId)
val refreshRateMode: LiveData<Int> = _refreshRateMode
/**
* Find the best display mode for the given stream and signal to change to it
*/
@ -54,10 +50,10 @@ class RefreshRateService
stream: MediaStream,
switchRefreshRate: Boolean,
switchResolution: Boolean,
) {
) = withContext(Dispatchers.IO) {
if (!switchRefreshRate && !switchResolution) {
Timber.v("Not switching either refresh rate nor resolution")
return
return@withContext
}
val currentDisplayMode = display.mode
require(stream.type == MediaStreamType.VIDEO) { "Stream is not video" }
@ -67,7 +63,7 @@ class RefreshRateService
if (switchRefreshRate) stream.realFrameRate else currentDisplayMode.refreshRate
if (width == null || height == null || frameRate == null) {
Timber.w("Video stream missing required info: width=%s, height=%s, frameRate=%s", width, height, frameRate)
return
return@withContext
}
Timber.d("Getting refresh rate for: width=%s, height=%s, frameRate=%s", width, height, frameRate)
val targetMode =
@ -86,14 +82,20 @@ class RefreshRateService
listener,
Handler(Looper.myLooper() ?: Looper.getMainLooper()),
)
_refreshRateMode.setValueOnMain(targetMode.modeId)
try {
if (!listener.latch.await(5, TimeUnit.SECONDS)) {
MainActivity.instance.changeDisplayMode(targetMode.modeId)
val result =
withTimeoutOrNull(5.seconds) {
listener.deferred.await()
}
if (result == null) {
Timber.w("Timed out waiting for display change")
showToast(context, "Refresh rate switch is taking a long time")
}
} catch (ex: InterruptedException) {
} catch (ex: Exception) {
Timber.w(ex, "Exception waiting for refresh rate switch")
} finally {
displayManager.unregisterDisplayListener(listener)
}
val targetRate = (targetMode.refreshRate * 1000).roundToInt()
val isSeamless =
@ -110,7 +112,6 @@ class RefreshRateService
// Wait the recommended 2 seconds (https://developer.android.com/media/optimize/performance/frame-rate)
delay(2.seconds)
}
displayManager.unregisterDisplayListener(listener)
}
}
@ -118,13 +119,13 @@ class RefreshRateService
* Reset the display mode to the original
*/
fun resetRefreshRate() {
_refreshRateMode.value = originalMode.modeId
MainActivity.instance.changeDisplayMode(0)
}
private class Listener(
val displayId: Int,
) : DisplayManager.DisplayListener {
val latch = CountDownLatch(1)
val deferred = CompletableDeferred<Unit>()
override fun onDisplayAdded(displayId: Int) {
}
@ -132,7 +133,7 @@ class RefreshRateService
override fun onDisplayChanged(displayId: Int) {
if (displayId == this.displayId) {
Timber.v("Got display change for $displayId")
latch.countDown()
deferred.complete(Unit)
}
}