From 6d029086e9e1c4b45a48bffaac9b39b8767f3b27 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 27 Oct 2026 18:47:02 -0400 Subject: [PATCH] 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 --- .../wholphin/ui/detail/DebugPage.kt | 145 ++++++++++++++++-- .../profile/MediaCodecCapabilitiesTest.kt | 9 ++ 2 files changed, 143 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt index b63ae54a..fb4cdbdd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DebugPage.kt @@ -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>(emptyList()) val logcat = MutableStateFlow>(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 = emptyList(), val deviceInfo: List = emptyList(), - val displayModes: List = emptyList(), + val displayModes: List = emptyList(), val audioInfo: List = emptyList(), + val hdrSupport: Map = 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)" + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt index ab4de03e..a769c400 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt @@ -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(