mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +02:00
Support resolution switching (#597)
## Description Adds a settings for automatic resolution switching. When enabled, the app tries to output content at the video stream's resolution. For example, if playing 1080p content on a 4K TV, the actual output by the Android TV device will be 1080p. This works in hand-in-hand with automatic refresh rate switching. So enabling both allows for the output to match both refresh rate and content resolution. Outputting the best refresh rate is preferred over best resolution. Also, shows the current display mode in the playback debug info. ### Related issues Closes #428 Fixes #531
This commit is contained in:
parent
38697b011d
commit
26a913b05e
11 changed files with 319 additions and 44 deletions
|
|
@ -38,7 +38,7 @@
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout">
|
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout|smallestScreenSize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin
|
package com.github.damontecres.wholphin
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
|
@ -114,12 +115,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
appUpgradeHandler.copySubfont(false)
|
appUpgradeHandler.copySubfont(false)
|
||||||
}
|
}
|
||||||
refreshRateService.refreshRateMode.observe(this) { mode ->
|
refreshRateService.refreshRateMode.observe(this) { modeId ->
|
||||||
// Listen for refresh rate changes
|
// Listen for refresh rate changes
|
||||||
val attrs = window.attributes
|
val attrs = window.attributes
|
||||||
if (attrs.preferredDisplayModeId != mode.modeId) {
|
if (attrs.preferredDisplayModeId != modeId) {
|
||||||
Timber.d("Switch preferredRefreshRate to %s", mode.refreshRate)
|
Timber.d("Switch preferredDisplayModeId to %s", modeId)
|
||||||
window.attributes = attrs.apply { preferredRefreshRate = mode.refreshRate }
|
window.attributes = attrs.apply { preferredDisplayModeId = modeId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.appStart()
|
viewModel.appStart()
|
||||||
|
|
@ -318,6 +319,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
Timber.d("onDestroy")
|
Timber.d("onDestroy")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
Timber.d("onConfigurationChanged")
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
Timber.v("onNewIntent")
|
Timber.v("onNewIntent")
|
||||||
|
|
|
||||||
|
|
@ -696,7 +696,25 @@ sealed interface AppPreference<Pref, T> {
|
||||||
defaultValue = false,
|
defaultValue = false,
|
||||||
getter = { it.playbackPreferences.refreshRateSwitching },
|
getter = { it.playbackPreferences.refreshRateSwitching },
|
||||||
setter = { prefs, value ->
|
setter = { prefs, value ->
|
||||||
prefs.updatePlaybackPreferences { refreshRateSwitching = value }
|
prefs.updatePlaybackPreferences {
|
||||||
|
if (!value) resolutionSwitching = false
|
||||||
|
refreshRateSwitching = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
summaryOn = R.string.automatic,
|
||||||
|
summaryOff = R.string.disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
val ResolutionSwitching =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
title = R.string.resolution_switching,
|
||||||
|
defaultValue = false,
|
||||||
|
getter = { it.playbackPreferences.resolutionSwitching },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updatePlaybackPreferences {
|
||||||
|
if (value) refreshRateSwitching = true
|
||||||
|
resolutionSwitching = value
|
||||||
|
}
|
||||||
},
|
},
|
||||||
summaryOn = R.string.automatic,
|
summaryOn = R.string.automatic,
|
||||||
summaryOff = R.string.disabled,
|
summaryOff = R.string.disabled,
|
||||||
|
|
@ -934,6 +952,7 @@ val advancedPreferences =
|
||||||
AppPreference.GlobalContentScale,
|
AppPreference.GlobalContentScale,
|
||||||
AppPreference.MaxBitrate,
|
AppPreference.MaxBitrate,
|
||||||
AppPreference.RefreshRateSwitching,
|
AppPreference.RefreshRateSwitching,
|
||||||
|
AppPreference.ResolutionSwitching,
|
||||||
AppPreference.PlaybackDebugInfo,
|
AppPreference.PlaybackDebugInfo,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ class AppPreferencesSerializer
|
||||||
playerBackend = AppPreference.PlayerBackendPref.defaultValue
|
playerBackend = AppPreference.PlayerBackendPref.defaultValue
|
||||||
refreshRateSwitching =
|
refreshRateSwitching =
|
||||||
AppPreference.RefreshRateSwitching.defaultValue
|
AppPreference.RefreshRateSwitching.defaultValue
|
||||||
|
resolutionSwitching = AppPreference.ResolutionSwitching.defaultValue
|
||||||
|
|
||||||
overrides =
|
overrides =
|
||||||
PlaybackOverrides
|
PlaybackOverrides
|
||||||
|
|
|
||||||
|
|
@ -32,41 +32,61 @@ class RefreshRateService
|
||||||
private val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
private val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
||||||
private val originalMode = display.mode
|
private val originalMode = display.mode
|
||||||
|
|
||||||
val displayModes get() = display.supportedModes
|
val supportedDisplayModes get() = display.supportedModes.orEmpty()
|
||||||
|
|
||||||
private val _refreshRateMode = EqualityMutableLiveData<Display.Mode>(originalMode)
|
private val displayModes: List<DisplayMode> by lazy {
|
||||||
val refreshRateMode: LiveData<Display.Mode> = _refreshRateMode
|
display.supportedModes
|
||||||
|
.orEmpty()
|
||||||
|
.map { DisplayMode(it) }
|
||||||
|
.sortedWith(
|
||||||
|
compareByDescending<DisplayMode>({ it.physicalWidth * it.physicalHeight })
|
||||||
|
.thenBy { it.refreshRateRounded },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
* Find the best display mode for the given stream and signal to change to it
|
||||||
*/
|
*/
|
||||||
suspend fun changeRefreshRate(stream: MediaStream) {
|
suspend fun changeRefreshRate(
|
||||||
|
stream: MediaStream,
|
||||||
|
switchRefreshRate: Boolean,
|
||||||
|
switchResolution: Boolean,
|
||||||
|
) {
|
||||||
|
if (!switchRefreshRate && !switchResolution) {
|
||||||
|
Timber.v("Not switching either refresh rate nor resolution")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val currentDisplayMode = display.mode
|
||||||
require(stream.type == MediaStreamType.VIDEO) { "Stream is not video" }
|
require(stream.type == MediaStreamType.VIDEO) { "Stream is not video" }
|
||||||
val width = stream.width
|
val width = stream.width
|
||||||
val height = stream.height
|
val height = stream.height
|
||||||
val frameRate = stream.realFrameRate?.times(100)?.roundToInt()
|
val frameRate =
|
||||||
|
if (switchRefreshRate) stream.realFrameRate else currentDisplayMode.refreshRate
|
||||||
if (width == null || height == null || frameRate == null) {
|
if (width == null || height == null || frameRate == null) {
|
||||||
Timber.w("Video stream missing required info: width=%s, height=%s, frameRate=%s", width, height, frameRate)
|
Timber.w("Video stream missing required info: width=%s, height=%s, frameRate=%s", width, height, frameRate)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Timber.d("Getting refresh rate for: width=%s, height=%s, frameRate=%s", width, height, frameRate)
|
Timber.d("Getting refresh rate for: width=%s, height=%s, frameRate=%s", width, height, frameRate)
|
||||||
val targetMode =
|
val targetMode =
|
||||||
display.supportedModes
|
findDisplayMode(
|
||||||
.filterNot { it.physicalHeight < height || it.physicalWidth < width }
|
displayModes = displayModes,
|
||||||
.filter {
|
streamWidth = width,
|
||||||
(it.refreshRate * 100).roundToInt().let { modeRate ->
|
streamHeight = height,
|
||||||
frameRate % modeRate == 0 || // Exact multiple
|
targetFrameRate = frameRate,
|
||||||
modeRate == (frameRate * 2.5).roundToInt() // eg 24 & 60fps
|
refreshRateSwitch = switchRefreshRate,
|
||||||
}
|
resolutionSwitch = switchResolution,
|
||||||
}.maxByOrNull { it.physicalWidth * it.physicalHeight }
|
)
|
||||||
Timber.i("Found display mode: %s, current=${display.mode}", targetMode)
|
Timber.i("Found display mode: %s, current=%s", targetMode, currentDisplayMode)
|
||||||
if (targetMode != null && targetMode != display.mode) {
|
if (targetMode != null && targetMode.modeId != currentDisplayMode.modeId) {
|
||||||
val listener = Listener(display.displayId)
|
val listener = Listener(display.displayId)
|
||||||
displayManager.registerDisplayListener(
|
displayManager.registerDisplayListener(
|
||||||
listener,
|
listener,
|
||||||
Handler(Looper.myLooper() ?: Looper.getMainLooper()),
|
Handler(Looper.myLooper() ?: Looper.getMainLooper()),
|
||||||
)
|
)
|
||||||
_refreshRateMode.setValueOnMain(targetMode)
|
_refreshRateMode.setValueOnMain(targetMode.modeId)
|
||||||
try {
|
try {
|
||||||
if (!listener.latch.await(5, TimeUnit.SECONDS)) {
|
if (!listener.latch.await(5, TimeUnit.SECONDS)) {
|
||||||
Timber.w("Timed out waiting for display change")
|
Timber.w("Timed out waiting for display change")
|
||||||
|
|
@ -98,7 +118,7 @@ class RefreshRateService
|
||||||
* Reset the display mode to the original
|
* Reset the display mode to the original
|
||||||
*/
|
*/
|
||||||
fun resetRefreshRate() {
|
fun resetRefreshRate() {
|
||||||
_refreshRateMode.value = originalMode
|
_refreshRateMode.value = originalMode.modeId
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Listener(
|
private class Listener(
|
||||||
|
|
@ -119,4 +139,69 @@ class RefreshRateService
|
||||||
override fun onDisplayRemoved(displayId: Int) {
|
override fun onDisplayRemoved(displayId: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Find the best display mode for the given stream & preferences
|
||||||
|
*
|
||||||
|
* @param displayModes candidates that are sorted by resolution and frame rate descending
|
||||||
|
*/
|
||||||
|
fun findDisplayMode(
|
||||||
|
displayModes: List<DisplayMode>,
|
||||||
|
streamWidth: Int,
|
||||||
|
streamHeight: Int,
|
||||||
|
targetFrameRate: Float,
|
||||||
|
refreshRateSwitch: Boolean,
|
||||||
|
resolutionSwitch: Boolean,
|
||||||
|
): DisplayMode? {
|
||||||
|
val streamRate = targetFrameRate.times(1000).roundToInt()
|
||||||
|
// Timber.v("display modes: %s", displayModes.joinToString("\n"))
|
||||||
|
val candidates =
|
||||||
|
if (refreshRateSwitch) {
|
||||||
|
displayModes
|
||||||
|
.filterNot { it.physicalHeight < streamHeight || it.physicalWidth < streamWidth }
|
||||||
|
.filter {
|
||||||
|
it.refreshRateRounded % streamRate == 0 || // Exact multiple
|
||||||
|
it.refreshRateRounded == (streamRate * 2.5).roundToInt() // eg 24 & 60fps
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
displayModes
|
||||||
|
.filterNot { it.physicalHeight < streamHeight || it.physicalWidth < streamWidth }
|
||||||
|
}
|
||||||
|
// Timber.v("display modes candidates: %s", candidates.joinToString("\n"))
|
||||||
|
return if (!resolutionSwitch) {
|
||||||
|
candidates.maxByOrNull { it.physicalWidth * it.physicalHeight }
|
||||||
|
} else {
|
||||||
|
candidates.firstOrNull {
|
||||||
|
it.physicalWidth == streamWidth &&
|
||||||
|
it.physicalHeight == streamHeight &&
|
||||||
|
it.refreshRateRounded == streamRate
|
||||||
|
}
|
||||||
|
?: candidates.firstOrNull {
|
||||||
|
it.physicalWidth == streamWidth &&
|
||||||
|
it.physicalHeight >= streamHeight &&
|
||||||
|
it.refreshRateRounded == streamRate
|
||||||
|
}
|
||||||
|
?: candidates
|
||||||
|
.filter { it.refreshRateRounded == streamRate }
|
||||||
|
.maxByOrNull { it.physicalWidth * it.physicalHeight }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DisplayMode(
|
||||||
|
val modeId: Int,
|
||||||
|
val physicalWidth: Int,
|
||||||
|
val physicalHeight: Int,
|
||||||
|
val refreshRate: Float,
|
||||||
|
) {
|
||||||
|
val refreshRateRounded: Int = (refreshRate * 1000).roundToInt()
|
||||||
|
|
||||||
|
constructor(mode: Display.Mode) : this(
|
||||||
|
mode.modeId,
|
||||||
|
mode.physicalWidth,
|
||||||
|
mode.physicalHeight,
|
||||||
|
mode.refreshRate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ fun DebugPage(
|
||||||
"Manufacturer: ${Build.MANUFACTURER}",
|
"Manufacturer: ${Build.MANUFACTURER}",
|
||||||
"Model: ${Build.MODEL}",
|
"Model: ${Build.MODEL}",
|
||||||
"Display Modes:",
|
"Display Modes:",
|
||||||
*viewModel.refreshRateService.displayModes,
|
*viewModel.refreshRateService.supportedDisplayModes,
|
||||||
).forEach {
|
).forEach {
|
||||||
Text(
|
Text(
|
||||||
text = it.toString(),
|
text = it.toString(),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
package com.github.damontecres.wholphin.ui.playback
|
package com.github.damontecres.wholphin.ui.playback
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.hardware.display.DisplayManager
|
||||||
|
import android.view.Display
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.ProvideTextStyle
|
import androidx.tv.material3.ProvideTextStyle
|
||||||
|
|
@ -14,13 +22,40 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.ui.byteRateSuffixes
|
import com.github.damontecres.wholphin.ui.byteRateSuffixes
|
||||||
import com.github.damontecres.wholphin.ui.formatBytes
|
import com.github.damontecres.wholphin.ui.formatBytes
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import org.jellyfin.sdk.model.api.TranscodingInfo
|
import org.jellyfin.sdk.model.api.TranscodingInfo
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaybackDebugOverlay(
|
fun PlaybackDebugOverlay(
|
||||||
currentPlayback: CurrentPlayback?,
|
currentPlayback: CurrentPlayback?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val display =
|
||||||
|
remember(context) {
|
||||||
|
try {
|
||||||
|
val displayManager =
|
||||||
|
context.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager
|
||||||
|
displayManager?.getDisplay(Display.DEFAULT_DISPLAY)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val displayMode by produceState<String?>(null) {
|
||||||
|
while (isActive) {
|
||||||
|
value =
|
||||||
|
display?.mode?.let {
|
||||||
|
val rate = String.format(Locale.getDefault(), "%.3f", it.refreshRate)
|
||||||
|
"${it.physicalWidth}x${it.physicalHeight}@${rate}fps, id=${it.modeId}"
|
||||||
|
}
|
||||||
|
delay(10.seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -38,10 +73,12 @@ fun PlaybackDebugOverlay(
|
||||||
add("Video Decoder:" to currentPlayback.videoDecoder)
|
add("Video Decoder:" to currentPlayback.videoDecoder)
|
||||||
add("Audio Decoder:" to currentPlayback.audioDecoder)
|
add("Audio Decoder:" to currentPlayback.audioDecoder)
|
||||||
}
|
}
|
||||||
|
add("Display Mode: " to displayMode?.toString())
|
||||||
},
|
},
|
||||||
|
modifier = Modifier.weight(1f, fill = false),
|
||||||
)
|
)
|
||||||
currentPlayback?.transcodeInfo?.let {
|
currentPlayback?.transcodeInfo?.let {
|
||||||
TranscodeInfo(it, Modifier)
|
TranscodeInfo(it, Modifier.weight(2f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -86,27 +123,21 @@ fun SimpleTable(
|
||||||
rows: List<Pair<String, String?>>,
|
rows: List<Pair<String, String?>>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
rows.forEach {
|
rows.forEach {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = it.first,
|
text = it.first,
|
||||||
|
modifier = Modifier.width(100.dp),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
modifier = Modifier,
|
|
||||||
) {
|
|
||||||
rows.forEach {
|
|
||||||
Text(
|
Text(
|
||||||
text = it.second.toString(),
|
text = it.second.toString(),
|
||||||
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.analytics.AnalyticsListener
|
import androidx.media3.exoplayer.analytics.AnalyticsListener
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.size.Size
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
|
@ -641,9 +642,15 @@ class PlaybackViewModel
|
||||||
mediaSourceInfo = source,
|
mediaSourceInfo = source,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (preferences.appPreferences.playbackPreferences.refreshRateSwitching) {
|
preferences.appPreferences.playbackPreferences.let { prefs ->
|
||||||
source.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO }?.let {
|
source.mediaStreams
|
||||||
refreshRateService.changeRefreshRate(it)
|
?.firstOrNull { it.type == MediaStreamType.VIDEO }
|
||||||
|
?.let { stream ->
|
||||||
|
refreshRateService.changeRefreshRate(
|
||||||
|
stream = stream,
|
||||||
|
switchRefreshRate = prefs.refreshRateSwitching,
|
||||||
|
switchResolution = prefs.resolutionSwitching,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|
@ -754,7 +761,7 @@ class PlaybackViewModel
|
||||||
ImageRequest
|
ImageRequest
|
||||||
.Builder(context)
|
.Builder(context)
|
||||||
.data(url)
|
.data(url)
|
||||||
.size(coil3.size.Size.ORIGINAL)
|
.size(Size.ORIGINAL)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ message PlaybackPreferences {
|
||||||
PlayerBackend player_backend = 20;
|
PlayerBackend player_backend = 20;
|
||||||
MpvOptions mpv_options = 21;
|
MpvOptions mpv_options = 21;
|
||||||
bool refresh_rate_switching = 22;
|
bool refresh_rate_switching = 22;
|
||||||
|
bool resolution_switching = 23;
|
||||||
}
|
}
|
||||||
|
|
||||||
message HomePagePreferences{
|
message HomePagePreferences{
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,7 @@
|
||||||
<string name="color_code_programs">Color-code programs</string>
|
<string name="color_code_programs">Color-code programs</string>
|
||||||
<string name="subtitle_delay">Subtitle delay</string>
|
<string name="subtitle_delay">Subtitle delay</string>
|
||||||
<string name="backdrop_display">Backdrop style</string>
|
<string name="backdrop_display">Backdrop style</string>
|
||||||
|
<string name="resolution_switching">Resolution switching</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package com.github.damontecres.wholphin.test
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.services.DisplayMode
|
||||||
|
import com.github.damontecres.wholphin.services.RefreshRateService
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class TestDisplayModeChoice {
|
||||||
|
companion object {
|
||||||
|
val HD_60 = DisplayMode(0, 1920, 1080, 60f)
|
||||||
|
val HD_30 = DisplayMode(1, 1920, 1080, 30f)
|
||||||
|
val HD_24 = DisplayMode(2, 1920, 1080, 24f)
|
||||||
|
|
||||||
|
val UHD_60 = DisplayMode(3, 3840, 2160, 60f)
|
||||||
|
val UHD_30 = DisplayMode(4, 3840, 2160, 30f)
|
||||||
|
val UHD_24 = DisplayMode(5, 3840, 2160, 24f)
|
||||||
|
|
||||||
|
val ALL_MODES = listOf(UHD_24, UHD_30, UHD_60, HD_24, HD_30, HD_60)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test1() {
|
||||||
|
val streamWidth = 1920
|
||||||
|
val streamHeight = 1080
|
||||||
|
val streamRealFrameRate = 60f
|
||||||
|
val result =
|
||||||
|
RefreshRateService.findDisplayMode(
|
||||||
|
displayModes = ALL_MODES,
|
||||||
|
streamWidth = streamWidth,
|
||||||
|
streamHeight = streamHeight,
|
||||||
|
targetFrameRate = streamRealFrameRate,
|
||||||
|
refreshRateSwitch = true,
|
||||||
|
resolutionSwitch = false,
|
||||||
|
)
|
||||||
|
Assert.assertEquals(3, result?.modeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test2() {
|
||||||
|
val streamWidth = 1920
|
||||||
|
val streamHeight = 1080
|
||||||
|
val streamRealFrameRate = 60f
|
||||||
|
val result =
|
||||||
|
RefreshRateService.findDisplayMode(
|
||||||
|
displayModes = ALL_MODES,
|
||||||
|
streamWidth = streamWidth,
|
||||||
|
streamHeight = streamHeight,
|
||||||
|
targetFrameRate = streamRealFrameRate,
|
||||||
|
refreshRateSwitch = true,
|
||||||
|
resolutionSwitch = true,
|
||||||
|
)
|
||||||
|
Assert.assertEquals(0, result?.modeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test3() {
|
||||||
|
val streamWidth = 1920
|
||||||
|
val streamHeight = 1080
|
||||||
|
val streamRealFrameRate = 30f
|
||||||
|
val result =
|
||||||
|
RefreshRateService.findDisplayMode(
|
||||||
|
displayModes = ALL_MODES,
|
||||||
|
streamWidth = streamWidth,
|
||||||
|
streamHeight = streamHeight,
|
||||||
|
targetFrameRate = streamRealFrameRate,
|
||||||
|
refreshRateSwitch = true,
|
||||||
|
resolutionSwitch = false,
|
||||||
|
)
|
||||||
|
Assert.assertEquals(4, result?.modeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test4() {
|
||||||
|
val streamWidth = 1920
|
||||||
|
val streamHeight = 804
|
||||||
|
val streamRealFrameRate = 30f
|
||||||
|
val result =
|
||||||
|
RefreshRateService.findDisplayMode(
|
||||||
|
displayModes = ALL_MODES,
|
||||||
|
streamWidth = streamWidth,
|
||||||
|
streamHeight = streamHeight,
|
||||||
|
targetFrameRate = streamRealFrameRate,
|
||||||
|
refreshRateSwitch = false,
|
||||||
|
resolutionSwitch = true,
|
||||||
|
)
|
||||||
|
Assert.assertEquals(1, result?.modeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFraction() {
|
||||||
|
val streamWidth = 1920
|
||||||
|
val streamHeight = 1080
|
||||||
|
val streamRealFrameRate = 30f
|
||||||
|
|
||||||
|
val displayModes =
|
||||||
|
listOf(
|
||||||
|
DisplayMode(0, 1920, 1080, 59.940f),
|
||||||
|
DisplayMode(1, 1920, 1080, 60f),
|
||||||
|
// DisplayMode(2, 1920, 1080, 29.970f),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result =
|
||||||
|
RefreshRateService.findDisplayMode(
|
||||||
|
displayModes = displayModes,
|
||||||
|
streamWidth = streamWidth,
|
||||||
|
streamHeight = streamHeight,
|
||||||
|
targetFrameRate = 29.970f,
|
||||||
|
refreshRateSwitch = true,
|
||||||
|
resolutionSwitch = false,
|
||||||
|
)
|
||||||
|
Assert.assertEquals(0, result?.modeId)
|
||||||
|
|
||||||
|
val result2 =
|
||||||
|
RefreshRateService.findDisplayMode(
|
||||||
|
displayModes = displayModes,
|
||||||
|
streamWidth = streamWidth,
|
||||||
|
streamHeight = streamHeight,
|
||||||
|
targetFrameRate = 24f,
|
||||||
|
refreshRateSwitch = true,
|
||||||
|
resolutionSwitch = false,
|
||||||
|
)
|
||||||
|
Assert.assertEquals(1, result2?.modeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue