mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-15 04:01:58 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c98aea7e3 | ||
|
|
da61644dbc | ||
|
|
5de7ecd4c5 | ||
|
|
4b65eebd5d | ||
|
|
921c00c472 | ||
|
|
b6bd76534f | ||
|
|
780cfb4cca | ||
|
|
8f9272d985 | ||
|
|
f679647ec9 |
85
.github/workflows/docker.yml
vendored
Normal file
85
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Docker image tag
|
||||
required: false
|
||||
default: latest
|
||||
platforms:
|
||||
description: Docker platforms
|
||||
required: false
|
||||
default: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
DOCKER_PLATFORMS: ${{ github.event_name == 'workflow_dispatch' && inputs.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
||||
DOCKER_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 360
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y unzip xz-utils
|
||||
cargo install cross --locked
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: web
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build linux binaries
|
||||
run: bash build/build-images.sh
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Aliyun Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.cn-hangzhou.aliyuncs.com
|
||||
username: ${{ secrets.ALIYUN_USERNAME }}
|
||||
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||
|
||||
- name: Publish Docker images
|
||||
run: |
|
||||
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry docker.io/silentwind0 --tag "$DOCKER_TAG" --push
|
||||
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry registry.cn-hangzhou.aliyuncs.com/silentwind --tag "$DOCKER_TAG" --push
|
||||
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry docker.io/silentwind0 --variant full --tag "$DOCKER_TAG" --push
|
||||
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry registry.cn-hangzhou.aliyuncs.com/silentwind --variant full --tag "$DOCKER_TAG" --push
|
||||
@@ -20,6 +20,7 @@ desktop = [
|
||||
"dep:sqlx",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
"dep:toml_edit",
|
||||
"dep:tracing",
|
||||
"dep:tracing-subscriber",
|
||||
"dep:thiserror",
|
||||
@@ -38,6 +39,7 @@ desktop = [
|
||||
"dep:axum-server",
|
||||
"dep:clap",
|
||||
"dep:time",
|
||||
"dep:tempfile",
|
||||
"dep:bytes",
|
||||
"dep:bytemuck",
|
||||
"dep:xxhash-rust",
|
||||
@@ -98,6 +100,7 @@ android = [
|
||||
"dep:sdp-types",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
"dep:toml_edit",
|
||||
"dep:serialport",
|
||||
"dep:sha2",
|
||||
"dep:sodiumoxide",
|
||||
@@ -106,6 +109,7 @@ android = [
|
||||
"dep:audiopus",
|
||||
"dep:thiserror",
|
||||
"dep:time",
|
||||
"dep:tempfile",
|
||||
"dep:tokio",
|
||||
"dep:tokio-tungstenite",
|
||||
"dep:tokio-util",
|
||||
@@ -143,6 +147,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"], optional = tru
|
||||
# Serialization
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
toml_edit = { version = "0.25", optional = true }
|
||||
|
||||
# Logging
|
||||
tracing = { version = "0.1", optional = true }
|
||||
@@ -160,6 +165,7 @@ rand = { version = "0.9", optional = true }
|
||||
# Utilities
|
||||
uuid = { version = "1", features = ["v4", "serde"], optional = true }
|
||||
base64 = { version = "0.22", optional = true }
|
||||
tempfile = { version = "3", optional = true }
|
||||
|
||||
# HTTP client (for URL downloads)
|
||||
# Use rustls by default, but allow native-tls for systems with older GLIBC
|
||||
|
||||
14
agents.md
14
agents.md
@@ -1,14 +0,0 @@
|
||||
# Agents Notes
|
||||
|
||||
## Windows MSVC Build
|
||||
|
||||
Run from the repository root in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:VCPKG_ROOT='C:\Users\mofen\code\vcpkg'
|
||||
$env:TURBOJPEG_SOURCE='explicit'
|
||||
$env:TURBOJPEG_LIB_DIR='C:\Users\mofen\code\vcpkg\installed\x64-windows-static\lib'
|
||||
$env:TURBOJPEG_INCLUDE_DIR='C:\Users\mofen\code\vcpkg\installed\x64-windows-static\include'
|
||||
|
||||
cargo build --target x86_64-pc-windows-msvc
|
||||
```
|
||||
@@ -44,8 +44,6 @@ val oneKvmVersion = Regex("""(?m)^version\s*=\s*"([^"]+)"""")
|
||||
?.groupValues
|
||||
?.get(1)
|
||||
?: throw GradleException("Failed to resolve version from root Cargo.toml")
|
||||
val androidFfmpegSourceDir = rootProject.layout.projectDirectory
|
||||
.dir("../.tmp/android-ffmpeg-check/src/ffmpeg-rockchip")
|
||||
val localProperties = Properties().apply {
|
||||
val file = rootProject.file("local.properties")
|
||||
if (file.exists()) {
|
||||
@@ -207,7 +205,6 @@ tasks.register<Exec>("buildAndroidFfmpegMediaCodec") {
|
||||
group = "build"
|
||||
|
||||
val ffmpegRoot = file(androidFfmpegRoot.get())
|
||||
val sourceDir = androidFfmpegSourceDir.asFile
|
||||
val scriptFile = androidFfmpegBuildScript.asFile
|
||||
val stampFile = ffmpegRoot.resolve(".one-kvm-android-ffmpeg.stamp")
|
||||
|
||||
@@ -215,8 +212,6 @@ tasks.register<Exec>("buildAndroidFfmpegMediaCodec") {
|
||||
commandLine(
|
||||
"bash",
|
||||
scriptFile.absolutePath,
|
||||
"--source",
|
||||
sourceDir.absolutePath,
|
||||
"--output",
|
||||
ffmpegRoot.absolutePath,
|
||||
"--ndk",
|
||||
@@ -227,7 +222,6 @@ tasks.register<Exec>("buildAndroidFfmpegMediaCodec") {
|
||||
selectedAndroidAbis.joinToString(","),
|
||||
)
|
||||
|
||||
inputs.dir(sourceDir)
|
||||
inputs.file(scriptFile)
|
||||
outputs.dir(ffmpegRoot)
|
||||
|
||||
@@ -235,12 +229,6 @@ tasks.register<Exec>("buildAndroidFfmpegMediaCodec") {
|
||||
val hasAndroidFfmpeg = androidFfmpegRequiredFiles(ffmpegRoot).all { it.exists() }
|
||||
val hasCurrentBuildStamp =
|
||||
stampFile.exists() && stampFile.readText() == androidFfmpegBuildStamp(scriptFile)
|
||||
if (!hasAndroidFfmpeg && !sourceDir.resolve("configure").exists()) {
|
||||
throw GradleException(
|
||||
"Missing Android FFmpeg MediaCodec build at ${ffmpegRoot.absolutePath}, " +
|
||||
"and source was not found at ${sourceDir.absolutePath}",
|
||||
)
|
||||
}
|
||||
!hasAndroidFfmpeg || !hasCurrentBuildStamp
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
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()
|
||||
@@ -7,8 +18,18 @@ pluginManagement {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
34
build/build-android.sh
Normal file → Executable file
34
build/build-android.sh
Normal file → Executable file
@@ -17,28 +17,21 @@ fail() {
|
||||
build_android() {
|
||||
local arch="$1"
|
||||
local docker_build_args=()
|
||||
local docker_mount_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}"
|
||||
local gradle_cache="${ONE_KVM_ANDROID_GRADLE_CACHE_DIR:-one-kvm-android-gradle-cache}"
|
||||
local cargo_registry_cache="${ONE_KVM_ANDROID_CARGO_REGISTRY_CACHE_DIR:-one-kvm-android-cargo-registry}"
|
||||
local cargo_git_cache="${ONE_KVM_ANDROID_CARGO_GIT_CACHE_DIR:-one-kvm-android-cargo-git}"
|
||||
|
||||
add_cache_mount() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
|
||||
if [[ "$source" == /* || "$source" == ./* || "$source" == ../* ]]; then
|
||||
mkdir -p "$source"
|
||||
source="$(cd "$source" && pwd)"
|
||||
fi
|
||||
|
||||
docker_mount_args+=("-v" "$source:$target")
|
||||
}
|
||||
|
||||
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
|
||||
@@ -55,16 +48,12 @@ build_android() {
|
||||
"$PROJECT_ROOT/build/cross"
|
||||
fi
|
||||
|
||||
add_cache_mount "$gradle_cache" "/root/.gradle"
|
||||
add_cache_mount "$cargo_registry_cache" "/root/.cargo/registry"
|
||||
add_cache_mount "$cargo_git_cache" "/root/.cargo/git"
|
||||
|
||||
echo "=== Building Android APK: $arch ==="
|
||||
docker run --rm \
|
||||
-v "$PROJECT_ROOT:/workspace" \
|
||||
"${docker_mount_args[@]}" \
|
||||
-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" \
|
||||
@@ -102,9 +91,6 @@ Examples:
|
||||
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_GRADLE_CACHE_DIR Host Gradle cache path or Docker volume name
|
||||
ONE_KVM_ANDROID_CARGO_REGISTRY_CACHE_DIR Host Cargo registry cache path or Docker volume name
|
||||
ONE_KVM_ANDROID_CARGO_GIT_CACHE_DIR Host Cargo git cache path or Docker volume name
|
||||
ONE_KVM_ANDROID_SKIP_DOCKER_BUILD=1 Reuse an already loaded Docker image
|
||||
|
||||
APK output:
|
||||
|
||||
@@ -21,16 +21,21 @@ build_arch() {
|
||||
|
||||
case "${CHINAMIRRO:-}" in
|
||||
1|true|TRUE|yes|YES|on|ON)
|
||||
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--build-arg CHINAMIRRO=1"
|
||||
echo "=== China mirror acceleration: enabled (Tsinghua) ==="
|
||||
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--progress=plain --build-arg CHINAMIRRO=1 --build-arg GH_PROXY=${GH_PROXY:-https://gh-proxy.com/} --build-arg DEBIAN_IMAGE=${DEBIAN_IMAGE:-docker.1ms.run/library/debian:11}"
|
||||
cross_build_opts="$cross_build_opts --build-arg HTTP_PROXY= --build-arg HTTPS_PROXY= --build-arg ALL_PROXY= --build-arg NO_PROXY="
|
||||
cross_build_opts="$cross_build_opts --build-arg http_proxy= --build-arg https_proxy= --build-arg all_proxy= --build-arg no_proxy="
|
||||
echo "=== China mirror acceleration: enabled ==="
|
||||
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
||||
env \
|
||||
CROSS_BUILD_OPTS="$cross_build_opts" \
|
||||
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=tuna \
|
||||
CARGO_SOURCE_TUNA_REGISTRY=sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/ \
|
||||
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=rsproxy-sparse \
|
||||
CARGO_SOURCE_RSPROXY_REGISTRY=https://rsproxy.cn/crates.io-index \
|
||||
CARGO_SOURCE_RSPROXY_SPARSE_REGISTRY=sparse+https://rsproxy.cn/index/ \
|
||||
CARGO_REGISTRIES_RSPROXY_INDEX=https://rsproxy.cn/crates.io-index \
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||
RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup \
|
||||
RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup \
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||
RUSTUP_DIST_SERVER=https://rsproxy.cn \
|
||||
RUSTUP_UPDATE_ROOT=https://rsproxy.cn/rustup \
|
||||
cross build --release --target "$rust_target"
|
||||
return
|
||||
;;
|
||||
@@ -66,7 +71,7 @@ case "${1:-all}" in
|
||||
echo "Examples:"
|
||||
echo " $0 # Build all"
|
||||
echo " $0 x86_64 # Build x86_64 only"
|
||||
echo " CHINAMIRRO=1 $0 arm64 # Build with Tsinghua mirrors"
|
||||
echo " CHINAMIRRO=1 $0 arm64 # Build with China mirrors"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
@@ -83,16 +88,4 @@ for target in "${ARCH_MAP[@]}"; do
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "Static libraries:"
|
||||
for target in "${ARCH_MAP[@]}"; do
|
||||
case "$target" in
|
||||
x86_64-unknown-linux-gnu) gnu_target="x86_64-linux-gnu" ;;
|
||||
aarch64-unknown-linux-gnu) gnu_target="aarch64-linux-gnu" ;;
|
||||
armv7-unknown-linux-gnueabihf) gnu_target="armv7-linux-gnueabihf" ;;
|
||||
esac
|
||||
if [ -d "$PROJECT_DIR/target/one-kvm-libs/$gnu_target/lib" ]; then
|
||||
echo " $gnu_target: OK"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "Next step: ./build/package-docker.sh or ./build/package-deb.sh"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Android build image for One-KVM
|
||||
# Based on Debian 11 for stable toolchain/runtime compatibility
|
||||
|
||||
FROM debian:11
|
||||
ARG DEBIAN_IMAGE=debian:11
|
||||
FROM ${DEBIAN_IMAGE}
|
||||
|
||||
ARG CHINAMIRRO=0
|
||||
ARG ANDROID_SDK_ROOT=/root/android-sdk
|
||||
@@ -10,11 +11,13 @@ 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://mirrors.tuna.tsinghua.edu.cn/rustup
|
||||
ARG RUSTUP_UPDATE_ROOT_CN=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup
|
||||
ARG CARGO_REGISTRY_CN=sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/
|
||||
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=
|
||||
|
||||
@@ -29,9 +32,8 @@ 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 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
||||
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
|
||||
|
||||
@@ -39,6 +41,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
wget \
|
||||
bzip2 \
|
||||
unzip \
|
||||
zip \
|
||||
git \
|
||||
@@ -63,11 +66,15 @@ RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
mkdir -p /root/.cargo; \
|
||||
printf '%s\n' \
|
||||
'[source.crates-io]' \
|
||||
"replace-with = 'tuna'" \
|
||||
'[source.tuna]' \
|
||||
"replace-with = 'rsproxy-sparse'" \
|
||||
'[source.rsproxy]' \
|
||||
"registry = '${CARGO_INDEX_CN}'" \
|
||||
'[source.rsproxy-sparse]' \
|
||||
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||
'[registries.tuna]' \
|
||||
"index = '${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 \
|
||||
@@ -102,6 +109,7 @@ RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
"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}') }" \
|
||||
"}" \
|
||||
@@ -238,7 +246,9 @@ RUN printf '%s\n' \
|
||||
' '"'"' "$WRAPPER_PROPERTIES" > "$WRAPPER_PROPERTIES_TMP"' \
|
||||
' cp "$WRAPPER_PROPERTIES_TMP" "$WRAPPER_PROPERTIES"' \
|
||||
' rm -f "$WRAPPER_PROPERTIES_TMP"' \
|
||||
' find /root/.gradle/wrapper/dists -name "*.lck" -o -name "*.part" 2>/dev/null | xargs -r rm -f' \
|
||||
' 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' \
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
# Cross-compilation image for ARM64 based on Debian 11
|
||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
||||
|
||||
FROM debian:11
|
||||
ARG DEBIAN_IMAGE=debian:11
|
||||
FROM ${DEBIAN_IMAGE}
|
||||
|
||||
# Linux headers used by v4l2r bindgen
|
||||
ARG LINUX_HEADERS_VERSION=6.6
|
||||
ARG LINUX_HEADERS_SHA256=
|
||||
ARG CHINAMIRRO=0
|
||||
ARG GH_PROXY=https://gh-proxy.com/
|
||||
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 LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||
ARG LIBVPX_VERSION=1.16.0
|
||||
ARG X265_VERSION=3.4
|
||||
ARG OPUS_VERSION=1.5.2
|
||||
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||
|
||||
# Optionally use Tsinghua mirrors for builds in China.
|
||||
# Optionally use China mirrors for builds in China.
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
sed -i \
|
||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
||||
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
|
||||
|
||||
@@ -21,19 +29,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
mkdir -p /root/.cargo; \
|
||||
printf '%s\n' \
|
||||
'[source.crates-io]' \
|
||||
"replace-with = 'tuna'" \
|
||||
'[source.tuna]' \
|
||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||
'[registries.tuna]' \
|
||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||
"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 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -69,6 +81,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
# Install ARM64 development libraries (without VAAPI/X11 for ARM)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libssl1.1 \
|
||||
libssl1.1:arm64 \
|
||||
libasound2-dev:arm64 \
|
||||
libv4l-dev:arm64 \
|
||||
libudev-dev:arm64 \
|
||||
@@ -78,20 +92,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libdrm-dev:arm64 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install newer V4L2 headers for v4l2r bindgen
|
||||
RUN mkdir -p /opt/v4l2-headers \
|
||||
&& wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \
|
||||
&& if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \
|
||||
&& tar -xf /tmp/linux-headers.tar.xz -C /tmp \
|
||||
&& cd /tmp/linux-${LINUX_HEADERS_VERSION} \
|
||||
&& make ARCH=arm64 headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \
|
||||
&& rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz
|
||||
|
||||
ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include
|
||||
|
||||
# Build static libjpeg-turbo from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libjpeg-turbo \
|
||||
&& cd /tmp/libjpeg-turbo \
|
||||
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||
@@ -105,8 +113,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
||||
&& rm -rf /tmp/libjpeg-turbo
|
||||
|
||||
# Build static libyuv from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
RUN git init /tmp/libyuv \
|
||||
&& cd /tmp/libyuv \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||
@@ -125,8 +138,13 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
|
||||
# Build static libvpx from source (cross-compile for ARM64)
|
||||
# CC/CXX/LD/AR must be environment variables, not configure arguments
|
||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libvpx \
|
||||
&& cd /tmp/libvpx \
|
||||
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& echo "=== libvpx: Configuring for ARM64 ===" \
|
||||
&& export CC=aarch64-linux-gnu-gcc \
|
||||
&& export CXX=aarch64-linux-gnu-g++ \
|
||||
@@ -149,7 +167,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
&& rm -rf /tmp/libvpx
|
||||
|
||||
# Build static libx264 from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||
&& cd /tmp/x264 \
|
||||
&& export CC=aarch64-linux-gnu-gcc \
|
||||
&& export AR=aarch64-linux-gnu-ar \
|
||||
@@ -165,12 +185,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
&& rm -rf /tmp/x264
|
||||
|
||||
# Build static libx265 from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/x265 \
|
||||
&& cd /tmp/x265 \
|
||||
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& cd source \
|
||||
&& mkdir -p build \
|
||||
&& cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||
-DCMAKE_SYSTEM_NAME=Linux \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||
@@ -204,12 +230,15 @@ Cflags: -I\${includedir}
|
||||
EOF
|
||||
|
||||
# Build static libopus from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||
&& cd /tmp/opus \
|
||||
&& export CC=aarch64-linux-gnu-gcc \
|
||||
&& export AR=aarch64-linux-gnu-ar \
|
||||
&& export RANLIB=aarch64-linux-gnu-ranlib \
|
||||
&& ./autogen.sh \
|
||||
&& ./configure \
|
||||
--prefix=/usr/aarch64-linux-gnu \
|
||||
--host=aarch64-linux-gnu \
|
||||
@@ -217,13 +246,20 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
--disable-doc \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/opus
|
||||
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||
|
||||
# Download and build FFmpeg with RKMPP support
|
||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
||||
&& tar -xzf ffmpeg.tar.gz \
|
||||
&& cd ffmpeg \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 "https://gitee.com/nyanmisaka/mpp.git" rkmpp \
|
||||
&& git clone --depth 1 "https://gitee.com/nyanmisaka/rga.git" rkrga \
|
||||
&& git init ffmpeg-rockchip \
|
||||
&& cd ffmpeg-rockchip \
|
||||
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& cd .. \
|
||||
# Build RKMPP
|
||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||
&& cmake .. \
|
||||
@@ -354,8 +390,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
|
||||
# Add Rust target
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& rustup target add aarch64-unknown-linux-gnu
|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
# Cross-compilation image for ARMv7 based on Debian 11
|
||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
||||
|
||||
FROM debian:11
|
||||
ARG DEBIAN_IMAGE=debian:11
|
||||
FROM ${DEBIAN_IMAGE}
|
||||
|
||||
# Linux headers used by v4l2r bindgen
|
||||
ARG LINUX_HEADERS_VERSION=6.6
|
||||
ARG LINUX_HEADERS_SHA256=
|
||||
ARG CHINAMIRRO=0
|
||||
ARG GH_PROXY=https://gh-proxy.com/
|
||||
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 LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||
ARG LIBVPX_VERSION=1.16.0
|
||||
ARG X265_VERSION=3.4
|
||||
ARG OPUS_VERSION=1.5.2
|
||||
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||
|
||||
# Optionally use Tsinghua mirrors for builds in China.
|
||||
# Optionally use China mirrors for builds in China.
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
sed -i \
|
||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
||||
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
|
||||
|
||||
@@ -21,19 +29,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
mkdir -p /root/.cargo; \
|
||||
printf '%s\n' \
|
||||
'[source.crates-io]' \
|
||||
"replace-with = 'tuna'" \
|
||||
'[source.tuna]' \
|
||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||
'[registries.tuna]' \
|
||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||
"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 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -69,6 +81,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
# Install ARMv7 development libraries (without VAAPI/X11 for ARM)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libssl1.1 \
|
||||
libssl1.1:armhf \
|
||||
libasound2-dev:armhf \
|
||||
libv4l-dev:armhf \
|
||||
libudev-dev:armhf \
|
||||
@@ -77,20 +91,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libdrm-dev:armhf \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install newer V4L2 headers for v4l2r bindgen
|
||||
RUN mkdir -p /opt/v4l2-headers \
|
||||
&& wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \
|
||||
&& if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \
|
||||
&& tar -xf /tmp/linux-headers.tar.xz -C /tmp \
|
||||
&& cd /tmp/linux-${LINUX_HEADERS_VERSION} \
|
||||
&& make ARCH=arm headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \
|
||||
&& rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz
|
||||
|
||||
ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include
|
||||
|
||||
# Build static libjpeg-turbo from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libjpeg-turbo \
|
||||
&& cd /tmp/libjpeg-turbo \
|
||||
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||
@@ -104,8 +112,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
||||
&& rm -rf /tmp/libjpeg-turbo
|
||||
|
||||
# Build static libyuv from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
RUN git init /tmp/libyuv \
|
||||
&& cd /tmp/libyuv \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||
@@ -123,8 +136,13 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
&& rm -rf /tmp/libyuv
|
||||
|
||||
# Build static libvpx from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libvpx \
|
||||
&& cd /tmp/libvpx \
|
||||
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& export CC=arm-linux-gnueabihf-gcc \
|
||||
&& export CXX=arm-linux-gnueabihf-g++ \
|
||||
&& export LD=arm-linux-gnueabihf-ld \
|
||||
@@ -141,7 +159,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
&& rm -rf /tmp/libvpx
|
||||
|
||||
# Build static libx264 from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||
&& cd /tmp/x264 \
|
||||
&& export CC=arm-linux-gnueabihf-gcc \
|
||||
&& export AR=arm-linux-gnueabihf-ar \
|
||||
@@ -158,12 +178,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
&& rm -rf /tmp/x264
|
||||
|
||||
# Build static libx265 from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/x265 \
|
||||
&& cd /tmp/x265 \
|
||||
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& cd source \
|
||||
&& mkdir -p build \
|
||||
&& cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||
-DCMAKE_SYSTEM_NAME=Linux \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||
@@ -193,12 +219,15 @@ Cflags: -I\${includedir}
|
||||
EOF
|
||||
|
||||
# Build static libopus from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||
&& cd /tmp/opus \
|
||||
&& export CC=arm-linux-gnueabihf-gcc \
|
||||
&& export AR=arm-linux-gnueabihf-ar \
|
||||
&& export RANLIB=arm-linux-gnueabihf-ranlib \
|
||||
&& ./autogen.sh \
|
||||
&& ./configure \
|
||||
--prefix=/usr/arm-linux-gnueabihf \
|
||||
--host=arm-linux-gnueabihf \
|
||||
@@ -206,13 +235,20 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
--disable-doc \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/opus
|
||||
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||
|
||||
# Download and build FFmpeg with RKMPP support
|
||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
||||
&& tar -xzf ffmpeg.tar.gz \
|
||||
&& cd ffmpeg \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 "https://gitee.com/nyanmisaka/mpp.git" rkmpp \
|
||||
&& git clone --depth 1 "https://gitee.com/nyanmisaka/rga.git" rkrga \
|
||||
&& git init ffmpeg-rockchip \
|
||||
&& cd ffmpeg-rockchip \
|
||||
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& cd .. \
|
||||
# Build RKMPP
|
||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||
&& cmake .. \
|
||||
@@ -343,8 +379,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
|
||||
# Add Rust target
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& rustup target add armv7-unknown-linux-gnueabihf
|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
# Cross-compilation image for x86_64 based on Debian 11
|
||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
||||
|
||||
FROM debian:11
|
||||
ARG DEBIAN_IMAGE=debian:11
|
||||
FROM ${DEBIAN_IMAGE}
|
||||
|
||||
# Linux headers used by v4l2r bindgen
|
||||
ARG LINUX_HEADERS_VERSION=6.6
|
||||
ARG LINUX_HEADERS_SHA256=
|
||||
ARG CHINAMIRRO=0
|
||||
ARG GH_PROXY=https://gh-proxy.com/
|
||||
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 LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||
ARG LIBVPX_VERSION=1.16.0
|
||||
ARG X265_VERSION=3.4
|
||||
ARG OPUS_VERSION=1.5.2
|
||||
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||
|
||||
# Optionally use Tsinghua mirrors for builds in China.
|
||||
# Optionally use China mirrors for builds in China.
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
sed -i \
|
||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
||||
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
|
||||
|
||||
@@ -21,19 +29,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
mkdir -p /root/.cargo; \
|
||||
printf '%s\n' \
|
||||
'[source.crates-io]' \
|
||||
"replace-with = 'tuna'" \
|
||||
'[source.tuna]' \
|
||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||
'[registries.tuna]' \
|
||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||
"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 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -76,20 +88,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libxdmcp-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install newer V4L2 headers for v4l2r bindgen
|
||||
RUN mkdir -p /opt/v4l2-headers \
|
||||
&& wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \
|
||||
&& if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \
|
||||
&& tar -xf /tmp/linux-headers.tar.xz -C /tmp \
|
||||
&& cd /tmp/linux-${LINUX_HEADERS_VERSION} \
|
||||
&& make ARCH=x86 headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \
|
||||
&& rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz
|
||||
|
||||
ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include
|
||||
|
||||
# Build static libjpeg-turbo from source (needed by libyuv)
|
||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libjpeg-turbo \
|
||||
&& cd /tmp/libjpeg-turbo \
|
||||
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||
@@ -100,8 +106,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
||||
&& rm -rf /tmp/libjpeg-turbo
|
||||
|
||||
# Build static libyuv from source (uses libjpeg-turbo headers)
|
||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
RUN git init /tmp/libyuv \
|
||||
&& cd /tmp/libyuv \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DJPEG_FOUND=TRUE \
|
||||
@@ -114,17 +125,25 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
&& rm -rf /tmp/libyuv
|
||||
|
||||
# Build static libvpx from source
|
||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libvpx \
|
||||
&& cd /tmp/libvpx \
|
||||
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& ./configure \
|
||||
--enable-static --disable-shared --enable-pic \
|
||||
--disable-examples --disable-tools --disable-docs \
|
||||
--disable-unit-tests \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/libvpx
|
||||
|
||||
# Build static libx264 from source
|
||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||
&& cd /tmp/x264 \
|
||||
&& ./configure --enable-static --disable-cli \
|
||||
&& make -j$(nproc) \
|
||||
@@ -132,12 +151,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
&& rm -rf /tmp/x264
|
||||
|
||||
# Build static libx265 from source
|
||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/x265 \
|
||||
&& cd /tmp/x265 \
|
||||
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& cd source \
|
||||
&& mkdir -p build \
|
||||
&& cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||
-DENABLE_SHARED=OFF \
|
||||
-DENABLE_CLI=OFF \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
@@ -162,21 +187,28 @@ Cflags: -I\${includedir}
|
||||
EOF
|
||||
|
||||
# Build static libopus from source
|
||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||
&& cd /tmp/opus \
|
||||
&& ./autogen.sh \
|
||||
&& ./configure \
|
||||
--enable-static --disable-shared \
|
||||
--disable-doc \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/opus
|
||||
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||
|
||||
# Download and build FFmpeg with minimal configuration for encoding only
|
||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
||||
&& tar -xzf ffmpeg.tar.gz \
|
||||
&& cd ffmpeg/ffmpeg-rockchip \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init ffmpeg-rockchip \
|
||||
&& cd ffmpeg-rockchip \
|
||||
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& ./configure \
|
||||
--enable-gpl \
|
||||
--enable-version3 \
|
||||
@@ -249,8 +281,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
|
||||
# Add Rust target
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& rustup target add x86_64-unknown-linux-gnu
|
||||
|
||||
|
||||
@@ -125,8 +125,8 @@ EOF
|
||||
chmod 755 "$PKG_DIR/DEBIAN/prerm"
|
||||
|
||||
# Create control file
|
||||
BASE_DEPS="libc6 (>= 2.31), libgcc-s1, libstdc++6, libasound2 (>= 1.1), libdrm2 (>= 2.4)"
|
||||
AMD64_DEPS="libva2 (>= 2.0), libva-drm2 (>= 2.10), libva-x11-2 (>= 2.10), libmfx1 (>= 21.1), libx11-6 (>= 1.6), libxcb1 (>= 1.14), i965-va-driver-shaders (>= 2.4), intel-media-va-driver-non-free (>= 21.1)"
|
||||
BASE_DEPS="libc6 (>= 2.31), libgcc-s1, libstdc++6, libasound2t64 (>= 1.1) | libasound2 (>= 1.1), libdrm2 (>= 2.4)"
|
||||
AMD64_DEPS="libva2 (>= 2.0), libva-drm2 (>= 2.10), libva-x11-2 (>= 2.10), libmfx-gen1.2 (>= 22.0) | libmfx1 (>= 21.1), libx11-6 (>= 1.6), libxcb1 (>= 1.14), i965-va-driver-shaders (>= 2.4), intel-media-va-driver-non-free (>= 21.1)"
|
||||
DEPS="$BASE_DEPS"
|
||||
if [ "$DEB_ARCH" = "amd64" ]; then
|
||||
DEPS="$DEPS, $AMD64_DEPS"
|
||||
|
||||
@@ -15,9 +15,8 @@ parts are intentionally removed here so this dependency stays scoped to capture.
|
||||
|
||||
## Build options
|
||||
|
||||
`cargo build` generates V4L2 bindings from `/usr/include/linux/videodev2.h` by
|
||||
default. Set `V4L2R_VIDEODEV2_H_PATH` to a directory containing `videodev2.h` to
|
||||
generate bindings from a different header.
|
||||
`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
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
use std::env::{self, VarError};
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
include!("bindgen.rs");
|
||||
|
||||
/// Environment variable that can be set to point to the directory containing the `videodev2.h`
|
||||
/// file to use to generate the bindings.
|
||||
const V4L2R_VIDEODEV_ENV: &str = "V4L2R_VIDEODEV2_H_PATH";
|
||||
|
||||
/// Default header file to parse if the `V4L2R_VIDEODEV2_H_PATH` environment variable is not set.
|
||||
const DEFAULT_VIDEODEV2_H_PATH: &str = "/usr/include/linux";
|
||||
/// Vendored Linux UAPI include root used for non-Android targets.
|
||||
const VENDORED_INCLUDE_DIR: &str = "include";
|
||||
|
||||
/// Wrapper file to use as input of bindgen.
|
||||
const WRAPPER_H: &str = "v4l2r_wrapper.h";
|
||||
@@ -20,29 +16,14 @@ fn main() {
|
||||
let target = env::var("TARGET").unwrap_or_default();
|
||||
let is_android = target.contains("android");
|
||||
|
||||
let default_videodev2_h_path = if is_android {
|
||||
android_sysroot().join("usr/include").display().to_string()
|
||||
let include_root = if is_android {
|
||||
android_sysroot().join("usr/include")
|
||||
} else {
|
||||
DEFAULT_VIDEODEV2_H_PATH.to_string()
|
||||
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` is not set"))
|
||||
.join(VENDORED_INCLUDE_DIR)
|
||||
};
|
||||
let videodev2_h = include_root.join("linux/videodev2.h");
|
||||
|
||||
let videodev2_h_path = env::var(V4L2R_VIDEODEV_ENV)
|
||||
.or_else(|e| {
|
||||
if let VarError::NotPresent = e {
|
||||
Ok(default_videodev2_h_path.clone())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
.expect("invalid `V4L2R_VIDEODEV2_H_PATH` environment variable");
|
||||
|
||||
let videodev2_h = PathBuf::from(videodev2_h_path.clone()).join(if is_android {
|
||||
"linux/videodev2.h"
|
||||
} else {
|
||||
"videodev2.h"
|
||||
});
|
||||
|
||||
println!("cargo::rerun-if-env-changed={}", V4L2R_VIDEODEV_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");
|
||||
@@ -54,7 +35,7 @@ fn main() {
|
||||
println!("cargo::rerun-if-changed={}", WRAPPER_H);
|
||||
|
||||
let mut clang_args = vec![
|
||||
format!("-I{videodev2_h_path}"),
|
||||
format!("-I{}", include_root.display()),
|
||||
#[cfg(all(feature = "arch64", not(feature = "arch32")))]
|
||||
"--target=x86_64-linux-gnu".into(),
|
||||
#[cfg(all(feature = "arch32", not(feature = "arch64")))]
|
||||
|
||||
31
libs/v4l2r/include/asm-generic/bitsperlong.h
Normal file
31
libs/v4l2r/include/asm-generic/bitsperlong.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
#ifndef __ASM_GENERIC_BITS_PER_LONG
|
||||
#define __ASM_GENERIC_BITS_PER_LONG
|
||||
|
||||
#ifndef __BITS_PER_LONG
|
||||
/*
|
||||
* In order to keep safe and avoid regression, only unify uapi
|
||||
* bitsperlong.h for some archs which are using newer toolchains
|
||||
* that have the definitions of __CHAR_BIT__ and __SIZEOF_LONG__.
|
||||
* See the following link for more info:
|
||||
* https://lore.kernel.org/linux-arch/b9624545-2c80-49a1-ac3c-39264a591f7b@app.fastmail.com/
|
||||
*/
|
||||
#if defined(__CHAR_BIT__) && defined(__SIZEOF_LONG__)
|
||||
#define __BITS_PER_LONG (__CHAR_BIT__ * __SIZEOF_LONG__)
|
||||
#else
|
||||
/*
|
||||
* There seems to be no way of detecting this automatically from user
|
||||
* space, so 64 bit architectures should override this in their
|
||||
* bitsperlong.h. In particular, an architecture that supports
|
||||
* both 32 and 64 bit user space must not rely on CONFIG_64BIT
|
||||
* to decide it, but rather check a compiler provided macro.
|
||||
*/
|
||||
#define __BITS_PER_LONG 32
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef __BITS_PER_LONG_LONG
|
||||
#define __BITS_PER_LONG_LONG 64
|
||||
#endif
|
||||
|
||||
#endif /* __ASM_GENERIC_BITS_PER_LONG */
|
||||
40
libs/v4l2r/include/asm-generic/int-ll64.h
Normal file
40
libs/v4l2r/include/asm-generic/int-ll64.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
/*
|
||||
* asm-generic/int-ll64.h
|
||||
*
|
||||
* Integer declarations for architectures which use "long long"
|
||||
* for 64-bit types.
|
||||
*/
|
||||
|
||||
#ifndef _ASM_GENERIC_INT_LL64_H
|
||||
#define _ASM_GENERIC_INT_LL64_H
|
||||
|
||||
#include <asm/bitsperlong.h>
|
||||
|
||||
#ifndef __ASSEMBLY__
|
||||
/*
|
||||
* __xx is ok: it doesn't pollute the POSIX namespace. Use these in the
|
||||
* header files exported to user space
|
||||
*/
|
||||
|
||||
typedef __signed__ char __s8;
|
||||
typedef unsigned char __u8;
|
||||
|
||||
typedef __signed__ short __s16;
|
||||
typedef unsigned short __u16;
|
||||
|
||||
typedef __signed__ int __s32;
|
||||
typedef unsigned int __u32;
|
||||
|
||||
#ifdef __GNUC__
|
||||
__extension__ typedef __signed__ long long __s64;
|
||||
__extension__ typedef unsigned long long __u64;
|
||||
#else
|
||||
typedef __signed__ long long __s64;
|
||||
typedef unsigned long long __u64;
|
||||
#endif
|
||||
|
||||
#endif /* __ASSEMBLY__ */
|
||||
|
||||
|
||||
#endif /* _ASM_GENERIC_INT_LL64_H */
|
||||
105
libs/v4l2r/include/asm-generic/ioctl.h
Normal file
105
libs/v4l2r/include/asm-generic/ioctl.h
Normal file
@@ -0,0 +1,105 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
#ifndef _ASM_GENERIC_IOCTL_H
|
||||
#define _ASM_GENERIC_IOCTL_H
|
||||
|
||||
/* ioctl command encoding: 32 bits total, command in lower 16 bits,
|
||||
* size of the parameter structure in the lower 14 bits of the
|
||||
* upper 16 bits.
|
||||
* Encoding the size of the parameter structure in the ioctl request
|
||||
* is useful for catching programs compiled with old versions
|
||||
* and to avoid overwriting user space outside the user buffer area.
|
||||
* The highest 2 bits are reserved for indicating the ``access mode''.
|
||||
* NOTE: This limits the max parameter size to 16kB -1 !
|
||||
*/
|
||||
|
||||
/*
|
||||
* The following is for compatibility across the various Linux
|
||||
* platforms. The generic ioctl numbering scheme doesn't really enforce
|
||||
* a type field. De facto, however, the top 8 bits of the lower 16
|
||||
* bits are indeed used as a type field, so we might just as well make
|
||||
* this explicit here. Please be sure to use the decoding macros
|
||||
* below from now on.
|
||||
*/
|
||||
#define _IOC_NRBITS 8
|
||||
#define _IOC_TYPEBITS 8
|
||||
|
||||
/*
|
||||
* Let any architecture override either of the following before
|
||||
* including this file.
|
||||
*/
|
||||
|
||||
#ifndef _IOC_SIZEBITS
|
||||
# define _IOC_SIZEBITS 14
|
||||
#endif
|
||||
|
||||
#ifndef _IOC_DIRBITS
|
||||
# define _IOC_DIRBITS 2
|
||||
#endif
|
||||
|
||||
#define _IOC_NRMASK ((1 << _IOC_NRBITS)-1)
|
||||
#define _IOC_TYPEMASK ((1 << _IOC_TYPEBITS)-1)
|
||||
#define _IOC_SIZEMASK ((1 << _IOC_SIZEBITS)-1)
|
||||
#define _IOC_DIRMASK ((1 << _IOC_DIRBITS)-1)
|
||||
|
||||
#define _IOC_NRSHIFT 0
|
||||
#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS)
|
||||
#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS)
|
||||
#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS)
|
||||
|
||||
/*
|
||||
* Direction bits, which any architecture can choose to override
|
||||
* before including this file.
|
||||
*
|
||||
* NOTE: _IOC_WRITE means userland is writing and kernel is
|
||||
* reading. _IOC_READ means userland is reading and kernel is writing.
|
||||
*/
|
||||
|
||||
#ifndef _IOC_NONE
|
||||
# define _IOC_NONE 0U
|
||||
#endif
|
||||
|
||||
#ifndef _IOC_WRITE
|
||||
# define _IOC_WRITE 1U
|
||||
#endif
|
||||
|
||||
#ifndef _IOC_READ
|
||||
# define _IOC_READ 2U
|
||||
#endif
|
||||
|
||||
#define _IOC(dir,type,nr,size) \
|
||||
(((dir) << _IOC_DIRSHIFT) | \
|
||||
((type) << _IOC_TYPESHIFT) | \
|
||||
((nr) << _IOC_NRSHIFT) | \
|
||||
((size) << _IOC_SIZESHIFT))
|
||||
|
||||
#define _IOC_TYPECHECK(t) (sizeof(t))
|
||||
|
||||
/*
|
||||
* Used to create numbers.
|
||||
*
|
||||
* NOTE: _IOW means userland is writing and kernel is reading. _IOR
|
||||
* means userland is reading and kernel is writing.
|
||||
*/
|
||||
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
|
||||
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
|
||||
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
|
||||
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
|
||||
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
|
||||
#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
|
||||
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
|
||||
|
||||
/* used to decode ioctl numbers.. */
|
||||
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
|
||||
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
|
||||
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
|
||||
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
|
||||
|
||||
/* ...and for the drivers/sound files... */
|
||||
|
||||
#define IOC_IN (_IOC_WRITE << _IOC_DIRSHIFT)
|
||||
#define IOC_OUT (_IOC_READ << _IOC_DIRSHIFT)
|
||||
#define IOC_INOUT ((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT)
|
||||
#define IOCSIZE_MASK (_IOC_SIZEMASK << _IOC_SIZESHIFT)
|
||||
#define IOCSIZE_SHIFT (_IOC_SIZESHIFT)
|
||||
|
||||
#endif /* _ASM_GENERIC_IOCTL_H */
|
||||
99
libs/v4l2r/include/asm-generic/posix_types.h
Normal file
99
libs/v4l2r/include/asm-generic/posix_types.h
Normal file
@@ -0,0 +1,99 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
#ifndef __ASM_GENERIC_POSIX_TYPES_H
|
||||
#define __ASM_GENERIC_POSIX_TYPES_H
|
||||
|
||||
#include <asm/bitsperlong.h>
|
||||
/*
|
||||
* This file is generally used by user-level software, so you need to
|
||||
* be a little careful about namespace pollution etc.
|
||||
*
|
||||
* First the types that are often defined in different ways across
|
||||
* architectures, so that you can override them.
|
||||
*/
|
||||
|
||||
#ifndef __kernel_long_t
|
||||
typedef long __kernel_long_t;
|
||||
typedef unsigned long __kernel_ulong_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_ino_t
|
||||
typedef __kernel_ulong_t __kernel_ino_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_mode_t
|
||||
typedef unsigned int __kernel_mode_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_pid_t
|
||||
typedef int __kernel_pid_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_ipc_pid_t
|
||||
typedef int __kernel_ipc_pid_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_uid_t
|
||||
typedef unsigned int __kernel_uid_t;
|
||||
typedef unsigned int __kernel_gid_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_suseconds_t
|
||||
typedef __kernel_long_t __kernel_suseconds_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_daddr_t
|
||||
typedef int __kernel_daddr_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_uid32_t
|
||||
typedef unsigned int __kernel_uid32_t;
|
||||
typedef unsigned int __kernel_gid32_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_old_uid_t
|
||||
typedef __kernel_uid_t __kernel_old_uid_t;
|
||||
typedef __kernel_gid_t __kernel_old_gid_t;
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_old_dev_t
|
||||
typedef unsigned int __kernel_old_dev_t;
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Most 32 bit architectures use "unsigned int" size_t,
|
||||
* and all 64 bit architectures use "unsigned long" size_t.
|
||||
*/
|
||||
#ifndef __kernel_size_t
|
||||
#if __BITS_PER_LONG != 64
|
||||
typedef unsigned int __kernel_size_t;
|
||||
typedef int __kernel_ssize_t;
|
||||
typedef int __kernel_ptrdiff_t;
|
||||
#else
|
||||
typedef __kernel_ulong_t __kernel_size_t;
|
||||
typedef __kernel_long_t __kernel_ssize_t;
|
||||
typedef __kernel_long_t __kernel_ptrdiff_t;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef __kernel_fsid_t
|
||||
typedef struct {
|
||||
int val[2];
|
||||
} __kernel_fsid_t;
|
||||
#endif
|
||||
|
||||
/*
|
||||
* anything below here should be completely generic
|
||||
*/
|
||||
typedef __kernel_long_t __kernel_off_t;
|
||||
typedef long long __kernel_loff_t;
|
||||
typedef __kernel_long_t __kernel_old_time_t;
|
||||
typedef __kernel_long_t __kernel_time_t;
|
||||
typedef long long __kernel_time64_t;
|
||||
typedef __kernel_long_t __kernel_clock_t;
|
||||
typedef int __kernel_timer_t;
|
||||
typedef int __kernel_clockid_t;
|
||||
typedef char * __kernel_caddr_t;
|
||||
typedef unsigned short __kernel_uid16_t;
|
||||
typedef unsigned short __kernel_gid16_t;
|
||||
|
||||
#endif /* __ASM_GENERIC_POSIX_TYPES_H */
|
||||
9
libs/v4l2r/include/asm-generic/types.h
Normal file
9
libs/v4l2r/include/asm-generic/types.h
Normal file
@@ -0,0 +1,9 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
#ifndef _ASM_GENERIC_TYPES_H
|
||||
#define _ASM_GENERIC_TYPES_H
|
||||
/*
|
||||
* int-ll64 is used everywhere now.
|
||||
*/
|
||||
#include <asm-generic/int-ll64.h>
|
||||
|
||||
#endif /* _ASM_GENERIC_TYPES_H */
|
||||
53
libs/v4l2r/include/linux/const.h
Normal file
53
libs/v4l2r/include/linux/const.h
Normal file
@@ -0,0 +1,53 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
/* const.h: Macros for dealing with constants. */
|
||||
|
||||
#ifndef _LINUX_CONST_H
|
||||
#define _LINUX_CONST_H
|
||||
|
||||
/* Some constant macros are used in both assembler and
|
||||
* C code. Therefore we cannot annotate them always with
|
||||
* 'UL' and other type specifiers unilaterally. We
|
||||
* use the following macros to deal with this.
|
||||
*
|
||||
* Similarly, _AT() will cast an expression with a type in C, but
|
||||
* leave it unchanged in asm.
|
||||
*/
|
||||
|
||||
#ifdef __ASSEMBLY__
|
||||
#define _AC(X,Y) X
|
||||
#define _AT(T,X) X
|
||||
#else
|
||||
#define __AC(X,Y) (X##Y)
|
||||
#define _AC(X,Y) __AC(X,Y)
|
||||
#define _AT(T,X) ((T)(X))
|
||||
#endif
|
||||
|
||||
#define _UL(x) (_AC(x, UL))
|
||||
#define _ULL(x) (_AC(x, ULL))
|
||||
|
||||
#define _BITUL(x) (_UL(1) << (x))
|
||||
#define _BITULL(x) (_ULL(1) << (x))
|
||||
|
||||
#if !defined(__ASSEMBLY__)
|
||||
/*
|
||||
* Missing __asm__ support
|
||||
*
|
||||
* __BIT128() would not work in the __asm__ code, as it shifts an
|
||||
* 'unsigned __init128' data type as direct representation of
|
||||
* 128 bit constants is not supported in the gcc compiler, as
|
||||
* they get silently truncated.
|
||||
*
|
||||
* TODO: Please revisit this implementation when gcc compiler
|
||||
* starts representing 128 bit constants directly like long
|
||||
* and unsigned long etc. Subsequently drop the comment for
|
||||
* GENMASK_U128() which would then start supporting __asm__ code.
|
||||
*/
|
||||
#define _BIT128(x) ((unsigned __int128)(1) << (x))
|
||||
#endif
|
||||
|
||||
#define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (__typeof__(x))(a) - 1)
|
||||
#define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask))
|
||||
|
||||
#define __KERNEL_DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
|
||||
|
||||
#endif /* _LINUX_CONST_H */
|
||||
8
libs/v4l2r/include/linux/ioctl.h
Normal file
8
libs/v4l2r/include/linux/ioctl.h
Normal file
@@ -0,0 +1,8 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
#ifndef _LINUX_IOCTL_H
|
||||
#define _LINUX_IOCTL_H
|
||||
|
||||
#include <asm/ioctl.h>
|
||||
|
||||
#endif /* _LINUX_IOCTL_H */
|
||||
|
||||
38
libs/v4l2r/include/linux/posix_types.h
Normal file
38
libs/v4l2r/include/linux/posix_types.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
#ifndef _LINUX_POSIX_TYPES_H
|
||||
#define _LINUX_POSIX_TYPES_H
|
||||
|
||||
#include <linux/stddef.h>
|
||||
|
||||
/*
|
||||
* This allows for 1024 file descriptors: if NR_OPEN is ever grown
|
||||
* beyond that you'll have to change this too. But 1024 fd's seem to be
|
||||
* enough even for such "real" unices like OSF/1, so hopefully this is
|
||||
* one limit that doesn't have to be changed [again].
|
||||
*
|
||||
* Note that POSIX wants the FD_CLEAR(fd,fdsetp) defines to be in
|
||||
* <sys/time.h> (and thus <linux/time.h>) - but this is a more logical
|
||||
* place for them. Solved by having dummy defines in <sys/time.h>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This macro may have been defined in <gnu/types.h>. But we always
|
||||
* use the one here.
|
||||
*/
|
||||
#undef __FD_SETSIZE
|
||||
#define __FD_SETSIZE 1024
|
||||
|
||||
typedef struct {
|
||||
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
|
||||
} __kernel_fd_set;
|
||||
|
||||
/* Type of a signal handler. */
|
||||
typedef void (*__kernel_sighandler_t)(int);
|
||||
|
||||
/* Type of a SYSV IPC key. */
|
||||
typedef int __kernel_key_t;
|
||||
typedef int __kernel_mqd_t;
|
||||
|
||||
#include <asm/posix_types.h>
|
||||
|
||||
#endif /* _LINUX_POSIX_TYPES_H */
|
||||
73
libs/v4l2r/include/linux/stddef.h
Normal file
73
libs/v4l2r/include/linux/stddef.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
#ifndef _LINUX_STDDEF_H
|
||||
#define _LINUX_STDDEF_H
|
||||
|
||||
|
||||
|
||||
#ifndef __always_inline
|
||||
#define __always_inline __inline__
|
||||
#endif
|
||||
|
||||
/* Not all C++ standards support type declarations inside an anonymous union */
|
||||
#ifndef __cplusplus
|
||||
#define __struct_group_tag(TAG) TAG
|
||||
#else
|
||||
#define __struct_group_tag(TAG)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* __struct_group() - Create a mirrored named and anonyomous struct
|
||||
*
|
||||
* @TAG: The tag name for the named sub-struct (usually empty)
|
||||
* @NAME: The identifier name of the mirrored sub-struct
|
||||
* @ATTRS: Any struct attributes (usually empty)
|
||||
* @MEMBERS: The member declarations for the mirrored structs
|
||||
*
|
||||
* Used to create an anonymous union of two structs with identical layout
|
||||
* and size: one anonymous and one named. The former's members can be used
|
||||
* normally without sub-struct naming, and the latter can be used to
|
||||
* reason about the start, end, and size of the group of struct members.
|
||||
* The named struct can also be explicitly tagged for layer reuse (C only),
|
||||
* as well as both having struct attributes appended.
|
||||
*/
|
||||
#define __struct_group(TAG, NAME, ATTRS, MEMBERS...) \
|
||||
union { \
|
||||
struct { MEMBERS } ATTRS; \
|
||||
struct __struct_group_tag(TAG) { MEMBERS } ATTRS NAME; \
|
||||
} ATTRS
|
||||
|
||||
#ifdef __cplusplus
|
||||
/* sizeof(struct{}) is 1 in C++, not 0, can't use C version of the macro. */
|
||||
#define __DECLARE_FLEX_ARRAY(T, member) \
|
||||
T member[0]
|
||||
#else
|
||||
/**
|
||||
* __DECLARE_FLEX_ARRAY() - Declare a flexible array usable in a union
|
||||
*
|
||||
* @TYPE: The type of each flexible array element
|
||||
* @NAME: The name of the flexible array member
|
||||
*
|
||||
* In order to have a flexible array member in a union or alone in a
|
||||
* struct, it needs to be wrapped in an anonymous struct with at least 1
|
||||
* named member, but that member can be empty.
|
||||
*/
|
||||
#define __DECLARE_FLEX_ARRAY(TYPE, NAME) \
|
||||
struct { \
|
||||
struct { } __empty_ ## NAME; \
|
||||
TYPE NAME[]; \
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef __counted_by
|
||||
#define __counted_by(m)
|
||||
#endif
|
||||
|
||||
#ifndef __counted_by_le
|
||||
#define __counted_by_le(m)
|
||||
#endif
|
||||
|
||||
#ifndef __counted_by_be
|
||||
#define __counted_by_be(m)
|
||||
#endif
|
||||
|
||||
#endif /* _LINUX_STDDEF_H */
|
||||
58
libs/v4l2r/include/linux/types.h
Normal file
58
libs/v4l2r/include/linux/types.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
#ifndef _LINUX_TYPES_H
|
||||
#define _LINUX_TYPES_H
|
||||
|
||||
#include <asm/types.h>
|
||||
|
||||
#ifndef __ASSEMBLY__
|
||||
|
||||
#include <linux/posix_types.h>
|
||||
|
||||
#ifdef __SIZEOF_INT128__
|
||||
typedef __signed__ __int128 __s128 __attribute__((aligned(16)));
|
||||
typedef unsigned __int128 __u128 __attribute__((aligned(16)));
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Below are truly Linux-specific types that should never collide with
|
||||
* any application/library that wants linux/types.h.
|
||||
*/
|
||||
|
||||
/* sparse defines __CHECKER__; see Documentation/dev-tools/sparse.rst */
|
||||
#ifdef __CHECKER__
|
||||
#define __bitwise __attribute__((bitwise))
|
||||
#else
|
||||
#define __bitwise
|
||||
#endif
|
||||
|
||||
/* The kernel doesn't use this legacy form, but user space does */
|
||||
#define __bitwise__ __bitwise
|
||||
|
||||
typedef __u16 __bitwise __le16;
|
||||
typedef __u16 __bitwise __be16;
|
||||
typedef __u32 __bitwise __le32;
|
||||
typedef __u32 __bitwise __be32;
|
||||
typedef __u64 __bitwise __le64;
|
||||
typedef __u64 __bitwise __be64;
|
||||
|
||||
typedef __u16 __bitwise __sum16;
|
||||
typedef __u32 __bitwise __wsum;
|
||||
|
||||
/*
|
||||
* aligned_u64 should be used in defining kernel<->userspace ABIs to avoid
|
||||
* common 32/64-bit compat problems.
|
||||
* 64-bit values align to 4-byte boundaries on x86_32 (and possibly other
|
||||
* architectures) and to 8-byte boundaries on 64-bit architectures. The new
|
||||
* aligned_64 type enforces 8-byte alignment so that structs containing
|
||||
* aligned_64 values have the same alignment on 32-bit and 64-bit architectures.
|
||||
* No conversions are necessary between 32-bit user-space and a 64-bit kernel.
|
||||
*/
|
||||
#define __aligned_u64 __u64 __attribute__((aligned(8)))
|
||||
#define __aligned_s64 __s64 __attribute__((aligned(8)))
|
||||
#define __aligned_be64 __be64 __attribute__((aligned(8)))
|
||||
#define __aligned_le64 __le64 __attribute__((aligned(8)))
|
||||
|
||||
typedef unsigned __bitwise __poll_t;
|
||||
|
||||
#endif /* __ASSEMBLY__ */
|
||||
#endif /* _LINUX_TYPES_H */
|
||||
69
libs/v4l2r/include/linux/v4l2-common.h
Normal file
69
libs/v4l2r/include/linux/v4l2-common.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/* SPDX-License-Identifier: ((GPL-2.0+ WITH Linux-syscall-note) OR BSD-3-Clause) */
|
||||
/*
|
||||
* include/linux/v4l2-common.h
|
||||
*
|
||||
* Common V4L2 and V4L2 subdev definitions.
|
||||
*
|
||||
* Users are advised to #include this file either through videodev2.h
|
||||
* (V4L2) or through v4l2-subdev.h (V4L2 subdev) rather than to refer
|
||||
* to this file directly.
|
||||
*
|
||||
* Copyright (C) 2012 Nokia Corporation
|
||||
* Contact: Sakari Ailus <sakari.ailus@iki.fi>
|
||||
*/
|
||||
|
||||
#ifndef __V4L2_COMMON__
|
||||
#define __V4L2_COMMON__
|
||||
|
||||
#include <linux/types.h>
|
||||
|
||||
/*
|
||||
*
|
||||
* Selection interface definitions
|
||||
*
|
||||
*/
|
||||
|
||||
/* Current cropping area */
|
||||
#define V4L2_SEL_TGT_CROP 0x0000
|
||||
/* Default cropping area */
|
||||
#define V4L2_SEL_TGT_CROP_DEFAULT 0x0001
|
||||
/* Cropping bounds */
|
||||
#define V4L2_SEL_TGT_CROP_BOUNDS 0x0002
|
||||
/* Native frame size */
|
||||
#define V4L2_SEL_TGT_NATIVE_SIZE 0x0003
|
||||
/* Current composing area */
|
||||
#define V4L2_SEL_TGT_COMPOSE 0x0100
|
||||
/* Default composing area */
|
||||
#define V4L2_SEL_TGT_COMPOSE_DEFAULT 0x0101
|
||||
/* Composing bounds */
|
||||
#define V4L2_SEL_TGT_COMPOSE_BOUNDS 0x0102
|
||||
/* Current composing area plus all padding pixels */
|
||||
#define V4L2_SEL_TGT_COMPOSE_PADDED 0x0103
|
||||
|
||||
/* Selection flags */
|
||||
#define V4L2_SEL_FLAG_GE (1 << 0)
|
||||
#define V4L2_SEL_FLAG_LE (1 << 1)
|
||||
#define V4L2_SEL_FLAG_KEEP_CONFIG (1 << 2)
|
||||
|
||||
struct v4l2_edid {
|
||||
__u32 pad;
|
||||
__u32 start_block;
|
||||
__u32 blocks;
|
||||
__u32 reserved[5];
|
||||
__u8 *edid;
|
||||
};
|
||||
|
||||
/* Backward compatibility target definitions --- to be removed. */
|
||||
#define V4L2_SEL_TGT_CROP_ACTIVE V4L2_SEL_TGT_CROP
|
||||
#define V4L2_SEL_TGT_COMPOSE_ACTIVE V4L2_SEL_TGT_COMPOSE
|
||||
#define V4L2_SUBDEV_SEL_TGT_CROP_ACTUAL V4L2_SEL_TGT_CROP
|
||||
#define V4L2_SUBDEV_SEL_TGT_COMPOSE_ACTUAL V4L2_SEL_TGT_COMPOSE
|
||||
#define V4L2_SUBDEV_SEL_TGT_CROP_BOUNDS V4L2_SEL_TGT_CROP_BOUNDS
|
||||
#define V4L2_SUBDEV_SEL_TGT_COMPOSE_BOUNDS V4L2_SEL_TGT_COMPOSE_BOUNDS
|
||||
|
||||
/* Backward compatibility flag definitions --- to be removed. */
|
||||
#define V4L2_SUBDEV_SEL_FLAG_SIZE_GE V4L2_SEL_FLAG_GE
|
||||
#define V4L2_SUBDEV_SEL_FLAG_SIZE_LE V4L2_SEL_FLAG_LE
|
||||
#define V4L2_SUBDEV_SEL_FLAG_KEEP_CONFIG V4L2_SEL_FLAG_KEEP_CONFIG
|
||||
|
||||
#endif /* __V4L2_COMMON__ */
|
||||
3499
libs/v4l2r/include/linux/v4l2-controls.h
Normal file
3499
libs/v4l2r/include/linux/v4l2-controls.h
Normal file
File diff suppressed because it is too large
Load Diff
2730
libs/v4l2r/include/linux/videodev2.h
Normal file
2730
libs/v4l2r/include/linux/videodev2.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,13 +5,12 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
SOURCE_DIR=""
|
||||
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_REPO="${ALSA_REPO:-https://github.com/alsa-project/alsa-lib.git}"
|
||||
ALSA_VERSION="${ALSA_VERSION:-1.2.15}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -19,8 +18,6 @@ Usage:
|
||||
scripts/build-android-alsa.sh [options]
|
||||
|
||||
Options:
|
||||
--source <dir> Existing alsa-lib source checkout. If omitted, the
|
||||
script clones it into .tmp/android-alsa-src.
|
||||
--output <dir> Output root. Default: dist/android-alsa
|
||||
--ndk <dir> Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT.
|
||||
--api <level> Android API level. Default: 21.
|
||||
@@ -44,10 +41,6 @@ fail() {
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source)
|
||||
SOURCE_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_DIR="${2:-}"
|
||||
shift 2
|
||||
@@ -77,16 +70,15 @@ 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"
|
||||
|
||||
if [[ -z "$SOURCE_DIR" ]]; then
|
||||
SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-alsa-src"
|
||||
if [[ ! -d "$SOURCE_DIR/.git" ]]; then
|
||||
rm -rf "$SOURCE_DIR"
|
||||
git clone --depth 1 "$ALSA_REPO" "$SOURCE_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -d "$SOURCE_DIR" ]] || fail "alsa-lib source not found: $SOURCE_DIR"
|
||||
[[ -f "$SOURCE_DIR/configure.ac" || -f "$SOURCE_DIR/configure" ]] || fail "alsa-lib source layout not recognized under: $SOURCE_DIR"
|
||||
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"
|
||||
@@ -94,11 +86,6 @@ OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
||||
|
||||
HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64"
|
||||
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||
ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake"
|
||||
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||
[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE"
|
||||
command -v cmake >/dev/null 2>&1 || fail "cmake is required"
|
||||
command -v autoreconf >/dev/null 2>&1 || fail "autoreconf is required"
|
||||
|
||||
normalize_abis() {
|
||||
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||
@@ -147,14 +134,6 @@ build_one() {
|
||||
|
||||
clean_generated_source_headers
|
||||
|
||||
if [[ -f "$SOURCE_DIR/config.status" || -f "$SOURCE_DIR/Makefile" ]]; then
|
||||
(
|
||||
cd "$SOURCE_DIR"
|
||||
make distclean >/dev/null 2>&1 || true
|
||||
)
|
||||
clean_generated_source_headers
|
||||
fi
|
||||
|
||||
if [[ ! -x "$SOURCE_DIR/configure" ]]; then
|
||||
(
|
||||
cd "$SOURCE_DIR"
|
||||
@@ -178,6 +157,8 @@ build_one() {
|
||||
--disable-doc \
|
||||
--disable-oss \
|
||||
--disable-seq \
|
||||
--disable-ucm \
|
||||
--disable-topology \
|
||||
--disable-rawmidi \
|
||||
--disable-hwdep \
|
||||
--disable-usb \
|
||||
|
||||
@@ -5,21 +5,17 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
SOURCE_DIR=""
|
||||
OUTPUT_DIR="${PROJECT_ROOT}/dist/android-ffmpeg-mediacodec"
|
||||
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)}"
|
||||
FFMPEG_ROCKCHIP_REV="${FFMPEG_ROCKCHIP_REV:-40c412daccf08164493da0de990eb99a8948116b}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/build-android-ffmpeg-mediacodec.sh --source <ffmpeg-source-dir> [options]
|
||||
|
||||
Required:
|
||||
--source <dir> FFmpeg source directory. For the downloaded package,
|
||||
use the extracted ffmpeg-rockchip directory.
|
||||
scripts/build-android-ffmpeg-mediacodec.sh [options]
|
||||
|
||||
Options:
|
||||
--output <dir> Output root. Default: dist/android-ffmpeg-mediacodec
|
||||
@@ -35,9 +31,7 @@ The output layout is compatible with ONE_KVM_ANDROID_FFMPEG_ROOT:
|
||||
<output>/armeabi-v7a/lib
|
||||
|
||||
Example:
|
||||
scripts/build-android-ffmpeg-mediacodec.sh \
|
||||
--source .tmp/android-ffmpeg-check/src/ffmpeg-rockchip \
|
||||
--output /opt/one-kvm/android-ffmpeg
|
||||
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
|
||||
@@ -51,10 +45,6 @@ fail() {
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source)
|
||||
SOURCE_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_DIR="${2:-}"
|
||||
shift 2
|
||||
@@ -81,11 +71,24 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$SOURCE_DIR" ]] || fail "--source is required"
|
||||
[[ -d "$SOURCE_DIR" ]] || fail "FFmpeg source not found: $SOURCE_DIR"
|
||||
[[ -x "$SOURCE_DIR/configure" ]] || fail "FFmpeg configure script not found under: $SOURCE_DIR"
|
||||
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"
|
||||
[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT"
|
||||
|
||||
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
@@ -93,7 +96,6 @@ OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
||||
|
||||
HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64"
|
||||
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||
|
||||
normalize_abis() {
|
||||
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||
@@ -106,11 +108,6 @@ patch_android_ffmpeg_mjpeg_mediacodec() {
|
||||
local allcodecs="${avcodec_dir}/allcodecs.c"
|
||||
local makefile="${avcodec_dir}/Makefile"
|
||||
|
||||
[[ -f "$mediacodecdec" ]] || fail "FFmpeg mediacodecdec.c not found: $mediacodecdec"
|
||||
[[ -f "$allcodecs" ]] || fail "FFmpeg allcodecs.c not found: $allcodecs"
|
||||
[[ -f "$makefile" ]] || fail "FFmpeg libavcodec Makefile not found: $makefile"
|
||||
[[ -f "$configure_file" ]] || fail "FFmpeg configure not found: $configure_file"
|
||||
|
||||
python3 - "$mediacodecdec" "$allcodecs" "$configure_file" "$makefile" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
@@ -218,8 +215,6 @@ build_one() {
|
||||
extra_cflags="-fPIC"
|
||||
extra_ldflags=""
|
||||
|
||||
[[ -x "$cc" ]] || fail "Missing compiler: $cc"
|
||||
|
||||
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"
|
||||
|
||||
@@ -5,14 +5,13 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
SOURCE_DIR=""
|
||||
OUTPUT_DIR="${PROJECT_ROOT}/dist/android-libyuv"
|
||||
JPEG_ROOT="${ONE_KVM_ANDROID_TURBOJPEG_ROOT:-${PROJECT_ROOT}/dist/android-turbojpeg}"
|
||||
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)}"
|
||||
LIBYUV_REPO="${LIBYUV_REPO:-https://github.com/lemenkov/libyuv.git}"
|
||||
LIBYUV_REV="${LIBYUV_REV:-957f295ea946cbbd13fcfc46e7066f2efa801233}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -20,8 +19,6 @@ Usage:
|
||||
scripts/build-android-libyuv.sh [options]
|
||||
|
||||
Options:
|
||||
--source <dir> Existing libyuv source checkout. If omitted, the script
|
||||
clones libyuv into .tmp/android-libyuv-src.
|
||||
--output <dir> Output root. Default: dist/android-libyuv
|
||||
--ndk <dir> Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT.
|
||||
--api <level> Android API level. Default: 21.
|
||||
@@ -51,10 +48,6 @@ fail() {
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source)
|
||||
SOURCE_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_DIR="${2:-}"
|
||||
shift 2
|
||||
@@ -88,27 +81,29 @@ 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"
|
||||
|
||||
if [[ -z "$SOURCE_DIR" ]]; then
|
||||
SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-libyuv-src"
|
||||
if [[ ! -d "$SOURCE_DIR/.git" ]]; then
|
||||
rm -rf "$SOURCE_DIR"
|
||||
git clone --depth 1 "$LIBYUV_REPO" "$SOURCE_DIR"
|
||||
fi
|
||||
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
|
||||
|
||||
[[ -d "$SOURCE_DIR" ]] || fail "libyuv source not found: $SOURCE_DIR"
|
||||
[[ -f "$SOURCE_DIR/CMakeLists.txt" ]] || fail "libyuv CMakeLists.txt not found under: $SOURCE_DIR"
|
||||
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"
|
||||
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||
ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake"
|
||||
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||
[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE"
|
||||
command -v cmake >/dev/null 2>&1 || fail "cmake is required"
|
||||
|
||||
normalize_abis() {
|
||||
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||
|
||||
@@ -5,16 +5,12 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
SOURCE_DIR=""
|
||||
OUTPUT_DIR="${PROJECT_ROOT}/dist/android-opus"
|
||||
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)}"
|
||||
OPUS_VERSION="${OPUS_VERSION:-1.5.2}"
|
||||
OPUS_TARBALL_URL="${OPUS_TARBALL_URL:-https://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz}"
|
||||
OPUS_TARBALL_SHA256="${OPUS_TARBALL_SHA256:-65c1d2f78b9f2fb20082c38cbe47c951ad5839345876e46941612ee87f9a7ce1}"
|
||||
LOCAL_OPUS_TARBALL="${LOCAL_OPUS_TARBALL:-${PROJECT_ROOT}/opus-${OPUS_VERSION}.tar.gz}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -22,9 +18,6 @@ Usage:
|
||||
scripts/build-android-opus.sh [options]
|
||||
|
||||
Options:
|
||||
--source <dir> Existing opus source checkout. If omitted, the script
|
||||
downloads and extracts the official source tarball
|
||||
into .tmp/android-opus-src.
|
||||
--output <dir> Output root. Default: dist/android-opus
|
||||
--ndk <dir> Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT.
|
||||
--api <level> Android API level. Default: 21.
|
||||
@@ -46,10 +39,6 @@ fail() {
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source)
|
||||
SOURCE_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_DIR="${2:-}"
|
||||
shift 2
|
||||
@@ -79,25 +68,13 @@ 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"
|
||||
|
||||
if [[ -z "$SOURCE_DIR" ]]; then
|
||||
SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-opus-src"
|
||||
if [[ ! -f "$SOURCE_DIR/configure" ]]; then
|
||||
rm -rf "$SOURCE_DIR"
|
||||
mkdir -p "$SOURCE_DIR"
|
||||
tarball="${PROJECT_ROOT}/.tmp/opus-${OPUS_VERSION}.tar.gz"
|
||||
if [[ -f "$LOCAL_OPUS_TARBALL" ]]; then
|
||||
cp "$LOCAL_OPUS_TARBALL" "$tarball"
|
||||
else
|
||||
command -v curl >/dev/null 2>&1 || fail "curl is required to download opus source"
|
||||
curl -fsSL "$OPUS_TARBALL_URL" -o "$tarball"
|
||||
fi
|
||||
echo "${OPUS_TARBALL_SHA256} ${tarball}" | sha256sum -c -
|
||||
tar -xzf "$tarball" -C "$SOURCE_DIR" --strip-components=1
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -d "$SOURCE_DIR" ]] || fail "opus source not found: $SOURCE_DIR"
|
||||
[[ -x "$SOURCE_DIR/configure" || -f "$SOURCE_DIR/configure.ac" ]] || fail "opus source layout not recognized under: $SOURCE_DIR"
|
||||
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"
|
||||
@@ -105,10 +82,6 @@ OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
||||
|
||||
HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64"
|
||||
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||
ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake"
|
||||
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||
[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE"
|
||||
command -v cmake >/dev/null 2>&1 || fail "cmake is required"
|
||||
|
||||
normalize_abis() {
|
||||
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||
@@ -161,14 +134,6 @@ build_one() {
|
||||
make install
|
||||
)
|
||||
|
||||
mkdir -p "$prefix/lib" "$prefix/include"
|
||||
if [[ -f "$prefix/include/opus/opus.h" ]]; then
|
||||
:
|
||||
elif [[ -f "$SOURCE_DIR/include/opus/opus.h" ]]; then
|
||||
mkdir -p "$prefix/include/opus"
|
||||
cp "$SOURCE_DIR/include/opus/opus.h" "$prefix/include/opus/opus.h"
|
||||
fi
|
||||
|
||||
echo "Built Opus for ${abi}: ${prefix}"
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,12 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
SOURCE_DIR=""
|
||||
OUTPUT_DIR="${PROJECT_ROOT}/dist/android-turbojpeg"
|
||||
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)}"
|
||||
LIBJPEG_TURBO_REPO="${LIBJPEG_TURBO_REPO:-https://github.com/libjpeg-turbo/libjpeg-turbo.git}"
|
||||
LIBJPEG_TURBO_VERSION="${LIBJPEG_TURBO_VERSION:-3.1.4.1}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -19,8 +18,6 @@ Usage:
|
||||
scripts/build-android-turbojpeg.sh [options]
|
||||
|
||||
Options:
|
||||
--source <dir> Existing libjpeg-turbo source checkout. If omitted,
|
||||
the script clones it into .tmp/android-turbojpeg-src.
|
||||
--output <dir> Output root. Default: dist/android-turbojpeg
|
||||
--ndk <dir> Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT.
|
||||
--api <level> Android API level. Default: 21.
|
||||
@@ -46,10 +43,6 @@ fail() {
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source)
|
||||
SOURCE_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_DIR="${2:-}"
|
||||
shift 2
|
||||
@@ -79,27 +72,29 @@ 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"
|
||||
|
||||
if [[ -z "$SOURCE_DIR" ]]; then
|
||||
SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-turbojpeg-src"
|
||||
if [[ ! -d "$SOURCE_DIR/.git" ]]; then
|
||||
rm -rf "$SOURCE_DIR"
|
||||
git clone --depth 1 "$LIBJPEG_TURBO_REPO" "$SOURCE_DIR"
|
||||
fi
|
||||
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
|
||||
|
||||
[[ -d "$SOURCE_DIR" ]] || fail "libjpeg-turbo source not found: $SOURCE_DIR"
|
||||
[[ -f "$SOURCE_DIR/CMakeLists.txt" ]] || fail "libjpeg-turbo CMakeLists.txt not found under: $SOURCE_DIR"
|
||||
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"
|
||||
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||
ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake"
|
||||
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||
[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE"
|
||||
command -v cmake >/dev/null 2>&1 || fail "cmake is required"
|
||||
|
||||
normalize_abis() {
|
||||
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||
|
||||
@@ -34,6 +34,38 @@ impl Default for OtgDescriptorConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Ch9329DescriptorConfig {
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
pub manufacturer: String,
|
||||
pub product: String,
|
||||
pub serial_number: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Ch9329DescriptorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
vendor_id: 0x1a86,
|
||||
product_id: 0xe129,
|
||||
manufacturer: "WCH.CN".to_string(),
|
||||
product: "CH9329".to_string(),
|
||||
serial_number: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Ch9329DescriptorState {
|
||||
pub descriptor: Ch9329DescriptorConfig,
|
||||
pub manufacturer_enabled: bool,
|
||||
pub product_enabled: bool,
|
||||
pub serial_enabled: bool,
|
||||
pub config_mode_available: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -191,6 +223,10 @@ pub struct HidConfig {
|
||||
pub otg_keyboard_leds: bool,
|
||||
pub ch9329_port: String,
|
||||
pub ch9329_baudrate: u32,
|
||||
#[serde(default)]
|
||||
pub ch9329_hybrid_mouse: bool,
|
||||
#[serde(default)]
|
||||
pub ch9329_descriptor: Ch9329DescriptorConfig,
|
||||
pub mouse_absolute: bool,
|
||||
}
|
||||
|
||||
@@ -206,6 +242,8 @@ impl Default for HidConfig {
|
||||
otg_keyboard_leds: false,
|
||||
ch9329_port: "/dev/ttyUSB0".to_string(),
|
||||
ch9329_baudrate: 9600,
|
||||
ch9329_hybrid_mouse: false,
|
||||
ch9329_descriptor: Ch9329DescriptorConfig::default(),
|
||||
mouse_absolute: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,14 @@ pub struct AppConfig {
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn enforce_invariants(&mut self) {
|
||||
if self.hid.backend != HidBackend::Otg {
|
||||
self.msd.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_platform_defaults(&mut self) {
|
||||
crate::platform::defaults::apply(self);
|
||||
self.enforce_invariants();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ impl ConfigStore {
|
||||
}
|
||||
|
||||
pub async fn load(&self) -> Result<()> {
|
||||
let config = Self::load_config(&self.pool).await?;
|
||||
let mut config = Self::load_config(&self.pool).await?;
|
||||
config.enforce_invariants();
|
||||
self.cache.store(Arc::new(config));
|
||||
Ok(())
|
||||
}
|
||||
@@ -73,6 +74,8 @@ impl ConfigStore {
|
||||
|
||||
pub async fn set(&self, config: AppConfig) -> Result<()> {
|
||||
let _guard = self.write_lock.lock().await;
|
||||
let mut config = config;
|
||||
config.enforce_invariants();
|
||||
Self::save_config_to_db(&self.pool, &config).await?;
|
||||
self.cache.store(Arc::new(config));
|
||||
|
||||
@@ -91,6 +94,7 @@ impl ConfigStore {
|
||||
let current = self.cache.load();
|
||||
let mut config = (**current).clone();
|
||||
f(&mut config);
|
||||
config.enforce_invariants();
|
||||
|
||||
Self::save_config_to_db(&self.pool, &config).await?;
|
||||
|
||||
|
||||
@@ -363,7 +363,7 @@ mod tests {
|
||||
fn parse_cpu_model_from_model_name_field() {
|
||||
let input = "processor\t: 0\nmodel name\t: Intel(R) Xeon(R)\n";
|
||||
assert_eq!(
|
||||
parse_cpu_model_from_cpuinfo_content(input),
|
||||
parse_cpu_model_from_cpuinfo_content(Some(input)),
|
||||
Some("Intel(R) Xeon(R)".to_string())
|
||||
);
|
||||
}
|
||||
@@ -372,7 +372,7 @@ mod tests {
|
||||
fn parse_cpu_model_from_model_field() {
|
||||
let input = "processor\t: 0\nModel\t\t: Raspberry Pi 4 Model B Rev 1.4\n";
|
||||
assert_eq!(
|
||||
parse_cpu_model_from_cpuinfo_content(input),
|
||||
parse_cpu_model_from_cpuinfo_content(Some(input)),
|
||||
Some("Raspberry Pi 4 Model B Rev 1.4".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::RwLock;
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
use super::types::*;
|
||||
use crate::events::EventBus;
|
||||
|
||||
const LOG_BUFFER_SIZE: usize = 200;
|
||||
const LOG_BATCH_SIZE: usize = 16;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub const TTYD_SOCKET_PATH: &str = "/var/run/one-kvm/ttyd.sock";
|
||||
@@ -25,6 +27,12 @@ const TTYD_TCP_PORT: &str = "7681";
|
||||
struct ExtensionProcess {
|
||||
child: Child,
|
||||
logs: Arc<RwLock<VecDeque<String>>>,
|
||||
_temp_dir: Option<TempDir>,
|
||||
}
|
||||
|
||||
struct ExtensionLaunch {
|
||||
args: Vec<String>,
|
||||
temp_dir: Option<TempDir>,
|
||||
}
|
||||
|
||||
pub struct ExtensionManager {
|
||||
@@ -82,6 +90,17 @@ impl ExtensionManager {
|
||||
ExtensionId::Easytier => {
|
||||
config.easytier.enabled && !config.easytier.network_name.is_empty()
|
||||
}
|
||||
ExtensionId::Frpc => {
|
||||
config.frpc.enabled
|
||||
&& match config.frpc.config_mode {
|
||||
FrpcConfigMode::Quick => {
|
||||
!config.frpc.proxy_name.trim().is_empty()
|
||||
&& !config.frpc.server_addr.trim().is_empty()
|
||||
&& !config.frpc.token.is_empty()
|
||||
}
|
||||
FrpcConfigMode::Full => !config.frpc.custom_toml.trim().is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,17 +154,17 @@ impl ExtensionManager {
|
||||
|
||||
self.stop(id).await.ok();
|
||||
|
||||
let args = self.build_args(id, config).await?;
|
||||
let launch = self.build_launch(id, config).await?;
|
||||
|
||||
tracing::info!(
|
||||
"Starting extension {}: {} {}",
|
||||
id,
|
||||
id.binary_path().display(),
|
||||
Self::redact_args_for_log(&args).join(" ")
|
||||
launch.args.join(" ")
|
||||
);
|
||||
|
||||
let mut child = Command::new(id.binary_path())
|
||||
.args(&args)
|
||||
.args(&launch.args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
@@ -172,9 +191,21 @@ impl ExtensionManager {
|
||||
|
||||
let pid = child.id();
|
||||
tracing::info!("Extension {} started with PID {:?}", id, pid);
|
||||
Self::push_log(
|
||||
&logs,
|
||||
format!("Extension {} started with PID {:?}", id, pid),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut processes = self.processes.write().await;
|
||||
processes.insert(id, ExtensionProcess { child, logs });
|
||||
processes.insert(
|
||||
id,
|
||||
ExtensionProcess {
|
||||
child,
|
||||
logs,
|
||||
_temp_dir: launch.temp_dir,
|
||||
},
|
||||
);
|
||||
drop(processes);
|
||||
self.mark_ttyd_status_dirty(id).await;
|
||||
|
||||
@@ -212,22 +243,14 @@ impl ExtensionManager {
|
||||
) {
|
||||
let reader = BufReader::new(reader);
|
||||
let mut lines = reader.lines();
|
||||
let mut local_buffer = Vec::with_capacity(LOG_BATCH_SIZE);
|
||||
|
||||
loop {
|
||||
match lines.next_line().await {
|
||||
Ok(Some(line)) => {
|
||||
tracing::info!("[{}] {}", id, line);
|
||||
local_buffer.push(line);
|
||||
|
||||
if local_buffer.len() >= LOG_BATCH_SIZE {
|
||||
Self::flush_logs(&logs, &mut local_buffer).await;
|
||||
}
|
||||
Self::push_log(&logs, line).await;
|
||||
}
|
||||
Ok(None) => {
|
||||
if !local_buffer.is_empty() {
|
||||
Self::flush_logs(&logs, &mut local_buffer).await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -238,29 +261,27 @@ impl ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fn flush_logs(logs: &RwLock<VecDeque<String>>, buffer: &mut Vec<String>) {
|
||||
async fn push_log(logs: &RwLock<VecDeque<String>>, line: String) {
|
||||
let mut logs = logs.write().await;
|
||||
for line in buffer.drain(..) {
|
||||
if logs.len() >= LOG_BUFFER_SIZE {
|
||||
logs.pop_front();
|
||||
}
|
||||
logs.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_args(
|
||||
async fn build_launch(
|
||||
&self,
|
||||
id: ExtensionId,
|
||||
config: &ExtensionsConfig,
|
||||
) -> Result<Vec<String>, String> {
|
||||
match id {
|
||||
) -> Result<ExtensionLaunch, String> {
|
||||
let args = match id {
|
||||
ExtensionId::Ttyd => {
|
||||
let c = &config.ttyd;
|
||||
|
||||
let mut args = Self::build_ttyd_listen_args().await?;
|
||||
|
||||
args.push(c.shell.clone());
|
||||
Ok(args)
|
||||
args
|
||||
}
|
||||
|
||||
ExtensionId::Gostc => {
|
||||
@@ -282,7 +303,7 @@ impl ExtensionManager {
|
||||
|
||||
args.extend(["-key".to_string(), c.key.clone()]);
|
||||
|
||||
Ok(args)
|
||||
args
|
||||
}
|
||||
|
||||
ExtensionId::Easytier => {
|
||||
@@ -314,9 +335,153 @@ impl ExtensionManager {
|
||||
args.push("-d".to_string());
|
||||
}
|
||||
|
||||
Ok(args)
|
||||
args
|
||||
}
|
||||
|
||||
ExtensionId::Frpc => {
|
||||
return Self::build_frpc_launch(&config.frpc).await;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ExtensionLaunch {
|
||||
args,
|
||||
temp_dir: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_frpc_launch(config: &FrpcConfig) -> Result<ExtensionLaunch, String> {
|
||||
let config_text = match config.config_mode {
|
||||
FrpcConfigMode::Quick => Self::build_frpc_quick_toml(config)?,
|
||||
FrpcConfigMode::Full => Self::validate_frpc_full_toml(config)?.to_string(),
|
||||
};
|
||||
|
||||
let temp_dir =
|
||||
tempfile::tempdir().map_err(|e| format!("Failed to create FRPC config dir: {}", e))?;
|
||||
let config_path = temp_dir.path().join("frpc.toml");
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(temp_dir.path(), std::fs::Permissions::from_mode(0o700))
|
||||
.map_err(|e| format!("Failed to protect FRPC config dir: {}", e))?;
|
||||
}
|
||||
|
||||
tokio::fs::write(&config_path, config_text)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to write FRPC config: {}", e))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
tokio::fs::set_permissions(&config_path, std::fs::Permissions::from_mode(0o600))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to protect FRPC config: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(ExtensionLaunch {
|
||||
args: vec!["-c".to_string(), Self::path_to_arg(&config_path)],
|
||||
temp_dir: Some(temp_dir),
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_frpc_full_toml(config: &FrpcConfig) -> Result<&str, String> {
|
||||
let trimmed = config.custom_toml.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("FRPC full configuration is required".into());
|
||||
}
|
||||
|
||||
trimmed
|
||||
.parse::<DocumentMut>()
|
||||
.map_err(|e| format!("FRPC full configuration is not valid TOML: {}", e))?;
|
||||
|
||||
Ok(config.custom_toml.as_str())
|
||||
}
|
||||
|
||||
fn build_frpc_quick_toml(config: &FrpcConfig) -> Result<String, String> {
|
||||
if config.proxy_name.trim().is_empty() {
|
||||
return Err("FRPC proxy name is required".into());
|
||||
}
|
||||
if config.server_addr.trim().is_empty() {
|
||||
return Err("FRPC server address is required".into());
|
||||
}
|
||||
if config.token.is_empty() {
|
||||
return Err("FRPC token is required".into());
|
||||
}
|
||||
if config.local_ip.trim().is_empty() {
|
||||
return Err("FRPC local IP is required".into());
|
||||
}
|
||||
|
||||
let proxy_type = match config.proxy_type {
|
||||
FrpProxyType::Tcp => "tcp",
|
||||
FrpProxyType::Udp => "udp",
|
||||
FrpProxyType::Http => "http",
|
||||
FrpProxyType::Https => "https",
|
||||
FrpProxyType::Stcp => "stcp",
|
||||
FrpProxyType::Sudp => "sudp",
|
||||
FrpProxyType::Xtcp => "xtcp",
|
||||
};
|
||||
|
||||
let mut toml = String::new();
|
||||
toml.push_str(&format!(
|
||||
"serverAddr = {}\nserverPort = {}\n\n",
|
||||
Self::toml_string(config.server_addr.trim()),
|
||||
config.server_port
|
||||
));
|
||||
toml.push_str("[auth]\n");
|
||||
toml.push_str("method = \"token\"\n");
|
||||
toml.push_str(&format!("token = {}\n\n", Self::toml_string(&config.token)));
|
||||
toml.push_str("[transport]\n");
|
||||
toml.push_str("protocol = \"tcp\"\n\n");
|
||||
toml.push_str("[transport.tls]\n");
|
||||
toml.push_str(&format!("enable = {}\n\n", config.tls));
|
||||
toml.push_str("[[proxies]]\n");
|
||||
toml.push_str(&format!(
|
||||
"name = {}\ntype = {}\nlocalIP = {}\nlocalPort = {}\n",
|
||||
Self::toml_string(config.proxy_name.trim()),
|
||||
Self::toml_string(proxy_type),
|
||||
Self::toml_string(config.local_ip.trim()),
|
||||
config.local_port
|
||||
));
|
||||
|
||||
match config.proxy_type {
|
||||
FrpProxyType::Tcp | FrpProxyType::Udp => {
|
||||
let remote_port = config.remote_port.ok_or_else(|| {
|
||||
"FRPC remote port is required for TCP/UDP proxies".to_string()
|
||||
})?;
|
||||
toml.push_str(&format!("remotePort = {}\n", remote_port));
|
||||
}
|
||||
FrpProxyType::Http | FrpProxyType::Https => {
|
||||
if let Some(domain) = config
|
||||
.custom_domain
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
toml.push_str(&format!(
|
||||
"customDomains = [{}]\n",
|
||||
Self::toml_string(domain)
|
||||
));
|
||||
}
|
||||
}
|
||||
FrpProxyType::Stcp | FrpProxyType::Sudp | FrpProxyType::Xtcp => {
|
||||
if !config.secret_key.is_empty() {
|
||||
toml.push_str(&format!(
|
||||
"secretKey = {}\n",
|
||||
Self::toml_string(&config.secret_key)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(toml)
|
||||
}
|
||||
|
||||
fn toml_string(value: &str) -> String {
|
||||
serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
|
||||
}
|
||||
|
||||
fn path_to_arg(path: &PathBuf) -> String {
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -356,34 +521,6 @@ impl ExtensionManager {
|
||||
])
|
||||
}
|
||||
|
||||
fn redact_args_for_log(args: &[String]) -> Vec<String> {
|
||||
let mut redacted = Vec::with_capacity(args.len());
|
||||
let mut redact_next = false;
|
||||
|
||||
for arg in args {
|
||||
if redact_next {
|
||||
redacted.push("****".to_string());
|
||||
redact_next = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if arg == "-key" || arg == "--key" {
|
||||
redacted.push(arg.clone());
|
||||
redact_next = true;
|
||||
} else if let Some((flag, _)) = arg.split_once('=') {
|
||||
if flag == "-key" || flag == "--key" {
|
||||
redacted.push(format!("{}=****", flag));
|
||||
} else {
|
||||
redacted.push(arg.clone());
|
||||
}
|
||||
} else {
|
||||
redacted.push(arg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
redacted
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn prepare_ttyd_socket() -> Result<(), String> {
|
||||
let socket_path = std::path::Path::new(TTYD_SOCKET_PATH);
|
||||
|
||||
@@ -7,6 +7,7 @@ pub fn default_binary_path(id: ExtensionId) -> &'static str {
|
||||
ExtensionId::Ttyd => "/usr/bin/ttyd",
|
||||
ExtensionId::Gostc => "/usr/bin/gostc",
|
||||
ExtensionId::Easytier => "/usr/bin/easytier-core",
|
||||
ExtensionId::Frpc => "/usr/bin/frpc",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pub fn default_binary_path(id: ExtensionId) -> &'static str {
|
||||
ExtensionId::Ttyd => "ttyd.win32.exe",
|
||||
ExtensionId::Gostc => "gostc.exe",
|
||||
ExtensionId::Easytier => "easytier-core.exe",
|
||||
ExtensionId::Frpc => "frpc.exe",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ pub enum ExtensionId {
|
||||
Ttyd,
|
||||
Gostc,
|
||||
Easytier,
|
||||
Frpc,
|
||||
}
|
||||
|
||||
impl ExtensionId {
|
||||
@@ -18,7 +19,7 @@ impl ExtensionId {
|
||||
}
|
||||
|
||||
pub fn all() -> &'static [ExtensionId] {
|
||||
&[Self::Ttyd, Self::Gostc, Self::Easytier]
|
||||
&[Self::Ttyd, Self::Gostc, Self::Easytier, Self::Frpc]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ impl std::fmt::Display for ExtensionId {
|
||||
Self::Ttyd => write!(f, "ttyd"),
|
||||
Self::Gostc => write!(f, "gostc"),
|
||||
Self::Easytier => write!(f, "easytier"),
|
||||
Self::Frpc => write!(f, "frpc"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +42,7 @@ impl std::str::FromStr for ExtensionId {
|
||||
"ttyd" => Ok(Self::Ttyd),
|
||||
"gostc" => Ok(Self::Gostc),
|
||||
"easytier" => Ok(Self::Easytier),
|
||||
"frpc" => Ok(Self::Frpc),
|
||||
_ => Err(format!("Unknown extension: {}", s)),
|
||||
}
|
||||
}
|
||||
@@ -114,6 +117,85 @@ pub struct EasytierConfig {
|
||||
pub virtual_ip: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FrpProxyType {
|
||||
Tcp,
|
||||
Udp,
|
||||
Http,
|
||||
Https,
|
||||
Stcp,
|
||||
Sudp,
|
||||
Xtcp,
|
||||
}
|
||||
|
||||
impl Default for FrpProxyType {
|
||||
fn default() -> Self {
|
||||
Self::Tcp
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FrpcConfigMode {
|
||||
Quick,
|
||||
Full,
|
||||
}
|
||||
|
||||
impl Default for FrpcConfigMode {
|
||||
fn default() -> Self {
|
||||
Self::Quick
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct FrpcConfig {
|
||||
pub enabled: bool,
|
||||
pub config_mode: FrpcConfigMode,
|
||||
pub proxy_name: String,
|
||||
pub proxy_type: FrpProxyType,
|
||||
pub server_addr: String,
|
||||
pub server_port: u16,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub token: String,
|
||||
pub local_ip: String,
|
||||
pub local_port: u16,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub remote_port: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_domain: Option<String>,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub secret_key: String,
|
||||
pub tls: bool,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub custom_toml: String,
|
||||
}
|
||||
|
||||
impl Default for FrpcConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_mode: FrpcConfigMode::Quick,
|
||||
proxy_name: String::new(),
|
||||
proxy_type: FrpProxyType::Tcp,
|
||||
server_addr: String::new(),
|
||||
server_port: 7000,
|
||||
token: String::new(),
|
||||
local_ip: "127.0.0.1".to_string(),
|
||||
local_port: 22,
|
||||
remote_port: None,
|
||||
custom_domain: None,
|
||||
secret_key: String::new(),
|
||||
tls: true,
|
||||
custom_toml: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
@@ -121,6 +203,7 @@ pub struct ExtensionsConfig {
|
||||
pub ttyd: TtydConfig,
|
||||
pub gostc: GostcConfig,
|
||||
pub easytier: EasytierConfig,
|
||||
pub frpc: FrpcConfig,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -154,12 +237,21 @@ pub struct EasytierInfo {
|
||||
pub config: EasytierConfig,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FrpcInfo {
|
||||
pub available: bool,
|
||||
pub status: ExtensionStatus,
|
||||
pub config: FrpcConfig,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExtensionsStatus {
|
||||
pub ttyd: TtydInfo,
|
||||
pub gostc: GostcInfo,
|
||||
pub easytier: EasytierInfo,
|
||||
pub frpc: FrpcInfo,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use super::types::{ConsumerEvent, KeyboardEvent, MouseEvent};
|
||||
use crate::config::{Ch9329DescriptorConfig, Ch9329DescriptorState};
|
||||
use crate::error::Result;
|
||||
use crate::events::LedState;
|
||||
|
||||
@@ -21,6 +22,8 @@ pub enum HidBackendType {
|
||||
port: String,
|
||||
#[serde(default = "default_ch9329_baud_rate")]
|
||||
baud_rate: u32,
|
||||
#[serde(default)]
|
||||
hybrid_mouse: bool,
|
||||
},
|
||||
#[default]
|
||||
None,
|
||||
@@ -63,6 +66,21 @@ pub trait HidBackend: Send + Sync {
|
||||
))
|
||||
}
|
||||
|
||||
async fn apply_ch9329_descriptor(
|
||||
&self,
|
||||
_descriptor: &Ch9329DescriptorConfig,
|
||||
) -> Result<Ch9329DescriptorState> {
|
||||
Err(crate::error::AppError::BadRequest(
|
||||
"CH9329 descriptor configuration is not supported by this backend".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn read_ch9329_descriptor(&self) -> Result<Ch9329DescriptorState> {
|
||||
Err(crate::error::AppError::BadRequest(
|
||||
"CH9329 descriptor reading is not supported by this backend".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn reset(&self) -> Result<()>;
|
||||
|
||||
async fn shutdown(&self) -> Result<()>;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,10 @@ pub mod cmd {
|
||||
pub const SEND_KB_MEDIA_DATA: u8 = 0x03;
|
||||
pub const SEND_MS_ABS_DATA: u8 = 0x04;
|
||||
pub const SEND_MS_REL_DATA: u8 = 0x05;
|
||||
pub const GET_PARA_CFG: u8 = 0x08;
|
||||
pub const SET_PARA_CFG: u8 = 0x09;
|
||||
pub const GET_USB_STRING: u8 = 0x0A;
|
||||
pub const SET_USB_STRING: u8 = 0x0B;
|
||||
pub const RESET: u8 = 0x0F;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,13 +39,19 @@ impl HidBackendFactory {
|
||||
async fn create(&self, backend_type: &HidBackendType) -> Result<Option<Arc<dyn HidBackend>>> {
|
||||
match backend_type {
|
||||
HidBackendType::Otg => self.create_otg_backend().await.map(Some),
|
||||
HidBackendType::Ch9329 { port, baud_rate } => {
|
||||
HidBackendType::Ch9329 {
|
||||
port,
|
||||
baud_rate,
|
||||
hybrid_mouse,
|
||||
} => {
|
||||
info!(
|
||||
"Initializing CH9329 HID backend on {} @ {} baud",
|
||||
port, baud_rate
|
||||
"Initializing CH9329 HID backend on {} @ {} baud, hybrid_mouse={}",
|
||||
port, baud_rate, hybrid_mouse
|
||||
);
|
||||
Ok(Some(Arc::new(ch9329::Ch9329Backend::with_baud_rate(
|
||||
port, *baud_rate,
|
||||
Ok(Some(Arc::new(ch9329::Ch9329Backend::with_options(
|
||||
port,
|
||||
*baud_rate,
|
||||
*hybrid_mouse,
|
||||
)?)))
|
||||
}
|
||||
HidBackendType::None => {
|
||||
|
||||
@@ -98,6 +98,7 @@ use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::config::{Ch9329DescriptorConfig, Ch9329DescriptorState};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::EventBus;
|
||||
#[cfg(unix)]
|
||||
@@ -287,6 +288,36 @@ impl HidController {
|
||||
self.runtime_state.read().await.clone()
|
||||
}
|
||||
|
||||
async fn ch9329_backend(&self) -> Result<Arc<dyn HidBackend>> {
|
||||
if !matches!(
|
||||
*self.backend_type.read().await,
|
||||
HidBackendType::Ch9329 { .. }
|
||||
) {
|
||||
return Err(AppError::BadRequest(
|
||||
"Current HID backend is not CH9329".to_string(),
|
||||
));
|
||||
}
|
||||
self.backend
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.ok_or_else(|| AppError::BadRequest("CH9329 backend not available".to_string()))
|
||||
}
|
||||
|
||||
pub async fn apply_ch9329_descriptor(
|
||||
&self,
|
||||
descriptor: &Ch9329DescriptorConfig,
|
||||
) -> Result<Ch9329DescriptorState> {
|
||||
self.ch9329_backend()
|
||||
.await?
|
||||
.apply_ch9329_descriptor(descriptor)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn read_ch9329_descriptor(&self) -> Result<Ch9329DescriptorState> {
|
||||
self.ch9329_backend().await?.read_ch9329_descriptor().await
|
||||
}
|
||||
|
||||
pub async fn reload(&self, new_backend_type: HidBackendType) -> Result<()> {
|
||||
info!("Reloading HID backend: {:?}", new_backend_type);
|
||||
self.backend_available.store(false, Ordering::Release);
|
||||
|
||||
@@ -305,6 +305,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -300,7 +300,10 @@ impl OtgGadgetManager {
|
||||
|
||||
let strings_path = self.config_path.join("strings/0x409");
|
||||
create_dir(&strings_path)?;
|
||||
write_file(&strings_path.join("configuration"), "Config 1: HID + MSD")?;
|
||||
write_file(
|
||||
&strings_path.join("configuration"),
|
||||
self.configuration_label(),
|
||||
)?;
|
||||
|
||||
write_file(&self.config_path.join("MaxPower"), "500")?;
|
||||
|
||||
@@ -308,6 +311,18 @@ impl OtgGadgetManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configuration_label(&self) -> &'static str {
|
||||
if self
|
||||
.functions
|
||||
.iter()
|
||||
.any(|func| func.name().starts_with("mass_storage."))
|
||||
{
|
||||
"Config 1: HID + MSD"
|
||||
} else {
|
||||
"Config 1: HID"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gadget_path(&self) -> &PathBuf {
|
||||
&self.gadget_path
|
||||
}
|
||||
|
||||
@@ -315,6 +315,7 @@ async fn build_app_state(
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ fn hid_backend_type(config: &HidConfig) -> crate::hid::HidBackendType {
|
||||
HidBackend::Ch9329 => crate::hid::HidBackendType::Ch9329 {
|
||||
port: config.ch9329_port.clone(),
|
||||
baud_rate: config.ch9329_baudrate,
|
||||
hybrid_mouse: config.ch9329_hybrid_mouse,
|
||||
},
|
||||
HidBackend::None => crate::hid::HidBackendType::None,
|
||||
}
|
||||
@@ -167,7 +168,8 @@ pub async fn apply_hid_config(
|
||||
new_config: &HidConfig,
|
||||
options: ConfigApplyOptions,
|
||||
) -> Result<()> {
|
||||
let current_msd_enabled = state.config.get().msd.enabled;
|
||||
let current_config = state.config.get();
|
||||
let current_msd_enabled = current_config.msd.enabled && new_config.backend == HidBackend::Otg;
|
||||
new_config.validate_otg_endpoint_budget(current_msd_enabled)?;
|
||||
|
||||
let descriptor_changed = old_config.otg_descriptor != new_config.otg_descriptor;
|
||||
@@ -178,10 +180,12 @@ pub async fn apply_hid_config(
|
||||
old_config.effective_otg_keyboard_leds() != new_config.effective_otg_keyboard_leds();
|
||||
let endpoint_budget_changed =
|
||||
old_config.resolved_otg_endpoint_limit() != new_config.resolved_otg_endpoint_limit();
|
||||
let ch9329_runtime_changed = old_config.ch9329_hybrid_mouse != new_config.ch9329_hybrid_mouse;
|
||||
|
||||
if old_config.backend == new_config.backend
|
||||
&& old_config.ch9329_port == new_config.ch9329_port
|
||||
&& old_config.ch9329_baudrate == new_config.ch9329_baudrate
|
||||
&& !ch9329_runtime_changed
|
||||
&& old_config.otg_udc == new_config.otg_udc
|
||||
&& !descriptor_changed
|
||||
&& !hid_functions_changed
|
||||
@@ -235,18 +239,19 @@ pub async fn apply_msd_config(
|
||||
new_config: &MsdConfig,
|
||||
options: ConfigApplyOptions,
|
||||
) -> Result<()> {
|
||||
state
|
||||
.config
|
||||
.get()
|
||||
let current_config = state.config.get();
|
||||
let hid_backend_is_otg = current_config.hid.backend == HidBackend::Otg;
|
||||
let effective_new_msd_enabled = new_config.enabled && hid_backend_is_otg;
|
||||
current_config
|
||||
.hid
|
||||
.validate_otg_endpoint_budget(new_config.enabled)?;
|
||||
.validate_otg_endpoint_budget(effective_new_msd_enabled)?;
|
||||
|
||||
tracing::info!("MSD config sent, checking if reload needed...");
|
||||
tracing::debug!("Old MSD config: {:?}", old_config);
|
||||
tracing::debug!("New MSD config: {:?}", new_config);
|
||||
|
||||
let old_msd_enabled = old_config.enabled;
|
||||
let new_msd_enabled = new_config.enabled;
|
||||
let new_msd_enabled = effective_new_msd_enabled;
|
||||
let msd_dir_changed = old_config.msd_dir != new_config.msd_dir;
|
||||
|
||||
tracing::info!(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::HidConfig;
|
||||
use crate::config::{HidBackend, HidConfig};
|
||||
use crate::error::Result;
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -21,10 +21,21 @@ pub async fn update_hid_config(
|
||||
let _apply_guard = try_apply_lock(&state.config_apply_locks.otg, "otg")?;
|
||||
let old_hid_config = state.config.get().hid.clone();
|
||||
|
||||
let mut staged_hid_config = old_hid_config.clone();
|
||||
req.apply_to(&mut staged_hid_config);
|
||||
let descriptor_update = req
|
||||
.ch9329_descriptor
|
||||
.as_ref()
|
||||
.map(|_| staged_hid_config.ch9329_descriptor.clone());
|
||||
if descriptor_update.is_some() {
|
||||
staged_hid_config.ch9329_descriptor = old_hid_config.ch9329_descriptor.clone();
|
||||
}
|
||||
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.hid);
|
||||
config.hid = staged_hid_config.clone();
|
||||
config.enforce_invariants();
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -38,5 +49,21 @@ pub async fn update_hid_config(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(descriptor) = descriptor_update {
|
||||
if new_hid_config.backend != HidBackend::Ch9329 {
|
||||
return Ok(Json(new_hid_config));
|
||||
}
|
||||
|
||||
let actual = state.hid.apply_ch9329_descriptor(&descriptor).await?;
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.hid.ch9329_descriptor = actual.descriptor.clone();
|
||||
config.enforce_invariants();
|
||||
})
|
||||
.await?;
|
||||
return Ok(Json(state.config.get().hid.clone()));
|
||||
}
|
||||
|
||||
Ok(Json(new_hid_config))
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ pub async fn update_msd_config(
|
||||
.config
|
||||
.update(|config| {
|
||||
req.apply_to(&mut config.msd);
|
||||
config.enforce_invariants();
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -292,12 +292,63 @@ impl OtgHidFunctionsUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Ch9329DescriptorConfigUpdate {
|
||||
pub vendor_id: Option<u16>,
|
||||
pub product_id: Option<u16>,
|
||||
pub manufacturer: Option<String>,
|
||||
pub product: Option<String>,
|
||||
pub serial_number: Option<String>,
|
||||
}
|
||||
|
||||
impl Ch9329DescriptorConfigUpdate {
|
||||
pub fn validate(&self) -> crate::error::Result<()> {
|
||||
Self::validate_optional_string("Manufacturer", self.manufacturer.as_deref())?;
|
||||
Self::validate_optional_string("Product", self.product.as_deref())?;
|
||||
Self::validate_optional_string("Serial number", self.serial_number.as_deref())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_optional_string(label: &str, value: Option<&str>) -> crate::error::Result<()> {
|
||||
if let Some(value) = value {
|
||||
if value.as_bytes().len() > 23 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"{} string too long (max 23 bytes for CH9329)",
|
||||
label
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, config: &mut Ch9329DescriptorConfig) {
|
||||
if let Some(v) = self.vendor_id {
|
||||
config.vendor_id = v;
|
||||
}
|
||||
if let Some(v) = self.product_id {
|
||||
config.product_id = v;
|
||||
}
|
||||
if let Some(ref v) = self.manufacturer {
|
||||
config.manufacturer = v.clone();
|
||||
}
|
||||
if let Some(ref v) = self.product {
|
||||
config.product = v.clone();
|
||||
}
|
||||
if let Some(ref v) = self.serial_number {
|
||||
config.serial_number = if v.is_empty() { None } else { Some(v.clone()) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct HidConfigUpdate {
|
||||
pub backend: Option<HidBackend>,
|
||||
pub ch9329_port: Option<String>,
|
||||
pub ch9329_baudrate: Option<u32>,
|
||||
pub ch9329_hybrid_mouse: Option<bool>,
|
||||
pub ch9329_descriptor: Option<Ch9329DescriptorConfigUpdate>,
|
||||
pub otg_udc: Option<String>,
|
||||
pub otg_descriptor: Option<OtgDescriptorConfigUpdate>,
|
||||
pub otg_profile: Option<OtgHidProfile>,
|
||||
@@ -320,6 +371,9 @@ impl HidConfigUpdate {
|
||||
if let Some(ref desc) = self.otg_descriptor {
|
||||
desc.validate()?;
|
||||
}
|
||||
if let Some(ref desc) = self.ch9329_descriptor {
|
||||
desc.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -333,6 +387,12 @@ impl HidConfigUpdate {
|
||||
if let Some(baudrate) = self.ch9329_baudrate {
|
||||
config.ch9329_baudrate = baudrate;
|
||||
}
|
||||
if let Some(enabled) = self.ch9329_hybrid_mouse {
|
||||
config.ch9329_hybrid_mouse = enabled;
|
||||
}
|
||||
if let Some(ref desc) = self.ch9329_descriptor {
|
||||
desc.apply_to(&mut config.ch9329_descriptor);
|
||||
}
|
||||
if let Some(ref udc) = self.otg_udc {
|
||||
config.otg_udc = Some(udc.clone());
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ use axum::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use toml_edit::DocumentMut;
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::extensions::{
|
||||
EasytierConfig, EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus,
|
||||
GostcConfig, GostcInfo, TtydConfig, TtydInfo,
|
||||
FrpProxyType, FrpcConfig, FrpcConfigMode, FrpcInfo, GostcConfig, GostcInfo, TtydConfig,
|
||||
TtydInfo,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -34,6 +36,46 @@ fn validate_easytier_enabled(config: &EasytierConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_frpc_enabled(config: &FrpcConfig) -> Result<()> {
|
||||
match config.config_mode {
|
||||
FrpcConfigMode::Quick => {
|
||||
if config.proxy_name.trim().is_empty() {
|
||||
return Err(AppError::BadRequest("FRPC proxy name is required".into()));
|
||||
}
|
||||
if config.server_addr.trim().is_empty() {
|
||||
return Err(AppError::BadRequest(
|
||||
"FRPC server address is required".into(),
|
||||
));
|
||||
}
|
||||
if config.token.is_empty() {
|
||||
return Err(AppError::BadRequest("FRPC token is required".into()));
|
||||
}
|
||||
if config.local_ip.trim().is_empty() {
|
||||
return Err(AppError::BadRequest("FRPC local IP is required".into()));
|
||||
}
|
||||
if matches!(config.proxy_type, FrpProxyType::Tcp | FrpProxyType::Udp)
|
||||
&& config.remote_port.is_none()
|
||||
{
|
||||
return Err(AppError::BadRequest(
|
||||
"FRPC remote port is required for TCP/UDP proxies".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
FrpcConfigMode::Full => {
|
||||
let toml = config.custom_toml.trim();
|
||||
if toml.is_empty() {
|
||||
return Err(AppError::BadRequest(
|
||||
"FRPC full configuration is required".into(),
|
||||
));
|
||||
}
|
||||
toml.parse::<DocumentMut>().map_err(|e| {
|
||||
AppError::BadRequest(format!("FRPC full configuration is not valid TOML: {}", e))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_extensions(State(state): State<Arc<AppState>>) -> Json<ExtensionsStatus> {
|
||||
let config = state.config.get();
|
||||
let mgr = &state.extensions;
|
||||
@@ -54,6 +96,11 @@ pub async fn list_extensions(State(state): State<Arc<AppState>>) -> Json<Extensi
|
||||
status: mgr.status(ExtensionId::Easytier).await,
|
||||
config: config.extensions.easytier.clone(),
|
||||
},
|
||||
frpc: FrpcInfo {
|
||||
available: mgr.check_available(ExtensionId::Frpc),
|
||||
status: mgr.status(ExtensionId::Frpc).await,
|
||||
config: config.extensions.frpc.clone(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -159,6 +206,25 @@ pub struct EasytierConfigUpdate {
|
||||
pub virtual_ip: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FrpcConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub config_mode: Option<FrpcConfigMode>,
|
||||
pub proxy_name: Option<String>,
|
||||
pub proxy_type: Option<FrpProxyType>,
|
||||
pub server_addr: Option<String>,
|
||||
pub server_port: Option<u16>,
|
||||
pub token: Option<String>,
|
||||
pub local_ip: Option<String>,
|
||||
pub local_port: Option<u16>,
|
||||
pub remote_port: Option<Option<u16>>,
|
||||
pub custom_domain: Option<Option<String>>,
|
||||
pub secret_key: Option<String>,
|
||||
pub tls: Option<bool>,
|
||||
pub custom_toml: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn update_ttyd_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<TtydConfigUpdate>,
|
||||
@@ -295,3 +361,81 @@ pub async fn update_easytier_config(
|
||||
|
||||
Ok(Json(new_config.extensions.easytier.clone()))
|
||||
}
|
||||
|
||||
pub async fn update_frpc_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<FrpcConfigUpdate>,
|
||||
) -> Result<Json<FrpcConfig>> {
|
||||
let current_config = state.config.get();
|
||||
let was_enabled = current_config.extensions.frpc.enabled;
|
||||
let mut next_frpc = current_config.extensions.frpc.clone();
|
||||
|
||||
if let Some(enabled) = req.enabled {
|
||||
next_frpc.enabled = enabled;
|
||||
}
|
||||
if let Some(config_mode) = req.config_mode {
|
||||
next_frpc.config_mode = config_mode;
|
||||
}
|
||||
if let Some(ref proxy_name) = req.proxy_name {
|
||||
next_frpc.proxy_name = proxy_name.clone();
|
||||
}
|
||||
if let Some(proxy_type) = req.proxy_type {
|
||||
next_frpc.proxy_type = proxy_type;
|
||||
}
|
||||
if let Some(ref addr) = req.server_addr {
|
||||
next_frpc.server_addr = addr.clone();
|
||||
}
|
||||
if let Some(port) = req.server_port {
|
||||
next_frpc.server_port = port;
|
||||
}
|
||||
if let Some(ref token) = req.token {
|
||||
next_frpc.token = token.clone();
|
||||
}
|
||||
if let Some(ref local_ip) = req.local_ip {
|
||||
next_frpc.local_ip = local_ip.clone();
|
||||
}
|
||||
if let Some(local_port) = req.local_port {
|
||||
next_frpc.local_port = local_port;
|
||||
}
|
||||
if let Some(remote_port) = req.remote_port {
|
||||
next_frpc.remote_port = remote_port;
|
||||
}
|
||||
if let Some(custom_domain) = req.custom_domain {
|
||||
next_frpc.custom_domain = custom_domain;
|
||||
}
|
||||
if let Some(ref secret_key) = req.secret_key {
|
||||
next_frpc.secret_key = secret_key.clone();
|
||||
}
|
||||
if let Some(tls) = req.tls {
|
||||
next_frpc.tls = tls;
|
||||
}
|
||||
if let Some(ref custom_toml) = req.custom_toml {
|
||||
next_frpc.custom_toml = custom_toml.clone();
|
||||
}
|
||||
|
||||
if next_frpc.enabled || matches!(next_frpc.config_mode, FrpcConfigMode::Full) {
|
||||
validate_frpc_enabled(&next_frpc)?;
|
||||
}
|
||||
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.extensions.frpc = next_frpc.clone();
|
||||
})
|
||||
.await?;
|
||||
|
||||
let new_config = state.config.get();
|
||||
let is_enabled = new_config.extensions.frpc.enabled;
|
||||
|
||||
if was_enabled && !is_enabled {
|
||||
state.extensions.stop(ExtensionId::Frpc).await.ok();
|
||||
} else if !was_enabled && is_enabled && state.extensions.check_available(ExtensionId::Frpc) {
|
||||
state
|
||||
.extensions
|
||||
.start(ExtensionId::Frpc, &new_config.extensions)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(Json(new_config.extensions.frpc.clone()))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
use super::*;
|
||||
use crate::error::AppError;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Ch9329DescriptorQuery {
|
||||
pub port: Option<String>,
|
||||
pub baud_rate: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct HidStatus {
|
||||
@@ -51,3 +58,57 @@ pub async fn hid_reset(State(state): State<Arc<AppState>>) -> Result<Json<LoginR
|
||||
message: Some("HID state reset".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Read the CH9329 USB descriptor, falling back to the saved config when SET is not low.
|
||||
pub async fn hid_ch9329_descriptor(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<Ch9329DescriptorQuery>,
|
||||
) -> Result<Json<crate::config::Ch9329DescriptorState>> {
|
||||
let config = state.config.get();
|
||||
let hid = &config.hid;
|
||||
let port = query.port.as_deref().filter(|port| !port.trim().is_empty());
|
||||
let baud_rate = query.baud_rate;
|
||||
|
||||
let descriptor_result = match (port, baud_rate) {
|
||||
(Some(port), Some(baud_rate))
|
||||
if port != hid.ch9329_port || baud_rate != hid.ch9329_baudrate =>
|
||||
{
|
||||
crate::hid::ch9329::Ch9329Backend::read_device_descriptor(port, baud_rate)
|
||||
}
|
||||
_ => state.hid.read_ch9329_descriptor().await,
|
||||
};
|
||||
|
||||
let descriptor = match descriptor_result {
|
||||
Ok(descriptor) => descriptor,
|
||||
Err(err) if is_ch9329_config_mode_unavailable(&err) => cached_ch9329_descriptor(hid),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
Ok(Json(descriptor))
|
||||
}
|
||||
|
||||
fn is_ch9329_config_mode_unavailable(err: &AppError) -> bool {
|
||||
matches!(
|
||||
err,
|
||||
AppError::HidError {
|
||||
backend,
|
||||
error_code,
|
||||
..
|
||||
} if backend == "ch9329" && error_code == "invalid_response"
|
||||
)
|
||||
}
|
||||
|
||||
fn cached_ch9329_descriptor(
|
||||
hid: &crate::config::HidConfig,
|
||||
) -> crate::config::Ch9329DescriptorState {
|
||||
let descriptor = hid.ch9329_descriptor.clone();
|
||||
crate::config::Ch9329DescriptorState {
|
||||
manufacturer_enabled: !descriptor.manufacturer.is_empty(),
|
||||
product_enabled: !descriptor.product.is_empty(),
|
||||
serial_enabled: descriptor
|
||||
.serial_number
|
||||
.as_ref()
|
||||
.is_some_and(|value| !value.is_empty()),
|
||||
config_mode_available: false,
|
||||
descriptor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ pub async fn setup_init(
|
||||
if let Some(enabled) = req.msd_enabled {
|
||||
config.msd.enabled = enabled;
|
||||
}
|
||||
config.enforce_invariants();
|
||||
|
||||
// Extension settings
|
||||
if let Some(enabled) = req.ttyd_enabled {
|
||||
@@ -169,6 +170,7 @@ pub async fn setup_init(
|
||||
crate::config::HidBackend::Ch9329 => crate::hid::HidBackendType::Ch9329 {
|
||||
port: new_config.hid.ch9329_port.clone(),
|
||||
baud_rate: new_config.hid.ch9329_baudrate,
|
||||
hybrid_mouse: new_config.hid.ch9329_hybrid_mouse,
|
||||
},
|
||||
crate::config::HidBackend::None => crate::hid::HidBackendType::None,
|
||||
};
|
||||
|
||||
@@ -73,6 +73,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
.route("/webrtc/close", post(handlers::webrtc_close_session))
|
||||
// HID endpoints
|
||||
.route("/hid/status", get(handlers::hid_status))
|
||||
.route(
|
||||
"/hid/ch9329/descriptor",
|
||||
get(handlers::hid_ch9329_descriptor),
|
||||
)
|
||||
.route("/hid/reset", post(handlers::hid_reset))
|
||||
// WebSocket HID endpoint (for MJPEG mode)
|
||||
.route("/ws/hid", any(ws_hid_handler))
|
||||
@@ -205,6 +209,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
"/extensions/easytier/config",
|
||||
patch(handlers::extensions::update_easytier_config),
|
||||
)
|
||||
.route(
|
||||
"/extensions/frpc/config",
|
||||
patch(handlers::extensions::update_frpc_config),
|
||||
)
|
||||
// Terminal (ttyd) reverse proxy - WebSocket and HTTP
|
||||
.route("/terminal", get(handlers::terminal::terminal_index))
|
||||
.route("/terminal/", get(handlers::terminal::terminal_index))
|
||||
|
||||
@@ -24,6 +24,8 @@ import type {
|
||||
GostcConfigUpdate,
|
||||
EasytierConfig,
|
||||
EasytierConfigUpdate,
|
||||
FrpcConfig,
|
||||
FrpcConfigUpdate,
|
||||
WebConfigResponse,
|
||||
WebConfigUpdate,
|
||||
} from '@/types/generated'
|
||||
@@ -159,6 +161,12 @@ export const extensionsApi = {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
updateFrpc: (config: FrpcConfigUpdate) =>
|
||||
request<FrpcConfig>('/extensions/frpc/config', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
}
|
||||
|
||||
export interface RustDeskConfigResponse {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { request, ApiError } from './request'
|
||||
import type { CanonicalKey } from '@/types/generated'
|
||||
import type { CanonicalKey, Ch9329DescriptorState } from '@/types/generated'
|
||||
import { useHidWebSocket, type HidKeyboardEvent, type HidMouseEvent } from '@/composables/useHidWebSocket'
|
||||
|
||||
const API_BASE = '/api'
|
||||
@@ -435,6 +435,14 @@ export const hidApi = {
|
||||
reset: () =>
|
||||
request<{ success: boolean }>('/hid/reset', { method: 'POST' }),
|
||||
|
||||
ch9329Descriptor: (params?: { port?: string; baudRate?: number }) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params?.port) query.set('port', params.port)
|
||||
if (params?.baudRate) query.set('baud_rate', String(params.baudRate))
|
||||
const suffix = query.toString()
|
||||
return request<Ch9329DescriptorState>(`/hid/ch9329/descriptor${suffix ? `?${suffix}` : ''}`)
|
||||
},
|
||||
|
||||
consumer: async (usage: number) => {
|
||||
await ensureHidConnection()
|
||||
await hidWs.sendConsumer({ usage })
|
||||
|
||||
@@ -23,6 +23,10 @@ function t(key: string, params?: Record<string, unknown>): string {
|
||||
return String(i18n.global.t(key, params as any))
|
||||
}
|
||||
|
||||
function hasTranslation(key: string): boolean {
|
||||
return i18n.global.te(key)
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
|
||||
@@ -52,9 +56,73 @@ function getToastKey(endpoint: string, config?: ApiRequestConfig): string {
|
||||
function getErrorMessage(data: unknown, fallback: string): string {
|
||||
if (data && typeof data === 'object') {
|
||||
const message = (data as any).message
|
||||
if (typeof message === 'string' && message.trim()) return message
|
||||
if (typeof message === 'string' && message.trim()) return localizeBackendErrorMessage(message)
|
||||
}
|
||||
return fallback
|
||||
return localizeBackendErrorMessage(fallback)
|
||||
}
|
||||
|
||||
function extractCh9329Command(reason: string): string {
|
||||
const match = reason.match(/cmd 0x([0-9a-f]{2})/i)
|
||||
const cmd = match?.[1]
|
||||
return cmd ? `0x${cmd.toUpperCase()}` : ''
|
||||
}
|
||||
|
||||
function localizeHidErrorMessage(raw: string): string | null {
|
||||
const match = raw.match(/^HID error \[([^\]]+)\]: (.*) \(code: ([^)]+)\)$/)
|
||||
if (!match) return null
|
||||
|
||||
const backend = match[1] ?? ''
|
||||
const reason = match[2] ?? ''
|
||||
const code = match[3] ?? ''
|
||||
const command = extractCh9329Command(reason)
|
||||
|
||||
const keyByCode: Record<string, string> = {
|
||||
udc_not_configured: 'hid.errorHints.udcNotConfigured',
|
||||
disabled: 'hid.errorHints.disabled',
|
||||
enoent: 'hid.errorHints.hidDeviceMissing',
|
||||
not_opened: 'hid.errorHints.notOpened',
|
||||
port_not_found: 'hid.errorHints.portNotFound',
|
||||
invalid_config: 'hid.errorHints.invalidConfig',
|
||||
no_response: command ? 'hid.errorHints.noResponseWithCmd' : 'hid.errorHints.noResponse',
|
||||
protocol_error: 'hid.errorHints.protocolError',
|
||||
invalid_response: 'hid.errorHints.protocolError',
|
||||
enxio: 'hid.errorHints.deviceDisconnected',
|
||||
enodev: 'hid.errorHints.deviceDisconnected',
|
||||
serial_error: 'hid.errorHints.serialError',
|
||||
init_failed: 'hid.errorHints.initFailed',
|
||||
shutdown: 'hid.errorHints.shutdown',
|
||||
reconnecting: 'hid.errorHints.reconnecting',
|
||||
worker_stopped: 'hid.errorHints.workerStopped',
|
||||
}
|
||||
|
||||
const ioErrorCodes = new Set([
|
||||
'eio',
|
||||
'epipe',
|
||||
'eshutdown',
|
||||
'io_error',
|
||||
'write_failed',
|
||||
'read_failed',
|
||||
'device_unavailable',
|
||||
])
|
||||
|
||||
const key = keyByCode[code]
|
||||
?? (ioErrorCodes.has(code)
|
||||
? backend === 'otg'
|
||||
? 'hid.errorHints.otgIoError'
|
||||
: backend === 'ch9329'
|
||||
? 'hid.errorHints.ch9329IoError'
|
||||
: 'hid.errorHints.ioError'
|
||||
: '')
|
||||
|
||||
if (key && hasTranslation(key)) {
|
||||
return t(key, { cmd: command })
|
||||
}
|
||||
|
||||
return t('hid.errorHints.backendError', { backend })
|
||||
}
|
||||
|
||||
function localizeBackendErrorMessage(raw: string): string {
|
||||
return localizeHidErrorMessage(raw) ?? raw
|
||||
}
|
||||
|
||||
export async function request<T>(
|
||||
|
||||
@@ -415,6 +415,9 @@ export default {
|
||||
serialError: 'Serial communication error, check CH9329 wiring and config',
|
||||
initFailed: 'CH9329 initialization failed, check serial settings and power',
|
||||
shutdown: 'HID backend has stopped',
|
||||
reconnecting: 'CH9329 is reconnecting. Try again shortly',
|
||||
workerStopped: 'CH9329 background communication has stopped. Check the device connection, then restart HID service or save HID settings again',
|
||||
backendError: '{backend} HID backend error, check device connection and configuration',
|
||||
},
|
||||
},
|
||||
audio: {
|
||||
@@ -522,6 +525,7 @@ export default {
|
||||
extRustdeskSubtitle: 'Remote graphical access via RustDesk',
|
||||
extRtspSubtitle: 'Provide an RTSP video stream for external clients',
|
||||
extRemoteAccessSubtitle: 'Remote access through NAT-traversal services',
|
||||
extFrpcSubtitle: 'NAT traversal through the FRP client',
|
||||
aboutDesc: 'Open and Lightweight IP-KVM Solution',
|
||||
deviceInfo: 'Device Info',
|
||||
deviceInfoDesc: 'Host system information',
|
||||
@@ -726,6 +730,18 @@ export default {
|
||||
hidBackend: 'HID Backend',
|
||||
serialDevice: 'Serial Device',
|
||||
baudRate: 'Baud Rate',
|
||||
ch9329Options: 'CH9329 Options',
|
||||
ch9329OptionsDesc: 'Configure runtime compatibility for the CH9329 serial HID chip',
|
||||
ch9329HybridMouse: 'Linux Absolute Mouse Compatibility',
|
||||
ch9329HybridMouseDesc: 'Keep absolute movement on absolute packets, but send buttons and wheel through relative packets',
|
||||
ch9329Descriptor: 'CH9329 USB Device Descriptor',
|
||||
ch9329DescriptorDesc: 'Read USB identification fields from the CH9329 chip before editing',
|
||||
ch9329DescriptorLoading: 'Reading CH9329 descriptor...',
|
||||
ch9329DescriptorLoadFailed: 'Failed to read CH9329 descriptor',
|
||||
ch9329ConfigModeUnavailable: 'CH9329 configuration mode is unavailable. Pull SET low to read or write chip parameters; showing the last saved descriptor.',
|
||||
ch9329DescriptorReadRequired: 'Read the CH9329 descriptor successfully before saving',
|
||||
ch9329DescriptorWarning: 'Saving writes CH9329 parameters; changes may not show until the device is power-cycled or reconnected',
|
||||
ch9329StringLengthWarning: 'CH9329 strings are limited to 23 bytes',
|
||||
otgHidProfile: 'OTG HID Functions',
|
||||
otgHidProfileDesc: 'Select which HID functions are exposed to the host',
|
||||
otgEndpointBudget: 'Max Endpoints',
|
||||
@@ -956,7 +972,7 @@ export default {
|
||||
binaryNotFound: '{path} not found, please install the required program',
|
||||
remoteAccess: {
|
||||
title: 'Remote Access',
|
||||
desc: 'GOSTC NAT traversal and Easytier networking',
|
||||
desc: 'GOSTC/FRPC NAT traversal and Easytier networking',
|
||||
},
|
||||
ttyd: {
|
||||
title: 'Ttyd Web Terminal',
|
||||
@@ -987,6 +1003,33 @@ export default {
|
||||
virtualIp: 'Virtual IP',
|
||||
virtualIpHint: 'Leave empty for DHCP, or specify with CIDR (e.g., 10.0.0.1/24)',
|
||||
},
|
||||
frpc: {
|
||||
title: 'FRPC NAT Traversal',
|
||||
desc: 'Connect to an frps server through the FRP client',
|
||||
quickConfig: 'Quick Config',
|
||||
fullConfig: 'Full Config',
|
||||
fullConfigHint: 'Paste the provider TOML configuration file here',
|
||||
fullConfigRequired: 'Enter the full frpc.toml configuration',
|
||||
proxyType: 'Proxy Type',
|
||||
proxyName: 'Proxy Name',
|
||||
proxyNamePlaceholder: 'one-kvm-ssh',
|
||||
proxyNameRequired: 'Enter the FRPC proxy name',
|
||||
serverAddr: 'Server Address',
|
||||
serverAddrPlaceholder: 'frps.example.com',
|
||||
serverAddrRequired: 'Enter the FRPC server address',
|
||||
serverPort: 'Server Port',
|
||||
token: 'Token',
|
||||
tokenRequired: 'Enter the FRPC token',
|
||||
localIp: 'Local Address',
|
||||
localIpRequired: 'Enter the FRPC local address',
|
||||
localPort: 'Local Port',
|
||||
remotePort: 'Remote Port',
|
||||
remotePortRequired: 'TCP/UDP proxies require a remote port',
|
||||
customDomain: 'Custom Domain',
|
||||
customDomainPlaceholder: 'kvm.example.com',
|
||||
secretKey: 'Secret Key',
|
||||
tls: 'Enable TLS',
|
||||
},
|
||||
rustdesk: {
|
||||
title: 'RustDesk Remote',
|
||||
desc: 'Remote access via RustDesk client',
|
||||
|
||||
@@ -414,6 +414,9 @@ export default {
|
||||
serialError: '串口通信异常,请检查 CH9329 接线与配置',
|
||||
initFailed: 'CH9329 初始化失败,请检查串口参数与供电',
|
||||
shutdown: 'HID 后端已停止',
|
||||
reconnecting: 'CH9329 正在重连,请稍后重试',
|
||||
workerStopped: 'CH9329 后台通信已停止,请检查设备连接后重启 HID 服务或重新保存 HID 设置',
|
||||
backendError: '{backend} HID 后端异常,请检查设备连接与配置',
|
||||
},
|
||||
},
|
||||
audio: {
|
||||
@@ -521,6 +524,7 @@ export default {
|
||||
extRustdeskSubtitle: '通过 RustDesk 实现远程图形访问',
|
||||
extRtspSubtitle: '提供 RTSP 视频流以供其他客户端拉流',
|
||||
extRemoteAccessSubtitle: '通过内网穿透服务实现远程访问',
|
||||
extFrpcSubtitle: '通过 FRP 客户端实现内网穿透',
|
||||
aboutDesc: '开放轻量的 IP-KVM 解决方案',
|
||||
deviceInfo: '设备信息',
|
||||
deviceInfoDesc: '主机系统信息',
|
||||
@@ -725,6 +729,18 @@ export default {
|
||||
hidBackend: 'HID 后端',
|
||||
serialDevice: '串口设备',
|
||||
baudRate: '波特率',
|
||||
ch9329Options: 'CH9329 选项',
|
||||
ch9329OptionsDesc: '配置 CH9329 串口 HID 芯片的运行兼容性',
|
||||
ch9329HybridMouse: 'Linux 绝对鼠标兼容模式',
|
||||
ch9329HybridMouseDesc: '绝对移动仍使用绝对鼠标包,点击和滚轮改用相对鼠标包发送',
|
||||
ch9329Descriptor: 'CH9329 USB 设备描述符',
|
||||
ch9329DescriptorDesc: '先从 CH9329 芯片读取 USB 标识信息,读取成功后再修改',
|
||||
ch9329DescriptorLoading: '正在读取 CH9329 描述符...',
|
||||
ch9329DescriptorLoadFailed: '读取 CH9329 描述符失败',
|
||||
ch9329ConfigModeUnavailable: 'CH9329 配置模式不可用。读取或写入芯片参数需要将 SET 拉低;当前显示上次保存的描述符。',
|
||||
ch9329DescriptorReadRequired: '需要先成功读取 CH9329 描述符才能保存',
|
||||
ch9329DescriptorWarning: '保存会写入 CH9329 参数;需要重新上电或重新插拔后才会变化',
|
||||
ch9329StringLengthWarning: 'CH9329 字符串最长为 23 字节',
|
||||
otgHidProfile: 'OTG HID 功能',
|
||||
otgHidProfileDesc: '选择对目标主机暴露的 HID 功能',
|
||||
otgEndpointBudget: '最大端点数量',
|
||||
@@ -955,7 +971,7 @@ export default {
|
||||
binaryNotFound: '未找到 {path},请先安装对应程序',
|
||||
remoteAccess: {
|
||||
title: '远程访问',
|
||||
desc: 'GOSTC 内网穿透与 Easytier 组网',
|
||||
desc: 'GOSTC/FRPC 内网穿透与 Easytier 组网',
|
||||
},
|
||||
ttyd: {
|
||||
title: 'Ttyd 网页终端',
|
||||
@@ -986,6 +1002,33 @@ export default {
|
||||
virtualIp: '虚拟 IP',
|
||||
virtualIpHint: '留空则自动分配,手动指定需包含网段(如 10.0.0.1/24)',
|
||||
},
|
||||
frpc: {
|
||||
title: 'FRPC 内网穿透',
|
||||
desc: '通过 FRP 客户端连接 frps 服务',
|
||||
quickConfig: '快速配置',
|
||||
fullConfig: '完整配置',
|
||||
fullConfigHint: '可在此粘贴供应商 TOML 配置文件',
|
||||
fullConfigRequired: '请填写完整 frpc.toml 配置',
|
||||
proxyType: '代理类型',
|
||||
proxyName: '代理名称',
|
||||
proxyNamePlaceholder: 'one-kvm-ssh',
|
||||
proxyNameRequired: '请填写 FRPC 代理名称',
|
||||
serverAddr: '服务器地址',
|
||||
serverAddrPlaceholder: 'frps.example.com',
|
||||
serverAddrRequired: '请填写 FRPC 服务器地址',
|
||||
serverPort: '服务器端口',
|
||||
token: '认证令牌',
|
||||
tokenRequired: '请填写 FRPC 认证令牌',
|
||||
localIp: '本地地址',
|
||||
localIpRequired: '请填写 FRPC 本地地址',
|
||||
localPort: '本地端口',
|
||||
remotePort: '远程端口',
|
||||
remotePortRequired: 'TCP/UDP 代理需要填写远程端口',
|
||||
customDomain: '自定义域名',
|
||||
customDomainPlaceholder: 'kvm.example.com',
|
||||
secretKey: '访问密钥',
|
||||
tls: '启用 TLS',
|
||||
},
|
||||
rustdesk: {
|
||||
title: 'RustDesk 远程',
|
||||
desc: '使用 RustDesk 客户端进行远程访问',
|
||||
|
||||
@@ -54,6 +54,22 @@ export interface OtgHidFunctions {
|
||||
consumer: boolean;
|
||||
}
|
||||
|
||||
export interface Ch9329DescriptorConfig {
|
||||
vendor_id: number;
|
||||
product_id: number;
|
||||
manufacturer: string;
|
||||
product: string;
|
||||
serial_number?: string;
|
||||
}
|
||||
|
||||
export interface Ch9329DescriptorState {
|
||||
descriptor: Ch9329DescriptorConfig;
|
||||
manufacturer_enabled: boolean;
|
||||
product_enabled: boolean;
|
||||
serial_enabled: boolean;
|
||||
config_mode_available: boolean;
|
||||
}
|
||||
|
||||
export interface HidConfig {
|
||||
backend: HidBackend;
|
||||
otg_udc?: string;
|
||||
@@ -64,6 +80,8 @@ export interface HidConfig {
|
||||
otg_keyboard_leds?: boolean;
|
||||
ch9329_port: string;
|
||||
ch9329_baudrate: number;
|
||||
ch9329_hybrid_mouse?: boolean;
|
||||
ch9329_descriptor?: Ch9329DescriptorConfig;
|
||||
mouse_absolute: boolean;
|
||||
}
|
||||
|
||||
@@ -175,10 +193,43 @@ export interface EasytierConfig {
|
||||
virtual_ip?: string;
|
||||
}
|
||||
|
||||
export enum FrpcConfigMode {
|
||||
Quick = "quick",
|
||||
Full = "full",
|
||||
}
|
||||
|
||||
export enum FrpProxyType {
|
||||
Tcp = "tcp",
|
||||
Udp = "udp",
|
||||
Http = "http",
|
||||
Https = "https",
|
||||
Stcp = "stcp",
|
||||
Sudp = "sudp",
|
||||
Xtcp = "xtcp",
|
||||
}
|
||||
|
||||
export interface FrpcConfig {
|
||||
enabled: boolean;
|
||||
config_mode: FrpcConfigMode;
|
||||
proxy_name: string;
|
||||
proxy_type: FrpProxyType;
|
||||
server_addr: string;
|
||||
server_port: number;
|
||||
token: string;
|
||||
local_ip: string;
|
||||
local_port: number;
|
||||
remote_port?: number;
|
||||
custom_domain?: string;
|
||||
secret_key: string;
|
||||
tls: boolean;
|
||||
custom_toml: string;
|
||||
}
|
||||
|
||||
export interface ExtensionsConfig {
|
||||
ttyd: TtydConfig;
|
||||
gostc: GostcConfig;
|
||||
easytier: EasytierConfig;
|
||||
frpc: FrpcConfig;
|
||||
}
|
||||
|
||||
export interface RustDeskConfig {
|
||||
@@ -269,6 +320,14 @@ export interface AuthConfigUpdate {
|
||||
single_user_allow_multiple_sessions?: boolean;
|
||||
}
|
||||
|
||||
export interface Ch9329DescriptorConfigUpdate {
|
||||
vendor_id?: number;
|
||||
product_id?: number;
|
||||
manufacturer?: string;
|
||||
product?: string;
|
||||
serial_number?: string;
|
||||
}
|
||||
|
||||
export interface EasytierConfigUpdate {
|
||||
enabled?: boolean;
|
||||
network_name?: string;
|
||||
@@ -299,6 +358,7 @@ export enum ExtensionId {
|
||||
Ttyd = "ttyd",
|
||||
Gostc = "gostc",
|
||||
Easytier = "easytier",
|
||||
Frpc = "frpc",
|
||||
}
|
||||
|
||||
export interface ExtensionLogs {
|
||||
@@ -318,10 +378,34 @@ export interface GostcInfo {
|
||||
config: GostcConfig;
|
||||
}
|
||||
|
||||
export interface FrpcInfo {
|
||||
available: boolean;
|
||||
status: ExtensionStatus;
|
||||
config: FrpcConfig;
|
||||
}
|
||||
|
||||
export interface ExtensionsStatus {
|
||||
ttyd: TtydInfo;
|
||||
gostc: GostcInfo;
|
||||
easytier: EasytierInfo;
|
||||
frpc: FrpcInfo;
|
||||
}
|
||||
|
||||
export interface FrpcConfigUpdate {
|
||||
enabled?: boolean;
|
||||
config_mode?: FrpcConfigMode;
|
||||
proxy_name?: string;
|
||||
proxy_type?: FrpProxyType;
|
||||
server_addr?: string;
|
||||
server_port?: number;
|
||||
token?: string;
|
||||
local_ip?: string;
|
||||
local_port?: number;
|
||||
remote_port?: number | null;
|
||||
custom_domain?: string | null;
|
||||
secret_key?: string;
|
||||
tls?: boolean;
|
||||
custom_toml?: string;
|
||||
}
|
||||
|
||||
export interface GostcConfigUpdate {
|
||||
@@ -351,6 +435,8 @@ export interface HidConfigUpdate {
|
||||
backend?: HidBackend;
|
||||
ch9329_port?: string;
|
||||
ch9329_baudrate?: number;
|
||||
ch9329_hybrid_mouse?: boolean;
|
||||
ch9329_descriptor?: Ch9329DescriptorConfigUpdate;
|
||||
otg_udc?: string;
|
||||
otg_descriptor?: OtgDescriptorConfigUpdate;
|
||||
otg_profile?: OtgHidProfile;
|
||||
@@ -597,4 +683,3 @@ export enum CanonicalKey {
|
||||
AltRight = "AltRight",
|
||||
MetaRight = "MetaRight",
|
||||
}
|
||||
|
||||
|
||||
@@ -319,6 +319,7 @@ function hidErrorHint(errorCode?: string | null, backend?: string | null, reason
|
||||
case 'io_error':
|
||||
case 'write_failed':
|
||||
case 'read_failed':
|
||||
case 'device_unavailable':
|
||||
if (backend === 'otg') return t('hid.errorHints.otgIoError')
|
||||
if (backend === 'ch9329') return t('hid.errorHints.ch9329IoError')
|
||||
return t('hid.errorHints.ioError')
|
||||
|
||||
@@ -42,7 +42,10 @@ import type {
|
||||
OtgEndpointBudget,
|
||||
OtgHidProfile,
|
||||
OtgHidFunctions,
|
||||
Ch9329DescriptorConfig,
|
||||
Ch9329DescriptorState,
|
||||
} from '@/types/generated'
|
||||
import { FrpProxyType, FrpcConfigMode } from '@/types/generated'
|
||||
import { formatFpsLabel, toConfigFps } from '@/lib/fps'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { getVideoFormatState } from '@/lib/video-format-support'
|
||||
@@ -218,6 +221,7 @@ function selectSection(id: string) {
|
||||
function normalizeSettingsSection(value: unknown): SettingsSectionId | null {
|
||||
if (typeof value !== 'string') return null
|
||||
if (value === 'access-control') return 'account'
|
||||
if (value === 'ext-frpc') return 'ext-remote-access'
|
||||
return isSettingsSectionId(value) ? value : null
|
||||
}
|
||||
|
||||
@@ -315,11 +319,13 @@ const extensionLogs = ref<Record<string, string[]>>({
|
||||
ttyd: [],
|
||||
gostc: [],
|
||||
easytier: [],
|
||||
frpc: [],
|
||||
})
|
||||
const showLogs = ref<Record<string, boolean>>({
|
||||
ttyd: false,
|
||||
gostc: false,
|
||||
easytier: false,
|
||||
frpc: false,
|
||||
})
|
||||
|
||||
const showTerminalDialog = ref(false)
|
||||
@@ -328,6 +334,22 @@ const extConfig = ref({
|
||||
ttyd: { enabled: false, shell: '/bin/bash' },
|
||||
gostc: { enabled: false, addr: '', key: '', tls: true },
|
||||
easytier: { enabled: false, network_name: '', network_secret: '', peer_urls: [] as string[], virtual_ip: '' },
|
||||
frpc: {
|
||||
enabled: false,
|
||||
config_mode: FrpcConfigMode.Quick,
|
||||
proxy_name: '',
|
||||
proxy_type: FrpProxyType.Tcp,
|
||||
server_addr: '',
|
||||
server_port: 7000,
|
||||
token: '',
|
||||
local_ip: '127.0.0.1',
|
||||
local_port: 22,
|
||||
remote_port: undefined as number | undefined,
|
||||
custom_domain: '',
|
||||
secret_key: '',
|
||||
tls: true,
|
||||
custom_toml: '',
|
||||
},
|
||||
})
|
||||
|
||||
const gostcValidationMessage = computed(() => {
|
||||
@@ -341,6 +363,25 @@ const easytierValidationMessage = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
const frpcRemotePortRequired = computed(() => ['tcp', 'udp'].includes(extConfig.value.frpc.proxy_type))
|
||||
const showFrpcRemotePort = computed(() => ['tcp', 'udp', 'stcp', 'sudp', 'xtcp'].includes(extConfig.value.frpc.proxy_type))
|
||||
const showFrpcCustomDomain = computed(() => ['http', 'https'].includes(extConfig.value.frpc.proxy_type))
|
||||
const showFrpcSecretKey = computed(() => ['stcp', 'sudp', 'xtcp'].includes(extConfig.value.frpc.proxy_type))
|
||||
const frpcQuickMode = computed(() => extConfig.value.frpc.config_mode === FrpcConfigMode.Quick)
|
||||
|
||||
const frpcValidationMessage = computed(() => {
|
||||
if (extConfig.value.frpc.config_mode === FrpcConfigMode.Full) {
|
||||
if (!extConfig.value.frpc.custom_toml?.trim()) return t('extensions.frpc.fullConfigRequired')
|
||||
return ''
|
||||
}
|
||||
if (!extConfig.value.frpc.proxy_name?.trim()) return t('extensions.frpc.proxyNameRequired')
|
||||
if (!extConfig.value.frpc.server_addr?.trim()) return t('extensions.frpc.serverAddrRequired')
|
||||
if (!extConfig.value.frpc.token) return t('extensions.frpc.tokenRequired')
|
||||
if (!extConfig.value.frpc.local_ip?.trim()) return t('extensions.frpc.localIpRequired')
|
||||
if (frpcRemotePortRequired.value && !extConfig.value.frpc.remote_port) return t('extensions.frpc.remotePortRequired')
|
||||
return ''
|
||||
})
|
||||
|
||||
const rustdeskConfig = ref<RustDeskConfigResponse | null>(null)
|
||||
const rustdeskStatus = ref<RustDeskStatusResponse | null>(null)
|
||||
const rustdeskPassword = ref<RustDeskPasswordResponse | null>(null)
|
||||
@@ -522,6 +563,7 @@ const config = ref({
|
||||
consumer: true,
|
||||
} as OtgHidFunctions,
|
||||
hid_otg_keyboard_leds: false,
|
||||
hid_ch9329_hybrid_mouse: false,
|
||||
msd_enabled: false,
|
||||
msd_dir: '',
|
||||
encoder_backend: 'auto',
|
||||
@@ -914,12 +956,135 @@ const otgProductIdHex = ref('0104')
|
||||
const otgManufacturer = ref('One-KVM')
|
||||
const otgProduct = ref('One-KVM USB Device')
|
||||
const otgSerialNumber = ref('')
|
||||
const ch9329VendorIdHex = ref('1a86')
|
||||
const ch9329ProductIdHex = ref('e129')
|
||||
const ch9329Manufacturer = ref('WCH.CN')
|
||||
const ch9329Product = ref('CH9329')
|
||||
const ch9329SerialNumber = ref('')
|
||||
const ch9329DescriptorLoaded = ref(false)
|
||||
const ch9329DescriptorLoading = ref(false)
|
||||
const ch9329DescriptorError = ref('')
|
||||
const ch9329DescriptorSource = ref<{ port: string; baudrate: number } | null>(null)
|
||||
const ch9329DescriptorBaseline = ref<{
|
||||
vendorId: string
|
||||
productId: string
|
||||
manufacturer: string
|
||||
product: string
|
||||
serialNumber: string
|
||||
} | null>(null)
|
||||
const utf8Encoder = new TextEncoder()
|
||||
|
||||
const validateHex = (event: Event, _field: string) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
input.value = input.value.replace(/[^0-9a-fA-F]/g, '').toLowerCase()
|
||||
}
|
||||
|
||||
function utf8ByteLength(value: string): number {
|
||||
return utf8Encoder.encode(value).length
|
||||
}
|
||||
|
||||
function applyCh9329DescriptorForm(descriptor: Ch9329DescriptorConfig, defaults = false) {
|
||||
ch9329VendorIdHex.value = descriptor.vendor_id?.toString(16).padStart(4, '0') || '1a86'
|
||||
ch9329ProductIdHex.value = descriptor.product_id?.toString(16).padStart(4, '0') || 'e129'
|
||||
ch9329Manufacturer.value = descriptor.manufacturer || (defaults ? 'WCH.CN' : '')
|
||||
ch9329Product.value = descriptor.product || (defaults ? 'CH9329' : '')
|
||||
ch9329SerialNumber.value = descriptor.serial_number || ''
|
||||
}
|
||||
|
||||
function applyCh9329DescriptorState(state: Ch9329DescriptorState) {
|
||||
applyCh9329DescriptorForm(state.descriptor)
|
||||
ch9329DescriptorBaseline.value = currentCh9329DescriptorForm()
|
||||
if (!state.config_mode_available) {
|
||||
ch9329DescriptorError.value = t('settings.ch9329ConfigModeUnavailable')
|
||||
}
|
||||
}
|
||||
|
||||
function currentCh9329DescriptorForm() {
|
||||
return {
|
||||
vendorId: ch9329VendorIdHex.value.toLowerCase().padStart(4, '0'),
|
||||
productId: ch9329ProductIdHex.value.toLowerCase().padStart(4, '0'),
|
||||
manufacturer: ch9329Manufacturer.value,
|
||||
product: ch9329Product.value,
|
||||
serialNumber: ch9329SerialNumber.value,
|
||||
}
|
||||
}
|
||||
|
||||
function currentCh9329DescriptorSource() {
|
||||
return {
|
||||
port: config.value.hid_serial_device || '',
|
||||
baudrate: Number(config.value.hid_serial_baudrate) || 9600,
|
||||
}
|
||||
}
|
||||
|
||||
function clearCh9329DescriptorState() {
|
||||
ch9329DescriptorLoaded.value = false
|
||||
ch9329DescriptorLoading.value = false
|
||||
ch9329DescriptorError.value = ''
|
||||
ch9329DescriptorSource.value = null
|
||||
ch9329DescriptorBaseline.value = null
|
||||
}
|
||||
|
||||
const isCh9329DescriptorSourceCurrent = computed(() => {
|
||||
if (config.value.hid_backend !== 'ch9329') return false
|
||||
const source = ch9329DescriptorSource.value
|
||||
if (!source) return false
|
||||
const current = currentCh9329DescriptorSource()
|
||||
return source.port === current.port && source.baudrate === current.baudrate
|
||||
})
|
||||
|
||||
async function loadCh9329Descriptor() {
|
||||
if (config.value.hid_backend !== 'ch9329') return
|
||||
const source = currentCh9329DescriptorSource()
|
||||
ch9329DescriptorLoading.value = true
|
||||
ch9329DescriptorLoaded.value = false
|
||||
ch9329DescriptorSource.value = null
|
||||
ch9329DescriptorError.value = ''
|
||||
try {
|
||||
const state = await hidApi.ch9329Descriptor({
|
||||
port: source.port,
|
||||
baudRate: source.baudrate,
|
||||
})
|
||||
applyCh9329DescriptorState(state)
|
||||
ch9329DescriptorLoaded.value = true
|
||||
ch9329DescriptorSource.value = source
|
||||
} catch (e) {
|
||||
ch9329DescriptorError.value = e instanceof Error ? e.message : t('settings.ch9329DescriptorLoadFailed')
|
||||
} finally {
|
||||
ch9329DescriptorLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isCh9329DescriptorValid = computed(() => {
|
||||
if (config.value.hid_backend !== 'ch9329') return true
|
||||
return utf8ByteLength(ch9329Manufacturer.value) <= 23
|
||||
&& utf8ByteLength(ch9329Product.value) <= 23
|
||||
&& utf8ByteLength(ch9329SerialNumber.value) <= 23
|
||||
})
|
||||
|
||||
const canEditCh9329Descriptor = computed(() =>
|
||||
config.value.hid_backend === 'ch9329'
|
||||
&& ch9329DescriptorLoaded.value
|
||||
&& isCh9329DescriptorSourceCurrent.value
|
||||
&& !ch9329DescriptorLoading.value
|
||||
)
|
||||
|
||||
const isCh9329DescriptorDirty = computed(() => {
|
||||
if (!canEditCh9329Descriptor.value || !ch9329DescriptorBaseline.value) return false
|
||||
const current = currentCh9329DescriptorForm()
|
||||
const baseline = ch9329DescriptorBaseline.value
|
||||
return current.vendorId !== baseline.vendorId
|
||||
|| current.productId !== baseline.productId
|
||||
|| current.manufacturer !== baseline.manufacturer
|
||||
|| current.product !== baseline.product
|
||||
|| current.serialNumber !== baseline.serialNumber
|
||||
})
|
||||
|
||||
const isHidSettingsValid = computed(() =>
|
||||
isHidFunctionSelectionValid.value
|
||||
&& isOtgEndpointBudgetValid.value
|
||||
&& isCh9329DescriptorValid.value
|
||||
)
|
||||
|
||||
watch(() => config.value.msd_enabled, (enabled) => {
|
||||
if (!enabled && activeSection.value === 'msd') {
|
||||
activeSection.value = 'hid'
|
||||
@@ -1226,13 +1391,23 @@ async function saveConfig() {
|
||||
}
|
||||
|
||||
if (activeSection.value === 'hid') {
|
||||
if (!isHidFunctionSelectionValid.value || !isOtgEndpointBudgetValid.value) {
|
||||
if (!isHidSettingsValid.value) {
|
||||
return
|
||||
}
|
||||
const hidUpdate: any = {
|
||||
backend: config.value.hid_backend as any,
|
||||
ch9329_port: config.value.hid_serial_device || undefined,
|
||||
ch9329_baudrate: config.value.hid_serial_baudrate,
|
||||
ch9329_hybrid_mouse: config.value.hid_ch9329_hybrid_mouse,
|
||||
}
|
||||
if (config.value.hid_backend === 'ch9329' && isCh9329DescriptorDirty.value) {
|
||||
hidUpdate.ch9329_descriptor = {
|
||||
vendor_id: parseInt(ch9329VendorIdHex.value, 16) || 0x1a86,
|
||||
product_id: parseInt(ch9329ProductIdHex.value, 16) || 0xe129,
|
||||
manufacturer: ch9329Manufacturer.value,
|
||||
product: ch9329Product.value,
|
||||
serial_number: ch9329SerialNumber.value || '',
|
||||
}
|
||||
}
|
||||
if (config.value.hid_backend === 'otg') {
|
||||
hidUpdate.otg_descriptor = {
|
||||
@@ -1247,10 +1422,12 @@ async function saveConfig() {
|
||||
hidUpdate.otg_functions = { ...config.value.hid_otg_functions }
|
||||
hidUpdate.otg_keyboard_leds = config.value.hid_otg_keyboard_leds
|
||||
}
|
||||
await configStore.updateMsd({
|
||||
enabled: config.value.msd_enabled,
|
||||
})
|
||||
await configStore.updateHid(hidUpdate)
|
||||
if (config.value.hid_backend === 'otg') {
|
||||
await configStore.updateMsd({ enabled: config.value.msd_enabled })
|
||||
} else {
|
||||
await configStore.updateMsd({ enabled: false })
|
||||
}
|
||||
}
|
||||
|
||||
if (activeSection.value === 'msd') {
|
||||
@@ -1259,7 +1436,9 @@ async function saveConfig() {
|
||||
})
|
||||
}
|
||||
|
||||
if (activeSection.value !== 'hid') {
|
||||
await loadSectionData(activeSection.value)
|
||||
}
|
||||
saved.value = true
|
||||
setTimeout(() => (saved.value = false), 2000)
|
||||
} catch {
|
||||
@@ -1296,6 +1475,7 @@ async function loadConfig() {
|
||||
consumer: hid.otg_functions?.consumer ?? true,
|
||||
} as OtgHidFunctions,
|
||||
hid_otg_keyboard_leds: hid.otg_keyboard_leds ?? false,
|
||||
hid_ch9329_hybrid_mouse: hid.ch9329_hybrid_mouse ?? false,
|
||||
msd_enabled: msd.enabled || false,
|
||||
msd_dir: msd.msd_dir || '',
|
||||
encoder_backend: stream.encoder || 'auto',
|
||||
@@ -1312,6 +1492,16 @@ async function loadConfig() {
|
||||
otgProduct.value = hid.otg_descriptor.product || 'One-KVM USB Device'
|
||||
otgSerialNumber.value = hid.otg_descriptor.serial_number || ''
|
||||
}
|
||||
if (hid.ch9329_descriptor) {
|
||||
if (hid.backend !== 'ch9329') {
|
||||
applyCh9329DescriptorForm(hid.ch9329_descriptor, true)
|
||||
}
|
||||
}
|
||||
if (hid.backend === 'ch9329') {
|
||||
await loadCh9329Descriptor()
|
||||
} else {
|
||||
clearCh9329DescriptorState()
|
||||
}
|
||||
|
||||
} catch {
|
||||
}
|
||||
@@ -1373,6 +1563,23 @@ async function loadExtensions() {
|
||||
peer_urls: easytier.peer_urls || [],
|
||||
virtual_ip: easytier.virtual_ip || '',
|
||||
}
|
||||
const frpc = extensions.value.frpc.config
|
||||
extConfig.value.frpc = {
|
||||
enabled: frpc.enabled,
|
||||
config_mode: frpc.config_mode || FrpcConfigMode.Quick,
|
||||
proxy_name: frpc.proxy_name,
|
||||
proxy_type: frpc.proxy_type,
|
||||
server_addr: frpc.server_addr,
|
||||
server_port: frpc.server_port,
|
||||
token: frpc.token,
|
||||
local_ip: frpc.local_ip,
|
||||
local_port: frpc.local_port,
|
||||
remote_port: frpc.remote_port,
|
||||
custom_domain: frpc.custom_domain || '',
|
||||
secret_key: frpc.secret_key,
|
||||
tls: frpc.tls,
|
||||
custom_toml: frpc.custom_toml || '',
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} finally {
|
||||
@@ -1380,8 +1587,11 @@ async function loadExtensions() {
|
||||
}
|
||||
}
|
||||
|
||||
async function startExtension(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
if ((id === 'gostc' || id === 'easytier') && !validateExtensionConfig(id)) return
|
||||
type ExtensionConfigId = 'ttyd' | 'gostc' | 'easytier' | 'frpc'
|
||||
type ValidatedExtensionConfigId = Exclude<ExtensionConfigId, 'ttyd'>
|
||||
|
||||
async function startExtension(id: ExtensionConfigId) {
|
||||
if (id !== 'ttyd' && !validateExtensionConfig(id)) return
|
||||
|
||||
try {
|
||||
await extensionsApi.start(id)
|
||||
@@ -1390,7 +1600,7 @@ async function startExtension(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
}
|
||||
}
|
||||
|
||||
async function stopExtension(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
async function stopExtension(id: ExtensionConfigId) {
|
||||
try {
|
||||
await extensionsApi.stop(id)
|
||||
await loadExtensions()
|
||||
@@ -1398,7 +1608,7 @@ async function stopExtension(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshExtensionLogs(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
async function refreshExtensionLogs(id: ExtensionConfigId) {
|
||||
try {
|
||||
const result = await extensionsApi.logs(id, 100)
|
||||
extensionLogs.value[id] = result.logs
|
||||
@@ -1406,8 +1616,12 @@ async function refreshExtensionLogs(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveExtensionConfig(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
if ((id === 'gostc' || id === 'easytier') && extConfig.value[id].enabled && !validateExtensionConfig(id)) return
|
||||
async function saveExtensionConfig(id: ExtensionConfigId) {
|
||||
if (id !== 'ttyd') {
|
||||
const shouldValidate = extConfig.value[id].enabled
|
||||
|| (id === 'frpc' && extConfig.value.frpc.config_mode === FrpcConfigMode.Full)
|
||||
if (shouldValidate && !validateExtensionConfig(id)) return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -1417,6 +1631,14 @@ async function saveExtensionConfig(id: 'ttyd' | 'gostc' | 'easytier') {
|
||||
await extensionsApi.updateGostc(extConfig.value.gostc)
|
||||
} else if (id === 'easytier') {
|
||||
await extensionsApi.updateEasytier(extConfig.value.easytier)
|
||||
} else if (id === 'frpc') {
|
||||
const frpc = extConfig.value.frpc
|
||||
await extensionsApi.updateFrpc({
|
||||
...frpc,
|
||||
remote_port: frpcQuickMode.value && showFrpcRemotePort.value ? frpc.remote_port : undefined,
|
||||
custom_domain: frpcQuickMode.value && showFrpcCustomDomain.value ? frpc.custom_domain || undefined : undefined,
|
||||
secret_key: frpcQuickMode.value && showFrpcSecretKey.value ? frpc.secret_key : '',
|
||||
})
|
||||
}
|
||||
await loadExtensions()
|
||||
saved.value = true
|
||||
@@ -1662,10 +1884,15 @@ function showValidationError(message: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
function validateExtensionConfig(id: 'gostc' | 'easytier'): boolean {
|
||||
const message = id === 'gostc'
|
||||
? gostcValidationMessage.value
|
||||
: easytierValidationMessage.value
|
||||
function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean {
|
||||
let message = ''
|
||||
if (id === 'gostc') {
|
||||
message = gostcValidationMessage.value
|
||||
} else if (id === 'easytier') {
|
||||
message = easytierValidationMessage.value
|
||||
} else {
|
||||
message = frpcValidationMessage.value
|
||||
}
|
||||
|
||||
return !message || showValidationError(message)
|
||||
}
|
||||
@@ -2247,8 +2474,22 @@ watch(updateChannel, async () => {
|
||||
watch(() => config.value.hid_backend, () => {
|
||||
otgSelfCheckResult.value = null
|
||||
otgSelfCheckError.value = ''
|
||||
if (config.value.hid_backend === 'ch9329') {
|
||||
void loadCh9329Descriptor()
|
||||
} else {
|
||||
clearCh9329DescriptorState()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [config.value.hid_serial_device, config.value.hid_serial_baudrate],
|
||||
() => {
|
||||
if (config.value.hid_backend === 'ch9329' && !isCh9329DescriptorSourceCurrent.value) {
|
||||
clearCh9329DescriptorState()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(() => route.query.tab, (tab) => {
|
||||
const section = normalizeSettingsSection(tab)
|
||||
if (section && activeSection.value !== section) {
|
||||
@@ -2649,6 +2890,109 @@ watch(isWindows, () => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<template v-if="config.hid_backend === 'ch9329'">
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">{{ t('settings.ch9329Options') }}</h4>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.ch9329OptionsDesc') }}</p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<Label>{{ t('settings.ch9329HybridMouse') }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t('settings.ch9329HybridMouseDesc') }}</p>
|
||||
</div>
|
||||
<Switch v-model="config.hid_ch9329_hybrid_mouse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">{{ t('settings.ch9329Descriptor') }}</h4>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.ch9329DescriptorDesc') }}</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8 shrink-0" :aria-label="t('common.refresh')" :disabled="ch9329DescriptorLoading" @click="loadCh9329Descriptor">
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': ch9329DescriptorLoading }" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="ch9329DescriptorLoading" class="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t('settings.ch9329DescriptorLoading') }}
|
||||
</p>
|
||||
<p v-else-if="ch9329DescriptorError" class="text-sm text-destructive">
|
||||
{{ ch9329DescriptorError }}
|
||||
</p>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="ch9329-vid">{{ t('settings.vendorId') }}</Label>
|
||||
<Input
|
||||
id="ch9329-vid"
|
||||
v-model="ch9329VendorIdHex"
|
||||
placeholder="1a86"
|
||||
maxlength="4"
|
||||
:disabled="!canEditCh9329Descriptor"
|
||||
@input="validateHex($event, 'ch9329-vid')"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="ch9329-pid">{{ t('settings.productId') }}</Label>
|
||||
<Input
|
||||
id="ch9329-pid"
|
||||
v-model="ch9329ProductIdHex"
|
||||
placeholder="e129"
|
||||
maxlength="4"
|
||||
:disabled="!canEditCh9329Descriptor"
|
||||
@input="validateHex($event, 'ch9329-pid')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="ch9329-manufacturer">{{ t('settings.manufacturer') }}</Label>
|
||||
<Input
|
||||
id="ch9329-manufacturer"
|
||||
v-model="ch9329Manufacturer"
|
||||
placeholder="WCH.CN"
|
||||
maxlength="23"
|
||||
:disabled="!canEditCh9329Descriptor"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="ch9329-product">{{ t('settings.productName') }}</Label>
|
||||
<Input
|
||||
id="ch9329-product"
|
||||
v-model="ch9329Product"
|
||||
placeholder="CH9329"
|
||||
maxlength="23"
|
||||
:disabled="!canEditCh9329Descriptor"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="ch9329-serial">{{ t('settings.serialNumber') }}</Label>
|
||||
<Input
|
||||
id="ch9329-serial"
|
||||
v-model="ch9329SerialNumber"
|
||||
:placeholder="t('settings.serialNumberAuto')"
|
||||
maxlength="23"
|
||||
:disabled="!canEditCh9329Descriptor"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="!ch9329DescriptorLoading && !ch9329DescriptorLoaded && !ch9329DescriptorError" class="text-xs text-muted-foreground">
|
||||
{{ t('settings.ch9329DescriptorReadRequired') }}
|
||||
</p>
|
||||
<p v-if="!isCh9329DescriptorValid" class="text-xs text-amber-600 dark:text-amber-400">
|
||||
{{ t('settings.ch9329StringLengthWarning') }}
|
||||
</p>
|
||||
<p class="text-sm text-amber-600 dark:text-amber-400">
|
||||
{{ t('settings.ch9329DescriptorWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- OTG Descriptor Settings -->
|
||||
<template v-if="config.hid_backend === 'otg'">
|
||||
<Separator class="my-4" />
|
||||
@@ -3982,6 +4326,183 @@ watch(isWindows, () => {
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- FRPC -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<CardTitle>{{ t('extensions.frpc.title') }}</CardTitle>
|
||||
<CardDescription>{{ t('extensions.frpc.desc') }}</CardDescription>
|
||||
</div>
|
||||
<Badge :variant="extensions?.frpc?.available ? 'default' : 'destructive'">
|
||||
{{ extensions?.frpc?.available ? t('extensions.available') : t('extensions.unavailable') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div v-if="!extensions?.frpc?.available" class="text-sm text-muted-foreground bg-muted p-3 rounded-md">
|
||||
{{ t('extensions.binaryNotFound', { path: isWindows ? 'frpc.exe' : '/usr/bin/frpc' }) }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div :class="['w-2 h-2 rounded-full', getExtStatusClass(extensions?.frpc?.status)]" />
|
||||
<span class="text-sm">{{ getExtStatusText(extensions?.frpc?.status) }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
v-if="!isExtRunning(extensions?.frpc?.status)"
|
||||
size="sm"
|
||||
@click="startExtension('frpc')"
|
||||
:disabled="extensionsLoading || !!frpcValidationMessage"
|
||||
>
|
||||
<Play class="h-4 w-4 mr-1" />
|
||||
{{ t('extensions.start') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="stopExtension('frpc')"
|
||||
:disabled="extensionsLoading"
|
||||
>
|
||||
<Square class="h-4 w-4 mr-1" />
|
||||
{{ t('extensions.stop') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="grid gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||
<Switch v-model="extConfig.frpc.enabled" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 rounded-md bg-muted p-1">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-sm px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
frpcQuickMode ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
||||
]"
|
||||
:disabled="isExtRunning(extensions?.frpc?.status)"
|
||||
@click="extConfig.frpc.config_mode = FrpcConfigMode.Quick"
|
||||
>
|
||||
{{ t('extensions.frpc.quickConfig') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-sm px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
!frpcQuickMode ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
||||
]"
|
||||
:disabled="isExtRunning(extensions?.frpc?.status)"
|
||||
@click="extConfig.frpc.config_mode = FrpcConfigMode.Full"
|
||||
>
|
||||
{{ t('extensions.frpc.fullConfig') }}
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="frpcQuickMode">
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.proxyType') }}</Label>
|
||||
<div class="sm:col-span-3">
|
||||
<RadioGroup v-model="extConfig.frpc.proxy_type" class="flex flex-wrap gap-4" :disabled="isExtRunning(extensions?.frpc?.status)">
|
||||
<div v-for="type in ['tcp', 'udp', 'http', 'https', 'stcp', 'sudp', 'xtcp']" :key="type" class="flex items-center space-x-2">
|
||||
<RadioGroupItem :value="type" :id="`frpc-${type}`" />
|
||||
<Label :for="`frpc-${type}`" class="cursor-pointer uppercase">{{ type }}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.proxyName') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model="extConfig.frpc.proxy_name" :placeholder="t('extensions.frpc.proxyNamePlaceholder')" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && !extConfig.frpc.proxy_name?.trim()" class="text-xs text-destructive">{{ t('extensions.frpc.proxyNameRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.serverAddr') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model="extConfig.frpc.server_addr" :placeholder="t('extensions.frpc.serverAddrPlaceholder')" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && !extConfig.frpc.server_addr?.trim()" class="text-xs text-destructive">{{ t('extensions.frpc.serverAddrRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.serverPort') }}</Label>
|
||||
<Input v-model.number="extConfig.frpc.server_port" class="sm:col-span-3" type="number" min="1" max="65535" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.token') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model="extConfig.frpc.token" type="password" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && !extConfig.frpc.token" class="text-xs text-destructive">{{ t('extensions.frpc.tokenRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.localIp') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model="extConfig.frpc.local_ip" placeholder="127.0.0.1" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && !extConfig.frpc.local_ip?.trim()" class="text-xs text-destructive">{{ t('extensions.frpc.localIpRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.localPort') }}</Label>
|
||||
<Input v-model.number="extConfig.frpc.local_port" class="sm:col-span-3" type="number" min="1" max="65535" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div v-if="showFrpcRemotePort" class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.remotePort') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input v-model.number="extConfig.frpc.remote_port" type="number" min="1" max="65535" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
<p v-if="extConfig.frpc.enabled && frpcRemotePortRequired && !extConfig.frpc.remote_port" class="text-xs text-destructive">{{ t('extensions.frpc.remotePortRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showFrpcCustomDomain" class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.customDomain') }}</Label>
|
||||
<Input v-model="extConfig.frpc.custom_domain" class="sm:col-span-3" :placeholder="t('extensions.frpc.customDomainPlaceholder')" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div v-if="showFrpcSecretKey" class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.secretKey') }}</Label>
|
||||
<Input v-model="extConfig.frpc.secret_key" class="sm:col-span-3" type="password" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.frpc.tls') }}</Label>
|
||||
<div class="sm:col-span-3">
|
||||
<Switch v-model="extConfig.frpc.tls" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="space-y-1">
|
||||
<Textarea
|
||||
v-model="extConfig.frpc.custom_toml"
|
||||
class="min-h-[300px] font-mono text-xs"
|
||||
spellcheck="false"
|
||||
:disabled="isExtRunning(extensions?.frpc?.status)"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">{{ t('extensions.frpc.fullConfigHint') }}</p>
|
||||
<p v-if="!extConfig.frpc.custom_toml?.trim()" class="text-xs text-destructive">{{ t('extensions.frpc.fullConfigRequired') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<button type="button" @click="showLogs.frpc = !showLogs.frpc; if (showLogs.frpc) refreshExtensionLogs('frpc')" class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
|
||||
<ChevronRight :class="['h-4 w-4 transition-transform', showLogs.frpc ? 'rotate-90' : '']" />
|
||||
{{ t('extensions.viewLogs') }}
|
||||
</button>
|
||||
<div v-if="showLogs.frpc" class="space-y-2">
|
||||
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.frpc || []).join('\n') || t('extensions.noLogs') }}</pre>
|
||||
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('frpc')">
|
||||
<RefreshCw class="h-3 w-3 mr-1" />
|
||||
{{ t('common.refresh') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div v-if="extensions?.frpc?.available" class="flex justify-end">
|
||||
<Button :disabled="loading || isExtRunning(extensions?.frpc?.status)" @click="saveExtensionConfig('frpc')">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RTSP Section -->
|
||||
@@ -4425,8 +4946,16 @@ watch(isWindows, () => {
|
||||
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
|
||||
<span class="truncate">{{ t('settings.otgFunctionMinWarning') }}</span>
|
||||
</p>
|
||||
<p v-else-if="activeSection === 'hid' && !isCh9329DescriptorValid" class="text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1.5 min-w-0">
|
||||
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
|
||||
<span class="truncate">{{ t('settings.ch9329StringLengthWarning') }}</span>
|
||||
</p>
|
||||
<p v-else-if="activeSection === 'hid' && config.hid_backend === 'ch9329' && ch9329DescriptorLoading" class="text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1.5 min-w-0">
|
||||
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
|
||||
<span class="truncate">{{ t('settings.ch9329DescriptorLoading') }}</span>
|
||||
</p>
|
||||
<p v-else class="text-xs text-muted-foreground hidden sm:block">{{ t('settings.unsavedChangesHint') }}</p>
|
||||
<Button class="shrink-0 ml-auto" :disabled="loading || (activeSection === 'hid' && !isHidFunctionSelectionValid)" @click="saveConfig">
|
||||
<Button class="shrink-0 ml-auto" :disabled="loading || (activeSection === 'hid' && !isHidSettingsValid)" @click="saveConfig">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user