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.os.Build
import android.util.Log import android.util.Log
import android.view.Display import android.view.Display
import android.view.Display.HdrCapabilities
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.gestures.scrollBy
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.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.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.key
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel 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.ServerRepository
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.preferences.UserPreferences 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.launchDefault
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
@ -61,6 +68,7 @@ import timber.log.Timber
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.Date import java.util.Date
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -73,15 +81,15 @@ class DebugViewModel
val seriesTrackChoiceDao: SeriesTrackChoiceDao, val seriesTrackChoiceDao: SeriesTrackChoiceDao,
val clientInfo: ClientInfo, val clientInfo: ClientInfo,
val deviceInfo: DeviceInfo, val deviceInfo: DeviceInfo,
private val deviceProfileService: DeviceProfileService,
) : ViewModel() { ) : ViewModel() {
val state = MutableStateFlow(DebugState()) val state = MutableStateFlow(DebugState())
val itemPlaybacks = MutableStateFlow<List<ItemPlayback>>(emptyList()) val itemPlaybacks = MutableStateFlow<List<ItemPlayback>>(emptyList())
val logcat = MutableStateFlow<List<LogcatLine>>(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 displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) displayManager.getDisplay(Display.DEFAULT_DISPLAY)
display.supportedModes.orEmpty()
} }
val av1Included by lazy { val av1Included by lazy {
@ -162,7 +170,7 @@ class DebugViewModel
state.update { state.update {
it.copy( it.copy(
deviceInfo = deviceInfoList, deviceInfo = deviceInfoList,
displayModes = supportedModes.map { it.toString() }, displayModes = display.supportedModes.map { it.toDisplayMode(display) },
) )
} }
} }
@ -216,6 +224,20 @@ class DebugViewModel
audioManager.registerAudioDeviceCallback(callback, null) audioManager.registerAudioDeviceCallback(callback, null)
addCloseable { audioManager.unregisterAudioDeviceCallback(callback) } 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 { companion object {
@ -272,8 +294,9 @@ data class LogcatLine(
data class DebugState( data class DebugState(
val appInfo: List<String> = emptyList(), val appInfo: List<String> = emptyList(),
val deviceInfo: List<String> = emptyList(), val deviceInfo: List<String> = emptyList(),
val displayModes: List<String> = emptyList(), val displayModes: List<DisplayMode> = emptyList(),
val audioInfo: List<String> = emptyList(), val audioInfo: List<String> = emptyList(),
val hdrSupport: Map<String, Boolean> = emptyMap(),
) )
val AudioDeviceInfo.details: String val AudioDeviceInfo.details: String
@ -393,6 +416,7 @@ val AudioDeviceInfo.details: String
"productName=$productName, address=$addressStr)" "productName=$productName, address=$addressStr)"
} }
@OptIn(ExperimentalGridApi::class)
@Composable @Composable
fun DebugPage( fun DebugPage(
preferences: UserPreferences, preferences: UserPreferences,
@ -476,15 +500,54 @@ fun DebugPage(
BodyText(it) BodyText(it)
} }
SubSectionTitle("Display Modes")
state.displayModes.forEach {
BodyText(it)
}
SubSectionTitle("Audio Devices") SubSectionTitle("Audio Devices")
state.audioInfo.forEach { state.audioInfo.forEach {
BodyText(it) 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 { item {
@ -575,11 +638,29 @@ private fun SubSectionTitle(text: String) {
} }
@Composable @Composable
private fun BodyText(text: String) { private fun BodyText(
text: String,
modifier: Modifier = Modifier,
) {
Text( Text(
text = text, text = text,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface, 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", "HardwareConfig:W",
"DfltImageHeaderParser: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) 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 = fun supportsHevcHDR10(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
hasDecoder( hasDecoder(