Add detected HDR info to debug page (#2140)

## Description
Adds information to the debug page about detected HDR format for both
the device and the display.

I also wanted to try out the new `Grid` layout composable! It's much
better than the `SimpleTable`/`TableRow` composables I wrote previously.

### Related issues
There's a few issues/PR for improvements to HDR/DV support, so this will
help with debugging

### Testing
Emulator & nvidia shield on HDR TV

## Screenshots
N/A, just tables of info

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-10-27 18:47:02 -04:00 • committed by GitHub
parent cecfdaeba9
commit 6d029086e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 143 additions and 11 deletions

View file

@ -9,11 +9,16 @@ import android.media.AudioManager
import android.os.Build
import android.util.Log
import android.view.Display
import android.view.Display.HdrCapabilities
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalGridApi
import androidx.compose.foundation.layout.Grid
import androidx.compose.foundation.layout.GridFlow
import androidx.compose.foundation.layout.GridTrackSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -32,6 +37,7 @@ import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
@ -44,6 +50,7 @@ import com.github.damontecres.wholphin.data.SeriesTrackChoiceDao
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.DeviceProfileService
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.util.ExceptionHandler
@ -61,6 +68,7 @@ import timber.log.Timber
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.Date
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
@ -73,15 +81,15 @@ class DebugViewModel
val seriesTrackChoiceDao: SeriesTrackChoiceDao,
val clientInfo: ClientInfo,
val deviceInfo: DeviceInfo,
private val deviceProfileService: DeviceProfileService,
) : ViewModel() {
val state = MutableStateFlow(DebugState())
val itemPlaybacks = MutableStateFlow<List<ItemPlayback>>(emptyList())
val logcat = MutableStateFlow<List<LogcatLine>>(emptyList())
val supportedModes by lazy {
val display: Display by lazy {
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
display.supportedModes.orEmpty()
displayManager.getDisplay(Display.DEFAULT_DISPLAY)
}
val av1Included by lazy {
@ -162,7 +170,7 @@ class DebugViewModel
state.update {
it.copy(
deviceInfo = deviceInfoList,
displayModes = supportedModes.map { it.toString() },
displayModes = display.supportedModes.map { it.toDisplayMode(display) },
)
}
}
@ -216,6 +224,20 @@ class DebugViewModel
audioManager.registerAudioDeviceCallback(callback, null)
addCloseable { audioManager.unregisterAudioDeviceCallback(callback) }
}
viewModelScope.launchIO {
val hdrSupport =
buildMap {
put("HEVC HDR10", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcHDR10())
put("HEVC HDR10+", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcHDR10Plus())
put("DoVi single layer", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcDolbyVision())
put("DoVi Profile 7 EL", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcDolbyVisionEL())
put("DoVi Profile 8", deviceProfileService.mediaCodecCapabilitiesTest.supportsHevcDolbyVisionProfile8())
put("AV1 HDR10", deviceProfileService.mediaCodecCapabilitiesTest.supportsAV1HDR10())
put("AV1 HDR10+", deviceProfileService.mediaCodecCapabilitiesTest.supportsAV1HDR10Plus())
put("AV1 DoVi", deviceProfileService.mediaCodecCapabilitiesTest.supportsAV1DolbyVision())
}
state.update { it.copy(hdrSupport = hdrSupport) }
}
}
companion object {
@ -272,8 +294,9 @@ data class LogcatLine(
data class DebugState(
val appInfo: List<String> = emptyList(),
val deviceInfo: List<String> = emptyList(),
val displayModes: List<String> = emptyList(),
val displayModes: List<DisplayMode> = emptyList(),
val audioInfo: List<String> = emptyList(),
val hdrSupport: Map<String, Boolean> = emptyMap(),
)
val AudioDeviceInfo.details: String
@ -393,6 +416,7 @@ val AudioDeviceInfo.details: String
"productName=$productName, address=$addressStr)"
}
@OptIn(ExperimentalGridApi::class)
@Composable
fun DebugPage(
preferences: UserPreferences,
@ -476,15 +500,54 @@ fun DebugPage(
BodyText(it)
}
SubSectionTitle("Display Modes")
state.displayModes.forEach {
BodyText(it)
}
SubSectionTitle("Audio Devices")
state.audioInfo.forEach {
BodyText(it)
}
SubSectionTitle("Display Modes")
Grid(
config = {
repeat(6) { column(GridTrackSize.Auto) }
flow = GridFlow.Row
columnGap(8.dp)
rowGap(8.dp)
},
) {
EmphasizedBodyText("ID")
EmphasizedBodyText("Width")
EmphasizedBodyText("Height")
EmphasizedBodyText("FPS")
EmphasizedBodyText("HDR")
EmphasizedBodyText("Alt FPS")
state.displayModes.forEachIndexed { index, mode ->
BodyText(mode.id.toString())
BodyText(mode.width.toString())
BodyText(mode.height.toString())
BodyText(mode.fps)
BodyText(mode.hdr)
BodyText(mode.altFps)
}
}
SubSectionTitle("Detected HDR Support")
Grid(
config = {
column(GridTrackSize.Auto)
column(GridTrackSize.Auto)
flow = GridFlow.Row
columnGap(8.dp)
rowGap(4.dp)
},
) {
EmphasizedBodyText("HDR Format")
EmphasizedBodyText("Supported")
state.hdrSupport.forEach { (name, supported) ->
BodyText(name)
BodyText(supported.toString())
}
}
}
}
item {
@ -575,11 +638,29 @@ private fun SubSectionTitle(text: String) {
}
@Composable
private fun BodyText(text: String) {
private fun BodyText(
text: String,
modifier: Modifier = Modifier,
) {
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = modifier,
)
}
@Composable
private fun EmphasizedBodyText(
text: String,
modifier: Modifier = Modifier,
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
modifier = modifier,
)
}
@ -611,3 +692,45 @@ private val THIRD_PARTY_TAGS =
"HardwareConfig:W",
"DfltImageHeaderParser:W",
)
data class DisplayMode(
val id: Int,
val width: Int,
val height: Int,
val fps: String,
val hdr: String,
val altFps: String,
)
fun Display.Mode.toDisplayMode(display: Display): DisplayMode {
val fps = String.format(Locale.getDefault(), "%.3f", refreshRate)
val altFps =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
alternativeRefreshRates.joinToString(", ") {
String.format(Locale.getDefault(), "%.3f", it)
}
} else {
"N/A"
}
val hdr =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
supportedHdrTypes.joinToString(", ") { hdrTypeToString(it) }
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
display.hdrCapabilities?.supportedHdrTypes?.joinToString(", ") { hdrTypeToString(it) }
?: "None found"
} else {
"N/A"
}
return DisplayMode(modeId, physicalWidth, physicalHeight, fps, hdr, altFps)
}
fun hdrTypeToString(type: Int): String =
when (type) {
HdrCapabilities.HDR_TYPE_HDR10 -> "HDR10"
HdrCapabilities.HDR_TYPE_HDR10_PLUS -> "HDR10+"
HdrCapabilities.HDR_TYPE_HLG -> "HLG"
HdrCapabilities.HDR_TYPE_HLG_PLUS -> "HLG+"
HdrCapabilities.HDR_TYPE_DOLBY_VISION -> "DoVi"
HdrCapabilities.HDR_TYPE_INVALID -> "Invalid"
else -> "Unknown ($type)"
}

View file

@ -209,6 +209,15 @@ class MediaCodecCapabilitiesTest(
) &&
supportsMultiInstance(MediaFormat.MIMETYPE_VIDEO_HEVC)
// Checks for single-layer Dolby Vision Profile 8, which the Profile 7 conversion produces
fun supportsHevcDolbyVisionProfile8(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 &&
hasDecoder(
MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION,
DolbyVisionProfiles.Profile8,
CodecProfileLevel.DolbyVisionLevelHd24,
)
fun supportsHevcHDR10(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
hasDecoder(