Skip to content

Commit b24d579

Browse files
committed
feat(mpv): Add Vulkan/HDR device compatibility check
Introduces a new feature to verify Vulkan and HDR display compatibility for MPV playback, guiding users to optimal settings and ensuring fallback to OpenGL if Vulkan is not verified. Key changes: * MPV Device Compatibility Check: Implemented a new preference and UI in Player Options settings to verify Vulkan and HDR display support. * Dynamic Settings Display: Conditionally renders GPU API and HDR Output settings based on Vulkan verification status. * Vulkan Fallback: Player logic now defaults to OpenGL if Vulkan is selected but not verified as compatible. * Persistent Device Signature: Stores a device signature to avoid re-checking Vulkan compatibility on every launch.
1 parent 50295c7 commit b24d579

12 files changed

Lines changed: 596 additions & 23 deletions

File tree

.gitignore renamed to .afinityignore

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
# AFinity Ignore Rules
2+
#
3+
# This file uses standard Git ignore patterns but is not
4+
# automatically recognized by Git.
5+
#
6+
# SETUP
7+
# Run the following command from the repository root:
8+
#
9+
# git config --local core.excludesFile "$(pwd)/.afinityignore"
10+
#
11+
# Windows PowerShell:
12+
# git config --local core.excludesFile "$((Get-Location).Path)/.afinityignore"
13+
#
14+
# This configures Git to read these rules for this repository.
15+
# Each contributor must run the setup command after cloning.
16+
#
17+
# IMPORTANT
18+
# - This file supplements any existing .gitignore rules.
19+
# - Git does not ignore files that are already tracked.
20+
# - The configuration is local and is not inherited by forks.
21+
22+
123
# Built application files
224
*.apk
325
*.ap_

‎README.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ and podcasts, and direct media requests via Jellyseerr.
3535
```bash
3636
git clone https://github.com/MakD/AFinity.git
3737
cd AFinity
38+
bash setup-git.sh
3839
./gradlew assembleRelease
3940
```
4041

41-
> **Note:** Without a `key.properties` file in the project root, release builds are signed with the
42-
> debug key. They install fine, but can't update an official release — uninstall the official APK
43-
> first. To sign with your own key, create `key.properties` with `storeFile`, `storePassword`,
44-
> `keyAlias`, and `keyPassword` entries.
42+
AFinity uses a custom `.afinityignore` file for Git ignore patterns. `setup-git.sh` configures Git to use these rules for this repository only. This setup is required once per clone.
43+
44+
45+
> **Note:** Without a `key.properties` file in the project root, release builds are signed with the debug key. They install fine, but can't update an official release — uninstall the official APK first. To sign with your own key, create `key.properties` with `storeFile`, `storePassword`, `keyAlias`, and `keyPassword` entries.
4546
4647
## Initial Setup
4748

@@ -183,6 +184,8 @@ directly in your browser without needing any technical knowledge.
183184

184185
### Code Contributions
185186

187+
Before making changes, run `bash setup-git.sh` from the repository root to configure Git to use AFinity's custom `.afinityignore` rules. This setup is required once per clone.
188+
186189
1. **Fork** the repository
187190
2. **Create** a feature branch (`git checkout -b feature/AmazingFeature`)
188191
3. **Commit** your changes (`git commit -m 'Add some AmazingFeature'`)

‎app/src/main/java/com/makd/afinity/data/repository/PreferencesRepository.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ interface PreferencesRepository {
183183

184184
fun getMpvHdrPeakDetectionFlow(): Flow<Boolean>
185185

186+
suspend fun setMpvDeviceCheckSignature(signature: String)
187+
188+
suspend fun getMpvDeviceCheckSignature(): String?
189+
190+
fun getMpvDeviceCheckSignatureFlow(): Flow<String?>
191+
186192
suspend fun setPreferredAudioLanguage(language: String)
187193

188194
suspend fun getPreferredAudioLanguage(): String

‎app/src/main/java/com/makd/afinity/data/repository/impl/PreferencesRepositoryImpl.kt‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ constructor(@param:AppPreferences private val dataStore: DataStore<Preferences>)
147147
val MPV_HDR_OUTPUT = stringPreferencesKey("mpv_hdr_output")
148148
val MPV_TONE_MAPPING = stringPreferencesKey("mpv_tone_mapping")
149149
val MPV_HDR_PEAK_DETECTION = booleanPreferencesKey("mpv_hdr_peak_detection")
150+
val MPV_DEVICE_CHECK_SIGNATURE = stringPreferencesKey("mpv_device_check_signature")
150151
val MPV_VIDEO_OUTPUT = stringPreferencesKey("mpv_video_output")
151152
val MPV_AUDIO_OUTPUT = stringPreferencesKey("mpv_audio_output")
152153

@@ -899,6 +900,18 @@ constructor(@param:AppPreferences private val dataStore: DataStore<Preferences>)
899900
}
900901
}
901902

903+
override suspend fun setMpvDeviceCheckSignature(signature: String) {
904+
dataStore.edit { preferences -> preferences[Keys.MPV_DEVICE_CHECK_SIGNATURE] = signature }
905+
}
906+
907+
override suspend fun getMpvDeviceCheckSignature(): String? {
908+
return dataStore.data.first()[Keys.MPV_DEVICE_CHECK_SIGNATURE]
909+
}
910+
911+
override fun getMpvDeviceCheckSignatureFlow(): Flow<String?> {
912+
return dataStore.data.map { preferences -> preferences[Keys.MPV_DEVICE_CHECK_SIGNATURE] }
913+
}
914+
902915
override suspend fun setPreferredAudioLanguage(language: String) {
903916
dataStore.edit { preferences -> preferences[Keys.PREFERRED_AUDIO_LANGUAGE] = language }
904917
}

‎app/src/main/java/com/makd/afinity/data/repository/settings/PortablePreferences.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ internal object PortablePreferences {
4141
"mpv_hdr_peak_detection",
4242
"mpv_video_output",
4343
"mpv_audio_output",
44+
"mpv_device_check_signature",
4445
)
4546

4647
val specs: List<PrefSpec> =
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.makd.afinity.player.mpv
2+
3+
import android.content.Context
4+
import android.content.pm.PackageManager
5+
import android.hardware.display.DisplayManager
6+
import android.view.Display
7+
8+
data class MpvDeviceCapabilities(
9+
val vulkanVersion: String?,
10+
val vulkanSupported: Boolean,
11+
val hdrTypes: List<Int>,
12+
)
13+
14+
object MpvDeviceSupport {
15+
// The bundled libplacebo (v7) rejects devices below Vulkan 1.2.
16+
private const val MIN_VULKAN_VERSION = 0x402000
17+
const val MIN_VULKAN_VERSION_LABEL = "1.2"
18+
19+
fun supportsVulkan(context: Context): Boolean =
20+
context.packageManager.hasSystemFeature(
21+
PackageManager.FEATURE_VULKAN_HARDWARE_VERSION,
22+
MIN_VULKAN_VERSION,
23+
)
24+
25+
fun capabilities(context: Context): MpvDeviceCapabilities =
26+
MpvDeviceCapabilities(
27+
vulkanVersion = vulkanVersion(context),
28+
vulkanSupported = supportsVulkan(context),
29+
hdrTypes = hdrTypes(context),
30+
)
31+
32+
fun isVulkanVerified(capabilities: MpvDeviceCapabilities, savedSignature: String?): Boolean =
33+
capabilities.vulkanSupported && savedSignature == signature(capabilities)
34+
35+
fun signature(capabilities: MpvDeviceCapabilities): String =
36+
"vulkan=${capabilities.vulkanVersion ?: "none"};hdr=${capabilities.hdrTypes.isNotEmpty()}"
37+
38+
private fun vulkanVersion(context: Context): String? =
39+
context.packageManager.systemAvailableFeatures
40+
.firstOrNull { it.name == PackageManager.FEATURE_VULKAN_HARDWARE_VERSION }
41+
?.version
42+
?.let { "${(it ushr 22) and 0x7f}.${(it ushr 12) and 0x3ff}.${it and 0xfff}" }
43+
44+
private fun hdrTypes(context: Context): List<Int> {
45+
val displayManager =
46+
context.getSystemService(DisplayManager::class.java) ?: return emptyList()
47+
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) ?: return emptyList()
48+
return display.mode.supportedHdrTypes.toList()
49+
}
50+
}

‎app/src/main/java/com/makd/afinity/ui/player/PlayerViewModel.kt‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import com.makd.afinity.data.models.media.AfinitySources
6969
import com.makd.afinity.data.models.media.AfinityTrickplayInfo
7070
import com.makd.afinity.data.models.player.AssRenderMode
7171
import com.makd.afinity.data.models.player.GestureConfig
72+
import com.makd.afinity.data.models.player.MpvGpuApi
7273
import com.makd.afinity.data.models.player.MpvHdrOutput
7374
import com.makd.afinity.data.models.player.PlaybackStats
7475
import com.makd.afinity.data.models.player.PlayerEvent
@@ -97,6 +98,7 @@ import com.makd.afinity.player.audiobookshelf.AudiobookshelfPlayer
9798
import com.makd.afinity.player.common.TrackMapping
9899
import com.makd.afinity.player.common.TrackSelection
99100
import com.makd.afinity.player.mpv.MPVPlayer
101+
import com.makd.afinity.player.mpv.MpvDeviceSupport
100102
import com.makd.afinity.ui.player.utils.VolumeManager
101103
import com.makd.afinity.util.NetworkConnectivityMonitor
102104
import com.makd.afinity.util.formatFileSize
@@ -859,7 +861,10 @@ constructor(
859861
mpvVideoOutputValue = mpvVideoOutput
860862

861863
val bufferSizeMb = preferencesRepository.getBufferSizeMb()
862-
val gpuApi = preferencesRepository.getMpvGpuApi()
864+
val gpuApi =
865+
preferencesRepository.getMpvGpuApi().let {
866+
if (it == MpvGpuApi.VULKAN && !isVulkanVerified()) MpvGpuApi.OPENGL else it
867+
}
863868
val hdrOutput = preferencesRepository.getMpvHdrOutput()
864869
val toneMapping = preferencesRepository.getMpvToneMapping()
865870
val hdrPeakDetection = preferencesRepository.getMpvHdrPeakDetection()
@@ -1999,6 +2004,22 @@ constructor(
19992004
}
20002005
}
20012006

2007+
private suspend fun isVulkanVerified(): Boolean =
2008+
try {
2009+
val savedSignature = preferencesRepository.getMpvDeviceCheckSignature()
2010+
withContext(Dispatchers.IO) {
2011+
MpvDeviceSupport.isVulkanVerified(
2012+
MpvDeviceSupport.capabilities(context),
2013+
savedSignature,
2014+
)
2015+
}
2016+
} catch (e: CancellationException) {
2017+
throw e
2018+
} catch (e: Exception) {
2019+
Timber.e(e, "Failed to verify Vulkan support")
2020+
false
2021+
}
2022+
20022023
private fun isDisplayHdr(): Boolean? =
20032024
try {
20042025
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager

0 commit comments

Comments
 (0)