From 1ded7a8a66ef658e3d9ec1cacbd8f24f8ce38839 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Tue, 14 Jul 2026 21:01:47 +0800 Subject: [PATCH] =?UTF-8?q?del:=20=E7=A7=BB=E9=99=A4=E5=AE=89=E5=8D=93?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 71 +- android/.gitignore | 7 - android/app/build.gradle.kts | 559 ------------- android/app/src/main/AndroidManifest.xml | 36 - .../cn/one_kvm/androidhost/BootReceiver.kt | 13 - .../cn/one_kvm/androidhost/HostSettings.kt | 33 - .../java/cn/one_kvm/androidhost/LogConfig.kt | 30 - .../java/cn/one_kvm/androidhost/LogStore.kt | 71 -- .../cn/one_kvm/androidhost/MainActivity.kt | 452 ---------- .../cn/one_kvm/androidhost/NativeBridge.kt | 21 - .../cn/one_kvm/androidhost/OneKvmService.kt | 413 --------- .../one_kvm/androidhost/ServiceStatusStore.kt | 75 -- .../main/res/drawable/ic_launcher_one_kvm.xml | 38 - .../src/main/res/drawable/ic_stat_one_kvm.xml | 13 - android/app/src/main/res/values/strings.xml | 4 - android/app/src/main/res/values/styles.xml | 7 - android/build.gradle.kts | 3 - android/gradle.properties | 3 - android/gradle/wrapper/gradle-wrapper.jar | Bin 45457 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - android/gradlew | 251 ------ android/gradlew.bat | 94 --- android/native/Cargo.toml | 21 - .../native/src/bin/one-kvm-android-host.rs | 24 - android/native/src/lib.rs | 182 ---- android/settings.gradle.kts | 39 - build/build-android.sh | 103 --- build/cross/Dockerfile.android | 319 ------- libs/hwcodec/.gitignore | 2 - libs/hwcodec/README.md | 13 - libs/hwcodec/build.rs | 213 +---- libs/hwcodec/cpp/common/util.cpp | 35 +- .../cpp/ffmpeg_ram/ffmpeg_ram_decode.cpp | 11 +- .../cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp | 113 +-- libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_ffi.h | 3 +- libs/hwcodec/src/common.rs | 4 +- libs/hwcodec/src/ffmpeg_ram/decode.rs | 2 +- libs/hwcodec/src/ffmpeg_ram/encode.rs | 91 +- libs/hwcodec/src/ffmpeg_ram/mod.rs | 17 +- libs/hwcodec/src/lib.rs | 5 +- libs/v4l2r/Android.bp | 52 -- libs/v4l2r/README.md | 4 - libs/v4l2r/bindgen.rs | 2 +- libs/v4l2r/build.rs | 126 +-- libs/v4l2r/v4l2r_wrapper.h | 6 - res/vcpkg/libyuv/build.rs | 255 +----- scripts/build-android-alsa.sh | 200 ----- scripts/build-android-ffmpeg-mediacodec.sh | 291 ------- scripts/build-android-libyuv.sh | 184 ---- scripts/build-android-opus.sh | 151 ---- scripts/build-android-turbojpeg.sh | 173 ---- src/audio/capture.rs | 6 +- src/audio/capture_android.rs | 292 ------- src/audio/device.rs | 6 +- src/audio/device_android.rs | 185 ---- src/diagnostics/linux.rs | 106 +-- src/lib.rs | 61 +- src/main.rs | 40 +- src/msd/controller.rs | 81 +- src/otg/configfs.rs | 6 +- src/otg/manager.rs | 13 +- src/otg/self_check.rs | 32 +- src/platform/android.rs | 44 - src/platform/android_bionic.rs | 175 ---- src/platform/capabilities.rs | 12 +- src/platform/defaults.rs | 55 +- src/platform/mod.rs | 4 - src/redfish/routes/mod.rs | 4 +- src/runtime/android.rs | 789 ------------------ src/runtime/mod.rs | 4 - src/stream/mjpeg.rs | 4 +- src/utils/mod.rs | 4 +- src/video/codec/android_mediacodec.rs | 122 --- src/video/codec/android_mjpeg.rs | 137 --- src/video/codec/h264.rs | 6 - src/video/codec/h265.rs | 6 - src/video/codec/mod.rs | 8 - src/video/codec/registry.rs | 14 +- src/video/codec/self_check.rs | 28 - src/video/codec/vp8.rs | 2 - src/video/codec/vp9.rs | 2 - src/video/device/linux.rs | 54 -- src/video/mod.rs | 12 +- src/video/pipeline/encoder_state.rs | 198 +---- src/video/pipeline/shared.rs | 22 +- src/web/handlers/update_api.rs | 18 - web/src/api/index.ts | 2 +- web/src/views/SettingsView.vue | 10 +- 88 files changed, 299 insertions(+), 7107 deletions(-) delete mode 100644 android/.gitignore delete mode 100644 android/app/build.gradle.kts delete mode 100644 android/app/src/main/AndroidManifest.xml delete mode 100644 android/app/src/main/java/cn/one_kvm/androidhost/BootReceiver.kt delete mode 100644 android/app/src/main/java/cn/one_kvm/androidhost/HostSettings.kt delete mode 100644 android/app/src/main/java/cn/one_kvm/androidhost/LogConfig.kt delete mode 100644 android/app/src/main/java/cn/one_kvm/androidhost/LogStore.kt delete mode 100644 android/app/src/main/java/cn/one_kvm/androidhost/MainActivity.kt delete mode 100644 android/app/src/main/java/cn/one_kvm/androidhost/NativeBridge.kt delete mode 100644 android/app/src/main/java/cn/one_kvm/androidhost/OneKvmService.kt delete mode 100644 android/app/src/main/java/cn/one_kvm/androidhost/ServiceStatusStore.kt delete mode 100644 android/app/src/main/res/drawable/ic_launcher_one_kvm.xml delete mode 100644 android/app/src/main/res/drawable/ic_stat_one_kvm.xml delete mode 100644 android/app/src/main/res/values/strings.xml delete mode 100644 android/app/src/main/res/values/styles.xml delete mode 100644 android/build.gradle.kts delete mode 100644 android/gradle.properties delete mode 100644 android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 android/gradle/wrapper/gradle-wrapper.properties delete mode 100755 android/gradlew delete mode 100644 android/gradlew.bat delete mode 100644 android/native/Cargo.toml delete mode 100644 android/native/src/bin/one-kvm-android-host.rs delete mode 100644 android/native/src/lib.rs delete mode 100644 android/settings.gradle.kts delete mode 100755 build/build-android.sh delete mode 100644 build/cross/Dockerfile.android delete mode 100644 libs/v4l2r/Android.bp delete mode 100644 scripts/build-android-alsa.sh delete mode 100644 scripts/build-android-ffmpeg-mediacodec.sh delete mode 100644 scripts/build-android-libyuv.sh delete mode 100644 scripts/build-android-opus.sh delete mode 100644 scripts/build-android-turbojpeg.sh delete mode 100644 src/audio/capture_android.rs delete mode 100644 src/audio/device_android.rs delete mode 100644 src/platform/android.rs delete mode 100644 src/platform/android_bionic.rs delete mode 100644 src/runtime/android.rs delete mode 100644 src/runtime/mod.rs delete mode 100644 src/video/codec/android_mediacodec.rs delete mode 100644 src/video/codec/android_mjpeg.rs 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 8bdaf60c75ab801e22807dde59e12a8735a34077..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45457 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 c82=K%QXLKFx%@O{wJjEi6Y56o#$)Bpeg 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') }}