## Description **On Amlogic boxes (S905X4 and similar) Dolby Vision Profile 7 does not play as Dolby Vision.** The server transcodes it, or, with "Direct play Dolby Vision Profile 7" switched on by hand, the decoder answers `NoSupport` and fallback ends in HDR10. Devices with a native Profile 7 decoder, such as the Shield, are not affected and nothing changes for them. This PR converts Profile 7 to 8.1 on the fly. It has two halves and one decision. The preference `convertDolbyVisionProfile7` is proto field 6 in the experimental settings, off by default. In `WholphinRenderersFactory` the filter comes first in the list, followed by the HDR10+ masking from #1876. **What this PR does not do.** The enhancement layer is dropped; that is the price of the conversion, which is why devices with a native Profile 7 decoder are left alone. `BlockAdditional` (an enhancement layer outside the stream) is not read — like the rest of Wholphin today — so such a file stays HDR10, as it is now. ### Related issues Closes #244 Builds on #1876 (the `BitstreamFilter` list). Related to #1540. @fanni42 asked for exactly this on an S905X4 in #1876. ### Testing **Hardware** — a debug build of this branch, Homatics box (Amlogic S905X4, Android 14) + LG BX, Jellyfin 12.1.0, a Profile 7 FEL remux with the enhancement layer in band, played from the start for 39 seconds. Only the new preference was switched on; manual "Direct play Dolby Vision Profile 7" was **off**: * `DolbyVisionProfile7FilterTest` (14) — a fake rewriter on a direct buffer: offset ≠ 0, position/limit untouched, every "bytes stay as they were" branch, three-byte start codes, `UnsatisfiedLinkError` tried once; * `DolbyVisionProfile7ExtractorsFactoryTest` (5, mockk) — only the video track is rewritten; * `DolbyVisionProfile7ConversionTest` (6) — codec strings, DRM, `dvh1`. CI in my fork ran on this branch plus a debug signing commit: `pre-commit`, `testDefaultDebugUnitTest` and `assembleDefaultDebug` are all green. The hardware measurement above is that build. ## Screenshots The only UI change is one toggle in the experimental settings. ## AI or LLM usage The code was written with Claude (Claude Code): the filter, the extractor wrapper, the condition and the tests. I reviewed every line and can explain it. It was tested with the unit tests and the hardware measurement above. This description was written by me in Russian and translated into English with the same assistant. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Damontecres <damontecres@gmail.com>
413 lines
14 KiB
Kotlin
413 lines
14 KiB
Kotlin
import com.android.build.api.dsl.ApplicationExtension
|
|
import com.android.build.api.dsl.ProductFlavor
|
|
import com.android.build.api.variant.FilterConfiguration
|
|
import com.google.protobuf.gradle.id
|
|
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
|
import com.mikepenz.aboutlibraries.plugin.DuplicateRule
|
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
import java.util.Base64
|
|
import java.util.Properties
|
|
|
|
plugins {
|
|
alias(libs.plugins.android.application)
|
|
alias(libs.plugins.ksp)
|
|
alias(libs.plugins.kotlin.compose)
|
|
alias(libs.plugins.hilt)
|
|
alias(libs.plugins.room)
|
|
alias(libs.plugins.protobuf)
|
|
alias(libs.plugins.kotlin.plugin.serialization)
|
|
alias(libs.plugins.aboutLibraries)
|
|
alias(libs.plugins.openapi.generator)
|
|
}
|
|
|
|
val isCI = providers.environmentVariable("CI").orElse("false").map { it.toBoolean() }
|
|
val shouldSign =
|
|
isCI.zip(
|
|
providers.environmentVariable("KEY_ALIAS").orElse("").map { it.isNotBlank() },
|
|
) { isCI, hasKey ->
|
|
isCI && hasKey
|
|
}
|
|
val ffmpegModuleExists =
|
|
providers.provider { project.file("libs/lib-decoder-ffmpeg-release.aar").exists() }
|
|
val av1ModuleExists =
|
|
providers.provider { project.file("libs/lib-decoder-av1-release.aar").exists() }
|
|
val mpvModuleExists =
|
|
providers.provider { project.file("libs/wholphin-mpv-release.aar").exists() }
|
|
val extensionsRepoActive =
|
|
providers.provider { project.hasProperty("WholphinExtensionsUsername") }
|
|
|
|
// See https://issuetracker.google.com/issues/402800800
|
|
val isBuildingBundle =
|
|
providers.provider {
|
|
gradle.startParameter.taskNames.any { it.lowercase().contains("bundle") }
|
|
}
|
|
|
|
val gitTags =
|
|
providers
|
|
.exec { commandLine("git", "tag", "--list", "v*", "p*") }
|
|
.standardOutput.asText
|
|
.get()
|
|
|
|
val gitDescribe =
|
|
providers
|
|
.exec { commandLine("git", "describe", "--tags", "--long", "--match=v*") }
|
|
.standardOutput.asText
|
|
.getOrElse("v0.0.0")
|
|
|
|
kotlin {
|
|
compilerOptions {
|
|
languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3
|
|
jvmTarget = JvmTarget.JVM_11
|
|
javaParameters = true
|
|
|
|
// Workaround for https://github.com/google/ksp/issues/2964
|
|
moduleName = "${project.group}_${project.name}"
|
|
}
|
|
}
|
|
|
|
private fun Provider<String>.getInt() = get().toInt()
|
|
|
|
configure<ApplicationExtension> {
|
|
namespace = "com.github.damontecres.wholphin"
|
|
compileSdk = libs.versions.compileSdk.getInt()
|
|
|
|
defaultConfig {
|
|
applicationId = "com.github.damontecres.wholphin"
|
|
minSdk = libs.versions.minSdk.getInt()
|
|
targetSdk = libs.versions.targetSdk.getInt()
|
|
versionCode = gitTags.trim().lines().size
|
|
versionName = gitDescribe.trim().removePrefix("v").ifBlank { "0.0.0" }
|
|
testInstrumentationRunner = "com.github.damontecres.wholphin.test.WholphinTestRunner"
|
|
|
|
buildConfigField("long", "BUILD_TIME", System.currentTimeMillis().toString())
|
|
}
|
|
|
|
signingConfigs {
|
|
if (shouldSign.get()) {
|
|
create("ci") {
|
|
file("ci.keystore").writeBytes(
|
|
Base64.getDecoder().decode(System.getenv("SIGNING_KEY")),
|
|
)
|
|
keyAlias = System.getenv("KEY_ALIAS")
|
|
keyPassword = System.getenv("KEY_PASSWORD")
|
|
storePassword = System.getenv("KEY_STORE_PASSWORD")
|
|
storeFile = file("ci.keystore")
|
|
enableV1Signing = true
|
|
enableV2Signing = true
|
|
enableV3Signing = true
|
|
enableV4Signing = true
|
|
}
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
release {
|
|
isMinifyEnabled = true
|
|
isShrinkResources = true
|
|
proguardFiles(
|
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
"proguard-rules.pro",
|
|
)
|
|
isDebuggable = false
|
|
if (shouldSign.get()) {
|
|
signingConfig = signingConfigs.getByName("ci")
|
|
} else {
|
|
val localPropertiesFile = project.rootProject.file("local.properties")
|
|
if (localPropertiesFile.exists()) {
|
|
val properties = Properties()
|
|
properties.load(localPropertiesFile.inputStream())
|
|
val signingConfigName = properties["release.signing.config"]?.toString()
|
|
if (signingConfigName != null) {
|
|
signingConfig = signingConfigs.getByName(signingConfigName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
debug {
|
|
isMinifyEnabled = false
|
|
isShrinkResources = false
|
|
isDebuggable = true
|
|
applicationIdSuffix = ".debug"
|
|
}
|
|
}
|
|
flavorDimensions += "version"
|
|
productFlavors {
|
|
val featureLeanback = "leanback"
|
|
val featureUpdate = "UPDATING_ENABLED"
|
|
val featureDiscover = "DISCOVER_ENABLED"
|
|
|
|
fun ProductFlavor.setFeatureFlag(
|
|
name: String,
|
|
enabled: Boolean,
|
|
) {
|
|
this.buildConfigField("boolean", name, "Boolean.parseBoolean(\"${enabled}\")")
|
|
}
|
|
create("default") {
|
|
dimension = "version"
|
|
isDefault = true
|
|
manifestPlaceholders += mapOf(featureLeanback to false)
|
|
setFeatureFlag(featureUpdate, true)
|
|
setFeatureFlag(featureDiscover, true)
|
|
}
|
|
create("appstore") {
|
|
dimension = "version"
|
|
manifestPlaceholders += mapOf(featureLeanback to true)
|
|
setFeatureFlag(featureUpdate, false)
|
|
setFeatureFlag(featureDiscover, true)
|
|
}
|
|
create("firetv") {
|
|
dimension = "version"
|
|
manifestPlaceholders += mapOf(featureLeanback to true)
|
|
setFeatureFlag(featureUpdate, false)
|
|
setFeatureFlag(featureDiscover, false)
|
|
}
|
|
}
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_11
|
|
targetCompatibility = JavaVersion.VERSION_11
|
|
isCoreLibraryDesugaringEnabled = true
|
|
}
|
|
|
|
buildFeatures {
|
|
buildConfig = true
|
|
compose = true
|
|
}
|
|
|
|
splits {
|
|
abi {
|
|
// Disable split abis when building bundles
|
|
isEnable = !isBuildingBundle.get()
|
|
|
|
reset()
|
|
include("armeabi-v7a", "arm64-v8a", "x86_64")
|
|
isUniversalApk = true
|
|
}
|
|
}
|
|
packaging {
|
|
jniLibs {
|
|
// Work around because libass-android & wholphin-mpv both (incorrectly) package libc++_shared.so
|
|
pickFirsts += "lib/*/libc++_shared.so"
|
|
}
|
|
}
|
|
|
|
sourceSets {
|
|
getByName("main") {
|
|
kotlin.directories += "$buildDir/generated/seerr_api/src/main/kotlin"
|
|
}
|
|
}
|
|
|
|
testOptions {
|
|
unitTests {
|
|
isIncludeAndroidResources = true
|
|
}
|
|
}
|
|
|
|
lint {
|
|
disable.add("MissingTranslation")
|
|
}
|
|
androidResources {
|
|
generateLocaleConfig = true
|
|
}
|
|
}
|
|
|
|
androidComponents {
|
|
onVariants(selector().all()) { variant ->
|
|
variant.outputs
|
|
.map { it as com.android.build.api.variant.impl.VariantOutputImpl }
|
|
.forEach { output ->
|
|
val abi =
|
|
output
|
|
.getFilter(FilterConfiguration.FilterType.ABI)
|
|
.let { if (it != null) "-${it.identifier}" else "" }
|
|
val outputFileName =
|
|
"Wholphin-${variant.flavorName}-${variant.buildType}-${output.versionName.get()}-${output.versionCode.get()}$abi.apk"
|
|
output.outputFileName = outputFileName
|
|
}
|
|
}
|
|
}
|
|
|
|
room {
|
|
schemaDirectory("$projectDir/schemas")
|
|
}
|
|
|
|
protobuf {
|
|
protoc {
|
|
artifact = "com.google.protobuf:protoc:${libs.protobuf.kotlin.lite.get().version}"
|
|
}
|
|
generateProtoTasks {
|
|
all().forEach {
|
|
it.plugins {
|
|
id("java") {
|
|
option("lite")
|
|
}
|
|
}
|
|
it.builtins {
|
|
id("kotlin") {
|
|
option("lite")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
aboutLibraries {
|
|
collect {
|
|
configPath = file("config")
|
|
}
|
|
library {
|
|
duplicationMode = DuplicateMode.MERGE
|
|
duplicationRule = DuplicateRule.SIMPLE
|
|
}
|
|
}
|
|
|
|
openApiGenerate {
|
|
generatorName.set("kotlin")
|
|
inputSpec.set("$projectDir/src/main/seerr/seerr-api.yml")
|
|
templateDir.set("$projectDir/src/main/seerr/templates")
|
|
outputDir.set("$buildDir/generated/seerr_api")
|
|
apiPackage.set("com.github.damontecres.wholphin.api.seerr")
|
|
modelPackage.set("com.github.damontecres.wholphin.api.seerr.model")
|
|
groupId.set("com.github.damontecres.wholphin.api.seerr")
|
|
id.set("seerr-api")
|
|
packageName.set("com.github.damontecres.wholphin.api.seerr")
|
|
additionalProperties.apply {
|
|
put("serializationLibrary", "kotlinx_serialization")
|
|
put("sortModelPropertiesByRequiredFlag", true)
|
|
put("sortParamsByRequiredFlag", true)
|
|
put("useCoroutines", true)
|
|
put("enumPropertyNaming", "UPPERCASE")
|
|
put("modelMutable", false)
|
|
|
|
// Note: this is only for downloading files, so it's not necessary to enable
|
|
put("supportAndroidApiLevel25AndBelow", false)
|
|
}
|
|
}
|
|
|
|
tasks.named("preBuild") {
|
|
dependsOn.add(tasks.named("openApiGenerate"))
|
|
}
|
|
|
|
dependencies {
|
|
implementation(libs.androidx.core.ktx)
|
|
implementation(libs.androidx.appcompat)
|
|
implementation(platform(libs.androidx.compose.bom))
|
|
implementation(libs.androidx.compose.ui)
|
|
implementation(libs.androidx.compose.ui.graphics)
|
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
|
implementation(libs.androidx.compose.runtime)
|
|
implementation(libs.androidx.tv.foundation)
|
|
implementation(libs.androidx.tv.material)
|
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
implementation(libs.androidx.activity.compose)
|
|
implementation(libs.androidx.datastore)
|
|
implementation(libs.androidx.datastore.preferences)
|
|
implementation(libs.protobuf.kotlin.lite)
|
|
implementation(libs.androidx.tvprovider)
|
|
implementation(libs.androidx.work.runtime.ktx)
|
|
implementation(libs.androidx.hilt.work)
|
|
implementation(libs.androidx.profileinstaller)
|
|
|
|
implementation(libs.androidx.media3.exoplayer)
|
|
implementation(libs.androidx.media3.session)
|
|
implementation(libs.androidx.media3.datasource.okhttp)
|
|
implementation(libs.androidx.media3.exoplayer.hls)
|
|
implementation(libs.androidx.media3.exoplayer.dash)
|
|
implementation(libs.androidx.media3.ui)
|
|
implementation(libs.androidx.media3.ui.compose)
|
|
implementation(libs.ass.media)
|
|
implementation(libs.exoplayerhdrutils)
|
|
|
|
implementation(libs.coil.core)
|
|
implementation(libs.coil.compose)
|
|
implementation(libs.coil.network.cachecontrol)
|
|
implementation(libs.coil.network.okhttp)
|
|
implementation(libs.coil.gif)
|
|
implementation(libs.coil.svg)
|
|
|
|
implementation(libs.jellyfin.core)
|
|
implementation(libs.jellyfin.api)
|
|
implementation(libs.jellyfin.api.okhttp)
|
|
|
|
implementation(libs.androidx.navigation3.ui)
|
|
implementation(libs.androidx.navigation3.runtime)
|
|
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
|
implementation(libs.androidx.material3.adaptive.navigation3)
|
|
implementation(libs.kotlinx.serialization.core)
|
|
implementation(libs.kotlinx.serialization.json)
|
|
|
|
implementation(libs.hilt.android)
|
|
implementation(libs.androidx.room.common.jvm)
|
|
implementation(libs.androidx.room.ktx)
|
|
implementation(libs.androidx.compose.material3)
|
|
implementation(libs.androidx.hilt.navigation.compose)
|
|
implementation(libs.androidx.preference.ktx)
|
|
implementation(libs.androidx.room.testing)
|
|
implementation(libs.androidx.palette.ktx)
|
|
implementation(libs.androidx.media3.effect)
|
|
implementation(libs.androidx.runner)
|
|
ksp(libs.androidx.room.compiler)
|
|
ksp(libs.hilt.android.compiler)
|
|
ksp(libs.androidx.hilt.compiler)
|
|
|
|
implementation(libs.timber)
|
|
implementation(libs.slf4j2.timber)
|
|
implementation(libs.aboutlibraries.core)
|
|
implementation(libs.aboutlibraries.compose.m3)
|
|
implementation(libs.multiplatform.markdown.renderer)
|
|
implementation(libs.multiplatform.markdown.renderer.m3)
|
|
implementation(libs.programguide)
|
|
implementation(libs.acra.http)
|
|
implementation(libs.acra.dialog)
|
|
implementation(libs.acra.limiter)
|
|
compileOnly(libs.auto.service.annotations)
|
|
ksp(libs.auto.service.ksp)
|
|
implementation(platform(libs.okhttp.bom))
|
|
implementation(libs.okhttp)
|
|
implementation(libs.kache)
|
|
implementation(libs.kache.file)
|
|
|
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
|
|
|
if (ffmpegModuleExists.get()) {
|
|
logger.info("Using local ffmpeg decoder")
|
|
implementation(files("libs/lib-decoder-ffmpeg-release.aar"))
|
|
} else if (extensionsRepoActive.get()) {
|
|
logger.info("Using prebuilt ffmpeg decoder")
|
|
implementation(libs.wholphin.extensions.ffmpeg)
|
|
} else {
|
|
logger.warn("Media3 ffmpeg decoder was NOT found")
|
|
}
|
|
if (av1ModuleExists.get()) {
|
|
logger.info("Using local av1 decoder")
|
|
implementation(files("libs/lib-decoder-av1-release.aar"))
|
|
} else if (extensionsRepoActive.get()) {
|
|
logger.info("Using prebuilt av1 decoder")
|
|
implementation(libs.wholphin.extensions.av1)
|
|
} else {
|
|
logger.warn("Media3 av1 decoder was NOT found")
|
|
}
|
|
if (mpvModuleExists.get()) {
|
|
logger.info("Using local libMPV build")
|
|
implementation(files("libs/wholphin-mpv-release.aar"))
|
|
} else if (extensionsRepoActive.get()) {
|
|
logger.info("Using prebuilt libMPV")
|
|
implementation(libs.wholphin.extensions.mpv)
|
|
} else {
|
|
logger.warn("libMPV was NOT found, using stub library")
|
|
implementation(project(":wholphin-mpv-stub"))
|
|
}
|
|
|
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
|
testImplementation(libs.mockk.android)
|
|
testImplementation(libs.mockk.agent)
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
testImplementation(libs.androidx.core.testing)
|
|
testImplementation(libs.robolectric)
|
|
testImplementation(libs.hilt.android.testing)
|
|
testImplementation(libs.androidx.compose.ui.test.junit4)
|
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
|
androidTestImplementation(libs.mockk.android)
|
|
androidTestImplementation(libs.hilt.android.testing)
|
|
androidTestImplementation(libs.androidx.compose.ui.test.manifest)
|
|
}
|