diff --git a/Cargo.toml b/Cargo.toml index 23d8439b..123ff3f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,68 +71,6 @@ desktop = [ "dep:cpal", "dep:windows-sys", ] -android = [ - "dep:anyhow", - "dep:argon2", - "dep:arc-swap", - "dep:async-stream", - "dep:async-trait", - "dep:axum", - "dep:axum-extra", - "dep:base64", - "dep:bytemuck", - "dep:bytes", - "dep:futures", - "dep:gpio-cdev", - "dep:hwcodec", - "dep:libc", - "dep:libyuv", - "dep:mime_guess", - "dep:nix", - "dep:parking_lot", - "dep:protobuf", - "dep:rand", - "dep:rcgen", - "dep:reqwest", - "dep:rtp", - "dep:rtsp-types", - "dep:rust-embed", - "dep:rustls", - "dep:sdp-types", - "dep:serde", - "dep:serde_json", - "dep:toml_edit", - "dep:serialport", - "dep:sha2", - "dep:sodiumoxide", - "dep:des", - "dep:sqlx", - "dep:alsa", - "dep:audiopus", - "dep:thiserror", - "dep:time", - "dep:tempfile", - "dep:tokio", - "dep:tokio-tungstenite", - "dep:tokio-util", - "dep:axum-server", - "dep:tower-http", - "dep:tracing", - "dep:tracing-log", - "dep:tracing-subscriber", - "dep:turbojpeg", - "dep:typeshare", - "dep:urlencoding", - "dep:uuid", - "dep:ventoy-img", - "dep:v4l2r", - "dep:webrtc", - "dep:xxhash-rust", -] -android-mediacodec = [ - "android", -] - [dependencies] # Async runtime tokio = { version = "1", features = ["full"], optional = true } @@ -229,7 +167,7 @@ sha2 = { version = "0.10", optional = true } # TypeScript type generation typeshare = { version = "1.0", optional = true } -[target.'cfg(any(unix, windows))'.dependencies] +[target.'cfg(any(target_os = "linux", windows))'.dependencies] # Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux) hwcodec = { path = "libs/hwcodec", features = ["bytes"], optional = true } libyuv = { path = "res/vcpkg/libyuv", optional = true } @@ -237,15 +175,10 @@ turbojpeg = { version = "1.3", optional = true } # Note: audiopus links to libopus.so (unavoidable for audio support) audiopus = { version = "0.2", optional = true } -[target.'cfg(all(unix, not(target_os = "android")))'.dependencies] +[target.'cfg(target_os = "linux")'.dependencies] # Utilities nix = { version = "0.30", default-features = false, features = ["fs", "socket", "net", "hostname", "poll"], optional = true } -[target.'cfg(target_os = "android")'.dependencies] -# Utilities -nix = { version = "0.30", default-features = false, features = ["fs", "socket", "hostname", "poll"], optional = true } - -[target.'cfg(unix)'.dependencies] # Video capture (V4L2) v4l2r = { path = "libs/v4l2r", optional = true } diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 957f56d0..00000000 --- a/android/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.gradle/ -.kotlin/ -build/ -local.properties -app/build/ -app/src/main/jniLibs/ -native/target/ diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts deleted file mode 100644 index 3e0e0e30..00000000 --- a/android/app/build.gradle.kts +++ /dev/null @@ -1,559 +0,0 @@ -import org.gradle.api.tasks.Exec -import java.security.MessageDigest -import java.util.Properties - -plugins { - id("com.android.application") -} - -val androidNdkVersion = "27.3.13750724" -val androidApiLevel = 21 -val nativeCrateDir = layout.projectDirectory.dir("../native") -val rootCrateDir = layout.projectDirectory.dir("../..") -val nativeCargoOutputDir = layout.buildDirectory.dir("generated/oneKvm/cargoJniLibs") -val nativeOutputRoot = layout.buildDirectory.dir("generated/oneKvm/jniLibs") -val nativeAssetRoot = layout.buildDirectory.dir("generated/oneKvm/assets") -val defaultAndroidFfmpegRoot = rootProject.layout.projectDirectory.dir("../dist/android-ffmpeg-mediacodec") -val defaultAndroidLibyuvRoot = rootProject.layout.projectDirectory.dir("../dist/android-libyuv") -val defaultAndroidTurbojpegRoot = rootProject.layout.projectDirectory.dir("../dist/android-turbojpeg") -val defaultAndroidAlsaRoot = rootProject.layout.projectDirectory.dir("../dist/android-alsa") -val defaultAndroidOpusRoot = rootProject.layout.projectDirectory.dir("../dist/android-opus") -val androidFfmpegRoot = providers.environmentVariable("ONE_KVM_ANDROID_FFMPEG_ROOT") - .orElse(defaultAndroidFfmpegRoot.asFile.absolutePath) -val androidLibyuvRoot = providers.environmentVariable("ONE_KVM_ANDROID_LIBYUV_ROOT") - .orElse(defaultAndroidLibyuvRoot.asFile.absolutePath) -val androidTurbojpegRoot = providers.environmentVariable("ONE_KVM_ANDROID_TURBOJPEG_ROOT") - .orElse(defaultAndroidTurbojpegRoot.asFile.absolutePath) -val androidAlsaRoot = providers.environmentVariable("ONE_KVM_ANDROID_ALSA_ROOT") - .orElse(defaultAndroidAlsaRoot.asFile.absolutePath) -val androidOpusRoot = providers.environmentVariable("ONE_KVM_ANDROID_OPUS_ROOT") - .orElse(defaultAndroidOpusRoot.asFile.absolutePath) -val selectedAndroidAbis = providers.environmentVariable("ONE_KVM_ANDROID_ABIS") - .orElse("arm64-v8a,armeabi-v7a") - .get() - .split(',', ' ', ';') - .map { it.trim() } - .filter { it.isNotEmpty() } - .distinct() -val androidBuildProfile = providers.environmentVariable("ONE_KVM_ANDROID_PROFILE") - .orElse("debug") - .get() - .lowercase() -val oneKvmVersion = Regex("""(?m)^version\s*=\s*"([^"]+)"""") - .find(rootCrateDir.file("Cargo.toml").asFile.readText()) - ?.groupValues - ?.get(1) - ?: throw GradleException("Failed to resolve version from root Cargo.toml") -val localProperties = Properties().apply { - val file = rootProject.file("local.properties") - if (file.exists()) { - file.inputStream().use { load(it) } - } -} -val androidSdkDir = file( - providers.environmentVariable("ANDROID_HOME") - .orElse(providers.environmentVariable("ANDROID_SDK_ROOT")) - .orElse(localProperties.getProperty("sdk.dir") ?: "/root/android-sdk") - .get(), -) -val androidNdkDir = androidSdkDir.resolve("ndk/$androidNdkVersion") - -val androidFfmpegBuildScript = rootProject.layout.projectDirectory - .dir("..") - .file("scripts/build-android-ffmpeg-mediacodec.sh") -val androidLibyuvBuildScript = rootProject.layout.projectDirectory - .dir("..") - .file("scripts/build-android-libyuv.sh") -val androidTurbojpegBuildScript = rootProject.layout.projectDirectory - .dir("..") - .file("scripts/build-android-turbojpeg.sh") -val androidAlsaBuildScript = rootProject.layout.projectDirectory - .dir("..") - .file("scripts/build-android-alsa.sh") -val androidOpusBuildScript = rootProject.layout.projectDirectory - .dir("..") - .file("scripts/build-android-opus.sh") - -val androidAbiTargets = mapOf( - "arm64-v8a" to Triple("arm64", "aarch64-linux-android", "aarch64-linux-android"), - "armeabi-v7a" to Triple("arm32", "armv7-linux-androideabi", "arm-linux-androideabi"), -) - -val selectedAndroidAbiTargets = selectedAndroidAbis.associateWith { abi -> - androidAbiTargets[abi] ?: throw GradleException( - "Unsupported ONE_KVM_ANDROID_ABIS entry: $abi. Supported values: ${androidAbiTargets.keys.joinToString(", ")}", - ) -} - -if (androidBuildProfile != "debug" && androidBuildProfile != "release") { - throw GradleException("Unsupported ONE_KVM_ANDROID_PROFILE: $androidBuildProfile. Use debug or release.") -} - -fun androidFfmpegBuildStamp(script: File): String { - val digest = MessageDigest.getInstance("SHA-256") - .digest(script.readBytes()) - .joinToString("") { "%02x".format(it) } - return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest" -} - -fun androidFfmpegRequiredFiles(root: File): List = listOf( - "include/libavcodec/avcodec.h", - "lib/libavcodec.a", - "lib/libavutil.a", -).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } } - -fun androidLibyuvBuildStamp(script: File): String { - val digest = MessageDigest.getInstance("SHA-256") - .digest(script.readBytes()) - .joinToString("") { "%02x".format(it) } - val turbojpegScriptDigest = MessageDigest.getInstance("SHA-256") - .digest(androidTurbojpegBuildScript.asFile.readBytes()) - .joinToString("") { "%02x".format(it) } - return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest;turbojpegScript=$turbojpegScriptDigest" -} - -fun androidLibyuvRequiredFiles(root: File): List = listOf( - "include/libyuv.h", - "lib/libyuv.a", -).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } } - -fun androidTurbojpegBuildStamp(script: File): String { - val digest = MessageDigest.getInstance("SHA-256") - .digest(script.readBytes()) - .joinToString("") { "%02x".format(it) } - return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest" -} - -fun androidAlsaBuildStamp(script: File): String { - val digest = MessageDigest.getInstance("SHA-256") - .digest(script.readBytes()) - .joinToString("") { "%02x".format(it) } - return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest" -} - -fun androidOpusBuildStamp(script: File): String { - val digest = MessageDigest.getInstance("SHA-256") - .digest(script.readBytes()) - .joinToString("") { "%02x".format(it) } - return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest" -} - -fun androidTurbojpegRequiredFiles(root: File): List = listOf( - "include/turbojpeg.h", - "include/jpeglib.h", - "lib/libjpeg.a", - "lib/libturbojpeg.a", -).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } } - -fun androidAlsaRequiredFiles(root: File): List = listOf( - "include/alsa/asoundlib.h", - "lib/libasound.so", -).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } } - -fun androidOpusRequiredFiles(root: File): List = listOf( - "include/opus/opus.h", - "lib/libopus.so", -).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } } - -android { - namespace = "cn.one_kvm.androidhost" - compileSdk = 36 - ndkVersion = androidNdkVersion - flavorDimensions += "abi" - - defaultConfig { - applicationId = "cn.one_kvm.androidhost" - minSdk = androidApiLevel - targetSdk = 36 - versionCode = 1 - versionName = oneKvmVersion - } - - productFlavors { - create("arm32") { - dimension = "abi" - ndk { - abiFilters += "armeabi-v7a" - } - } - create("arm64") { - dimension = "abi" - ndk { - abiFilters += "arm64-v8a" - } - } - } - - sourceSets { - getByName("main") { - assets.directories.clear() - jniLibs.directories.clear() - } - getByName("arm32") { - assets.directories.add("build/generated/oneKvm/assets/arm32") - jniLibs.directories.add("build/generated/oneKvm/jniLibs/arm32") - } - getByName("arm64") { - assets.directories.add("build/generated/oneKvm/assets/arm64") - jniLibs.directories.add("build/generated/oneKvm/jniLibs/arm64") - } - } -} - -tasks.register("buildAndroidFfmpegMediaCodec") { - description = "Builds the default Android FFmpeg MediaCodec static libraries." - group = "build" - - val ffmpegRoot = file(androidFfmpegRoot.get()) - val scriptFile = androidFfmpegBuildScript.asFile - val stampFile = ffmpegRoot.resolve(".one-kvm-android-ffmpeg.stamp") - - workingDir = rootProject.layout.projectDirectory.dir("..").asFile - commandLine( - "bash", - scriptFile.absolutePath, - "--output", - ffmpegRoot.absolutePath, - "--ndk", - androidNdkDir.absolutePath, - "--api", - androidApiLevel.toString(), - "--abis", - selectedAndroidAbis.joinToString(","), - ) - - inputs.file(scriptFile) - outputs.dir(ffmpegRoot) - - onlyIf { - val hasAndroidFfmpeg = androidFfmpegRequiredFiles(ffmpegRoot).all { it.exists() } - val hasCurrentBuildStamp = - stampFile.exists() && stampFile.readText() == androidFfmpegBuildStamp(scriptFile) - !hasAndroidFfmpeg || !hasCurrentBuildStamp - } - - doLast { - stampFile.writeText(androidFfmpegBuildStamp(scriptFile)) - } -} - -tasks.register("buildAndroidLibyuv") { - description = "Builds Android libyuv static libraries." - group = "build" - - val libyuvRoot = file(androidLibyuvRoot.get()) - val turbojpegRoot = file(androidTurbojpegRoot.get()) - val scriptFile = androidLibyuvBuildScript.asFile - val stampFile = libyuvRoot.resolve(".one-kvm-android-libyuv.stamp") - - dependsOn("buildAndroidTurbojpeg") - - workingDir = rootProject.layout.projectDirectory.dir("..").asFile - commandLine( - "bash", - scriptFile.absolutePath, - "--output", - libyuvRoot.absolutePath, - "--ndk", - androidNdkDir.absolutePath, - "--api", - androidApiLevel.toString(), - "--abis", - selectedAndroidAbis.joinToString(","), - "--jpeg-root", - turbojpegRoot.absolutePath, - ) - - inputs.file(scriptFile) - outputs.dir(libyuvRoot) - - onlyIf { - val hasAndroidLibyuv = androidLibyuvRequiredFiles(libyuvRoot).all { it.exists() } - val hasCurrentBuildStamp = - stampFile.exists() && stampFile.readText() == androidLibyuvBuildStamp(scriptFile) - !hasAndroidLibyuv || !hasCurrentBuildStamp - } - - doLast { - stampFile.writeText(androidLibyuvBuildStamp(scriptFile)) - } -} - -tasks.register("buildAndroidTurbojpeg") { - description = "Builds Android TurboJPEG static libraries." - group = "build" - - val turbojpegRoot = file(androidTurbojpegRoot.get()) - val scriptFile = androidTurbojpegBuildScript.asFile - val stampFile = turbojpegRoot.resolve(".one-kvm-android-turbojpeg.stamp") - - workingDir = rootProject.layout.projectDirectory.dir("..").asFile - commandLine( - "bash", - scriptFile.absolutePath, - "--output", - turbojpegRoot.absolutePath, - "--ndk", - androidNdkDir.absolutePath, - "--api", - androidApiLevel.toString(), - "--abis", - selectedAndroidAbis.joinToString(","), - ) - - inputs.file(scriptFile) - outputs.dir(turbojpegRoot) - - onlyIf { - val hasAndroidTurbojpeg = androidTurbojpegRequiredFiles(turbojpegRoot).all { it.exists() } - val hasCurrentBuildStamp = - stampFile.exists() && stampFile.readText() == androidTurbojpegBuildStamp(scriptFile) - !hasAndroidTurbojpeg || !hasCurrentBuildStamp - } - - doLast { - stampFile.writeText(androidTurbojpegBuildStamp(scriptFile)) - } -} - -tasks.register("buildAndroidAlsa") { - description = "Builds Android ALSA shared libraries." - group = "build" - - val alsaRoot = file(androidAlsaRoot.get()) - val scriptFile = androidAlsaBuildScript.asFile - val stampFile = alsaRoot.resolve(".one-kvm-android-alsa.stamp") - - workingDir = rootProject.layout.projectDirectory.dir("..").asFile - commandLine( - "bash", - scriptFile.absolutePath, - "--output", - alsaRoot.absolutePath, - "--ndk", - androidNdkDir.absolutePath, - "--api", - androidApiLevel.toString(), - "--abis", - selectedAndroidAbis.joinToString(","), - ) - - inputs.file(scriptFile) - outputs.dir(alsaRoot) - - onlyIf { - val hasAndroidAlsa = androidAlsaRequiredFiles(alsaRoot).all { it.exists() } - val hasCurrentBuildStamp = - stampFile.exists() && stampFile.readText() == androidAlsaBuildStamp(scriptFile) - !hasAndroidAlsa || !hasCurrentBuildStamp - } - - doLast { - stampFile.writeText(androidAlsaBuildStamp(scriptFile)) - } -} - -tasks.register("buildAndroidOpus") { - description = "Builds Android Opus shared libraries." - group = "build" - - val opusRoot = file(androidOpusRoot.get()) - val scriptFile = androidOpusBuildScript.asFile - val stampFile = opusRoot.resolve(".one-kvm-android-opus.stamp") - - workingDir = rootProject.layout.projectDirectory.dir("..").asFile - commandLine( - "bash", - scriptFile.absolutePath, - "--output", - opusRoot.absolutePath, - "--ndk", - androidNdkDir.absolutePath, - "--api", - androidApiLevel.toString(), - "--abis", - selectedAndroidAbis.joinToString(","), - ) - - inputs.file(scriptFile) - outputs.dir(opusRoot) - - onlyIf { - val hasAndroidOpus = androidOpusRequiredFiles(opusRoot).all { it.exists() } - val hasCurrentBuildStamp = - stampFile.exists() && stampFile.readText() == androidOpusBuildStamp(scriptFile) - !hasAndroidOpus || !hasCurrentBuildStamp - } - - doLast { - stampFile.writeText(androidOpusBuildStamp(scriptFile)) - } -} - -val cargoBuildAndroidAbiTaskNames = selectedAndroidAbiTargets.map { (abi, targets) -> - val (flavor, _, _) = targets - val taskName = "cargoBuildAndroid" + flavor.replaceFirstChar { - if (it.isLowerCase()) it.titlecase() else it.toString() - } - - tasks.register(taskName) { - description = "Builds the Android Rust bootstrap libraries for $abi." - group = "build" - - dependsOn( - "buildAndroidFfmpegMediaCodec", - "buildAndroidLibyuv", - "buildAndroidTurbojpeg", - "buildAndroidAlsa", - "buildAndroidOpus", - ) - - val cargoCommand = mutableListOf( - "cargo", - "ndk", - "-t", - abi, - "-P", - androidApiLevel.toString(), - "-o", - nativeCargoOutputDir.get().asFile.absolutePath, - "build", - "--lib", - "--bins", - ) - if (androidBuildProfile == "release") { - cargoCommand.add("--release") - } - - workingDir = nativeCrateDir.asFile - commandLine(cargoCommand) - args("--features", "android-mediacodec") - environment("ONE_KVM_ANDROID_FFMPEG_ROOT", androidFfmpegRoot.get()) - environment("ONE_KVM_ANDROID_LIBYUV_ROOT", androidLibyuvRoot.get()) - environment("ONE_KVM_ANDROID_LIBYUV_STATIC", "1") - environment("TURBOJPEG_SOURCE", "explicit") - environment("TURBOJPEG_STATIC", "1") - environment( - "TURBOJPEG_LIB_DIR", - file(androidTurbojpegRoot.get()).resolve("$abi/lib").absolutePath, - ) - environment( - "TURBOJPEG_INCLUDE_DIR", - file(androidTurbojpegRoot.get()).resolve("$abi/include").absolutePath, - ) - environment("PKG_CONFIG_ALLOW_CROSS", "1") - environment( - "PKG_CONFIG_LIBDIR", - file(androidAlsaRoot.get()).resolve("$abi/lib/pkgconfig").absolutePath, - ) - environment("PKG_CONFIG_SYSROOT_DIR", "") - environment("LIBOPUS_NO_PKG", "1") - environment("LIBOPUS_LIB_DIR", file(androidOpusRoot.get()).resolve("$abi/lib").absolutePath) - environment("ANDROID_HOME", androidSdkDir.absolutePath) - environment("ANDROID_SDK_ROOT", androidSdkDir.absolutePath) - environment("ANDROID_NDK_HOME", androidNdkDir.absolutePath) - environment("ANDROID_NDK", androidNdkDir.absolutePath) - environment("ANDROID_NDK_ROOT", androidNdkDir.absolutePath) - - inputs.files( - nativeCrateDir.file("Cargo.toml"), - nativeCrateDir.dir("src"), - rootCrateDir.file("Cargo.lock"), - rootCrateDir.file("Cargo.toml"), - rootCrateDir.file("build.rs"), - rootCrateDir.dir("libs"), - rootCrateDir.dir("res/vcpkg/libyuv"), - rootCrateDir.dir("src"), - ) - outputs.dir(nativeCargoOutputDir) - outputs.dir(file(androidFfmpegRoot.get())) - outputs.dir(file(androidLibyuvRoot.get())) - outputs.dir(file(androidTurbojpegRoot.get())) - outputs.dir(file(androidAlsaRoot.get())) - outputs.dir(file(androidOpusRoot.get())) - } - - taskName -} - -tasks.register("cargoBuildAndroid") { - description = "Builds the Android Rust bootstrap libraries." - group = "build" - - dependsOn(cargoBuildAndroidAbiTaskNames) - - outputs.dir(nativeOutputRoot) - outputs.dir(nativeAssetRoot) - - doLast { - selectedAndroidAbiTargets.forEach { (abi, targets) -> - val (flavor, rustTriple, ndkTriple) = targets - val nativeLibSource = nativeCargoOutputDir.get().file("$abi/libone_kvm_android_bootstrap.so").asFile - if (!nativeLibSource.exists()) { - throw GradleException("Missing Android JNI library: ${nativeLibSource.absolutePath}") - } - copy { - from(nativeLibSource) - into(nativeOutputRoot.get().dir(flavor).dir(abi)) - } - - val source = nativeCrateDir.file("target/$rustTriple/$androidBuildProfile/one-kvm-android-host").asFile - if (!source.exists()) { - throw GradleException("Missing Android host binary: ${source.absolutePath}") - } - copy { - from(source) - into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi")) - rename { "one-kvm-android-host" } - } - - val cxxShared = androidNdkDir - .resolve("toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/$ndkTriple/libc++_shared.so") - if (!cxxShared.exists()) { - throw GradleException("Missing NDK libc++_shared.so: ${cxxShared.absolutePath}") - } - copy { - from(cxxShared) - into(nativeOutputRoot.get().dir(flavor).dir(abi)) - } - copy { - from(cxxShared) - into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi")) - } - - val alsaShared = file(androidAlsaRoot.get()).resolve("$abi/lib/libasound.so") - if (!alsaShared.exists()) { - throw GradleException("Missing Android ALSA library: ${alsaShared.absolutePath}") - } - copy { - from(alsaShared) - into(nativeOutputRoot.get().dir(flavor).dir(abi)) - } - copy { - from(alsaShared) - into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi")) - } - copy { - from(file(androidAlsaRoot.get()).resolve("$abi/share/alsa")) - into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi/alsa")) - } - - val opusShared = file(androidOpusRoot.get()).resolve("$abi/lib/libopus.so") - if (!opusShared.exists()) { - throw GradleException("Missing Android Opus library: ${opusShared.absolutePath}") - } - copy { - from(opusShared) - into(nativeOutputRoot.get().dir(flavor).dir(abi)) - } - copy { - from(opusShared) - into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi")) - } - } - } -} - -tasks.named("preBuild") { - dependsOn("cargoBuildAndroid") -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 891c2456..00000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/java/cn/one_kvm/androidhost/BootReceiver.kt b/android/app/src/main/java/cn/one_kvm/androidhost/BootReceiver.kt deleted file mode 100644 index a89d28ad..00000000 --- a/android/app/src/main/java/cn/one_kvm/androidhost/BootReceiver.kt +++ /dev/null @@ -1,13 +0,0 @@ -package cn.one_kvm.androidhost - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -class BootReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Intent.ACTION_BOOT_COMPLETED && HostSettings.getAutoStart(context)) { - OneKvmService.start(context) - } - } -} diff --git a/android/app/src/main/java/cn/one_kvm/androidhost/HostSettings.kt b/android/app/src/main/java/cn/one_kvm/androidhost/HostSettings.kt deleted file mode 100644 index 9f4ba5c0..00000000 --- a/android/app/src/main/java/cn/one_kvm/androidhost/HostSettings.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cn.one_kvm.androidhost - -import android.content.Context - -object HostSettings { - private const val PREFS = "one_kvm_android" - private const val KEY_AUTO_START = "auto_start" - private const val KEY_CLEAR_EXISTING_OTG = "clear_existing_otg" - - fun getAutoStart(context: Context): Boolean { - return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .getBoolean(KEY_AUTO_START, false) - } - - fun setAutoStart(context: Context, enabled: Boolean) { - context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .edit() - .putBoolean(KEY_AUTO_START, enabled) - .apply() - } - - fun getClearExistingOtg(context: Context): Boolean { - return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .getBoolean(KEY_CLEAR_EXISTING_OTG, false) - } - - fun setClearExistingOtg(context: Context, enabled: Boolean) { - context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .edit() - .putBoolean(KEY_CLEAR_EXISTING_OTG, enabled) - .apply() - } -} diff --git a/android/app/src/main/java/cn/one_kvm/androidhost/LogConfig.kt b/android/app/src/main/java/cn/one_kvm/androidhost/LogConfig.kt deleted file mode 100644 index 6e201fd2..00000000 --- a/android/app/src/main/java/cn/one_kvm/androidhost/LogConfig.kt +++ /dev/null @@ -1,30 +0,0 @@ -package cn.one_kvm.androidhost - -import android.content.Context - -object LogConfig { - private const val PREFS = "one_kvm_android" - private const val KEY_LOG_LEVEL = "log_level" - const val DEFAULT_LEVEL = "info" - val LEVELS = arrayOf("error", "warn", "info", "debug", "trace") - - fun getLevel(context: Context): String { - val value = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .getString(KEY_LOG_LEVEL, DEFAULT_LEVEL) - ?: DEFAULT_LEVEL - return if (LEVELS.contains(value)) value else DEFAULT_LEVEL - } - - fun setLevel(context: Context, level: String) { - val safeLevel = if (LEVELS.contains(level)) level else DEFAULT_LEVEL - context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .edit() - .putString(KEY_LOG_LEVEL, safeLevel) - .apply() - } - - fun rustLogFilter(level: String): String { - val safeLevel = if (LEVELS.contains(level)) level else DEFAULT_LEVEL - return "one_kvm=$safeLevel,hwcodec=$safeLevel,tower_http=$safeLevel,webrtc_sctp=warn" - } -} diff --git a/android/app/src/main/java/cn/one_kvm/androidhost/LogStore.kt b/android/app/src/main/java/cn/one_kvm/androidhost/LogStore.kt deleted file mode 100644 index 398d807d..00000000 --- a/android/app/src/main/java/cn/one_kvm/androidhost/LogStore.kt +++ /dev/null @@ -1,71 +0,0 @@ -package cn.one_kvm.androidhost - -import android.content.Context -import java.io.File -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit - -object LogStore { - private const val FLUSH_DELAY_MS = 250L - private const val MAX_BUFFER_CHARS = 64 * 1024 - - private val lock = Any() - private val buffer = StringBuilder() - private val executor = Executors.newSingleThreadScheduledExecutor { runnable -> - Thread(runnable, "OneKvmLogStore").apply { isDaemon = true } - } - - private var logFile: File? = null - private var flushScheduled = false - - fun defaultLogFile(context: Context): File { - return File(File(context.getExternalFilesDir(null), "runtime"), "one-kvm.log") - } - - fun configure(file: File) { - synchronized(lock) { - flushLocked() - file.parentFile?.mkdirs() - file.writeText("") - buffer.clear() - logFile = file - flushScheduled = false - } - } - - fun append(line: String) { - synchronized(lock) { - if (logFile == null) return - - buffer.append(line).append('\n') - if (buffer.length >= MAX_BUFFER_CHARS) { - flushLocked() - return - } - - if (!flushScheduled) { - flushScheduled = true - executor.schedule({ flush() }, FLUSH_DELAY_MS, TimeUnit.MILLISECONDS) - } - } - } - - fun flush() { - synchronized(lock) { - flushLocked() - } - } - - private fun flushLocked() { - val file = logFile ?: return - if (buffer.isEmpty()) { - flushScheduled = false - return - } - - val text = buffer.toString() - buffer.clear() - flushScheduled = false - file.appendText(text) - } -} diff --git a/android/app/src/main/java/cn/one_kvm/androidhost/MainActivity.kt b/android/app/src/main/java/cn/one_kvm/androidhost/MainActivity.kt deleted file mode 100644 index 75a239b5..00000000 --- a/android/app/src/main/java/cn/one_kvm/androidhost/MainActivity.kt +++ /dev/null @@ -1,452 +0,0 @@ -package cn.one_kvm.androidhost - -import android.app.Activity -import android.graphics.Color -import android.graphics.Typeface -import android.graphics.drawable.GradientDrawable -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.view.Gravity -import android.view.View -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Button -import android.widget.CompoundButton -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.Spinner -import android.widget.Switch -import android.widget.TextView -import java.net.Inet4Address -import java.net.InetSocketAddress -import java.net.NetworkInterface -import java.net.Socket -import java.util.Collections - -class MainActivity : Activity() { - private lateinit var statusValue: TextView - private lateinit var hostActionButton: Button - private lateinit var logLevelSpinner: Spinner - private lateinit var autoStartSwitch: Switch - private lateinit var clearOtgSwitch: Switch - private val statusHandler = Handler(Looper.getMainLooper()) - private var statusPollsRemaining = 0 - private val statusPoller = object : Runnable { - override fun run() { - refreshStatus() - statusPollsRemaining -= 1 - if (statusPollsRemaining > 0) { - statusHandler.postDelayed(this, STATUS_POLL_INTERVAL_MS) - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - window.statusBarColor = color("#F8FAFC") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - window.navigationBarColor = color("#F8FAFC") - } - - val content = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - setPadding(20.dp(), 24.dp(), 20.dp(), 28.dp()) - background = solid("#F8FAFC") - } - - content.addView(startCard()) - content.addView(settingsCard()) - content.addView(infoCard()) - - setContentView(ScrollView(this).apply { - isFillViewport = true - setBackgroundColor(color("#F8FAFC")) - addView(content) - }) - } - - override fun onResume() { - super.onResume() - reconcilePersistedStatus() - refreshStatus() - autoStartSwitch.isChecked = HostSettings.getAutoStart(this) - clearOtgSwitch.isChecked = HostSettings.getClearExistingOtg(this) - } - - override fun onPause() { - statusHandler.removeCallbacks(statusPoller) - super.onPause() - } - - private fun startCard(): View { - return card { - addView(sectionTitle("启动管理")) - addView(TextView(this@MainActivity).apply { - text = "管理本机 One-KVM 服务进程。暂停会停止前台服务并释放运行资源。" - textSize = 14f - setTextColor(color("#64748B")) - setPadding(0, 6.dp(), 0, 14.dp()) - }) - - statusValue = TextView(this@MainActivity).apply { - textSize = 14f - typeface = Typeface.DEFAULT_BOLD - setTextColor(color("#0F172A")) - background = rounded("#EFF6FF", "#BFDBFE", 8) - setPadding(12.dp(), 8.dp(), 12.dp(), 8.dp()) - } - addView(statusValue, matchWrap()) - - addView(LinearLayout(this@MainActivity).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(0, 14.dp(), 0, 0) - hostActionButton = actionButton("启动", primary = true) { toggleHost() } - addView(hostActionButton, matchButton()) - }) - refreshStatus() - } - } - - private fun settingsCard(): View { - return card { - addView(sectionTitle("运行设置")) - - val (autoStartRow, autoStartControl) = settingSwitchRow( - title = "开机自启动", - subtitle = "系统启动完成后自动拉起 One-KVM 前台服务。", - checked = HostSettings.getAutoStart(this@MainActivity), - ) { _, checked -> - HostSettings.setAutoStart(this@MainActivity, checked) - LogStore.append("Boot auto-start ${if (checked) "enabled" else "disabled"}") - } - autoStartSwitch = autoStartControl - addView(autoStartRow) - - addView(divider()) - - val (clearOtgRow, clearOtgControl) = settingSwitchRow( - title = "清除已有 OTG Gadget", - subtitle = "启动 root host 前尝试解绑并删除 configfs 中已有的 USB gadget。", - checked = HostSettings.getClearExistingOtg(this@MainActivity), - ) { _, checked -> - HostSettings.setClearExistingOtg(this@MainActivity, checked) - LogStore.append("Clear existing OTG gadget ${if (checked) "enabled" else "disabled"}") - } - clearOtgSwitch = clearOtgControl - addView(clearOtgRow) - - addView(divider()) - addView(logLevelRow()) - } - } - - private fun infoCard(): View { - return card { - addView(sectionTitle("应用信息")) - addView(infoRow("软件内核版本", kernelVersion())) - addView(infoRow("访问地址", accessAddresses(), selectable = true)) - addView(infoRow("日志文件", LogStore.defaultLogFile(this@MainActivity).absolutePath, selectable = true)) - } - } - - private fun settingSwitchRow( - title: String, - subtitle: String, - checked: Boolean, - listener: CompoundButton.OnCheckedChangeListener, - ): Pair { - val switch = Switch(this).apply { - isChecked = checked - setOnCheckedChangeListener(listener) - } - - val row = LinearLayout(this).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(0, 12.dp(), 0, 12.dp()) - addView(LinearLayout(this@MainActivity).apply { - orientation = LinearLayout.VERTICAL - addView(TextView(this@MainActivity).apply { - text = title - textSize = 15f - typeface = Typeface.DEFAULT_BOLD - setTextColor(color("#0F172A")) - }) - addView(TextView(this@MainActivity).apply { - text = subtitle - textSize = 13f - setTextColor(color("#64748B")) - setPadding(0, 4.dp(), 12.dp(), 0) - }) - }, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)) - addView(switch) - } - - return row to switch - } - - private fun infoRow(label: String, value: String, selectable: Boolean = false): View { - return LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - setPadding(0, 12.dp(), 0, 12.dp()) - addView(TextView(this@MainActivity).apply { - text = label - textSize = 13f - setTextColor(color("#64748B")) - }) - addView(TextView(this@MainActivity).apply { - text = value - textSize = 15f - setTextColor(color("#0F172A")) - setPadding(0, 4.dp(), 0, 0) - setTextIsSelectable(selectable) - }) - addView(divider()) - } - } - - private fun logLevelRow(): View { - return LinearLayout(this).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(0, 12.dp(), 0, 0) - addView(TextView(this@MainActivity).apply { - text = "日志级别" - textSize = 15f - typeface = Typeface.DEFAULT_BOLD - setTextColor(color("#0F172A")) - }, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)) - - logLevelSpinner = Spinner(this@MainActivity).apply { - adapter = ArrayAdapter( - this@MainActivity, - android.R.layout.simple_spinner_dropdown_item, - LogConfig.LEVELS, - ) - setSelection(LogConfig.LEVELS.indexOf(LogConfig.getLevel(this@MainActivity)).coerceAtLeast(0)) - onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - val level = LogConfig.LEVELS[position] - if (level != LogConfig.getLevel(this@MainActivity)) { - LogConfig.setLevel(this@MainActivity, level) - LogStore.append("Log level set to $level; restart service to apply") - } - } - - override fun onNothingSelected(parent: AdapterView<*>?) = Unit - } - } - addView(logLevelSpinner) - } - } - - private fun card(build: LinearLayout.() -> Unit): View { - return LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - setPadding(16.dp(), 16.dp(), 16.dp(), 16.dp()) - background = rounded("#FFFFFF", "#E2E8F0", 10) - elevation = 1.5f.dpFloat() - build() - }.also { - it.layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT, - ).apply { setMargins(0, 0, 0, 14.dp()) } - } - } - - private fun sectionTitle(text: String): View { - return TextView(this).apply { - this.text = text - textSize = 17f - typeface = Typeface.DEFAULT_BOLD - setTextColor(color("#0F172A")) - } - } - - private fun actionButton(text: String, primary: Boolean, action: () -> Unit): Button { - return Button(this).apply { - this.text = text - textSize = 15f - isAllCaps = false - minHeight = 44.dp() - setTextColor(color(if (primary) "#FFFFFF" else "#0F172A")) - background = if (primary) rounded("#2563EB", "#2563EB", 8) else rounded("#FFFFFF", "#CBD5E1", 8) - setOnClickListener { action() } - } - } - - private fun toggleHost() { - when (ServiceStatusStore.snapshot(this).state) { - ServiceStatusStore.STATE_RUNNING -> pauseHost() - ServiceStatusStore.STATE_STOPPED, ServiceStatusStore.STATE_ERROR -> startHost() - } - } - - private fun startHost() { - ServiceStatusStore.setStarting(this) - refreshStatus() - OneKvmService.start(this) - LogStore.append("Start requested from app UI") - pollStatusForAWhile() - } - - private fun pauseHost() { - ServiceStatusStore.setStopping(this) - refreshStatus() - OneKvmService.stop(this) - LogStore.append("Pause requested from app UI") - pollStatusForAWhile() - } - - private fun refreshStatus() { - if (::statusValue.isInitialized) { - statusValue.text = "状态:${hostStatusSummary()}" - } - updateHostActionButton() - } - - private fun hostStatusSummary(): String { - val serviceStatus = ServiceStatusStore.snapshot(this) - if (serviceStatus.state != ServiceStatusStore.STATE_STOPPED) { - return serviceStatus.labelText() - } - - val nativeRunning = runCatching { - NativeBridge.hostStatus().contains("running", ignoreCase = true) - }.getOrDefault(false) - - return if (nativeRunning) "运行中" else "已停止" - } - - private fun reconcilePersistedStatus() { - val serviceStatus = ServiceStatusStore.snapshot(this) - if (serviceStatus.state == ServiceStatusStore.STATE_STOPPED) return - if ( - serviceStatus.state == ServiceStatusStore.STATE_STARTING && - System.currentTimeMillis() - serviceStatus.updatedAt < STARTING_RECONCILE_GRACE_MS - ) { - return - } - - Thread { - val portOpen = isLocalWebPortOpen() - val nativeRunning = runCatching { NativeBridge.hostStatus().contains("running", ignoreCase = true) } - .getOrDefault(false) - if (!portOpen && !nativeRunning) { - ServiceStatusStore.setStopped(this, "服务未运行") - runOnUiThread { refreshStatus() } - } - }.start() - } - - private fun isLocalWebPortOpen(): Boolean { - return runCatching { - Socket().use { socket -> - socket.connect(InetSocketAddress("127.0.0.1", 8080), 250) - } - true - }.getOrDefault(false) - } - - private fun updateHostActionButton() { - if (!::hostActionButton.isInitialized) return - - when (ServiceStatusStore.snapshot(this).state) { - ServiceStatusStore.STATE_STARTING -> setHostActionButton("启动中...", enabled = false, primary = true) - ServiceStatusStore.STATE_RUNNING -> setHostActionButton("停止", enabled = true, primary = false) - ServiceStatusStore.STATE_STOPPING -> setHostActionButton("停止中...", enabled = false, primary = false) - else -> setHostActionButton("启动", enabled = true, primary = true) - } - } - - private fun setHostActionButton(text: String, enabled: Boolean, primary: Boolean) { - hostActionButton.text = text - hostActionButton.isEnabled = enabled - hostActionButton.alpha = if (enabled) 1f else 0.65f - hostActionButton.setTextColor(color(if (primary) "#FFFFFF" else "#0F172A")) - hostActionButton.background = if (primary) { - rounded("#2563EB", "#2563EB", 8) - } else { - rounded("#FFFFFF", "#CBD5E1", 8) - } - } - - private fun pollStatusForAWhile() { - statusPollsRemaining = 20 - statusHandler.removeCallbacks(statusPoller) - statusHandler.postDelayed(statusPoller, STATUS_POLL_INTERVAL_MS) - } - - private fun kernelVersion(): String { - return runCatching { NativeBridge.kernelVersion() } - .getOrElse { "unknown" } - } - - private fun accessAddresses(): String { - val addresses = runCatching { - Collections.list(NetworkInterface.getNetworkInterfaces()) - .filter { it.isUp && !it.isLoopback } - .flatMap { iface -> Collections.list(iface.inetAddresses) } - .filterIsInstance() - .filter { !it.isLoopbackAddress } - .map { "http://${it.hostAddress}:8080" } - .distinct() - }.getOrDefault(emptyList()) - - return (addresses.ifEmpty { listOf("http://127.0.0.1:8080") }).joinToString("\n") - } - - private fun divider(): View { - return View(this).apply { - setBackgroundColor(color("#E2E8F0")) - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - 1, - ).apply { setMargins(0, 0, 0, 0) } - } - } - - private fun matchWrap(): LinearLayout.LayoutParams { - return LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT, - ) - } - - private fun matchButton(): LinearLayout.LayoutParams { - return LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - 48.dp(), - ) - } - - private fun solid(hex: String): GradientDrawable = GradientDrawable().apply { - setColor(color(hex)) - } - - private fun rounded(fill: String, stroke: String, radiusDp: Int): GradientDrawable { - return GradientDrawable().apply { - setColor(color(fill)) - cornerRadius = radiusDp.dpFloat() - setStroke(1.dp(), color(stroke)) - } - } - - private fun color(hex: String): Int = Color.parseColor(hex) - - private fun Int.dp(): Int = (this * resources.displayMetrics.density + 0.5f).toInt() - - private fun Int.dpFloat(): Float = this * resources.displayMetrics.density - - private fun Float.dpFloat(): Float = this * resources.displayMetrics.density - - companion object { - private const val STATUS_POLL_INTERVAL_MS = 500L - private const val STARTING_RECONCILE_GRACE_MS = 15_000L - } -} diff --git a/android/app/src/main/java/cn/one_kvm/androidhost/NativeBridge.kt b/android/app/src/main/java/cn/one_kvm/androidhost/NativeBridge.kt deleted file mode 100644 index 3437057d..00000000 --- a/android/app/src/main/java/cn/one_kvm/androidhost/NativeBridge.kt +++ /dev/null @@ -1,21 +0,0 @@ -package cn.one_kvm.androidhost - -import android.content.Context - -object NativeBridge { - init { - System.loadLibrary("one_kvm_android_bootstrap") - } - - external fun initTlsVerifier(context: Context): Int - - external fun setEnv(name: String, value: String): Int - - external fun startHost(dataDir: String, bindAddress: String, port: Int): String - - external fun stopHost(): String - - external fun hostStatus(): String - - external fun kernelVersion(): String -} diff --git a/android/app/src/main/java/cn/one_kvm/androidhost/OneKvmService.kt b/android/app/src/main/java/cn/one_kvm/androidhost/OneKvmService.kt deleted file mode 100644 index 571389c8..00000000 --- a/android/app/src/main/java/cn/one_kvm/androidhost/OneKvmService.kt +++ /dev/null @@ -1,413 +0,0 @@ -package cn.one_kvm.androidhost - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.IBinder -import java.io.BufferedReader -import java.io.File -import java.io.InputStreamReader -import java.io.InterruptedIOException -import java.util.concurrent.Executors - -class OneKvmService : Service() { - private var rootProcess: Process? = null - private val commandExecutor = Executors.newSingleThreadExecutor { runnable -> - Thread(runnable, "OneKvmServiceCommand") - } - - override fun onCreate() { - super.onCreate() - ensureNotificationChannel() - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - when (intent?.action ?: ACTION_START) { - ACTION_STOP -> { - ServiceStatusStore.setStopping(this) - commandExecutor.execute { - stopHostRuntime() - stopSelfResult(startId) - } - return START_NOT_STICKY - } - ACTION_START -> { - ServiceStatusStore.setStarting(this) - startForegroundCompat(NOTIFICATION_ID, notification("启动中")) - commandExecutor.execute { - val currentState = ServiceStatusStore.snapshot(this).state - if (currentState == ServiceStatusStore.STATE_RUNNING && isPortOpen(8080, 100)) { - return@execute - } - val dataDir = File(getExternalFilesDir(null), "runtime") - if (!dataDir.exists()) dataDir.mkdirs() - val result = startRustHost(dataDir) - if (result.startsWith("Running") && !result.contains("start failed", ignoreCase = true)) { - ServiceStatusStore.setRunning(this, "服务已启动") - notificationManager().notify(NOTIFICATION_ID, notification("运行中")) - } else { - ServiceStatusStore.setError(this, "启动失败") - notificationManager().notify(NOTIFICATION_ID, notification("启动失败")) - } - } - } - } - return START_STICKY - } - - override fun onDestroy() { - stopHostRuntime(updateNotification = false) - commandExecutor.shutdownNow() - ServiceStatusStore.setStopped(this) - super.onDestroy() - } - - override fun onBind(intent: Intent?): IBinder? = null - - private fun notification(state: String): Notification { - val intent = Intent(this, MainActivity::class.java) - val pendingIntent = createContentIntent(intent) - val builder = createNotificationBuilder() - return builder - .setSmallIcon(R.drawable.ic_stat_one_kvm) - .setContentTitle("One-KVM Android Host") - .setContentText(state) - .setContentIntent(pendingIntent) - .setOngoing(true) - .build() - } - - private fun ensureNotificationChannel() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - val channel = NotificationChannel( - CHANNEL_ID, - "One-KVM Host", - NotificationManager.IMPORTANCE_LOW, - ) - notificationManager().createNotificationChannel(channel) - } - - @Suppress("DEPRECATION") - private fun createNotificationBuilder(): Notification.Builder { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Notification.Builder(this, CHANNEL_ID) - } else { - Notification.Builder(this) - } - } - - private fun createContentIntent(intent: Intent): PendingIntent { - val flags = pendingIntentFlags() - return PendingIntent.getActivity(this, 0, intent, flags) - } - - private fun pendingIntentFlags(): Int { - var flags = PendingIntent.FLAG_UPDATE_CURRENT - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - flags = flags or pendingIntentImmutableFlag() - } - return flags - } - - private fun pendingIntentImmutableFlag(): Int { - return try { - PendingIntent::class.java.getField("FLAG_IMMUTABLE").getInt(null) - } catch (_: ReflectiveOperationException) { - 0 - } - } - - private fun notificationManager(): NotificationManager { - return getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - } - - private fun stopHostRuntime(updateNotification: Boolean = true) { - stopRootHost() - NativeBridge.stopHost() - waitForPortRelease(8080, 2_000) - LogStore.flush() - ServiceStatusStore.setStopped(this) - if (updateNotification) { - notificationManager().notify(NOTIFICATION_ID, notification("已停止")) - } - } - - private fun startRustHost(dataDir: File): String { - val logLevel = LogConfig.getLevel(this) - val rustLog = LogConfig.rustLogFilter(logLevel) - val appLogFile = LogStore.defaultLogFile(this) - LogStore.configure(appLogFile) - val rustLogFile = appLogFile - LogStore.append("Starting One-KVM Rust host, data_dir=${dataDir.absolutePath}, log_level=$logLevel") - val executable = extractHostBinary() - return runCatching { - val tlsInit = NativeBridge.initTlsVerifier(this) - if (tlsInit != 0) { - throw IllegalStateException("rustls platform verifier init failed with code $tlsInit") - } - stopRootHost(executable) - clearExistingOtgGadgetsIfEnabled() - startRootHost(executable, dataDir, rustLog, rustLogFile, logLevel) - LogStore.append("Rust host running as root on port 8080") - "Running as root on port 8080" - }.getOrElse { rootError -> - LogStore.append("Root host unavailable: ${rootError.message ?: rootError::class.java.simpleName}") - configureAlsaEnvironment(executable) - NativeBridge.setEnv("RUST_LOG", rustLog) - NativeBridge.setEnv("ONE_KVM_FFMPEG_LOG", ffmpegLogLevel(logLevel)) - NativeBridge.setEnv("ONE_KVM_ANDROID_LOG_FILE", rustLogFile.absolutePath) - val jniResult = NativeBridge.startHost(dataDir.absolutePath, "0.0.0.0", 8080) - LogStore.append("Rust host running in app process on port 8080: $jniResult") - "Running in app process on port 8080 (${rootError.message ?: "root unavailable"}; $jniResult)" - } - } - - private fun clearExistingOtgGadgetsIfEnabled() { - if (!HostSettings.getClearExistingOtg(this)) return - - val command = """ - root=/sys/kernel/config/usb_gadget - [ -d "${'$'}root" ] || exit 0 - for gadget in "${'$'}root"/*; do - [ -d "${'$'}gadget" ] || continue - [ -w "${'$'}gadget/UDC" ] && echo "" > "${'$'}gadget/UDC" 2>/dev/null || true - find "${'$'}gadget/configs" -type l -delete 2>/dev/null || true - rm -rf "${'$'}gadget" 2>/dev/null || true - done - """.trimIndent() - - runCatching { - ProcessBuilder("/system/xbin/su", "0", "sh", "-c", command) - .redirectErrorStream(true) - .start() - .waitFor() - }.onSuccess { exit -> - LogStore.append("Existing OTG gadget cleanup finished with exit code $exit") - }.onFailure { err -> - LogStore.append("Existing OTG gadget cleanup failed: ${err.message ?: err::class.java.simpleName}") - } - } - - private fun configureAlsaEnvironment(executable: File) { - val binDir = executable.parentFile - ?: throw IllegalStateException("host binary has no parent directory") - val alsaConfigDir = File(binDir, "alsa") - val alsaConfigPath = File(alsaConfigDir, "alsa.conf") - NativeBridge.setEnv("ALSA_CONFIG_DIR", alsaConfigDir.absolutePath) - NativeBridge.setEnv("ALSA_CONFIG_PATH", alsaConfigPath.absolutePath) - } - - private fun extractHostBinary(): File { - val abi = Build.SUPPORTED_ABIS.firstOrNull { it == "arm64-v8a" || it == "armeabi-v7a" } - ?: throw IllegalStateException("unsupported ABI: ${Build.SUPPORTED_ABIS.joinToString()}") - val binDir = File(filesDir, "bin/$abi") - val target = File(binDir, "one-kvm-android-host") - copyAssetIfChanged("bin/$abi/one-kvm-android-host", target) - copyAssetIfChanged("bin/$abi/libc++_shared.so", File(binDir, "libc++_shared.so")) - copyAssetIfChanged("bin/$abi/libasound.so", File(binDir, "libasound.so")) - copyAssetIfChanged("bin/$abi/libopus.so", File(binDir, "libopus.so")) - copyAssetDirectoryIfChanged("bin/$abi/alsa", File(binDir, "alsa")) - if (!target.setExecutable(true, false)) { - throw IllegalStateException("cannot mark host binary executable") - } - return target - } - - private fun copyAssetIfChanged(assetPath: String, target: File) { - val stamp = File(target.parentFile, "${target.name}.stamp") - @Suppress("DEPRECATION") - val packageInfo = packageManager.getPackageInfo(packageName, 0) - val expectedStamp = "${packageInfo.lastUpdateTime}:$assetPath" - if (target.exists() && stamp.exists() && stamp.readText() == expectedStamp) return - target.parentFile?.mkdirs() - assets.open(assetPath).use { input -> - target.outputStream().use { output -> input.copyTo(output) } - } - stamp.writeText(expectedStamp) - } - - private fun copyAssetDirectoryIfChanged(assetDir: String, targetDir: File) { - @Suppress("DEPRECATION") - val packageInfo = packageManager.getPackageInfo(packageName, 0) - val stamp = File(targetDir, ".stamp") - val expectedStamp = "${packageInfo.lastUpdateTime}:$assetDir" - if (targetDir.exists() && stamp.exists() && stamp.readText() == expectedStamp) return - if (targetDir.exists()) targetDir.deleteRecursively() - copyAssetDirectory(assetDir, targetDir) - stamp.writeText(expectedStamp) - } - - private fun copyAssetDirectory(assetDir: String, targetDir: File) { - targetDir.mkdirs() - val children = assets.list(assetDir)?.filter { it.isNotEmpty() }.orEmpty() - for (child in children) { - val childAsset = "$assetDir/$child" - val childTarget = File(targetDir, child) - val grandChildren = assets.list(childAsset)?.filter { it.isNotEmpty() }.orEmpty() - if (grandChildren.isEmpty()) { - copyAssetIfChanged(childAsset, childTarget) - } else { - copyAssetDirectory(childAsset, childTarget) - } - } - } - - private fun startRootHost( - executable: File, - dataDir: File, - rustLog: String, - rustLogFile: File, - logLevel: String, - ) { - stopRootHost(executable) - waitForPortRelease(8080, 2_000) - val libDir = executable.parentFile?.absolutePath - ?: throw IllegalStateException("host binary has no parent directory") - val alsaConfigDir = File(executable.parentFile, "alsa") - val alsaConfigPath = File(alsaConfigDir, "alsa.conf") - val command = - "export LD_LIBRARY_PATH=${shellQuote(libDir)}:\${LD_LIBRARY_PATH:-}; " + - "export ALSA_CONFIG_DIR=${shellQuote(alsaConfigDir.absolutePath)}; " + - "export ALSA_CONFIG_PATH=${shellQuote(alsaConfigPath.absolutePath)}; " + - "export RUST_LOG=${shellQuote(rustLog)}; " + - "export ONE_KVM_FFMPEG_LOG=${shellQuote(ffmpegLogLevel(logLevel))}; " + - "export ONE_KVM_ANDROID_LOG_FILE=${shellQuote(rustLogFile.absolutePath)}; " + - "${shellQuote(executable.absolutePath)} ${shellQuote(dataDir.absolutePath)} 0.0.0.0 8080" - val process = ProcessBuilder("/system/xbin/su", "0", "sh", "-c", command) - .redirectErrorStream(true) - .start() - rootProcess = process - - Thread { - val readError = runCatching { - BufferedReader(InputStreamReader(process.inputStream)).useLines { lines -> - lines.forEach { - android.util.Log.i("OneKvmService", it) - } - } - }.exceptionOrNull() - if (readError != null && readError !is InterruptedIOException) { - android.util.Log.w("OneKvmService", "Root host log reader stopped", readError) - LogStore.append("Root host log reader stopped: ${readError.message ?: readError::class.java.simpleName}") - } - val exit = runCatching { process.waitFor() }.getOrNull() - if (rootProcess === process && exit != null) { - rootProcess = null - ServiceStatusStore.setError(this, "Root host exited with code $exit") - } - }.start() - - Thread.sleep(500) - val exit = runCatching { process.exitValue() }.getOrNull() - if (exit != null) { - rootProcess = null - throw IllegalStateException("root host exited immediately: $exit") - } - } - - private fun startForegroundCompat(id: Int, notification: Notification) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val invoked = runCatching { - val method = Service::class.java.getMethod( - "startForeground", - Int::class.javaPrimitiveType, - Notification::class.java, - Int::class.javaPrimitiveType, - ) - method.invoke(this, id, notification, foregroundServiceTypeConnectedDevice()) - }.isSuccess - if (invoked) return - } - super.startForeground(id, notification) - } - - private fun foregroundServiceTypeConnectedDevice(): Int { - return try { - Service::class.java.getField("FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE").getInt(null) - } catch (_: ReflectiveOperationException) { - 0 - } - } - - private fun stopRootHost(executable: File? = null) { - rootProcess?.destroy() - rootProcess = null - stopRootHostProcess(executable) - } - - private fun stopRootHostProcess(executable: File? = null) { - val command = buildString { - append("pkill -TERM -f '[o]ne-kvm-android-host' 2>/dev/null || true; ") - append("for pid in $(pidof one-kvm-android-host 2>/dev/null); do kill -TERM \"${'$'}pid\" 2>/dev/null || true; done; ") - append("sleep 0.2; ") - append("pkill -KILL -f '[o]ne-kvm-android-host' 2>/dev/null || true; ") - append("for pid in $(pidof one-kvm-android-host 2>/dev/null); do kill -KILL \"${'$'}pid\" 2>/dev/null || true; done; ") - } - - runCatching { - ProcessBuilder("/system/xbin/su", "0", "sh", "-c", command) - .redirectErrorStream(true) - .start() - .waitFor() - }.onFailure { err -> - LogStore.append("Failed to stop stale root host: ${err.message ?: err::class.java.simpleName}") - } - } - - private fun waitForPortRelease(port: Int, timeoutMs: Long) { - val deadline = System.currentTimeMillis() + timeoutMs - while (System.currentTimeMillis() < deadline) { - val inUse = isPortOpen(port, 100) - if (!inUse) return - Thread.sleep(100) - } - } - - private fun isPortOpen(port: Int, timeoutMs: Int): Boolean { - return runCatching { - java.net.Socket().use { socket -> - socket.connect(java.net.InetSocketAddress("127.0.0.1", port), timeoutMs) - } - true - }.getOrDefault(false) - } - - private fun shellQuote(value: String): String { - return "'" + value.replace("'", "'\\''") + "'" - } - - private fun ffmpegLogLevel(level: String): String { - return when (level) { - "trace" -> "trace" - "debug" -> "debug" - "info" -> "info" - "warn" -> "warning" - else -> "error" - } - } - - companion object { - private const val CHANNEL_ID = "one_kvm_host" - private const val NOTIFICATION_ID = 1001 - const val ACTION_START = "cn.one_kvm.androidhost.START" - const val ACTION_STOP = "cn.one_kvm.androidhost.STOP" - - fun start(context: Context) { - val intent = Intent(context, OneKvmService::class.java).setAction(ACTION_START) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } - } - - fun stop(context: Context) { - context.startService(Intent(context, OneKvmService::class.java).setAction(ACTION_STOP)) - } - } -} diff --git a/android/app/src/main/java/cn/one_kvm/androidhost/ServiceStatusStore.kt b/android/app/src/main/java/cn/one_kvm/androidhost/ServiceStatusStore.kt deleted file mode 100644 index d3b819ce..00000000 --- a/android/app/src/main/java/cn/one_kvm/androidhost/ServiceStatusStore.kt +++ /dev/null @@ -1,75 +0,0 @@ -package cn.one_kvm.androidhost - -import android.content.Context - -object ServiceStatusStore { - private const val PREFS = "one_kvm_android_status" - private const val KEY_STATE = "state" - private const val KEY_MESSAGE = "message" - private const val KEY_UPDATED_AT = "updated_at" - - const val STATE_STOPPED = "stopped" - const val STATE_STARTING = "starting" - const val STATE_RUNNING = "running" - const val STATE_STOPPING = "stopping" - const val STATE_ERROR = "error" - - data class Snapshot( - val state: String, - val message: String, - val updatedAt: Long, - ) { - fun labelText(): String { - return when (state) { - STATE_STARTING -> "启动中" - STATE_RUNNING -> "运行中" - STATE_STOPPING -> "停止中" - STATE_ERROR -> "错误" - else -> "已停止" - } - } - - fun displayText(): String { - val label = labelText() - return if (message.isBlank()) label else "$label:$message" - } - } - - fun setStarting(context: Context, message: String = "正在启动服务") { - write(context, STATE_STARTING, message) - } - - fun setRunning(context: Context, message: String) { - write(context, STATE_RUNNING, message) - } - - fun setStopping(context: Context, message: String = "正在停止服务") { - write(context, STATE_STOPPING, message) - } - - fun setStopped(context: Context, message: String = "服务已停止") { - write(context, STATE_STOPPED, message) - } - - fun setError(context: Context, message: String) { - write(context, STATE_ERROR, message) - } - - fun snapshot(context: Context): Snapshot { - val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - return Snapshot( - state = prefs.getString(KEY_STATE, STATE_STOPPED) ?: STATE_STOPPED, - message = prefs.getString(KEY_MESSAGE, "") ?: "", - updatedAt = prefs.getLong(KEY_UPDATED_AT, 0L), - ) - } - - private fun write(context: Context, state: String, message: String) { - context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .edit() - .putString(KEY_STATE, state) - .putString(KEY_MESSAGE, message) - .putLong(KEY_UPDATED_AT, System.currentTimeMillis()) - .apply() - } -} diff --git a/android/app/src/main/res/drawable/ic_launcher_one_kvm.xml b/android/app/src/main/res/drawable/ic_launcher_one_kvm.xml deleted file mode 100644 index 70e6565f..00000000 --- a/android/app/src/main/res/drawable/ic_launcher_one_kvm.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/ic_stat_one_kvm.xml b/android/app/src/main/res/drawable/ic_stat_one_kvm.xml deleted file mode 100644 index 222fdd18..00000000 --- a/android/app/src/main/res/drawable/ic_stat_one_kvm.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml deleted file mode 100644 index f502aaae..00000000 --- a/android/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - One-KVM Android Host - diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml deleted file mode 100644 index d8da6140..00000000 --- a/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/build.gradle.kts b/android/build.gradle.kts deleted file mode 100644 index 6eda0fca..00000000 --- a/android/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id("com.android.application") version "9.0.0" apply false -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index bae90346..00000000 --- a/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -android.useAndroidX=true -android.nonTransitiveRClass=true -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 8bdaf60c..00000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 2e111328..00000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew deleted file mode 100755 index ef07e016..00000000 --- a/android/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH="\\\"\\\"" - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index 5eed7ee8..00000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/native/Cargo.toml b/android/native/Cargo.toml deleted file mode 100644 index fb2c9f72..00000000 --- a/android/native/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "one-kvm-android-bootstrap" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -name = "one_kvm_android_bootstrap" -crate-type = ["cdylib"] - -[[bin]] -name = "one-kvm-android-host" -path = "src/bin/one-kvm-android-host.rs" - -[dependencies] -jni = "0.22.4" -one-kvm = { path = "../..", default-features = false, features = ["android", "android-mediacodec"] } -rustls-platform-verifier = "0.7" - -[features] -android-mediacodec = ["one-kvm/android-mediacodec"] diff --git a/android/native/src/bin/one-kvm-android-host.rs b/android/native/src/bin/one-kvm-android-host.rs deleted file mode 100644 index 90daea46..00000000 --- a/android/native/src/bin/one-kvm-android-host.rs +++ /dev/null @@ -1,24 +0,0 @@ -use one_kvm::runtime::android::{self, AndroidRuntimeConfig}; - -fn main() { - let mut args = std::env::args().skip(1); - let data_dir = args - .next() - .unwrap_or_else(|| "/data/local/tmp/one-kvm".to_string()); - let bind_address = args.next().unwrap_or_else(|| "0.0.0.0".to_string()); - let port = args - .next() - .and_then(|value| value.parse::().ok()) - .unwrap_or(8080); - - one_kvm::runtime::android::init_rustls_provider(); - - if let Err(err) = android::run_foreground(AndroidRuntimeConfig { - data_dir, - bind_address, - port, - }) { - eprintln!("one-kvm android host failed: {err}"); - std::process::exit(1); - } -} diff --git a/android/native/src/lib.rs b/android/native/src/lib.rs deleted file mode 100644 index 0c23691a..00000000 --- a/android/native/src/lib.rs +++ /dev/null @@ -1,182 +0,0 @@ -use jni::errors::{ErrorPolicy, ThrowRuntimeExAndDefault}; -use jni::objects::{JClass, JObject, JString}; -use jni::sys::{jint, jstring}; -use jni::{Env, EnvOutcome, EnvUnowned}; -use one_kvm::runtime::android::{self, AndroidRuntimeConfig}; - -#[derive(Debug)] -struct BridgeError(String); - -impl From for BridgeError { - fn from(err: jni::errors::Error) -> Self { - Self(err.to_string()) - } -} - -impl From for BridgeError { - fn from(err: String) -> Self { - Self(err) - } -} - -impl std::fmt::Display for BridgeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0) - } -} - -#[derive(Debug, Default)] -struct StatusPolicy; - -impl ErrorPolicy for StatusPolicy { - type Captures<'unowned_env_local: 'native_method, 'native_method> = (); - - fn on_error<'unowned_env_local: 'native_method, 'native_method>( - _env: &mut Env<'unowned_env_local>, - _cap: &mut Self::Captures<'unowned_env_local, 'native_method>, - _err: BridgeError, - ) -> jni::errors::Result { - Ok(-1) - } - - fn on_panic<'unowned_env_local: 'native_method, 'native_method>( - _env: &mut Env<'unowned_env_local>, - _cap: &mut Self::Captures<'unowned_env_local, 'native_method>, - _payload: Box, - ) -> jni::errors::Result { - Ok(-1) - } -} - -#[derive(Debug, Default)] -struct StringResultPolicy; - -impl ErrorPolicy for StringResultPolicy { - type Captures<'unowned_env_local: 'native_method, 'native_method> = (); - - fn on_error<'unowned_env_local: 'native_method, 'native_method>( - _env: &mut Env<'unowned_env_local>, - _cap: &mut Self::Captures<'unowned_env_local, 'native_method>, - err: BridgeError, - ) -> jni::errors::Result { - Ok(format!("start failed: {err}")) - } - - fn on_panic<'unowned_env_local: 'native_method, 'native_method>( - _env: &mut Env<'unowned_env_local>, - _cap: &mut Self::Captures<'unowned_env_local, 'native_method>, - _payload: Box, - ) -> jni::errors::Result { - Ok("start failed: panic in native bridge".to_string()) - } -} - -#[no_mangle] -pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_setEnv<'local>( - mut env: EnvUnowned<'local>, - _class: JClass<'local>, - name: JString<'local>, - value: JString<'local>, -) -> jint { - let outcome: EnvOutcome<'local, jint, BridgeError> = env.with_env_no_catch(|env| { - let name = name - .try_to_string(env) - .map_err(|err| BridgeError(format!("invalid env name: {err}")))?; - let value = value - .try_to_string(env) - .map_err(|err| BridgeError(format!("invalid env value: {err}")))?; - if name.contains('\0') || value.contains('\0') { - return Err(BridgeError("env contains NUL".to_string())); - } - std::env::set_var(name, value); - Ok(0) - }); - - outcome.resolve_with::(|| ()) -} - -#[no_mangle] -pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_initTlsVerifier<'local>( - mut env: EnvUnowned<'local>, - _class: JClass<'local>, - context: JObject<'local>, -) -> jint { - let outcome: EnvOutcome<'local, jint, BridgeError> = - env.with_env_no_catch(|env| init_tls_verifier(env, context)); - - outcome.resolve_with::(|| ()) -} - -#[cfg(target_os = "android")] -fn init_tls_verifier(env: &mut Env<'_>, context: JObject<'_>) -> Result { - rustls_platform_verifier::android::init_with_env(env, context) - .map_err(|err| BridgeError(format!("failed to initialize rustls platform verifier: {err}")))?; - Ok(0) -} - -#[cfg(not(target_os = "android"))] -fn init_tls_verifier(_env: &mut Env<'_>, _context: JObject<'_>) -> Result { - Ok(0) -} - -#[no_mangle] -pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_startHost<'local>( - mut env: EnvUnowned<'local>, - _class: JClass<'local>, - data_dir: JString<'local>, - bind_address: JString<'local>, - port: i32, -) -> jstring { - let outcome: EnvOutcome<'local, String, BridgeError> = env.with_env_no_catch(|env| { - let data_dir = data_dir - .try_to_string(env) - .map_err(|err| BridgeError(format!("invalid data dir: {err}")))?; - let bind_address = bind_address - .try_to_string(env) - .map_err(|err| BridgeError(format!("invalid bind address: {err}")))?; - let port = u16::try_from(port).map_err(|_| BridgeError("invalid port".to_string()))?; - - android::start(AndroidRuntimeConfig { - data_dir, - bind_address, - port, - }) - .map_err(BridgeError) - }); - - let result = outcome.resolve_with::(|| ()); - - env.with_env_no_catch(|env| env.new_string(result)) - .resolve_with::(|| ()) - .into_raw() -} - -#[no_mangle] -pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_stopHost<'local>( - mut env: EnvUnowned<'local>, - _class: JClass<'local>, -) -> jstring { - env.with_env_no_catch(|env| env.new_string(android::stop())) - .resolve_with::(|| ()) - .into_raw() -} - -#[no_mangle] -pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_hostStatus<'local>( - mut env: EnvUnowned<'local>, - _class: JClass<'local>, -) -> jstring { - env.with_env_no_catch(|env| env.new_string(android::status())) - .resolve_with::(|| ()) - .into_raw() -} - -#[no_mangle] -pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_kernelVersion<'local>( - mut env: EnvUnowned<'local>, - _class: JClass<'local>, -) -> jstring { - env.with_env_no_catch(|env| env.new_string(env!("CARGO_PKG_VERSION"))) - .resolve_with::(|| ()) - .into_raw() -} diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts deleted file mode 100644 index ab5a160e..00000000 --- a/android/settings.gradle.kts +++ /dev/null @@ -1,39 +0,0 @@ -pluginManagement { - fun isEnabled(value: String?): Boolean = when (value?.lowercase()) { - "1", "true", "yes", "on" -> true - else -> false - } - val mirrorAcceleration = isEnabled(System.getenv("CHINAMIRRO")) - - repositories { - if (mirrorAcceleration) { - maven("https://maven.aliyun.com/repository/google") - maven("https://maven.aliyun.com/repository/public") - maven("https://maven.aliyun.com/repository/gradle-plugin") - } - google() - mavenCentral() - gradlePluginPortal() - } -} - -dependencyResolutionManagement { - fun isEnabled(value: String?): Boolean = when (value?.lowercase()) { - "1", "true", "yes", "on" -> true - else -> false - } - val mirrorAcceleration = isEnabled(System.getenv("CHINAMIRRO")) - - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - if (mirrorAcceleration) { - maven("https://maven.aliyun.com/repository/google") - maven("https://maven.aliyun.com/repository/public") - } - google() - mavenCentral() - } -} - -rootProject.name = "OneKvmAndroidHost" -include(":app") diff --git a/build/build-android.sh b/build/build-android.sh deleted file mode 100755 index e97a8ee6..00000000 --- a/build/build-android.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -# Build Android APKs using the Docker build image. -# Usage: ./build/build-android.sh [arm64|armv7|all|help] - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -DOCKERFILE="$PROJECT_ROOT/build/cross/Dockerfile.android" -IMAGE_NAME="${ONE_KVM_ANDROID_DOCKER_IMAGE:-one-kvm-android-build:cn}" - -fail() { - echo "Error: $*" >&2 - exit 1 -} - -build_android() { - local arch="$1" - local docker_build_args=() - local gradle_distribution_url="${ONE_KVM_GRADLE_DISTRIBUTION_URL:-}" - local gradle_distribution_url_cn="${ONE_KVM_GRADLE_DISTRIBUTION_URL_CN:-https://mirrors.cloud.tencent.com/gradle/gradle-9.1.0-bin.zip}" - local gradle_network_timeout="${ONE_KVM_GRADLE_NETWORK_TIMEOUT:-120000}" - - if [[ "${CHINAMIRRO:-}" == "1" ]]; then - docker_build_args+=("--build-arg" "CHINAMIRRO=1") - docker_build_args+=("--build-arg" "DEBIAN_IMAGE=${DEBIAN_IMAGE:-docker.1ms.run/library/debian:11}") - docker_build_args+=("--build-arg" "RUSTUP_DIST_SERVER_CN=${RUSTUP_DIST_SERVER_CN:-https://rsproxy.cn}") - docker_build_args+=("--build-arg" "RUSTUP_UPDATE_ROOT_CN=${RUSTUP_UPDATE_ROOT_CN:-https://rsproxy.cn/rustup}") - docker_build_args+=("--build-arg" "CARGO_INDEX_CN=${CARGO_INDEX_CN:-https://rsproxy.cn/crates.io-index}") - docker_build_args+=("--build-arg" "CARGO_REGISTRY_CN=${CARGO_REGISTRY_CN:-sparse+https://rsproxy.cn/index/}") - docker_build_args+=("--build-arg" "MAVEN_REPOSITORY_CN=${MAVEN_REPOSITORY_CN:-https://maven.aliyun.com/repository/public}") - docker_build_args+=("--build-arg" "GOOGLE_MAVEN_REPOSITORY_CN=${GOOGLE_MAVEN_REPOSITORY_CN:-https://maven.aliyun.com/repository/google}") - docker_build_args+=("--build-arg" "GRADLE_PLUGIN_REPOSITORY_CN=${GRADLE_PLUGIN_REPOSITORY_CN:-https://maven.aliyun.com/repository/gradle-plugin}") - docker_build_args+=("--build-arg" "GRADLE_DISTRIBUTION_URL_CN=$gradle_distribution_url_cn") - if [[ -z "$gradle_distribution_url" ]]; then - gradle_distribution_url="$gradle_distribution_url_cn" - fi - fi - - if [[ "${ONE_KVM_ANDROID_SKIP_DOCKER_BUILD:-0}" == "1" ]]; then - echo "=== Skipping Android image build: $IMAGE_NAME ===" - else - echo "=== Building Android image: $IMAGE_NAME ===" - docker build \ - -f "$DOCKERFILE" \ - -t "$IMAGE_NAME" \ - "${docker_build_args[@]}" \ - "$PROJECT_ROOT/build/cross" - fi - - echo "=== Building Android APK: $arch ===" - docker run --rm \ - -v "$PROJECT_ROOT:/workspace" \ - -w /workspace \ - -e "CHINAMIRRO=${CHINAMIRRO:-0}" \ - -e "GH_PROXY=${GH_PROXY:-https://gh-proxy.com}" \ - -e "ONE_KVM_GRADLE_DISTRIBUTION_URL=$gradle_distribution_url" \ - -e "ONE_KVM_GRADLE_DISTRIBUTION_URL_CN=$gradle_distribution_url_cn" \ - -e "ONE_KVM_GRADLE_NETWORK_TIMEOUT=$gradle_network_timeout" \ - "$IMAGE_NAME" \ - "$arch" -} - -[[ -f "$DOCKERFILE" ]] || fail "Android Dockerfile not found: $DOCKERFILE" -command -v docker >/dev/null 2>&1 || fail "docker is required" - -case "${1:-all}" in -all) - build_android all - ;; -arm64) - build_android arm64 - ;; -armv7) - build_android armv7 - ;; -help | --help | -h) - cat <<'EOF' -Usage: build/build-android.sh [arch|help] - -Commands: - all (default) Build arm64 and armv7 APKs - arm64 Build only arm64 APK - armv7 Build only ARMv7 APK - help Show this help - -Examples: - build/build-android.sh - build/build-android.sh arm64 - CHINAMIRRO=1 build/build-android.sh all - CHINAMIRRO=1 ONE_KVM_GRADLE_DISTRIBUTION_URL=https://mirrors.aliyun.com/macports/distfiles/gradle/gradle-9.1.0-bin.zip build/build-android.sh all - -Environment: - ONE_KVM_ANDROID_SKIP_DOCKER_BUILD=1 Reuse an already loaded Docker image - -APK output: - target/android/one-kvm__.apk -EOF - ;; -*) - fail "Unknown argument: $1" - ;; -esac diff --git a/build/cross/Dockerfile.android b/build/cross/Dockerfile.android deleted file mode 100644 index a8932424..00000000 --- a/build/cross/Dockerfile.android +++ /dev/null @@ -1,319 +0,0 @@ -# Android build image for One-KVM -# Based on Debian 11 for stable toolchain/runtime compatibility - -ARG DEBIAN_IMAGE=debian:11 -FROM ${DEBIAN_IMAGE} - -ARG CHINAMIRRO=0 -ARG ANDROID_SDK_ROOT=/root/android-sdk -ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708_latest -ARG ANDROID_NDK_VERSION=27.3.13750724 -ARG ANDROID_PLATFORM=36 -ARG ANDROID_BUILD_TOOLS=36.0.0 -ARG CARGO_NDK_VERSION=4.1.2 -ARG RUSTUP_DIST_SERVER_CN=https://rsproxy.cn -ARG RUSTUP_UPDATE_ROOT_CN=https://rsproxy.cn/rustup -ARG CARGO_INDEX_CN=https://rsproxy.cn/crates.io-index -ARG CARGO_REGISTRY_CN=sparse+https://rsproxy.cn/index/ -ARG MAVEN_REPOSITORY_CN=https://maven.aliyun.com/repository/public -ARG GOOGLE_MAVEN_REPOSITORY_CN=https://maven.aliyun.com/repository/google -ARG GRADLE_PLUGIN_REPOSITORY_CN=https://maven.aliyun.com/repository/gradle-plugin -ARG GRADLE_DISTRIBUTION_URL_CN=https://mirrors.cloud.tencent.com/gradle/gradle-9.1.0-bin.zip -ARG ANDROID_CMDLINE_TOOLS_URL= - -ENV DEBIAN_FRONTEND=noninteractive -ENV ANDROID_HOME=${ANDROID_SDK_ROOT} -ENV ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT} -ENV ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION} -ENV ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION} -ENV ANDROID_BUILD_TOOLS=${ANDROID_BUILD_TOOLS} -ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 -ENV PATH=/root/.cargo/bin:${PATH} -ENV ONE_KVM_GRADLE_DISTRIBUTION_URL_CN=${GRADLE_DISTRIBUTION_URL_CN} - -RUN if [ "$CHINAMIRRO" = "1" ]; then \ - sed -i -E \ - -e 's|http://deb.debian.org/debian([[:space:]])|http://mirrors.tuna.tsinghua.edu.cn/debian\1|g' \ - /etc/apt/sources.list; \ - fi - -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - wget \ - bzip2 \ - unzip \ - zip \ - git \ - bash \ - build-essential \ - pkg-config \ - cmake \ - ninja-build \ - autoconf \ - automake \ - libtool \ - nasm \ - yasm \ - python3 \ - openjdk-17-jdk-headless \ - libstdc++6 \ - && rm -rf /var/lib/apt/lists/* - -RUN if [ "$CHINAMIRRO" = "1" ]; then \ - export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \ - export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \ - mkdir -p /root/.cargo; \ - printf '%s\n' \ - '[source.crates-io]' \ - "replace-with = 'rsproxy-sparse'" \ - '[source.rsproxy]' \ - "registry = '${CARGO_INDEX_CN}'" \ - '[source.rsproxy-sparse]' \ - "registry = '${CARGO_REGISTRY_CN}'" \ - '[registries.rsproxy]' \ - "index = '${CARGO_INDEX_CN}'" \ - '[net]' \ - 'git-fetch-with-cli = true' \ - > /root/.cargo/config.toml; \ - fi \ - && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \ - && cargo install cargo-ndk --version ${CARGO_NDK_VERSION} --locked \ - && rustup target add armv7-linux-androideabi aarch64-linux-android - -RUN mkdir -p /opt/android-cmdline-tools \ - && cd /tmp \ - && if [ -n "$ANDROID_CMDLINE_TOOLS_URL" ]; then \ - wget -q "$ANDROID_CMDLINE_TOOLS_URL" -O cmdline-tools.zip; \ - else \ - wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}.zip -O cmdline-tools.zip; \ - fi \ - && unzip -q cmdline-tools.zip -d /opt/android-cmdline-tools \ - && mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools/latest \ - && mv /opt/android-cmdline-tools/cmdline-tools/* ${ANDROID_SDK_ROOT}/cmdline-tools/latest/ \ - && rm -rf /tmp/cmdline-tools.zip /opt/android-cmdline-tools - -RUN mkdir -p ${ANDROID_SDK_ROOT}/licenses \ - && yes | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} --licenses >/dev/null \ - && ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} \ - "platform-tools" \ - "platforms;android-${ANDROID_PLATFORM}" \ - "build-tools;${ANDROID_BUILD_TOOLS}" \ - "ndk;${ANDROID_NDK_VERSION}" \ - "cmake;3.22.1" \ - && mkdir -p ${ANDROID_NDK_HOME} - -RUN if [ "$CHINAMIRRO" = "1" ]; then \ - mkdir -p /root/.gradle; \ - printf '%s\n' \ - "beforeSettings { settings ->" \ - " settings.pluginManagement.repositories.maven { url = uri('${GOOGLE_MAVEN_REPOSITORY_CN}') }" \ - " settings.pluginManagement.repositories.maven { url = uri('${MAVEN_REPOSITORY_CN}') }" \ - " settings.pluginManagement.repositories.maven { url = uri('${GRADLE_PLUGIN_REPOSITORY_CN}') }" \ - " settings.dependencyResolutionManagement.repositories.maven { url = uri('${GOOGLE_MAVEN_REPOSITORY_CN}') }" \ - " settings.dependencyResolutionManagement.repositories.maven { url = uri('${MAVEN_REPOSITORY_CN}') }" \ - "}" \ - "allprojects {" \ - " buildscript.repositories.maven { url = uri('${GOOGLE_MAVEN_REPOSITORY_CN}') }" \ - " buildscript.repositories.maven { url = uri('${MAVEN_REPOSITORY_CN}') }" \ - "}" \ - > /root/.gradle/init.gradle; \ - fi - -RUN apt-get update && apt-get install -y --no-install-recommends \ - libclang-dev \ - llvm \ - && rm -rf /var/lib/apt/lists/* - -ENV LIBCLANG_PATH=/usr/lib/llvm-11/lib - -RUN printf '%s\n' \ - '#!/usr/bin/env bash' \ - 'set -euo pipefail' \ - '' \ - 'PROJECT_ROOT="${ONE_KVM_ANDROID_PROJECT_ROOT:-/workspace}"' \ - 'ANDROID_DIR="${PROJECT_ROOT}/android"' \ - 'BUILD_TYPE="release"' \ - 'ARCH="${1:-all}"' \ - 'FFMPEG_ROOT="${ONE_KVM_ANDROID_FFMPEG_ROOT:-${PROJECT_ROOT}/dist/android-ffmpeg-mediacodec}"' \ - 'OUTPUT_DIR="${PROJECT_ROOT}/target/android"' \ - 'SIGNING_DIR="${PROJECT_ROOT}/target/android-signing"' \ - 'KEYSTORE_PATH="${SIGNING_DIR}/one-kvm-release.jks"' \ - 'KEY_ALIAS="one-kvm-release"' \ - 'KEY_PASSWORD="one-kvm-release"' \ - 'ANDROID_BUILD_TOOLS_DIR="${ANDROID_SDK_ROOT}/build-tools/${ANDROID_BUILD_TOOLS}"' \ - 'WRAPPER_PROPERTIES="$ANDROID_DIR/gradle/wrapper/gradle-wrapper.properties"' \ - 'GRADLE_DISTRIBUTION_URL="${ONE_KVM_GRADLE_DISTRIBUTION_URL:-}"' \ - 'GRADLE_DISTRIBUTION_URL_CN="${ONE_KVM_GRADLE_DISTRIBUTION_URL_CN:-https://mirrors.cloud.tencent.com/gradle/gradle-9.1.0-bin.zip}"' \ - 'GRADLE_NETWORK_TIMEOUT="${ONE_KVM_GRADLE_NETWORK_TIMEOUT:-120000}"' \ - '' \ - 'usage() {' \ - ' cat <_.apk' \ - 'EOF' \ - '}' \ - '' \ - 'fail() {' \ - ' echo "Error: $*" >&2' \ - ' exit 1' \ - '}' \ - '' \ - 'read_project_version() {' \ - ' local version' \ - ' version="$(awk -F "\"" '"'"'/^version[[:space:]]*=/ { print $2; exit }'"'"' "$PROJECT_ROOT/Cargo.toml")"' \ - ' [[ -n "$version" ]] || fail "Failed to resolve version from $PROJECT_ROOT/Cargo.toml"' \ - ' printf "%s\n" "$version"' \ - '}' \ - '' \ - 'copy_apks() {' \ - ' local flavor="$1"' \ - ' local src_dir="$ANDROID_DIR/app/build/outputs/apk/$flavor/$BUILD_TYPE"' \ - ' local found=0' \ - ' mkdir -p "$OUTPUT_DIR"' \ - ' for apk in "$src_dir"/*.apk; do' \ - ' [[ -f "$apk" ]] || continue' \ - ' sign_apk "$apk" "$OUTPUT_DIR/one-kvm_${PROJECT_VERSION}_${flavor}.apk"' \ - ' found=1' \ - ' done' \ - ' [[ "$found" == "1" ]] || fail "No APK files found in: $src_dir"' \ - '}' \ - '' \ - 'ensure_keystore() {' \ - ' if [[ -f "$KEYSTORE_PATH" ]]; then' \ - ' return' \ - ' fi' \ - ' mkdir -p "$SIGNING_DIR"' \ - ' keytool -genkeypair -noprompt -keystore "$KEYSTORE_PATH" -storetype PKCS12 -alias "$KEY_ALIAS" -keyalg RSA -keysize 2048 -validity 10000 -storepass "$KEY_PASSWORD" -keypass "$KEY_PASSWORD" -dname "CN=One-KVM, OU=One-KVM, O=One-KVM, L=Local, S=Local, C=US" >/dev/null' \ - '}' \ - '' \ - 'sign_apk() {' \ - ' local input_apk="$1"' \ - ' local output_apk="$2"' \ - ' local aligned_apk' \ - ' aligned_apk="$(mktemp --suffix=.apk)"' \ - ' "$ANDROID_BUILD_TOOLS_DIR/zipalign" -f -p 4 "$input_apk" "$aligned_apk"' \ - ' "$ANDROID_BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE_PATH" --ks-key-alias "$KEY_ALIAS" --ks-pass "pass:$KEY_PASSWORD" --key-pass "pass:$KEY_PASSWORD" --out "$output_apk" "$aligned_apk"' \ - ' "$ANDROID_BUILD_TOOLS_DIR/apksigner" verify --verbose "$output_apk" >/dev/null' \ - ' rm -f "$aligned_apk"' \ - '}' \ - '' \ - 'cd "$PROJECT_ROOT"' \ - '' \ - 'case "$ARCH" in' \ - 'help | --help | -h)' \ - ' usage' \ - ' exit 0' \ - ' ;;' \ - 'esac' \ - '' \ - '[[ -d "$ANDROID_DIR" ]] || fail "Android project not found: $ANDROID_DIR"' \ - '[[ -x "$ANDROID_DIR/gradlew" ]] || fail "Gradle wrapper is not executable: $ANDROID_DIR/gradlew"' \ - '[[ -f "$WRAPPER_PROPERTIES" ]] || fail "Gradle wrapper properties not found: $WRAPPER_PROPERTIES"' \ - '' \ - 'ORIGINAL_WRAPPER_PROPERTIES="$(mktemp)"' \ - 'cp "$WRAPPER_PROPERTIES" "$ORIGINAL_WRAPPER_PROPERTIES"' \ - 'cleanup_wrapper_properties() {' \ - ' cp "$ORIGINAL_WRAPPER_PROPERTIES" "$WRAPPER_PROPERTIES"' \ - ' rm -f "$ORIGINAL_WRAPPER_PROPERTIES"' \ - '}' \ - 'trap cleanup_wrapper_properties EXIT' \ - '' \ - 'if [[ "${CHINAMIRRO:-0}" == "1" && -z "$GRADLE_DISTRIBUTION_URL" ]]; then' \ - ' GRADLE_DISTRIBUTION_URL="$GRADLE_DISTRIBUTION_URL_CN"' \ - 'fi' \ - '' \ - 'if [[ -n "$GRADLE_DISTRIBUTION_URL" ]]; then' \ - ' WRAPPER_PROPERTIES_TMP="$(mktemp)"' \ - ' awk -v url="$GRADLE_DISTRIBUTION_URL" -v timeout="$GRADLE_NETWORK_TIMEOUT" '"'"'' \ - ' BEGIN { seen_url = 0; seen_timeout = 0 }' \ - ' /^distributionUrl=/ { print "distributionUrl=" url; seen_url = 1; next }' \ - ' /^networkTimeout=/ { print "networkTimeout=" timeout; seen_timeout = 1; next }' \ - ' { print }' \ - ' END {' \ - ' if (!seen_url) print "distributionUrl=" url;' \ - ' if (!seen_timeout) print "networkTimeout=" timeout;' \ - ' }' \ - ' '"'"' "$WRAPPER_PROPERTIES" > "$WRAPPER_PROPERTIES_TMP"' \ - ' cp "$WRAPPER_PROPERTIES_TMP" "$WRAPPER_PROPERTIES"' \ - ' rm -f "$WRAPPER_PROPERTIES_TMP"' \ - ' if [[ -d /root/.gradle/wrapper/dists ]]; then' \ - ' find /root/.gradle/wrapper/dists \( -name "*.lck" -o -name "*.part" \) -print0 | xargs -0 -r rm -f' \ - ' fi' \ - 'fi' \ - '' \ - 'ensure_keystore' \ - '' \ - 'case "$ARCH" in' \ - 'arm64)' \ - ' ANDROID_ABIS="arm64-v8a"' \ - ' GRADLE_TASK=":app:assembleArm64Release"' \ - ' APK_FLAVORS="arm64"' \ - ' ;;' \ - 'armv7)' \ - ' ANDROID_ABIS="armeabi-v7a"' \ - ' GRADLE_TASK=":app:assembleArm32Release"' \ - ' APK_FLAVORS="arm32"' \ - ' ;;' \ - 'all)' \ - ' ANDROID_ABIS="arm64-v8a,armeabi-v7a"' \ - ' GRADLE_TASK=":app:assembleRelease"' \ - ' APK_FLAVORS="arm64 arm32"' \ - ' ;;' \ - '*) fail "Unsupported architecture: $ARCH (expected arm64, armv7, or all)" ;;' \ - 'esac' \ - '' \ - 'printf "sdk.dir=%s\n" "$ANDROID_HOME" > "$ANDROID_DIR/local.properties"' \ - 'mkdir -p "$OUTPUT_DIR"' \ - 'PROJECT_VERSION="$(read_project_version)"' \ - '' \ - 'export ONE_KVM_ANDROID_PROFILE="$BUILD_TYPE"' \ - 'export ONE_KVM_ANDROID_ABIS="$ANDROID_ABIS"' \ - 'export ONE_KVM_ANDROID_FFMPEG_ROOT="$FFMPEG_ROOT"' \ - 'export ANDROID_HOME' \ - 'export ANDROID_SDK_ROOT' \ - 'export ANDROID_NDK_HOME' \ - 'export ANDROID_NDK_ROOT' \ - '' \ - 'echo "Building Android APK"' \ - 'echo " task: $GRADLE_TASK"' \ - 'echo " profile: $ONE_KVM_ANDROID_PROFILE"' \ - 'echo " version: $PROJECT_VERSION"' \ - 'echo " abis: $ONE_KVM_ANDROID_ABIS"' \ - 'echo " output: $OUTPUT_DIR"' \ - 'echo " sdk: $ANDROID_HOME"' \ - 'echo " ndk: $ANDROID_NDK_HOME"' \ - 'echo " build tools: $ANDROID_BUILD_TOOLS_DIR"' \ - 'echo " ffmpeg root: $ONE_KVM_ANDROID_FFMPEG_ROOT"' \ - 'if [[ -n "$GRADLE_DISTRIBUTION_URL" ]]; then' \ - ' echo " gradle distribution: $GRADLE_DISTRIBUTION_URL"' \ - 'fi' \ - '' \ - '(' \ - ' cd "$ANDROID_DIR"' \ - ' ./gradlew "$GRADLE_TASK"' \ - ')' \ - '' \ - 'for flavor in $APK_FLAVORS; do' \ - ' copy_apks "$flavor"' \ - 'done' \ - '' \ - 'echo' \ - 'echo "APK output:"' \ - 'ls -1 "$OUTPUT_DIR"' \ - > /usr/local/bin/build-one-kvm-android \ - && chmod +x /usr/local/bin/build-one-kvm-android - -WORKDIR /workspace - -ENTRYPOINT ["/usr/local/bin/build-one-kvm-android"] -CMD ["all"] diff --git a/libs/hwcodec/.gitignore b/libs/hwcodec/.gitignore index 7406876d..c436e228 100644 --- a/libs/hwcodec/.gitignore +++ b/libs/hwcodec/.gitignore @@ -8,8 +8,6 @@ /ffmpeg/linux/debug !/ffmpeg/mac /ffmpeg/mac/debug -!/ffmpeg/android -/ffmpeg/android/debug !/ffmpeg/ios /ffmpeg/ios/debug /input diff --git a/libs/hwcodec/README.md b/libs/hwcodec/README.md index e2faf5f9..d81e614b 100644 --- a/libs/hwcodec/README.md +++ b/libs/hwcodec/README.md @@ -41,18 +41,6 @@ Based on the information above, there are several optimizations and changes made * remove hevc_vaapi because of possible poor quality * amf: not tested, https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/378 -### MacOS - -| FFmpeg ram encode | FFmpeg ram decode | -| ------------------ | ------------------ | -| h265 only | Y | - -### Android - -| FFmpeg ram encode | -| ------------------ | -| Y | - ## System requirements * intel @@ -76,4 +64,3 @@ Based on the information above, there are several optimizations and changes made https://docs.nvidia.com/video-technologies/video-codec-sdk/11.1/read-me/index.html https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new?ncid=em-prod-816193 - diff --git a/libs/hwcodec/build.rs b/libs/hwcodec/build.rs index 6ad68271..53dcc822 100644 --- a/libs/hwcodec/build.rs +++ b/libs/hwcodec/build.rs @@ -21,15 +21,11 @@ fn build_common(builder: &mut Build) { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let common_dir = manifest_dir.join("cpp").join("common"); - let mut bindings = bindgen::builder() + let bindings = bindgen::builder() .header(common_dir.join("common.h").to_string_lossy().to_string()) .header(common_dir.join("callback.h").to_string_lossy().to_string()) .rustified_enum(".*") .parse_callbacks(Box::new(CommonCallbacks)); - if target_os == "android" { - print_android_bindgen_env(); - bindings = bindings.clang_args(android_clang_args()); - } bindings .generate() .unwrap() @@ -62,9 +58,9 @@ fn build_common(builder: &mut Build) { } // Unsupported platforms - if target_os != "windows" && target_os != "linux" && target_os != "android" { + if target_os != "windows" && target_os != "linux" { panic!( - "Unsupported OS: {}. Only Windows, Linux, and Android are supported.", + "Unsupported OS: {}. Only Windows and Linux are supported.", target_os ); } @@ -89,123 +85,12 @@ impl bindgen::callbacks::ParseCallbacks for CommonCallbacks { } } -fn print_android_bindgen_env() { - println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME"); - println!("cargo:rerun-if-env-changed=ANDROID_NDK_ROOT"); - println!("cargo:rerun-if-env-changed=NDK_HOME"); - println!("cargo:rerun-if-env-changed=ANDROID_HOME"); - println!("cargo:rerun-if-env-changed=ANDROID_SDK_ROOT"); - println!("cargo:rerun-if-env-changed=CARGO_NDK_PLATFORM"); -} - -fn android_clang_args() -> Vec { - let ndk = android_ndk_home(); - let target = env::var("TARGET").unwrap_or_default(); - let toolchain = ndk.join("toolchains/llvm/prebuilt").join(host_tag()); - let sysroot = toolchain.join("sysroot"); - let clang_include = toolchain - .join("lib/clang") - .join(clang_version(&toolchain)) - .join("include"); - let api = env::var("CARGO_NDK_PLATFORM") - .ok() - .and_then(|value| value.parse::().ok()) - .unwrap_or(21); - let clang_target = android_clang_target(&target); - - vec![ - format!("--target={clang_target}"), - format!("--sysroot={}", sysroot.display()), - format!("-D__ANDROID_API__={api}"), - format!("-isystem{}", clang_include.display()), - format!("-isystem{}", sysroot.join("usr/include").display()), - format!( - "-isystem{}", - sysroot.join("usr/include").join(clang_target).display() - ), - ] -} - -fn android_clang_target(target: &str) -> &'static str { - match target { - "aarch64-linux-android" => "aarch64-linux-android", - "armv7-linux-androideabi" => "armv7a-linux-androideabi", - "i686-linux-android" => "i686-linux-android", - "x86_64-linux-android" => "x86_64-linux-android", - other => panic!("unsupported Android target for hwcodec bindgen: {other}"), - } -} - -fn android_ndk_home() -> PathBuf { - for key in ["ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "NDK_HOME"] { - if let Ok(value) = env::var(key) { - return PathBuf::from(value); - } - } - - for key in ["ANDROID_HOME", "ANDROID_SDK_ROOT"] { - if let Ok(value) = env::var(key) { - let ndk_dir = PathBuf::from(value).join("ndk"); - if let Some(newest) = newest_child_dir(&ndk_dir) { - return newest; - } - } - } - - panic!( - "hwcodec Android bindgen requires ANDROID_NDK_HOME, ANDROID_NDK_ROOT, NDK_HOME, \ - or ANDROID_HOME/ANDROID_SDK_ROOT with an ndk directory" - ); -} - -fn newest_child_dir(path: &Path) -> Option { - let mut entries = std::fs::read_dir(path) - .ok()? - .filter_map(|entry| entry.ok()) - .map(|entry| entry.path()) - .filter(|path| path.is_dir()) - .collect::>(); - entries.sort(); - entries.pop() -} - -fn host_tag() -> &'static str { - if cfg!(target_os = "linux") { - "linux-x86_64" - } else if cfg!(target_os = "macos") { - "darwin-x86_64" - } else if cfg!(target_os = "windows") { - "windows-x86_64" - } else { - panic!("unsupported host OS for Android NDK"); - } -} - -fn clang_version(toolchain: &Path) -> String { - let clang_dir = toolchain.join("lib/clang"); - let mut entries = std::fs::read_dir(&clang_dir) - .unwrap_or_else(|_| panic!("missing NDK clang directory: {}", clang_dir.display())) - .filter_map(|entry| entry.ok()) - .map(|entry| entry.file_name().to_string_lossy().into_owned()) - .collect::>(); - entries.sort(); - entries - .pop() - .unwrap_or_else(|| panic!("no clang versions found under: {}", clang_dir.display())) -} - mod ffmpeg { use super::*; pub fn build_ffmpeg(builder: &mut Build) { ffmpeg_ffi(); - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") { - link_android_ffmpeg(builder); - build_ffmpeg_ram(builder); - return; - } - // Try VCPKG first, fallback to system FFmpeg via pkg-config if let Some(vcpkg_installed) = vcpkg_installed_root() { link_vcpkg(builder, vcpkg_installed); @@ -220,67 +105,6 @@ mod ffmpeg { build_ffmpeg_capture(builder); } - fn link_android_ffmpeg(builder: &mut Build) { - let root = std::env::var("ONE_KVM_ANDROID_FFMPEG_ROOT").unwrap_or_else(|_| { - panic!( - "ONE_KVM_ANDROID_FFMPEG_ROOT is required when building hwcodec for Android. \ - It must point to an FFmpeg Android build with MediaCodec enabled." - ) - }); - let root = PathBuf::from(root); - let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); - let abi = match target_arch.as_str() { - "aarch64" => "arm64-v8a", - "arm" => "armeabi-v7a", - "x86" => "x86", - "x86_64" => "x86_64", - _ => target_arch.as_str(), - }; - - let abi_root = root.join(abi); - let lib_dir = if abi_root.join("lib").exists() { - abi_root.join("lib") - } else { - root.join("lib") - }; - let include_dir = if abi_root.join("include").exists() { - abi_root.join("include") - } else { - root.join("include") - }; - - if !include_dir.exists() || !lib_dir.exists() { - panic!( - "Invalid ONE_KVM_ANDROID_FFMPEG_ROOT: include/lib not found for ABI {} under {}", - abi, - root.display() - ); - } - - println!("cargo:rustc-link-search=native={}", lib_dir.display()); - builder.include(&include_dir); - - let use_static = std::env::var("ONE_KVM_ANDROID_FFMPEG_STATIC") - .map(|value| value != "0") - .unwrap_or(true); - for lib in ["avcodec", "avutil"] { - if use_static { - println!("cargo:rustc-link-lib=static={}", lib); - } else { - println!("cargo:rustc-link-lib={}", lib); - } - } - - println!("cargo:rustc-link-lib=log"); - println!("cargo:rustc-link-lib=mediandk"); - println!("cargo:rustc-link-lib=android"); - println!("cargo:rustc-link-lib=dl"); - println!("cargo:rustc-link-lib=m"); - println!("cargo:rustc-link-lib=z"); - println!("cargo:rustc-link-lib=c++_shared"); - println!("cargo:info=Using Android FFmpeg from {}", root.display()); - } - fn vcpkg_installed_root() -> Option { println!("cargo:rerun-if-env-changed=VCPKG_INSTALLED_DIR"); println!("cargo:rerun-if-env-changed=VCPKG_ROOT"); @@ -530,11 +354,9 @@ mod ffmpeg { } // ARM (aarch64, arm): no X11 needed, uses RKMPP/V4L2 v - } else if target_os == "android" { - Vec::new() } else { panic!( - "Unsupported OS: {}. Only Windows, Linux, and Android are supported.", + "Unsupported OS: {}. Only Windows and Linux are supported.", target_os ); }; @@ -550,13 +372,7 @@ mod ffmpeg { let ffi_header_path = ffmpeg_ram_dir.join("ffmpeg_ffi.h"); println!("cargo:rerun-if-changed={}", ffi_header_path.display()); let ffi_header = ffi_header_path.to_string_lossy().to_string(); - let mut bindings = bindgen::builder() - .header(ffi_header) - .rustified_enum(".*"); - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") { - print_android_bindgen_env(); - bindings = bindings.clang_args(android_clang_args()); - } + let bindings = bindgen::builder().header(ffi_header).rustified_enum(".*"); bindings .generate() .unwrap() @@ -571,13 +387,7 @@ mod ffmpeg { .join("ffmpeg_ram_ffi.h") .to_string_lossy() .to_string(); - let mut bindings = bindgen::builder() - .header(ffi_header) - .rustified_enum(".*"); - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") { - print_android_bindgen_env(); - bindings = bindings.clang_args(android_clang_args()); - } + let bindings = bindgen::builder().header(ffi_header).rustified_enum(".*"); bindings .generate() .unwrap() @@ -589,12 +399,13 @@ mod ffmpeg { // RKMPP decode only exists on ARM builds where FFmpeg is compiled with RKMPP support. // Avoid compiling this file on x86/x64 where `AV_HWDEVICE_TYPE_RKMPP` doesn't exist. let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - let enable_rkmpp = target_os != "android" - && matches!(target_arch.as_str(), "aarch64" | "arm") + let enable_rkmpp = matches!(target_arch.as_str(), "aarch64" | "arm") || std::env::var_os("CARGO_FEATURE_RKMPP").is_some(); if enable_rkmpp { builder.file(ffmpeg_ram_dir.join("ffmpeg_ram_decode.cpp")); + if enable_rkmpp { + builder.define("ONE_KVM_FFMPEG_RKMPP", None); + } } else { println!( "cargo:info=Skipping ffmpeg_ram_decode.cpp (RKMPP) for arch {}", @@ -647,9 +458,7 @@ mod ffmpeg { .unwrap(); let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - let enable_rkmpp = target_os != "android" - && matches!(target_arch.as_str(), "aarch64" | "arm") + let enable_rkmpp = matches!(target_arch.as_str(), "aarch64" | "arm") || std::env::var_os("CARGO_FEATURE_RKMPP").is_some(); if enable_rkmpp { // Include RGA headers for NV16->NV12 conversion (RGA im2d API) diff --git a/libs/hwcodec/cpp/common/util.cpp b/libs/hwcodec/cpp/common/util.cpp index 08e4f87a..38320d7c 100644 --- a/libs/hwcodec/cpp/common/util.cpp +++ b/libs/hwcodec/cpp/common/util.cpp @@ -24,7 +24,7 @@ bool is_software_h264(const std::string &name) { // Exclude all hardware encoders static const char* hw_suffixes[] = { "nvenc", "amf", "qsv", "vaapi", "rkmpp", - "v4l2m2m", "videotoolbox", "mediacodec", "_mf" + "v4l2m2m", "videotoolbox", "_mf" }; for (const auto& suffix : hw_suffixes) { if (name.find(suffix) != std::string::npos) return false; @@ -37,7 +37,7 @@ bool is_software_hevc(const std::string &name) { if (name != "hevc" && name != "libx265") return false; static const char* hw_suffixes[] = { "nvenc", "amf", "qsv", "vaapi", "rkmpp", - "v4l2m2m", "videotoolbox", "mediacodec", "_mf" + "v4l2m2m", "videotoolbox", "_mf" }; for (const auto& suffix : hw_suffixes) { if (name.find(suffix) != std::string::npos) return false; @@ -100,13 +100,8 @@ void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs, c->color_primaries = AVCOL_PRI_SMPTE170M; c->color_trc = AVCOL_TRC_SMPTE170M; - // WebRTC SDP advertises constrained baseline. Keep most hardware and software - // encoders on the same browser-friendly H264 profile. Android MediaCodec is - // deliberately excluded because older vendor OMX encoders can reject explicit - // profile/level combinations during configure(). - if (name.find("mediacodec") != std::string::npos) { - return; - } + // WebRTC SDP advertises constrained baseline. Keep hardware and software + // encoders on the same browser-friendly H264 profile. if (name.find("h264") != std::string::npos) { c->profile = AV_PROFILE_H264_CONSTRAINED_BASELINE; } else if (name.find("hevc") != std::string::npos) { @@ -310,9 +305,6 @@ bool set_quality(void *priv_data, const std::string &name, int quality) { break; } } - // Do not force MediaCodec level here. Some Android TV vendor encoders, - // including older Amlogic OMX implementations, reject explicit level values - // even when they support the requested resolution and bitrate. // libx264 software encoder presets if (is_software_h264(name)) { const char* preset = nullptr; @@ -367,7 +359,7 @@ struct CodecOptions { }; bool set_rate_control(AVCodecContext *c, const std::string &name, int rc, - int q) { + int /*q*/) { if (name.find("qsv") != std::string::npos) { // https://github.com/LizardByte/Sunshine/blob/3e47cd3cc8fd37a7a88be82444ff4f3c0022856b/src/video.cpp#L1635 c->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL; @@ -375,9 +367,6 @@ bool set_rate_control(AVCodecContext *c, const std::string &name, int rc, std::vector codecs = { {"nvenc", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr"}}}, {"amf", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr_latency"}}}, - {"mediacodec", - "bitrate_mode", - {{RC_CBR, "cbr"}, {RC_VBR, "vbr"}, {RC_CQ, "cq"}}}, // {"videotoolbox", "constant_bit_rate", {{RC_CBR, "1"}}}, }; @@ -392,13 +381,6 @@ bool set_rate_control(AVCodecContext *c, const std::string &name, int rc, it->second + " failed, ret = " + av_err2str(ret)); return false; } - if (name.find("mediacodec") != std::string::npos) { - if (rc == RC_CQ) { - if (q >= 0 && q <= 51) { - c->global_quality = q; - } - } - } } break; } @@ -448,13 +430,6 @@ bool set_others(void *priv_data, const std::string &name) { return false; } } - if (name.find("mediacodec") != std::string::npos) { - if ((ret = av_opt_set_int(priv_data, "ndk_codec", 1, 0)) < 0) { - LOG_ERROR(std::string("mediacodec set ndk_codec failed, ret = ") + - av_err2str(ret)); - return false; - } - } // NOTE: Removed idr_interval = INT_MAX for VAAPI. // This was disabling automatic keyframe generation. // The encoder should respect c->gop_size for keyframe interval. diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_decode.cpp b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_decode.cpp index 64287dc4..bd8b5bd7 100644 --- a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_decode.cpp +++ b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_decode.cpp @@ -1,4 +1,4 @@ -// Minimal FFmpeg RAM MJPEG decoder (RKMPP only) -> NV12 in CPU memory. +// FFmpeg RAM decoder with optional RKMPP hardware-frame support. extern "C" { #include @@ -54,9 +54,11 @@ public: thread_count_ = thread_count > 0 ? thread_count : 1; callback_ = callback; +#ifdef ONE_KVM_FFMPEG_RKMPP if (name_.find("rkmpp") != std::string::npos) { hw_device_type_ = AV_HWDEVICE_TYPE_RKMPP; } +#endif } ~FFmpegRamDecoder() {} @@ -137,13 +139,6 @@ public: av_buffer_unref(&frames_ref); } - if (name_.find("mediacodec") != std::string::npos && c_->priv_data) { - if ((ret = av_opt_set_int(c_->priv_data, "ndk_codec", 1, 0)) < 0) { - LOG_WARN(std::string("mediacodec decoder ndk_codec option failed, ret = ") + - av_err2str(ret)); - } - } - if ((ret = avcodec_open2(c_, codec, NULL)) < 0) { set_last_error(std::string("avcodec_open2 failed, ret = ") + av_err2str(ret)); return false; diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp index c4507a54..2c1348d4 100644 --- a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp +++ b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp @@ -11,6 +11,7 @@ extern "C" { #include #include #include +#include #include "common.h" @@ -21,6 +22,13 @@ extern "C" { #include "win.h" #endif +static thread_local std::string g_encoder_last_error; + +static void set_encoder_last_error(const std::string &message) { + g_encoder_last_error = message; + LOG_ERROR(message); +} + static int calculate_offset_length(int pix_fmt, int height, const int *linesize, int *offset, int *length) { switch (pix_fmt) { @@ -122,7 +130,6 @@ public: AVFrame *frame_ = NULL; AVPacket *pkt_ = NULL; std::string name_; - std::string mc_name_; // for mediacodec int width_ = 0; int height_ = 0; @@ -145,14 +152,12 @@ public: AVPixelFormat hw_pixfmt_ = AV_PIX_FMT_NONE; AVBufferRef *hw_device_ctx_ = NULL; AVFrame *hw_frame_ = NULL; - AVFrame *borrowed_frame_ = NULL; - FFmpegRamEncoder(const char *name, const char *mc_name, int width, int height, + FFmpegRamEncoder(const char *name, int width, int height, int pixfmt, int align, int fps, int gop, int rc, int quality, int kbs, int q, int thread_count, int gpu, RamEncodeCallback callback) { name_ = name; - mc_name_ = mc_name ? mc_name : ""; width_ = width; height_ = height; pixfmt_ = (AVPixelFormat)pixfmt; @@ -184,12 +189,13 @@ public: } bool init(int *linesize, int *offset, int *length) { + g_encoder_last_error.clear(); const AVCodec *codec = NULL; int ret; if (!(codec = avcodec_find_encoder_by_name(name_.c_str()))) { - LOG_ERROR(std::string("Codec ") + name_ + " not found"); + set_encoder_last_error(std::string("Codec ") + name_ + " not found"); return false; } @@ -252,12 +258,6 @@ public: LOG_ERROR(std::string("Could not allocate video packet")); return false; } - borrowed_frame_ = av_frame_alloc(); - if (!borrowed_frame_) { - LOG_ERROR(std::string("Could not allocate borrowed video frame")); - return false; - } - /* resolution must be a multiple of two */ c_->width = width_; c_->height = height_; @@ -277,19 +277,9 @@ public: util_encode::set_gpu(c_->priv_data, name_, gpu_); util_encode::force_hw(c_->priv_data, name_); util_encode::set_others(c_->priv_data, name_); - if (name_.find("mediacodec") != std::string::npos) { - if (mc_name_.length() > 0) { - LOG_INFO(std::string("mediacodec codec_name: ") + mc_name_); - if ((ret = av_opt_set(c_->priv_data, "codec_name", mc_name_.c_str(), - 0)) < 0) { - LOG_ERROR(std::string("mediacodec codec_name failed, ret = ") + av_err2str(ret)); - } - } - } - if ((ret = avcodec_open2(c_, codec, NULL)) < 0) { - LOG_ERROR(std::string("avcodec_open2 failed, ret = ") + av_err2str(ret) + - ", name: " + name_); + set_encoder_last_error(std::string("avcodec_open2 failed, ret = ") + + av_err2str(ret) + ", name: " + name_); return false; } @@ -307,14 +297,6 @@ public: int encode(const uint8_t *data, int length, const void *obj, uint64_t ms) { int ret; - if (can_borrow_input(length)) { - AVFrame *borrowed = wrap_borrowed_frame(data, length); - if (!borrowed) { - return -1; - } - return do_encode(borrowed, obj, ms); - } - if ((ret = av_frame_make_writable(frame_)) != 0) { LOG_ERROR(std::string("av_frame_make_writable failed, ret = ") + av_err2str(ret)); return ret; @@ -350,8 +332,6 @@ public: av_frame_free(&frame_); if (hw_frame_) av_frame_free(&hw_frame_); - if (borrowed_frame_) - av_frame_free(&borrowed_frame_); if (hw_device_ctx_) av_buffer_unref(&hw_device_ctx_); if (c_) @@ -610,65 +590,6 @@ private: return 0; } - bool can_borrow_input(int data_length) const { - if (hw_device_type_ != AV_HWDEVICE_TYPE_NONE) { - return false; - } - if (name_.find("mediacodec") == std::string::npos) { - return false; - } - switch (pixfmt_) { - case AV_PIX_FMT_NV12: - case AV_PIX_FMT_NV21: - return data_length >= width_ * height_ * 3 / 2; - case AV_PIX_FMT_YUV420P: - return data_length >= width_ * height_ * 3 / 2; - default: - return false; - } - } - - AVFrame *wrap_borrowed_frame(const uint8_t *data, int data_length) { - if (!borrowed_frame_) { - return NULL; - } - av_frame_unref(borrowed_frame_); - borrowed_frame_->format = pixfmt_; - borrowed_frame_->width = width_; - borrowed_frame_->height = height_; - - const int y_size = width_ * height_; - const int uv_size = y_size / 4; - switch (pixfmt_) { - case AV_PIX_FMT_NV12: - case AV_PIX_FMT_NV21: - if (data_length < y_size + y_size / 2) { - LOG_ERROR("wrap_borrowed_frame: NV12/NV21 data length error"); - return NULL; - } - borrowed_frame_->data[0] = const_cast(data); - borrowed_frame_->data[1] = const_cast(data + y_size); - borrowed_frame_->linesize[0] = width_; - borrowed_frame_->linesize[1] = width_; - break; - case AV_PIX_FMT_YUV420P: - if (data_length < y_size + uv_size * 2) { - LOG_ERROR("wrap_borrowed_frame: YUV420P data length error"); - return NULL; - } - borrowed_frame_->data[0] = const_cast(data); - borrowed_frame_->data[1] = const_cast(data + y_size); - borrowed_frame_->data[2] = const_cast(data + y_size + uv_size); - borrowed_frame_->linesize[0] = width_; - borrowed_frame_->linesize[1] = width_ / 2; - borrowed_frame_->linesize[2] = width_ / 2; - break; - default: - return NULL; - } - return borrowed_frame_; - } - int bytes_per_pixel(int pix_fmt) { switch (pix_fmt) { case AV_PIX_FMT_YUYV422: @@ -687,14 +608,14 @@ private: } // namespace extern "C" FFmpegRamEncoder * -ffmpeg_ram_new_encoder(const char *name, const char *mc_name, int width, +ffmpeg_ram_new_encoder(const char *name, int width, int height, int pixfmt, int align, int fps, int gop, int rc, int quality, int kbs, int q, int thread_count, int gpu, int *linesize, int *offset, int *length, RamEncodeCallback callback) { FFmpegRamEncoder *encoder = NULL; try { - encoder = new FFmpegRamEncoder(name, mc_name, width, height, pixfmt, align, + encoder = new FFmpegRamEncoder(name, width, height, pixfmt, align, fps, gop, rc, quality, kbs, q, thread_count, gpu, callback); if (encoder) { @@ -772,3 +693,7 @@ extern "C" void ffmpeg_ram_request_keyframe(FFmpegRamEncoder *encoder) { LOG_ERROR(std::string("ffmpeg_ram_request_keyframe failed, ") + std::string(e.what())); } } + +extern "C" const char *ffmpeg_ram_encoder_last_error(void) { + return g_encoder_last_error.c_str(); +} diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h index e728fd85..b30de54c 100644 --- a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h +++ b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h @@ -13,7 +13,7 @@ typedef void (*RamEncodePacketCallback)(void *packet, const uint8_t *data, typedef void (*RamDecodeCallback)(const uint8_t *data, int len, int width, int height, int pixfmt, const void *obj); -void *ffmpeg_ram_new_encoder(const char *name, const char *mc_name, int width, +void *ffmpeg_ram_new_encoder(const char *name, int width, int height, int pixfmt, int align, int fps, int gop, int rc, int quality, int kbs, int q, int thread_count, int gpu, int *linesize, @@ -31,6 +31,7 @@ int ffmpeg_ram_get_linesize_offset_length(int pix_fmt, int width, int height, int *length); int ffmpeg_ram_set_bitrate(void *encoder, int kbs); void ffmpeg_ram_request_keyframe(void *encoder); +const char *ffmpeg_ram_encoder_last_error(void); void *ffmpeg_ram_new_decoder(const char *name, int width, int height, int sw_pixfmt, int thread_count, diff --git a/libs/hwcodec/src/common.rs b/libs/hwcodec/src/common.rs index ed01375a..b545d9ef 100644 --- a/libs/hwcodec/src/common.rs +++ b/libs/hwcodec/src/common.rs @@ -13,7 +13,7 @@ pub enum Driver { FFMPEG, } -#[cfg(any(windows, target_os = "linux", target_os = "android"))] +#[cfg(any(windows, target_os = "linux"))] pub(crate) fn supported_gpu(_encode: bool) -> (bool, bool, bool) { #[cfg(target_os = "linux")] use std::ffi::c_int; @@ -39,8 +39,6 @@ pub(crate) fn supported_gpu(_encode: bool) -> (bool, bool, bool) { linux_support_amd() == 0, linux_support_intel() == 0, ); - #[cfg(target_os = "android")] - return (false, false, false); #[allow(unreachable_code)] (false, false, false) } diff --git a/libs/hwcodec/src/ffmpeg_ram/decode.rs b/libs/hwcodec/src/ffmpeg_ram/decode.rs index 0cd9caf4..f93f6f48 100644 --- a/libs/hwcodec/src/ffmpeg_ram/decode.rs +++ b/libs/hwcodec/src/ffmpeg_ram/decode.rs @@ -114,7 +114,7 @@ impl Drop for Decoder { } } -fn last_error_message() -> String { +pub fn last_error_message() -> String { unsafe { let ptr = ffmpeg_ram_last_error(); if ptr.is_null() { diff --git a/libs/hwcodec/src/ffmpeg_ram/encode.rs b/libs/hwcodec/src/ffmpeg_ram/encode.rs index a39edbc4..23a3b0dc 100644 --- a/libs/hwcodec/src/ffmpeg_ram/encode.rs +++ b/libs/hwcodec/src/ffmpeg_ram/encode.rs @@ -3,8 +3,9 @@ use crate::{ ffmpeg::{init_av_log, AVPixelFormat}, ffmpeg_ram::{ ffmpeg_linesize_offset_length, ffmpeg_ram_encode, ffmpeg_ram_encode_packet, - ffmpeg_ram_free_encoder, ffmpeg_ram_free_packet, ffmpeg_ram_new_encoder, - ffmpeg_ram_request_keyframe, ffmpeg_ram_set_bitrate, CodecInfo, AV_NUM_DATA_POINTERS, + ffmpeg_ram_encoder_last_error, ffmpeg_ram_free_encoder, ffmpeg_ram_free_packet, + ffmpeg_ram_new_encoder, ffmpeg_ram_request_keyframe, ffmpeg_ram_set_bitrate, CodecInfo, + AV_NUM_DATA_POINTERS, }, }; #[cfg(feature = "bytes")] @@ -17,7 +18,7 @@ use std::{ slice, }; -#[cfg(any(windows, target_os = "linux", target_os = "android"))] +#[cfg(any(windows, target_os = "linux"))] use crate::common::Driver; /// Timeout for encoder test in milliseconds @@ -28,7 +29,6 @@ const PRIORITY_AMF: i32 = 2; const PRIORITY_RKMPP: i32 = 3; const PRIORITY_VAAPI: i32 = 4; const PRIORITY_V4L2M2M: i32 = 5; -const PRIORITY_MEDIACODEC: i32 = 2; #[derive(Clone, Copy)] struct CandidateCodecSpec { @@ -95,32 +95,12 @@ fn linux_support_v4l2m2m() -> bool { false } -#[cfg(any(windows, target_os = "linux", target_os = "android"))] +#[cfg(any(windows, target_os = "linux"))] fn enumerate_candidate_codecs(ctx: &EncodeContext) -> Vec { use log::debug; let mut codecs = Vec::new(); - if cfg!(target_os = "android") { - push_candidate( - &mut codecs, - CandidateCodecSpec { - name: "h264_mediacodec", - format: H264, - priority: PRIORITY_MEDIACODEC, - }, - ); - push_candidate( - &mut codecs, - CandidateCodecSpec { - name: "hevc_mediacodec", - format: H265, - priority: PRIORITY_MEDIACODEC, - }, - ); - return codecs; - } - let contains = |_vendor: Driver, _format: DataFormat| { // Without VRAM feature, we can't check SDK availability. // Keep the prefilter coarse and let FFmpeg validation do the real check. @@ -281,13 +261,7 @@ struct ProbePolicy { impl ProbePolicy { fn for_codec(codec_name: &str) -> Self { - if codec_name.contains("mediacodec") { - Self { - max_attempts: 30, - request_keyframe: true, - accept_any_output: true, - } - } else if codec_name.contains("amf") { + if codec_name.contains("amf") { Self { max_attempts: 5, request_keyframe: true, @@ -340,7 +314,8 @@ fn log_failed_probe_attempt( if frames.is_empty() { trace!( "Encoder {} test produced no output on attempt {}", - codec_name, attempt + codec_name, + attempt ); } else { debug!( @@ -373,7 +348,6 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo let test_ctx = EncodeContext { name: codec.name.clone(), - mc_name: codec.mc_name.clone(), ..ctx.clone() }; @@ -418,13 +392,13 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo ); } } - Err(err) => { - last_err = Some(err); - warn!( - "Encoder {} test attempt {} returned error: {}", - codec.name, attempt_no, err - ); - } + Err(err) => { + last_err = Some(err); + warn!( + "Encoder {} test attempt {} returned error: {}", + codec.name, attempt_no, err + ); + } } } @@ -437,20 +411,16 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo ); false } - Err(_) => { - warn!("Failed to create encoder {}", codec.name); - false - } - } + Err(_) => { + warn!("Failed to create encoder {}", codec.name); + false + } + } } fn add_software_fallback(codecs: &mut Vec) { use log::debug; - if cfg!(target_os = "android") { - return; - } - for fallback in CodecInfo::soft().into_vec() { if !codecs.iter().any(|codec| codec.format == fallback.format) { debug!( @@ -465,7 +435,6 @@ fn add_software_fallback(codecs: &mut Vec) { #[derive(Debug, Clone, PartialEq)] pub struct EncodeContext { pub name: String, - pub mc_name: Option, pub width: i32, pub height: i32, pub pixfmt: i32, @@ -550,10 +519,8 @@ impl Encoder { .unwrap_or("-1".to_owned()) .parse() .unwrap_or(-1); - let mc_name = ctx.mc_name.clone().unwrap_or_default(); let codec = ffmpeg_ram_new_encoder( CString::new(ctx.name.as_str()).map_err(|_| ())?.as_ptr(), - CString::new(mc_name.as_str()).map_err(|_| ())?.as_ptr(), ctx.width, ctx.height, ctx.pixfmt, @@ -573,6 +540,10 @@ impl Encoder { ); if codec.is_null() { + let message = encoder_last_error_message(); + if !message.is_empty() { + log::error!("ffmpeg_ram_new_encoder failed: {}", message); + } return Err(()); } @@ -698,11 +669,11 @@ impl Encoder { pub fn available_encoders(ctx: EncodeContext, _sdk: Option) -> Vec { use log::debug; - if !(cfg!(windows) || cfg!(target_os = "linux") || cfg!(target_os = "android")) { + if !(cfg!(windows) || cfg!(target_os = "linux")) { return vec![]; } let mut res = vec![]; - #[cfg(any(windows, target_os = "linux", target_os = "android"))] + #[cfg(any(windows, target_os = "linux"))] let codecs = enumerate_candidate_codecs(&ctx); if let Ok(yuv) = Encoder::dummy_yuv(ctx.clone()) { @@ -736,6 +707,16 @@ impl Encoder { } } +pub fn encoder_last_error_message() -> String { + unsafe { + let ptr = ffmpeg_ram_encoder_last_error(); + if ptr.is_null() { + return String::new(); + } + std::ffi::CStr::from_ptr(ptr).to_string_lossy().to_string() + } +} + impl Drop for Encoder { fn drop(&mut self) { unsafe { diff --git a/libs/hwcodec/src/ffmpeg_ram/mod.rs b/libs/hwcodec/src/ffmpeg_ram/mod.rs index 47a90399..f90b0ac6 100644 --- a/libs/hwcodec/src/ffmpeg_ram/mod.rs +++ b/libs/hwcodec/src/ffmpeg_ram/mod.rs @@ -9,18 +9,12 @@ use std::ffi::c_int; include!(concat!(env!("OUT_DIR"), "/ffmpeg_ram_ffi.rs")); -#[cfg(all( - any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"), - not(target_os = "android") -))] +#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))] pub mod decode; // Provide a small stub on non-ARM builds so dependents can still compile, but decoder // construction will fail (since the C++ RKMPP decoder isn't built/linked). -#[cfg(any( - not(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp")), - target_os = "android" -))] +#[cfg(not(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp")))] pub mod decode { use crate::ffmpeg::AVPixelFormat; @@ -69,8 +63,6 @@ pub enum Priority { #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] pub struct CodecInfo { pub name: String, - #[serde(skip)] - pub mc_name: Option, pub format: DataFormat, pub priority: i32, pub hwdevice: AVHWDeviceType, @@ -80,7 +72,6 @@ impl Default for CodecInfo { fn default() -> Self { Self { name: Default::default(), - mc_name: Default::default(), format: DataFormat::H264, priority: Default::default(), hwdevice: AVHWDeviceType::AV_HWDEVICE_TYPE_NONE, @@ -93,28 +84,24 @@ impl CodecInfo { match format { H264 => Some(CodecInfo { name: "libx264".to_owned(), - mc_name: Default::default(), format: H264, hwdevice: AV_HWDEVICE_TYPE_NONE, priority: Priority::Soft as _, }), H265 => Some(CodecInfo { name: "libx265".to_owned(), - mc_name: Default::default(), format: H265, hwdevice: AV_HWDEVICE_TYPE_NONE, priority: Priority::Soft as _, }), VP8 => Some(CodecInfo { name: "libvpx".to_owned(), - mc_name: Default::default(), format: VP8, hwdevice: AV_HWDEVICE_TYPE_NONE, priority: Priority::Soft as _, }), VP9 => Some(CodecInfo { name: "libvpx-vp9".to_owned(), - mc_name: Default::default(), format: VP9, hwdevice: AV_HWDEVICE_TYPE_NONE, priority: Priority::Soft as _, diff --git a/libs/hwcodec/src/lib.rs b/libs/hwcodec/src/lib.rs index 7dc82340..9a57fa75 100644 --- a/libs/hwcodec/src/lib.rs +++ b/libs/hwcodec/src/lib.rs @@ -2,10 +2,7 @@ pub mod capture; pub mod common; pub mod ffmpeg; -#[cfg(all( - any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"), - not(target_os = "android") -))] +#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))] pub mod ffmpeg_hw; pub mod ffmpeg_ram; diff --git a/libs/v4l2r/Android.bp b/libs/v4l2r/Android.bp deleted file mode 100644 index 01858ee3..00000000 --- a/libs/v4l2r/Android.bp +++ /dev/null @@ -1,52 +0,0 @@ -// This file is generated by cargo_embargo. -// Do not modify this file because the changes will be overridden on upgrade. - -package { - default_applicable_licenses: ["external_rust_crates_v4l2r_license"], -} - -rust_library { - name: "libv4l2r", - crate_name: "v4l2r", - cargo_env_compat: true, - cargo_pkg_version: "0.0.7", - crate_root: "src/lib.rs", - edition: "2021", - rustlibs: [ - "libbitflags", - "liblog_rust", - "libnix", - "libthiserror", - ], - proc_macros: ["libenumn"], - apex_available: [ - "//apex_available:platform", - "//apex_available:anyapex", - ], - product_available: true, - vendor_available: true, - // Bindgen-generated bindings of our local videodev2.h. - srcs: [":libv4l2r_bindgen"], - -} - -rust_test { - name: "v4l2r_test_src_lib", - crate_name: "v4l2r", - cargo_env_compat: true, - cargo_pkg_version: "0.0.7", - crate_root: "src/lib.rs", - test_suites: ["general-tests"], - auto_gen_config: true, - edition: "2021", - rustlibs: [ - "libbitflags", - "liblog_rust", - "libnix", - "libthiserror", - ], - proc_macros: ["libenumn"], - // Bindgen-generated bindings of our local videodev2.h. - srcs: [":libv4l2r_bindgen"], - -} diff --git a/libs/v4l2r/README.md b/libs/v4l2r/README.md index 1f16de36..7a714800 100644 --- a/libs/v4l2r/README.md +++ b/libs/v4l2r/README.md @@ -17,7 +17,3 @@ parts are intentionally removed here so this dependency stays scoped to capture. `cargo build` generates V4L2 bindings from the vendored Linux UAPI headers in `include/`. - -For Android targets, the build script uses the Android NDK sysroot. Set one of -`ANDROID_NDK_HOME`, `ANDROID_NDK_ROOT`, `NDK_HOME`, `ANDROID_HOME`, or -`ANDROID_SDK_ROOT` if the NDK cannot be found automatically. diff --git a/libs/v4l2r/bindgen.rs b/libs/v4l2r/bindgen.rs index c5ab0946..edb73a21 100644 --- a/libs/v4l2r/bindgen.rs +++ b/libs/v4l2r/bindgen.rs @@ -1,7 +1,7 @@ // This file defines the customizations to the bindgen builder used to generate the v4l2r // bindings. // -// It is meant to be included from `lib/build.rs` and `android/build.rs`. +// It is meant to be included from `build.rs`. #[derive(Debug)] /// Workaround for https://github.com/rust-lang/rust-bindgen/issues/753. diff --git a/libs/v4l2r/build.rs b/libs/v4l2r/build.rs index ce507e63..3c3c4a88 100644 --- a/libs/v4l2r/build.rs +++ b/libs/v4l2r/build.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; include!("bindgen.rs"); -/// Vendored Linux UAPI include root used for non-Android targets. +/// Vendored Linux UAPI include root. const VENDORED_INCLUDE_DIR: &str = "include"; /// Wrapper file to use as input of bindgen. @@ -13,28 +13,16 @@ const WRAPPER_H: &str = "v4l2r_wrapper.h"; const FIX753_H: &str = "fix753.h"; fn main() { - let target = env::var("TARGET").unwrap_or_default(); - let is_android = target.contains("android"); - - let include_root = if is_android { - android_sysroot().join("usr/include") - } else { + let include_root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` is not set")) - .join(VENDORED_INCLUDE_DIR) - }; + .join(VENDORED_INCLUDE_DIR); let videodev2_h = include_root.join("linux/videodev2.h"); - println!("cargo::rerun-if-env-changed=ANDROID_NDK_HOME"); - println!("cargo::rerun-if-env-changed=ANDROID_NDK_ROOT"); - println!("cargo::rerun-if-env-changed=NDK_HOME"); - println!("cargo::rerun-if-env-changed=ANDROID_HOME"); - println!("cargo::rerun-if-env-changed=ANDROID_SDK_ROOT"); - println!("cargo::rerun-if-env-changed=CARGO_NDK_PLATFORM"); println!("cargo::rerun-if-changed={}", videodev2_h.display()); println!("cargo::rerun-if-changed={}", FIX753_H); println!("cargo::rerun-if-changed={}", WRAPPER_H); - let mut clang_args = vec![ + let clang_args = vec![ format!("-I{}", include_root.display()), #[cfg(all(feature = "arch64", not(feature = "arch32")))] "--target=x86_64-linux-gnu".into(), @@ -42,10 +30,6 @@ fn main() { "--target=i686-linux-gnu".into(), ]; - if is_android { - clang_args.extend(android_clang_args(&target)); - } - let bindings = v4l2r_bindgen_builder(bindgen::Builder::default()) .header(WRAPPER_H) .clang_args(clang_args) @@ -57,105 +41,3 @@ fn main() { .write_to_file(out_path.join("bindings.rs")) .expect("Couldn't write bindings!"); } - -fn android_clang_args(target: &str) -> Vec { - let ndk = android_ndk_home(); - let toolchain = ndk.join("toolchains/llvm/prebuilt").join(host_tag()); - let sysroot = toolchain.join("sysroot"); - let clang_include = toolchain - .join("lib/clang") - .join(clang_version(&toolchain)) - .join("include"); - let api = env::var("CARGO_NDK_PLATFORM") - .ok() - .and_then(|value| value.parse::().ok()) - .unwrap_or(21); - let clang_target = android_clang_target(target); - - vec![ - format!("--target={clang_target}"), - format!("--sysroot={}", sysroot.display()), - format!("-D__ANDROID_API__={api}"), - format!("-isystem{}", clang_include.display()), - format!("-isystem{}", sysroot.join("usr/include").display()), - format!( - "-isystem{}", - sysroot.join("usr/include").join(clang_target).display() - ), - ] -} - -fn android_clang_target(target: &str) -> &'static str { - match target { - "aarch64-linux-android" => "aarch64-linux-android", - "armv7-linux-androideabi" => "armv7a-linux-androideabi", - "i686-linux-android" => "i686-linux-android", - "x86_64-linux-android" => "x86_64-linux-android", - other => panic!("unsupported Android target for v4l2r bindgen: {other}"), - } -} - -fn android_sysroot() -> PathBuf { - android_ndk_home() - .join("toolchains/llvm/prebuilt") - .join(host_tag()) - .join("sysroot") -} - -fn android_ndk_home() -> PathBuf { - for key in ["ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "NDK_HOME"] { - if let Ok(value) = env::var(key) { - return PathBuf::from(value); - } - } - - for key in ["ANDROID_HOME", "ANDROID_SDK_ROOT"] { - if let Ok(value) = env::var(key) { - let ndk_dir = PathBuf::from(value).join("ndk"); - if let Some(newest) = newest_child_dir(&ndk_dir) { - return newest; - } - } - } - - panic!( - "v4l2r Android bindgen requires ANDROID_NDK_HOME, ANDROID_NDK_ROOT, NDK_HOME, \ - or ANDROID_HOME/ANDROID_SDK_ROOT with an ndk directory" - ); -} - -fn newest_child_dir(path: &PathBuf) -> Option { - let mut entries = std::fs::read_dir(path) - .ok()? - .filter_map(|entry| entry.ok()) - .map(|entry| entry.path()) - .filter(|path| path.is_dir()) - .collect::>(); - entries.sort(); - entries.pop() -} - -fn host_tag() -> &'static str { - if cfg!(target_os = "linux") { - "linux-x86_64" - } else if cfg!(target_os = "macos") { - "darwin-x86_64" - } else if cfg!(target_os = "windows") { - "windows-x86_64" - } else { - panic!("unsupported host OS for Android NDK"); - } -} - -fn clang_version(toolchain: &PathBuf) -> String { - let clang_dir = toolchain.join("lib/clang"); - let mut entries = std::fs::read_dir(&clang_dir) - .unwrap_or_else(|err| panic!("failed to read {}: {err}", clang_dir.display())) - .filter_map(|entry| entry.ok()) - .map(|entry| entry.file_name().to_string_lossy().into_owned()) - .collect::>(); - entries.sort(); - entries - .pop() - .unwrap_or_else(|| panic!("no clang resource directory in {}", clang_dir.display())) -} diff --git a/libs/v4l2r/v4l2r_wrapper.h b/libs/v4l2r/v4l2r_wrapper.h index 4fbf738f..39e5a9f0 100644 --- a/libs/v4l2r/v4l2r_wrapper.h +++ b/libs/v4l2r/v4l2r_wrapper.h @@ -1,9 +1,3 @@ -#ifdef __ANDROID__ -#include -#include -#include -#endif - #include #define MARK_FIX_753(name) const unsigned long int Fix753_##name = name; diff --git a/res/vcpkg/libyuv/build.rs b/res/vcpkg/libyuv/build.rs index d0358453..3d7f8ffe 100644 --- a/res/vcpkg/libyuv/build.rs +++ b/res/vcpkg/libyuv/build.rs @@ -19,17 +19,7 @@ fn main() { fn generate_bindings(cpp_dir: &Path) { let ffi_header = cpp_dir.join("yuv_ffi.h"); - let mut builder = bindgen::builder().header(ffi_header.to_string_lossy().to_string()); - - if env::var("CARGO_CFG_TARGET_OS").ok().as_deref() == Some("android") { - println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME"); - println!("cargo:rerun-if-env-changed=ANDROID_NDK_ROOT"); - println!("cargo:rerun-if-env-changed=NDK_HOME"); - println!("cargo:rerun-if-env-changed=ANDROID_HOME"); - println!("cargo:rerun-if-env-changed=ANDROID_SDK_ROOT"); - println!("cargo:rerun-if-env-changed=CARGO_NDK_PLATFORM"); - builder = builder.clang_args(android_clang_args()); - } + let builder = bindgen::builder().header(ffi_header.to_string_lossy().to_string()); builder // YUYV conversions @@ -96,30 +86,6 @@ fn generate_bindings(cpp_dir: &Path) { } fn link_libyuv() { - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - - if target_os == "android" { - if link_android_libyuv() { - return; - } - if let Some(vcpkg_installed) = vcpkg_installed_root() { - if link_vcpkg(vcpkg_installed) { - return; - } - } - - panic!( - "Android libyuv not found!\n\ - \n\ - Build it with scripts/build-android-libyuv.sh and set:\n\ - export ONE_KVM_ANDROID_LIBYUV_ROOT=/path/to/android-libyuv\n\ - \n\ - Expected layout:\n\ - $ONE_KVM_ANDROID_LIBYUV_ROOT//include\n\ - $ONE_KVM_ANDROID_LIBYUV_ROOT//lib/libyuv.a" - ); - } - // Try vcpkg first if let Some(vcpkg_installed) = vcpkg_installed_root() { if link_vcpkg(vcpkg_installed) { @@ -148,217 +114,6 @@ fn link_libyuv() { ); } -fn link_android_libyuv() -> bool { - println!("cargo:rerun-if-env-changed=ONE_KVM_ANDROID_LIBYUV_ROOT"); - println!("cargo:rerun-if-env-changed=ONE_KVM_ANDROID_LIBYUV_STATIC"); - - let root = match env::var("ONE_KVM_ANDROID_LIBYUV_ROOT") - .ok() - .filter(|path| !path.trim().is_empty()) - { - Some(path) => PathBuf::from(path), - None => return false, - }; - - let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); - let abi = android_abi(&target_arch); - let abi_root = root.join(abi); - let lib_dir = if abi_root.join("lib").exists() { - abi_root.join("lib") - } else { - root.join("lib") - }; - let include_dir = if abi_root.join("include").exists() { - abi_root.join("include") - } else { - root.join("include") - }; - - let static_lib = lib_dir.join("libyuv.a"); - let shared_lib = lib_dir.join("libyuv.so"); - let use_static = env::var("ONE_KVM_ANDROID_LIBYUV_STATIC") - .or_else(|_| env::var("LIBYUV_STATIC")) - .map(|value| value != "0") - .unwrap_or(true); - - if use_static && static_lib.exists() { - println!("cargo:rustc-link-search=native={}", lib_dir.display()); - println!("cargo:rustc-link-lib=static=yuv"); - link_android_libjpeg(&root, abi); - println!("cargo:rustc-link-lib=c++_shared"); - println!( - "cargo:info=Using Android libyuv from {} (static linking)", - root.display() - ); - return true; - } - - if shared_lib.exists() { - println!("cargo:rustc-link-search=native={}", lib_dir.display()); - println!("cargo:rustc-link-lib=yuv"); - println!("cargo:rustc-link-lib=c++_shared"); - println!( - "cargo:info=Using Android libyuv from {} (dynamic linking)", - root.display() - ); - return true; - } - - println!( - "cargo:warning=Android libyuv not found under {} for ABI {} (checked {}, {})", - root.display(), - abi, - static_lib.display(), - shared_lib.display() - ); - if !include_dir.exists() { - println!( - "cargo:warning=Android libyuv include directory not found: {}", - include_dir.display() - ); - } - false -} - -fn link_android_libjpeg(libyuv_root: &Path, abi: &str) { - println!("cargo:rerun-if-env-changed=ONE_KVM_ANDROID_TURBOJPEG_ROOT"); - - let mut roots = Vec::new(); - if let Ok(root) = env::var("ONE_KVM_ANDROID_TURBOJPEG_ROOT") { - if !root.trim().is_empty() { - roots.push(PathBuf::from(root)); - } - } - roots.push(libyuv_root.with_file_name("android-turbojpeg")); - - for root in roots { - let abi_lib_dir = root.join(abi).join("lib"); - let lib_dir = if abi_lib_dir.exists() { - abi_lib_dir - } else { - root.join("lib") - }; - let jpeg_lib = lib_dir.join("libjpeg.a"); - if jpeg_lib.exists() { - println!("cargo:rustc-link-search=native={}", lib_dir.display()); - println!("cargo:rustc-link-lib=static=jpeg"); - println!( - "cargo:info=Using Android libjpeg for libyuv MJPEG from {}", - root.display() - ); - return; - } - } - - println!("cargo:warning=Android libjpeg.a not found; libyuv MJPEG symbols may fail to link"); -} - -fn android_abi(target_arch: &str) -> &'static str { - match target_arch { - "aarch64" => "arm64-v8a", - "arm" => "armeabi-v7a", - "x86" => "x86", - "x86_64" => "x86_64", - _ => "unknown", - } -} - -fn android_clang_args() -> Vec { - let ndk = android_ndk_home(); - let target = env::var("TARGET").unwrap_or_default(); - let toolchain = ndk.join("toolchains/llvm/prebuilt").join(host_tag()); - let sysroot = toolchain.join("sysroot"); - let clang_include = toolchain - .join("lib/clang") - .join(clang_version(&toolchain)) - .join("include"); - let api = env::var("CARGO_NDK_PLATFORM") - .ok() - .and_then(|value| value.parse::().ok()) - .unwrap_or(21); - let clang_target = android_clang_target(&target); - - vec![ - format!("--target={clang_target}"), - format!("--sysroot={}", sysroot.display()), - format!("-D__ANDROID_API__={api}"), - format!("-isystem{}", clang_include.display()), - format!("-isystem{}", sysroot.join("usr/include").display()), - format!( - "-isystem{}", - sysroot.join("usr/include").join(clang_target).display() - ), - ] -} - -fn android_clang_target(target: &str) -> &'static str { - match target { - "aarch64-linux-android" => "aarch64-linux-android", - "armv7-linux-androideabi" => "armv7a-linux-androideabi", - "i686-linux-android" => "i686-linux-android", - "x86_64-linux-android" => "x86_64-linux-android", - other => panic!("unsupported Android target for libyuv bindgen: {other}"), - } -} - -fn android_ndk_home() -> PathBuf { - for key in ["ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "NDK_HOME"] { - if let Ok(value) = env::var(key) { - return PathBuf::from(value); - } - } - - for key in ["ANDROID_HOME", "ANDROID_SDK_ROOT"] { - if let Ok(value) = env::var(key) { - let ndk_dir = PathBuf::from(value).join("ndk"); - if let Some(newest) = newest_child_dir(&ndk_dir) { - return newest; - } - } - } - - panic!( - "libyuv Android bindgen requires ANDROID_NDK_HOME, ANDROID_NDK_ROOT, NDK_HOME, \ - or ANDROID_HOME/ANDROID_SDK_ROOT with an ndk directory" - ); -} - -fn newest_child_dir(path: &Path) -> Option { - let mut entries = std::fs::read_dir(path) - .ok()? - .filter_map(|entry| entry.ok()) - .map(|entry| entry.path()) - .filter(|path| path.is_dir()) - .collect::>(); - entries.sort(); - entries.pop() -} - -fn host_tag() -> &'static str { - if cfg!(target_os = "linux") { - "linux-x86_64" - } else if cfg!(target_os = "macos") { - "darwin-x86_64" - } else if cfg!(target_os = "windows") { - "windows-x86_64" - } else { - panic!("unsupported host OS for Android NDK"); - } -} - -fn clang_version(toolchain: &Path) -> String { - let clang_dir = toolchain.join("lib/clang"); - let mut entries = std::fs::read_dir(&clang_dir) - .unwrap_or_else(|_| panic!("missing NDK clang directory: {}", clang_dir.display())) - .filter_map(|entry| entry.ok()) - .map(|entry| entry.file_name().to_string_lossy().into_owned()) - .collect::>(); - entries.sort(); - entries - .pop() - .unwrap_or_else(|| panic!("no clang versions found under: {}", clang_dir.display())) -} - fn vcpkg_installed_root() -> Option { println!("cargo:rerun-if-env-changed=VCPKG_INSTALLED_DIR"); println!("cargo:rerun-if-env-changed=VCPKG_ROOT"); @@ -383,10 +138,6 @@ fn link_vcpkg(mut path: PathBuf) -> bool { ("linux", "x86_64") => "x64-linux", ("linux", "aarch64") => "arm64-linux", ("linux", "arm") => "arm-linux", - ("android", "x86_64") => "x64-android", - ("android", "x86") => "x86-android", - ("android", "aarch64") => "arm64-android", - ("android", "arm") => "arm-neon-android", ("windows", "x86_64") => "x64-windows-static", ("windows", "x86") => "x86-windows-static", ("macos", "x86_64") => "x64-osx", @@ -426,8 +177,6 @@ fn link_vcpkg(mut path: PathBuf) -> bool { link_libjpeg_for_static_libyuv(&[lib_path.clone()], &target_os); if target_os == "linux" { println!("cargo:rustc-link-lib=stdc++"); - } else if target_os == "android" { - println!("cargo:rustc-link-lib=c++_shared"); } println!("cargo:info=Using libyuv from vcpkg (static linking)"); } else { @@ -435,8 +184,6 @@ fn link_vcpkg(mut path: PathBuf) -> bool { println!("cargo:rustc-link-lib=yuv"); if target_os == "linux" { println!("cargo:rustc-link-lib=stdc++"); - } else if target_os == "android" { - println!("cargo:rustc-link-lib=c++_shared"); } println!("cargo:info=Using libyuv from vcpkg (dynamic linking)"); } diff --git a/scripts/build-android-alsa.sh b/scripts/build-android-alsa.sh deleted file mode 100644 index 13a1448c..00000000 --- a/scripts/build-android-alsa.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - -OUTPUT_DIR="${PROJECT_ROOT}/dist/android-alsa" -ANDROID_API="${ANDROID_API:-21}" -NDK_ROOT="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}" -BUILD_ABIS="arm64-v8a armeabi-v7a" -JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}" -ALSA_VERSION="${ALSA_VERSION:-1.2.15}" - -usage() { - cat <<'EOF' -Usage: - scripts/build-android-alsa.sh [options] - -Options: - --output Output root. Default: dist/android-alsa - --ndk Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT. - --api Android API level. Default: 21. - --abis Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a. - -h, --help Show this help. - -The output layout is compatible with ONE_KVM_ANDROID_ALSA_ROOT: - /arm64-v8a/include/alsa/asoundlib.h - /arm64-v8a/lib/libasound.so - /arm64-v8a/lib/pkgconfig/alsa.pc - /armeabi-v7a/include/alsa/asoundlib.h - /armeabi-v7a/lib/libasound.so - /armeabi-v7a/lib/pkgconfig/alsa.pc -EOF -} - -fail() { - echo "Error: $*" >&2 - exit 1 -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --output) - OUTPUT_DIR="${2:-}" - shift 2 - ;; - --ndk) - NDK_ROOT="${2:-}" - shift 2 - ;; - --api) - ANDROID_API="${2:-}" - shift 2 - ;; - --abis) - BUILD_ABIS="${2:-}" - shift 2 - ;; - -h | --help) - usage - exit 0 - ;; - *) - fail "Unknown argument: $1" - ;; - esac -done - -[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required" -[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT" - -SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-alsa-src" -rm -rf "$SOURCE_DIR" -mkdir -p "${PROJECT_ROOT}/.tmp" -archive="${PROJECT_ROOT}/.tmp/alsa-lib-${ALSA_VERSION}.tar.bz2" -url="https://www.alsa-project.org/files/pub/lib/alsa-lib-${ALSA_VERSION}.tar.bz2" -echo "Downloading ALSA ${ALSA_VERSION}: $url" -curl -fL "$url" -o "$archive" -tar -xjf "$archive" -C "${PROJECT_ROOT}/.tmp" -mv "${PROJECT_ROOT}/.tmp/alsa-lib-${ALSA_VERSION}" "$SOURCE_DIR" - -SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)" -mkdir -p "$OUTPUT_DIR" -OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" - -HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64" -TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}" - -normalize_abis() { - printf '%s\n' "$BUILD_ABIS" | tr ',' ' ' -} - -clean_generated_source_headers() { - rm -f \ - "$SOURCE_DIR/include/asoundlib.h" \ - "$SOURCE_DIR/include/version.h" \ - "$SOURCE_DIR/include/stamp-vh" \ - "$SOURCE_DIR/include/alsa" -} - -build_one() { - local abi="$1" - local prefix build_dir - - case "$abi" in - arm64-v8a | armeabi-v7a) ;; - *) fail "Unsupported ABI: $abi" ;; - esac - - prefix="${OUTPUT_DIR}/${abi}" - build_dir="${PROJECT_ROOT}/.tmp/alsa-android-build/${abi}" - - rm -rf "$build_dir" - mkdir -p "$build_dir" "$prefix" - - case "$abi" in - arm64-v8a) - export CC="${TOOLCHAIN}/bin/aarch64-linux-android${ANDROID_API}-clang" - export CXX="${TOOLCHAIN}/bin/aarch64-linux-android${ANDROID_API}-clang++" - export HOST_TRIPLE="aarch64-linux-android" - ;; - armeabi-v7a) - export CC="${TOOLCHAIN}/bin/armv7a-linux-androideabi${ANDROID_API}-clang" - export CXX="${TOOLCHAIN}/bin/armv7a-linux-androideabi${ANDROID_API}-clang++" - export HOST_TRIPLE="arm-linux-androideabi" - ;; - esac - export AR="${TOOLCHAIN}/bin/llvm-ar" - export RANLIB="${TOOLCHAIN}/bin/llvm-ranlib" - export STRIP="${TOOLCHAIN}/bin/llvm-strip" - export CFLAGS="-fPIC" - export CXXFLAGS="-fPIC" - - clean_generated_source_headers - - if [[ ! -x "$SOURCE_DIR/configure" ]]; then - ( - cd "$SOURCE_DIR" - autoreconf -fi - ) - fi - - ( - cd "$build_dir" - pcm_plugins="copy linear route mulaw alaw adpcm rate plug multi file null empty meter hooks lfloat ladspa asym iec958 softvol extplug ioplug mmap_emul" - ctl_plugins="remap ext" - ac_cv_header_sys_shm_h=no \ - "$SOURCE_DIR/configure" \ - --host="$HOST_TRIPLE" \ - --prefix="$prefix" \ - --enable-shared \ - --disable-static \ - --disable-python \ - --with-pcm-plugins="$pcm_plugins" \ - --with-ctl-plugins="$ctl_plugins" \ - --disable-doc \ - --disable-oss \ - --disable-seq \ - --disable-ucm \ - --disable-topology \ - --disable-rawmidi \ - --disable-hwdep \ - --disable-usb \ - --disable-firewire \ - --disable-instr \ - --disable-alisp - make -j"$JOBS" - make install - ) - - mkdir -p "$prefix/lib/pkgconfig" - cat > "$prefix/lib/pkgconfig/alsa.pc" < Output root. Default: dist/android-ffmpeg-mediacodec - --ndk Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT. - --api Android API level. Default: 21. - --abis Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a. - -h, --help Show this help. - -The output layout is compatible with ONE_KVM_ANDROID_FFMPEG_ROOT: - /arm64-v8a/include - /arm64-v8a/lib - /armeabi-v7a/include - /armeabi-v7a/lib - -Example: - scripts/build-android-ffmpeg-mediacodec.sh --output /opt/one-kvm/android-ffmpeg - - export ONE_KVM_ANDROID_FFMPEG_ROOT=/opt/one-kvm/android-ffmpeg - cd android && ./gradlew :app:assembleDebug -EOF -} - -fail() { - echo "Error: $*" >&2 - exit 1 -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --output) - OUTPUT_DIR="${2:-}" - shift 2 - ;; - --ndk) - NDK_ROOT="${2:-}" - shift 2 - ;; - --api) - ANDROID_API="${2:-}" - shift 2 - ;; - --abis) - BUILD_ABIS="${2:-}" - shift 2 - ;; - -h | --help) - usage - exit 0 - ;; - *) - fail "Unknown argument: $1" - ;; - esac -done - -SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-ffmpeg-check/src/ffmpeg-rockchip" -rm -rf "$SOURCE_DIR" -mkdir -p "$(dirname "$SOURCE_DIR")" -repo_url="https://github.com/nyanmisaka/ffmpeg-rockchip.git" -if [[ "${CHINAMIRRO:-0}" == "1" ]]; then - repo_url="${GH_PROXY:-https://gh-proxy.com}" - repo_url="${repo_url%/}/https://github.com/nyanmisaka/ffmpeg-rockchip.git" -fi -echo "Cloning FFmpeg source: $repo_url" -git init "$SOURCE_DIR" -( - cd "$SOURCE_DIR" - git remote add origin "$repo_url" - git fetch --depth 1 origin "$FFMPEG_ROCKCHIP_REV" - git checkout --detach FETCH_HEAD -) - -[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required" - -SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)" -mkdir -p "$OUTPUT_DIR" -OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" - -HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64" -TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}" - -normalize_abis() { - printf '%s\n' "$BUILD_ABIS" | tr ',' ' ' -} - -patch_android_ffmpeg_mjpeg_mediacodec() { - local avcodec_dir="${SOURCE_DIR}/libavcodec" - local configure_file="${SOURCE_DIR}/configure" - local mediacodecdec="${avcodec_dir}/mediacodecdec.c" - local allcodecs="${avcodec_dir}/allcodecs.c" - local makefile="${avcodec_dir}/Makefile" - - python3 - "$mediacodecdec" "$allcodecs" "$configure_file" "$makefile" <<'PY' -from pathlib import Path -import sys - -mediacodecdec, allcodecs, configure_file, makefile = map(Path, sys.argv[1:]) - -def replace_once(path: Path, old: str, new: str) -> None: - text = path.read_text() - if new in text: - return - if old not in text: - raise SystemExit(f"patch anchor not found in {path}: {old!r}") - path.write_text(text.replace(old, new, 1)) - -replace_once( - mediacodecdec, - "CONFIG_MPEG2_MEDIACODEC_DECODER || \\\n", - "CONFIG_MJPEG_MEDIACODEC_DECODER || \\\n" - " CONFIG_MPEG2_MEDIACODEC_DECODER || \\\n", -) -replace_once( - mediacodecdec, - "#if CONFIG_MPEG2_MEDIACODEC_DECODER\n" - " case AV_CODEC_ID_MPEG2VIDEO:", - "#if CONFIG_MJPEG_MEDIACODEC_DECODER\n" - " case AV_CODEC_ID_MJPEG:\n" - " codec_mime = \"video/mjpeg\";\n\n" - " ret = common_set_extradata(avctx, format);\n" - " if (ret < 0)\n" - " goto done;\n" - " break;\n" - "#endif\n" - "#if CONFIG_MPEG2_MEDIACODEC_DECODER\n" - " case AV_CODEC_ID_MPEG2VIDEO:", -) -replace_once( - mediacodecdec, - "#if CONFIG_MPEG2_MEDIACODEC_DECODER\n" - "DECLARE_MEDIACODEC_VDEC(mpeg2, \"MPEG-2\", AV_CODEC_ID_MPEG2VIDEO, NULL)", - "#if CONFIG_MJPEG_MEDIACODEC_DECODER\n" - "DECLARE_MEDIACODEC_VDEC(mjpeg, \"MJPEG\", AV_CODEC_ID_MJPEG, NULL)\n" - "#endif\n\n" - "#if CONFIG_MPEG2_MEDIACODEC_DECODER\n" - "DECLARE_MEDIACODEC_VDEC(mpeg2, \"MPEG-2\", AV_CODEC_ID_MPEG2VIDEO, NULL)", -) -replace_once( - allcodecs, - "extern const FFCodec ff_mjpeg_cuvid_decoder;", - "extern const FFCodec ff_mjpeg_cuvid_decoder;\n" - "extern const FFCodec ff_mjpeg_mediacodec_decoder;", -) -replace_once( - configure_file, - 'mjpeg_cuvid_decoder_deps="cuvid"', - 'mjpeg_cuvid_decoder_deps="cuvid"\n' - 'mjpeg_mediacodec_decoder_deps="mediacodec"', -) -replace_once( - makefile, - "OBJS-$(CONFIG_MJPEG_RKMPP_DECODER)", - "OBJS-$(CONFIG_MJPEG_MEDIACODEC_DECODER) += mediacodecdec.o\n" - "OBJS-$(CONFIG_MJPEG_RKMPP_DECODER)", -) -PY -} - -abi_arch() { - case "$1" in - arm64-v8a) echo "aarch64" ;; - armeabi-v7a) echo "arm" ;; - *) fail "Unsupported ABI: $1" ;; - esac -} - -abi_cpu() { - case "$1" in - arm64-v8a) echo "armv8-a" ;; - armeabi-v7a) echo "armv7-a" ;; - *) fail "Unsupported ABI: $1" ;; - esac -} - -abi_target() { - case "$1" in - arm64-v8a) echo "aarch64-linux-android" ;; - armeabi-v7a) echo "armv7a-linux-androideabi" ;; - *) fail "Unsupported ABI: $1" ;; - esac -} - -build_one() { - local abi="$1" - local arch cpu target prefix build_dir cc cxx ar ranlib strip extra_cflags extra_ldflags - - arch="$(abi_arch "$abi")" - cpu="$(abi_cpu "$abi")" - target="$(abi_target "$abi")" - prefix="${OUTPUT_DIR}/${abi}" - build_dir="${PROJECT_ROOT}/.tmp/ffmpeg-android-build/${abi}" - cc="${TOOLCHAIN}/bin/${target}${ANDROID_API}-clang" - cxx="${TOOLCHAIN}/bin/${target}${ANDROID_API}-clang++" - ar="${TOOLCHAIN}/bin/llvm-ar" - ranlib="${TOOLCHAIN}/bin/llvm-ranlib" - strip="${TOOLCHAIN}/bin/llvm-strip" - extra_cflags="-fPIC" - extra_ldflags="" - - if [[ "$abi" == "armeabi-v7a" ]]; then - extra_cflags="${extra_cflags} -march=armv7-a -mfloat-abi=softfp -mfpu=neon" - extra_ldflags="${extra_ldflags} -Wl,--fix-cortex-a8" - fi - - rm -rf "$build_dir" - mkdir -p "$build_dir" "$prefix" - - ( - cd "$build_dir" - "${SOURCE_DIR}/configure" \ - --prefix="$prefix" \ - --target-os=android \ - --arch="$arch" \ - --cpu="$cpu" \ - --cc="$cc" \ - --cxx="$cxx" \ - --ar="$ar" \ - --ranlib="$ranlib" \ - --strip="$strip" \ - --cross-prefix="${TOOLCHAIN}/bin/llvm-" \ - --sysroot="${TOOLCHAIN}/sysroot" \ - --enable-cross-compile \ - --enable-static \ - --disable-shared \ - --disable-programs \ - --disable-doc \ - --disable-avdevice \ - --disable-avformat \ - --disable-avfilter \ - --disable-swscale \ - --disable-swresample \ - --disable-postproc \ - --disable-network \ - --disable-everything \ - --disable-hwaccels \ - --disable-cuda-llvm \ - --disable-v4l2-m2m \ - --disable-vulkan \ - --enable-pthreads \ - --enable-jni \ - --enable-mediacodec \ - --enable-decoder=mjpeg_mediacodec \ - --enable-decoder=mjpeg \ - --enable-encoder=h264_mediacodec \ - --enable-encoder=hevc_mediacodec \ - --enable-parser=mjpeg \ - --enable-bsf=h264_metadata \ - --enable-bsf=hevc_metadata \ - --enable-protocol=file \ - --extra-cflags="$extra_cflags" \ - --extra-ldflags="$extra_ldflags" - - make -j"$JOBS" - make install - ) - - echo "Built FFmpeg MediaCodec for ${abi}: ${prefix}" -} - -patch_android_ffmpeg_mjpeg_mediacodec - -for abi in $(normalize_abis); do - build_one "$abi" -done - -cat < Output root. Default: dist/android-libyuv - --ndk Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT. - --api Android API level. Default: 21. - --abis Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a. - --jpeg-root Android libjpeg root. Defaults to ONE_KVM_ANDROID_TURBOJPEG_ROOT - or dist/android-turbojpeg when present. Enables libyuv HAVE_JPEG. - -h, --help Show this help. - -The output layout is compatible with ONE_KVM_ANDROID_LIBYUV_ROOT: - /arm64-v8a/include - /arm64-v8a/lib/libyuv.a - /armeabi-v7a/include - /armeabi-v7a/lib/libyuv.a - -Example: - scripts/build-android-libyuv.sh --output /opt/one-kvm/android-libyuv - - export ONE_KVM_ANDROID_LIBYUV_ROOT=/opt/one-kvm/android-libyuv - cd android && ./gradlew :app:assembleDebug -EOF -} - -fail() { - echo "Error: $*" >&2 - exit 1 -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --output) - OUTPUT_DIR="${2:-}" - shift 2 - ;; - --ndk) - NDK_ROOT="${2:-}" - shift 2 - ;; - --api) - ANDROID_API="${2:-}" - shift 2 - ;; - --abis) - BUILD_ABIS="${2:-}" - shift 2 - ;; - --jpeg-root) - JPEG_ROOT="${2:-}" - shift 2 - ;; - -h | --help) - usage - exit 0 - ;; - *) - fail "Unknown argument: $1" - ;; - esac -done - -[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required" -[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT" - -SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-libyuv-src" -rm -rf "$SOURCE_DIR" -repo_url="https://github.com/lemenkov/libyuv.git" -if [[ "${CHINAMIRRO:-0}" == "1" ]]; then - repo_url="${GH_PROXY:-https://gh-proxy.com}" - repo_url="${repo_url%/}/https://github.com/lemenkov/libyuv.git" -fi -echo "Cloning libyuv source: $repo_url" -git init "$SOURCE_DIR" -( - cd "$SOURCE_DIR" - git remote add origin "$repo_url" - git fetch --depth 1 origin "$LIBYUV_REV" - git checkout --detach FETCH_HEAD -) - -SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)" -mkdir -p "$OUTPUT_DIR" -OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" - -HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64" -ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake" -[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE" - -normalize_abis() { - printf '%s\n' "$BUILD_ABIS" | tr ',' ' ' -} - -build_one() { - local abi="$1" - local prefix build_dir jpeg_include jpeg_library - local -a jpeg_args - - case "$abi" in - arm64-v8a | armeabi-v7a | x86 | x86_64) ;; - *) fail "Unsupported ABI: $abi" ;; - esac - - prefix="${OUTPUT_DIR}/${abi}" - build_dir="${PROJECT_ROOT}/.tmp/libyuv-android-build/${abi}" - - rm -rf "$build_dir" - mkdir -p "$build_dir" "$prefix" - - jpeg_include="$JPEG_ROOT/$abi/include" - jpeg_library="$JPEG_ROOT/$abi/lib/libjpeg.a" - jpeg_args=() - if [[ -f "$jpeg_library" && -f "$jpeg_include/jpeglib.h" ]]; then - jpeg_args=( - -DJPEG_FOUND=TRUE - -DJPEG_INCLUDE_DIR="$jpeg_include" - -DJPEG_LIBRARY="$jpeg_library" - -DCMAKE_C_FLAGS="-DHAVE_JPEG" - -DCMAKE_CXX_FLAGS="-DHAVE_JPEG" - ) - else - echo "Warning: Android libjpeg not found for ${abi}; libyuv MJPEG APIs will be disabled." >&2 - echo " Checked: $jpeg_library and $jpeg_include/jpeglib.h" >&2 - fi - - cmake -S "$SOURCE_DIR" -B "$build_dir" \ - -DCMAKE_TOOLCHAIN_FILE="$ANDROID_TOOLCHAIN_FILE" \ - -DANDROID_ABI="$abi" \ - -DANDROID_PLATFORM="android-${ANDROID_API}" \ - -DANDROID_STL=c++_shared \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX="$prefix" \ - -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ - -DBUILD_SHARED_LIBS=OFF \ - -DUNIT_TEST=OFF \ - -DTEST=OFF \ - "${jpeg_args[@]}" - - cmake --build "$build_dir" --target yuv --parallel "$JOBS" - - mkdir -p "$prefix/lib" "$prefix/include" - if [[ -f "$build_dir/libyuv.a" ]]; then - cp "$build_dir/libyuv.a" "$prefix/lib/libyuv.a" - elif [[ -f "$build_dir/lib/libyuv.a" ]]; then - cp "$build_dir/lib/libyuv.a" "$prefix/lib/libyuv.a" - else - fail "Built libyuv.a was not found under: $build_dir" - fi - cp -R "$SOURCE_DIR/include/." "$prefix/include/" - - echo "Built libyuv for ${abi}: ${prefix}" -} - -for abi in $(normalize_abis); do - build_one "$abi" -done - -cat < Output root. Default: dist/android-opus - --ndk Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT. - --api Android API level. Default: 21. - --abis Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a. - -h, --help Show this help. - -The output layout is compatible with ONE_KVM_ANDROID_OPUS_ROOT: - /arm64-v8a/include/opus/opus.h - /arm64-v8a/lib/libopus.so - /armeabi-v7a/include/opus/opus.h - /armeabi-v7a/lib/libopus.so -EOF -} - -fail() { - echo "Error: $*" >&2 - exit 1 -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --output) - OUTPUT_DIR="${2:-}" - shift 2 - ;; - --ndk) - NDK_ROOT="${2:-}" - shift 2 - ;; - --api) - ANDROID_API="${2:-}" - shift 2 - ;; - --abis) - BUILD_ABIS="${2:-}" - shift 2 - ;; - -h | --help) - usage - exit 0 - ;; - *) - fail "Unknown argument: $1" - ;; - esac -done - -[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required" -[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT" - -SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-opus-src" -rm -rf "$SOURCE_DIR" -mkdir -p "$SOURCE_DIR" -tarball="${PROJECT_ROOT}/.tmp/opus-${OPUS_VERSION}.tar.gz" -url="https://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz" -curl -fsSL "$url" -o "$tarball" -tar -xzf "$tarball" -C "$SOURCE_DIR" --strip-components=1 - -SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)" -mkdir -p "$OUTPUT_DIR" -OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" - -HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64" -TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}" - -normalize_abis() { - printf '%s\n' "$BUILD_ABIS" | tr ',' ' ' -} - -build_one() { - local abi="$1" - local prefix build_dir - - case "$abi" in - arm64-v8a | armeabi-v7a) ;; - *) fail "Unsupported ABI: $abi" ;; - esac - - prefix="${OUTPUT_DIR}/${abi}" - build_dir="${PROJECT_ROOT}/.tmp/opus-android-build/${abi}" - - rm -rf "$build_dir" - mkdir -p "$build_dir" "$prefix" - - ( - cd "$build_dir" - case "$abi" in - arm64-v8a) - export CC="${TOOLCHAIN}/bin/aarch64-linux-android${ANDROID_API}-clang" - export CXX="${TOOLCHAIN}/bin/aarch64-linux-android${ANDROID_API}-clang++" - export HOST_TRIPLE="aarch64-linux-android" - ;; - armeabi-v7a) - export CC="${TOOLCHAIN}/bin/armv7a-linux-androideabi${ANDROID_API}-clang" - export CXX="${TOOLCHAIN}/bin/armv7a-linux-androideabi${ANDROID_API}-clang++" - export HOST_TRIPLE="arm-linux-androideabi" - ;; - esac - export AR="${TOOLCHAIN}/bin/llvm-ar" - export RANLIB="${TOOLCHAIN}/bin/llvm-ranlib" - export STRIP="${TOOLCHAIN}/bin/llvm-strip" - export CFLAGS="-fPIC" - export CXXFLAGS="-fPIC" - export LDFLAGS="" - "$SOURCE_DIR/configure" \ - --prefix="$prefix" \ - --host="$HOST_TRIPLE" \ - --disable-static \ - --enable-shared \ - --disable-doc \ - --disable-extra-programs \ - --with-pic - make -j"$JOBS" - make install - ) - - echo "Built Opus for ${abi}: ${prefix}" -} - -for abi in $(normalize_abis); do - build_one "$abi" -done - -cat < Output root. Default: dist/android-turbojpeg - --ndk Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT. - --api Android API level. Default: 21. - --abis Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a. - -h, --help Show this help. - -The output layout is compatible with ONE_KVM_ANDROID_TURBOJPEG_ROOT: - /arm64-v8a/include/turbojpeg.h - /arm64-v8a/lib/libturbojpeg.a - /arm64-v8a/include/jpeglib.h - /arm64-v8a/lib/libjpeg.a - /armeabi-v7a/include/turbojpeg.h - /armeabi-v7a/lib/libturbojpeg.a - /armeabi-v7a/include/jpeglib.h - /armeabi-v7a/lib/libjpeg.a -EOF -} - -fail() { - echo "Error: $*" >&2 - exit 1 -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --output) - OUTPUT_DIR="${2:-}" - shift 2 - ;; - --ndk) - NDK_ROOT="${2:-}" - shift 2 - ;; - --api) - ANDROID_API="${2:-}" - shift 2 - ;; - --abis) - BUILD_ABIS="${2:-}" - shift 2 - ;; - -h | --help) - usage - exit 0 - ;; - *) - fail "Unknown argument: $1" - ;; - esac -done - -[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required" -[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT" - -SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-turbojpeg-src" -rm -rf "$SOURCE_DIR" -repo_url="https://github.com/libjpeg-turbo/libjpeg-turbo.git" -if [[ "${CHINAMIRRO:-0}" == "1" ]]; then - repo_url="${GH_PROXY:-https://gh-proxy.com}" - repo_url="${repo_url%/}/https://github.com/libjpeg-turbo/libjpeg-turbo.git" -fi -echo "Cloning libjpeg-turbo ${LIBJPEG_TURBO_VERSION}: $repo_url" -git init "$SOURCE_DIR" -( - cd "$SOURCE_DIR" - git remote add origin "$repo_url" - git fetch --depth 1 origin "refs/tags/$LIBJPEG_TURBO_VERSION" - git checkout --detach FETCH_HEAD -) - -SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)" -mkdir -p "$OUTPUT_DIR" -OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" - -HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64" -ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake" -[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE" - -normalize_abis() { - printf '%s\n' "$BUILD_ABIS" | tr ',' ' ' -} - -build_one() { - local abi="$1" - local prefix build_dir lib_path - - case "$abi" in - arm64-v8a | armeabi-v7a | x86 | x86_64) ;; - *) fail "Unsupported ABI: $abi" ;; - esac - - prefix="${OUTPUT_DIR}/${abi}" - build_dir="${PROJECT_ROOT}/.tmp/turbojpeg-android-build/${abi}" - - rm -rf "$build_dir" - mkdir -p "$build_dir" "$prefix" - - cmake -S "$SOURCE_DIR" -B "$build_dir" \ - -DCMAKE_TOOLCHAIN_FILE="$ANDROID_TOOLCHAIN_FILE" \ - -DANDROID_ABI="$abi" \ - -DANDROID_PLATFORM="android-${ANDROID_API}" \ - -DANDROID_STL=c++_shared \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX="$prefix" \ - -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ - -DCMAKE_C_FLAGS="-DANDROID -Dstderr=__sF+2" \ - -DCMAKE_CXX_FLAGS="-DANDROID -Dstderr=__sF+2" \ - -DENABLE_SHARED=OFF \ - -DENABLE_STATIC=ON \ - -DWITH_TURBOJPEG=ON \ - -DWITH_JAVA=OFF \ - -DWITH_12BIT=OFF \ - -DWITH_ARITH_DEC=ON \ - -DWITH_ARITH_ENC=ON - - cmake --build "$build_dir" --target turbojpeg-static jpeg-static --parallel "$JOBS" - - mkdir -p "$prefix/lib" "$prefix/include" - lib_path="$build_dir/libturbojpeg.a" - if [[ ! -f "$lib_path" ]]; then - lib_path="$build_dir/lib/libturbojpeg.a" - fi - [[ -f "$lib_path" ]] || fail "Built libturbojpeg.a was not found under: $build_dir" - - cp "$lib_path" "$prefix/lib/libturbojpeg.a" - lib_path="$build_dir/libjpeg.a" - if [[ ! -f "$lib_path" ]]; then - lib_path="$build_dir/lib/libjpeg.a" - fi - [[ -f "$lib_path" ]] || fail "Built libjpeg.a was not found under: $build_dir" - - cp "$lib_path" "$prefix/lib/libjpeg.a" - cp "$SOURCE_DIR/src/turbojpeg.h" "$prefix/include/turbojpeg.h" - cp "$SOURCE_DIR/src/jerror.h" "$prefix/include/jerror.h" - cp "$SOURCE_DIR/src/jmorecfg.h" "$prefix/include/jmorecfg.h" - cp "$SOURCE_DIR/src/jpeglib.h" "$prefix/include/jpeglib.h" - cp "$build_dir/jconfig.h" "$prefix/include/jconfig.h" - - echo "Built TurboJPEG for ${abi}: ${prefix}" -} - -for abi in $(normalize_abis); do - build_one "$abi" -done - -cat < Self { - Self { - device_name: String::new(), - sample_rate: 48_000, - channels: 2, - frame_size: 960, - buffer_frames: 4096, - period_frames: 960, - } - } -} - -impl AudioConfig { - pub fn for_device(device: &AudioDeviceInfo) -> Self { - Self { - device_name: device.name.clone(), - ..Default::default() - } - } - - pub fn bytes_per_sample(&self) -> u32 { - 2 * self.channels - } - - pub fn bytes_per_frame(&self) -> usize { - (self.frame_size * self.bytes_per_sample()) as usize - } -} - -#[derive(Debug, Clone)] -pub struct AudioFrame { - pub data: Bytes, - pub sample_rate: u32, - pub channels: u32, - pub samples: u32, - pub sequence: u64, - pub timestamp: Instant, -} - -impl AudioFrame { - pub fn new_interleaved(data: Bytes, channels: u32, sample_rate: u32, sequence: u64) -> Self { - let bps = 2 * channels; - Self { - samples: data.len() as u32 / bps, - data, - sample_rate, - channels, - sequence, - timestamp: Instant::now(), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CaptureState { - Stopped, - Running, - Error, -} - -pub struct AudioCapturer { - config: AudioConfig, - state: Arc>, - state_rx: watch::Receiver, - frame_tx: broadcast::Sender, - stop_flag: Arc, - sequence: Arc, - capture_handle: Mutex>>, - log_throttler: LogThrottler, -} - -impl AudioCapturer { - pub fn new(config: AudioConfig) -> Self { - let (state_tx, state_rx) = watch::channel(CaptureState::Stopped); - let (frame_tx, _) = broadcast::channel(16); - - Self { - config, - state: Arc::new(state_tx), - state_rx, - frame_tx, - stop_flag: Arc::new(AtomicBool::new(false)), - sequence: Arc::new(AtomicU64::new(0)), - capture_handle: Mutex::new(None), - log_throttler: LogThrottler::with_secs(5), - } - } - - pub fn state(&self) -> CaptureState { - *self.state_rx.borrow() - } - - pub fn state_watch(&self) -> watch::Receiver { - self.state_rx.clone() - } - - pub fn subscribe(&self) -> broadcast::Receiver { - self.frame_tx.subscribe() - } - - pub async fn start(&self) -> Result<()> { - if self.state() == CaptureState::Running { - return Ok(()); - } - - debug!( - "Starting audio capture on {} at {}Hz {}ch", - self.config.device_name, self.config.sample_rate, self.config.channels - ); - - self.stop_flag.store(false, Ordering::SeqCst); - - let config = self.config.clone(); - let state = self.state.clone(); - let frame_tx = self.frame_tx.clone(); - let stop_flag = self.stop_flag.clone(); - let sequence = self.sequence.clone(); - let log_throttler = self.log_throttler.clone(); - - let handle = tokio::task::spawn_blocking(move || { - let result = run_capture( - &config, - &state, - &frame_tx, - &stop_flag, - &sequence, - &log_throttler, - ); - - if let Err(e) = result { - error_throttled!(log_throttler, "capture_error", "Audio capture error: {}", e); - let _ = state.send(CaptureState::Error); - } else { - let _ = state.send(CaptureState::Stopped); - } - }); - - *self.capture_handle.lock().await = Some(handle); - Ok(()) - } - - pub async fn stop(&self) -> Result<()> { - info!("Stopping audio capture"); - self.stop_flag.store(true, Ordering::SeqCst); - - if let Some(handle) = self.capture_handle.lock().await.take() { - let _ = handle.await; - } - - let _ = self.state.send(CaptureState::Stopped); - Ok(()) - } - - pub fn is_running(&self) -> bool { - self.state() == CaptureState::Running - } -} - -fn run_capture( - config: &AudioConfig, - state: &watch::Sender, - frame_tx: &broadcast::Sender, - stop_flag: &AtomicBool, - sequence: &AtomicU64, - log_throttler: &LogThrottler, -) -> Result<()> { - let pcm = PCM::new(&config.device_name, Direction::Capture, false).map_err(|e| { - AppError::AudioError(format!( - "Failed to open audio device {}: {}", - config.device_name, e - )) - })?; - - { - let hwp = HwParams::any(&pcm) - .map_err(|e| AppError::AudioError(format!("Failed to get HwParams: {}", e)))?; - - hwp.set_channels(config.channels) - .map_err(|e| AppError::AudioError(format!("Failed to set channels: {}", e)))?; - hwp.set_rate(config.sample_rate, ValueOr::Nearest) - .map_err(|e| AppError::AudioError(format!("Failed to set sample rate: {}", e)))?; - hwp.set_format(Format::s16()) - .map_err(|e| AppError::AudioError(format!("Failed to set format: {}", e)))?; - hwp.set_access(Access::RWInterleaved) - .map_err(|e| AppError::AudioError(format!("Failed to set access: {}", e)))?; - hwp.set_buffer_size_near(config.buffer_frames as Frames) - .map_err(|e| AppError::AudioError(format!("Failed to set buffer size: {}", e)))?; - hwp.set_period_size_near(config.period_frames as Frames, ValueOr::Nearest) - .map_err(|e| AppError::AudioError(format!("Failed to set period size: {}", e)))?; - pcm.hw_params(&hwp) - .map_err(|e| AppError::AudioError(format!("Failed to apply hw params: {}", e)))?; - } - - let hw_now = pcm.hw_params_current().map_err(|e| { - AppError::AudioError(format!("Failed to read hw_params after apply: {}", e)) - })?; - let actual_rate = hw_now - .get_rate() - .map_err(|e| AppError::AudioError(format!("Failed to read sample rate: {}", e)))?; - let actual_ch = hw_now - .get_channels() - .map_err(|e| AppError::AudioError(format!("Failed to read channels: {}", e)))?; - if actual_rate != 48_000 { - return Err(AppError::AudioError(format!( - "Audio capture requires 48000 Hz; device is {} Hz", - actual_rate - ))); - } - if actual_ch != 2 { - return Err(AppError::AudioError(format!( - "Audio capture requires 2 channels (stereo); device has {}", - actual_ch - ))); - } - debug!("Audio capture: 48000 Hz, 2 ch"); - - pcm.prepare() - .map_err(|e| AppError::AudioError(format!("Failed to prepare PCM: {}", e)))?; - let _ = state.send(CaptureState::Running); - - let period_frames = pcm - .hw_params_current() - .ok() - .and_then(|h| h.get_period_size().ok()) - .map(|f| f as usize) - .unwrap_or(1024) - .max(256); - let buf_frames = period_frames.saturating_mul(4).max(2048); - let io = pcm - .io_i16() - .map_err(|e| AppError::AudioError(format!("Failed to get PCM IO: {}", e)))?; - - let mut buffer = vec![0i16; buf_frames * 2]; - let mut next_log = Instant::now(); - - while !stop_flag.load(Ordering::SeqCst) { - match io.readi(&mut buffer[..period_frames * 2]) { - Ok(frames_read) => { - if frames_read == 0 { - continue; - } - let samples = frames_read * 2; - let data = Bytes::copy_from_slice(bytemuck::cast_slice(&buffer[..samples])); - let seq = sequence.fetch_add(1, Ordering::SeqCst); - let frame = AudioFrame::new_interleaved(data, 2, 48_000, seq); - let _ = frame_tx.send(frame); - if next_log.elapsed().as_secs() >= 5 { - debug!("Captured audio frame {} ({} samples)", seq, samples / 2); - next_log = Instant::now(); - } - } - Err(err) => { - warn_throttled!( - log_throttler, - "alsa_read", - "ALSA read error on {}: {}", - config.device_name, - err - ); - let _ = pcm.try_recover(err, false); - } - } - } - - let _ = pcm.drain(); - Ok(()) -} diff --git a/src/audio/device.rs b/src/audio/device.rs index e19c8a74..e8752ac2 100644 --- a/src/audio/device.rs +++ b/src/audio/device.rs @@ -1,11 +1,7 @@ -#[cfg(all(unix, not(feature = "android")))] +#[cfg(unix)] #[path = "device_linux.rs"] mod imp; -#[cfg(feature = "android")] -#[path = "device_android.rs"] -mod imp; - #[cfg(windows)] #[path = "device_windows.rs"] mod imp; diff --git a/src/audio/device_android.rs b/src/audio/device_android.rs deleted file mode 100644 index dc20514e..00000000 --- a/src/audio/device_android.rs +++ /dev/null @@ -1,185 +0,0 @@ -use alsa::pcm::HwParams; -use alsa::{Direction, PCM}; -use serde::Serialize; -use tracing::{debug, info, warn}; - -use crate::error::{AppError, Result}; - -#[derive(Debug, Clone, Serialize)] -pub struct AudioDeviceInfo { - pub name: String, - pub description: String, - pub card_index: i32, - pub device_index: i32, - pub sample_rates: Vec, - pub channels: Vec, - pub is_capture: bool, - pub is_hdmi: bool, - pub usb_bus: Option, -} - -fn get_usb_bus_info(card_index: i32) -> Option { - if card_index < 0 { - return None; - } - - let device_path = format!("/sys/class/sound/card{}/device", card_index); - let link_target = std::fs::read_link(&device_path).ok()?; - let link_str = link_target.to_string_lossy(); - - for component in link_str.split('/') { - if component.contains('-') && !component.contains(':') { - if component - .chars() - .next() - .map(|c| c.is_ascii_digit()) - .unwrap_or(false) - { - return Some(component.to_string()); - } - } - } - - None -} - -pub fn enumerate_audio_devices() -> Result> { - enumerate_audio_devices_with_current(None) -} - -pub fn enumerate_audio_devices_with_current( - current_device: Option<&str>, -) -> Result> { - let mut devices = Vec::new(); - - for card_result in alsa::card::Iter::new() { - let card = match card_result { - Ok(card) => card, - Err(err) => { - debug!("Error iterating card: {}", err); - continue; - } - }; - - let card_index = card.get_index(); - let card_name = card.get_name().unwrap_or_else(|_| "Unknown".to_string()); - let card_longname = card.get_longname().unwrap_or_else(|_| card_name.clone()); - - debug!("Found audio card {}: {}", card_index, card_longname); - - let long_lower = card_longname.to_lowercase(); - let is_hdmi = long_lower.contains("hdmi") - || long_lower.contains("capture") - || long_lower.contains("usb"); - let usb_bus = get_usb_bus_info(card_index); - - for device_index in 0..8 { - let device_name = format!("hw:{},{}", card_index, device_index); - let is_current_device = current_device == Some(device_name.as_str()); - - let mut push_info = - |sample_rates: Vec, channels: Vec, description: String| { - devices.push(AudioDeviceInfo { - name: device_name.clone(), - description, - card_index, - device_index, - sample_rates, - channels, - is_capture: true, - is_hdmi, - usb_bus: usb_bus.clone(), - }); - }; - - match PCM::new(&device_name, Direction::Capture, false) { - Ok(pcm) => { - let (sample_rates, channels) = query_device_caps(&pcm); - if !sample_rates.is_empty() && !channels.is_empty() { - push_info( - sample_rates, - channels, - format!("{} - Device {}", card_longname, device_index), - ); - } - } - Err(_) if is_current_device => { - debug!( - "Device {} is busy (in use by us), adding with default caps", - device_name - ); - push_info( - vec![44_100, 48_000], - vec![2], - format!("{} - Device {} (in use)", card_longname, device_index), - ); - } - Err(_) => {} - } - } - } - - info!("Found {} audio capture devices", devices.len()); - Ok(devices) -} - -fn query_device_caps(pcm: &PCM) -> (Vec, Vec) { - let hwp = match HwParams::any(pcm) { - Ok(h) => h, - Err(_) => return (vec![], vec![]), - }; - - let common_rates = [8000, 16000, 22050, 44100, 48000, 96000]; - let mut supported_rates = Vec::new(); - - for rate in &common_rates { - if hwp.test_rate(*rate).is_ok() { - supported_rates.push(*rate); - } - } - - let mut supported_channels = Vec::new(); - for ch in 1..=8 { - if hwp.test_channels(ch).is_ok() { - supported_channels.push(ch); - } - } - - (supported_rates, supported_channels) -} - -pub fn find_best_audio_device() -> Result { - let devices = enumerate_audio_devices()?; - - if devices.is_empty() { - return Err(AppError::AudioError( - "No audio capture devices found".to_string(), - )); - } - - let mut first_48k_stereo: Option<&AudioDeviceInfo> = None; - for device in &devices { - if !device.sample_rates.contains(&48_000) || !device.channels.contains(&2) { - continue; - } - if device.is_hdmi { - info!("Selected HDMI audio device: {}", device.description); - return Ok(device.clone()); - } - if first_48k_stereo.is_none() { - first_48k_stereo = Some(device); - } - } - - if let Some(device) = first_48k_stereo { - info!("Selected audio device: {}", device.description); - return Ok(device.clone()); - } - - let device = devices.into_iter().next().unwrap(); - warn!( - "Using fallback audio device: {} (may not support optimal settings)", - device.description - ); - Ok(device) -} diff --git a/src/diagnostics/linux.rs b/src/diagnostics/linux.rs index e989100a..2045ffd0 100644 --- a/src/diagnostics/linux.rs +++ b/src/diagnostics/linux.rs @@ -176,18 +176,9 @@ fn get_meminfo() -> MemInfo { } fn get_network_addresses() -> Vec { - #[cfg(target_os = "android")] - { - return get_network_addresses_android(); - } - - #[cfg(not(target_os = "android"))] - { - get_network_addresses_ifaddrs() - } + get_network_addresses_ifaddrs() } -#[cfg(not(target_os = "android"))] fn get_network_addresses_ifaddrs() -> Vec { let all_addrs = match nix::ifaddrs::getifaddrs() { Ok(addrs) => addrs, @@ -260,101 +251,6 @@ fn get_network_addresses_ifaddrs() -> Vec { addresses } -#[cfg(target_os = "android")] -fn get_network_addresses_android() -> Vec { - let net_dir = match std::fs::read_dir("/sys/class/net") { - Ok(dir) => dir, - Err(_) => return Vec::new(), - }; - - let mut addresses = Vec::new(); - let mut seen = std::collections::HashSet::new(); - - for entry in net_dir.flatten() { - let iface_name = match entry.file_name().into_string() { - Ok(name) => name, - Err(_) => continue, - }; - - if iface_name == "lo" { - continue; - } - - let operstate_path = entry.path().join("operstate"); - let is_up = std::fs::read_to_string(&operstate_path) - .map(|s| s.trim() == "up") - .unwrap_or(false); - if !is_up { - continue; - } - - let Some(ip) = android_ipv4_for_interface(&iface_name) else { - continue; - }; - if ip.is_loopback() || ip.is_unspecified() { - continue; - } - - let ip_str = ip.to_string(); - if seen.insert((iface_name.clone(), ip_str.clone())) { - addresses.push(NetworkAddress { - interface: iface_name, - ip: ip_str, - }); - } - } - - addresses -} - -#[cfg(target_os = "android")] -fn android_ipv4_for_interface(iface_name: &str) -> Option { - use std::ffi::CString; - use std::mem::{size_of, zeroed}; - - let name = CString::new(iface_name).ok()?; - if name.as_bytes().len() >= libc::IFNAMSIZ { - return None; - } - - unsafe { - let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0); - if fd < 0 { - return None; - } - - let mut request: libc::ifreq = zeroed(); - std::ptr::copy_nonoverlapping( - name.as_ptr(), - request.ifr_name.as_mut_ptr(), - name.as_bytes_with_nul().len(), - ); - - let request_code = libc::SIOCGIFADDR.try_into().ok()?; - let result = libc::ioctl(fd, request_code, &mut request); - libc::close(fd); - if result < 0 { - return None; - } - - let sockaddr = request.ifr_ifru.ifru_addr; - if sockaddr.sa_family as libc::c_int != libc::AF_INET { - return None; - } - - let mut storage = [0u8; size_of::()]; - std::ptr::copy_nonoverlapping( - &sockaddr as *const libc::sockaddr as *const u8, - storage.as_mut_ptr(), - size_of::(), - ); - let sockaddr_in = &*(storage.as_ptr() as *const libc::sockaddr_in); - Some(std::net::Ipv4Addr::from(u32::from_be( - sockaddr_in.sin_addr.s_addr, - ))) - } -} - #[cfg(test)] mod tests { use super::{parse_cpu_model_from_cpuinfo_content, parse_device_tree_model_bytes}; diff --git a/src/lib.rs b/src/lib.rs index f2c4f0c1..610fd9c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,68 +1,65 @@ //! Core library for One-KVM (IP‑KVM: capture, HID, OTG, streaming, Web UI glue). -#[cfg(not(any(feature = "android", unix, windows)))] +#[cfg(not(any(target_os = "linux", windows)))] compile_error!("One-KVM supports Linux and Windows targets only."); -#[cfg(any(feature = "android", feature = "desktop"))] -pub mod runtime; - -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod atx; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod audio; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod auth; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod computer_use; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod config; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod db; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod diagnostics; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod error; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod events; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod extensions; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod hid; -#[cfg(all(unix, any(feature = "android", feature = "desktop")))] +#[cfg(all(unix, feature = "desktop"))] pub mod msd; -#[cfg(all(unix, any(feature = "android", feature = "desktop")))] +#[cfg(all(unix, feature = "desktop"))] pub mod otg; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod platform; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod redfish; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod rtsp; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod rustdesk; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod state; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod stream; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod stream_encoder; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod update; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod utils; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod video; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod vnc; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod web; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod webrtc; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod secrets { include!(concat!(env!("OUT_DIR"), "/secrets_generated.rs")); } -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub use error::{AppError, Result}; diff --git a/src/main.rs b/src/main.rs index 8039b9ef..6cb8f2b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,12 @@ struct CliArgs { address: Option, /// HTTP port (overrides database config) - #[arg(short = 'p', long, value_name = "PORT")] + #[arg( + short = 'p', + long = "port", + visible_alias = "http-port", + value_name = "PORT" + )] http_port: Option, /// HTTPS port (overrides database config) @@ -84,7 +89,7 @@ struct CliArgs { #[arg(long, value_name = "FILE", requires = "ssl_cert")] ssl_key: Option, - /// Data directory path (default: /etc/one-kvm, or the executable directory on Windows) + /// Data directory path #[arg(short = 'd', long, value_name = "DIR")] data_dir: Option, @@ -668,9 +673,11 @@ async fn main() -> anyhow::Result<()> { let mut shutdown_rx = shutdown_tx.subscribe(); async move { tokio::select! { - result = tokio::signal::ctrl_c() => { - result.expect("Failed to install CTRL+C handler"); - tracing::info!("Shutdown signal received"); + result = shutdown_signal() => { + if let Err(e) = result { + tracing::error!("Failed while waiting for shutdown signal: {}", e); + } + tracing::info!("SIGINT or SIGTERM received"); ShutdownAction::Exit } request = shutdown_rx.recv() => { @@ -794,6 +801,24 @@ fn get_data_dir() -> PathBuf { PathBuf::from("/etc/one-kvm") } +#[cfg(unix)] +async fn shutdown_signal() -> anyhow::Result<()> { + use tokio::signal::unix::{signal, SignalKind}; + + let mut terminate = signal(SignalKind::terminate())?; + tokio::select! { + result = tokio::signal::ctrl_c() => result?, + _ = terminate.recv() => {}, + } + Ok(()) +} + +#[cfg(not(unix))] +async fn shutdown_signal() -> anyhow::Result<()> { + tokio::signal::ctrl_c().await?; + Ok(()) +} + async fn open_database_pool(data_dir: &Path) -> anyhow::Result { let db_path = data_dir.join("one-kvm.db"); let db = DatabasePool::new(&db_path).await?; @@ -1207,6 +1232,11 @@ async fn cleanup(state: &Arc) { } } + #[cfg(unix)] + if let Err(e) = state.otg_service.shutdown().await { + tracing::warn!("Failed to shutdown OTG: {}", e); + } + if let Some(atx) = state.atx.write().await.as_mut() { if let Err(e) = atx.shutdown().await { tracing::warn!("Failed to shutdown ATX: {}", e); diff --git a/src/msd/controller.rs b/src/msd/controller.rs index d8b9de85..b742aefd 100644 --- a/src/msd/controller.rs +++ b/src/msd/controller.rs @@ -114,6 +114,28 @@ impl MsdController { } pub async fn mount_image(&self, image: &ImageInfo, cdrom: bool, read_only: bool) -> Result<()> { + self.mount_image_in_slot(image, cdrom, read_only, None) + .await + } + + pub async fn mount_image_at_lun( + &self, + image: &ImageInfo, + cdrom: bool, + read_only: bool, + lun: u8, + ) -> Result<()> { + self.mount_image_in_slot(image, cdrom, read_only, Some(lun)) + .await + } + + async fn mount_image_in_slot( + &self, + image: &ImageInfo, + cdrom: bool, + read_only: bool, + requested_lun: Option, + ) -> Result<()> { let _op_guard = self.operation_lock.write().await; let mut state = self.state.write().await; let previous_state = state.clone(); @@ -136,8 +158,7 @@ impl MsdController { return Err(AppError::BadRequest("Image is already mounted".to_string())); } - let lun = Self::lowest_free_lun(&state) - .ok_or_else(|| AppError::BadRequest("Media slots are full".to_string()))?; + let lun = Self::select_lun(&state, requested_lun)?; let media = MountedMedia::image(lun, image, cdrom, read_only); if let Err(e) = self.configure_media(&media).await { @@ -250,6 +271,27 @@ impl MsdController { .find(|lun| !state.mounted_media.iter().any(|media| media.lun == *lun)) } + fn select_lun(state: &MsdState, requested_lun: Option) -> Result { + let Some(lun) = requested_lun else { + return Self::lowest_free_lun(state) + .ok_or_else(|| AppError::BadRequest("Media slots are full".to_string())); + }; + + if lun >= state.disk_mode.capacity() { + return Err(AppError::BadRequest(format!( + "Media slot {} is outside the current disk mode capacity", + lun + 1 + ))); + } + if state.mounted_media.iter().any(|media| media.lun == lun) { + return Err(AppError::BadRequest(format!( + "Media slot {} is already occupied", + lun + 1 + ))); + } + Ok(lun) + } + fn reset_mounts_for_mode(state: &mut MsdState, disk_mode: DiskMode) { state.disk_mode = disk_mode; state.mounted_media.clear(); @@ -326,14 +368,20 @@ impl MsdController { pub async fn unmount_image(&self, image_id: &str) -> Result<()> { self.unmount_media(|media| media.kind == MountedMediaKind::Image && media.id == image_id) .await + .map(|_| ()) } pub async fn unmount_drive(&self) -> Result<()> { self.unmount_media(|media| media.kind == MountedMediaKind::Drive) .await + .map(|_| ()) } - async fn unmount_media(&self, predicate: F) -> Result<()> + pub async fn unmount_lun(&self, lun: u8) -> Result { + self.unmount_media(|media| media.lun == lun).await + } + + async fn unmount_media(&self, predicate: F) -> Result where F: Fn(&MountedMedia) -> bool, { @@ -342,7 +390,7 @@ impl MsdController { let mut state = self.state.write().await; let Some(index) = state.mounted_media.iter().position(predicate) else { debug!("Requested media was not mounted, skipping unmount"); - return Ok(()); + return Ok(false); }; let media = state.mounted_media[index].clone(); @@ -355,7 +403,7 @@ impl MsdController { self.mark_device_info_dirty().await; - Ok(()) + Ok(true) } async fn configure_media(&self, media: &MountedMedia) -> Result<()> { @@ -710,6 +758,29 @@ mod tests { assert_eq!(MsdController::lowest_free_lun(&state), Some(2)); } + #[test] + fn explicit_lun_selection_rejects_occupied_and_out_of_range_slots() { + let temp_dir = TempDir::new().unwrap(); + let image_path = temp_dir.path().join("test.img"); + std::fs::write(&image_path, b"img").unwrap(); + let image = ImageInfo::new("test".into(), "test.img".into(), image_path, 3); + let mut state = MsdState::default(); + MsdController::reset_mounts_for_mode(&mut state, DiskMode::Multi); + state + .mounted_media + .push(MountedMedia::image(3, &image, false, true)); + + assert_eq!(MsdController::select_lun(&state, Some(5)).unwrap(), 5); + assert!(MsdController::select_lun(&state, Some(3)) + .unwrap_err() + .to_string() + .contains("already occupied")); + assert!(MsdController::select_lun(&state, Some(8)) + .unwrap_err() + .to_string() + .contains("outside")); + } + #[test] fn multi_disk_mode_supports_eight_images_and_rejects_ninth_slot() { let temp_dir = TempDir::new().unwrap(); diff --git a/src/otg/configfs.rs b/src/otg/configfs.rs index 58ae0d4b..07bb19ec 100644 --- a/src/otg/configfs.rs +++ b/src/otg/configfs.rs @@ -13,7 +13,11 @@ pub const DEFAULT_USB_BCD_DEVICE: u16 = 0x0100; pub const USB_BCD_USB: u16 = 0x0200; pub fn is_configfs_available() -> bool { - Path::new(CONFIGFS_PATH).exists() + configfs_path().exists() +} + +pub fn configfs_path() -> &'static Path { + Path::new(CONFIGFS_PATH) } /// Loads `libcomposite` if needed; does not mount configfs. diff --git a/src/otg/manager.rs b/src/otg/manager.rs index 4a0b2f9f..c0bb0a5c 100644 --- a/src/otg/manager.rs +++ b/src/otg/manager.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use tracing::{debug, error, info, warn}; use super::configfs::{ - create_dir, create_symlink, find_udc, is_configfs_available, remove_dir, remove_file, - write_file, CONFIGFS_PATH, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE, DEFAULT_USB_PRODUCT_ID, + configfs_path, create_dir, create_symlink, find_udc, is_configfs_available, remove_dir, + remove_file, write_file, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE, DEFAULT_USB_PRODUCT_ID, DEFAULT_USB_VENDOR_ID, USB_BCD_USB, }; use super::endpoint::{EndpointAllocator, DEFAULT_MAX_ENDPOINTS}; @@ -65,7 +65,7 @@ impl OtgGadgetManager { max_endpoints: u8, descriptor: GadgetDescriptor, ) -> Self { - let gadget_path = PathBuf::from(CONFIGFS_PATH).join(gadget_name); + let gadget_path = configfs_path().join(gadget_name); let config_path = gadget_path.join("configs/c.1"); Self { @@ -166,9 +166,10 @@ impl OtgGadgetManager { debug!("Setting up OTG USB Gadget: {}", self.gadget_name); if !Self::is_available() { - return Err(AppError::Internal( - "ConfigFS not available. Is it mounted at /sys/kernel/config?".to_string(), - )); + return Err(AppError::Internal(format!( + "ConfigFS not available at {}", + configfs_path().display() + ))); } if self.gadget_exists() { diff --git a/src/otg/self_check.rs b/src/otg/self_check.rs index a2180196..fe42cd90 100644 --- a/src/otg/self_check.rs +++ b/src/otg/self_check.rs @@ -286,7 +286,10 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { ); } - let gadget_root = std::path::Path::new("/sys/kernel/config/usb_gadget"); + let gadget_root = crate::otg::configfs::configfs_path(); + let configfs_mount = gadget_root + .parent() + .unwrap_or_else(|| std::path::Path::new("/sys/kernel/config")); let configfs_mounted = std::fs::read_to_string("/proc/mounts") .ok() .map(|mounts| { @@ -295,7 +298,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { let _src = parts.next(); let mount_point = parts.next(); let fs_type = parts.next(); - mount_point == Some("/sys/kernel/config") && fs_type == Some("configfs") + mount_point == configfs_mount.to_str() && fs_type == Some("configfs") }) }) .unwrap_or(false); @@ -310,7 +313,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { OtgSelfCheckLevel::Info, "Check configfs mount status", None::, - Some("/sys/kernel/config"), + Some(configfs_mount.display().to_string()), ); } else { gadget_config_ok = false; @@ -320,8 +323,11 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { false, OtgSelfCheckLevel::Error, "Check configfs mount status", - Some("Try: mount -t configfs none /sys/kernel/config"), - Some("/sys/kernel/config"), + Some(format!( + "Try: mount -t configfs none {}", + configfs_mount.display() + )), + Some(configfs_mount.display().to_string()), ); } @@ -331,9 +337,9 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { "usb_gadget_dir_exists", true, OtgSelfCheckLevel::Info, - "Check /sys/kernel/config/usb_gadget access", + format!("Check {} access", gadget_root.display()), None::, - Some("/sys/kernel/config/usb_gadget"), + Some(gadget_root.display().to_string()), ); } else { gadget_config_ok = false; @@ -342,9 +348,9 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { "usb_gadget_dir_exists", false, OtgSelfCheckLevel::Error, - "Check /sys/kernel/config/usb_gadget access", + format!("Check {} access", gadget_root.display()), Some("Ensure configfs and USB gadget support are enabled"), - Some("/sys/kernel/config/usb_gadget"), + Some(gadget_root.display().to_string()), ); } @@ -426,7 +432,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { OtgSelfCheckLevel::Info, "Check for other gadget services", None::, - Some("/sys/kernel/config/usb_gadget"), + Some(gadget_root.display().to_string()), ); } else { push_otg_check( @@ -436,7 +442,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { OtgSelfCheckLevel::Warn, "Check for other gadget services", Some("Potential UDC contention with one-kvm; check other OTG services"), - Some("/sys/kernel/config/usb_gadget"), + Some(gadget_root.display().to_string()), ); } @@ -621,7 +627,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { OtgSelfCheckLevel::Info, "Check UDC binding conflicts", None::, - Some("/sys/kernel/config/usb_gadget/*/UDC"), + Some(format!("{}/*/UDC", gadget_root.display())), ); } else { push_otg_check( @@ -631,7 +637,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse { OtgSelfCheckLevel::Error, "Check UDC binding conflicts", Some("Stop other OTG services or switch one-kvm to an idle UDC"), - Some("/sys/kernel/config/usb_gadget/*/UDC"), + Some(format!("{}/*/UDC", gadget_root.display())), ); } } diff --git a/src/platform/android.rs b/src/platform/android.rs deleted file mode 100644 index fd8614d2..00000000 --- a/src/platform/android.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Android Amlogic platform capabilities. - -use super::{FeatureCapability, PlatformCapabilities, PlatformMode}; - -#[cfg(feature = "android")] -#[allow(dead_code)] -fn _keep_android_bionic_ifaddrs_shim_linked() { - let _ = crate::platform::android_bionic::freeifaddrs - as unsafe extern "C" fn(*mut crate::platform::android_bionic::ifaddrs); - let _ = crate::platform::android_bionic::getifaddrs - as unsafe extern "C" fn(*mut *mut crate::platform::android_bionic::ifaddrs) -> i32; -} - -pub fn capabilities() -> PlatformCapabilities { - #[cfg(feature = "android")] - _keep_android_bionic_ifaddrs_shim_linked(); - - PlatformCapabilities { - mode: PlatformMode::AndroidAmlogic, - mode_label: PlatformMode::AndroidAmlogic.label(), - video_capture: FeatureCapability::available(["v4l2_uvc"]) - .with_selected_backend(Some("v4l2_uvc".to_string())), - encoder: FeatureCapability::available(["ffmpeg_mediacodec_h264", "mjpeg"]) - .with_selected_backend(Some( - if cfg!(feature = "android-mediacodec") { - "ffmpeg_mediacodec_h264" - } else { - "mjpeg" - } - .to_string(), - )), - hid: FeatureCapability::available(["otg_configfs", "ch9329", "none"]), - atx: FeatureCapability::available(["gpio", "usb_relay", "serial", "wol", "none"]), - msd: FeatureCapability::available(["otg_configfs"]), - otg: FeatureCapability::available(["configfs"]), - audio: FeatureCapability::available(["alsa", "opus"]) - .with_selected_backend(Some("alsa".to_string())), - rustdesk: FeatureCapability::available(["builtin"]), - vnc: FeatureCapability::available(["builtin", "tight_jpeg", "h264"]), - diagnostics: FeatureCapability::available(["android_linux"]), - extensions: FeatureCapability::unsupported("unsupported on Android Amlogic v1"), - service_installation: FeatureCapability::available(["android_foreground_service"]), - } -} diff --git a/src/platform/android_bionic.rs b/src/platform/android_bionic.rs deleted file mode 100644 index 6dde13b5..00000000 --- a/src/platform/android_bionic.rs +++ /dev/null @@ -1,175 +0,0 @@ -#![allow(clippy::missing_safety_doc)] - -use std::ffi::CString; -use std::mem::{size_of, zeroed}; -use std::os::raw::{c_char, c_int, c_uint, c_void}; - -#[repr(C)] -pub struct ifaddrs { - pub ifa_next: *mut ifaddrs, - pub ifa_name: *mut c_char, - pub ifa_flags: c_uint, - pub ifa_addr: *mut libc::sockaddr, - pub ifa_netmask: *mut libc::sockaddr, - pub ifa_ifu: *mut libc::sockaddr, - pub ifa_data: *mut c_void, -} - -#[repr(C)] -struct AddrNode { - ifa: ifaddrs, - name: CString, - addr: libc::sockaddr_in, - next: *mut AddrNode, -} - -fn sockaddr_to_ipv4(addr: libc::sockaddr) -> Option { - if addr.sa_family as c_int != libc::AF_INET { - return None; - } - - unsafe { - let sin = &*(&addr as *const libc::sockaddr as *const libc::sockaddr_in); - Some(std::net::Ipv4Addr::from(u32::from_be(sin.sin_addr.s_addr))) - } -} - -fn query_ipv4(iface_name: &str) -> Option { - let name = CString::new(iface_name).ok()?; - if name.as_bytes().len() >= libc::IFNAMSIZ { - return None; - } - - unsafe { - let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0); - if fd < 0 { - return None; - } - - let mut request: libc::ifreq = zeroed(); - std::ptr::copy_nonoverlapping( - name.as_ptr(), - request.ifr_name.as_mut_ptr(), - name.as_bytes_with_nul().len(), - ); - - let request_code = libc::SIOCGIFADDR.try_into().ok()?; - let rc = libc::ioctl(fd, request_code, &mut request); - libc::close(fd); - if rc < 0 { - return None; - } - - let addr = request.ifr_ifru.ifru_addr; - if addr.sa_family as c_int != libc::AF_INET { - return None; - } - - let mut sin: libc::sockaddr_in = zeroed(); - std::ptr::copy_nonoverlapping( - &addr as *const libc::sockaddr as *const u8, - &mut sin as *mut libc::sockaddr_in as *mut u8, - size_of::(), - ); - Some(sin) - } -} - -#[no_mangle] -pub unsafe extern "C" fn getifaddrs(addrs: *mut *mut ifaddrs) -> c_int { - if addrs.is_null() { - return -1; - } - *addrs = std::ptr::null_mut(); - - let net_dir = match std::fs::read_dir("/sys/class/net") { - Ok(dir) => dir, - Err(_) => return -1, - }; - - let mut head: *mut AddrNode = std::ptr::null_mut(); - let mut tail: *mut AddrNode = std::ptr::null_mut(); - - for entry in net_dir.flatten() { - let iface_name = match entry.file_name().into_string() { - Ok(name) => name, - Err(_) => continue, - }; - if iface_name == "lo" { - continue; - } - - let operstate_path = entry.path().join("operstate"); - let is_up = std::fs::read_to_string(&operstate_path) - .map(|s| s.trim() == "up") - .unwrap_or(false); - if !is_up { - continue; - } - - let Some(addr) = query_ipv4(&iface_name) else { - continue; - }; - let ip = sockaddr_to_ipv4(unsafe { - std::mem::transmute::(addr) - }); - if ip - .map(|ip| ip.is_loopback() || ip.is_unspecified()) - .unwrap_or(true) - { - continue; - } - - let name = match CString::new(iface_name) { - Ok(name) => name, - Err(_) => continue, - }; - - let mut node = Box::new(AddrNode { - ifa: ifaddrs { - ifa_next: std::ptr::null_mut(), - ifa_name: std::ptr::null_mut(), - ifa_flags: 0, - ifa_addr: std::ptr::null_mut(), - ifa_netmask: std::ptr::null_mut(), - ifa_ifu: std::ptr::null_mut(), - ifa_data: std::ptr::null_mut(), - }, - name, - addr, - next: std::ptr::null_mut(), - }); - - node.ifa.ifa_name = node.name.as_ptr() as *mut c_char; - node.ifa.ifa_addr = &mut node.addr as *mut libc::sockaddr_in as *mut libc::sockaddr; - node.ifa.ifa_ifu = std::ptr::null_mut(); - node.ifa.ifa_netmask = std::ptr::null_mut(); - node.ifa.ifa_flags = (libc::IFF_UP | libc::IFF_RUNNING) as c_uint; - - let raw = Box::into_raw(node); - if head.is_null() { - head = raw; - } else { - (*tail).next = raw; - (*tail).ifa.ifa_next = raw as *mut ifaddrs; - } - tail = raw; - } - - *addrs = if head.is_null() { - std::ptr::null_mut() - } else { - head as *mut ifaddrs - }; - 0 -} - -#[no_mangle] -pub unsafe extern "C" fn freeifaddrs(addrs: *mut ifaddrs) { - let mut current = addrs as *mut AddrNode; - while !current.is_null() { - let next = (*current).next; - drop(Box::from_raw(current)); - current = next; - } -} diff --git a/src/platform/capabilities.rs b/src/platform/capabilities.rs index e7566721..0fbf54a6 100644 --- a/src/platform/capabilities.rs +++ b/src/platform/capabilities.rs @@ -5,16 +5,13 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PlatformMode { - AndroidAmlogic, Linux, Windows, } impl PlatformMode { pub const fn current() -> Self { - if cfg!(feature = "android") { - Self::AndroidAmlogic - } else if cfg!(windows) { + if cfg!(windows) { Self::Windows } else { Self::Linux @@ -23,7 +20,6 @@ impl PlatformMode { pub const fn label(self) -> &'static str { match self { - Self::AndroidAmlogic => "Android Amlogic", Self::Linux => "Linux", Self::Windows => "Windows", } @@ -86,15 +82,11 @@ pub struct PlatformCapabilities { impl PlatformCapabilities { pub fn current() -> Self { - #[cfg(feature = "android")] - { - return crate::platform::android::capabilities(); - } #[cfg(windows)] { return crate::platform::windows::capabilities(); } - #[cfg(all(unix, not(feature = "android")))] + #[cfg(target_os = "linux")] { return crate::platform::linux::capabilities(); } diff --git a/src/platform/defaults.rs b/src/platform/defaults.rs index ba951cf9..f93d0540 100644 --- a/src/platform/defaults.rs +++ b/src/platform/defaults.rs @@ -1,72 +1,21 @@ use crate::config::AppConfig; #[cfg(windows)] use crate::config::AtxDriverType; -#[cfg(any(windows, all(unix, feature = "android")))] +#[cfg(windows)] use crate::config::HidBackend; pub fn apply(config: &mut AppConfig) { - #[cfg(not(any(windows, all(unix, feature = "android"))))] + #[cfg(not(windows))] { let _ = config; } - #[cfg(all(unix, feature = "android"))] - { - apply_android(config); - } - #[cfg(windows)] { apply_windows(config); } } -#[cfg(all(unix, feature = "android"))] -fn apply_android(config: &mut AppConfig) { - let detected_udc = crate::otg::configfs::find_udc(); - if config - .hid - .otg_udc - .as_deref() - .map(str::trim) - .unwrap_or("") - .is_empty() - { - config.hid.otg_udc = detected_udc; - } - - let otg_available = config.hid.otg_udc.is_some(); - if !config.initialized && otg_available { - config.hid.backend = HidBackend::Otg; - } else if config.hid.backend == HidBackend::Ch9329 - && config.hid.ch9329_port == "/dev/ttyUSB0" - && !std::path::Path::new(&config.hid.ch9329_port).exists() - && otg_available - { - config.hid.backend = HidBackend::Otg; - } - - if !config.initialized { - config.audio.enabled = false; - config.audio.device.clear(); - config.atx.enabled = false; - config.rustdesk.enabled = false; - config.rtsp.enabled = false; - config.redfish.enabled = false; - } - - config - .video - .device - .get_or_insert_with(|| "auto".to_string()); - config - .video - .format - .get_or_insert_with(|| "MJPEG".to_string()); - config.web.bind_address = "0.0.0.0".to_string(); - config.web.bind_addresses = vec!["0.0.0.0".to_string()]; -} - #[cfg(windows)] fn apply_windows(config: &mut AppConfig) { config.msd.enabled = false; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 8794c260..cfc8265f 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -1,9 +1,5 @@ //! Platform selection and capability reporting. -#[cfg(feature = "android")] -pub mod android; -#[cfg(feature = "android")] -pub mod android_bionic; pub mod capabilities; pub mod defaults; #[cfg(target_os = "linux")] diff --git a/src/redfish/routes/mod.rs b/src/redfish/routes/mod.rs index e08412cf..c46fc17c 100644 --- a/src/redfish/routes/mod.rs +++ b/src/redfish/routes/mod.rs @@ -4,7 +4,7 @@ mod event; mod managers; mod session; mod systems; -#[cfg(all(unix, not(feature = "android")))] +#[cfg(unix)] mod virtual_media; use axum::{ @@ -196,7 +196,7 @@ pub fn create_redfish_router(state: Arc) -> Router { .merge(account::router(state.clone())) .merge(event::router(state.clone())); - #[cfg(all(unix, not(feature = "android")))] + #[cfg(unix)] let redfish_routes = redfish_routes.merge(virtual_media::router(state.clone())); let redfish_routes = redfish_routes.layer(middleware::from_fn_with_state( diff --git a/src/runtime/android.rs b/src/runtime/android.rs deleted file mode 100644 index 0302414b..00000000 --- a/src/runtime/android.rs +++ /dev/null @@ -1,789 +0,0 @@ -//! Android service runtime. -//! -//! Android is treated as a packaged Linux distribution: the APK/Java layer only -//! starts and stops this runtime, while the Rust side builds the same AppState -//! and Axum router used by the desktop service. - -use std::net::{IpAddr, SocketAddr}; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex, OnceLock}; -use std::thread::JoinHandle; -use std::time::{Duration, Instant}; - -use rustls::crypto::{ring, CryptoProvider}; -use tokio::runtime::Runtime; -use tokio::sync::{broadcast, mpsc, oneshot}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -use crate::atx::AtxController; -use crate::audio::{AudioController, AudioControllerConfig, AudioQuality}; -use crate::auth::{SessionStore, UserStore}; -use crate::computer_use::ComputerUseManager; -use crate::config::{self, AppConfig, ConfigStore}; -use crate::db::DatabasePool; -use crate::events::EventBus; -use crate::extensions::ExtensionManager; -use crate::hid::{HidBackendType, HidController}; -use crate::msd::MsdController; -use crate::otg::OtgService; -use crate::rtsp::RtspService; -use crate::rustdesk::RustDeskService; -use crate::state::{AppState, ShutdownAction}; -use crate::stream_encoder::encoder_type_to_backend; -use crate::update::UpdateService; -use crate::utils::bind_tcp_listener; -use crate::video::codec_constraints::{ - enforce_constraints_with_stream_manager, validate_third_party_codec_compatibility, - StreamCodecConstraints, -}; -use crate::video::format::{PixelFormat, Resolution}; -use crate::video::{Streamer, VideoStreamManager}; -use crate::vnc::VncService; -use crate::web; -use crate::webrtc::{config::WebRtcConfig, WebRtcStreamer, WebRtcStreamerConfig}; - -#[derive(Debug, Clone)] -pub struct AndroidRuntimeConfig { - pub data_dir: String, - pub bind_address: String, - pub port: u16, -} - -struct RuntimeHandle { - stop_tx: oneshot::Sender<()>, - join: JoinHandle<()>, -} - -static HANDLE: OnceLock>> = OnceLock::new(); - -fn handle_slot() -> &'static Mutex> { - HANDLE.get_or_init(|| Mutex::new(None)) -} - -pub fn start(config: AndroidRuntimeConfig) -> Result { - init_logging(); - - let mut slot = handle_slot() - .lock() - .map_err(|_| "runtime lock poisoned".to_string())?; - if slot.is_some() { - return Ok(status()); - } - - let (stop_tx, stop_rx) = oneshot::channel(); - let config_for_thread = config.clone(); - let join = std::thread::Builder::new() - .name("one-kvm-android-runtime".to_string()) - .spawn(move || { - if let Err(err) = run_runtime(config_for_thread, stop_rx) { - tracing::error!("One-KVM Android runtime exited: {}", err); - } - }) - .map_err(|err| format!("failed to spawn runtime: {err}"))?; - - *slot = Some(RuntimeHandle { stop_tx, join }); - Ok(format!( - "One-KVM Android runtime starting on http://{}:{}", - config.bind_address, config.port - )) -} - -pub fn run_foreground(config: AndroidRuntimeConfig) -> Result<(), String> { - init_logging(); - let (_stop_tx, stop_rx) = oneshot::channel(); - run_runtime(config, stop_rx) -} - -pub fn init_rustls_provider() { - ensure_rustls_provider(); -} - -pub fn stop() -> String { - let handle = match handle_slot().lock() { - Ok(mut slot) => slot.take(), - Err(_) => return "runtime lock poisoned".to_string(), - }; - - let Some(handle) = handle else { - return "One-KVM Android runtime is not running".to_string(); - }; - - let _ = handle.stop_tx.send(()); - match handle.join.join() { - Ok(()) => "One-KVM Android runtime stopped".to_string(), - Err(_) => "One-KVM Android runtime stopped after panic".to_string(), - } -} - -pub fn status() -> String { - match handle_slot().lock() { - Ok(slot) if slot.is_some() => "One-KVM Android runtime running".to_string(), - Ok(_) => "One-KVM Android runtime stopped".to_string(), - Err(_) => "runtime lock poisoned".to_string(), - } -} - -fn run_runtime(config: AndroidRuntimeConfig, stop_rx: oneshot::Receiver<()>) -> Result<(), String> { - ensure_rustls_provider(); - let runtime = Runtime::new().map_err(|err| format!("failed to create tokio runtime: {err}"))?; - runtime.block_on(async move { run_async(config, stop_rx).await }) -} - -async fn run_async( - config: AndroidRuntimeConfig, - stop_rx: oneshot::Receiver<()>, -) -> Result<(), String> { - let (db, config_store, app_config) = - load_runtime_config(&PathBuf::from(&config.data_dir), &config).await?; - let (shutdown_tx, _) = broadcast::channel::(1); - let state = build_app_state( - PathBuf::from(&config.data_dir), - db, - config_store, - app_config, - shutdown_tx.clone(), - ) - .await?; - - let app = web::create_router(state.clone()); - let listener = bind_android_listener(&config.bind_address, config.port)?; - let local_addr = listener - .local_addr() - .map_err(|err| format!("failed to get listener address: {err}"))?; - tracing::info!( - "Starting One-KVM desktop router on Android at http://{}", - local_addr - ); - - let listener = tokio::net::TcpListener::from_std(listener) - .map_err(|err| format!("failed to create tokio listener: {err}"))?; - let server = axum::serve(listener, app); - - let shutdown_signal = { - let mut shutdown_rx = shutdown_tx.subscribe(); - async move { - tokio::select! { - _ = stop_rx => { - tracing::info!("Android stop request received"); - let _ = shutdown_tx.send(ShutdownAction::Exit); - } - request = shutdown_rx.recv() => { - match request { - Ok(action) => { - tracing::info!("Android shutdown request received: {:?}", action); - } - Err(err) => { - tracing::warn!("Android shutdown request channel closed: {}", err); - } - } - } - } - } - }; - - tokio::select! { - result = server => { - if let Err(err) = result { - tracing::error!("Android HTTP server error: {}", err); - } - } - _ = shutdown_signal => {} - } - - cleanup(&state).await; - Ok(()) -} - -async fn load_runtime_config( - data_dir: &Path, - runtime_config: &AndroidRuntimeConfig, -) -> Result<(DatabasePool, ConfigStore, AppConfig), String> { - tokio::fs::create_dir_all(data_dir) - .await - .map_err(|err| format!("failed to create data dir {}: {err}", data_dir.display()))?; - - let db_path = data_dir.join("one-kvm.db"); - let db = DatabasePool::new(&db_path) - .await - .map_err(|err| format!("failed to open database {}: {err}", db_path.display()))?; - db.init_schema() - .await - .map_err(|err| format!("failed to initialize database schema: {err}"))?; - - let config_store = ConfigStore::new(db.clone_pool()) - .map_err(|err| format!("failed to create config store: {err}"))?; - config_store - .load() - .await - .map_err(|err| format!("failed to load config: {err}"))?; - - let mut config = (*config_store.get()).clone(); - config.apply_platform_defaults(); - config.web.bind_address = runtime_config.bind_address.clone(); - config.web.bind_addresses = vec![runtime_config.bind_address.clone()]; - config.web.http_port = runtime_config.port; - config.web.https_enabled = false; - prepare_android_runtime_dirs(data_dir, &config_store, &mut config).await?; - - if let Some(device) = config.video.device.as_deref() { - if device == "auto" { - config.video.device = None; - } - } - - config_store - .set(config.clone()) - .await - .map_err(|err| format!("failed to persist Android runtime config: {err}"))?; - - Ok((db, config_store, config)) -} - -async fn prepare_android_runtime_dirs( - data_dir: &Path, - config_store: &ConfigStore, - config: &mut AppConfig, -) -> Result<(), String> { - let mut updated = false; - if config.msd.msd_dir.trim().is_empty() { - config.msd.msd_dir = data_dir.join("msd").to_string_lossy().to_string(); - updated = true; - } else if !PathBuf::from(&config.msd.msd_dir).is_absolute() { - config.msd.msd_dir = data_dir - .join(&config.msd.msd_dir) - .to_string_lossy() - .to_string(); - updated = true; - } - - let msd_dir = config.msd.msd_dir_path(); - tokio::fs::create_dir_all(msd_dir.join("images")) - .await - .map_err(|err| format!("failed to create Android MSD images dir: {err}"))?; - tokio::fs::create_dir_all(msd_dir.join("ventoy")) - .await - .map_err(|err| format!("failed to create Android MSD ventoy dir: {err}"))?; - - if updated { - config_store - .set(config.clone()) - .await - .map_err(|err| format!("failed to persist Android MSD dir: {err}"))?; - } - - Ok(()) -} - -#[allow(clippy::too_many_lines)] -async fn build_app_state( - data_dir: PathBuf, - db: DatabasePool, - config_store: ConfigStore, - config: AppConfig, - shutdown_tx: broadcast::Sender, -) -> Result, String> { - let session_store = SessionStore::new(config.auth.session_timeout_secs as i64); - let user_store = UserStore::new(db.clone_pool()); - let events = Arc::new(EventBus::new()); - - let (video_format, video_resolution) = parse_video_config(&config); - let streamer = Streamer::new(); - streamer.set_event_bus(events.clone()).await; - if let Some(ref device_path) = config.video.device { - if let Err(err) = streamer - .apply_video_config( - device_path, - video_format, - video_resolution, - config.video.fps, - ) - .await - { - tracing::warn!("Android video config failed, falling back to auto: {}", err); - } - } - - let webrtc_streamer = WebRtcStreamer::with_config(WebRtcStreamerConfig { - resolution: video_resolution, - input_format: video_format, - fps: config.video.fps, - bitrate_preset: config.stream.bitrate_preset, - encoder_backend: encoder_type_to_backend(config.stream.encoder.clone()), - webrtc: build_webrtc_config(&config), - ..Default::default() - }); - - let hid_backend = match config.hid.backend { - config::HidBackend::Otg => HidBackendType::Otg, - config::HidBackend::Ch9329 => HidBackendType::Ch9329 { - port: config.hid.ch9329_port.clone(), - baud_rate: config.hid.ch9329_baudrate, - hybrid_mouse: config.hid.ch9329_hybrid_mouse, - }, - config::HidBackend::None => HidBackendType::None, - }; - let otg_service = Arc::new(OtgService::new()); - if let Err(err) = otg_service.apply_config(&config.hid, &config.msd).await { - tracing::warn!("Failed to apply Android OTG config: {}", err); - } - - let hid = Arc::new(HidController::new(hid_backend, Some(otg_service.clone()))); - hid.set_event_bus(events.clone()).await; - if let Err(err) = hid.init().await { - tracing::warn!("Failed to initialize Android HID backend: {}", err); - } - - let msd = if config.msd.enabled { - let ventoy_resource_dir = data_dir.join("ventoy"); - if ventoy_resource_dir.exists() { - if let Err(err) = ventoy_img::init_resources(&ventoy_resource_dir) { - tracing::warn!("Failed to initialize Android Ventoy resources: {}", err); - } - } - - let controller = MsdController::new(otg_service.clone(), config.msd.msd_dir_path()); - if let Err(err) = controller.init().await { - tracing::warn!("Failed to initialize Android MSD controller: {}", err); - None - } else { - controller.set_event_bus(events.clone()).await; - Some(controller) - } - } else { - None - }; - - let atx = if config.atx.enabled { - let controller = AtxController::new(config.atx.to_controller_config()); - if let Err(err) = controller.init().await { - tracing::warn!("Failed to initialize Android ATX controller: {}", err); - None - } else { - Some(controller) - } - } else { - None - }; - - let audio = { - let audio_config = AudioControllerConfig { - enabled: config.audio.enabled, - device: config.audio.device.clone(), - quality: config - .audio - .quality - .parse::() - .unwrap_or(AudioQuality::Balanced), - }; - let controller = AudioController::new(audio_config); - controller.set_event_bus(events.clone()).await; - if config.audio.enabled { - if let Err(err) = controller.start_streaming().await { - tracing::warn!("Failed to start Android audio: {}", err); - } - } - Arc::new(controller) - }; - - let extensions = Arc::new(ExtensionManager::new()); - webrtc_streamer.set_hid_controller(hid.clone()).await; - webrtc_streamer.set_audio_controller(audio.clone()).await; - - let (device_path, actual_resolution, actual_format, actual_fps, jpeg_quality) = - streamer.current_capture_config().await; - webrtc_streamer - .update_video_config(actual_resolution, actual_format, actual_fps) - .await; - if let Some(device_path) = device_path { - let (subdev_path, bridge_kind, v4l2_driver) = streamer - .current_device() - .await - .map(|device| { - ( - device.subdev_path.clone(), - device.bridge_kind.clone(), - Some(device.driver.clone()), - ) - }) - .unwrap_or((None, None, None)); - webrtc_streamer - .set_capture_device( - device_path, - jpeg_quality, - subdev_path, - bridge_kind, - v4l2_driver, - ) - .await; - } - - let stream_manager = VideoStreamManager::with_webrtc_streamer( - streamer.clone(), - webrtc_streamer.clone() as Arc, - ); - stream_manager.set_event_bus(events.clone()).await; - stream_manager.set_config_store(config_store.clone()).await; - { - let stream_manager_weak = Arc::downgrade(&stream_manager); - audio - .set_recovered_callback(Arc::new(move || { - if let Some(stream_manager) = stream_manager_weak.upgrade() { - tokio::spawn(async move { - stream_manager.reconnect_webrtc_audio_sources().await; - }); - } - })) - .await; - } - - if let Err(err) = stream_manager - .init_with_mode(config.stream.mode.clone()) - .await - { - tracing::warn!("Failed to initialize Android stream manager: {}", err); - } - - let third_party_codec_config_valid = match validate_third_party_codec_compatibility(&config) { - Ok(()) => true, - Err(e) => { - tracing::warn!( - "Android third-party access codec configuration is invalid; RustDesk/VNC/RTSP will not start: {}", - e - ); - false - } - }; - - let rustdesk = if third_party_codec_config_valid && config.rustdesk.is_valid() { - Some(Arc::new(RustDeskService::new( - config.rustdesk.clone(), - stream_manager.clone(), - hid.clone(), - audio.clone(), - ))) - } else { - None - }; - - let rtsp = if third_party_codec_config_valid && config.rtsp.enabled { - Some(Arc::new(RtspService::new( - config.rtsp.clone(), - stream_manager.clone(), - ))) - } else { - None - }; - let vnc = if third_party_codec_config_valid && config.vnc.enabled { - Some(Arc::new(VncService::new( - config.vnc.clone(), - stream_manager.clone(), - hid.clone(), - ))) - } else { - None - }; - - let update_service = Arc::new(UpdateService::new(data_dir.join("updates"))); - let computer_use = ComputerUseManager::new(config_store.clone(), hid.clone()); - let state = AppState::new( - db, - config_store.clone(), - session_store, - user_store, - otg_service, - stream_manager, - webrtc_streamer, - hid, - computer_use, - msd, - atx, - audio, - rustdesk.clone(), - vnc.clone(), - rtsp.clone(), - extensions.clone(), - events.clone(), - update_service, - shutdown_tx, - data_dir, - ); - - extensions.set_event_bus(events.clone()).await; - - if let Some(service) = rustdesk { - if let Err(err) = service.start().await { - tracing::warn!("Failed to start Android RustDesk service: {}", err); - } - } - if let Some(service) = vnc { - if let Err(err) = service.start().await { - tracing::warn!("Failed to start Android VNC service: {}", err); - } - } - if let Some(service) = rtsp { - if let Err(err) = service.start().await { - tracing::warn!("Failed to start Android RTSP service: {}", err); - } - } - - let constraints = StreamCodecConstraints::from_config(&state.config.get()); - if let Err(err) = - enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await - { - tracing::warn!("Failed to enforce Android stream constraints: {}", err); - } - - state.publish_device_info().await; - spawn_device_info_broadcaster(state.clone(), events); - - Ok(state) -} - -fn build_webrtc_config(config: &AppConfig) -> WebRtcConfig { - let mut webrtc = WebRtcConfig::default(); - if let Some(stun) = config - .stream - .stun_server - .as_ref() - .filter(|value| !value.is_empty()) - { - webrtc.stun_servers.push(stun.clone()); - } - if let Some(turn) = config - .stream - .turn_server - .as_ref() - .filter(|value| !value.is_empty()) - { - webrtc - .turn_servers - .push(crate::webrtc::config::TurnServer::new( - turn.clone(), - config.stream.turn_username.clone().unwrap_or_default(), - config.stream.turn_password.clone().unwrap_or_default(), - )); - } - webrtc -} - -fn parse_video_config(config: &AppConfig) -> (PixelFormat, Resolution) { - let format = config - .video - .format - .as_ref() - .and_then(|value| value.parse::().ok()) - .unwrap_or(PixelFormat::Mjpeg); - ( - format, - Resolution::new(config.video.width, config.video.height), - ) -} - -fn bind_android_listener(bind_address: &str, port: u16) -> Result { - let ip = bind_address - .parse::() - .map_err(|err| format!("invalid Android bind address {bind_address}: {err}"))?; - bind_tcp_listener(SocketAddr::new(ip, port)) - .map_err(|err| format!("failed to bind Android listener {bind_address}:{port}: {err}")) -} - -fn spawn_device_info_broadcaster(state: Arc, events: Arc) { - enum DeviceInfoTrigger { - Event, - Lagged { topic: &'static str, count: u64 }, - } - - const DEVICE_INFO_TOPICS: &[&str] = &[ - "stream.state_changed", - "stream.config_applied", - "stream.mode_ready", - ]; - const DEBOUNCE_MS: u64 = 100; - - let (trigger_tx, mut trigger_rx) = mpsc::unbounded_channel(); - for topic in DEVICE_INFO_TOPICS { - let Some(mut rx) = events.subscribe_topic(topic) else { - continue; - }; - let trigger_tx = trigger_tx.clone(); - let topic_name = *topic; - tokio::spawn(async move { - loop { - match rx.recv().await { - Ok(_) => { - if trigger_tx.send(DeviceInfoTrigger::Event).is_err() { - break; - } - } - Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => { - if trigger_tx - .send(DeviceInfoTrigger::Lagged { - topic: topic_name, - count, - }) - .is_err() - { - break; - } - } - Err(tokio::sync::broadcast::error::RecvError::Closed) => break, - } - } - }); - } - - { - let mut dirty_rx = events.subscribe_device_info_dirty(); - let trigger_tx = trigger_tx.clone(); - tokio::spawn(async move { - loop { - match dirty_rx.recv().await { - Ok(()) => { - if trigger_tx.send(DeviceInfoTrigger::Event).is_err() { - break; - } - } - Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => { - if trigger_tx - .send(DeviceInfoTrigger::Lagged { - topic: "device_info_dirty", - count, - }) - .is_err() - { - break; - } - } - Err(tokio::sync::broadcast::error::RecvError::Closed) => break, - } - } - }); - } - - tokio::spawn(async move { - let mut last_broadcast = Instant::now() - Duration::from_millis(DEBOUNCE_MS); - let mut pending_broadcast = false; - - loop { - let recv_result = if pending_broadcast { - let remaining = - DEBOUNCE_MS.saturating_sub(last_broadcast.elapsed().as_millis() as u64); - tokio::time::timeout(Duration::from_millis(remaining), trigger_rx.recv()).await - } else { - Ok(trigger_rx.recv().await) - }; - - match recv_result { - Ok(Some(DeviceInfoTrigger::Event)) => pending_broadcast = true, - Ok(Some(DeviceInfoTrigger::Lagged { topic, count })) => { - tracing::warn!( - "Android device info broadcaster lagged by {} events on {}", - count, - topic - ); - pending_broadcast = true; - } - Ok(None) => break, - Err(_) => {} - } - - if pending_broadcast && last_broadcast.elapsed() >= Duration::from_millis(DEBOUNCE_MS) { - state.publish_device_info().await; - last_broadcast = Instant::now(); - pending_broadcast = false; - } - } - }); -} - -async fn cleanup(state: &Arc) { - state.extensions.stop_all().await; - - if let Some(service) = state.rustdesk.read().await.as_ref() { - if let Err(err) = service.stop().await { - tracing::warn!("Failed to stop Android RustDesk service: {}", err); - } - } - - if let Some(service) = state.vnc.read().await.as_ref() { - if let Err(err) = service.stop().await { - tracing::warn!("Failed to stop Android VNC service: {}", err); - } - } - - if let Some(service) = state.rtsp.read().await.as_ref() { - if let Err(err) = service.stop().await { - tracing::warn!("Failed to stop Android RTSP service: {}", err); - } - } - - if let Err(err) = state.stream_manager.stop().await { - tracing::warn!("Failed to stop Android stream manager: {}", err); - } - if let Err(err) = state.hid.shutdown().await { - tracing::warn!("Failed to stop Android HID: {}", err); - } - if let Some(msd) = state.msd.write().await.as_mut() { - if let Err(err) = msd.shutdown().await { - tracing::warn!("Failed to stop Android MSD: {}", err); - } - } - if let Err(err) = state.otg_service.shutdown().await { - tracing::warn!("Failed to stop Android OTG: {}", err); - } - if let Some(atx) = state.atx.write().await.as_mut() { - if let Err(err) = atx.shutdown().await { - tracing::warn!("Failed to stop Android ATX: {}", err); - } - } - if let Err(err) = state.audio.shutdown().await { - tracing::warn!("Failed to stop Android audio: {}", err); - } -} - -fn init_logging() { - static INIT: OnceLock<()> = OnceLock::new(); - INIT.get_or_init(|| { - let _ = tracing_log::LogTracer::init(); - let filter = tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "one_kvm=info,tower_http=info,webrtc_sctp=warn".into()); - let fmt_layer = tracing_subscriber::fmt::layer(); - if let Ok(path) = std::env::var("ONE_KVM_ANDROID_LOG_FILE") { - match std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&path) - { - Ok(file) => { - let file_layer = tracing_subscriber::fmt::layer() - .with_ansi(false) - .with_writer(Arc::new(file)); - let _ = tracing_subscriber::registry() - .with(filter) - .with(fmt_layer) - .with(file_layer) - .try_init(); - } - Err(err) => { - eprintln!("failed to open Android Rust log file {path}: {err}"); - let _ = tracing_subscriber::registry() - .with(filter) - .with(fmt_layer) - .try_init(); - } - } - } else { - let _ = tracing_subscriber::registry() - .with(filter) - .with(fmt_layer) - .try_init(); - } - }); -} - -fn ensure_rustls_provider() { - static INIT: OnceLock<()> = OnceLock::new(); - INIT.get_or_init(|| { - let _ = CryptoProvider::install_default(ring::default_provider()); - }); -} diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs deleted file mode 100644 index 3ddf992d..00000000 --- a/src/runtime/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Runtime entry points for packaged service modes. - -#[cfg(feature = "android")] -pub mod android; diff --git a/src/stream/mjpeg.rs b/src/stream/mjpeg.rs index 981e9211..e6713d28 100644 --- a/src/stream/mjpeg.rs +++ b/src/stream/mjpeg.rs @@ -175,9 +175,7 @@ impl MjpegStreamHandler { } #[cfg(not(feature = "desktop"))] { - warn!( - "Dropping non-JPEG frame for MJPEG stream on Android; native encoder is not wired yet" - ); + warn!("Dropping non-JPEG frame because this build has no JPEG encoder"); return; } } else { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d1fb32fe..dbd54519 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,9 +2,9 @@ pub mod fs; pub mod host; -#[cfg(all(unix, not(target_os = "android")))] +#[cfg(unix)] pub mod net; -#[cfg(any(not(unix), target_os = "android"))] +#[cfg(not(unix))] #[path = "net_disabled.rs"] pub mod net; pub mod serial; diff --git a/src/video/codec/android_mediacodec.rs b/src/video/codec/android_mediacodec.rs deleted file mode 100644 index 4aa3aa5d..00000000 --- a/src/video/codec/android_mediacodec.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Android FFmpeg/MediaCodec encoder glue. - -use bytes::Bytes; -use hwcodec::common::{Quality, RateControl}; -use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat}; -use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder}; - -use crate::error::{AppError, Result}; -use crate::video::format::{PixelFormat, Resolution}; - -pub struct AndroidMediaCodecH264Encoder { - inner: HwEncoder, - resolution: Resolution, - input_format: PixelFormat, - bitrate_kbps: u32, -} - -impl AndroidMediaCodecH264Encoder { - pub fn new( - resolution: Resolution, - input_format: PixelFormat, - fps: u32, - bitrate_kbps: u32, - ) -> Result { - let pixfmt = match input_format { - PixelFormat::Nv12 => resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12), - PixelFormat::Yuv420 => { - resolve_pixel_format("yuv420p", AVPixelFormat::AV_PIX_FMT_YUV420P) - } - other => { - return Err(AppError::VideoError(format!( - "FFmpeg h264_mediacodec accepts NV12/YUV420P memory frames; {other} requires conversion first" - ))) - } - }; - - let ctx = EncodeContext { - name: "h264_mediacodec".to_string(), - mc_name: None, - width: resolution.width as i32, - height: resolution.height as i32, - pixfmt, - align: 1, - fps: fps.max(1) as i32, - gop: fps.max(1) as i32, - rc: RateControl::RC_CBR, - quality: Quality::Quality_Low, - kbs: bitrate_kbps.max(1) as i32, - q: 23, - thread_count: 1, - }; - - let inner = HwEncoder::new(ctx).map_err(|_| { - AppError::VideoError("Failed to create FFmpeg h264_mediacodec encoder".to_string()) - })?; - - Ok(Self { - inner, - resolution, - input_format, - bitrate_kbps: bitrate_kbps.max(1), - }) - } - - pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result> { - let min_len = self - .input_format - .frame_size(self.resolution) - .ok_or_else(|| AppError::VideoError("MediaCodec input must be raw YUV".to_string()))?; - if data.len() < min_len { - return Err(AppError::VideoError(format!( - "MediaCodec {} frame too small: {} < {}", - self.input_format, - data.len(), - min_len - ))); - } - - let packets = self - .inner - .encode_bytes(data, pts_ms) - .map_err(|err| AppError::VideoError(format!("h264_mediacodec encode failed: {err}")))?; - - Ok(packets - .into_iter() - .map(|packet| AndroidH264Packet { - data: packet.data, - pts: packet.pts, - key_frame: packet.key == 1, - }) - .collect()) - } - - pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> { - self.inner - .set_bitrate(bitrate_kbps.max(1) as i32) - .map_err(|_| AppError::VideoError("Failed to set MediaCodec bitrate".to_string()))?; - self.bitrate_kbps = bitrate_kbps.max(1); - Ok(()) - } - - pub fn request_keyframe(&mut self) { - self.inner.request_keyframe(); - } - - pub fn codec_name(&self) -> &str { - "h264_mediacodec" - } - - pub fn input_format(&self) -> PixelFormat { - self.input_format - } -} - -unsafe impl Send for AndroidMediaCodecH264Encoder {} - -#[derive(Debug, Clone)] -pub struct AndroidH264Packet { - pub data: Bytes, - pub pts: i64, - pub key_frame: bool, -} diff --git a/src/video/codec/android_mjpeg.rs b/src/video/codec/android_mjpeg.rs deleted file mode 100644 index d7d6b04a..00000000 --- a/src/video/codec/android_mjpeg.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Android FFmpeg/MediaCodec MJPEG decoder glue. - -use hwcodec::ffmpeg::AVPixelFormat; -use hwcodec::ffmpeg_ram::decode::{DecodeContext, Decoder}; -use tracing::{info, warn}; - -use crate::error::{AppError, Result}; -use crate::video::codec::convert::Nv12Converter; -use crate::video::format::{PixelFormat, Resolution}; - -pub struct AndroidMediaCodecMjpegDecoder { - decoder: Decoder, - resolution: Resolution, - nv12_converter: Option, - last_output_format: Option, - pending_frames: u32, -} - -impl AndroidMediaCodecMjpegDecoder { - pub fn new(resolution: Resolution) -> Result { - let ctx = DecodeContext { - name: "mjpeg_mediacodec".to_string(), - width: resolution.width as i32, - height: resolution.height as i32, - sw_pixfmt: AVPixelFormat::AV_PIX_FMT_NV12, - thread_count: 1, - }; - let decoder = Decoder::new(ctx).map_err(|_| { - AppError::VideoError("Failed to create FFmpeg mjpeg_mediacodec decoder".to_string()) - })?; - Ok(Self { - decoder, - resolution, - nv12_converter: None, - last_output_format: None, - pending_frames: 0, - }) - } - - pub fn decode_to_nv12(&mut self, mjpeg: &[u8]) -> Result> { - let frames = match self.decoder.decode(mjpeg) { - Ok(frames) => frames, - Err(err) if err == -11 => { - self.pending_frames += 1; - if self.pending_frames <= 3 { - return Err(AppError::VideoError( - "mjpeg_mediacodec decode needs more input".to_string(), - )); - } - return Err(AppError::VideoError( - "mjpeg_mediacodec decoder did not output after 3 frames".to_string(), - )); - } - Err(err) => { - return Err(AppError::VideoError(format!( - "mjpeg_mediacodec decode failed: {err}" - ))); - } - }; - if frames.is_empty() { - self.pending_frames += 1; - if self.pending_frames <= 3 { - return Err(AppError::VideoError( - "mjpeg_mediacodec decode needs more input".to_string(), - )); - } - return Err(AppError::VideoError( - "mjpeg_mediacodec decoder did not output after 3 frames".to_string(), - )); - } - self.pending_frames = 0; - if frames.len() > 1 { - warn!( - "mjpeg_mediacodec decode returned {} frames, using last", - frames.len() - ); - } - - let frame = frames.pop().ok_or_else(|| { - AppError::VideoError("mjpeg_mediacodec decode returned empty".to_string()) - })?; - - if frame.width as u32 != self.resolution.width - || frame.height as u32 != self.resolution.height - { - warn!( - "mjpeg_mediacodec output size {}x{} differs from expected {}x{}", - frame.width, frame.height, self.resolution.width, self.resolution.height - ); - } - - let output_format = pixel_format_from_av(frame.pixfmt).ok_or_else(|| { - AppError::VideoError(format!( - "mjpeg_mediacodec output pixfmt {:?} is not supported", - frame.pixfmt - )) - })?; - - if self.last_output_format != Some(output_format) { - info!("mjpeg_mediacodec output format: {}", output_format); - self.last_output_format = Some(output_format); - } - - match output_format { - PixelFormat::Nv12 => Ok(frame.data), - PixelFormat::Nv21 => { - let converter = self - .nv12_converter - .get_or_insert_with(|| Nv12Converter::nv21_to_nv12(self.resolution)); - Ok(converter.convert(&frame.data)?.to_vec()) - } - PixelFormat::Yuv420 => { - let converter = self - .nv12_converter - .get_or_insert_with(|| Nv12Converter::yuv420_to_nv12(self.resolution)); - Ok(converter.convert(&frame.data)?.to_vec()) - } - other => Err(AppError::VideoError(format!( - "mjpeg_mediacodec output {} cannot be converted to NV12", - other - ))), - } - } -} - -fn pixel_format_from_av(format: AVPixelFormat) -> Option { - match format { - AVPixelFormat::AV_PIX_FMT_NV12 => Some(PixelFormat::Nv12), - AVPixelFormat::AV_PIX_FMT_NV21 => Some(PixelFormat::Nv21), - AVPixelFormat::AV_PIX_FMT_YUV420P | AVPixelFormat::AV_PIX_FMT_YUVJ420P => { - Some(PixelFormat::Yuv420) - } - _ => None, - } -} - -unsafe impl Send for AndroidMediaCodecMjpegDecoder {} diff --git a/src/video/codec/h264.rs b/src/video/codec/h264.rs index b08b6b74..13d7a3a8 100644 --- a/src/video/codec/h264.rs +++ b/src/video/codec/h264.rs @@ -48,8 +48,6 @@ pub enum H264EncoderType { Rkmpp, /// V4L2 M2M (ARM generic) - requires hwcodec extension V4l2M2m, - /// Android MediaCodec via FFmpeg - MediaCodec, /// Software encoding (libx264/openh264) Software, /// No encoder available @@ -66,7 +64,6 @@ impl std::fmt::Display for H264EncoderType { H264EncoderType::Vaapi => write!(f, "VAAPI"), H264EncoderType::Rkmpp => write!(f, "RKMPP"), H264EncoderType::V4l2M2m => write!(f, "V4L2 M2M"), - H264EncoderType::MediaCodec => write!(f, "MediaCodec"), H264EncoderType::Software => write!(f, "Software"), H264EncoderType::None => write!(f, "None"), } @@ -83,7 +80,6 @@ impl From for H264EncoderType { EncoderBackend::Vaapi => H264EncoderType::Vaapi, EncoderBackend::Rkmpp => H264EncoderType::Rkmpp, EncoderBackend::V4l2m2m => H264EncoderType::V4l2M2m, - EncoderBackend::MediaCodec => H264EncoderType::MediaCodec, EncoderBackend::Software => H264EncoderType::Software, } } @@ -196,7 +192,6 @@ pub fn get_available_encoders(width: u32, height: u32) -> Vec { let ctx = EncodeContext { name: String::new(), - mc_name: None, width: width as i32, height: height as i32, pixfmt: resolve_pixel_format("yuv420p", AVPixelFormat::AV_PIX_FMT_YUV420P), @@ -296,7 +291,6 @@ impl H264Encoder { let ctx = EncodeContext { name: codec_name.to_string(), - mc_name: None, width: width as i32, height: height as i32, pixfmt, diff --git a/src/video/codec/h265.rs b/src/video/codec/h265.rs index 10dddd99..08f3dbed 100644 --- a/src/video/codec/h265.rs +++ b/src/video/codec/h265.rs @@ -45,8 +45,6 @@ pub enum H265EncoderType { Rkmpp, /// V4L2 M2M (ARM generic) V4l2M2m, - /// Android MediaCodec via FFmpeg - MediaCodec, /// Software encoder (libx265) Software, /// No encoder available @@ -63,7 +61,6 @@ impl std::fmt::Display for H265EncoderType { H265EncoderType::Vaapi => write!(f, "VAAPI"), H265EncoderType::Rkmpp => write!(f, "RKMPP"), H265EncoderType::V4l2M2m => write!(f, "V4L2 M2M"), - H265EncoderType::MediaCodec => write!(f, "MediaCodec"), H265EncoderType::Software => write!(f, "Software"), H265EncoderType::None => write!(f, "None"), } @@ -79,7 +76,6 @@ impl From for H265EncoderType { EncoderBackend::Vaapi => H265EncoderType::Vaapi, EncoderBackend::Rkmpp => H265EncoderType::Rkmpp, EncoderBackend::V4l2m2m => H265EncoderType::V4l2M2m, - EncoderBackend::MediaCodec => H265EncoderType::MediaCodec, EncoderBackend::Software => H265EncoderType::Software, } } @@ -199,7 +195,6 @@ pub fn get_available_h265_encoders(width: u32, height: u32) -> Vec { let ctx = EncodeContext { name: String::new(), - mc_name: None, width: width as i32, height: height as i32, pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12), @@ -373,7 +368,6 @@ impl H265Encoder { let ctx = EncodeContext { name: codec_name.to_string(), - mc_name: None, width: width as i32, height: height as i32, pixfmt, diff --git a/src/video/codec/mod.rs b/src/video/codec/mod.rs index 981fae31..ba0ab378 100644 --- a/src/video/codec/mod.rs +++ b/src/video/codec/mod.rs @@ -3,10 +3,6 @@ use hwcodec::common::DataFormat; use hwcodec::ffmpeg_ram::CodecInfo; -#[cfg(feature = "android-mediacodec")] -pub mod android_mediacodec; -#[cfg(feature = "android-mediacodec")] -pub mod android_mjpeg; pub mod convert; pub mod h264; @@ -23,10 +19,6 @@ pub mod vp9; #[cfg(all(feature = "desktop", any(target_arch = "aarch64", target_arch = "arm")))] pub mod mjpeg_rkmpp; -#[cfg(feature = "android-mediacodec")] -pub use android_mediacodec::{AndroidH264Packet, AndroidMediaCodecH264Encoder}; -#[cfg(feature = "android-mediacodec")] -pub use android_mjpeg::AndroidMediaCodecMjpegDecoder; pub use convert::{MjpegToNv12Decoder, PixelConverter, Yuv420pBuffer}; pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat}; pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat}; diff --git a/src/video/codec/registry.rs b/src/video/codec/registry.rs index 8f83920d..d12bb663 100644 --- a/src/video/codec/registry.rs +++ b/src/video/codec/registry.rs @@ -96,8 +96,6 @@ pub enum EncoderBackend { Rkmpp, /// V4L2 Memory-to-Memory (ARM) V4l2m2m, - /// Android MediaCodec via FFmpeg - MediaCodec, /// Software encoding (libx264, libx265, libvpx) Software, } @@ -117,8 +115,6 @@ impl EncoderBackend { EncoderBackend::Rkmpp } else if name.contains("v4l2m2m") { EncoderBackend::V4l2m2m - } else if name.contains("mediacodec") { - EncoderBackend::MediaCodec } else { EncoderBackend::Software } @@ -138,7 +134,6 @@ impl EncoderBackend { EncoderBackend::Amf => "AMF", EncoderBackend::Rkmpp => "RKMPP", EncoderBackend::V4l2m2m => "V4L2 M2M", - EncoderBackend::MediaCodec => "MediaCodec", EncoderBackend::Software => "Software", } } @@ -153,7 +148,6 @@ impl EncoderBackend { "amf" => Some(EncoderBackend::Amf), "rkmpp" => Some(EncoderBackend::Rkmpp), "v4l2m2m" | "v4l2" => Some(EncoderBackend::V4l2m2m), - "mediacodec" | "android-mediacodec" => Some(EncoderBackend::MediaCodec), "software" | "cpu" => Some(EncoderBackend::Software), _ => None, } @@ -261,8 +255,8 @@ impl EncoderRegistry { let codec_name = match format { VideoEncoderType::H264 => "libx264", VideoEncoderType::H265 => "libx265", - VideoEncoderType::VP8 => "libvpx", - VideoEncoderType::VP9 => "libvpx-vp9", + VideoEncoderType::VP8 => "libvpx_vp8", + VideoEncoderType::VP9 => "libvpx_vp9", }; encoders.push(AvailableEncoder { @@ -309,10 +303,9 @@ impl EncoderRegistry { self.encoders.clear(); self.detection_resolution = (width, height); - // Create test context for encoder detection + // Create test context for encoder detection. let ctx = EncodeContext { name: String::new(), - mc_name: None, width: width as i32, height: height as i32, pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12), @@ -332,7 +325,6 @@ impl EncoderRegistry { ctx.clone(), Duration::from_millis(DETECT_TIMEOUT_MS), ); - info!("Found {} encoders from hwcodec", all_encoders.len()); for codec_info in &all_encoders { diff --git a/src/video/codec/self_check.rs b/src/video/codec/self_check.rs index f82d720e..be6eed32 100644 --- a/src/video/codec/self_check.rs +++ b/src/video/codec/self_check.rs @@ -2,8 +2,6 @@ use serde::Serialize; use std::sync::mpsc; use std::time::{Duration, Instant}; -#[cfg(feature = "android-mediacodec")] -use super::AndroidMediaCodecH264Encoder; use super::{ EncoderRegistry, H264Config, H264Encoder, H265Config, H265Encoder, VP8Config, VP8Encoder, VP9Config, VP9Encoder, VideoEncoderType, @@ -237,32 +235,6 @@ fn run_smoke_test( } fn run_h264_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> { - #[cfg(feature = "android-mediacodec")] - if codec_name_ffmpeg == "h264_mediacodec" { - let mut encoder = AndroidMediaCodecH264Encoder::new( - resolution, - PixelFormat::Nv12, - 30, - bitrate_kbps_for_resolution(resolution), - )?; - encoder.request_keyframe(); - let frame = build_nv12_test_frame( - resolution, - PixelFormat::Nv12.frame_size(resolution).unwrap_or(0), - ); - - for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS { - let frames = encoder.encode_raw(&frame, pts_ms(sequence))?; - if frames.iter().any(|frame| !frame.data.is_empty()) { - return Ok(()); - } - } - - return Err(AppError::VideoError( - "Encoder produced no output after multiple frames".to_string(), - )); - } - let mut encoder = H264Encoder::with_codec( H264Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)), codec_name_ffmpeg, diff --git a/src/video/codec/vp8.rs b/src/video/codec/vp8.rs index 4c0f37fb..0fde7440 100644 --- a/src/video/codec/vp8.rs +++ b/src/video/codec/vp8.rs @@ -130,7 +130,6 @@ pub fn get_available_vp8_encoders(width: u32, height: u32) -> Vec { let ctx = EncodeContext { name: String::new(), - mc_name: None, width: width as i32, height: height as i32, pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12), @@ -271,7 +270,6 @@ impl VP8Encoder { let ctx = EncodeContext { name: codec_name.to_string(), - mc_name: None, width: width as i32, height: height as i32, pixfmt, diff --git a/src/video/codec/vp9.rs b/src/video/codec/vp9.rs index 5ce1cbc1..a9fafaad 100644 --- a/src/video/codec/vp9.rs +++ b/src/video/codec/vp9.rs @@ -130,7 +130,6 @@ pub fn get_available_vp9_encoders(width: u32, height: u32) -> Vec { let ctx = EncodeContext { name: String::new(), - mc_name: None, width: width as i32, height: height as i32, pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12), @@ -271,7 +270,6 @@ impl VP9Encoder { let ctx = EncodeContext { name: codec_name.to_string(), - mc_name: None, width: width as i32, height: height as i32, pixfmt, diff --git a/src/video/device/linux.rs b/src/video/device/linux.rs index c3c3c929..4cd4adf1 100644 --- a/src/video/device/linux.rs +++ b/src/video/device/linux.rs @@ -963,19 +963,6 @@ pub fn enumerate_devices() -> Result> { // for a single MIPI CSI pipeline. Keep only the highest-priority node per // (driver, bus_info) group so users see one device instead of ~11. dedup_platform_subdevices(&mut devices); - devices.retain(|device| { - let hide = should_hide_android_platform_node(device); - if hide { - debug!( - "Hiding Android platform video node: {} ({}) {}", - device.name, - device.driver, - device.path.display() - ); - } - !hide - }); - info!("Found {} video capture devices", devices.len()); Ok(devices) } @@ -1055,33 +1042,6 @@ fn dedup_platform_subdevices(devices: &mut Vec) { }); } -fn should_hide_android_platform_node(device: &VideoDeviceInfo) -> bool { - if !cfg!(feature = "android") { - return false; - } - - let driver = device.driver.to_ascii_lowercase(); - let name = device.name.to_ascii_lowercase(); - let card = device.card.to_ascii_lowercase(); - let usb_device = driver == "uvcvideo" || device.bus_info.starts_with("usb-"); - let known_bridge = - driver.contains("rkcif") || driver.contains("rk_hdmirx") || driver.contains("tc358743"); - if usb_device || known_bridge { - return false; - } - - matches!( - driver.as_str(), - "ionvideo" | "amlvideo" | "amlvideo2" | "videosync" - ) || matches!( - name.as_str(), - "ionvideo" | "amlvideo" | "amlvideo2" | "videosync" - ) || matches!( - card.as_str(), - "ionvideo" | "amlvideo" | "amlvideo2" | "videosync" - ) -} - /// rkcif registers many `/dev/video*` queues; probing all in parallel can /// contend and time out. Keep one node per board (lowest `videoN`). fn collapse_rkcif_probe_candidates(candidates: &mut Vec) { @@ -1185,20 +1145,6 @@ fn sysfs_maybe_capture(path: &Path) -> bool { .to_lowercase(); let driver = extract_uevent_value(&uevent, "driver"); - if cfg!(feature = "android") { - let platform_skip = ["ionvideo", "amlvideo", "amlvideo2", "videosync"]; - let driver_skip = driver - .as_ref() - .is_some_and(|driver| platform_skip.iter().any(|hint| driver == hint)); - if driver_skip || platform_skip.iter().any(|hint| sysfs_name == *hint) { - debug!( - "Skipping Android platform video node {:?}: {}", - path, sysfs_name - ); - return false; - } - } - let mut maybe_capture = false; let capture_hints = [ "capture", diff --git a/src/video/mod.rs b/src/video/mod.rs index 204cafa5..acdd38f4 100644 --- a/src/video/mod.rs +++ b/src/video/mod.rs @@ -8,21 +8,19 @@ pub mod codec_constraints; pub mod device; pub mod format; pub mod frame; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod pipeline; pub mod signal; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod stream_manager; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod streamer; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod traits; -#[cfg(any(feature = "android", feature = "desktop"))] +#[cfg(feature = "desktop")] pub mod types; pub use capture::{CaptureMeta, CaptureStream}; -#[cfg(feature = "android-mediacodec")] -pub use codec::{AndroidH264Packet, AndroidMediaCodecH264Encoder}; pub use codec::{H264Encoder, H264EncoderType, JpegEncoder, PixelConverter, Yuv420pBuffer}; pub use device::{VideoDevice, VideoDeviceInfo}; pub use format::PixelFormat; diff --git a/src/video/pipeline/encoder_state.rs b/src/video/pipeline/encoder_state.rs index c97c5cb7..158edf1c 100644 --- a/src/video/pipeline/encoder_state.rs +++ b/src/video/pipeline/encoder_state.rs @@ -6,16 +6,9 @@ use crate::video::codec::registry::{EncoderBackend, EncoderRegistry, VideoEncode use crate::video::codec::traits::EncoderConfig; use crate::video::codec::vp8::{VP8Config, VP8Encoder}; use crate::video::codec::vp9::{VP9Config, VP9Encoder}; -#[cfg(feature = "android-mediacodec")] -use crate::video::codec::AndroidMediaCodecH264Encoder; -#[cfg(feature = "android-mediacodec")] -use crate::video::codec::AndroidMediaCodecMjpegDecoder; use crate::video::format::{PixelFormat, Resolution}; use bytes::Bytes; -#[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") -))] +#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] use hwcodec::ffmpeg_hw::{ last_error_message as ffmpeg_hw_last_error, HwMjpegH26xConfig, HwMjpegH26xPipeline, }; @@ -29,15 +22,9 @@ pub(super) struct EncoderThreadState { pub(super) nv12_converter: Option, pub(super) yuv420p_converter: Option, pub(super) encoder_needs_yuv420p: bool, - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] pub(super) ffmpeg_hw_pipeline: Option, - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] pub(super) ffmpeg_hw_enabled: bool, pub(super) fps: u32, pub(super) codec: VideoEncoderType, @@ -129,35 +116,6 @@ impl VideoEncoderTrait for H265EncoderWrapper { } } -#[cfg(feature = "android-mediacodec")] -struct AndroidMediaCodecH264EncoderWrapper(AndroidMediaCodecH264Encoder); - -#[cfg(feature = "android-mediacodec")] -impl VideoEncoderTrait for AndroidMediaCodecH264EncoderWrapper { - fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result> { - let frames = self.0.encode_raw(data, pts_ms)?; - Ok(frames - .into_iter() - .map(|f| EncodedFrame { - data: f.data, - key: if f.key_frame { 1 } else { 0 }, - }) - .collect()) - } - - fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> { - self.0.set_bitrate(bitrate_kbps) - } - - fn codec_name(&self) -> &str { - self.0.codec_name() - } - - fn request_keyframe(&mut self) { - self.0.request_keyframe() - } -} - struct VP8EncoderWrapper(VP8Encoder); impl VideoEncoderTrait for VP8EncoderWrapper { @@ -209,50 +167,12 @@ impl VideoEncoderTrait for VP9EncoderWrapper { } pub(super) enum MjpegDecoderKind { - #[cfg(feature = "android-mediacodec")] - AndroidMediaCodec { - decoder: AndroidMediaCodecMjpegDecoder, - fallback: Box, - fallback_active: bool, - output: Vec, - }, - Libyuv { - decoder: MjpegToNv12Decoder, - }, + Libyuv { decoder: MjpegToNv12Decoder }, } impl MjpegDecoderKind { pub(super) fn decode(&mut self, data: &[u8]) -> Result<&[u8]> { match self { - #[cfg(feature = "android-mediacodec")] - MjpegDecoderKind::AndroidMediaCodec { - decoder, - fallback, - fallback_active, - output, - } => { - if !*fallback_active { - match decoder.decode_to_nv12(data) { - Ok(decoded) => { - *output = decoded; - return Ok(output.as_slice()); - } - Err(AppError::VideoError(message)) - if message.contains("needs more input") => - { - return Err(AppError::VideoError(message)); - } - Err(err) => { - tracing::warn!( - "Android MediaCodec MJPEG decode failed; falling back to libyuv MJPEG->NV12: {}", - err - ); - *fallback_active = true; - } - } - } - fallback.decode(data) - } MjpegDecoderKind::Libyuv { decoder } => decoder.decode(data), } } @@ -265,40 +185,6 @@ fn libyuv_mjpeg_decoder(resolution: Resolution) -> MjpegDecoderKind { } fn create_mjpeg_decoder(resolution: Resolution) -> Result<(MjpegDecoderKind, PixelFormat)> { - #[cfg(feature = "android-mediacodec")] - { - if std::env::var_os("ONE_KVM_ANDROID_MJPEG_MEDIACODEC").is_none() { - info!("MJPEG input detected, using libyuv decoder (MJPEG -> NV12)"); - return Ok((libyuv_mjpeg_decoder(resolution), PixelFormat::Nv12)); - } - - info!("MJPEG input detected, trying Android MediaCodec decoder (MJPEG -> NV12)"); - match AndroidMediaCodecMjpegDecoder::new(resolution) { - Ok(decoder) => { - info!("Using Android MediaCodec MJPEG decoder"); - return Ok(( - MjpegDecoderKind::AndroidMediaCodec { - decoder, - fallback: Box::new(libyuv_mjpeg_decoder(resolution)), - fallback_active: false, - output: Vec::with_capacity( - PixelFormat::Nv12 - .frame_size(resolution) - .unwrap_or((resolution.width * resolution.height * 3 / 2) as usize), - ), - }, - PixelFormat::Nv12, - )); - } - Err(err) => { - tracing::warn!( - "Android MediaCodec MJPEG decoder unavailable; using libyuv MJPEG->NV12: {}", - err - ); - } - } - } - info!("MJPEG input detected, using libyuv decoder (MJPEG -> NV12)"); Ok((libyuv_mjpeg_decoder(resolution), PixelFormat::Nv12)) } @@ -400,15 +286,9 @@ pub(super) fn build_encoder_state( } }; - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] let is_rkmpp_encoder = selected_codec_name.contains("rkmpp"); - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] if needs_mjpeg_decode && is_rkmpp_encoder && matches!( @@ -448,15 +328,9 @@ pub(super) fn build_encoder_state( nv12_converter: None, yuv420p_converter: None, encoder_needs_yuv420p: false, - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] ffmpeg_hw_pipeline: Some(pipeline), - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] ffmpeg_hw_enabled: true, fps: config.fps, codec: config.output_codec, @@ -495,40 +369,7 @@ pub(super) fn build_encoder_state( ); } - #[cfg(feature = "android-mediacodec")] - { - if codec_name == "h264_mediacodec" { - info!( - "Creating Android MediaCodec H264 encoder for {:?} input", - input_format - ); - let pixel_format = match input_format { - H264InputFormat::Nv12 => PixelFormat::Nv12, - H264InputFormat::Yuv420p => PixelFormat::Yuv420, - other => { - return Err(AppError::VideoError(format!( - "Android MediaCodec H264 does not support {:?} direct input", - other - ))); - } - }; - let encoder = AndroidMediaCodecH264Encoder::new( - config.resolution, - pixel_format, - config.fps, - config.bitrate_kbps(), - )?; - info!("Created Android MediaCodec H264 encoder"); - Box::new(AndroidMediaCodecH264EncoderWrapper(encoder)) - } else { - create_h264_encoder(config, input_format, &codec_name)? - } - } - - #[cfg(not(feature = "android-mediacodec"))] - { - create_h264_encoder(config, input_format, &codec_name)? - } + create_h264_encoder(config, input_format, &codec_name)? } VideoEncoderType::H265 => { let codec_name = selected_codec_name.clone(); @@ -622,11 +463,6 @@ pub(super) fn build_encoder_state( pipeline_input_format, PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv21 | PixelFormat::Yuv420 ) - } else if codec_name.contains("mediacodec") { - matches!( - pipeline_input_format, - PixelFormat::Nv12 | PixelFormat::Yuv420 - ) } else { false }; @@ -676,15 +512,9 @@ pub(super) fn build_encoder_state( nv12_converter, yuv420p_converter, encoder_needs_yuv420p: needs_yuv420p, - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] ffmpeg_hw_pipeline: None, - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] ffmpeg_hw_enabled: false, fps: config.fps, codec: config.output_codec, @@ -708,12 +538,6 @@ fn h264_direct_input_format( PixelFormat::Nv24 => Some(H264InputFormat::Nv24), _ => None, } - } else if codec_name.contains("mediacodec") { - match input_format { - PixelFormat::Nv12 => Some(H264InputFormat::Nv12), - PixelFormat::Yuv420 => Some(H264InputFormat::Yuv420p), - _ => None, - } } else if codec_name.contains("libx264") { match input_format { PixelFormat::Nv12 => Some(H264InputFormat::Nv12), diff --git a/src/video/pipeline/shared.rs b/src/video/pipeline/shared.rs index f0ee968c..77300af8 100644 --- a/src/video/pipeline/shared.rs +++ b/src/video/pipeline/shared.rs @@ -61,10 +61,7 @@ use crate::video::signal::SignalStatus; const MIN_CAPTURE_FRAME_SIZE: usize = 128; -#[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") -))] +#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] use hwcodec::ffmpeg_hw::last_error_message as ffmpeg_hw_last_error; /// Encoded video frame for distribution @@ -484,15 +481,9 @@ impl SharedVideoPipeline { fn apply_cmd(&self, state: &mut EncoderThreadState, cmd: PipelineCmd) -> Result<()> { match cmd { PipelineCmd::SetBitrate { bitrate_kbps, gop } => { - #[cfg(any( - not(any(target_arch = "aarch64", target_arch = "arm")), - target_os = "android" - ))] + #[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))] let _ = gop; - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] if state.ffmpeg_hw_enabled { if let Some(ref mut pipeline) = state.ffmpeg_hw_pipeline { pipeline @@ -659,7 +650,7 @@ impl SharedVideoPipeline { *guard = Some(cmd_tx); } - // Encoder loop uses a dedicated OS thread because FFmpeg/MediaCodec work is synchronous. + // Encoder loop uses a dedicated OS thread because FFmpeg work is synchronous. { let pipeline = pipeline.clone(); let latest_frame = latest_frame.clone(); @@ -1289,10 +1280,7 @@ impl SharedVideoPipeline { current_ts_us.saturating_sub(start_ts_us) / 1000 }; - #[cfg(all( - any(target_arch = "aarch64", target_arch = "arm"), - not(target_os = "android") - ))] + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] if state.ffmpeg_hw_enabled { if input_format != PixelFormat::Mjpeg { return Err(AppError::VideoError( diff --git a/src/web/handlers/update_api.rs b/src/web/handlers/update_api.rs index bf607b22..f83e265b 100644 --- a/src/web/handlers/update_api.rs +++ b/src/web/handlers/update_api.rs @@ -9,12 +9,6 @@ pub async fn update_overview( State(state): State>, axum::extract::Query(query): axum::extract::Query, ) -> Result> { - if cfg!(feature = "android") { - return Err(AppError::BadRequest( - "Online upgrade is disabled on Android".to_string(), - )); - } - let channel = query.channel.unwrap_or(UpdateChannel::Stable); let response = state.update.overview(channel).await?; Ok(Json(response)) @@ -24,12 +18,6 @@ pub async fn update_upgrade( State(state): State>, Json(req): Json, ) -> Result> { - if cfg!(feature = "android") { - return Err(AppError::BadRequest( - "Online upgrade is disabled on Android".to_string(), - )); - } - state.update.start_upgrade(req, state.shutdown_tx.clone())?; Ok(Json(LoginResponse { @@ -41,11 +29,5 @@ pub async fn update_upgrade( pub async fn update_status( State(state): State>, ) -> Result> { - if cfg!(feature = "android") { - return Err(AppError::BadRequest( - "Online upgrade is disabled on Android".to_string(), - )); - } - Ok(Json(state.update.status().await)) } diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 5d29f6d5..68238bd8 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -66,7 +66,7 @@ export interface FeatureCapability { } export interface PlatformCapabilities { - mode: 'android_amlogic' | 'linux' | 'windows' + mode: 'linux' | 'windows' mode_label: string video_capture: FeatureCapability encoder: FeatureCapability diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index 5e74f80d..d25c90c3 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -125,7 +125,6 @@ const authStore = useAuthStore() const featureVisibility = useFeatureVisibility() const isWindows = computed(() => systemStore.platform?.mode === 'windows') -const isAndroid = computed(() => systemStore.platform?.mode === 'android_amlogic') const msdAvailable = computed(() => systemStore.platform?.msd.available ?? systemStore.capabilities?.msd.available ?? false) const activeSection = ref('appearance') @@ -285,7 +284,6 @@ async function loadSectionData(section: SettingsSectionId) { ]) return case 'about': - if (isAndroid.value) return await Promise.all([ loadUpdateOverview(), refreshUpdateStatus(), @@ -2181,7 +2179,6 @@ async function triggerAutoRestart() { } async function loadUpdateOverview() { - if (isAndroid.value) return updateLoading.value = true try { updateOverview.value = await updateApi.overview(updateChannel.value) @@ -2197,7 +2194,6 @@ async function loadUpdateOverview() { } async function refreshUpdateStatus() { - if (isAndroid.value) return try { updateStatus.value = await updateApi.status() @@ -2230,7 +2226,6 @@ function stopUpdatePolling() { } function startUpdatePolling() { - if (isAndroid.value) return if (updateStatusTimer !== null) return updateStatusTimer = window.setInterval(async () => { await refreshUpdateStatus() @@ -2245,7 +2240,6 @@ function startUpdatePolling() { } async function startOnlineUpgrade() { - if (isAndroid.value) return try { updateSawRestarting.value = false updateSawRequestFailure.value = false @@ -2642,7 +2636,7 @@ onMounted(async () => { }) watch(updateChannel, async () => { - if (activeSection.value === 'about' && !isAndroid.value) { + if (activeSection.value === 'about') { await loadUpdateOverview() } }) @@ -5198,7 +5192,7 @@ watch(isWindows, () => {
- +
{{ t('settings.onlineUpgrade') }}