mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-21 23:13:21 +08:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ef5011760 | ||
|
|
d71112380c | ||
|
|
491bfbecb5 | ||
|
|
8a6cc98806 | ||
|
|
728373b067 | ||
|
|
c62ff2da87 | ||
|
|
572bf859ba | ||
|
|
aef136241a | ||
|
|
78af9f67de | ||
|
|
9f34b4109b | ||
|
|
1967d22dff | ||
|
|
0cba8ab127 | ||
|
|
d002226acb | ||
|
|
1db572e020 | ||
|
|
5de5ee42c2 | ||
|
|
2c19208094 | ||
|
|
3014edffbb | ||
|
|
ece4f303ce | ||
|
|
db9d79554a | ||
|
|
620fe0be54 | ||
|
|
79a4dcf2b0 | ||
|
|
a8ee9552a0 | ||
|
|
3749bafae0 | ||
|
|
b573f478fc | ||
|
|
d67a3ab7f8 | ||
|
|
dbe5672df4 | ||
|
|
48fd64319e | ||
|
|
ccf821ab22 | ||
|
|
b074aa5779 | ||
|
|
95a1fdf42d | ||
|
|
e678ec394d | ||
|
|
628909a192 | ||
|
|
5cab585115 | ||
|
|
dcfa3eadaf | ||
|
|
1647e70243 | ||
|
|
9c6d8a614c | ||
|
|
db7a845d3b | ||
|
|
1d48d45592 | ||
|
|
d613739b06 | ||
|
|
6e1194fe4c | ||
|
|
f101ddb918 | ||
|
|
0054100414 | ||
|
|
486f3887c2 | ||
|
|
37af369b43 | ||
|
|
827d24fde4 | ||
|
|
b8ea38b5f3 | ||
|
|
4530d8663d | ||
|
|
a4073d64cb | ||
|
|
16400df182 | ||
|
|
47af17bebc | ||
|
|
814f23a27c | ||
|
|
6bbe137f09 | ||
|
|
6fdcf5c7c9 | ||
|
|
ce1712ff2e | ||
|
|
8b09824e04 | ||
|
|
9fb23476ac | ||
|
|
971c263bf8 | ||
|
|
cb3fd3e646 | ||
|
|
d414557d74 | ||
|
|
fbcd95b617 | ||
|
|
887f29096f | ||
|
|
e0bddc2faa | ||
|
|
4f770a1eea | ||
|
|
376dc97134 | ||
|
|
27c8da9a75 | ||
|
|
5963dfa01a | ||
|
|
f86cba6ce5 | ||
|
|
e1b82519b9 | ||
|
|
32d8a62fec | ||
|
|
4cd622ff07 | ||
|
|
6ec4361f43 | ||
|
|
e9bed3688f | ||
|
|
8d5366444b | ||
|
|
4e32b05124 | ||
|
|
e0055bf491 | ||
|
|
d60ddb21dc | ||
|
|
219d1ccd08 | ||
|
|
6f697e56c8 | ||
|
|
05535b9bb2 | ||
|
|
5ce2cb658c | ||
|
|
a86df52d9a | ||
|
|
275c2ca343 | ||
|
|
5d871e8b21 | ||
|
|
694afdd07f | ||
|
|
d31e70515a | ||
|
|
701b141347 | ||
|
|
eaa114f40c | ||
|
|
563c808836 | ||
|
|
79b6683817 | ||
|
|
8bd16df6f8 | ||
|
|
fdea52d59d | ||
|
|
4031abd8d6 | ||
|
|
cf2bfaf6a7 | ||
|
|
d35021ee5d | ||
|
|
57d1597264 | ||
|
|
aca847e76c | ||
|
|
8036c164b4 | ||
|
|
0482987bf1 | ||
|
|
50dd419e5a | ||
|
|
213359b6c8 | ||
|
|
63c2bb4ca2 | ||
|
|
6693020e6d | ||
|
|
05c5f45995 | ||
|
|
1ded7a8a66 | ||
|
|
809d0ca65c | ||
|
|
139064de35 | ||
|
|
54e0cbc84e | ||
|
|
7c9166a8cf | ||
|
|
a9a16f2218 | ||
|
|
c9ae9dd893 | ||
|
|
010d50b727 | ||
|
|
c88fb03243 | ||
|
|
7bae9b633c | ||
|
|
fc3f1d6aac | ||
|
|
b719be3e38 | ||
|
|
db2410cc7f | ||
|
|
d9c2854911 | ||
|
|
99c051a874 | ||
|
|
a0ad151fbc | ||
|
|
4806a5e0a9 | ||
|
|
e60152d38b | ||
|
|
f6e97a06f5 | ||
|
|
618599266e | ||
|
|
696c1b4bb2 | ||
|
|
b346af35d3 | ||
|
|
16a65289f2 | ||
|
|
bb2691fe8f | ||
|
|
7753c83e27 | ||
|
|
1d6fb2e3c5 | ||
|
|
6c1bff5d0c | ||
|
|
f1e362a820 | ||
|
|
e2c19d550c | ||
|
|
c101ef1c80 | ||
|
|
4b7be20fe0 | ||
|
|
5c98aea7e3 | ||
|
|
da61644dbc | ||
|
|
5de7ecd4c5 | ||
|
|
3fc6b055a0 | ||
|
|
4b65eebd5d | ||
|
|
921c00c472 | ||
|
|
b6bd76534f | ||
|
|
780cfb4cca | ||
|
|
8f9272d985 | ||
|
|
f679647ec9 |
146
.github/workflows/build.yml
vendored
146
.github/workflows/build.yml
vendored
@@ -34,13 +34,11 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: web/package-lock.json
|
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
working-directory: web
|
working-directory: web
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Upload frontend dist
|
- name: Upload frontend dist
|
||||||
@@ -66,8 +64,16 @@ jobs:
|
|||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
# The Docker workflow builds the same targets with the same Cross images.
|
||||||
|
shared-key: linux-cross-release
|
||||||
|
key: ${{ hashFiles('Cross.toml', 'build/cross/Dockerfile.*') }}
|
||||||
|
cache-all-crates: true
|
||||||
|
|
||||||
- name: Install cross
|
- name: Install cross
|
||||||
run: cargo install cross --locked
|
run: cargo install cross --version 0.2.5 --locked
|
||||||
|
|
||||||
- name: Build linux binary
|
- name: Build linux binary
|
||||||
run: bash build/build-images.sh
|
run: bash build/build-images.sh
|
||||||
@@ -127,6 +133,12 @@ jobs:
|
|||||||
"TURBOJPEG_LIB_DIR=$env:TURBOJPEG_LIB_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
"TURBOJPEG_LIB_DIR=$env:TURBOJPEG_LIB_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
"TURBOJPEG_INCLUDE_DIR=$env:TURBOJPEG_INCLUDE_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
"TURBOJPEG_INCLUDE_DIR=$env:TURBOJPEG_INCLUDE_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
shared-key: windows-msvc-release
|
||||||
|
key: ${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }}
|
||||||
|
|
||||||
- name: Build Windows exe
|
- name: Build Windows exe
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: .\build\windows\build.ps1 -Configuration release -Package
|
run: .\build\windows\build.ps1 -Configuration release -Package
|
||||||
@@ -139,126 +151,9 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
android:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
needs: frontend
|
|
||||||
timeout-minutes: 240
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download frontend dist
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: web-dist
|
|
||||||
path: web/dist
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Cache Android Docker layers
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache/android
|
|
||||||
key: android-buildx-${{ runner.os }}-${{ hashFiles('build/cross/Dockerfile.android') }}
|
|
||||||
restore-keys: |
|
|
||||||
android-buildx-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Build Android Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: build/cross
|
|
||||||
file: build/cross/Dockerfile.android
|
|
||||||
tags: one-kvm-android-build:ci
|
|
||||||
load: true
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache/android
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache/android-new,mode=max
|
|
||||||
|
|
||||||
- name: Rotate Android Docker layer cache
|
|
||||||
run: |
|
|
||||||
rm -rf /tmp/.buildx-cache/android
|
|
||||||
mv /tmp/.buildx-cache/android-new /tmp/.buildx-cache/android
|
|
||||||
|
|
||||||
- name: Cache Android build dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
.github/android-cache/gradle
|
|
||||||
.github/android-cache/cargo-registry
|
|
||||||
.github/android-cache/cargo-git
|
|
||||||
.tmp/android-ffmpeg-check
|
|
||||||
.tmp/android-turbojpeg-src
|
|
||||||
.tmp/android-libyuv-src
|
|
||||||
.tmp/android-alsa-src
|
|
||||||
.tmp/android-opus-src
|
|
||||||
dist/android-ffmpeg-mediacodec
|
|
||||||
dist/android-turbojpeg
|
|
||||||
dist/android-libyuv
|
|
||||||
dist/android-alsa
|
|
||||||
dist/android-opus
|
|
||||||
key: android-deps-${{ runner.os }}-${{ hashFiles('android/**/*.gradle.kts', 'android/gradle/wrapper/gradle-wrapper.properties', 'android/native/Cargo.lock', 'Cargo.lock', 'scripts/build-android-*.sh') }}
|
|
||||||
restore-keys: |
|
|
||||||
android-deps-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Prepare Android FFmpeg source
|
|
||||||
run: |
|
|
||||||
chmod +x android/gradlew
|
|
||||||
if [ ! -x .tmp/android-ffmpeg-check/src/ffmpeg-rockchip/configure ]; then
|
|
||||||
rm -rf .tmp/android-ffmpeg-check/src
|
|
||||||
mkdir -p .tmp/android-ffmpeg-check/src
|
|
||||||
wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz -O .tmp/android-ffmpeg-check/ffmpeg.tar.gz
|
|
||||||
tar -xzf .tmp/android-ffmpeg-check/ffmpeg.tar.gz -C .tmp/android-ffmpeg-check/src --strip-components=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build Android APK
|
|
||||||
env:
|
|
||||||
ONE_KVM_ANDROID_DOCKER_IMAGE: one-kvm-android-build:ci
|
|
||||||
ONE_KVM_ANDROID_SKIP_DOCKER_BUILD: "1"
|
|
||||||
ONE_KVM_ANDROID_GRADLE_CACHE_DIR: ${{ github.workspace }}/.github/android-cache/gradle
|
|
||||||
ONE_KVM_ANDROID_CARGO_REGISTRY_CACHE_DIR: ${{ github.workspace }}/.github/android-cache/cargo-registry
|
|
||||||
ONE_KVM_ANDROID_CARGO_GIT_CACHE_DIR: ${{ github.workspace }}/.github/android-cache/cargo-git
|
|
||||||
run: bash build/build-android.sh all
|
|
||||||
|
|
||||||
- name: Fix Android build permissions
|
|
||||||
if: ${{ always() }}
|
|
||||||
run: |
|
|
||||||
paths=(
|
|
||||||
.github/android-cache
|
|
||||||
.tmp/android-ffmpeg-check
|
|
||||||
.tmp/android-turbojpeg-src
|
|
||||||
.tmp/android-libyuv-src
|
|
||||||
.tmp/android-alsa-src
|
|
||||||
.tmp/android-opus-src
|
|
||||||
dist/android-ffmpeg-mediacodec
|
|
||||||
dist/android-turbojpeg
|
|
||||||
dist/android-libyuv
|
|
||||||
dist/android-alsa
|
|
||||||
dist/android-opus
|
|
||||||
target/android
|
|
||||||
android/.gradle
|
|
||||||
android/app/build
|
|
||||||
android/native/target
|
|
||||||
)
|
|
||||||
existing=()
|
|
||||||
for path in "${paths[@]}"; do
|
|
||||||
if [ -e "$path" ]; then
|
|
||||||
existing+=("$path")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "${#existing[@]}" -gt 0 ]; then
|
|
||||||
sudo chown -R "$USER:$USER" "${existing[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload Android APK
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: one-kvm-android-apk
|
|
||||||
path: target/android/one-kvm_*.apk
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: [deb, windows, android]
|
needs: [deb, windows]
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release }}
|
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release }}
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
permissions:
|
permissions:
|
||||||
@@ -283,12 +178,6 @@ jobs:
|
|||||||
name: one-kvm-windows-exe
|
name: one-kvm-windows-exe
|
||||||
path: release-artifacts/windows
|
path: release-artifacts/windows
|
||||||
|
|
||||||
- name: Download Android artifact
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: one-kvm-android-apk
|
|
||||||
path: release-artifacts/android
|
|
||||||
|
|
||||||
- name: Publish GitHub Release
|
- name: Publish GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
@@ -298,4 +187,3 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
release-artifacts/deb/*.deb
|
release-artifacts/deb/*.deb
|
||||||
release-artifacts/windows/*.exe
|
release-artifacts/windows/*.exe
|
||||||
release-artifacts/android/*.apk
|
|
||||||
|
|||||||
91
.github/workflows/docker.yml
vendored
Normal file
91
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
# Reuse dependency artifacts produced by the deb job and vice versa.
|
||||||
|
shared-key: linux-cross-release
|
||||||
|
key: ${{ hashFiles('Cross.toml', 'build/cross/Dockerfile.*') }}
|
||||||
|
cache-all-crates: true
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y unzip xz-utils
|
||||||
|
cargo install cross --version 0.2.5 --locked
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
working-directory: web
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
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
|
||||||
109
Cargo.toml
109
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "one-kvm"
|
name = "one-kvm"
|
||||||
version = "0.2.2"
|
version = "0.2.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["SilentWind"]
|
authors = ["SilentWind"]
|
||||||
description = "A open and lightweight IP-KVM solution written in Rust"
|
description = "A open and lightweight IP-KVM solution written in Rust"
|
||||||
@@ -20,12 +20,14 @@ desktop = [
|
|||||||
"dep:sqlx",
|
"dep:sqlx",
|
||||||
"dep:serde",
|
"dep:serde",
|
||||||
"dep:serde_json",
|
"dep:serde_json",
|
||||||
|
"dep:toml_edit",
|
||||||
"dep:tracing",
|
"dep:tracing",
|
||||||
"dep:tracing-subscriber",
|
"dep:tracing-subscriber",
|
||||||
"dep:thiserror",
|
"dep:thiserror",
|
||||||
"dep:anyhow",
|
"dep:anyhow",
|
||||||
"dep:argon2",
|
"dep:argon2",
|
||||||
"dep:rand",
|
"dep:rand",
|
||||||
|
"dep:totp-rs",
|
||||||
"dep:uuid",
|
"dep:uuid",
|
||||||
"dep:base64",
|
"dep:base64",
|
||||||
"dep:nix",
|
"dep:nix",
|
||||||
@@ -38,6 +40,7 @@ desktop = [
|
|||||||
"dep:axum-server",
|
"dep:axum-server",
|
||||||
"dep:clap",
|
"dep:clap",
|
||||||
"dep:time",
|
"dep:time",
|
||||||
|
"dep:tempfile",
|
||||||
"dep:bytes",
|
"dep:bytes",
|
||||||
"dep:bytemuck",
|
"dep:bytemuck",
|
||||||
"dep:xxhash-rust",
|
"dep:xxhash-rust",
|
||||||
@@ -53,9 +56,11 @@ desktop = [
|
|||||||
"dep:serialport",
|
"dep:serialport",
|
||||||
"dep:async-trait",
|
"dep:async-trait",
|
||||||
"dep:libc",
|
"dep:libc",
|
||||||
|
"dep:libloading",
|
||||||
"dep:ventoy-img",
|
"dep:ventoy-img",
|
||||||
"dep:protobuf",
|
"dep:protobuf",
|
||||||
"dep:sodiumoxide",
|
"dep:sodiumoxide",
|
||||||
|
"dep:des",
|
||||||
"dep:sha2",
|
"dep:sha2",
|
||||||
"dep:typeshare",
|
"dep:typeshare",
|
||||||
"dep:hwcodec",
|
"dep:hwcodec",
|
||||||
@@ -65,71 +70,13 @@ desktop = [
|
|||||||
"dep:v4l2r",
|
"dep:v4l2r",
|
||||||
"dep:alsa",
|
"dep:alsa",
|
||||||
"dep:gpio-cdev",
|
"dep:gpio-cdev",
|
||||||
|
"dep:one-kvm-bluetooth-hid",
|
||||||
"dep:cpal",
|
"dep:cpal",
|
||||||
"dep:windows-sys",
|
"dep:windows-sys",
|
||||||
]
|
]
|
||||||
android = [
|
|
||||||
"dep:anyhow",
|
|
||||||
"dep:argon2",
|
|
||||||
"dep:arc-swap",
|
|
||||||
"dep:async-stream",
|
|
||||||
"dep:async-trait",
|
|
||||||
"dep:axum",
|
|
||||||
"dep:axum-extra",
|
|
||||||
"dep:base64",
|
|
||||||
"dep:bytemuck",
|
|
||||||
"dep:bytes",
|
|
||||||
"dep:futures",
|
|
||||||
"dep:gpio-cdev",
|
|
||||||
"dep:hwcodec",
|
|
||||||
"dep:libc",
|
|
||||||
"dep:libyuv",
|
|
||||||
"dep:mime_guess",
|
|
||||||
"dep:nix",
|
|
||||||
"dep:parking_lot",
|
|
||||||
"dep:protobuf",
|
|
||||||
"dep:rand",
|
|
||||||
"dep:rcgen",
|
|
||||||
"dep:reqwest",
|
|
||||||
"dep:rtp",
|
|
||||||
"dep:rtsp-types",
|
|
||||||
"dep:rust-embed",
|
|
||||||
"dep:rustls",
|
|
||||||
"dep:sdp-types",
|
|
||||||
"dep:serde",
|
|
||||||
"dep:serde_json",
|
|
||||||
"dep:serialport",
|
|
||||||
"dep:sha2",
|
|
||||||
"dep:sodiumoxide",
|
|
||||||
"dep:sqlx",
|
|
||||||
"dep:alsa",
|
|
||||||
"dep:audiopus",
|
|
||||||
"dep:thiserror",
|
|
||||||
"dep:time",
|
|
||||||
"dep:tokio",
|
|
||||||
"dep:tokio-tungstenite",
|
|
||||||
"dep:tokio-util",
|
|
||||||
"dep:axum-server",
|
|
||||||
"dep:tower-http",
|
|
||||||
"dep:tracing",
|
|
||||||
"dep:tracing-log",
|
|
||||||
"dep:tracing-subscriber",
|
|
||||||
"dep:turbojpeg",
|
|
||||||
"dep:typeshare",
|
|
||||||
"dep:urlencoding",
|
|
||||||
"dep:uuid",
|
|
||||||
"dep:ventoy-img",
|
|
||||||
"dep:v4l2r",
|
|
||||||
"dep:webrtc",
|
|
||||||
"dep:xxhash-rust",
|
|
||||||
]
|
|
||||||
android-mediacodec = [
|
|
||||||
"android",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1", features = ["full"], optional = true }
|
tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"], optional = true }
|
||||||
tokio-util = { version = "0.7", features = ["rt"], optional = true }
|
tokio-util = { version = "0.7", features = ["rt"], optional = true }
|
||||||
|
|
||||||
# Web framework
|
# Web framework
|
||||||
@@ -138,15 +85,15 @@ axum-extra = { version = "0.12", features = ["cookie"], optional = true }
|
|||||||
tower-http = { version = "0.6", features = ["cors", "trace", "set-header"], optional = true }
|
tower-http = { version = "0.6", features = ["cors", "trace", "set-header"], optional = true }
|
||||||
|
|
||||||
# Database - Use bundled SQLite for static linking
|
# Database - Use bundled SQLite for static linking
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"], optional = true }
|
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "sqlite-bundled"], optional = true }
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
|
toml_edit = { version = "0.25", optional = true }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
tracing = { version = "0.1", optional = true }
|
tracing = { version = "0.1", optional = true }
|
||||||
tracing-log = { version = "0.2", optional = true }
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "tracing-log"], optional = true }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "tracing-log"], optional = true }
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
@@ -155,15 +102,17 @@ anyhow = { version = "1", optional = true }
|
|||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
argon2 = { version = "0.5", optional = true }
|
argon2 = { version = "0.5", optional = true }
|
||||||
rand = { version = "0.9", optional = true }
|
rand = { version = "0.10", optional = true }
|
||||||
|
totp-rs = { version = "5.7", features = ["gen_secret", "otpauth", "zeroize"], optional = true }
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
uuid = { version = "1", features = ["v4", "serde"], optional = true }
|
uuid = { version = "1", features = ["v4", "serde"], optional = true }
|
||||||
base64 = { version = "0.22", optional = true }
|
base64 = { version = "0.22", optional = true }
|
||||||
|
tempfile = { version = "3", optional = true }
|
||||||
|
|
||||||
# HTTP client (for URL downloads)
|
# HTTP client (for URL downloads)
|
||||||
# Use rustls by default, but allow native-tls for systems with older GLIBC
|
# Use rustls by default, but allow native-tls for systems with older GLIBC
|
||||||
reqwest = { version = "0.13", features = ["stream", "rustls", "json"], default-features = false, optional = true }
|
reqwest = { version = "0.13", features = ["stream", "rustls-no-provider", "json"], default-features = false, optional = true }
|
||||||
urlencoding = { version = "2", optional = true }
|
urlencoding = { version = "2", optional = true }
|
||||||
|
|
||||||
# Static file embedding
|
# Static file embedding
|
||||||
@@ -171,9 +120,9 @@ rust-embed = { version = "8", features = ["compression", "debug-embed"], optiona
|
|||||||
mime_guess = { version = "2", optional = true }
|
mime_guess = { version = "2", optional = true }
|
||||||
|
|
||||||
# TLS/HTTPS
|
# TLS/HTTPS
|
||||||
rustls = { version = "0.23", features = ["ring"], optional = true }
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"], optional = true }
|
||||||
rcgen = { version = "0.14", optional = true }
|
rcgen = { version = "0.14", optional = true }
|
||||||
axum-server = { version = "0.8", features = ["tls-rustls"], optional = true }
|
axum-server = { version = "0.8", features = ["tls-rustls-no-provider"], optional = true }
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
clap = { version = "4", features = ["derive"], optional = true }
|
clap = { version = "4", features = ["derive"], optional = true }
|
||||||
@@ -193,15 +142,15 @@ async-stream = { version = "0.3", optional = true }
|
|||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
|
|
||||||
# WebSocket client (for ttyd proxy)
|
# WebSocket client (for ttyd proxy)
|
||||||
tokio-tungstenite = { version = "0.28", optional = true }
|
tokio-tungstenite = { version = "0.29", optional = true }
|
||||||
|
|
||||||
# High-performance synchronization
|
# High-performance synchronization
|
||||||
parking_lot = { version = "0.12", optional = true }
|
parking_lot = { version = "0.12", optional = true }
|
||||||
arc-swap = { version = "1.8", optional = true }
|
arc-swap = { version = "1.8", optional = true }
|
||||||
|
|
||||||
# WebRTC
|
# WebRTC
|
||||||
webrtc = { version = "0.14", optional = true }
|
webrtc = { version = "0.17", optional = true }
|
||||||
rtp = { version = "0.14", optional = true }
|
rtp = { version = "0.17", optional = true }
|
||||||
rtsp-types = { version = "0.1", optional = true }
|
rtsp-types = { version = "0.1", optional = true }
|
||||||
sdp-types = { version = "0.1", optional = true }
|
sdp-types = { version = "0.1", optional = true }
|
||||||
|
|
||||||
@@ -209,6 +158,7 @@ sdp-types = { version = "0.1", optional = true }
|
|||||||
serialport = { version = "4", optional = true }
|
serialport = { version = "4", optional = true }
|
||||||
async-trait = { version = "0.1", optional = true }
|
async-trait = { version = "0.1", optional = true }
|
||||||
libc = { version = "0.2", optional = true }
|
libc = { version = "0.2", optional = true }
|
||||||
|
libloading = { version = "0.8", optional = true }
|
||||||
|
|
||||||
# Ventoy bootable image support
|
# Ventoy bootable image support
|
||||||
ventoy-img = { path = "libs/ventoy-img-rs", optional = true }
|
ventoy-img = { path = "libs/ventoy-img-rs", optional = true }
|
||||||
@@ -216,11 +166,12 @@ ventoy-img = { path = "libs/ventoy-img-rs", optional = true }
|
|||||||
# RustDesk protocol support
|
# RustDesk protocol support
|
||||||
protobuf = { version = "3.7", features = ["with-bytes"], optional = true }
|
protobuf = { version = "3.7", features = ["with-bytes"], optional = true }
|
||||||
sodiumoxide = { version = "0.2", optional = true }
|
sodiumoxide = { version = "0.2", optional = true }
|
||||||
sha2 = { version = "0.10", optional = true }
|
des = { version = "0.9", optional = true }
|
||||||
|
sha2 = { version = "0.11", optional = true }
|
||||||
# TypeScript type generation
|
# TypeScript type generation
|
||||||
typeshare = { version = "1.0", optional = true }
|
typeshare = { version = "1.0", optional = true }
|
||||||
|
|
||||||
[target.'cfg(any(unix, windows))'.dependencies]
|
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
||||||
# Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux)
|
# Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux)
|
||||||
hwcodec = { path = "libs/hwcodec", features = ["bytes"], optional = true }
|
hwcodec = { path = "libs/hwcodec", features = ["bytes"], optional = true }
|
||||||
libyuv = { path = "res/vcpkg/libyuv", optional = true }
|
libyuv = { path = "res/vcpkg/libyuv", optional = true }
|
||||||
@@ -228,15 +179,12 @@ turbojpeg = { version = "1.3", optional = true }
|
|||||||
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
||||||
audiopus = { version = "0.2", optional = true }
|
audiopus = { version = "0.2", optional = true }
|
||||||
|
|
||||||
[target.'cfg(all(unix, not(target_os = "android")))'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
# Utilities
|
one-kvm-bluetooth-hid = { path = "libs/bluetooth-hid", optional = true }
|
||||||
nix = { version = "0.30", default-features = false, features = ["fs", "socket", "net", "hostname", "poll"], optional = true }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
|
||||||
# Utilities
|
# Utilities
|
||||||
nix = { version = "0.30", default-features = false, features = ["fs", "socket", "hostname", "poll"], optional = true }
|
nix = { version = "0.31", default-features = false, features = ["fs", "socket", "net", "hostname", "poll"], optional = true }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
|
||||||
# Video capture (V4L2)
|
# Video capture (V4L2)
|
||||||
v4l2r = { path = "libs/v4l2r", optional = true }
|
v4l2r = { path = "libs/v4l2r", optional = true }
|
||||||
|
|
||||||
@@ -247,7 +195,7 @@ alsa = { version = "0.11", optional = true }
|
|||||||
gpio-cdev = { version = "0.6", optional = true }
|
gpio-cdev = { version = "0.6", optional = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
cpal = { version = "0.17", default-features = false, optional = true }
|
cpal = { version = "0.18", default-features = false, optional = true }
|
||||||
windows-sys = { version = "0.61", features = [
|
windows-sys = { version = "0.61", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_NetworkManagement_IpHelper",
|
"Win32_NetworkManagement_IpHelper",
|
||||||
@@ -257,9 +205,6 @@ windows-sys = { version = "0.61", features = [
|
|||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
protobuf-codegen = "3.7"
|
protobuf-codegen = "3.7"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Cross-compilation configuration for One-KVM
|
# Cross-compilation configuration for One-KVM
|
||||||
# Uses pre-built custom Debian 11 based images for consistent build/runtime environment
|
# Uses pre-built custom Debian 12 based images for consistent build/runtime environment
|
||||||
# See: https://github.com/cross-rs/cross
|
# See: https://github.com/cross-rs/cross
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
|
|||||||
27
README.en.md
27
README.en.md
@@ -22,6 +22,8 @@ Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
|||||||
- **Lightweight**: shipped as a binary with minimal moving parts for deployment.
|
- **Lightweight**: shipped as a binary with minimal moving parts for deployment.
|
||||||
- **Easy to use**: no hand-edited config files required; settings are done in the web UI.
|
- **Easy to use**: no hand-edited config files required; settings are done in the web UI.
|
||||||
|
|
||||||
|
For more information, see the [One-KVM Rust documentation](https://docs.one-kvm.cn/).
|
||||||
|
|
||||||
> **One-KVM (Python)** is no longer maintained. If you still need it, see <https://github.com/mofeng-git/One-KVM/tree/python>.
|
> **One-KVM (Python)** is no longer maintained. If you still need it, see <https://github.com/mofeng-git/One-KVM/tree/python>.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -32,7 +34,7 @@ Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Core
|
**Core**
|
||||||
|
|
||||||
| Area | Capabilities |
|
| Area | Capabilities |
|
||||||
|------|----------------|
|
|------|----------------|
|
||||||
@@ -40,11 +42,9 @@ Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
|||||||
| Video encoding | VAAPI / QSV / RKMPP / V4L2 M2M hardware paths, with software fallback |
|
| Video encoding | VAAPI / QSV / RKMPP / V4L2 M2M hardware paths, with software fallback |
|
||||||
| Keyboard & mouse | USB OTG HID or CH340 + CH9329 HID; absolute / relative mouse |
|
| Keyboard & mouse | USB OTG HID or CH340 + CH9329 HID; absolute / relative mouse |
|
||||||
| Virtual media | USB mass storage; ISO/IMG mount and Ventoy-style virtual USB |
|
| Virtual media | USB mass storage; ISO/IMG mount and Ventoy-style virtual USB |
|
||||||
| ATX power | GPIO or USB relay; power and reset control |
|
| ATX power | GPIO or USB relay; power control |
|
||||||
| Audio | ALSA capture + Opus (HTTP / WebRTC) |
|
| Audio | ALSA capture + Opus (HTTP / WebRTC) |
|
||||||
|
|
||||||
The web UI supports visual configuration and Chinese/English locales. Built-ins include a web terminal (ttyd), intranet tunnel (gostc), P2P (EasyTier), RustDesk protocol (optional cross-platform remote access), and RTSP streaming.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Release artifacts are on [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases). Below are short paths for common setups. For **system requirements, hardware, Docker env vars, USB OTG**, and full troubleshooting, see the [One-KVM documentation](https://docs.one-kvm.cn/) (Chinese; use a translator if needed).
|
Release artifacts are on [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases). Below are short paths for common setups. For **system requirements, hardware, Docker env vars, USB OTG**, and full troubleshooting, see the [One-KVM documentation](https://docs.one-kvm.cn/) (Chinese; use a translator if needed).
|
||||||
@@ -216,15 +216,22 @@ One-KVM builds on many great open-source projects; a lot of time goes into testi
|
|||||||
|
|
||||||
- 爱发电用户_d3d9c
|
- 爱发电用户_d3d9c
|
||||||
|
|
||||||
|
- 爱发电用户_97b41
|
||||||
|
|
||||||
|
- 偶然
|
||||||
|
|
||||||
|
- 爱发电用户_dba45
|
||||||
|
|
||||||
|
- 爱发电用户_d4f8b
|
||||||
|
|
||||||
|
- 故人。
|
||||||
|
|
||||||
- ......
|
- ......
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Sponsors
|
### Sponsors
|
||||||
|
|
||||||
**Mirror Download Services:**
|
|
||||||
- **[Chongqing University Open Source Software Mirror](https://mirrors.cqu.edu.cn/)** — provides mirror download services
|
|
||||||
|
|
||||||
**File hosting**
|
**File hosting**
|
||||||
|
|
||||||
- **[Huang1111 public-interest program](https://pan.huang1111.cn/s/mxkx3T1)** — login-free downloads
|
- **[Huang1111 public-interest program](https://pan.huang1111.cn/s/mxkx3T1)** — login-free downloads
|
||||||
@@ -236,3 +243,9 @@ One-KVM builds on many great open-source projects; a lot of time goes into testi
|
|||||||

|

|
||||||
|
|
||||||
林枫云 offers premium network routes, high-frequency game servers, and high-bandwidth servers in China and abroad.
|
林枫云 offers premium network routes, high-frequency game servers, and high-bandwidth servers in China and abroad.
|
||||||
|
|
||||||
|
- **[Beta Network](https://my.beita.cc/?ref=github_onekvm)** — project server sponsorship
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Remote computers, consumer GPU servers, and dedicated physical machines with fully automated online delivery.
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 项目概述
|
## 项目概述
|
||||||
|
|
||||||
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
|
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
- **轻量**:以二进制文件形式分发,无繁杂的依赖项,部署过程简单。
|
- **轻量**:以二进制文件形式分发,无繁杂的依赖项,部署过程简单。
|
||||||
- **易用**:无需手动编辑配置文件,参数设置均可通过网页界面完成。
|
- **易用**:无需手动编辑配置文件,参数设置均可通过网页界面完成。
|
||||||
|
|
||||||
|
更多内容可访问 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
||||||
|
|
||||||
> **One-KVM Python** 已停止开发,如有需要可访问 <https://github.com/mofeng-git/One-KVM/tree/python>。
|
> **One-KVM Python** 已停止开发,如有需要可访问 <https://github.com/mofeng-git/One-KVM/tree/python>。
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -30,9 +32,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 📊 功能介绍
|
## 功能介绍
|
||||||
|
|
||||||
### 核心功能
|
**核心功能**
|
||||||
|
|
||||||
| 功能 | 能力说明 |
|
| 功能 | 能力说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -40,12 +42,10 @@
|
|||||||
| 视频编码 | VAAPI/QSV/RKMPP/V4L2M2M 硬件编码支持,以及软件编码兜底 |
|
| 视频编码 | VAAPI/QSV/RKMPP/V4L2M2M 硬件编码支持,以及软件编码兜底 |
|
||||||
| 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID,支持绝对/相对鼠标模式 |
|
| 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID,支持绝对/相对鼠标模式 |
|
||||||
| 虚拟媒体 | USB Mass Storage,支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
|
| 虚拟媒体 | USB Mass Storage,支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
|
||||||
| ATX 电源控制 | GPIO /USB 继电器,支持控制电源、重启按钮 |
|
| ATX 电源控制 | GPIO/USB 继电器,支持电源控制 |
|
||||||
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
|
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
|
||||||
|
|
||||||
此外提供基于 Web UI 的可视化配置与中英文界面;并集成 Web 终端(ttyd)、内网穿透(gostc)、P2P 组网(EasyTier)、RustDesk 协议(扩展跨平台远程访问)以及 RTSP 推流等能力。
|
## 安装使用
|
||||||
|
|
||||||
## ⚡ 安装使用
|
|
||||||
|
|
||||||
构建产物见 [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases)。以下为常见安装方式的简要步骤;**系统要求、硬件准备、Docker 环境变量与 USB OTG 等完整说明**请查阅 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
构建产物见 [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases)。以下为常见安装方式的简要步骤;**系统要求、硬件准备、Docker 环境变量与 USB OTG 等完整说明**请查阅 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
||||||
|
|
||||||
@@ -210,6 +210,16 @@ One-KVM 已上架飞牛 **应用市场**,在 NAS 上直接搜索安装即可
|
|||||||
|
|
||||||
- 爱发电用户_d3d9c
|
- 爱发电用户_d3d9c
|
||||||
|
|
||||||
|
- 爱发电用户_97b41
|
||||||
|
|
||||||
|
- 偶然
|
||||||
|
|
||||||
|
- 爱发电用户_dba45
|
||||||
|
|
||||||
|
- 爱发电用户_d4f8b
|
||||||
|
|
||||||
|
- 故人。
|
||||||
|
|
||||||
- ......
|
- ......
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -218,9 +228,6 @@ One-KVM 已上架飞牛 **应用市场**,在 NAS 上直接搜索安装即可
|
|||||||
|
|
||||||
本项目得到以下赞助商的支持:
|
本项目得到以下赞助商的支持:
|
||||||
|
|
||||||
**镜像下载服务:**
|
|
||||||
- **[重庆大学开源软件镜像站](https://mirrors.cqu.edu.cn/)** - 提供镜像站下载服务
|
|
||||||
|
|
||||||
**文件存储服务:**
|
**文件存储服务:**
|
||||||
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
|
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
|
||||||
|
|
||||||
|
|||||||
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
|
|
||||||
```
|
|
||||||
7
android/.gitignore
vendored
7
android/.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
.gradle/
|
|
||||||
.kotlin/
|
|
||||||
build/
|
|
||||||
local.properties
|
|
||||||
app/build/
|
|
||||||
app/src/main/jniLibs/
|
|
||||||
native/target/
|
|
||||||
@@ -1,571 +0,0 @@
|
|||||||
import org.gradle.api.tasks.Exec
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.Properties
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("com.android.application")
|
|
||||||
}
|
|
||||||
|
|
||||||
val androidNdkVersion = "27.3.13750724"
|
|
||||||
val androidApiLevel = 21
|
|
||||||
val nativeCrateDir = layout.projectDirectory.dir("../native")
|
|
||||||
val rootCrateDir = layout.projectDirectory.dir("../..")
|
|
||||||
val nativeCargoOutputDir = layout.buildDirectory.dir("generated/oneKvm/cargoJniLibs")
|
|
||||||
val nativeOutputRoot = layout.buildDirectory.dir("generated/oneKvm/jniLibs")
|
|
||||||
val nativeAssetRoot = layout.buildDirectory.dir("generated/oneKvm/assets")
|
|
||||||
val defaultAndroidFfmpegRoot = rootProject.layout.projectDirectory.dir("../dist/android-ffmpeg-mediacodec")
|
|
||||||
val defaultAndroidLibyuvRoot = rootProject.layout.projectDirectory.dir("../dist/android-libyuv")
|
|
||||||
val defaultAndroidTurbojpegRoot = rootProject.layout.projectDirectory.dir("../dist/android-turbojpeg")
|
|
||||||
val defaultAndroidAlsaRoot = rootProject.layout.projectDirectory.dir("../dist/android-alsa")
|
|
||||||
val defaultAndroidOpusRoot = rootProject.layout.projectDirectory.dir("../dist/android-opus")
|
|
||||||
val androidFfmpegRoot = providers.environmentVariable("ONE_KVM_ANDROID_FFMPEG_ROOT")
|
|
||||||
.orElse(defaultAndroidFfmpegRoot.asFile.absolutePath)
|
|
||||||
val androidLibyuvRoot = providers.environmentVariable("ONE_KVM_ANDROID_LIBYUV_ROOT")
|
|
||||||
.orElse(defaultAndroidLibyuvRoot.asFile.absolutePath)
|
|
||||||
val androidTurbojpegRoot = providers.environmentVariable("ONE_KVM_ANDROID_TURBOJPEG_ROOT")
|
|
||||||
.orElse(defaultAndroidTurbojpegRoot.asFile.absolutePath)
|
|
||||||
val androidAlsaRoot = providers.environmentVariable("ONE_KVM_ANDROID_ALSA_ROOT")
|
|
||||||
.orElse(defaultAndroidAlsaRoot.asFile.absolutePath)
|
|
||||||
val androidOpusRoot = providers.environmentVariable("ONE_KVM_ANDROID_OPUS_ROOT")
|
|
||||||
.orElse(defaultAndroidOpusRoot.asFile.absolutePath)
|
|
||||||
val selectedAndroidAbis = providers.environmentVariable("ONE_KVM_ANDROID_ABIS")
|
|
||||||
.orElse("arm64-v8a,armeabi-v7a")
|
|
||||||
.get()
|
|
||||||
.split(',', ' ', ';')
|
|
||||||
.map { it.trim() }
|
|
||||||
.filter { it.isNotEmpty() }
|
|
||||||
.distinct()
|
|
||||||
val androidBuildProfile = providers.environmentVariable("ONE_KVM_ANDROID_PROFILE")
|
|
||||||
.orElse("debug")
|
|
||||||
.get()
|
|
||||||
.lowercase()
|
|
||||||
val oneKvmVersion = Regex("""(?m)^version\s*=\s*"([^"]+)"""")
|
|
||||||
.find(rootCrateDir.file("Cargo.toml").asFile.readText())
|
|
||||||
?.groupValues
|
|
||||||
?.get(1)
|
|
||||||
?: throw GradleException("Failed to resolve version from root Cargo.toml")
|
|
||||||
val 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()) {
|
|
||||||
file.inputStream().use { load(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val androidSdkDir = file(
|
|
||||||
providers.environmentVariable("ANDROID_HOME")
|
|
||||||
.orElse(providers.environmentVariable("ANDROID_SDK_ROOT"))
|
|
||||||
.orElse(localProperties.getProperty("sdk.dir") ?: "/root/android-sdk")
|
|
||||||
.get(),
|
|
||||||
)
|
|
||||||
val androidNdkDir = androidSdkDir.resolve("ndk/$androidNdkVersion")
|
|
||||||
|
|
||||||
val androidFfmpegBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-ffmpeg-mediacodec.sh")
|
|
||||||
val androidLibyuvBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-libyuv.sh")
|
|
||||||
val androidTurbojpegBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-turbojpeg.sh")
|
|
||||||
val androidAlsaBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-alsa.sh")
|
|
||||||
val androidOpusBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-opus.sh")
|
|
||||||
|
|
||||||
val androidAbiTargets = mapOf(
|
|
||||||
"arm64-v8a" to Triple("arm64", "aarch64-linux-android", "aarch64-linux-android"),
|
|
||||||
"armeabi-v7a" to Triple("arm32", "armv7-linux-androideabi", "arm-linux-androideabi"),
|
|
||||||
)
|
|
||||||
|
|
||||||
val selectedAndroidAbiTargets = selectedAndroidAbis.associateWith { abi ->
|
|
||||||
androidAbiTargets[abi] ?: throw GradleException(
|
|
||||||
"Unsupported ONE_KVM_ANDROID_ABIS entry: $abi. Supported values: ${androidAbiTargets.keys.joinToString(", ")}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (androidBuildProfile != "debug" && androidBuildProfile != "release") {
|
|
||||||
throw GradleException("Unsupported ONE_KVM_ANDROID_PROFILE: $androidBuildProfile. Use debug or release.")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidFfmpegBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidFfmpegRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/libavcodec/avcodec.h",
|
|
||||||
"lib/libavcodec.a",
|
|
||||||
"lib/libavutil.a",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
fun androidLibyuvBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
val turbojpegScriptDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(androidTurbojpegBuildScript.asFile.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest;turbojpegScript=$turbojpegScriptDigest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidLibyuvRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/libyuv.h",
|
|
||||||
"lib/libyuv.a",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
fun androidTurbojpegBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidAlsaBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidOpusBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidTurbojpegRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/turbojpeg.h",
|
|
||||||
"include/jpeglib.h",
|
|
||||||
"lib/libjpeg.a",
|
|
||||||
"lib/libturbojpeg.a",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
fun androidAlsaRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/alsa/asoundlib.h",
|
|
||||||
"lib/libasound.so",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
fun androidOpusRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/opus/opus.h",
|
|
||||||
"lib/libopus.so",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "cn.one_kvm.androidhost"
|
|
||||||
compileSdk = 36
|
|
||||||
ndkVersion = androidNdkVersion
|
|
||||||
flavorDimensions += "abi"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "cn.one_kvm.androidhost"
|
|
||||||
minSdk = androidApiLevel
|
|
||||||
targetSdk = 36
|
|
||||||
versionCode = 1
|
|
||||||
versionName = oneKvmVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
create("arm32") {
|
|
||||||
dimension = "abi"
|
|
||||||
ndk {
|
|
||||||
abiFilters += "armeabi-v7a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
create("arm64") {
|
|
||||||
dimension = "abi"
|
|
||||||
ndk {
|
|
||||||
abiFilters += "arm64-v8a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
getByName("main") {
|
|
||||||
assets.directories.clear()
|
|
||||||
jniLibs.directories.clear()
|
|
||||||
}
|
|
||||||
getByName("arm32") {
|
|
||||||
assets.directories.add("build/generated/oneKvm/assets/arm32")
|
|
||||||
jniLibs.directories.add("build/generated/oneKvm/jniLibs/arm32")
|
|
||||||
}
|
|
||||||
getByName("arm64") {
|
|
||||||
assets.directories.add("build/generated/oneKvm/assets/arm64")
|
|
||||||
jniLibs.directories.add("build/generated/oneKvm/jniLibs/arm64")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidFfmpegMediaCodec") {
|
|
||||||
description = "Builds the default Android FFmpeg MediaCodec static libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val ffmpegRoot = file(androidFfmpegRoot.get())
|
|
||||||
val sourceDir = androidFfmpegSourceDir.asFile
|
|
||||||
val scriptFile = androidFfmpegBuildScript.asFile
|
|
||||||
val stampFile = ffmpegRoot.resolve(".one-kvm-android-ffmpeg.stamp")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--source",
|
|
||||||
sourceDir.absolutePath,
|
|
||||||
"--output",
|
|
||||||
ffmpegRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.dir(sourceDir)
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(ffmpegRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidFfmpegBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidLibyuv") {
|
|
||||||
description = "Builds Android libyuv static libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val libyuvRoot = file(androidLibyuvRoot.get())
|
|
||||||
val turbojpegRoot = file(androidTurbojpegRoot.get())
|
|
||||||
val scriptFile = androidLibyuvBuildScript.asFile
|
|
||||||
val stampFile = libyuvRoot.resolve(".one-kvm-android-libyuv.stamp")
|
|
||||||
|
|
||||||
dependsOn("buildAndroidTurbojpeg")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--output",
|
|
||||||
libyuvRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
"--jpeg-root",
|
|
||||||
turbojpegRoot.absolutePath,
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(libyuvRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
val hasAndroidLibyuv = androidLibyuvRequiredFiles(libyuvRoot).all { it.exists() }
|
|
||||||
val hasCurrentBuildStamp =
|
|
||||||
stampFile.exists() && stampFile.readText() == androidLibyuvBuildStamp(scriptFile)
|
|
||||||
!hasAndroidLibyuv || !hasCurrentBuildStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidLibyuvBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidTurbojpeg") {
|
|
||||||
description = "Builds Android TurboJPEG static libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val turbojpegRoot = file(androidTurbojpegRoot.get())
|
|
||||||
val scriptFile = androidTurbojpegBuildScript.asFile
|
|
||||||
val stampFile = turbojpegRoot.resolve(".one-kvm-android-turbojpeg.stamp")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--output",
|
|
||||||
turbojpegRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(turbojpegRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
val hasAndroidTurbojpeg = androidTurbojpegRequiredFiles(turbojpegRoot).all { it.exists() }
|
|
||||||
val hasCurrentBuildStamp =
|
|
||||||
stampFile.exists() && stampFile.readText() == androidTurbojpegBuildStamp(scriptFile)
|
|
||||||
!hasAndroidTurbojpeg || !hasCurrentBuildStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidTurbojpegBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidAlsa") {
|
|
||||||
description = "Builds Android ALSA shared libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val alsaRoot = file(androidAlsaRoot.get())
|
|
||||||
val scriptFile = androidAlsaBuildScript.asFile
|
|
||||||
val stampFile = alsaRoot.resolve(".one-kvm-android-alsa.stamp")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--output",
|
|
||||||
alsaRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(alsaRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
val hasAndroidAlsa = androidAlsaRequiredFiles(alsaRoot).all { it.exists() }
|
|
||||||
val hasCurrentBuildStamp =
|
|
||||||
stampFile.exists() && stampFile.readText() == androidAlsaBuildStamp(scriptFile)
|
|
||||||
!hasAndroidAlsa || !hasCurrentBuildStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidAlsaBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidOpus") {
|
|
||||||
description = "Builds Android Opus shared libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val opusRoot = file(androidOpusRoot.get())
|
|
||||||
val scriptFile = androidOpusBuildScript.asFile
|
|
||||||
val stampFile = opusRoot.resolve(".one-kvm-android-opus.stamp")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--output",
|
|
||||||
opusRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(opusRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
val hasAndroidOpus = androidOpusRequiredFiles(opusRoot).all { it.exists() }
|
|
||||||
val hasCurrentBuildStamp =
|
|
||||||
stampFile.exists() && stampFile.readText() == androidOpusBuildStamp(scriptFile)
|
|
||||||
!hasAndroidOpus || !hasCurrentBuildStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidOpusBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cargoBuildAndroidAbiTaskNames = selectedAndroidAbiTargets.map { (abi, targets) ->
|
|
||||||
val (flavor, _, _) = targets
|
|
||||||
val taskName = "cargoBuildAndroid" + flavor.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) it.titlecase() else it.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>(taskName) {
|
|
||||||
description = "Builds the Android Rust bootstrap libraries for $abi."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
dependsOn(
|
|
||||||
"buildAndroidFfmpegMediaCodec",
|
|
||||||
"buildAndroidLibyuv",
|
|
||||||
"buildAndroidTurbojpeg",
|
|
||||||
"buildAndroidAlsa",
|
|
||||||
"buildAndroidOpus",
|
|
||||||
)
|
|
||||||
|
|
||||||
val cargoCommand = mutableListOf(
|
|
||||||
"cargo",
|
|
||||||
"ndk",
|
|
||||||
"-t",
|
|
||||||
abi,
|
|
||||||
"-P",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"-o",
|
|
||||||
nativeCargoOutputDir.get().asFile.absolutePath,
|
|
||||||
"build",
|
|
||||||
"--lib",
|
|
||||||
"--bins",
|
|
||||||
)
|
|
||||||
if (androidBuildProfile == "release") {
|
|
||||||
cargoCommand.add("--release")
|
|
||||||
}
|
|
||||||
|
|
||||||
workingDir = nativeCrateDir.asFile
|
|
||||||
commandLine(cargoCommand)
|
|
||||||
args("--features", "android-mediacodec")
|
|
||||||
environment("ONE_KVM_ANDROID_FFMPEG_ROOT", androidFfmpegRoot.get())
|
|
||||||
environment("ONE_KVM_ANDROID_LIBYUV_ROOT", androidLibyuvRoot.get())
|
|
||||||
environment("ONE_KVM_ANDROID_LIBYUV_STATIC", "1")
|
|
||||||
environment("TURBOJPEG_SOURCE", "explicit")
|
|
||||||
environment("TURBOJPEG_STATIC", "1")
|
|
||||||
environment(
|
|
||||||
"TURBOJPEG_LIB_DIR",
|
|
||||||
file(androidTurbojpegRoot.get()).resolve("$abi/lib").absolutePath,
|
|
||||||
)
|
|
||||||
environment(
|
|
||||||
"TURBOJPEG_INCLUDE_DIR",
|
|
||||||
file(androidTurbojpegRoot.get()).resolve("$abi/include").absolutePath,
|
|
||||||
)
|
|
||||||
environment("PKG_CONFIG_ALLOW_CROSS", "1")
|
|
||||||
environment(
|
|
||||||
"PKG_CONFIG_LIBDIR",
|
|
||||||
file(androidAlsaRoot.get()).resolve("$abi/lib/pkgconfig").absolutePath,
|
|
||||||
)
|
|
||||||
environment("PKG_CONFIG_SYSROOT_DIR", "")
|
|
||||||
environment("LIBOPUS_NO_PKG", "1")
|
|
||||||
environment("LIBOPUS_LIB_DIR", file(androidOpusRoot.get()).resolve("$abi/lib").absolutePath)
|
|
||||||
environment("ANDROID_HOME", androidSdkDir.absolutePath)
|
|
||||||
environment("ANDROID_SDK_ROOT", androidSdkDir.absolutePath)
|
|
||||||
environment("ANDROID_NDK_HOME", androidNdkDir.absolutePath)
|
|
||||||
environment("ANDROID_NDK", androidNdkDir.absolutePath)
|
|
||||||
environment("ANDROID_NDK_ROOT", androidNdkDir.absolutePath)
|
|
||||||
|
|
||||||
inputs.files(
|
|
||||||
nativeCrateDir.file("Cargo.toml"),
|
|
||||||
nativeCrateDir.dir("src"),
|
|
||||||
rootCrateDir.file("Cargo.lock"),
|
|
||||||
rootCrateDir.file("Cargo.toml"),
|
|
||||||
rootCrateDir.file("build.rs"),
|
|
||||||
rootCrateDir.dir("libs"),
|
|
||||||
rootCrateDir.dir("res/vcpkg/libyuv"),
|
|
||||||
rootCrateDir.dir("src"),
|
|
||||||
)
|
|
||||||
outputs.dir(nativeCargoOutputDir)
|
|
||||||
outputs.dir(file(androidFfmpegRoot.get()))
|
|
||||||
outputs.dir(file(androidLibyuvRoot.get()))
|
|
||||||
outputs.dir(file(androidTurbojpegRoot.get()))
|
|
||||||
outputs.dir(file(androidAlsaRoot.get()))
|
|
||||||
outputs.dir(file(androidOpusRoot.get()))
|
|
||||||
}
|
|
||||||
|
|
||||||
taskName
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("cargoBuildAndroid") {
|
|
||||||
description = "Builds the Android Rust bootstrap libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
dependsOn(cargoBuildAndroidAbiTaskNames)
|
|
||||||
|
|
||||||
outputs.dir(nativeOutputRoot)
|
|
||||||
outputs.dir(nativeAssetRoot)
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
selectedAndroidAbiTargets.forEach { (abi, targets) ->
|
|
||||||
val (flavor, rustTriple, ndkTriple) = targets
|
|
||||||
val nativeLibSource = nativeCargoOutputDir.get().file("$abi/libone_kvm_android_bootstrap.so").asFile
|
|
||||||
if (!nativeLibSource.exists()) {
|
|
||||||
throw GradleException("Missing Android JNI library: ${nativeLibSource.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(nativeLibSource)
|
|
||||||
into(nativeOutputRoot.get().dir(flavor).dir(abi))
|
|
||||||
}
|
|
||||||
|
|
||||||
val source = nativeCrateDir.file("target/$rustTriple/$androidBuildProfile/one-kvm-android-host").asFile
|
|
||||||
if (!source.exists()) {
|
|
||||||
throw GradleException("Missing Android host binary: ${source.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(source)
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi"))
|
|
||||||
rename { "one-kvm-android-host" }
|
|
||||||
}
|
|
||||||
|
|
||||||
val cxxShared = androidNdkDir
|
|
||||||
.resolve("toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/$ndkTriple/libc++_shared.so")
|
|
||||||
if (!cxxShared.exists()) {
|
|
||||||
throw GradleException("Missing NDK libc++_shared.so: ${cxxShared.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(cxxShared)
|
|
||||||
into(nativeOutputRoot.get().dir(flavor).dir(abi))
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(cxxShared)
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val alsaShared = file(androidAlsaRoot.get()).resolve("$abi/lib/libasound.so")
|
|
||||||
if (!alsaShared.exists()) {
|
|
||||||
throw GradleException("Missing Android ALSA library: ${alsaShared.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(alsaShared)
|
|
||||||
into(nativeOutputRoot.get().dir(flavor).dir(abi))
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(alsaShared)
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi"))
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(file(androidAlsaRoot.get()).resolve("$abi/share/alsa"))
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi/alsa"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val opusShared = file(androidOpusRoot.get()).resolve("$abi/lib/libopus.so")
|
|
||||||
if (!opusShared.exists()) {
|
|
||||||
throw GradleException("Missing Android Opus library: ${opusShared.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(opusShared)
|
|
||||||
into(nativeOutputRoot.get().dir(flavor).dir(abi))
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(opusShared)
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("preBuild") {
|
|
||||||
dependsOn("cargoBuildAndroid")
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="false"
|
|
||||||
android:icon="@drawable/ic_launcher_one_kvm"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/AppTheme">
|
|
||||||
<service
|
|
||||||
android:name=".OneKvmService"
|
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="connectedDevice" />
|
|
||||||
<receiver
|
|
||||||
android:name=".BootReceiver"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED && HostSettings.getAutoStart(context)) {
|
|
||||||
OneKvmService.start(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
object HostSettings {
|
|
||||||
private const val PREFS = "one_kvm_android"
|
|
||||||
private const val KEY_AUTO_START = "auto_start"
|
|
||||||
private const val KEY_CLEAR_EXISTING_OTG = "clear_existing_otg"
|
|
||||||
|
|
||||||
fun getAutoStart(context: Context): Boolean {
|
|
||||||
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.getBoolean(KEY_AUTO_START, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAutoStart(context: Context, enabled: Boolean) {
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putBoolean(KEY_AUTO_START, enabled)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getClearExistingOtg(context: Context): Boolean {
|
|
||||||
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.getBoolean(KEY_CLEAR_EXISTING_OTG, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setClearExistingOtg(context: Context, enabled: Boolean) {
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putBoolean(KEY_CLEAR_EXISTING_OTG, enabled)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
object LogConfig {
|
|
||||||
private const val PREFS = "one_kvm_android"
|
|
||||||
private const val KEY_LOG_LEVEL = "log_level"
|
|
||||||
const val DEFAULT_LEVEL = "info"
|
|
||||||
val LEVELS = arrayOf("error", "warn", "info", "debug", "trace")
|
|
||||||
|
|
||||||
fun getLevel(context: Context): String {
|
|
||||||
val value = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.getString(KEY_LOG_LEVEL, DEFAULT_LEVEL)
|
|
||||||
?: DEFAULT_LEVEL
|
|
||||||
return if (LEVELS.contains(value)) value else DEFAULT_LEVEL
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLevel(context: Context, level: String) {
|
|
||||||
val safeLevel = if (LEVELS.contains(level)) level else DEFAULT_LEVEL
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putString(KEY_LOG_LEVEL, safeLevel)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rustLogFilter(level: String): String {
|
|
||||||
val safeLevel = if (LEVELS.contains(level)) level else DEFAULT_LEVEL
|
|
||||||
return "one_kvm=$safeLevel,hwcodec=$safeLevel,tower_http=$safeLevel,webrtc_sctp=warn"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
object LogStore {
|
|
||||||
private const val FLUSH_DELAY_MS = 250L
|
|
||||||
private const val MAX_BUFFER_CHARS = 64 * 1024
|
|
||||||
|
|
||||||
private val lock = Any()
|
|
||||||
private val buffer = StringBuilder()
|
|
||||||
private val executor = Executors.newSingleThreadScheduledExecutor { runnable ->
|
|
||||||
Thread(runnable, "OneKvmLogStore").apply { isDaemon = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var logFile: File? = null
|
|
||||||
private var flushScheduled = false
|
|
||||||
|
|
||||||
fun defaultLogFile(context: Context): File {
|
|
||||||
return File(File(context.getExternalFilesDir(null), "runtime"), "one-kvm.log")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun configure(file: File) {
|
|
||||||
synchronized(lock) {
|
|
||||||
flushLocked()
|
|
||||||
file.parentFile?.mkdirs()
|
|
||||||
file.writeText("")
|
|
||||||
buffer.clear()
|
|
||||||
logFile = file
|
|
||||||
flushScheduled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun append(line: String) {
|
|
||||||
synchronized(lock) {
|
|
||||||
if (logFile == null) return
|
|
||||||
|
|
||||||
buffer.append(line).append('\n')
|
|
||||||
if (buffer.length >= MAX_BUFFER_CHARS) {
|
|
||||||
flushLocked()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!flushScheduled) {
|
|
||||||
flushScheduled = true
|
|
||||||
executor.schedule({ flush() }, FLUSH_DELAY_MS, TimeUnit.MILLISECONDS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun flush() {
|
|
||||||
synchronized(lock) {
|
|
||||||
flushLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun flushLocked() {
|
|
||||||
val file = logFile ?: return
|
|
||||||
if (buffer.isEmpty()) {
|
|
||||||
flushScheduled = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val text = buffer.toString()
|
|
||||||
buffer.clear()
|
|
||||||
flushScheduled = false
|
|
||||||
file.appendText(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.ScrollView
|
|
||||||
import android.widget.Spinner
|
|
||||||
import android.widget.Switch
|
|
||||||
import android.widget.TextView
|
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.NetworkInterface
|
|
||||||
import java.net.Socket
|
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
class MainActivity : Activity() {
|
|
||||||
private lateinit var statusValue: TextView
|
|
||||||
private lateinit var hostActionButton: Button
|
|
||||||
private lateinit var logLevelSpinner: Spinner
|
|
||||||
private lateinit var autoStartSwitch: Switch
|
|
||||||
private lateinit var clearOtgSwitch: Switch
|
|
||||||
private val statusHandler = Handler(Looper.getMainLooper())
|
|
||||||
private var statusPollsRemaining = 0
|
|
||||||
private val statusPoller = object : Runnable {
|
|
||||||
override fun run() {
|
|
||||||
refreshStatus()
|
|
||||||
statusPollsRemaining -= 1
|
|
||||||
if (statusPollsRemaining > 0) {
|
|
||||||
statusHandler.postDelayed(this, STATUS_POLL_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
window.statusBarColor = color("#F8FAFC")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
window.navigationBarColor = color("#F8FAFC")
|
|
||||||
}
|
|
||||||
|
|
||||||
val content = LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
setPadding(20.dp(), 24.dp(), 20.dp(), 28.dp())
|
|
||||||
background = solid("#F8FAFC")
|
|
||||||
}
|
|
||||||
|
|
||||||
content.addView(startCard())
|
|
||||||
content.addView(settingsCard())
|
|
||||||
content.addView(infoCard())
|
|
||||||
|
|
||||||
setContentView(ScrollView(this).apply {
|
|
||||||
isFillViewport = true
|
|
||||||
setBackgroundColor(color("#F8FAFC"))
|
|
||||||
addView(content)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
reconcilePersistedStatus()
|
|
||||||
refreshStatus()
|
|
||||||
autoStartSwitch.isChecked = HostSettings.getAutoStart(this)
|
|
||||||
clearOtgSwitch.isChecked = HostSettings.getClearExistingOtg(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
statusHandler.removeCallbacks(statusPoller)
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startCard(): View {
|
|
||||||
return card {
|
|
||||||
addView(sectionTitle("启动管理"))
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = "管理本机 One-KVM 服务进程。暂停会停止前台服务并释放运行资源。"
|
|
||||||
textSize = 14f
|
|
||||||
setTextColor(color("#64748B"))
|
|
||||||
setPadding(0, 6.dp(), 0, 14.dp())
|
|
||||||
})
|
|
||||||
|
|
||||||
statusValue = TextView(this@MainActivity).apply {
|
|
||||||
textSize = 14f
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
background = rounded("#EFF6FF", "#BFDBFE", 8)
|
|
||||||
setPadding(12.dp(), 8.dp(), 12.dp(), 8.dp())
|
|
||||||
}
|
|
||||||
addView(statusValue, matchWrap())
|
|
||||||
|
|
||||||
addView(LinearLayout(this@MainActivity).apply {
|
|
||||||
orientation = LinearLayout.HORIZONTAL
|
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
|
||||||
setPadding(0, 14.dp(), 0, 0)
|
|
||||||
hostActionButton = actionButton("启动", primary = true) { toggleHost() }
|
|
||||||
addView(hostActionButton, matchButton())
|
|
||||||
})
|
|
||||||
refreshStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun settingsCard(): View {
|
|
||||||
return card {
|
|
||||||
addView(sectionTitle("运行设置"))
|
|
||||||
|
|
||||||
val (autoStartRow, autoStartControl) = settingSwitchRow(
|
|
||||||
title = "开机自启动",
|
|
||||||
subtitle = "系统启动完成后自动拉起 One-KVM 前台服务。",
|
|
||||||
checked = HostSettings.getAutoStart(this@MainActivity),
|
|
||||||
) { _, checked ->
|
|
||||||
HostSettings.setAutoStart(this@MainActivity, checked)
|
|
||||||
LogStore.append("Boot auto-start ${if (checked) "enabled" else "disabled"}")
|
|
||||||
}
|
|
||||||
autoStartSwitch = autoStartControl
|
|
||||||
addView(autoStartRow)
|
|
||||||
|
|
||||||
addView(divider())
|
|
||||||
|
|
||||||
val (clearOtgRow, clearOtgControl) = settingSwitchRow(
|
|
||||||
title = "清除已有 OTG Gadget",
|
|
||||||
subtitle = "启动 root host 前尝试解绑并删除 configfs 中已有的 USB gadget。",
|
|
||||||
checked = HostSettings.getClearExistingOtg(this@MainActivity),
|
|
||||||
) { _, checked ->
|
|
||||||
HostSettings.setClearExistingOtg(this@MainActivity, checked)
|
|
||||||
LogStore.append("Clear existing OTG gadget ${if (checked) "enabled" else "disabled"}")
|
|
||||||
}
|
|
||||||
clearOtgSwitch = clearOtgControl
|
|
||||||
addView(clearOtgRow)
|
|
||||||
|
|
||||||
addView(divider())
|
|
||||||
addView(logLevelRow())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun infoCard(): View {
|
|
||||||
return card {
|
|
||||||
addView(sectionTitle("应用信息"))
|
|
||||||
addView(infoRow("软件内核版本", kernelVersion()))
|
|
||||||
addView(infoRow("访问地址", accessAddresses(), selectable = true))
|
|
||||||
addView(infoRow("日志文件", LogStore.defaultLogFile(this@MainActivity).absolutePath, selectable = true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun settingSwitchRow(
|
|
||||||
title: String,
|
|
||||||
subtitle: String,
|
|
||||||
checked: Boolean,
|
|
||||||
listener: CompoundButton.OnCheckedChangeListener,
|
|
||||||
): Pair<View, Switch> {
|
|
||||||
val switch = Switch(this).apply {
|
|
||||||
isChecked = checked
|
|
||||||
setOnCheckedChangeListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
val row = LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.HORIZONTAL
|
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
|
||||||
setPadding(0, 12.dp(), 0, 12.dp())
|
|
||||||
addView(LinearLayout(this@MainActivity).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = title
|
|
||||||
textSize = 15f
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
})
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = subtitle
|
|
||||||
textSize = 13f
|
|
||||||
setTextColor(color("#64748B"))
|
|
||||||
setPadding(0, 4.dp(), 12.dp(), 0)
|
|
||||||
})
|
|
||||||
}, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
||||||
addView(switch)
|
|
||||||
}
|
|
||||||
|
|
||||||
return row to switch
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun infoRow(label: String, value: String, selectable: Boolean = false): View {
|
|
||||||
return LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
setPadding(0, 12.dp(), 0, 12.dp())
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = label
|
|
||||||
textSize = 13f
|
|
||||||
setTextColor(color("#64748B"))
|
|
||||||
})
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = value
|
|
||||||
textSize = 15f
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
setPadding(0, 4.dp(), 0, 0)
|
|
||||||
setTextIsSelectable(selectable)
|
|
||||||
})
|
|
||||||
addView(divider())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logLevelRow(): View {
|
|
||||||
return LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.HORIZONTAL
|
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
|
||||||
setPadding(0, 12.dp(), 0, 0)
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = "日志级别"
|
|
||||||
textSize = 15f
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
}, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
||||||
|
|
||||||
logLevelSpinner = Spinner(this@MainActivity).apply {
|
|
||||||
adapter = ArrayAdapter(
|
|
||||||
this@MainActivity,
|
|
||||||
android.R.layout.simple_spinner_dropdown_item,
|
|
||||||
LogConfig.LEVELS,
|
|
||||||
)
|
|
||||||
setSelection(LogConfig.LEVELS.indexOf(LogConfig.getLevel(this@MainActivity)).coerceAtLeast(0))
|
|
||||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
val level = LogConfig.LEVELS[position]
|
|
||||||
if (level != LogConfig.getLevel(this@MainActivity)) {
|
|
||||||
LogConfig.setLevel(this@MainActivity, level)
|
|
||||||
LogStore.append("Log level set to $level; restart service to apply")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addView(logLevelSpinner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun card(build: LinearLayout.() -> Unit): View {
|
|
||||||
return LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
setPadding(16.dp(), 16.dp(), 16.dp(), 16.dp())
|
|
||||||
background = rounded("#FFFFFF", "#E2E8F0", 10)
|
|
||||||
elevation = 1.5f.dpFloat()
|
|
||||||
build()
|
|
||||||
}.also {
|
|
||||||
it.layoutParams = LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
).apply { setMargins(0, 0, 0, 14.dp()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sectionTitle(text: String): View {
|
|
||||||
return TextView(this).apply {
|
|
||||||
this.text = text
|
|
||||||
textSize = 17f
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun actionButton(text: String, primary: Boolean, action: () -> Unit): Button {
|
|
||||||
return Button(this).apply {
|
|
||||||
this.text = text
|
|
||||||
textSize = 15f
|
|
||||||
isAllCaps = false
|
|
||||||
minHeight = 44.dp()
|
|
||||||
setTextColor(color(if (primary) "#FFFFFF" else "#0F172A"))
|
|
||||||
background = if (primary) rounded("#2563EB", "#2563EB", 8) else rounded("#FFFFFF", "#CBD5E1", 8)
|
|
||||||
setOnClickListener { action() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleHost() {
|
|
||||||
when (ServiceStatusStore.snapshot(this).state) {
|
|
||||||
ServiceStatusStore.STATE_RUNNING -> pauseHost()
|
|
||||||
ServiceStatusStore.STATE_STOPPED, ServiceStatusStore.STATE_ERROR -> startHost()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startHost() {
|
|
||||||
ServiceStatusStore.setStarting(this)
|
|
||||||
refreshStatus()
|
|
||||||
OneKvmService.start(this)
|
|
||||||
LogStore.append("Start requested from app UI")
|
|
||||||
pollStatusForAWhile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pauseHost() {
|
|
||||||
ServiceStatusStore.setStopping(this)
|
|
||||||
refreshStatus()
|
|
||||||
OneKvmService.stop(this)
|
|
||||||
LogStore.append("Pause requested from app UI")
|
|
||||||
pollStatusForAWhile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshStatus() {
|
|
||||||
if (::statusValue.isInitialized) {
|
|
||||||
statusValue.text = "状态:${hostStatusSummary()}"
|
|
||||||
}
|
|
||||||
updateHostActionButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hostStatusSummary(): String {
|
|
||||||
val serviceStatus = ServiceStatusStore.snapshot(this)
|
|
||||||
if (serviceStatus.state != ServiceStatusStore.STATE_STOPPED) {
|
|
||||||
return serviceStatus.labelText()
|
|
||||||
}
|
|
||||||
|
|
||||||
val nativeRunning = runCatching {
|
|
||||||
NativeBridge.hostStatus().contains("running", ignoreCase = true)
|
|
||||||
}.getOrDefault(false)
|
|
||||||
|
|
||||||
return if (nativeRunning) "运行中" else "已停止"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reconcilePersistedStatus() {
|
|
||||||
val serviceStatus = ServiceStatusStore.snapshot(this)
|
|
||||||
if (serviceStatus.state == ServiceStatusStore.STATE_STOPPED) return
|
|
||||||
if (
|
|
||||||
serviceStatus.state == ServiceStatusStore.STATE_STARTING &&
|
|
||||||
System.currentTimeMillis() - serviceStatus.updatedAt < STARTING_RECONCILE_GRACE_MS
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
val portOpen = isLocalWebPortOpen()
|
|
||||||
val nativeRunning = runCatching { NativeBridge.hostStatus().contains("running", ignoreCase = true) }
|
|
||||||
.getOrDefault(false)
|
|
||||||
if (!portOpen && !nativeRunning) {
|
|
||||||
ServiceStatusStore.setStopped(this, "服务未运行")
|
|
||||||
runOnUiThread { refreshStatus() }
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isLocalWebPortOpen(): Boolean {
|
|
||||||
return runCatching {
|
|
||||||
Socket().use { socket ->
|
|
||||||
socket.connect(InetSocketAddress("127.0.0.1", 8080), 250)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}.getOrDefault(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateHostActionButton() {
|
|
||||||
if (!::hostActionButton.isInitialized) return
|
|
||||||
|
|
||||||
when (ServiceStatusStore.snapshot(this).state) {
|
|
||||||
ServiceStatusStore.STATE_STARTING -> setHostActionButton("启动中...", enabled = false, primary = true)
|
|
||||||
ServiceStatusStore.STATE_RUNNING -> setHostActionButton("停止", enabled = true, primary = false)
|
|
||||||
ServiceStatusStore.STATE_STOPPING -> setHostActionButton("停止中...", enabled = false, primary = false)
|
|
||||||
else -> setHostActionButton("启动", enabled = true, primary = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setHostActionButton(text: String, enabled: Boolean, primary: Boolean) {
|
|
||||||
hostActionButton.text = text
|
|
||||||
hostActionButton.isEnabled = enabled
|
|
||||||
hostActionButton.alpha = if (enabled) 1f else 0.65f
|
|
||||||
hostActionButton.setTextColor(color(if (primary) "#FFFFFF" else "#0F172A"))
|
|
||||||
hostActionButton.background = if (primary) {
|
|
||||||
rounded("#2563EB", "#2563EB", 8)
|
|
||||||
} else {
|
|
||||||
rounded("#FFFFFF", "#CBD5E1", 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollStatusForAWhile() {
|
|
||||||
statusPollsRemaining = 20
|
|
||||||
statusHandler.removeCallbacks(statusPoller)
|
|
||||||
statusHandler.postDelayed(statusPoller, STATUS_POLL_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun kernelVersion(): String {
|
|
||||||
return runCatching { NativeBridge.kernelVersion() }
|
|
||||||
.getOrElse { "unknown" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun accessAddresses(): String {
|
|
||||||
val addresses = runCatching {
|
|
||||||
Collections.list(NetworkInterface.getNetworkInterfaces())
|
|
||||||
.filter { it.isUp && !it.isLoopback }
|
|
||||||
.flatMap { iface -> Collections.list(iface.inetAddresses) }
|
|
||||||
.filterIsInstance<Inet4Address>()
|
|
||||||
.filter { !it.isLoopbackAddress }
|
|
||||||
.map { "http://${it.hostAddress}:8080" }
|
|
||||||
.distinct()
|
|
||||||
}.getOrDefault(emptyList())
|
|
||||||
|
|
||||||
return (addresses.ifEmpty { listOf("http://127.0.0.1:8080") }).joinToString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun divider(): View {
|
|
||||||
return View(this).apply {
|
|
||||||
setBackgroundColor(color("#E2E8F0"))
|
|
||||||
layoutParams = LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
1,
|
|
||||||
).apply { setMargins(0, 0, 0, 0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun matchWrap(): LinearLayout.LayoutParams {
|
|
||||||
return LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun matchButton(): LinearLayout.LayoutParams {
|
|
||||||
return LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
48.dp(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun solid(hex: String): GradientDrawable = GradientDrawable().apply {
|
|
||||||
setColor(color(hex))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rounded(fill: String, stroke: String, radiusDp: Int): GradientDrawable {
|
|
||||||
return GradientDrawable().apply {
|
|
||||||
setColor(color(fill))
|
|
||||||
cornerRadius = radiusDp.dpFloat()
|
|
||||||
setStroke(1.dp(), color(stroke))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun color(hex: String): Int = Color.parseColor(hex)
|
|
||||||
|
|
||||||
private fun Int.dp(): Int = (this * resources.displayMetrics.density + 0.5f).toInt()
|
|
||||||
|
|
||||||
private fun Int.dpFloat(): Float = this * resources.displayMetrics.density
|
|
||||||
|
|
||||||
private fun Float.dpFloat(): Float = this * resources.displayMetrics.density
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val STATUS_POLL_INTERVAL_MS = 500L
|
|
||||||
private const val STARTING_RECONCILE_GRACE_MS = 15_000L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
object NativeBridge {
|
|
||||||
init {
|
|
||||||
System.loadLibrary("one_kvm_android_bootstrap")
|
|
||||||
}
|
|
||||||
|
|
||||||
external fun initTlsVerifier(context: Context): Int
|
|
||||||
|
|
||||||
external fun setEnv(name: String, value: String): Int
|
|
||||||
|
|
||||||
external fun startHost(dataDir: String, bindAddress: String, port: Int): String
|
|
||||||
|
|
||||||
external fun stopHost(): String
|
|
||||||
|
|
||||||
external fun hostStatus(): String
|
|
||||||
|
|
||||||
external fun kernelVersion(): String
|
|
||||||
}
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.io.InterruptedIOException
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
class OneKvmService : Service() {
|
|
||||||
private var rootProcess: Process? = null
|
|
||||||
private val commandExecutor = Executors.newSingleThreadExecutor { runnable ->
|
|
||||||
Thread(runnable, "OneKvmServiceCommand")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
ensureNotificationChannel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
when (intent?.action ?: ACTION_START) {
|
|
||||||
ACTION_STOP -> {
|
|
||||||
ServiceStatusStore.setStopping(this)
|
|
||||||
commandExecutor.execute {
|
|
||||||
stopHostRuntime()
|
|
||||||
stopSelfResult(startId)
|
|
||||||
}
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
ACTION_START -> {
|
|
||||||
ServiceStatusStore.setStarting(this)
|
|
||||||
startForegroundCompat(NOTIFICATION_ID, notification("启动中"))
|
|
||||||
commandExecutor.execute {
|
|
||||||
val currentState = ServiceStatusStore.snapshot(this).state
|
|
||||||
if (currentState == ServiceStatusStore.STATE_RUNNING && isPortOpen(8080, 100)) {
|
|
||||||
return@execute
|
|
||||||
}
|
|
||||||
val dataDir = File(getExternalFilesDir(null), "runtime")
|
|
||||||
if (!dataDir.exists()) dataDir.mkdirs()
|
|
||||||
val result = startRustHost(dataDir)
|
|
||||||
if (result.startsWith("Running") && !result.contains("start failed", ignoreCase = true)) {
|
|
||||||
ServiceStatusStore.setRunning(this, "服务已启动")
|
|
||||||
notificationManager().notify(NOTIFICATION_ID, notification("运行中"))
|
|
||||||
} else {
|
|
||||||
ServiceStatusStore.setError(this, "启动失败")
|
|
||||||
notificationManager().notify(NOTIFICATION_ID, notification("启动失败"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
stopHostRuntime(updateNotification = false)
|
|
||||||
commandExecutor.shutdownNow()
|
|
||||||
ServiceStatusStore.setStopped(this)
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
private fun notification(state: String): Notification {
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
val pendingIntent = createContentIntent(intent)
|
|
||||||
val builder = createNotificationBuilder()
|
|
||||||
return builder
|
|
||||||
.setSmallIcon(R.drawable.ic_stat_one_kvm)
|
|
||||||
.setContentTitle("One-KVM Android Host")
|
|
||||||
.setContentText(state)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setOngoing(true)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureNotificationChannel() {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
CHANNEL_ID,
|
|
||||||
"One-KVM Host",
|
|
||||||
NotificationManager.IMPORTANCE_LOW,
|
|
||||||
)
|
|
||||||
notificationManager().createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun createNotificationBuilder(): Notification.Builder {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
Notification.Builder(this, CHANNEL_ID)
|
|
||||||
} else {
|
|
||||||
Notification.Builder(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createContentIntent(intent: Intent): PendingIntent {
|
|
||||||
val flags = pendingIntentFlags()
|
|
||||||
return PendingIntent.getActivity(this, 0, intent, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pendingIntentFlags(): Int {
|
|
||||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
flags = flags or pendingIntentImmutableFlag()
|
|
||||||
}
|
|
||||||
return flags
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pendingIntentImmutableFlag(): Int {
|
|
||||||
return try {
|
|
||||||
PendingIntent::class.java.getField("FLAG_IMMUTABLE").getInt(null)
|
|
||||||
} catch (_: ReflectiveOperationException) {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notificationManager(): NotificationManager {
|
|
||||||
return getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopHostRuntime(updateNotification: Boolean = true) {
|
|
||||||
stopRootHost()
|
|
||||||
NativeBridge.stopHost()
|
|
||||||
waitForPortRelease(8080, 2_000)
|
|
||||||
LogStore.flush()
|
|
||||||
ServiceStatusStore.setStopped(this)
|
|
||||||
if (updateNotification) {
|
|
||||||
notificationManager().notify(NOTIFICATION_ID, notification("已停止"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startRustHost(dataDir: File): String {
|
|
||||||
val logLevel = LogConfig.getLevel(this)
|
|
||||||
val rustLog = LogConfig.rustLogFilter(logLevel)
|
|
||||||
val appLogFile = LogStore.defaultLogFile(this)
|
|
||||||
LogStore.configure(appLogFile)
|
|
||||||
val rustLogFile = appLogFile
|
|
||||||
LogStore.append("Starting One-KVM Rust host, data_dir=${dataDir.absolutePath}, log_level=$logLevel")
|
|
||||||
val executable = extractHostBinary()
|
|
||||||
return runCatching {
|
|
||||||
val tlsInit = NativeBridge.initTlsVerifier(this)
|
|
||||||
if (tlsInit != 0) {
|
|
||||||
throw IllegalStateException("rustls platform verifier init failed with code $tlsInit")
|
|
||||||
}
|
|
||||||
stopRootHost(executable)
|
|
||||||
clearExistingOtgGadgetsIfEnabled()
|
|
||||||
startRootHost(executable, dataDir, rustLog, rustLogFile, logLevel)
|
|
||||||
LogStore.append("Rust host running as root on port 8080")
|
|
||||||
"Running as root on port 8080"
|
|
||||||
}.getOrElse { rootError ->
|
|
||||||
LogStore.append("Root host unavailable: ${rootError.message ?: rootError::class.java.simpleName}")
|
|
||||||
configureAlsaEnvironment(executable)
|
|
||||||
NativeBridge.setEnv("RUST_LOG", rustLog)
|
|
||||||
NativeBridge.setEnv("ONE_KVM_FFMPEG_LOG", ffmpegLogLevel(logLevel))
|
|
||||||
NativeBridge.setEnv("ONE_KVM_ANDROID_LOG_FILE", rustLogFile.absolutePath)
|
|
||||||
val jniResult = NativeBridge.startHost(dataDir.absolutePath, "0.0.0.0", 8080)
|
|
||||||
LogStore.append("Rust host running in app process on port 8080: $jniResult")
|
|
||||||
"Running in app process on port 8080 (${rootError.message ?: "root unavailable"}; $jniResult)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearExistingOtgGadgetsIfEnabled() {
|
|
||||||
if (!HostSettings.getClearExistingOtg(this)) return
|
|
||||||
|
|
||||||
val command = """
|
|
||||||
root=/sys/kernel/config/usb_gadget
|
|
||||||
[ -d "${'$'}root" ] || exit 0
|
|
||||||
for gadget in "${'$'}root"/*; do
|
|
||||||
[ -d "${'$'}gadget" ] || continue
|
|
||||||
[ -w "${'$'}gadget/UDC" ] && echo "" > "${'$'}gadget/UDC" 2>/dev/null || true
|
|
||||||
find "${'$'}gadget/configs" -type l -delete 2>/dev/null || true
|
|
||||||
rm -rf "${'$'}gadget" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
ProcessBuilder("/system/xbin/su", "0", "sh", "-c", command)
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
.waitFor()
|
|
||||||
}.onSuccess { exit ->
|
|
||||||
LogStore.append("Existing OTG gadget cleanup finished with exit code $exit")
|
|
||||||
}.onFailure { err ->
|
|
||||||
LogStore.append("Existing OTG gadget cleanup failed: ${err.message ?: err::class.java.simpleName}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureAlsaEnvironment(executable: File) {
|
|
||||||
val binDir = executable.parentFile
|
|
||||||
?: throw IllegalStateException("host binary has no parent directory")
|
|
||||||
val alsaConfigDir = File(binDir, "alsa")
|
|
||||||
val alsaConfigPath = File(alsaConfigDir, "alsa.conf")
|
|
||||||
NativeBridge.setEnv("ALSA_CONFIG_DIR", alsaConfigDir.absolutePath)
|
|
||||||
NativeBridge.setEnv("ALSA_CONFIG_PATH", alsaConfigPath.absolutePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractHostBinary(): File {
|
|
||||||
val abi = Build.SUPPORTED_ABIS.firstOrNull { it == "arm64-v8a" || it == "armeabi-v7a" }
|
|
||||||
?: throw IllegalStateException("unsupported ABI: ${Build.SUPPORTED_ABIS.joinToString()}")
|
|
||||||
val binDir = File(filesDir, "bin/$abi")
|
|
||||||
val target = File(binDir, "one-kvm-android-host")
|
|
||||||
copyAssetIfChanged("bin/$abi/one-kvm-android-host", target)
|
|
||||||
copyAssetIfChanged("bin/$abi/libc++_shared.so", File(binDir, "libc++_shared.so"))
|
|
||||||
copyAssetIfChanged("bin/$abi/libasound.so", File(binDir, "libasound.so"))
|
|
||||||
copyAssetIfChanged("bin/$abi/libopus.so", File(binDir, "libopus.so"))
|
|
||||||
copyAssetDirectoryIfChanged("bin/$abi/alsa", File(binDir, "alsa"))
|
|
||||||
if (!target.setExecutable(true, false)) {
|
|
||||||
throw IllegalStateException("cannot mark host binary executable")
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyAssetIfChanged(assetPath: String, target: File) {
|
|
||||||
val stamp = File(target.parentFile, "${target.name}.stamp")
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
|
||||||
val expectedStamp = "${packageInfo.lastUpdateTime}:$assetPath"
|
|
||||||
if (target.exists() && stamp.exists() && stamp.readText() == expectedStamp) return
|
|
||||||
target.parentFile?.mkdirs()
|
|
||||||
assets.open(assetPath).use { input ->
|
|
||||||
target.outputStream().use { output -> input.copyTo(output) }
|
|
||||||
}
|
|
||||||
stamp.writeText(expectedStamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyAssetDirectoryIfChanged(assetDir: String, targetDir: File) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
|
||||||
val stamp = File(targetDir, ".stamp")
|
|
||||||
val expectedStamp = "${packageInfo.lastUpdateTime}:$assetDir"
|
|
||||||
if (targetDir.exists() && stamp.exists() && stamp.readText() == expectedStamp) return
|
|
||||||
if (targetDir.exists()) targetDir.deleteRecursively()
|
|
||||||
copyAssetDirectory(assetDir, targetDir)
|
|
||||||
stamp.writeText(expectedStamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyAssetDirectory(assetDir: String, targetDir: File) {
|
|
||||||
targetDir.mkdirs()
|
|
||||||
val children = assets.list(assetDir)?.filter { it.isNotEmpty() }.orEmpty()
|
|
||||||
for (child in children) {
|
|
||||||
val childAsset = "$assetDir/$child"
|
|
||||||
val childTarget = File(targetDir, child)
|
|
||||||
val grandChildren = assets.list(childAsset)?.filter { it.isNotEmpty() }.orEmpty()
|
|
||||||
if (grandChildren.isEmpty()) {
|
|
||||||
copyAssetIfChanged(childAsset, childTarget)
|
|
||||||
} else {
|
|
||||||
copyAssetDirectory(childAsset, childTarget)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startRootHost(
|
|
||||||
executable: File,
|
|
||||||
dataDir: File,
|
|
||||||
rustLog: String,
|
|
||||||
rustLogFile: File,
|
|
||||||
logLevel: String,
|
|
||||||
) {
|
|
||||||
stopRootHost(executable)
|
|
||||||
waitForPortRelease(8080, 2_000)
|
|
||||||
val libDir = executable.parentFile?.absolutePath
|
|
||||||
?: throw IllegalStateException("host binary has no parent directory")
|
|
||||||
val alsaConfigDir = File(executable.parentFile, "alsa")
|
|
||||||
val alsaConfigPath = File(alsaConfigDir, "alsa.conf")
|
|
||||||
val command =
|
|
||||||
"export LD_LIBRARY_PATH=${shellQuote(libDir)}:\${LD_LIBRARY_PATH:-}; " +
|
|
||||||
"export ALSA_CONFIG_DIR=${shellQuote(alsaConfigDir.absolutePath)}; " +
|
|
||||||
"export ALSA_CONFIG_PATH=${shellQuote(alsaConfigPath.absolutePath)}; " +
|
|
||||||
"export RUST_LOG=${shellQuote(rustLog)}; " +
|
|
||||||
"export ONE_KVM_FFMPEG_LOG=${shellQuote(ffmpegLogLevel(logLevel))}; " +
|
|
||||||
"export ONE_KVM_ANDROID_LOG_FILE=${shellQuote(rustLogFile.absolutePath)}; " +
|
|
||||||
"${shellQuote(executable.absolutePath)} ${shellQuote(dataDir.absolutePath)} 0.0.0.0 8080"
|
|
||||||
val process = ProcessBuilder("/system/xbin/su", "0", "sh", "-c", command)
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
rootProcess = process
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
val readError = runCatching {
|
|
||||||
BufferedReader(InputStreamReader(process.inputStream)).useLines { lines ->
|
|
||||||
lines.forEach {
|
|
||||||
android.util.Log.i("OneKvmService", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.exceptionOrNull()
|
|
||||||
if (readError != null && readError !is InterruptedIOException) {
|
|
||||||
android.util.Log.w("OneKvmService", "Root host log reader stopped", readError)
|
|
||||||
LogStore.append("Root host log reader stopped: ${readError.message ?: readError::class.java.simpleName}")
|
|
||||||
}
|
|
||||||
val exit = runCatching { process.waitFor() }.getOrNull()
|
|
||||||
if (rootProcess === process && exit != null) {
|
|
||||||
rootProcess = null
|
|
||||||
ServiceStatusStore.setError(this, "Root host exited with code $exit")
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
|
|
||||||
Thread.sleep(500)
|
|
||||||
val exit = runCatching { process.exitValue() }.getOrNull()
|
|
||||||
if (exit != null) {
|
|
||||||
rootProcess = null
|
|
||||||
throw IllegalStateException("root host exited immediately: $exit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startForegroundCompat(id: Int, notification: Notification) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val invoked = runCatching {
|
|
||||||
val method = Service::class.java.getMethod(
|
|
||||||
"startForeground",
|
|
||||||
Int::class.javaPrimitiveType,
|
|
||||||
Notification::class.java,
|
|
||||||
Int::class.javaPrimitiveType,
|
|
||||||
)
|
|
||||||
method.invoke(this, id, notification, foregroundServiceTypeConnectedDevice())
|
|
||||||
}.isSuccess
|
|
||||||
if (invoked) return
|
|
||||||
}
|
|
||||||
super.startForeground(id, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun foregroundServiceTypeConnectedDevice(): Int {
|
|
||||||
return try {
|
|
||||||
Service::class.java.getField("FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE").getInt(null)
|
|
||||||
} catch (_: ReflectiveOperationException) {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopRootHost(executable: File? = null) {
|
|
||||||
rootProcess?.destroy()
|
|
||||||
rootProcess = null
|
|
||||||
stopRootHostProcess(executable)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopRootHostProcess(executable: File? = null) {
|
|
||||||
val command = buildString {
|
|
||||||
append("pkill -TERM -f '[o]ne-kvm-android-host' 2>/dev/null || true; ")
|
|
||||||
append("for pid in $(pidof one-kvm-android-host 2>/dev/null); do kill -TERM \"${'$'}pid\" 2>/dev/null || true; done; ")
|
|
||||||
append("sleep 0.2; ")
|
|
||||||
append("pkill -KILL -f '[o]ne-kvm-android-host' 2>/dev/null || true; ")
|
|
||||||
append("for pid in $(pidof one-kvm-android-host 2>/dev/null); do kill -KILL \"${'$'}pid\" 2>/dev/null || true; done; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
ProcessBuilder("/system/xbin/su", "0", "sh", "-c", command)
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
.waitFor()
|
|
||||||
}.onFailure { err ->
|
|
||||||
LogStore.append("Failed to stop stale root host: ${err.message ?: err::class.java.simpleName}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun waitForPortRelease(port: Int, timeoutMs: Long) {
|
|
||||||
val deadline = System.currentTimeMillis() + timeoutMs
|
|
||||||
while (System.currentTimeMillis() < deadline) {
|
|
||||||
val inUse = isPortOpen(port, 100)
|
|
||||||
if (!inUse) return
|
|
||||||
Thread.sleep(100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isPortOpen(port: Int, timeoutMs: Int): Boolean {
|
|
||||||
return runCatching {
|
|
||||||
java.net.Socket().use { socket ->
|
|
||||||
socket.connect(java.net.InetSocketAddress("127.0.0.1", port), timeoutMs)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}.getOrDefault(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shellQuote(value: String): String {
|
|
||||||
return "'" + value.replace("'", "'\\''") + "'"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ffmpegLogLevel(level: String): String {
|
|
||||||
return when (level) {
|
|
||||||
"trace" -> "trace"
|
|
||||||
"debug" -> "debug"
|
|
||||||
"info" -> "info"
|
|
||||||
"warn" -> "warning"
|
|
||||||
else -> "error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CHANNEL_ID = "one_kvm_host"
|
|
||||||
private const val NOTIFICATION_ID = 1001
|
|
||||||
const val ACTION_START = "cn.one_kvm.androidhost.START"
|
|
||||||
const val ACTION_STOP = "cn.one_kvm.androidhost.STOP"
|
|
||||||
|
|
||||||
fun start(context: Context) {
|
|
||||||
val intent = Intent(context, OneKvmService::class.java).setAction(ACTION_START)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
} else {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop(context: Context) {
|
|
||||||
context.startService(Intent(context, OneKvmService::class.java).setAction(ACTION_STOP))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
object ServiceStatusStore {
|
|
||||||
private const val PREFS = "one_kvm_android_status"
|
|
||||||
private const val KEY_STATE = "state"
|
|
||||||
private const val KEY_MESSAGE = "message"
|
|
||||||
private const val KEY_UPDATED_AT = "updated_at"
|
|
||||||
|
|
||||||
const val STATE_STOPPED = "stopped"
|
|
||||||
const val STATE_STARTING = "starting"
|
|
||||||
const val STATE_RUNNING = "running"
|
|
||||||
const val STATE_STOPPING = "stopping"
|
|
||||||
const val STATE_ERROR = "error"
|
|
||||||
|
|
||||||
data class Snapshot(
|
|
||||||
val state: String,
|
|
||||||
val message: String,
|
|
||||||
val updatedAt: Long,
|
|
||||||
) {
|
|
||||||
fun labelText(): String {
|
|
||||||
return when (state) {
|
|
||||||
STATE_STARTING -> "启动中"
|
|
||||||
STATE_RUNNING -> "运行中"
|
|
||||||
STATE_STOPPING -> "停止中"
|
|
||||||
STATE_ERROR -> "错误"
|
|
||||||
else -> "已停止"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun displayText(): String {
|
|
||||||
val label = labelText()
|
|
||||||
return if (message.isBlank()) label else "$label:$message"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStarting(context: Context, message: String = "正在启动服务") {
|
|
||||||
write(context, STATE_STARTING, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setRunning(context: Context, message: String) {
|
|
||||||
write(context, STATE_RUNNING, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStopping(context: Context, message: String = "正在停止服务") {
|
|
||||||
write(context, STATE_STOPPING, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStopped(context: Context, message: String = "服务已停止") {
|
|
||||||
write(context, STATE_STOPPED, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setError(context: Context, message: String) {
|
|
||||||
write(context, STATE_ERROR, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun snapshot(context: Context): Snapshot {
|
|
||||||
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
return Snapshot(
|
|
||||||
state = prefs.getString(KEY_STATE, STATE_STOPPED) ?: STATE_STOPPED,
|
|
||||||
message = prefs.getString(KEY_MESSAGE, "") ?: "",
|
|
||||||
updatedAt = prefs.getLong(KEY_UPDATED_AT, 0L),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun write(context: Context, state: String, message: String) {
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putString(KEY_STATE, state)
|
|
||||||
.putString(KEY_MESSAGE, message)
|
|
||||||
.putLong(KEY_UPDATED_AT, System.currentTimeMillis())
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#1D7BF2"
|
|
||||||
android:pathData="M24,0h60a24,24 0,0 1,24 24v60a24,24 0,0 1,-24 24H24a24,24 0,0 1,-24 -24V24a24,24 0,0 1,24 -24z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#AED8E8"
|
|
||||||
android:pathData="M29,25h50a3,3 0,0 1,3 3v31a3,3 0,0 1,-3 3H29a3,3 0,0 1,-3 -3V28a3,3 0,0 1,3 -3z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#3F3F3D"
|
|
||||||
android:pathData="M31,30h46v27H31z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#E7F1F4"
|
|
||||||
android:pathData="M31,26h10a1.4,1.4 0,0 1,0 2.8H31a1.4,1.4 0,0 1,0 -2.8z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#8BBFD1"
|
|
||||||
android:pathData="M49,62h10l1.5,8h-13z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#9FCFE0"
|
|
||||||
android:pathData="M40,70a14,4.5 0,1 0,28 0a14,4.5 0,1 0,-28 0z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#E8F5F8"
|
|
||||||
android:pathData="M45,70a7,1.8 0,1 0,14 0a7,1.8 0,1 0,-14 0z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#BFE6F1"
|
|
||||||
android:pathData="M32,76h38l5,8H27z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#76ADC2"
|
|
||||||
android:pathData="M28,84h47v2H28z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillAlpha="0.82"
|
|
||||||
android:pathData="M37,79h6v2h-6zM46,79h5v2h-5zM54,79h5v2h-5zM62,79h6v2h-6zM34,82h7v2h-7zM44,82h6v2h-6zM53,82h11v2H53zM67,82h4v2h-4z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M4,5h16v10H4z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M9,17h6v2h3v2H6v-2h3z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name">One-KVM Android Host</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
|
|
||||||
<item name="android:fontFamily">sans</item>
|
|
||||||
<item name="android:colorAccent">#2563EB</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("com.android.application") version "9.0.0" apply false
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
android.useAndroidX=true
|
|
||||||
android.nonTransitiveRClass=true
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
||||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
251
android/gradlew
vendored
251
android/gradlew
vendored
@@ -1,251 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH="\\\"\\\""
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
if ! command -v java >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
94
android/gradlew.bat
vendored
94
android/gradlew.bat
vendored
@@ -1,94 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "one-kvm-android-bootstrap"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "one_kvm_android_bootstrap"
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "one-kvm-android-host"
|
|
||||||
path = "src/bin/one-kvm-android-host.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
jni = "0.22.4"
|
|
||||||
one-kvm = { path = "../..", default-features = false, features = ["android", "android-mediacodec"] }
|
|
||||||
rustls-platform-verifier = "0.7"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
android-mediacodec = ["one-kvm/android-mediacodec"]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
use one_kvm::runtime::android::{self, AndroidRuntimeConfig};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let mut args = std::env::args().skip(1);
|
|
||||||
let data_dir = args
|
|
||||||
.next()
|
|
||||||
.unwrap_or_else(|| "/data/local/tmp/one-kvm".to_string());
|
|
||||||
let bind_address = args.next().unwrap_or_else(|| "0.0.0.0".to_string());
|
|
||||||
let port = args
|
|
||||||
.next()
|
|
||||||
.and_then(|value| value.parse::<u16>().ok())
|
|
||||||
.unwrap_or(8080);
|
|
||||||
|
|
||||||
one_kvm::runtime::android::init_rustls_provider();
|
|
||||||
|
|
||||||
if let Err(err) = android::run_foreground(AndroidRuntimeConfig {
|
|
||||||
data_dir,
|
|
||||||
bind_address,
|
|
||||||
port,
|
|
||||||
}) {
|
|
||||||
eprintln!("one-kvm android host failed: {err}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
use jni::errors::{ErrorPolicy, ThrowRuntimeExAndDefault};
|
|
||||||
use jni::objects::{JClass, JObject, JString};
|
|
||||||
use jni::sys::{jint, jstring};
|
|
||||||
use jni::{Env, EnvOutcome, EnvUnowned};
|
|
||||||
use one_kvm::runtime::android::{self, AndroidRuntimeConfig};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct BridgeError(String);
|
|
||||||
|
|
||||||
impl From<jni::errors::Error> for BridgeError {
|
|
||||||
fn from(err: jni::errors::Error) -> Self {
|
|
||||||
Self(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for BridgeError {
|
|
||||||
fn from(err: String) -> Self {
|
|
||||||
Self(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for BridgeError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct StatusPolicy;
|
|
||||||
|
|
||||||
impl ErrorPolicy<jint, BridgeError> for StatusPolicy {
|
|
||||||
type Captures<'unowned_env_local: 'native_method, 'native_method> = ();
|
|
||||||
|
|
||||||
fn on_error<'unowned_env_local: 'native_method, 'native_method>(
|
|
||||||
_env: &mut Env<'unowned_env_local>,
|
|
||||||
_cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
|
|
||||||
_err: BridgeError,
|
|
||||||
) -> jni::errors::Result<jint> {
|
|
||||||
Ok(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_panic<'unowned_env_local: 'native_method, 'native_method>(
|
|
||||||
_env: &mut Env<'unowned_env_local>,
|
|
||||||
_cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
|
|
||||||
_payload: Box<dyn std::any::Any + Send + 'static>,
|
|
||||||
) -> jni::errors::Result<jint> {
|
|
||||||
Ok(-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct StringResultPolicy;
|
|
||||||
|
|
||||||
impl ErrorPolicy<String, BridgeError> for StringResultPolicy {
|
|
||||||
type Captures<'unowned_env_local: 'native_method, 'native_method> = ();
|
|
||||||
|
|
||||||
fn on_error<'unowned_env_local: 'native_method, 'native_method>(
|
|
||||||
_env: &mut Env<'unowned_env_local>,
|
|
||||||
_cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
|
|
||||||
err: BridgeError,
|
|
||||||
) -> jni::errors::Result<String> {
|
|
||||||
Ok(format!("start failed: {err}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_panic<'unowned_env_local: 'native_method, 'native_method>(
|
|
||||||
_env: &mut Env<'unowned_env_local>,
|
|
||||||
_cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
|
|
||||||
_payload: Box<dyn std::any::Any + Send + 'static>,
|
|
||||||
) -> jni::errors::Result<String> {
|
|
||||||
Ok("start failed: panic in native bridge".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_setEnv<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
name: JString<'local>,
|
|
||||||
value: JString<'local>,
|
|
||||||
) -> jint {
|
|
||||||
let outcome: EnvOutcome<'local, jint, BridgeError> = env.with_env_no_catch(|env| {
|
|
||||||
let name = name
|
|
||||||
.try_to_string(env)
|
|
||||||
.map_err(|err| BridgeError(format!("invalid env name: {err}")))?;
|
|
||||||
let value = value
|
|
||||||
.try_to_string(env)
|
|
||||||
.map_err(|err| BridgeError(format!("invalid env value: {err}")))?;
|
|
||||||
if name.contains('\0') || value.contains('\0') {
|
|
||||||
return Err(BridgeError("env contains NUL".to_string()));
|
|
||||||
}
|
|
||||||
std::env::set_var(name, value);
|
|
||||||
Ok(0)
|
|
||||||
});
|
|
||||||
|
|
||||||
outcome.resolve_with::<StatusPolicy, _>(|| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_initTlsVerifier<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
context: JObject<'local>,
|
|
||||||
) -> jint {
|
|
||||||
let outcome: EnvOutcome<'local, jint, BridgeError> =
|
|
||||||
env.with_env_no_catch(|env| init_tls_verifier(env, context));
|
|
||||||
|
|
||||||
outcome.resolve_with::<StatusPolicy, _>(|| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
fn init_tls_verifier(env: &mut Env<'_>, context: JObject<'_>) -> Result<jint, BridgeError> {
|
|
||||||
rustls_platform_verifier::android::init_with_env(env, context)
|
|
||||||
.map_err(|err| BridgeError(format!("failed to initialize rustls platform verifier: {err}")))?;
|
|
||||||
Ok(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
fn init_tls_verifier(_env: &mut Env<'_>, _context: JObject<'_>) -> Result<jint, BridgeError> {
|
|
||||||
Ok(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_startHost<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
data_dir: JString<'local>,
|
|
||||||
bind_address: JString<'local>,
|
|
||||||
port: i32,
|
|
||||||
) -> jstring {
|
|
||||||
let outcome: EnvOutcome<'local, String, BridgeError> = env.with_env_no_catch(|env| {
|
|
||||||
let data_dir = data_dir
|
|
||||||
.try_to_string(env)
|
|
||||||
.map_err(|err| BridgeError(format!("invalid data dir: {err}")))?;
|
|
||||||
let bind_address = bind_address
|
|
||||||
.try_to_string(env)
|
|
||||||
.map_err(|err| BridgeError(format!("invalid bind address: {err}")))?;
|
|
||||||
let port = u16::try_from(port).map_err(|_| BridgeError("invalid port".to_string()))?;
|
|
||||||
|
|
||||||
android::start(AndroidRuntimeConfig {
|
|
||||||
data_dir,
|
|
||||||
bind_address,
|
|
||||||
port,
|
|
||||||
})
|
|
||||||
.map_err(BridgeError)
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = outcome.resolve_with::<StringResultPolicy, _>(|| ());
|
|
||||||
|
|
||||||
env.with_env_no_catch(|env| env.new_string(result))
|
|
||||||
.resolve_with::<ThrowRuntimeExAndDefault, _>(|| ())
|
|
||||||
.into_raw()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_stopHost<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
) -> jstring {
|
|
||||||
env.with_env_no_catch(|env| env.new_string(android::stop()))
|
|
||||||
.resolve_with::<ThrowRuntimeExAndDefault, _>(|| ())
|
|
||||||
.into_raw()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_hostStatus<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
) -> jstring {
|
|
||||||
env.with_env_no_catch(|env| env.new_string(android::status()))
|
|
||||||
.resolve_with::<ThrowRuntimeExAndDefault, _>(|| ())
|
|
||||||
.into_raw()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_kernelVersion<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
) -> jstring {
|
|
||||||
env.with_env_no_catch(|env| env.new_string(env!("CARGO_PKG_VERSION")))
|
|
||||||
.resolve_with::<ThrowRuntimeExAndDefault, _>(|| ())
|
|
||||||
.into_raw()
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "OneKvmAndroidHost"
|
|
||||||
include(":app")
|
|
||||||
31
build.rs
31
build.rs
@@ -2,21 +2,6 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Set BUILD_DATE environment variable for compile-time access
|
|
||||||
// Use system time to avoid adding chrono as a build dependency
|
|
||||||
let now = std::time::SystemTime::now();
|
|
||||||
let duration = now.duration_since(std::time::UNIX_EPOCH).unwrap();
|
|
||||||
let secs = duration.as_secs();
|
|
||||||
|
|
||||||
// Convert Unix timestamp to date (simplified calculation)
|
|
||||||
// Days since epoch
|
|
||||||
let days = secs / 86400;
|
|
||||||
// Calculate year, month, day from days since 1970-01-01
|
|
||||||
let (year, month, day) = days_to_ymd(days as i64);
|
|
||||||
let build_date = format!("{:04}-{:02}-{:02}", year, month, day);
|
|
||||||
|
|
||||||
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
|
|
||||||
|
|
||||||
// Compile protobuf files for RustDesk protocol
|
// Compile protobuf files for RustDesk protocol
|
||||||
compile_protos();
|
compile_protos();
|
||||||
|
|
||||||
@@ -86,19 +71,3 @@ pub mod rustdesk {
|
|||||||
|
|
||||||
fs::write(&dest_path, code).expect("Failed to write secrets_generated.rs");
|
fs::write(&dest_path, code).expect("Failed to write secrets_generated.rs");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert days since Unix epoch to year-month-day
|
|
||||||
fn days_to_ymd(days: i64) -> (i32, u32, u32) {
|
|
||||||
// Algorithm from http://howardhinnant.github.io/date_algorithms.html
|
|
||||||
let z = days + 719468;
|
|
||||||
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
|
||||||
let doe = (z - era * 146097) as u32;
|
|
||||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
||||||
let y = yoe as i64 + era * 400;
|
|
||||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
||||||
let mp = (5 * doy + 2) / 153;
|
|
||||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
|
||||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
||||||
let year = if m <= 2 { y + 1 } else { y };
|
|
||||||
(year as i32, m, d)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
# One-KVM Runtime Image
|
# One-KVM Runtime Image
|
||||||
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
||||||
# Used after cross-compiling with `cross build`
|
# Used after cross-compiling with `cross build`
|
||||||
# Using Debian 11 for maximum compatibility (GLIBC 2.31)
|
# Using Debian 12 to match the build images (GLIBC 2.36)
|
||||||
|
|
||||||
ARG TARGETPLATFORM=linux/amd64
|
ARG TARGETPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM debian:11-slim
|
FROM debian:12-slim
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
# Install runtime dependencies in a single layer
|
# Install runtime dependencies in a single layer
|
||||||
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
||||||
# Only hardware acceleration drivers and core system libraries remain dynamic
|
# Only hardware acceleration drivers and core system libraries remain dynamic
|
||||||
RUN sed -i 's/ main$/ main contrib non-free/' /etc/apt/sources.list && \
|
RUN sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
# Core runtime (all platforms) - no codec libs needed
|
# Core runtime (all platforms) - no codec libs needed
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
# Bluetooth HID uses the host BlueZ system bus; btmgmt manages a dedicated adapter.
|
||||||
|
bluez \
|
||||||
libudev1 \
|
libudev1 \
|
||||||
libasound2 \
|
libasound2 \
|
||||||
|
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
|
||||||
|
network-manager \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping \
|
||||||
# v4l2 is handled by kernel, minimal userspace needed
|
# v4l2 is handled by kernel, minimal userspace needed
|
||||||
libv4l-0 \
|
libv4l-0 \
|
||||||
&& \
|
&& \
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
# One-KVM Runtime Image (full)
|
# One-KVM Runtime Image (full)
|
||||||
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
||||||
# Used after cross-compiling with `cross build`
|
# Used after cross-compiling with `cross build`
|
||||||
# Using Debian 11 for maximum compatibility (GLIBC 2.31)
|
# Using Debian 12 to match the build images (GLIBC 2.36)
|
||||||
|
|
||||||
ARG TARGETPLATFORM=linux/amd64
|
ARG TARGETPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM debian:11-slim
|
FROM debian:12-slim
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
# Install runtime dependencies in a single layer
|
# Install runtime dependencies in a single layer
|
||||||
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
||||||
# Only hardware acceleration drivers and core system libraries remain dynamic
|
# Only hardware acceleration drivers and core system libraries remain dynamic
|
||||||
RUN sed -i 's/ main$/ main contrib non-free/' /etc/apt/sources.list && \
|
RUN sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
# Core runtime (all platforms) - no codec libs needed
|
# Core runtime (all platforms) - no codec libs needed
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
# Bluetooth HID uses the host BlueZ system bus; btmgmt manages a dedicated adapter.
|
||||||
|
bluez \
|
||||||
libudev1 \
|
libudev1 \
|
||||||
libasound2 \
|
libasound2 \
|
||||||
|
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
|
||||||
|
network-manager \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping \
|
||||||
# v4l2 is handled by kernel, minimal userspace needed
|
# v4l2 is handled by kernel, minimal userspace needed
|
||||||
libv4l-0 \
|
libv4l-0 \
|
||||||
&& \
|
&& \
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Build Android APKs using the Docker build image.
|
|
||||||
# Usage: ./build/build-android.sh [arm64|armv7|all|help]
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
DOCKERFILE="$PROJECT_ROOT/build/cross/Dockerfile.android"
|
|
||||||
IMAGE_NAME="${ONE_KVM_ANDROID_DOCKER_IMAGE:-one-kvm-android-build:cn}"
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "Error: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
build_android() {
|
|
||||||
local arch="$1"
|
|
||||||
local docker_build_args=()
|
|
||||||
local 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")
|
|
||||||
if [[ -z "$gradle_distribution_url" ]]; then
|
|
||||||
gradle_distribution_url="$gradle_distribution_url_cn"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${ONE_KVM_ANDROID_SKIP_DOCKER_BUILD:-0}" == "1" ]]; then
|
|
||||||
echo "=== Skipping Android image build: $IMAGE_NAME ==="
|
|
||||||
else
|
|
||||||
echo "=== Building Android image: $IMAGE_NAME ==="
|
|
||||||
docker build \
|
|
||||||
-f "$DOCKERFILE" \
|
|
||||||
-t "$IMAGE_NAME" \
|
|
||||||
"${docker_build_args[@]}" \
|
|
||||||
"$PROJECT_ROOT/build/cross"
|
|
||||||
fi
|
|
||||||
|
|
||||||
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 "ONE_KVM_GRADLE_DISTRIBUTION_URL=$gradle_distribution_url" \
|
|
||||||
-e "ONE_KVM_GRADLE_DISTRIBUTION_URL_CN=$gradle_distribution_url_cn" \
|
|
||||||
-e "ONE_KVM_GRADLE_NETWORK_TIMEOUT=$gradle_network_timeout" \
|
|
||||||
"$IMAGE_NAME" \
|
|
||||||
"$arch"
|
|
||||||
}
|
|
||||||
|
|
||||||
[[ -f "$DOCKERFILE" ]] || fail "Android Dockerfile not found: $DOCKERFILE"
|
|
||||||
command -v docker >/dev/null 2>&1 || fail "docker is required"
|
|
||||||
|
|
||||||
case "${1:-all}" in
|
|
||||||
all)
|
|
||||||
build_android all
|
|
||||||
;;
|
|
||||||
arm64)
|
|
||||||
build_android arm64
|
|
||||||
;;
|
|
||||||
armv7)
|
|
||||||
build_android armv7
|
|
||||||
;;
|
|
||||||
help | --help | -h)
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: build/build-android.sh [arch|help]
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
all (default) Build arm64 and armv7 APKs
|
|
||||||
arm64 Build only arm64 APK
|
|
||||||
armv7 Build only ARMv7 APK
|
|
||||||
help Show this help
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
build/build-android.sh
|
|
||||||
build/build-android.sh arm64
|
|
||||||
CHINAMIRRO=1 build/build-android.sh all
|
|
||||||
CHINAMIRRO=1 ONE_KVM_GRADLE_DISTRIBUTION_URL=https://mirrors.aliyun.com/macports/distfiles/gradle/gradle-9.1.0-bin.zip build/build-android.sh all
|
|
||||||
|
|
||||||
Environment:
|
|
||||||
ONE_KVM_ANDROID_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:
|
|
||||||
target/android/one-kvm_<version>_<arm32|arm64>.apk
|
|
||||||
EOF
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
fail "Unknown argument: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -21,16 +21,21 @@ build_arch() {
|
|||||||
|
|
||||||
case "${CHINAMIRRO:-}" in
|
case "${CHINAMIRRO:-}" in
|
||||||
1|true|TRUE|yes|YES|on|ON)
|
1|true|TRUE|yes|YES|on|ON)
|
||||||
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--build-arg CHINAMIRRO=1"
|
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:12}"
|
||||||
echo "=== China mirror acceleration: enabled (Tsinghua) ==="
|
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) ==="
|
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
||||||
env \
|
env \
|
||||||
CROSS_BUILD_OPTS="$cross_build_opts" \
|
CROSS_BUILD_OPTS="$cross_build_opts" \
|
||||||
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=tuna \
|
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=rsproxy-sparse \
|
||||||
CARGO_SOURCE_TUNA_REGISTRY=sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/ \
|
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 \
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup \
|
CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||||
RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup \
|
RUSTUP_DIST_SERVER=https://rsproxy.cn \
|
||||||
|
RUSTUP_UPDATE_ROOT=https://rsproxy.cn/rustup \
|
||||||
cross build --release --target "$rust_target"
|
cross build --release --target "$rust_target"
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
@@ -66,7 +71,7 @@ case "${1:-all}" in
|
|||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 # Build all"
|
echo " $0 # Build all"
|
||||||
echo " $0 x86_64 # Build x86_64 only"
|
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
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -83,16 +88,4 @@ for target in "${ARCH_MAP[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo ""
|
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"
|
echo "Next step: ./build/package-docker.sh or ./build/package-deb.sh"
|
||||||
|
|||||||
@@ -1,309 +0,0 @@
|
|||||||
# Android build image for One-KVM
|
|
||||||
# Based on Debian 11 for stable toolchain/runtime compatibility
|
|
||||||
|
|
||||||
FROM debian:11
|
|
||||||
|
|
||||||
ARG CHINAMIRRO=0
|
|
||||||
ARG ANDROID_SDK_ROOT=/root/android-sdk
|
|
||||||
ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708_latest
|
|
||||||
ARG ANDROID_NDK_VERSION=27.3.13750724
|
|
||||||
ARG ANDROID_PLATFORM=36
|
|
||||||
ARG ANDROID_BUILD_TOOLS=36.0.0
|
|
||||||
ARG CARGO_NDK_VERSION=4.1.2
|
|
||||||
ARG RUSTUP_DIST_SERVER_CN=https://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 MAVEN_REPOSITORY_CN=https://maven.aliyun.com/repository/public
|
|
||||||
ARG GOOGLE_MAVEN_REPOSITORY_CN=https://maven.aliyun.com/repository/google
|
|
||||||
ARG GRADLE_DISTRIBUTION_URL_CN=https://mirrors.cloud.tencent.com/gradle/gradle-9.1.0-bin.zip
|
|
||||||
ARG ANDROID_CMDLINE_TOOLS_URL=
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
ENV ANDROID_HOME=${ANDROID_SDK_ROOT}
|
|
||||||
ENV ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT}
|
|
||||||
ENV ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}
|
|
||||||
ENV ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}
|
|
||||||
ENV ANDROID_BUILD_TOOLS=${ANDROID_BUILD_TOOLS}
|
|
||||||
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
|
||||||
ENV PATH=/root/.cargo/bin:${PATH}
|
|
||||||
ENV ONE_KVM_GRADLE_DISTRIBUTION_URL_CN=${GRADLE_DISTRIBUTION_URL_CN}
|
|
||||||
|
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
|
||||||
sed -i \
|
|
||||||
-e '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' \
|
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
unzip \
|
|
||||||
zip \
|
|
||||||
git \
|
|
||||||
bash \
|
|
||||||
build-essential \
|
|
||||||
pkg-config \
|
|
||||||
cmake \
|
|
||||||
ninja-build \
|
|
||||||
autoconf \
|
|
||||||
automake \
|
|
||||||
libtool \
|
|
||||||
nasm \
|
|
||||||
yasm \
|
|
||||||
python3 \
|
|
||||||
openjdk-17-jdk-headless \
|
|
||||||
libstdc++6 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
|
||||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
|
||||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
|
||||||
mkdir -p /root/.cargo; \
|
|
||||||
printf '%s\n' \
|
|
||||||
'[source.crates-io]' \
|
|
||||||
"replace-with = 'tuna'" \
|
|
||||||
'[source.tuna]' \
|
|
||||||
"registry = '${CARGO_REGISTRY_CN}'" \
|
|
||||||
'[registries.tuna]' \
|
|
||||||
"index = '${CARGO_REGISTRY_CN}'" \
|
|
||||||
> /root/.cargo/config.toml; \
|
|
||||||
fi \
|
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
|
||||||
&& cargo install cargo-ndk --version ${CARGO_NDK_VERSION} --locked \
|
|
||||||
&& rustup target add armv7-linux-androideabi aarch64-linux-android
|
|
||||||
|
|
||||||
RUN mkdir -p /opt/android-cmdline-tools \
|
|
||||||
&& cd /tmp \
|
|
||||||
&& if [ -n "$ANDROID_CMDLINE_TOOLS_URL" ]; then \
|
|
||||||
wget -q "$ANDROID_CMDLINE_TOOLS_URL" -O cmdline-tools.zip; \
|
|
||||||
else \
|
|
||||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}.zip -O cmdline-tools.zip; \
|
|
||||||
fi \
|
|
||||||
&& unzip -q cmdline-tools.zip -d /opt/android-cmdline-tools \
|
|
||||||
&& mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools/latest \
|
|
||||||
&& mv /opt/android-cmdline-tools/cmdline-tools/* ${ANDROID_SDK_ROOT}/cmdline-tools/latest/ \
|
|
||||||
&& rm -rf /tmp/cmdline-tools.zip /opt/android-cmdline-tools
|
|
||||||
|
|
||||||
RUN mkdir -p ${ANDROID_SDK_ROOT}/licenses \
|
|
||||||
&& yes | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} --licenses >/dev/null \
|
|
||||||
&& ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} \
|
|
||||||
"platform-tools" \
|
|
||||||
"platforms;android-${ANDROID_PLATFORM}" \
|
|
||||||
"build-tools;${ANDROID_BUILD_TOOLS}" \
|
|
||||||
"ndk;${ANDROID_NDK_VERSION}" \
|
|
||||||
"cmake;3.22.1" \
|
|
||||||
&& mkdir -p ${ANDROID_NDK_HOME}
|
|
||||||
|
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
|
||||||
mkdir -p /root/.gradle; \
|
|
||||||
printf '%s\n' \
|
|
||||||
"beforeSettings { settings ->" \
|
|
||||||
" settings.pluginManagement.repositories.maven { url = uri('${GOOGLE_MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
" settings.pluginManagement.repositories.maven { url = uri('${MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
" settings.dependencyResolutionManagement.repositories.maven { url = uri('${GOOGLE_MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
" settings.dependencyResolutionManagement.repositories.maven { url = uri('${MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
"}" \
|
|
||||||
"allprojects {" \
|
|
||||||
" buildscript.repositories.maven { url = uri('${GOOGLE_MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
" buildscript.repositories.maven { url = uri('${MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
"}" \
|
|
||||||
> /root/.gradle/init.gradle; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
libclang-dev \
|
|
||||||
llvm \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
ENV LIBCLANG_PATH=/usr/lib/llvm-11/lib
|
|
||||||
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'#!/usr/bin/env bash' \
|
|
||||||
'set -euo pipefail' \
|
|
||||||
'' \
|
|
||||||
'PROJECT_ROOT="${ONE_KVM_ANDROID_PROJECT_ROOT:-/workspace}"' \
|
|
||||||
'ANDROID_DIR="${PROJECT_ROOT}/android"' \
|
|
||||||
'BUILD_TYPE="release"' \
|
|
||||||
'ARCH="${1:-all}"' \
|
|
||||||
'FFMPEG_ROOT="${ONE_KVM_ANDROID_FFMPEG_ROOT:-${PROJECT_ROOT}/dist/android-ffmpeg-mediacodec}"' \
|
|
||||||
'OUTPUT_DIR="${PROJECT_ROOT}/target/android"' \
|
|
||||||
'SIGNING_DIR="${PROJECT_ROOT}/target/android-signing"' \
|
|
||||||
'KEYSTORE_PATH="${SIGNING_DIR}/one-kvm-release.jks"' \
|
|
||||||
'KEY_ALIAS="one-kvm-release"' \
|
|
||||||
'KEY_PASSWORD="one-kvm-release"' \
|
|
||||||
'ANDROID_BUILD_TOOLS_DIR="${ANDROID_SDK_ROOT}/build-tools/${ANDROID_BUILD_TOOLS}"' \
|
|
||||||
'WRAPPER_PROPERTIES="$ANDROID_DIR/gradle/wrapper/gradle-wrapper.properties"' \
|
|
||||||
'GRADLE_DISTRIBUTION_URL="${ONE_KVM_GRADLE_DISTRIBUTION_URL:-}"' \
|
|
||||||
'GRADLE_DISTRIBUTION_URL_CN="${ONE_KVM_GRADLE_DISTRIBUTION_URL_CN:-https://mirrors.cloud.tencent.com/gradle/gradle-9.1.0-bin.zip}"' \
|
|
||||||
'GRADLE_NETWORK_TIMEOUT="${ONE_KVM_GRADLE_NETWORK_TIMEOUT:-120000}"' \
|
|
||||||
'' \
|
|
||||||
'usage() {' \
|
|
||||||
' cat <<EOF' \
|
|
||||||
'Usage:' \
|
|
||||||
' docker run --rm -v "$PWD:/workspace" one-kvm-android-build:cn [arm64|armv7|all|help]' \
|
|
||||||
'' \
|
|
||||||
'Commands:' \
|
|
||||||
' all Build arm64 and armv7 APKs. Default.' \
|
|
||||||
' arm64 Build only arm64 APK.' \
|
|
||||||
' armv7 Build only ARMv7 APK.' \
|
|
||||||
' help Show this help.' \
|
|
||||||
'' \
|
|
||||||
'APK output:' \
|
|
||||||
' target/android/one-kvm_<version>_<arm32|arm64>.apk' \
|
|
||||||
'EOF' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'fail() {' \
|
|
||||||
' echo "Error: $*" >&2' \
|
|
||||||
' exit 1' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'read_project_version() {' \
|
|
||||||
' local version' \
|
|
||||||
' version="$(awk -F "\"" '"'"'/^version[[:space:]]*=/ { print $2; exit }'"'"' "$PROJECT_ROOT/Cargo.toml")"' \
|
|
||||||
' [[ -n "$version" ]] || fail "Failed to resolve version from $PROJECT_ROOT/Cargo.toml"' \
|
|
||||||
' printf "%s\n" "$version"' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'copy_apks() {' \
|
|
||||||
' local flavor="$1"' \
|
|
||||||
' local src_dir="$ANDROID_DIR/app/build/outputs/apk/$flavor/$BUILD_TYPE"' \
|
|
||||||
' local found=0' \
|
|
||||||
' mkdir -p "$OUTPUT_DIR"' \
|
|
||||||
' for apk in "$src_dir"/*.apk; do' \
|
|
||||||
' [[ -f "$apk" ]] || continue' \
|
|
||||||
' sign_apk "$apk" "$OUTPUT_DIR/one-kvm_${PROJECT_VERSION}_${flavor}.apk"' \
|
|
||||||
' found=1' \
|
|
||||||
' done' \
|
|
||||||
' [[ "$found" == "1" ]] || fail "No APK files found in: $src_dir"' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'ensure_keystore() {' \
|
|
||||||
' if [[ -f "$KEYSTORE_PATH" ]]; then' \
|
|
||||||
' return' \
|
|
||||||
' fi' \
|
|
||||||
' mkdir -p "$SIGNING_DIR"' \
|
|
||||||
' keytool -genkeypair -noprompt -keystore "$KEYSTORE_PATH" -storetype PKCS12 -alias "$KEY_ALIAS" -keyalg RSA -keysize 2048 -validity 10000 -storepass "$KEY_PASSWORD" -keypass "$KEY_PASSWORD" -dname "CN=One-KVM, OU=One-KVM, O=One-KVM, L=Local, S=Local, C=US" >/dev/null' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'sign_apk() {' \
|
|
||||||
' local input_apk="$1"' \
|
|
||||||
' local output_apk="$2"' \
|
|
||||||
' local aligned_apk' \
|
|
||||||
' aligned_apk="$(mktemp --suffix=.apk)"' \
|
|
||||||
' "$ANDROID_BUILD_TOOLS_DIR/zipalign" -f -p 4 "$input_apk" "$aligned_apk"' \
|
|
||||||
' "$ANDROID_BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE_PATH" --ks-key-alias "$KEY_ALIAS" --ks-pass "pass:$KEY_PASSWORD" --key-pass "pass:$KEY_PASSWORD" --out "$output_apk" "$aligned_apk"' \
|
|
||||||
' "$ANDROID_BUILD_TOOLS_DIR/apksigner" verify --verbose "$output_apk" >/dev/null' \
|
|
||||||
' rm -f "$aligned_apk"' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'cd "$PROJECT_ROOT"' \
|
|
||||||
'' \
|
|
||||||
'case "$ARCH" in' \
|
|
||||||
'help | --help | -h)' \
|
|
||||||
' usage' \
|
|
||||||
' exit 0' \
|
|
||||||
' ;;' \
|
|
||||||
'esac' \
|
|
||||||
'' \
|
|
||||||
'[[ -d "$ANDROID_DIR" ]] || fail "Android project not found: $ANDROID_DIR"' \
|
|
||||||
'[[ -x "$ANDROID_DIR/gradlew" ]] || fail "Gradle wrapper is not executable: $ANDROID_DIR/gradlew"' \
|
|
||||||
'[[ -f "$WRAPPER_PROPERTIES" ]] || fail "Gradle wrapper properties not found: $WRAPPER_PROPERTIES"' \
|
|
||||||
'' \
|
|
||||||
'ORIGINAL_WRAPPER_PROPERTIES="$(mktemp)"' \
|
|
||||||
'cp "$WRAPPER_PROPERTIES" "$ORIGINAL_WRAPPER_PROPERTIES"' \
|
|
||||||
'cleanup_wrapper_properties() {' \
|
|
||||||
' cp "$ORIGINAL_WRAPPER_PROPERTIES" "$WRAPPER_PROPERTIES"' \
|
|
||||||
' rm -f "$ORIGINAL_WRAPPER_PROPERTIES"' \
|
|
||||||
'}' \
|
|
||||||
'trap cleanup_wrapper_properties EXIT' \
|
|
||||||
'' \
|
|
||||||
'if [[ "${CHINAMIRRO:-0}" == "1" && -z "$GRADLE_DISTRIBUTION_URL" ]]; then' \
|
|
||||||
' GRADLE_DISTRIBUTION_URL="$GRADLE_DISTRIBUTION_URL_CN"' \
|
|
||||||
'fi' \
|
|
||||||
'' \
|
|
||||||
'if [[ -n "$GRADLE_DISTRIBUTION_URL" ]]; then' \
|
|
||||||
' WRAPPER_PROPERTIES_TMP="$(mktemp)"' \
|
|
||||||
' awk -v url="$GRADLE_DISTRIBUTION_URL" -v timeout="$GRADLE_NETWORK_TIMEOUT" '"'"'' \
|
|
||||||
' BEGIN { seen_url = 0; seen_timeout = 0 }' \
|
|
||||||
' /^distributionUrl=/ { print "distributionUrl=" url; seen_url = 1; next }' \
|
|
||||||
' /^networkTimeout=/ { print "networkTimeout=" timeout; seen_timeout = 1; next }' \
|
|
||||||
' { print }' \
|
|
||||||
' END {' \
|
|
||||||
' if (!seen_url) print "distributionUrl=" url;' \
|
|
||||||
' if (!seen_timeout) print "networkTimeout=" timeout;' \
|
|
||||||
' }' \
|
|
||||||
' '"'"' "$WRAPPER_PROPERTIES" > "$WRAPPER_PROPERTIES_TMP"' \
|
|
||||||
' cp "$WRAPPER_PROPERTIES_TMP" "$WRAPPER_PROPERTIES"' \
|
|
||||||
' rm -f "$WRAPPER_PROPERTIES_TMP"' \
|
|
||||||
' find /root/.gradle/wrapper/dists -name "*.lck" -o -name "*.part" 2>/dev/null | xargs -r rm -f' \
|
|
||||||
'fi' \
|
|
||||||
'' \
|
|
||||||
'ensure_keystore' \
|
|
||||||
'' \
|
|
||||||
'case "$ARCH" in' \
|
|
||||||
'arm64)' \
|
|
||||||
' ANDROID_ABIS="arm64-v8a"' \
|
|
||||||
' GRADLE_TASK=":app:assembleArm64Release"' \
|
|
||||||
' APK_FLAVORS="arm64"' \
|
|
||||||
' ;;' \
|
|
||||||
'armv7)' \
|
|
||||||
' ANDROID_ABIS="armeabi-v7a"' \
|
|
||||||
' GRADLE_TASK=":app:assembleArm32Release"' \
|
|
||||||
' APK_FLAVORS="arm32"' \
|
|
||||||
' ;;' \
|
|
||||||
'all)' \
|
|
||||||
' ANDROID_ABIS="arm64-v8a,armeabi-v7a"' \
|
|
||||||
' GRADLE_TASK=":app:assembleRelease"' \
|
|
||||||
' APK_FLAVORS="arm64 arm32"' \
|
|
||||||
' ;;' \
|
|
||||||
'*) fail "Unsupported architecture: $ARCH (expected arm64, armv7, or all)" ;;' \
|
|
||||||
'esac' \
|
|
||||||
'' \
|
|
||||||
'printf "sdk.dir=%s\n" "$ANDROID_HOME" > "$ANDROID_DIR/local.properties"' \
|
|
||||||
'mkdir -p "$OUTPUT_DIR"' \
|
|
||||||
'PROJECT_VERSION="$(read_project_version)"' \
|
|
||||||
'' \
|
|
||||||
'export ONE_KVM_ANDROID_PROFILE="$BUILD_TYPE"' \
|
|
||||||
'export ONE_KVM_ANDROID_ABIS="$ANDROID_ABIS"' \
|
|
||||||
'export ONE_KVM_ANDROID_FFMPEG_ROOT="$FFMPEG_ROOT"' \
|
|
||||||
'export ANDROID_HOME' \
|
|
||||||
'export ANDROID_SDK_ROOT' \
|
|
||||||
'export ANDROID_NDK_HOME' \
|
|
||||||
'export ANDROID_NDK_ROOT' \
|
|
||||||
'' \
|
|
||||||
'echo "Building Android APK"' \
|
|
||||||
'echo " task: $GRADLE_TASK"' \
|
|
||||||
'echo " profile: $ONE_KVM_ANDROID_PROFILE"' \
|
|
||||||
'echo " version: $PROJECT_VERSION"' \
|
|
||||||
'echo " abis: $ONE_KVM_ANDROID_ABIS"' \
|
|
||||||
'echo " output: $OUTPUT_DIR"' \
|
|
||||||
'echo " sdk: $ANDROID_HOME"' \
|
|
||||||
'echo " ndk: $ANDROID_NDK_HOME"' \
|
|
||||||
'echo " build tools: $ANDROID_BUILD_TOOLS_DIR"' \
|
|
||||||
'echo " ffmpeg root: $ONE_KVM_ANDROID_FFMPEG_ROOT"' \
|
|
||||||
'if [[ -n "$GRADLE_DISTRIBUTION_URL" ]]; then' \
|
|
||||||
' echo " gradle distribution: $GRADLE_DISTRIBUTION_URL"' \
|
|
||||||
'fi' \
|
|
||||||
'' \
|
|
||||||
'(' \
|
|
||||||
' cd "$ANDROID_DIR"' \
|
|
||||||
' ./gradlew "$GRADLE_TASK"' \
|
|
||||||
')' \
|
|
||||||
'' \
|
|
||||||
'for flavor in $APK_FLAVORS; do' \
|
|
||||||
' copy_apks "$flavor"' \
|
|
||||||
'done' \
|
|
||||||
'' \
|
|
||||||
'echo' \
|
|
||||||
'echo "APK output:"' \
|
|
||||||
'ls -1 "$OUTPUT_DIR"' \
|
|
||||||
> /usr/local/bin/build-one-kvm-android \
|
|
||||||
&& chmod +x /usr/local/bin/build-one-kvm-android
|
|
||||||
|
|
||||||
WORKDIR /workspace
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/build-one-kvm-android"]
|
|
||||||
CMD ["all"]
|
|
||||||
@@ -1,19 +1,29 @@
|
|||||||
# Cross-compilation image for ARM64 based on Debian 11
|
# Cross-compilation image for ARM64 based on Debian 12
|
||||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||||
|
|
||||||
FROM debian:11
|
ARG DEBIAN_IMAGE=debian:12
|
||||||
|
FROM ${DEBIAN_IMAGE}
|
||||||
|
|
||||||
# Linux headers used by v4l2r bindgen
|
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
|
||||||
ARG LINUX_HEADERS_SHA256=
|
|
||||||
ARG CHINAMIRRO=0
|
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 RKMPP_BRANCH=jellyfin-mpp
|
||||||
|
ARG RKRGA_BRANCH=jellyfin-rga
|
||||||
|
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 \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
sed -i \
|
sed -i -E \
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
/etc/apt/sources.list.d/debian.sources; \
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
@@ -21,19 +31,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
mkdir -p /root/.cargo; \
|
mkdir -p /root/.cargo; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[source.crates-io]' \
|
'[source.crates-io]' \
|
||||||
"replace-with = 'tuna'" \
|
"replace-with = 'rsproxy-sparse'" \
|
||||||
'[source.tuna]' \
|
'[source.rsproxy]' \
|
||||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_INDEX_CN}'" \
|
||||||
'[registries.tuna]' \
|
'[source.rsproxy-sparse]' \
|
||||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||||
|
'[registries.rsproxy]' \
|
||||||
|
"index = '${CARGO_INDEX_CN}'" \
|
||||||
|
'[net]' \
|
||||||
|
'git-fetch-with-cli = true' \
|
||||||
> /root/.cargo/config.toml; \
|
> /root/.cargo/config.toml; \
|
||||||
fi \
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -69,6 +83,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
# Install ARM64 development libraries (without VAAPI/X11 for ARM)
|
# Install ARM64 development libraries (without VAAPI/X11 for ARM)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libssl3 \
|
||||||
|
libssl3:arm64 \
|
||||||
libasound2-dev:arm64 \
|
libasound2-dev:arm64 \
|
||||||
libv4l-dev:arm64 \
|
libv4l-dev:arm64 \
|
||||||
libudev-dev:arm64 \
|
libudev-dev:arm64 \
|
||||||
@@ -78,20 +94,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libdrm-dev:arm64 \
|
libdrm-dev:arm64 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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)
|
# 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 \
|
&& 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 \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||||
@@ -105,8 +115,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& rm -rf /tmp/libjpeg-turbo
|
&& rm -rf /tmp/libjpeg-turbo
|
||||||
|
|
||||||
# Build static libyuv from source (cross-compile for ARM64)
|
# 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 \
|
&& 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 \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||||
@@ -125,8 +140,13 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
|||||||
|
|
||||||
# Build static libvpx from source (cross-compile for ARM64)
|
# Build static libvpx from source (cross-compile for ARM64)
|
||||||
# CC/CXX/LD/AR must be environment variables, not configure arguments
|
# 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 \
|
&& 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 ===" \
|
&& echo "=== libvpx: Configuring for ARM64 ===" \
|
||||||
&& export CC=aarch64-linux-gnu-gcc \
|
&& export CC=aarch64-linux-gnu-gcc \
|
||||||
&& export CXX=aarch64-linux-gnu-g++ \
|
&& export CXX=aarch64-linux-gnu-g++ \
|
||||||
@@ -149,7 +169,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
|||||||
&& rm -rf /tmp/libvpx
|
&& rm -rf /tmp/libvpx
|
||||||
|
|
||||||
# Build static libx264 from source (cross-compile for ARM64)
|
# 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 \
|
&& cd /tmp/x264 \
|
||||||
&& export CC=aarch64-linux-gnu-gcc \
|
&& export CC=aarch64-linux-gnu-gcc \
|
||||||
&& export AR=aarch64-linux-gnu-ar \
|
&& export AR=aarch64-linux-gnu-ar \
|
||||||
@@ -165,12 +187,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
|||||||
&& rm -rf /tmp/x264
|
&& rm -rf /tmp/x264
|
||||||
|
|
||||||
# Build static libx265 from source (cross-compile for ARM64)
|
# 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 \
|
&& 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 \
|
&& cd source \
|
||||||
&& mkdir -p build \
|
&& mkdir -p build \
|
||||||
&& cd build \
|
&& cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||||
-DCMAKE_SYSTEM_NAME=Linux \
|
-DCMAKE_SYSTEM_NAME=Linux \
|
||||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||||
@@ -204,12 +232,15 @@ Cflags: -I\${includedir}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build static libopus from source (cross-compile for ARM64)
|
# 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 \
|
&& cd /tmp/opus \
|
||||||
&& export CC=aarch64-linux-gnu-gcc \
|
&& export CC=aarch64-linux-gnu-gcc \
|
||||||
&& export AR=aarch64-linux-gnu-ar \
|
&& export AR=aarch64-linux-gnu-ar \
|
||||||
&& export RANLIB=aarch64-linux-gnu-ranlib \
|
&& export RANLIB=aarch64-linux-gnu-ranlib \
|
||||||
&& ./autogen.sh \
|
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--prefix=/usr/aarch64-linux-gnu \
|
--prefix=/usr/aarch64-linux-gnu \
|
||||||
--host=aarch64-linux-gnu \
|
--host=aarch64-linux-gnu \
|
||||||
@@ -217,13 +248,20 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
|||||||
--disable-doc \
|
--disable-doc \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/opus
|
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||||
|
|
||||||
# Download and build FFmpeg with RKMPP support
|
# Download and build FFmpeg with RKMPP support
|
||||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
&& github_prefix="" \
|
||||||
&& tar -xzf ffmpeg.tar.gz \
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
&& cd ffmpeg \
|
&& git clone --depth 1 --branch ${RKMPP_BRANCH} "${github_prefix}https://github.com/nyanmisaka/mpp.git" rkmpp \
|
||||||
|
&& git clone --depth 1 --branch ${RKRGA_BRANCH} "${github_prefix}https://github.com/nyanmisaka/rk-mirrors.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
|
# Build RKMPP
|
||||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||||
&& cmake .. \
|
&& cmake .. \
|
||||||
@@ -238,6 +276,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/rockchip_mpp.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/rockchip_mpp.pc \
|
||||||
|
&& rm -f /usr/aarch64-linux-gnu/lib/librockchip_mpp.so* \
|
||||||
&& cd ../.. \
|
&& cd ../.. \
|
||||||
# Build RKRGA - create cross file for meson
|
# Build RKRGA - create cross file for meson
|
||||||
&& echo '[binaries]' > /tmp/aarch64-cross.txt \
|
&& echo '[binaries]' > /tmp/aarch64-cross.txt \
|
||||||
@@ -265,6 +304,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& ar rcs /usr/aarch64-linux-gnu/lib/librga.a $(find build -name '*.o') \
|
&& ar rcs /usr/aarch64-linux-gnu/lib/librga.a $(find build -name '*.o') \
|
||||||
&& ranlib /usr/aarch64-linux-gnu/lib/librga.a \
|
&& ranlib /usr/aarch64-linux-gnu/lib/librga.a \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/librga.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/librga.pc \
|
||||||
|
&& rm -f /usr/aarch64-linux-gnu/lib/librga.so* \
|
||||||
&& cd .. \
|
&& cd .. \
|
||||||
# Create pkg-config wrapper for cross-compilation
|
# Create pkg-config wrapper for cross-compilation
|
||||||
&& echo '#!/bin/sh' > /tmp/aarch64-pkg-config \
|
&& echo '#!/bin/sh' > /tmp/aarch64-pkg-config \
|
||||||
@@ -354,8 +394,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& rustup target add aarch64-unknown-linux-gnu
|
&& rustup target add aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
@@ -371,4 +411,4 @@ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
|||||||
LIBYUV_STATIC=1 \
|
LIBYUV_STATIC=1 \
|
||||||
OPUS_STATIC=1 \
|
OPUS_STATIC=1 \
|
||||||
PKG_CONFIG_ALL_STATIC=1 \
|
PKG_CONFIG_ALL_STATIC=1 \
|
||||||
RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc"
|
RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc -C link-arg=-Wl,--allow-multiple-definition"
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
# Cross-compilation image for ARMv7 based on Debian 11
|
# Cross-compilation image for ARMv7 based on Debian 12
|
||||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||||
|
|
||||||
FROM debian:11
|
ARG DEBIAN_IMAGE=debian:12
|
||||||
|
FROM ${DEBIAN_IMAGE}
|
||||||
|
|
||||||
# Linux headers used by v4l2r bindgen
|
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
|
||||||
ARG LINUX_HEADERS_SHA256=
|
|
||||||
ARG CHINAMIRRO=0
|
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 RKMPP_BRANCH=jellyfin-mpp
|
||||||
|
ARG RKRGA_BRANCH=jellyfin-rga
|
||||||
|
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 \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
sed -i \
|
sed -i -E \
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
/etc/apt/sources.list.d/debian.sources; \
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
@@ -21,19 +31,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
mkdir -p /root/.cargo; \
|
mkdir -p /root/.cargo; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[source.crates-io]' \
|
'[source.crates-io]' \
|
||||||
"replace-with = 'tuna'" \
|
"replace-with = 'rsproxy-sparse'" \
|
||||||
'[source.tuna]' \
|
'[source.rsproxy]' \
|
||||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_INDEX_CN}'" \
|
||||||
'[registries.tuna]' \
|
'[source.rsproxy-sparse]' \
|
||||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||||
|
'[registries.rsproxy]' \
|
||||||
|
"index = '${CARGO_INDEX_CN}'" \
|
||||||
|
'[net]' \
|
||||||
|
'git-fetch-with-cli = true' \
|
||||||
> /root/.cargo/config.toml; \
|
> /root/.cargo/config.toml; \
|
||||||
fi \
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -69,6 +83,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
# Install ARMv7 development libraries (without VAAPI/X11 for ARM)
|
# Install ARMv7 development libraries (without VAAPI/X11 for ARM)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libssl3 \
|
||||||
|
libssl3:armhf \
|
||||||
libasound2-dev:armhf \
|
libasound2-dev:armhf \
|
||||||
libv4l-dev:armhf \
|
libv4l-dev:armhf \
|
||||||
libudev-dev:armhf \
|
libudev-dev:armhf \
|
||||||
@@ -77,20 +93,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libdrm-dev:armhf \
|
libdrm-dev:armhf \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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)
|
# 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 \
|
&& 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 \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||||
@@ -104,8 +114,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& rm -rf /tmp/libjpeg-turbo
|
&& rm -rf /tmp/libjpeg-turbo
|
||||||
|
|
||||||
# Build static libyuv from source (cross-compile for ARMv7)
|
# 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 \
|
&& 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 \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||||
@@ -123,8 +138,13 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
|||||||
&& rm -rf /tmp/libyuv
|
&& rm -rf /tmp/libyuv
|
||||||
|
|
||||||
# Build static libvpx from source (cross-compile for ARMv7)
|
# 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 \
|
&& 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 CC=arm-linux-gnueabihf-gcc \
|
||||||
&& export CXX=arm-linux-gnueabihf-g++ \
|
&& export CXX=arm-linux-gnueabihf-g++ \
|
||||||
&& export LD=arm-linux-gnueabihf-ld \
|
&& export LD=arm-linux-gnueabihf-ld \
|
||||||
@@ -141,7 +161,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
|||||||
&& rm -rf /tmp/libvpx
|
&& rm -rf /tmp/libvpx
|
||||||
|
|
||||||
# Build static libx264 from source (cross-compile for ARMv7)
|
# 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 \
|
&& cd /tmp/x264 \
|
||||||
&& export CC=arm-linux-gnueabihf-gcc \
|
&& export CC=arm-linux-gnueabihf-gcc \
|
||||||
&& export AR=arm-linux-gnueabihf-ar \
|
&& export AR=arm-linux-gnueabihf-ar \
|
||||||
@@ -158,12 +180,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
|||||||
&& rm -rf /tmp/x264
|
&& rm -rf /tmp/x264
|
||||||
|
|
||||||
# Build static libx265 from source (cross-compile for ARMv7)
|
# 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 \
|
&& 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 \
|
&& cd source \
|
||||||
&& mkdir -p build \
|
&& mkdir -p build \
|
||||||
&& cd build \
|
&& cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||||
-DCMAKE_SYSTEM_NAME=Linux \
|
-DCMAKE_SYSTEM_NAME=Linux \
|
||||||
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||||
@@ -193,12 +221,15 @@ Cflags: -I\${includedir}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build static libopus from source (cross-compile for ARMv7)
|
# 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 \
|
&& cd /tmp/opus \
|
||||||
&& export CC=arm-linux-gnueabihf-gcc \
|
&& export CC=arm-linux-gnueabihf-gcc \
|
||||||
&& export AR=arm-linux-gnueabihf-ar \
|
&& export AR=arm-linux-gnueabihf-ar \
|
||||||
&& export RANLIB=arm-linux-gnueabihf-ranlib \
|
&& export RANLIB=arm-linux-gnueabihf-ranlib \
|
||||||
&& ./autogen.sh \
|
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--prefix=/usr/arm-linux-gnueabihf \
|
--prefix=/usr/arm-linux-gnueabihf \
|
||||||
--host=arm-linux-gnueabihf \
|
--host=arm-linux-gnueabihf \
|
||||||
@@ -206,13 +237,20 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
|||||||
--disable-doc \
|
--disable-doc \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/opus
|
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||||
|
|
||||||
# Download and build FFmpeg with RKMPP support
|
# Download and build FFmpeg with RKMPP support
|
||||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
&& github_prefix="" \
|
||||||
&& tar -xzf ffmpeg.tar.gz \
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
&& cd ffmpeg \
|
&& git clone --depth 1 --branch ${RKMPP_BRANCH} "${github_prefix}https://github.com/nyanmisaka/mpp.git" rkmpp \
|
||||||
|
&& git clone --depth 1 --branch ${RKRGA_BRANCH} "${github_prefix}https://github.com/nyanmisaka/rk-mirrors.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
|
# Build RKMPP
|
||||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||||
&& cmake .. \
|
&& cmake .. \
|
||||||
@@ -227,6 +265,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/rockchip_mpp.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/rockchip_mpp.pc \
|
||||||
|
&& rm -f /usr/arm-linux-gnueabihf/lib/librockchip_mpp.so* \
|
||||||
&& cd ../.. \
|
&& cd ../.. \
|
||||||
# Build RKRGA - create cross file for meson
|
# Build RKRGA - create cross file for meson
|
||||||
&& echo '[binaries]' > /tmp/armhf-cross.txt \
|
&& echo '[binaries]' > /tmp/armhf-cross.txt \
|
||||||
@@ -254,6 +293,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& ar rcs /usr/arm-linux-gnueabihf/lib/librga.a $(find build -name '*.o') \
|
&& ar rcs /usr/arm-linux-gnueabihf/lib/librga.a $(find build -name '*.o') \
|
||||||
&& ranlib /usr/arm-linux-gnueabihf/lib/librga.a \
|
&& ranlib /usr/arm-linux-gnueabihf/lib/librga.a \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/librga.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/librga.pc \
|
||||||
|
&& rm -f /usr/arm-linux-gnueabihf/lib/librga.so* \
|
||||||
&& cd .. \
|
&& cd .. \
|
||||||
# Create pkg-config wrapper for cross-compilation
|
# Create pkg-config wrapper for cross-compilation
|
||||||
&& echo '#!/bin/sh' > /tmp/armhf-pkg-config \
|
&& echo '#!/bin/sh' > /tmp/armhf-pkg-config \
|
||||||
@@ -343,8 +383,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& rustup target add armv7-unknown-linux-gnueabihf
|
&& rustup target add armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
@@ -360,7 +400,7 @@ ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
|
|||||||
LIBYUV_STATIC=1 \
|
LIBYUV_STATIC=1 \
|
||||||
OPUS_STATIC=1 \
|
OPUS_STATIC=1 \
|
||||||
PKG_CONFIG_ALL_STATIC=1 \
|
PKG_CONFIG_ALL_STATIC=1 \
|
||||||
RUSTFLAGS="-C linker=arm-linux-gnueabihf-gcc"
|
RUSTFLAGS="-C linker=arm-linux-gnueabihf-gcc -C link-arg=-Wl,--allow-multiple-definition"
|
||||||
|
|
||||||
# Default command
|
# Default command
|
||||||
CMD ["bash"]
|
CMD ["bash"]
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
# Cross-compilation image for x86_64 based on Debian 11
|
# Cross-compilation image for x86_64 based on Debian 12
|
||||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||||
|
|
||||||
FROM debian:11
|
ARG DEBIAN_IMAGE=debian:12
|
||||||
|
FROM ${DEBIAN_IMAGE}
|
||||||
|
|
||||||
# Linux headers used by v4l2r bindgen
|
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
|
||||||
ARG LINUX_HEADERS_SHA256=
|
|
||||||
ARG CHINAMIRRO=0
|
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 \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
sed -i \
|
sed -i -E \
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
/etc/apt/sources.list.d/debian.sources; \
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
@@ -21,19 +29,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
mkdir -p /root/.cargo; \
|
mkdir -p /root/.cargo; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[source.crates-io]' \
|
'[source.crates-io]' \
|
||||||
"replace-with = 'tuna'" \
|
"replace-with = 'rsproxy-sparse'" \
|
||||||
'[source.tuna]' \
|
'[source.rsproxy]' \
|
||||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_INDEX_CN}'" \
|
||||||
'[registries.tuna]' \
|
'[source.rsproxy-sparse]' \
|
||||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||||
|
'[registries.rsproxy]' \
|
||||||
|
"index = '${CARGO_INDEX_CN}'" \
|
||||||
|
'[net]' \
|
||||||
|
'git-fetch-with-cli = true' \
|
||||||
> /root/.cargo/config.toml; \
|
> /root/.cargo/config.toml; \
|
||||||
fi \
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -76,20 +88,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libxdmcp-dev \
|
libxdmcp-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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)
|
# 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 \
|
&& 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 \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
-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
|
&& rm -rf /tmp/libjpeg-turbo
|
||||||
|
|
||||||
# Build static libyuv from source (uses libjpeg-turbo headers)
|
# 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 \
|
&& 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 \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DJPEG_FOUND=TRUE \
|
-DJPEG_FOUND=TRUE \
|
||||||
@@ -114,17 +125,25 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
|||||||
&& rm -rf /tmp/libyuv
|
&& rm -rf /tmp/libyuv
|
||||||
|
|
||||||
# Build static libvpx from source
|
# 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 \
|
&& 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 \
|
&& ./configure \
|
||||||
--enable-static --disable-shared --enable-pic \
|
--enable-static --disable-shared --enable-pic \
|
||||||
--disable-examples --disable-tools --disable-docs \
|
--disable-examples --disable-tools --disable-docs \
|
||||||
|
--disable-unit-tests \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/libvpx
|
&& rm -rf /tmp/libvpx
|
||||||
|
|
||||||
# Build static libx264 from source
|
# 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 \
|
&& cd /tmp/x264 \
|
||||||
&& ./configure --enable-static --disable-cli \
|
&& ./configure --enable-static --disable-cli \
|
||||||
&& make -j$(nproc) \
|
&& 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
|
&& rm -rf /tmp/x264
|
||||||
|
|
||||||
# Build static libx265 from source
|
# 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 \
|
&& 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 \
|
&& cd source \
|
||||||
&& mkdir -p build \
|
&& mkdir -p build \
|
||||||
&& cd build \
|
&& cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||||
-DENABLE_SHARED=OFF \
|
-DENABLE_SHARED=OFF \
|
||||||
-DENABLE_CLI=OFF \
|
-DENABLE_CLI=OFF \
|
||||||
-DBUILD_SHARED_LIBS=OFF \
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
@@ -162,21 +187,28 @@ Cflags: -I\${includedir}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build static libopus from source
|
# 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 \
|
&& cd /tmp/opus \
|
||||||
&& ./autogen.sh \
|
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--enable-static --disable-shared \
|
--enable-static --disable-shared \
|
||||||
--disable-doc \
|
--disable-doc \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/opus
|
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||||
|
|
||||||
# Download and build FFmpeg with minimal configuration for encoding only
|
# Download and build FFmpeg with minimal configuration for encoding only
|
||||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
&& github_prefix="" \
|
||||||
&& tar -xzf ffmpeg.tar.gz \
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
&& cd ffmpeg/ffmpeg-rockchip \
|
&& 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 \
|
&& ./configure \
|
||||||
--enable-gpl \
|
--enable-gpl \
|
||||||
--enable-version3 \
|
--enable-version3 \
|
||||||
@@ -249,8 +281,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& rustup target add x86_64-unknown-linux-gnu
|
&& rustup target add x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Maintainer: SilentWind <admin@mofeng.run>
|
|||||||
Package: one-kvm
|
Package: one-kvm
|
||||||
Architecture: {arch}
|
Architecture: {arch}
|
||||||
Depends: ${{auto}}, ca-certificates{distsuffix}
|
Depends: ${{auto}}, ca-certificates{distsuffix}
|
||||||
|
Recommends: bluez
|
||||||
Description: A open and lightweight IP-KVM solution written in Rust
|
Description: A open and lightweight IP-KVM solution written in Rust
|
||||||
Enables BIOS-level remote management of servers and workstations.
|
Enables BIOS-level remote management of servers and workstations.
|
||||||
.
|
.
|
||||||
@@ -17,6 +18,7 @@ Description: A open and lightweight IP-KVM solution written in Rust
|
|||||||
* Hardware-accelerated video encoding (VAAPI, QSV, RKMPP)
|
* Hardware-accelerated video encoding (VAAPI, QSV, RKMPP)
|
||||||
* WebRTC and MJPEG streaming with low latency
|
* WebRTC and MJPEG streaming with low latency
|
||||||
* USB HID emulation via OTG gadget
|
* USB HID emulation via OTG gadget
|
||||||
|
* Classic Bluetooth keyboard and mouse via BlueZ
|
||||||
* Mass storage device for ISO/IMG mounting
|
* Mass storage device for ISO/IMG mounting
|
||||||
* ATX power control via GPIO or USB relay
|
* ATX power control via GPIO or USB relay
|
||||||
Homepage: https://github.com/mofeng-git/One-KVM
|
Homepage: https://github.com/mofeng-git/One-KVM
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=One-KVM IP-KVM Service
|
Description=One-KVM IP-KVM Service
|
||||||
Documentation=https://github.com/mofeng-git/One-KVM
|
Documentation=https://github.com/mofeng-git/One-KVM
|
||||||
After=network.target
|
After=network-online.target bluetooth.service
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Build deb packages from pre-compiled binaries
|
# Build deb packages from pre-compiled binaries
|
||||||
# Binaries are compiled once on Debian 11 (GLIBC 2.31) via build-images.sh
|
# Binaries are compiled once on Debian 12 (GLIBC 2.36) via build-images.sh
|
||||||
# This script packages them directly on the host using dpkg-deb
|
# This script packages them directly on the host using dpkg-deb
|
||||||
# Usage: ./build/build-deb.sh [arch]
|
# Usage: ./build/build-deb.sh [arch]
|
||||||
# Example: ./build/build-deb.sh aarch64
|
# Example: ./build/build-deb.sh aarch64
|
||||||
@@ -125,8 +125,8 @@ EOF
|
|||||||
chmod 755 "$PKG_DIR/DEBIAN/prerm"
|
chmod 755 "$PKG_DIR/DEBIAN/prerm"
|
||||||
|
|
||||||
# Create control file
|
# Create control file
|
||||||
BASE_DEPS="libc6 (>= 2.31), libgcc-s1, libstdc++6, libasound2 (>= 1.1), libdrm2 (>= 2.4)"
|
BASE_DEPS="libc6 (>= 2.36), 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), libmfx1 (>= 21.1), libx11-6 (>= 1.6), libxcb1 (>= 1.14), i965-va-driver-shaders (>= 2.4), intel-media-va-driver-non-free (>= 21.1)"
|
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"
|
DEPS="$BASE_DEPS"
|
||||||
if [ "$DEB_ARCH" = "amd64" ]; then
|
if [ "$DEB_ARCH" = "amd64" ]; then
|
||||||
DEPS="$DEPS, $AMD64_DEPS"
|
DEPS="$DEPS, $AMD64_DEPS"
|
||||||
@@ -139,10 +139,11 @@ Section: admin
|
|||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: $DEB_ARCH
|
Architecture: $DEB_ARCH
|
||||||
Depends: $DEPS
|
Depends: $DEPS
|
||||||
|
Recommends: bluez
|
||||||
Maintainer: SilentWind <admin@mofeng.run>
|
Maintainer: SilentWind <admin@mofeng.run>
|
||||||
Description: A open and lightweight IP-KVM solution
|
Description: A open and lightweight IP-KVM solution
|
||||||
Enables BIOS-level remote management of servers and workstations.
|
Enables BIOS-level remote management of servers and workstations.
|
||||||
Built on Debian 11, compatible with Debian 11+, Ubuntu 20.04+.
|
Built on Debian 12, compatible with Debian 12+, Ubuntu 24.04+.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build deb directly on host
|
# Build deb directly on host
|
||||||
|
|||||||
@@ -314,6 +314,10 @@ build_for_platform() {
|
|||||||
|
|
||||||
if [ "$PUSH" = true ]; then
|
if [ "$PUSH" = true ]; then
|
||||||
build_cmd="$build_cmd --push"
|
build_cmd="$build_cmd --push"
|
||||||
|
# Aliyun ACR rejects the OCI artifact manifests used by BuildKit attestations.
|
||||||
|
if [[ "$REGISTRY" == registry.cn-*.aliyuncs.com/* ]]; then
|
||||||
|
build_cmd="$build_cmd --provenance=false --sbom=false"
|
||||||
|
fi
|
||||||
elif [ "$LOAD" = true ]; then
|
elif [ "$LOAD" = true ]; then
|
||||||
build_cmd="$build_cmd --load"
|
build_cmd="$build_cmd --load"
|
||||||
fi
|
fi
|
||||||
|
|||||||
24
docs/macos-drag-compatibility.md
Normal file
24
docs/macos-drag-compatibility.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# macOS 拖拽兼容模式(实验性)
|
||||||
|
|
||||||
|
适用于 OTG 和 CH9329 后端的绝对鼠标输入。遇到目标 Mac 上拖拽只能移动一小段便停止时,可在设置页的 HID 配置中开启「macOS 拖拽兼容模式」。默认关闭。
|
||||||
|
|
||||||
|
配置字段为 `hid.mouse_macos_drag`,旧 PR 的 `ch9329_macos_drag` 字段仍可作为读取别名。OTG 必须同时启用相对鼠标和绝对鼠标接口,否则配置保存会被拒绝。CH9329 同时启用 Linux 兼容开关时,此模式优先。
|
||||||
|
|
||||||
|
## 报告行为
|
||||||
|
|
||||||
|
- 未按键时仍使用绝对坐标定位。
|
||||||
|
- 从绝对输入开始的拖拽:按下使用绝对报告,移动和滚轮使用相对报告,释放时先更新相对按钮状态,再发送绝对按钮释放。
|
||||||
|
- 按键报告的通道保持到所有按钮释放,不随拖拽期间的输入模式切换而改变。
|
||||||
|
- 从相对输入开始的点击、拖拽继续使用相对按钮报告;此选项不修复原生相对模式的点击兼容问题。
|
||||||
|
- 位移从原始 15 位输入坐标换算,累计小数余量,大位移按单包范围分包并保留总量及方向。
|
||||||
|
- 运行时使用当前视频采集尺寸,并在 HID 重载后恢复尺寸;视频尚未提供尺寸时使用配置尺寸。
|
||||||
|
|
||||||
|
## 验证范围与限制
|
||||||
|
|
||||||
|
本次 OTG 扩展及位移换算修改没有 macOS 实机验证。原 PR 作者对旧 CH9329 实现的实机反馈不能视为本版本已经验证。
|
||||||
|
|
||||||
|
本地自动化覆盖位移累计/分包、1080p/4K 换算、往返移动、滚轮、输入模式切换、复位、CH9329 命令队列、OTG 临时文件模拟端点及配置校验。临时文件测试仅验证写出的字节,不验证 USB 枚举或目标系统的事件解释。
|
||||||
|
|
||||||
|
相对位移受 macOS 鼠标速度、加速、显示缩放及多屏布局影响,无法保证与绝对定位逐像素一致。松手报告携带最后的客户端绝对坐标,仍可能出现落点偏移或光标跳位;大位移分包也可能增加低波特率串口延迟。
|
||||||
|
|
||||||
|
后续实机验证应分别覆盖两个后端:窗口/文件/文本拖拽,快速及慢速移动,滚轮和多按钮组合,1080p/4K 与缩放显示,输入模式切换、断开重连、精确落点。异常时关闭此选项恢复默认行为。
|
||||||
16
libs/bluetooth-hid/Cargo.toml
Normal file
16
libs/bluetooth-hid/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "one-kvm-bluetooth-hid"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] }
|
||||||
|
dbus = { version = "0.9", features = ["vendored"] }
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "process"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
dbus-crossroads = "0.5"
|
||||||
|
dbus-tokio = "0.7"
|
||||||
|
libc = "0.2"
|
||||||
|
tracing = "0.1"
|
||||||
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//! Explicit hardware regression: opens a 10-second pairing window without
|
||||||
|
//! removing existing bonds or sending input. Stop the production HID first.
|
||||||
|
use one_kvm_bluetooth_hid::{Action, Config, Peripheral};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), String> {
|
||||||
|
let peripheral = Peripheral::start(Config {
|
||||||
|
adapter: "hci0".into(),
|
||||||
|
name: "One-KVM HID".into(),
|
||||||
|
peer: None,
|
||||||
|
})?;
|
||||||
|
let result = async {
|
||||||
|
for _ in 0..50 {
|
||||||
|
if peripheral.status().initialized {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
let before = peripheral.status();
|
||||||
|
if !before.initialized {
|
||||||
|
return Err(format!("Initialization failed: {:?}", before.error));
|
||||||
|
}
|
||||||
|
if !before
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.any(|d| Some(&d.address) == before.peer.as_ref() && d.paired)
|
||||||
|
{
|
||||||
|
return Err("This regression needs an existing bonded computer".into());
|
||||||
|
}
|
||||||
|
peripheral.action(Action::Pair(10)).await?;
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
let open = peripheral.status();
|
||||||
|
println!("open: {}", serde_json::to_string(&open).unwrap());
|
||||||
|
if open.pairing_seconds == 0 {
|
||||||
|
return Err("Existing bond prematurely closed pairing".into());
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||||
|
let expired = peripheral.status();
|
||||||
|
println!("expired: {}", serde_json::to_string(&expired).unwrap());
|
||||||
|
if expired.pairing_seconds != 0 || expired.peer != before.peer {
|
||||||
|
return Err("Window did not expire while preserving the bonded peer".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
let cleanup = peripheral.shutdown().await;
|
||||||
|
result.and(cleanup)
|
||||||
|
}
|
||||||
24
libs/bluetooth-hid/examples/probe.rs
Normal file
24
libs/bluetooth-hid/examples/probe.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//! Native hardware smoke check. Registers for 12 seconds, sends no input,
|
||||||
|
//! opens no pairing window, then restores the adapter configuration.
|
||||||
|
use one_kvm_bluetooth_hid::{Config, Peripheral};
|
||||||
|
use std::time::Duration;
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), String> {
|
||||||
|
let peripheral = Peripheral::start(Config {
|
||||||
|
adapter: "hci0".into(),
|
||||||
|
name: "One-KVM HID".into(),
|
||||||
|
peer: None,
|
||||||
|
})?;
|
||||||
|
let mut initialized = false;
|
||||||
|
for _ in 0..12 {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
let status = peripheral.status();
|
||||||
|
initialized |= status.initialized;
|
||||||
|
println!("{}", serde_json::to_string(&status).unwrap());
|
||||||
|
}
|
||||||
|
peripheral.shutdown().await?;
|
||||||
|
if !initialized {
|
||||||
|
return Err("Native BlueZ peripheral never initialized".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
236
libs/bluetooth-hid/src/agent.rs
Normal file
236
libs/bluetooth-hid/src/agent.rs
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
use crate::Shared;
|
||||||
|
use dbus::{channel::MatchingReceiver, message::MatchRule, nonblock::Proxy, Path};
|
||||||
|
use dbus_crossroads::Crossroads;
|
||||||
|
use std::{
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PATH: &str = "/org/onekvm/bluetooth/agent";
|
||||||
|
pub struct Agent {
|
||||||
|
connection: Arc<dbus::nonblock::SyncConnection>,
|
||||||
|
task: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
impl Agent {
|
||||||
|
pub async fn register(shared: Arc<Shared>, adapter: String) -> Result<Self, String> {
|
||||||
|
let (resource, connection) =
|
||||||
|
dbus_tokio::connection::new_system_sync().map_err(|e| e.to_string())?;
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let _ = resource.await;
|
||||||
|
});
|
||||||
|
let mut cr = Crossroads::new();
|
||||||
|
let iface = cr.register("org.bluez.Agent1", |b| {
|
||||||
|
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||||
|
b.method("Cancel", (), (), |_, _: &mut (), ()| Ok(()));
|
||||||
|
let state = shared.clone();
|
||||||
|
let name = adapter.clone();
|
||||||
|
b.method(
|
||||||
|
"RequestAuthorization",
|
||||||
|
("device",),
|
||||||
|
(),
|
||||||
|
move |_, _: &mut (), (path,): (Path<'static>,)| {
|
||||||
|
authorize(&state, &name, &path, true)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let state = shared.clone();
|
||||||
|
let name = adapter.clone();
|
||||||
|
b.method(
|
||||||
|
"RequestConfirmation",
|
||||||
|
("device", "passkey"),
|
||||||
|
(),
|
||||||
|
move |_, _: &mut (), (path, _): (Path<'static>, u32)| {
|
||||||
|
authorize(&state, &name, &path, true)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
b.method(
|
||||||
|
"AuthorizeService",
|
||||||
|
("device", "uuid"),
|
||||||
|
(),
|
||||||
|
move |_, _: &mut (), (path, uuid): (Path<'static>, String)| {
|
||||||
|
if !matches!(uuid.as_str(), "00001124-0000-1000-8000-00805f9b34fb") {
|
||||||
|
return Err(dbus::MethodErr::from((
|
||||||
|
"org.bluez.Error.Rejected",
|
||||||
|
"Not an HID service",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
authorize(&shared, &adapter, &path, false)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cr.insert(PATH, &[iface], ());
|
||||||
|
let profile = cr.register("org.bluez.Profile1", |b| {
|
||||||
|
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||||
|
b.method(
|
||||||
|
"RequestDisconnection",
|
||||||
|
("device",),
|
||||||
|
(),
|
||||||
|
|_, _: &mut (), (_device,): (Path<'static>,)| Ok(()),
|
||||||
|
);
|
||||||
|
b.method(
|
||||||
|
"NewConnection",
|
||||||
|
("device", "fd", "properties"),
|
||||||
|
(),
|
||||||
|
|_,
|
||||||
|
_: &mut (),
|
||||||
|
(_device, _fd, _props): (
|
||||||
|
Path<'static>,
|
||||||
|
dbus::arg::OwnedFd,
|
||||||
|
dbus::arg::PropMap,
|
||||||
|
)| {
|
||||||
|
// This profile only publishes SDP. Our L2CAP sockets own both HID channels.
|
||||||
|
Err::<(), _>(dbus::MethodErr::from((
|
||||||
|
"org.bluez.Error.Rejected",
|
||||||
|
"Unexpected profile connection",
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cr.insert("/org/onekvm/bluetooth/profile", &[profile], ());
|
||||||
|
connection.start_receive(
|
||||||
|
MatchRule::new_method_call(),
|
||||||
|
Box::new(move |msg, conn| {
|
||||||
|
let _ = cr.handle_message(msg, conn);
|
||||||
|
true
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let agent = Self { connection, task };
|
||||||
|
let proxy = Proxy::new(
|
||||||
|
"org.bluez",
|
||||||
|
"/org/bluez",
|
||||||
|
Duration::from_secs(5),
|
||||||
|
agent.connection.clone(),
|
||||||
|
);
|
||||||
|
let result: Result<(), dbus::Error> = proxy
|
||||||
|
.method_call(
|
||||||
|
"org.bluez.AgentManager1",
|
||||||
|
"RegisterAgent",
|
||||||
|
(Path::from(PATH), "NoInputNoOutput"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Err(error) = result {
|
||||||
|
return Err(error.to_string());
|
||||||
|
}
|
||||||
|
let result: Result<(), dbus::Error> = proxy
|
||||||
|
.method_call(
|
||||||
|
"org.bluez.AgentManager1",
|
||||||
|
"RequestDefaultAgent",
|
||||||
|
(Path::from(PATH),),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
result.map_err(|e| e.to_string())?;
|
||||||
|
let mut options: dbus::arg::PropMap = std::collections::HashMap::new();
|
||||||
|
options.insert(
|
||||||
|
"ServiceRecord".into(),
|
||||||
|
dbus::arg::Variant(Box::new(crate::protocol::sdp())),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"Role".into(),
|
||||||
|
dbus::arg::Variant(Box::new("server".to_string())),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"RequireAuthentication".into(),
|
||||||
|
dbus::arg::Variant(Box::new(true)),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"RequireAuthorization".into(),
|
||||||
|
dbus::arg::Variant(Box::new(false)),
|
||||||
|
);
|
||||||
|
let result: Result<(), dbus::Error> = proxy
|
||||||
|
.method_call(
|
||||||
|
"org.bluez.ProfileManager1",
|
||||||
|
"RegisterProfile",
|
||||||
|
(
|
||||||
|
Path::from("/org/onekvm/bluetooth/profile"),
|
||||||
|
crate::protocol::HID_UUID,
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
result.map_err(|e| format!("Register HID SDP: {e}"))?;
|
||||||
|
Ok(agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Agent {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.task.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn authorize(
|
||||||
|
shared: &Shared,
|
||||||
|
adapter: &str,
|
||||||
|
path: &str,
|
||||||
|
pairing: bool,
|
||||||
|
) -> Result<(), dbus::MethodErr> {
|
||||||
|
let rejected = || {
|
||||||
|
dbus::MethodErr::from((
|
||||||
|
"org.bluez.Error.Rejected",
|
||||||
|
"Open pairing in One-KVM or select the paired computer",
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let prefix = format!("/org/bluez/{adapter}/dev_");
|
||||||
|
let address = path
|
||||||
|
.strip_prefix(&prefix)
|
||||||
|
.ok_or_else(rejected)?
|
||||||
|
.replace('_', ":");
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
if pairing
|
||||||
|
&& !state
|
||||||
|
.pairing_until
|
||||||
|
.is_some_and(|until| until > Instant::now())
|
||||||
|
{
|
||||||
|
return Err(rejected());
|
||||||
|
}
|
||||||
|
if state.peer.as_ref().is_some_and(|peer| peer != &address) {
|
||||||
|
return Err(rejected());
|
||||||
|
}
|
||||||
|
if state.peer.is_none() {
|
||||||
|
if !pairing || state.bonded_before_pairing.contains(&address) {
|
||||||
|
return Err(rejected());
|
||||||
|
}
|
||||||
|
state.peer = Some(address);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn existing_unrelated_bond_cannot_be_adopted_by_agent_callback() {
|
||||||
|
let shared = Shared::new(None);
|
||||||
|
{
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state
|
||||||
|
.bonded_before_pairing
|
||||||
|
.insert("10:6F:D9:66:97:88".into());
|
||||||
|
}
|
||||||
|
assert!(authorize(
|
||||||
|
&shared,
|
||||||
|
"hci0",
|
||||||
|
"/org/bluez/hci0/dev_10_6F_D9_66_97_88",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(shared.state.lock().unwrap().peer.is_none());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn only_pair_during_window_and_pin_first_peer() {
|
||||||
|
let state = Shared::new(None);
|
||||||
|
let first = "/org/bluez/hci0/dev_10_6F_D9_66_97_88";
|
||||||
|
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||||
|
state.state.lock().unwrap().pairing_until = Some(Instant::now() + Duration::from_secs(60));
|
||||||
|
assert!(authorize(&state, "hci0", first, true).is_ok());
|
||||||
|
assert!(authorize(
|
||||||
|
&state,
|
||||||
|
"hci0",
|
||||||
|
"/org/bluez/hci0/dev_10_6F_D9_66_97_89",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(authorize(&state, "hci1", first, true).is_err());
|
||||||
|
state.state.lock().unwrap().pairing_until = None;
|
||||||
|
assert!(authorize(&state, "hci0", first, false).is_ok());
|
||||||
|
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
145
libs/bluetooth-hid/src/bonds.rs
Normal file
145
libs/bluetooth-hid/src/bonds.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
//! Only bonds explicitly owned by this HID peripheral may be removed.
|
||||||
|
use bluer::{Adapter, Session};
|
||||||
|
use std::{future::Future, pin::Pin};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Bond {
|
||||||
|
pub adapter: String,
|
||||||
|
pub peer: String,
|
||||||
|
pub pending: bool,
|
||||||
|
}
|
||||||
|
pub type Operation<'a, T> = Pin<Box<dyn Future<Output = Result<T, String>> + Send + 'a>>;
|
||||||
|
pub trait BondStore: Send + Sync {
|
||||||
|
fn list(&self) -> Operation<'_, Vec<Bond>>;
|
||||||
|
fn save(&self, bond: Bond) -> Operation<'_, ()>;
|
||||||
|
fn remove(&self, bond: Bond) -> Operation<'_, ()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clean_adapter(store: &dyn BondStore, adapter: &Adapter) -> Result<(), String> {
|
||||||
|
let address = adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string();
|
||||||
|
clean_owned(store, &address, |peer| async move {
|
||||||
|
let peer = peer.parse().map_err(|_| "Invalid stored HID peer")?;
|
||||||
|
if adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.contains(&peer)
|
||||||
|
{
|
||||||
|
adapter
|
||||||
|
.remove_device(peer)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clean_owned<F, Fut>(
|
||||||
|
store: &dyn BondStore,
|
||||||
|
address: &str,
|
||||||
|
mut remove: F,
|
||||||
|
) -> Result<(), String>
|
||||||
|
where
|
||||||
|
F: FnMut(String) -> Fut,
|
||||||
|
Fut: Future<Output = Result<(), String>>,
|
||||||
|
{
|
||||||
|
for bond in store
|
||||||
|
.list()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|b| b.pending && b.adapter == address)
|
||||||
|
{
|
||||||
|
remove(bond.peer.clone()).await?;
|
||||||
|
// Keep the tombstone until BlueZ confirms removal; retry safely after interruption.
|
||||||
|
store.remove(bond).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reset(store: &dyn BondStore, explicit: Vec<Bond>) -> Result<(), String> {
|
||||||
|
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||||
|
let mut owned = store.list().await?;
|
||||||
|
owned.extend(explicit);
|
||||||
|
for mut bond in owned {
|
||||||
|
bond.pending = true;
|
||||||
|
store.save(bond).await?;
|
||||||
|
}
|
||||||
|
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||||
|
clean_adapter(store, &session.adapter(&name).map_err(|e| e.to_string())?).await?;
|
||||||
|
}
|
||||||
|
// Records for absent adapters remain pending and are cleaned before their next use.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Memory(Mutex<Vec<Bond>>);
|
||||||
|
impl BondStore for Memory {
|
||||||
|
fn list(&self) -> Operation<'_, Vec<Bond>> {
|
||||||
|
Box::pin(async { Ok(self.0.lock().unwrap().clone()) })
|
||||||
|
}
|
||||||
|
fn save(&self, bond: Bond) -> Operation<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut rows = self.0.lock().unwrap();
|
||||||
|
rows.retain(|b| b.adapter != bond.adapter || b.peer != bond.peer);
|
||||||
|
rows.push(bond);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn remove(&self, bond: Bond) -> Operation<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.0.lock().unwrap().retain(|b| b != &bond);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn bond(adapter: &str, peer: &str, pending: bool) -> Bond {
|
||||||
|
Bond {
|
||||||
|
adapter: adapter.into(),
|
||||||
|
peer: peer.into(),
|
||||||
|
pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_only_removes_owned_pending_bonds_on_the_selected_hardware() {
|
||||||
|
let current = bond("adapter-A", "host-1", true);
|
||||||
|
let absent = bond("adapter-B", "host-2", true);
|
||||||
|
let retained = bond("adapter-A", "host-3", false);
|
||||||
|
let store = Memory(Mutex::new(vec![current, absent.clone(), retained.clone()]));
|
||||||
|
let removed = Mutex::new(Vec::new());
|
||||||
|
clean_owned(&store, "adapter-A", |peer| {
|
||||||
|
removed.lock().unwrap().push(peer);
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(*removed.lock().unwrap(), vec!["host-1"]);
|
||||||
|
assert_eq!(store.list().await.unwrap(), vec![absent, retained]);
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_failure_keeps_tombstone_and_is_reported() {
|
||||||
|
let record = bond("adapter-A", "host-1", true);
|
||||||
|
let store = Memory(Mutex::new(vec![record.clone()]));
|
||||||
|
assert_eq!(
|
||||||
|
clean_owned(&store, "adapter-A", |_| async {
|
||||||
|
Err("BlueZ denied removal".into())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_err(),
|
||||||
|
"BlueZ denied removal"
|
||||||
|
);
|
||||||
|
assert_eq!(store.list().await.unwrap(), vec![record]);
|
||||||
|
clean_owned(&store, "adapter-A", |_| async { Ok(()) })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(store.list().await.unwrap().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
135
libs/bluetooth-hid/src/controller.rs
Normal file
135
libs/bluetooth-hid/src/controller.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
//! Exclusive controller ownership. No BLE switching or power cycling.
|
||||||
|
use std::{
|
||||||
|
fs::{File, OpenOptions},
|
||||||
|
os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
|
||||||
|
process::Stdio,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tokio::process::Command;
|
||||||
|
pub struct Controller {
|
||||||
|
_lock: File,
|
||||||
|
adapter: String,
|
||||||
|
old_class: (u8, u8),
|
||||||
|
old_connectable: bool,
|
||||||
|
}
|
||||||
|
async fn run_once(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
let mut cmd = Command::new("btmgmt");
|
||||||
|
cmd.arg("--index")
|
||||||
|
.arg(adapter.trim_start_matches("hci"))
|
||||||
|
.args(args)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.kill_on_drop(true);
|
||||||
|
let mut child = cmd
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||||
|
// BlueZ 5.55's btmgmt exits on stdin EOF, even with a command argument.
|
||||||
|
// Keep a pipe open until it finishes; systemd normally supplies /dev/null.
|
||||||
|
let stdin = child.stdin.take();
|
||||||
|
let result = tokio::time::timeout(Duration::from_secs(5), child.wait_with_output())
|
||||||
|
.await
|
||||||
|
.map_err(|_| "btmgmt timeout")?
|
||||||
|
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||||
|
drop(stdin);
|
||||||
|
let text = String::from_utf8_lossy(&result.stdout).into_owned();
|
||||||
|
if !result.status.success()
|
||||||
|
|| !result.stderr.is_empty()
|
||||||
|
|| text.to_lowercase().contains("failed")
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"btmgmt {}: {text} {}",
|
||||||
|
args.join(" "),
|
||||||
|
String::from_utf8_lossy(&result.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
async fn run(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
// BlueZ updates the EIR/class asynchronously after registering SDP.
|
||||||
|
// Kernel management rejects a simultaneous class change with Busy.
|
||||||
|
for attempt in 0..5 {
|
||||||
|
match run_once(adapter, args).await {
|
||||||
|
Err(error) if error.contains("(Busy)") && attempt < 4 => {
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
}
|
||||||
|
result => return result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
impl Controller {
|
||||||
|
pub async fn acquire(adapter: &str) -> Result<Self, String> {
|
||||||
|
let lock = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(false)
|
||||||
|
.mode(0o600)
|
||||||
|
.custom_flags(libc::O_NOFOLLOW)
|
||||||
|
.open(format!("/run/one-kvm-bluetooth-{adapter}.lock"))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if unsafe { libc::flock(lock.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
|
||||||
|
return Err("Bluetooth adapter already owned by One-KVM".into());
|
||||||
|
}
|
||||||
|
let info = run(adapter, &["info"]).await?;
|
||||||
|
let settings = info
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.trim().strip_prefix("current settings:"))
|
||||||
|
.ok_or("Cannot read Bluetooth settings")?;
|
||||||
|
if !settings.split_whitespace().any(|s| s == "br/edr") {
|
||||||
|
return Err(
|
||||||
|
"Classic Bluetooth disabled; enable BR/EDR with btmgmt before using HID".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let class = info
|
||||||
|
.split_whitespace()
|
||||||
|
.skip_while(|s| *s != "class")
|
||||||
|
.nth(1)
|
||||||
|
.ok_or("Missing Bluetooth class")?;
|
||||||
|
let class =
|
||||||
|
u32::from_str_radix(class.trim_start_matches("0x"), 16).map_err(|e| e.to_string())?;
|
||||||
|
Ok(Self {
|
||||||
|
_lock: lock,
|
||||||
|
adapter: adapter.into(),
|
||||||
|
old_class: (((class >> 8) & 0x1f) as u8, (class & 0xfc) as u8),
|
||||||
|
old_connectable: settings.split_whitespace().any(|s| s == "connectable"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub async fn configure(&self) -> Result<(), String> {
|
||||||
|
run(&self.adapter, &["class", "5", "192"]).await?;
|
||||||
|
run(&self.adapter, &["connectable", "on"]).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub async fn restore(&self) -> Result<(), String> {
|
||||||
|
let mut errors = vec![];
|
||||||
|
if let Err(e) = run(
|
||||||
|
&self.adapter,
|
||||||
|
&[
|
||||||
|
"class",
|
||||||
|
&self.old_class.0.to_string(),
|
||||||
|
&self.old_class.1.to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
if let Err(e) = run(
|
||||||
|
&self.adapter,
|
||||||
|
&[
|
||||||
|
"connectable",
|
||||||
|
if self.old_connectable { "on" } else { "off" },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(errors.join("; "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
964
libs/bluetooth-hid/src/lib.rs
Normal file
964
libs/bluetooth-hid/src/lib.rs
Normal file
@@ -0,0 +1,964 @@
|
|||||||
|
//! Classic Bluetooth HID peripheral: SDP + L2CAP. Linux/BlueZ only.
|
||||||
|
mod agent;
|
||||||
|
pub mod bonds;
|
||||||
|
mod controller;
|
||||||
|
mod protocol;
|
||||||
|
|
||||||
|
use bluer::{
|
||||||
|
l2cap::{Security, SecurityLevel, SeqPacket, SeqPacketListener, Socket, SocketAddr},
|
||||||
|
Adapter, Address, AddressType, Session,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use tokio::sync::{mpsc, oneshot, watch, Mutex as AsyncMutex};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub adapter: String,
|
||||||
|
pub name: String,
|
||||||
|
pub peer: Option<String>,
|
||||||
|
}
|
||||||
|
impl Config {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if !self
|
||||||
|
.adapter
|
||||||
|
.strip_prefix("hci")
|
||||||
|
.is_some_and(|n| !n.is_empty() && n.bytes().all(|c| c.is_ascii_digit()))
|
||||||
|
{
|
||||||
|
return Err("Bluetooth adapter must be hci followed by an index".into());
|
||||||
|
}
|
||||||
|
if self.name.is_empty() || self.name.len() > 64 || self.name.chars().any(char::is_control) {
|
||||||
|
return Err(
|
||||||
|
"Bluetooth name must contain 1–64 UTF-8 bytes without control characters".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(peer) = &self.peer {
|
||||||
|
peer.parse::<Address>()
|
||||||
|
.map_err(|_| "Invalid Bluetooth peer address")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Status {
|
||||||
|
pub initialized: bool,
|
||||||
|
pub connected: bool,
|
||||||
|
pub ready: bool,
|
||||||
|
pub adapter: String,
|
||||||
|
pub adapter_address: String,
|
||||||
|
pub peer: Option<String>,
|
||||||
|
pub pairing_seconds: u32,
|
||||||
|
pub control_connected: bool,
|
||||||
|
pub interrupt_connected: bool,
|
||||||
|
pub leds: u8,
|
||||||
|
pub generation: u64,
|
||||||
|
pub error: Option<String>,
|
||||||
|
pub devices: Vec<Device>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Device {
|
||||||
|
pub address: String,
|
||||||
|
pub name: String,
|
||||||
|
pub paired: bool,
|
||||||
|
pub connected: bool,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Report {
|
||||||
|
Keyboard,
|
||||||
|
Mouse,
|
||||||
|
Consumer,
|
||||||
|
}
|
||||||
|
impl Report {
|
||||||
|
fn len(self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Keyboard => 8,
|
||||||
|
Self::Mouse => 4,
|
||||||
|
Self::Consumer => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn id(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Keyboard => 1,
|
||||||
|
Self::Mouse => 2,
|
||||||
|
Self::Consumer => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct State {
|
||||||
|
peer: Option<String>,
|
||||||
|
pairing_until: Option<Instant>,
|
||||||
|
bonded_before_pairing: HashSet<String>,
|
||||||
|
}
|
||||||
|
impl State {
|
||||||
|
fn observe_bond(&mut self, address: &str, paired: bool) {
|
||||||
|
// Just Works may complete without an Agent1 authorization callback.
|
||||||
|
// Adopt only a new bond from our explicit window, respecting a pinned peer.
|
||||||
|
if paired
|
||||||
|
&& self.peer.is_none()
|
||||||
|
&& self
|
||||||
|
.pairing_until
|
||||||
|
.is_some_and(|until| until > Instant::now())
|
||||||
|
&& !self.bonded_before_pairing.contains(address)
|
||||||
|
{
|
||||||
|
self.peer = Some(address.to_owned());
|
||||||
|
tracing::info!(
|
||||||
|
peer = address,
|
||||||
|
"Bluetooth HID selected newly bonded computer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn close_pairing(&mut self, paired: bool, fallback: Option<String>) {
|
||||||
|
self.pairing_until = None;
|
||||||
|
if !paired {
|
||||||
|
self.peer = fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn pairing_completed(&self, selected_paired: bool) -> bool {
|
||||||
|
selected_paired
|
||||||
|
&& self
|
||||||
|
.peer
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|peer| !self.bonded_before_pairing.contains(peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Shared {
|
||||||
|
state: Mutex<State>,
|
||||||
|
}
|
||||||
|
impl Shared {
|
||||||
|
fn new(peer: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Mutex::new(State {
|
||||||
|
peer,
|
||||||
|
pairing_until: None,
|
||||||
|
bonded_before_pairing: HashSet::new(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn allowed(&self, address: Address) -> bool {
|
||||||
|
self.state.lock().unwrap().peer.as_deref() == Some(address.to_string().as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub enum Action {
|
||||||
|
Pair(u32),
|
||||||
|
ClosePairing,
|
||||||
|
Forget,
|
||||||
|
Disconnect,
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
enum Command {
|
||||||
|
Report(Report, Vec<u8>),
|
||||||
|
Action(Action),
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
struct Request {
|
||||||
|
command: Command,
|
||||||
|
result: oneshot::Sender<Result<(), String>>,
|
||||||
|
expires: Instant,
|
||||||
|
}
|
||||||
|
pub struct Peripheral {
|
||||||
|
tx: mpsc::Sender<Request>,
|
||||||
|
status: watch::Receiver<Status>,
|
||||||
|
task: AsyncMutex<Option<tokio::task::JoinHandle<()>>>,
|
||||||
|
}
|
||||||
|
impl Peripheral {
|
||||||
|
pub fn start(config: Config) -> Result<Self, String> {
|
||||||
|
Self::start_with_store(config, None)
|
||||||
|
}
|
||||||
|
pub fn start_with_store(
|
||||||
|
config: Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
config.validate()?;
|
||||||
|
let (tx, rx) = mpsc::channel(64);
|
||||||
|
let (state_tx, status) = watch::channel(Status {
|
||||||
|
adapter: config.adapter.clone(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let task = tokio::spawn(supervise(config, bonds, rx, state_tx));
|
||||||
|
Ok(Self {
|
||||||
|
tx,
|
||||||
|
status,
|
||||||
|
task: AsyncMutex::new(Some(task)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn status(&self) -> Status {
|
||||||
|
self.status.borrow().clone()
|
||||||
|
}
|
||||||
|
pub fn subscribe(&self) -> watch::Receiver<Status> {
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
async fn request(&self, command: Command, duration: Duration) -> Result<(), String> {
|
||||||
|
let (result, rx) = oneshot::channel();
|
||||||
|
let request = Request {
|
||||||
|
command,
|
||||||
|
result,
|
||||||
|
expires: Instant::now() + duration,
|
||||||
|
};
|
||||||
|
self.tx
|
||||||
|
.try_send(request)
|
||||||
|
.map_err(|_| "Bluetooth input queue unavailable or full".to_string())?;
|
||||||
|
tokio::time::timeout(duration, rx)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Bluetooth operation timed out".to_string())?
|
||||||
|
.map_err(|_| "Bluetooth worker stopped".to_string())?
|
||||||
|
}
|
||||||
|
pub async fn send(&self, report: Report, value: Vec<u8>) -> Result<(), String> {
|
||||||
|
if value.len() != report.len() {
|
||||||
|
return Err("Invalid HID report size".into());
|
||||||
|
}
|
||||||
|
self.request(Command::Report(report, value), Duration::from_millis(500))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub async fn action(&self, action: Action) -> Result<(), String> {
|
||||||
|
self.request(Command::Action(action), Duration::from_secs(15))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub async fn shutdown(&self) -> Result<(), String> {
|
||||||
|
if let Some(task) = self.task.lock().await.take() {
|
||||||
|
let result = self.request(Command::Stop, Duration::from_secs(45)).await;
|
||||||
|
// Do not abort: the worker must restore the radio before a backend switch.
|
||||||
|
task.await.map_err(|e| e.to_string())?;
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listen(address: Address, psm: u16) -> Result<SeqPacketListener, String> {
|
||||||
|
let socket = Socket::<SeqPacket>::new_seq_packet().map_err(|e| e.to_string())?;
|
||||||
|
socket
|
||||||
|
.set_security(Security {
|
||||||
|
level: SecurityLevel::Medium,
|
||||||
|
key_size: 0,
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
socket.bind(SocketAddr::new(address,AddressType::BrEdr,psm))
|
||||||
|
.map_err(|e|format!("Cannot bind classic HID PSM 0x{psm:02x}: {e}. Disable BlueZ input plugin; see Bluetooth HID setup."))?;
|
||||||
|
socket.listen(2).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
struct Runtime {
|
||||||
|
adapter: Adapter,
|
||||||
|
_session: Session,
|
||||||
|
shared: Arc<Shared>,
|
||||||
|
agent: Option<agent::Agent>,
|
||||||
|
controller: controller::Controller,
|
||||||
|
listeners: [SeqPacketListener; 2],
|
||||||
|
channels: [Option<Arc<SeqPacket>>; 2],
|
||||||
|
channel_peer: Option<Address>,
|
||||||
|
partial_since: Option<Instant>,
|
||||||
|
hid: protocol::HidProtocol,
|
||||||
|
generation: u64,
|
||||||
|
old_pairable: bool,
|
||||||
|
old_powered: bool,
|
||||||
|
old_alias: String,
|
||||||
|
old_discoverable: bool,
|
||||||
|
old_discoverable_timeout: u32,
|
||||||
|
old_pairable_timeout: u32,
|
||||||
|
config: Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
recorded_peer: Option<String>,
|
||||||
|
}
|
||||||
|
impl Runtime {
|
||||||
|
async fn open(
|
||||||
|
config: &Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
let controller = controller::Controller::acquire(&config.adapter).await?;
|
||||||
|
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||||
|
let adapter = session
|
||||||
|
.adapter(&config.adapter)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let address = adapter.address().await.map_err(|e| e.to_string())?;
|
||||||
|
let mut config = config.clone();
|
||||||
|
if let Some(store) = &bonds {
|
||||||
|
bonds::clean_adapter(store.as_ref(), &adapter).await?;
|
||||||
|
if config.peer.is_none() {
|
||||||
|
config.peer = store
|
||||||
|
.list()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.find(|bond| bond.adapter == address.to_string() && !bond.pending)
|
||||||
|
.map(|bond| bond.peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Acquire BOTH channels before changing adapter state. Conflict is a startup error.
|
||||||
|
let listeners = [listen(address, 0x11)?, listen(address, 0x13)?];
|
||||||
|
let old_powered = adapter.is_powered().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_pairable = adapter.is_pairable().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_alias = adapter.alias().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_discoverable = adapter.is_discoverable().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_discoverable_timeout = adapter
|
||||||
|
.discoverable_timeout()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let old_pairable_timeout = adapter
|
||||||
|
.pairable_timeout()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let shared = Arc::new(Shared::new(
|
||||||
|
config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||||
|
));
|
||||||
|
let mut runtime = Self {
|
||||||
|
adapter,
|
||||||
|
_session: session,
|
||||||
|
shared,
|
||||||
|
agent: None,
|
||||||
|
controller,
|
||||||
|
listeners,
|
||||||
|
channels: [None, None],
|
||||||
|
channel_peer: None,
|
||||||
|
partial_since: None,
|
||||||
|
hid: Default::default(),
|
||||||
|
generation: 0,
|
||||||
|
old_pairable,
|
||||||
|
old_powered,
|
||||||
|
old_alias,
|
||||||
|
old_discoverable,
|
||||||
|
old_discoverable_timeout,
|
||||||
|
old_pairable_timeout,
|
||||||
|
config: config.clone(),
|
||||||
|
bonds,
|
||||||
|
recorded_peer: None,
|
||||||
|
};
|
||||||
|
if let Err(e) = runtime.setup().await {
|
||||||
|
let restore = runtime.close().await;
|
||||||
|
return Err(format!("{e}; cleanup: {restore:?}"));
|
||||||
|
}
|
||||||
|
Ok(runtime)
|
||||||
|
}
|
||||||
|
async fn setup(&mut self) -> Result<(), String> {
|
||||||
|
self.adapter
|
||||||
|
.set_powered(true)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_discoverable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_pairable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_alias(self.config.name.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.agent =
|
||||||
|
Some(agent::Agent::register(self.shared.clone(), self.config.adapter.clone()).await?);
|
||||||
|
self.controller.configure().await?;
|
||||||
|
self.public(None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn public(&self, seconds: Option<u32>) -> Result<(), String> {
|
||||||
|
if let Some(seconds) = seconds {
|
||||||
|
self.adapter
|
||||||
|
.set_pairable_timeout(seconds)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_discoverable_timeout(seconds)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_pairable(true)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if let Err(e) = self.adapter.set_discoverable(true).await {
|
||||||
|
let _ = self.adapter.set_pairable(false).await;
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.adapter
|
||||||
|
.set_discoverable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_pairable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn drop_channels(&mut self) {
|
||||||
|
for socket in self.channels.iter_mut().filter_map(Option::take) {
|
||||||
|
let _ = socket.shutdown(std::net::Shutdown::Both);
|
||||||
|
}
|
||||||
|
self.channel_peer = None;
|
||||||
|
self.partial_since = None;
|
||||||
|
self.hid = Default::default();
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
async fn packet(&self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||||
|
let socket = self.channels[index]
|
||||||
|
.as_ref()
|
||||||
|
.ok_or("Classic HID channel not connected")?;
|
||||||
|
let sent = tokio::time::timeout(Duration::from_millis(250), socket.send(data))
|
||||||
|
.await
|
||||||
|
.map_err(|_| "HID send timeout")?
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if sent != data.len() {
|
||||||
|
return Err("Short HID packet write".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn release(&mut self) {
|
||||||
|
for packet in self.hid.release() {
|
||||||
|
if self.channels[1].is_some() && self.packet(1, &packet).await.is_err() {
|
||||||
|
self.drop_channels();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
async fn accept(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
socket: SeqPacket,
|
||||||
|
peer: SocketAddr,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if peer.addr_type != AddressType::BrEdr {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// A host can open HID channels before the next status poll.
|
||||||
|
let paired = self
|
||||||
|
.adapter
|
||||||
|
.device(peer.addr)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_paired()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.observe_bond(&peer.addr.to_string(), paired);
|
||||||
|
if !self.shared.allowed(peer.addr) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if self.channel_peer.is_some_and(|p| p != peer.addr) || self.channels[index].is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// Kernel BT_SECURITY_MEDIUM negotiates encryption before accept completes.
|
||||||
|
if socket
|
||||||
|
.as_ref()
|
||||||
|
.security()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.key_size
|
||||||
|
< 7
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.channel_peer = Some(peer.addr);
|
||||||
|
self.channels[index] = Some(Arc::new(socket));
|
||||||
|
if self.channels.iter().all(Option::is_some) {
|
||||||
|
self.partial_since = None;
|
||||||
|
self.release().await;
|
||||||
|
} else {
|
||||||
|
self.partial_since = Some(Instant::now());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn status(&mut self) -> Result<Status, String> {
|
||||||
|
if !self.adapter.is_powered().await.map_err(|e| e.to_string())? {
|
||||||
|
return Err("Bluetooth adapter powered off".into());
|
||||||
|
}
|
||||||
|
// A daemon restart loses the SDP registration even if the adapter comes back powered.
|
||||||
|
if !self
|
||||||
|
.adapter
|
||||||
|
.uuids()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.any(|id| id.to_string() == protocol::HID_UUID)
|
||||||
|
{
|
||||||
|
return Err("Classic HID SDP registration lost; restarting Bluetooth backend".into());
|
||||||
|
}
|
||||||
|
let mut status = Status {
|
||||||
|
initialized: true,
|
||||||
|
adapter: self.config.adapter.clone(),
|
||||||
|
adapter_address: self
|
||||||
|
.adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for address in self
|
||||||
|
.adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
{
|
||||||
|
let device = self.adapter.device(address).map_err(|e| e.to_string())?;
|
||||||
|
let connected = device.is_connected().await.unwrap_or(false);
|
||||||
|
let paired = device.is_paired().await.unwrap_or(false);
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.observe_bond(&address.to_string(), paired);
|
||||||
|
if paired || connected {
|
||||||
|
status.devices.push(Device {
|
||||||
|
address: address.to_string(),
|
||||||
|
name: device.alias().await.unwrap_or_default(),
|
||||||
|
paired,
|
||||||
|
connected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.devices.sort_by(|a, b| a.address.cmp(&b.address));
|
||||||
|
let (peer, until) = {
|
||||||
|
let state = self.shared.state.lock().unwrap();
|
||||||
|
(state.peer.clone(), state.pairing_until)
|
||||||
|
};
|
||||||
|
status.peer = peer.clone();
|
||||||
|
let selected = status
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.find(|device| Some(&device.address) == peer.as_ref());
|
||||||
|
let selected_paired = selected.is_some_and(|device| device.paired);
|
||||||
|
if selected_paired && self.recorded_peer != peer {
|
||||||
|
if let (Some(store), Some(peer)) = (&self.bonds, &peer) {
|
||||||
|
store
|
||||||
|
.save(bonds::Bond {
|
||||||
|
adapter: status.adapter_address.clone(),
|
||||||
|
peer: peer.clone(),
|
||||||
|
pending: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
self.recorded_peer = Some(peer.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.connected = selected.is_some_and(|device| device.paired && device.connected);
|
||||||
|
if self.channels.iter().any(Option::is_some)
|
||||||
|
&& (!status.connected
|
||||||
|
|| self
|
||||||
|
.partial_since
|
||||||
|
.is_some_and(|time| time.elapsed() > Duration::from_secs(10)))
|
||||||
|
{
|
||||||
|
self.drop_channels();
|
||||||
|
}
|
||||||
|
if let Some(until) = until {
|
||||||
|
let completed = self
|
||||||
|
.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.pairing_completed(selected_paired);
|
||||||
|
if until <= Instant::now() || completed {
|
||||||
|
{
|
||||||
|
let mut state = self.shared.state.lock().unwrap();
|
||||||
|
state.close_pairing(
|
||||||
|
selected_paired,
|
||||||
|
self.config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||||
|
);
|
||||||
|
status.peer = state.peer.clone();
|
||||||
|
}
|
||||||
|
self.public(None).await?;
|
||||||
|
tracing::info!(reason = if completed { "bonded" } else { "expired" }, peer = ?status.peer, "Bluetooth HID pairing window closed");
|
||||||
|
} else {
|
||||||
|
status.pairing_seconds =
|
||||||
|
until.saturating_duration_since(Instant::now()).as_secs() as u32 + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.control_connected = self.channels[0].is_some();
|
||||||
|
status.interrupt_connected = self.channels[1].is_some();
|
||||||
|
status.ready = status.connected
|
||||||
|
&& status.control_connected
|
||||||
|
&& status.interrupt_connected
|
||||||
|
&& !self.hid.suspended;
|
||||||
|
status.leds = self.hid.leds;
|
||||||
|
status.generation = self.generation;
|
||||||
|
Ok(status)
|
||||||
|
}
|
||||||
|
async fn forget(&mut self) -> Result<(), String> {
|
||||||
|
self.release().await;
|
||||||
|
self.drop_channels();
|
||||||
|
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||||
|
if let Some(peer) = &peer {
|
||||||
|
let address = peer.parse().map_err(|_| "Invalid peer")?;
|
||||||
|
if self
|
||||||
|
.adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.contains(&address)
|
||||||
|
{
|
||||||
|
self.adapter
|
||||||
|
.remove_device(address)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.bonded_before_pairing
|
||||||
|
.remove(peer);
|
||||||
|
tracing::info!(%peer, "Bluetooth HID forgot computer");
|
||||||
|
}
|
||||||
|
if let Some(store) = &self.bonds {
|
||||||
|
let address = self
|
||||||
|
.adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string();
|
||||||
|
for bond in store
|
||||||
|
.list()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|b| b.adapter == address && Some(&b.peer) == peer.as_ref())
|
||||||
|
{
|
||||||
|
store.remove(bond).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.recorded_peer = None;
|
||||||
|
self.config.peer = None;
|
||||||
|
self.shared.state.lock().unwrap().peer = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn incoming(&mut self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||||
|
if index == 1 {
|
||||||
|
self.hid.output(data);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let result = self.hid.control(data);
|
||||||
|
if let Some(reply) = result.reply {
|
||||||
|
self.packet(0, &reply).await?;
|
||||||
|
}
|
||||||
|
if result.reset {
|
||||||
|
self.release().await;
|
||||||
|
}
|
||||||
|
if result.unplug {
|
||||||
|
self.forget().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn execute(&mut self, command: Command) -> Result<(), String> {
|
||||||
|
match command {
|
||||||
|
Command::Report(kind, value) => {
|
||||||
|
let peer = self.channel_peer.ok_or("Classic HID is not connected")?;
|
||||||
|
if !self.shared.allowed(peer) || self.channels.iter().any(Option::is_none) {
|
||||||
|
return Err("Classic HID channels are not ready".into());
|
||||||
|
}
|
||||||
|
let device = self.adapter.device(peer).map_err(|e| e.to_string())?;
|
||||||
|
if !device.is_paired().await.unwrap_or(false)
|
||||||
|
|| !device.is_connected().await.unwrap_or(false)
|
||||||
|
{
|
||||||
|
self.drop_channels();
|
||||||
|
return Err("Selected Bluetooth computer disconnected".into());
|
||||||
|
}
|
||||||
|
let packet = self.hid.input(kind, &value)?;
|
||||||
|
if let Err(e) = self.packet(1, &packet).await {
|
||||||
|
self.drop_channels();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Action(Action::Pair(seconds)) => {
|
||||||
|
if !(10..=300).contains(&seconds) {
|
||||||
|
return Err("Pairing window must be 10–300 seconds".into());
|
||||||
|
}
|
||||||
|
let mut bonded = HashSet::new();
|
||||||
|
for address in self
|
||||||
|
.adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
{
|
||||||
|
if self
|
||||||
|
.adapter
|
||||||
|
.device(address)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_paired()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
{
|
||||||
|
bonded.insert(address.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut state = self.shared.state.lock().unwrap();
|
||||||
|
// Preserve the original baseline when extending an open window.
|
||||||
|
if !state
|
||||||
|
.pairing_until
|
||||||
|
.is_some_and(|until| until > Instant::now())
|
||||||
|
{
|
||||||
|
state.bonded_before_pairing = bonded;
|
||||||
|
}
|
||||||
|
state.pairing_until =
|
||||||
|
Some(Instant::now() + Duration::from_secs(seconds.into()));
|
||||||
|
}
|
||||||
|
if let Err(e) = self.public(Some(seconds)).await {
|
||||||
|
self.shared.state.lock().unwrap().pairing_until = None;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
tracing::info!(seconds, "Bluetooth HID pairing window opened");
|
||||||
|
}
|
||||||
|
Command::Action(Action::ClosePairing) => {
|
||||||
|
self.public(None).await?;
|
||||||
|
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||||
|
let paired = if let Some(peer) = peer {
|
||||||
|
self.adapter
|
||||||
|
.device(peer.parse().map_err(|_| "Invalid HID peer")?)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_paired()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.close_pairing(paired, self.config.peer.clone());
|
||||||
|
tracing::info!(reason = "manual", "Bluetooth HID pairing window closed");
|
||||||
|
}
|
||||||
|
Command::Action(Action::Forget) => self.forget().await?,
|
||||||
|
Command::Action(Action::Disconnect) => {
|
||||||
|
self.release().await;
|
||||||
|
self.drop_channels();
|
||||||
|
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||||
|
if let Some(peer) = peer {
|
||||||
|
self.adapter
|
||||||
|
.device(peer.parse().map_err(|_| "Invalid peer")?)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.disconnect()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Action(Action::Reset) => self.release().await,
|
||||||
|
Command::Stop => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn close(&mut self) -> Result<(), String> {
|
||||||
|
self.release().await;
|
||||||
|
self.drop_channels();
|
||||||
|
self.agent.take();
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
let mut errors = vec![];
|
||||||
|
if let Err(e) = self.adapter.set_discoverable(false).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.controller.restore().await {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_alias(self.old_alias.clone()).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self
|
||||||
|
.adapter
|
||||||
|
.set_discoverable_timeout(self.old_discoverable_timeout)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self
|
||||||
|
.adapter
|
||||||
|
.set_pairable_timeout(self.old_pairable_timeout)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_pairable(self.old_pairable).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_discoverable(self.old_discoverable).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_powered(self.old_powered).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(errors.join("; "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn receive(socket: Option<Arc<SeqPacket>>, buffer: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
match socket {
|
||||||
|
Some(socket) => socket.recv(buffer).await,
|
||||||
|
None => std::future::pending().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn supervise(
|
||||||
|
config: Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
mut rx: mpsc::Receiver<Request>,
|
||||||
|
tx: watch::Sender<Status>,
|
||||||
|
) {
|
||||||
|
let mut generation = 0;
|
||||||
|
loop {
|
||||||
|
let mut runtime = match Runtime::open(&config, bonds.clone()).await {
|
||||||
|
Ok(runtime) => runtime,
|
||||||
|
Err(error) => {
|
||||||
|
tx.send_replace(Status {
|
||||||
|
adapter: config.adapter.clone(),
|
||||||
|
error: Some(error.clone()),
|
||||||
|
generation,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let delay = tokio::time::sleep(Duration::from_secs(5));
|
||||||
|
tokio::pin!(delay);
|
||||||
|
loop {
|
||||||
|
tokio::select! {_=&mut delay=>break,request=rx.recv()=>{
|
||||||
|
let Some(request)=request else{return;};
|
||||||
|
if matches!(request.command,Command::Stop){let _=request.result.send(Ok(()));return;}
|
||||||
|
let _=request.result.send(Err(error.clone()));
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
runtime.generation = generation;
|
||||||
|
let mut timer = tokio::time::interval(Duration::from_millis(500));
|
||||||
|
let failure = loop {
|
||||||
|
let ctl = runtime.channels[0].clone();
|
||||||
|
let intr = runtime.channels[1].clone();
|
||||||
|
let mut ctl_buffer = [0; 1024];
|
||||||
|
let mut intr_buffer = [0; 1024];
|
||||||
|
tokio::select! {
|
||||||
|
accepted=runtime.listeners[0].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(0,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||||
|
accepted=runtime.listeners[1].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(1,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||||
|
read=receive(ctl,&mut ctl_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(0,&ctl_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||||
|
read=receive(intr,&mut intr_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(1,&intr_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||||
|
_=timer.tick()=>{match runtime.status().await{Ok(status)=>{tx.send_if_modified(|old|{if *old!=status{*old=status;true}else{false}});},Err(e)=>break e}}
|
||||||
|
request=rx.recv()=>{
|
||||||
|
let Some(request)=request else{let _=runtime.close().await;return;};
|
||||||
|
if matches!(request.command,Command::Stop){let result=runtime.close().await;tx.send_replace(Status{adapter:config.adapter.clone(),..Default::default()});let _=request.result.send(result);return;}
|
||||||
|
if request.expires<=Instant::now()||request.result.is_closed(){runtime.release().await;continue;}
|
||||||
|
let action = matches!(&request.command, Command::Action(_));
|
||||||
|
let result=runtime.execute(request.command).await;
|
||||||
|
if result.is_err(){runtime.release().await;}
|
||||||
|
if action {
|
||||||
|
if let Ok(status) = runtime.status().await { tx.send_replace(status); }
|
||||||
|
}
|
||||||
|
let _=request.result.send(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let restore = runtime.close().await;
|
||||||
|
generation = runtime.generation.wrapping_add(1);
|
||||||
|
tx.send_replace(Status {
|
||||||
|
adapter: config.adapter.clone(),
|
||||||
|
error: Some(format!("{failure}; cleanup: {restore:?}")),
|
||||||
|
generation,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
drop(runtime);
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn closing_pairing_preserves_completed_bonds_but_releases_unpaired_targets() {
|
||||||
|
let shared = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state.close_pairing(true, None);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:88"));
|
||||||
|
assert!(state.pairing_until.is_none());
|
||||||
|
state.close_pairing(false, None);
|
||||||
|
assert!(state.peer.is_none());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn configuration_validation() {
|
||||||
|
let mut c = Config {
|
||||||
|
adapter: "hci0".into(),
|
||||||
|
name: "One-KVM".into(),
|
||||||
|
peer: None,
|
||||||
|
};
|
||||||
|
assert!(c.validate().is_ok());
|
||||||
|
c.adapter = "hci0;reboot".into();
|
||||||
|
assert!(c.validate().is_err());
|
||||||
|
c.adapter = "hci1".into();
|
||||||
|
c.peer = Some("bogus".into());
|
||||||
|
assert!(c.validate().is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn peer_isolation() {
|
||||||
|
let state = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||||
|
assert!(state.allowed("10:6F:D9:66:97:88".parse().unwrap()));
|
||||||
|
assert!(!state.allowed("10:6F:D9:66:97:89".parse().unwrap()));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn new_bond_without_agent_callback_completes_pairing() {
|
||||||
|
let shared = Shared::new(None);
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
let peer = "10:6F:D9:66:97:88";
|
||||||
|
state.observe_bond(peer, false);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||||
|
assert!(state.pairing_completed(true));
|
||||||
|
// A second computer cannot replace the one selected in this window.
|
||||||
|
state.observe_bond("10:6F:D9:66:97:89", true);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn old_bond_does_not_finish_a_new_window() {
|
||||||
|
let peer = "10:6F:D9:66:97:88";
|
||||||
|
let shared = Shared::new(Some(peer.into()));
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state.bonded_before_pairing.insert(peer.into());
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert!(!state.pairing_completed(true));
|
||||||
|
state.peer = None;
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn bond_observation_respects_closed_window_and_pinned_peer() {
|
||||||
|
let peer = "10:6F:D9:66:97:88";
|
||||||
|
let shared = Shared::new(None);
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
state.pairing_until = Some(Instant::now() - Duration::from_secs(1));
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state.peer = Some("10:6F:D9:66:97:89".into());
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:89"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct AdapterInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub address: String,
|
||||||
|
pub powered: bool,
|
||||||
|
}
|
||||||
|
pub async fn adapters() -> Result<Vec<AdapterInfo>, String> {
|
||||||
|
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||||
|
let mut output = vec![];
|
||||||
|
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||||
|
let adapter = session.adapter(&name).map_err(|e| e.to_string())?;
|
||||||
|
output.push(AdapterInfo {
|
||||||
|
name,
|
||||||
|
address: adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string(),
|
||||||
|
powered: adapter.is_powered().await.map_err(|e| e.to_string())?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
267
libs/bluetooth-hid/src/protocol.rs
Normal file
267
libs/bluetooth-hid/src/protocol.rs
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
//! Classic HIDP framing and report descriptor.
|
||||||
|
use crate::Report;
|
||||||
|
pub const MAP: &[u8] = &[
|
||||||
|
0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00,
|
||||||
|
0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
|
||||||
|
0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01,
|
||||||
|
0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00,
|
||||||
|
0xc0, 0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x85, 0x02, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19,
|
||||||
|
0x01, 0x29, 0x05, 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75,
|
||||||
|
0x03, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, 0x15, 0x81, 0x25, 0x7f, 0x75,
|
||||||
|
0x08, 0x95, 0x03, 0x81, 0x06, 0xc0, 0xc0, 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x85, 0x03, 0x15,
|
||||||
|
0x00, 0x26, 0xff, 0x03, 0x19, 0x00, 0x2a, 0xff, 0x03, 0x75, 0x10, 0x95, 0x01, 0x81, 0x00, 0xc0,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const HID_UUID: &str = "00001124-0000-1000-8000-00805f9b34fb";
|
||||||
|
pub fn sdp() -> String {
|
||||||
|
let hex: String = MAP.iter().map(|byte| format!("{byte:02x}")).collect();
|
||||||
|
format!(
|
||||||
|
r#"<?xml version="1.0"?>
|
||||||
|
<record>
|
||||||
|
<attribute id="0x0001"><sequence><uuid value="0x1124"/></sequence></attribute>
|
||||||
|
<attribute id="0x0004"><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0011"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0005"><sequence><uuid value="0x1002"/></sequence></attribute>
|
||||||
|
<attribute id="0x0006"><sequence><uint16 value="0x656e"/><uint16 value="0x006a"/><uint16 value="0x0100"/></sequence></attribute>
|
||||||
|
<attribute id="0x0009"><sequence><sequence><uuid value="0x1124"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x000d"><sequence><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0013"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0100"><text value="One-KVM Keyboard and Mouse"/></attribute>
|
||||||
|
<attribute id="0x0101"><text value="Classic Bluetooth HID"/></attribute>
|
||||||
|
<attribute id="0x0102"><text value="One-KVM"/></attribute>
|
||||||
|
<attribute id="0x0200"><uint16 value="0x0100"/></attribute>
|
||||||
|
<attribute id="0x0201"><uint16 value="0x0111"/></attribute>
|
||||||
|
<attribute id="0x0202"><uint8 value="0xc0"/></attribute>
|
||||||
|
<attribute id="0x0203"><uint8 value="0x00"/></attribute>
|
||||||
|
<attribute id="0x0204"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x0205"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x0206"><sequence><sequence><uint8 value="0x22"/><text encoding="hex" value="{hex}"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0207"><sequence><sequence><uint16 value="0x0409"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0209"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x020a"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x020b"><uint16 value="0x0100"/></attribute>
|
||||||
|
<attribute id="0x020c"><uint16 value="0x0c80"/></attribute>
|
||||||
|
<attribute id="0x020d"><boolean value="true"/></attribute>
|
||||||
|
<attribute id="0x020e"><boolean value="true"/></attribute>
|
||||||
|
</record>"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct HidProtocol {
|
||||||
|
pub boot: bool,
|
||||||
|
pub suspended: bool,
|
||||||
|
pub leds: u8,
|
||||||
|
pub keyboard: [u8; 8],
|
||||||
|
pub buttons: u8,
|
||||||
|
pub consumer: [u8; 2],
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct ControlResult {
|
||||||
|
pub reply: Option<Vec<u8>>,
|
||||||
|
pub unplug: bool,
|
||||||
|
pub reset: bool,
|
||||||
|
}
|
||||||
|
impl HidProtocol {
|
||||||
|
pub fn input(&mut self, kind: Report, value: &[u8]) -> Result<Vec<u8>, String> {
|
||||||
|
if value.len() != kind.len() {
|
||||||
|
return Err("Invalid HID report length".into());
|
||||||
|
}
|
||||||
|
if self.suspended {
|
||||||
|
return Err("Bluetooth HID is suspended".into());
|
||||||
|
}
|
||||||
|
let mut packet = vec![0xa1, kind.id()];
|
||||||
|
match kind {
|
||||||
|
Report::Keyboard => {
|
||||||
|
self.keyboard.copy_from_slice(value);
|
||||||
|
packet.extend_from_slice(value);
|
||||||
|
}
|
||||||
|
Report::Mouse => {
|
||||||
|
self.buttons = value[0];
|
||||||
|
packet.extend_from_slice(&value[..if self.boot { 3 } else { 4 }]);
|
||||||
|
if self.boot {
|
||||||
|
packet[2] &= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Report::Consumer if !self.boot => {
|
||||||
|
self.consumer.copy_from_slice(value);
|
||||||
|
packet.extend_from_slice(value);
|
||||||
|
}
|
||||||
|
Report::Consumer => return Err("Consumer keys unavailable in boot protocol".into()),
|
||||||
|
}
|
||||||
|
Ok(packet)
|
||||||
|
}
|
||||||
|
pub fn release(&mut self) -> Vec<Vec<u8>> {
|
||||||
|
self.keyboard = [0; 8];
|
||||||
|
self.buttons = 0;
|
||||||
|
self.consumer = [0; 2];
|
||||||
|
let mut packets = vec![
|
||||||
|
vec![0xa1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
if self.boot {
|
||||||
|
vec![0xa1, 2, 0, 0, 0]
|
||||||
|
} else {
|
||||||
|
vec![0xa1, 2, 0, 0, 0, 0]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if !self.boot {
|
||||||
|
packets.push(vec![0xa1, 3, 0, 0]);
|
||||||
|
}
|
||||||
|
packets
|
||||||
|
}
|
||||||
|
pub fn output(&mut self, data: &[u8]) -> bool {
|
||||||
|
if data.len() == 3 && data[0] == 0xa2 && data[1] == 1 {
|
||||||
|
self.leds = data[2] & 0x1f;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn control(&mut self, data: &[u8]) -> ControlResult {
|
||||||
|
let mut result = ControlResult::default();
|
||||||
|
let handshake = |code| ControlResult {
|
||||||
|
reply: Some(vec![code]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let Some(&header) = data.first() else {
|
||||||
|
return handshake(4);
|
||||||
|
};
|
||||||
|
match header {
|
||||||
|
0x13 if data.len() == 1 => {
|
||||||
|
self.suspended = true;
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x14 if data.len() == 1 => {
|
||||||
|
self.suspended = false;
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x15 if data.len() == 1 => {
|
||||||
|
result.unplug = true;
|
||||||
|
}
|
||||||
|
0x11 | 0x12 if data.len() == 1 => {
|
||||||
|
self.suspended = false;
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x60 if data.len() == 1 => {
|
||||||
|
result.reply = Some(vec![0xa0, if self.boot { 0 } else { 1 }]);
|
||||||
|
}
|
||||||
|
0x70 | 0x71 if data.len() == 1 => {
|
||||||
|
self.boot = header == 0x70;
|
||||||
|
result.reply = Some(vec![0]);
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x52 if data.len() == 3 && data[1] == 1 => {
|
||||||
|
self.leds = data[2] & 0x1f;
|
||||||
|
return handshake(0);
|
||||||
|
}
|
||||||
|
0x41 | 0x49 | 0x42 | 0x4a => {
|
||||||
|
let sized = header & 8 != 0;
|
||||||
|
if data.len() != if sized { 4 } else { 2 } {
|
||||||
|
return handshake(4);
|
||||||
|
}
|
||||||
|
let mut report = vec![0xa0 | (header & 3), data[1]];
|
||||||
|
match (header & 3, data[1]) {
|
||||||
|
(1, 1) => report.extend(self.keyboard),
|
||||||
|
(1, 2) => report.extend(if self.boot {
|
||||||
|
vec![self.buttons & 7, 0, 0]
|
||||||
|
} else {
|
||||||
|
vec![self.buttons, 0, 0, 0]
|
||||||
|
}),
|
||||||
|
(1, 3) if !self.boot => report.extend(self.consumer),
|
||||||
|
(2, 1) => report.push(self.leds),
|
||||||
|
_ => return handshake(2),
|
||||||
|
}
|
||||||
|
if sized {
|
||||||
|
report.truncate(1 + u16::from_le_bytes([data[2], data[3]]) as usize);
|
||||||
|
}
|
||||||
|
result.reply = Some(report);
|
||||||
|
}
|
||||||
|
_ => return handshake(3), // ERR_UNSUPPORTED_REQUEST
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn classic_frames_and_boot_protocol() {
|
||||||
|
let mut hid = HidProtocol::default();
|
||||||
|
assert_eq!(
|
||||||
|
hid.input(Report::Keyboard, &[2, 0, 4, 0, 0, 0, 0, 0])
|
||||||
|
.unwrap(),
|
||||||
|
[0xa1, 1, 2, 0, 4, 0, 0, 0, 0, 0]
|
||||||
|
);
|
||||||
|
assert_eq!(hid.control(&[0x60]).reply.unwrap(), [0xa0, 1]);
|
||||||
|
assert_eq!(hid.control(&[0x70]).reply.unwrap(), [0]);
|
||||||
|
assert_eq!(
|
||||||
|
hid.input(Report::Mouse, &[0x1f, 127, 128, 1]).unwrap(),
|
||||||
|
[0xa1, 2, 7, 127, 128]
|
||||||
|
);
|
||||||
|
assert!(hid.input(Report::Consumer, &[0, 0]).is_err());
|
||||||
|
assert!(hid.input(Report::Keyboard, &[0]).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn led_read_write_and_errors() {
|
||||||
|
let mut hid = HidProtocol::default();
|
||||||
|
assert!(hid.output(&[0xa2, 1, 3]));
|
||||||
|
assert_eq!(hid.leds, 3);
|
||||||
|
assert_eq!(hid.control(&[0x42, 1]).reply.unwrap(), [0xa2, 1, 3]);
|
||||||
|
assert_eq!(hid.control(&[0x52, 1, 2]).reply.unwrap(), [0]);
|
||||||
|
assert_eq!(hid.leds, 2);
|
||||||
|
assert_eq!(hid.control(&[0x41, 99]).reply.unwrap(), [2]);
|
||||||
|
assert_eq!(hid.control(&[0x41]).reply.unwrap(), [4]);
|
||||||
|
assert_eq!(hid.control(&[0x90, 0]).reply.unwrap(), [3]);
|
||||||
|
assert_eq!(
|
||||||
|
hid.control(&[0x49, 1, 3, 0]).reply.unwrap(),
|
||||||
|
[0xa1, 1, 0, 0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn suspend_reset_and_virtual_unplug() {
|
||||||
|
let mut hid = HidProtocol::default();
|
||||||
|
assert!(hid.control(&[0x13]).reset);
|
||||||
|
assert!(hid.input(Report::Mouse, &[0; 4]).is_err());
|
||||||
|
assert!(hid.control(&[0x14]).reset);
|
||||||
|
assert!(hid.input(Report::Mouse, &[0; 4]).is_ok());
|
||||||
|
assert!(hid.control(&[0x15]).unplug);
|
||||||
|
hid.keyboard[2] = 4;
|
||||||
|
hid.buttons = 1;
|
||||||
|
assert_eq!(hid.release().len(), 3);
|
||||||
|
assert_eq!(hid.keyboard, [0; 8]);
|
||||||
|
assert_eq!(hid.buttons, 0);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn sdp_describes_both_classic_channels() {
|
||||||
|
let record = sdp();
|
||||||
|
assert!(record.contains("uuid value=\"0x1124\""));
|
||||||
|
assert!(record.contains("uint16 value=\"0x0011\""));
|
||||||
|
assert!(record.contains("uint16 value=\"0x0013\""));
|
||||||
|
assert!(!record.contains("0x1812"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn descriptor_report_sizes() {
|
||||||
|
let (mut pos, mut size, mut count, mut id, mut depth) = (0, 0, 0, 0, 0);
|
||||||
|
let mut input = [0; 4];
|
||||||
|
let mut output = [0; 4];
|
||||||
|
while pos < MAP.len() {
|
||||||
|
let prefix = MAP[pos];
|
||||||
|
pos += 1;
|
||||||
|
let length = [0, 1, 2, 4][(prefix & 3) as usize];
|
||||||
|
let mut value = 0;
|
||||||
|
for i in 0..length {
|
||||||
|
value |= (MAP[pos + i] as usize) << (8 * i);
|
||||||
|
}
|
||||||
|
pos += length;
|
||||||
|
match ((prefix >> 2) & 3, prefix >> 4) {
|
||||||
|
(1, 7) => size = value,
|
||||||
|
(1, 8) => id = value,
|
||||||
|
(1, 9) => count = value,
|
||||||
|
(0, 8) => input[id] += size * count,
|
||||||
|
(0, 9) => output[id] += size * count,
|
||||||
|
(0, 10) => depth += 1,
|
||||||
|
(0, 12) => depth -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(depth, 0);
|
||||||
|
assert_eq!(input, [0, 64, 32, 16]);
|
||||||
|
assert_eq!(output, [0, 8, 0, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
libs/hwcodec/.gitignore
vendored
2
libs/hwcodec/.gitignore
vendored
@@ -8,8 +8,6 @@
|
|||||||
/ffmpeg/linux/debug
|
/ffmpeg/linux/debug
|
||||||
!/ffmpeg/mac
|
!/ffmpeg/mac
|
||||||
/ffmpeg/mac/debug
|
/ffmpeg/mac/debug
|
||||||
!/ffmpeg/android
|
|
||||||
/ffmpeg/android/debug
|
|
||||||
!/ffmpeg/ios
|
!/ffmpeg/ios
|
||||||
/ffmpeg/ios/debug
|
/ffmpeg/ios/debug
|
||||||
/input
|
/input
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ serde_json = "1.0"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cc = "1.0"
|
cc = "1.0"
|
||||||
bindgen = "0.70.1"
|
bindgen = "0.72"
|
||||||
|
|||||||
@@ -41,18 +41,6 @@ Based on the information above, there are several optimizations and changes made
|
|||||||
* remove hevc_vaapi because of possible poor quality
|
* remove hevc_vaapi because of possible poor quality
|
||||||
* amf: not tested, https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/378
|
* amf: not tested, https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/378
|
||||||
|
|
||||||
### MacOS
|
|
||||||
|
|
||||||
| FFmpeg ram encode | FFmpeg ram decode |
|
|
||||||
| ------------------ | ------------------ |
|
|
||||||
| h265 only | Y |
|
|
||||||
|
|
||||||
### Android
|
|
||||||
|
|
||||||
| FFmpeg ram encode |
|
|
||||||
| ------------------ |
|
|
||||||
| Y |
|
|
||||||
|
|
||||||
## System requirements
|
## System requirements
|
||||||
|
|
||||||
* intel
|
* intel
|
||||||
@@ -76,4 +64,3 @@ Based on the information above, there are several optimizations and changes made
|
|||||||
https://docs.nvidia.com/video-technologies/video-codec-sdk/11.1/read-me/index.html
|
https://docs.nvidia.com/video-technologies/video-codec-sdk/11.1/read-me/index.html
|
||||||
|
|
||||||
https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new?ncid=em-prod-816193
|
https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new?ncid=em-prod-816193
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
// Read the current source path when running, since build artifacts may move.
|
||||||
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let cpp_dir = manifest_dir.join("cpp");
|
let cpp_dir = manifest_dir.join("cpp");
|
||||||
println!("cargo:rerun-if-changed=src");
|
println!("cargo:rerun-if-changed=src");
|
||||||
println!("cargo:rerun-if-changed={}", cpp_dir.display());
|
println!("cargo:rerun-if-changed={}", cpp_dir.display());
|
||||||
@@ -17,19 +18,15 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_common(builder: &mut Build) {
|
fn build_common(builder: &mut Build) {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||||
let common_dir = manifest_dir.join("cpp").join("common");
|
let common_dir = manifest_dir.join("cpp").join("common");
|
||||||
|
|
||||||
let mut bindings = bindgen::builder()
|
let bindings = bindgen::builder()
|
||||||
.header(common_dir.join("common.h").to_string_lossy().to_string())
|
.header(common_dir.join("common.h").to_string_lossy().to_string())
|
||||||
.header(common_dir.join("callback.h").to_string_lossy().to_string())
|
.header(common_dir.join("callback.h").to_string_lossy().to_string())
|
||||||
.rustified_enum(".*")
|
.rustified_enum(".*")
|
||||||
.parse_callbacks(Box::new(CommonCallbacks));
|
.parse_callbacks(Box::new(CommonCallbacks));
|
||||||
if target_os == "android" {
|
|
||||||
print_android_bindgen_env();
|
|
||||||
bindings = bindings.clang_args(android_clang_args());
|
|
||||||
}
|
|
||||||
bindings
|
bindings
|
||||||
.generate()
|
.generate()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -62,9 +59,9 @@ fn build_common(builder: &mut Build) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unsupported platforms
|
// Unsupported platforms
|
||||||
if target_os != "windows" && target_os != "linux" && target_os != "android" {
|
if target_os != "windows" && target_os != "linux" {
|
||||||
panic!(
|
panic!(
|
||||||
"Unsupported OS: {}. Only Windows, Linux, and Android are supported.",
|
"Unsupported OS: {}. Only Windows and Linux are supported.",
|
||||||
target_os
|
target_os
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -89,123 +86,12 @@ impl bindgen::callbacks::ParseCallbacks for CommonCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_android_bindgen_env() {
|
|
||||||
println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME");
|
|
||||||
println!("cargo:rerun-if-env-changed=ANDROID_NDK_ROOT");
|
|
||||||
println!("cargo:rerun-if-env-changed=NDK_HOME");
|
|
||||||
println!("cargo:rerun-if-env-changed=ANDROID_HOME");
|
|
||||||
println!("cargo:rerun-if-env-changed=ANDROID_SDK_ROOT");
|
|
||||||
println!("cargo:rerun-if-env-changed=CARGO_NDK_PLATFORM");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn android_clang_args() -> Vec<String> {
|
|
||||||
let ndk = android_ndk_home();
|
|
||||||
let target = env::var("TARGET").unwrap_or_default();
|
|
||||||
let toolchain = ndk.join("toolchains/llvm/prebuilt").join(host_tag());
|
|
||||||
let sysroot = toolchain.join("sysroot");
|
|
||||||
let clang_include = toolchain
|
|
||||||
.join("lib/clang")
|
|
||||||
.join(clang_version(&toolchain))
|
|
||||||
.join("include");
|
|
||||||
let api = env::var("CARGO_NDK_PLATFORM")
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse::<u32>().ok())
|
|
||||||
.unwrap_or(21);
|
|
||||||
let clang_target = android_clang_target(&target);
|
|
||||||
|
|
||||||
vec![
|
|
||||||
format!("--target={clang_target}"),
|
|
||||||
format!("--sysroot={}", sysroot.display()),
|
|
||||||
format!("-D__ANDROID_API__={api}"),
|
|
||||||
format!("-isystem{}", clang_include.display()),
|
|
||||||
format!("-isystem{}", sysroot.join("usr/include").display()),
|
|
||||||
format!(
|
|
||||||
"-isystem{}",
|
|
||||||
sysroot.join("usr/include").join(clang_target).display()
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn android_clang_target(target: &str) -> &'static str {
|
|
||||||
match target {
|
|
||||||
"aarch64-linux-android" => "aarch64-linux-android",
|
|
||||||
"armv7-linux-androideabi" => "armv7a-linux-androideabi",
|
|
||||||
"i686-linux-android" => "i686-linux-android",
|
|
||||||
"x86_64-linux-android" => "x86_64-linux-android",
|
|
||||||
other => panic!("unsupported Android target for hwcodec bindgen: {other}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn android_ndk_home() -> PathBuf {
|
|
||||||
for key in ["ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "NDK_HOME"] {
|
|
||||||
if let Ok(value) = env::var(key) {
|
|
||||||
return PathBuf::from(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key in ["ANDROID_HOME", "ANDROID_SDK_ROOT"] {
|
|
||||||
if let Ok(value) = env::var(key) {
|
|
||||||
let ndk_dir = PathBuf::from(value).join("ndk");
|
|
||||||
if let Some(newest) = newest_child_dir(&ndk_dir) {
|
|
||||||
return newest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panic!(
|
|
||||||
"hwcodec Android bindgen requires ANDROID_NDK_HOME, ANDROID_NDK_ROOT, NDK_HOME, \
|
|
||||||
or ANDROID_HOME/ANDROID_SDK_ROOT with an ndk directory"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn newest_child_dir(path: &Path) -> Option<PathBuf> {
|
|
||||||
let mut entries = std::fs::read_dir(path)
|
|
||||||
.ok()?
|
|
||||||
.filter_map(|entry| entry.ok())
|
|
||||||
.map(|entry| entry.path())
|
|
||||||
.filter(|path| path.is_dir())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
entries.sort();
|
|
||||||
entries.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn host_tag() -> &'static str {
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
"linux-x86_64"
|
|
||||||
} else if cfg!(target_os = "macos") {
|
|
||||||
"darwin-x86_64"
|
|
||||||
} else if cfg!(target_os = "windows") {
|
|
||||||
"windows-x86_64"
|
|
||||||
} else {
|
|
||||||
panic!("unsupported host OS for Android NDK");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clang_version(toolchain: &Path) -> String {
|
|
||||||
let clang_dir = toolchain.join("lib/clang");
|
|
||||||
let mut entries = std::fs::read_dir(&clang_dir)
|
|
||||||
.unwrap_or_else(|_| panic!("missing NDK clang directory: {}", clang_dir.display()))
|
|
||||||
.filter_map(|entry| entry.ok())
|
|
||||||
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
entries.sort();
|
|
||||||
entries
|
|
||||||
.pop()
|
|
||||||
.unwrap_or_else(|| panic!("no clang versions found under: {}", clang_dir.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
mod ffmpeg {
|
mod ffmpeg {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn build_ffmpeg(builder: &mut Build) {
|
pub fn build_ffmpeg(builder: &mut Build) {
|
||||||
ffmpeg_ffi();
|
ffmpeg_ffi();
|
||||||
|
|
||||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") {
|
|
||||||
link_android_ffmpeg(builder);
|
|
||||||
build_ffmpeg_ram(builder);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try VCPKG first, fallback to system FFmpeg via pkg-config
|
// Try VCPKG first, fallback to system FFmpeg via pkg-config
|
||||||
if let Some(vcpkg_installed) = vcpkg_installed_root() {
|
if let Some(vcpkg_installed) = vcpkg_installed_root() {
|
||||||
link_vcpkg(builder, vcpkg_installed);
|
link_vcpkg(builder, vcpkg_installed);
|
||||||
@@ -220,67 +106,6 @@ mod ffmpeg {
|
|||||||
build_ffmpeg_capture(builder);
|
build_ffmpeg_capture(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn link_android_ffmpeg(builder: &mut Build) {
|
|
||||||
let root = std::env::var("ONE_KVM_ANDROID_FFMPEG_ROOT").unwrap_or_else(|_| {
|
|
||||||
panic!(
|
|
||||||
"ONE_KVM_ANDROID_FFMPEG_ROOT is required when building hwcodec for Android. \
|
|
||||||
It must point to an FFmpeg Android build with MediaCodec enabled."
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let root = PathBuf::from(root);
|
|
||||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
|
||||||
let abi = match target_arch.as_str() {
|
|
||||||
"aarch64" => "arm64-v8a",
|
|
||||||
"arm" => "armeabi-v7a",
|
|
||||||
"x86" => "x86",
|
|
||||||
"x86_64" => "x86_64",
|
|
||||||
_ => target_arch.as_str(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let abi_root = root.join(abi);
|
|
||||||
let lib_dir = if abi_root.join("lib").exists() {
|
|
||||||
abi_root.join("lib")
|
|
||||||
} else {
|
|
||||||
root.join("lib")
|
|
||||||
};
|
|
||||||
let include_dir = if abi_root.join("include").exists() {
|
|
||||||
abi_root.join("include")
|
|
||||||
} else {
|
|
||||||
root.join("include")
|
|
||||||
};
|
|
||||||
|
|
||||||
if !include_dir.exists() || !lib_dir.exists() {
|
|
||||||
panic!(
|
|
||||||
"Invalid ONE_KVM_ANDROID_FFMPEG_ROOT: include/lib not found for ABI {} under {}",
|
|
||||||
abi,
|
|
||||||
root.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("cargo:rustc-link-search=native={}", lib_dir.display());
|
|
||||||
builder.include(&include_dir);
|
|
||||||
|
|
||||||
let use_static = std::env::var("ONE_KVM_ANDROID_FFMPEG_STATIC")
|
|
||||||
.map(|value| value != "0")
|
|
||||||
.unwrap_or(true);
|
|
||||||
for lib in ["avcodec", "avutil"] {
|
|
||||||
if use_static {
|
|
||||||
println!("cargo:rustc-link-lib=static={}", lib);
|
|
||||||
} else {
|
|
||||||
println!("cargo:rustc-link-lib={}", lib);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("cargo:rustc-link-lib=log");
|
|
||||||
println!("cargo:rustc-link-lib=mediandk");
|
|
||||||
println!("cargo:rustc-link-lib=android");
|
|
||||||
println!("cargo:rustc-link-lib=dl");
|
|
||||||
println!("cargo:rustc-link-lib=m");
|
|
||||||
println!("cargo:rustc-link-lib=z");
|
|
||||||
println!("cargo:rustc-link-lib=c++_shared");
|
|
||||||
println!("cargo:info=Using Android FFmpeg from {}", root.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn vcpkg_installed_root() -> Option<PathBuf> {
|
fn vcpkg_installed_root() -> Option<PathBuf> {
|
||||||
println!("cargo:rerun-if-env-changed=VCPKG_INSTALLED_DIR");
|
println!("cargo:rerun-if-env-changed=VCPKG_INSTALLED_DIR");
|
||||||
println!("cargo:rerun-if-env-changed=VCPKG_ROOT");
|
println!("cargo:rerun-if-env-changed=VCPKG_ROOT");
|
||||||
@@ -338,7 +163,7 @@ mod ffmpeg {
|
|||||||
println!("cargo:rustc-link-lib=static=avcodec");
|
println!("cargo:rustc-link-lib=static=avcodec");
|
||||||
println!("cargo:rustc-link-lib=static=avutil");
|
println!("cargo:rustc-link-lib=static=avutil");
|
||||||
|
|
||||||
// Link hardware acceleration dependencies (dynamic)
|
// Link hardware acceleration dependencies
|
||||||
// These vary by architecture
|
// These vary by architecture
|
||||||
if target_arch == "x86_64" {
|
if target_arch == "x86_64" {
|
||||||
// VAAPI for x86_64
|
// VAAPI for x86_64
|
||||||
@@ -347,13 +172,11 @@ mod ffmpeg {
|
|||||||
println!("cargo:rustc-link-lib=va-x11"); // Required for vaGetDisplay
|
println!("cargo:rustc-link-lib=va-x11"); // Required for vaGetDisplay
|
||||||
println!("cargo:rustc-link-lib=mfx");
|
println!("cargo:rustc-link-lib=mfx");
|
||||||
} else {
|
} else {
|
||||||
// RKMPP for ARM
|
for lib in ["rockchip_mpp", "rga"] {
|
||||||
println!("cargo:rustc-link-lib=rockchip_mpp");
|
if !lib_dir.join(format!("lib{lib}.a")).exists() {
|
||||||
let rga_static = lib_dir.join("librga.a");
|
panic!("missing static library: lib{lib}.a");
|
||||||
if rga_static.exists() {
|
}
|
||||||
println!("cargo:rustc-link-lib=static=rga");
|
println!("cargo:rustc-link-lib=static={}", lib);
|
||||||
} else {
|
|
||||||
println!("cargo:rustc-link-lib=rga");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,12 +235,6 @@ mod ffmpeg {
|
|||||||
// For static linking, link FFmpeg libs statically, others dynamically
|
// For static linking, link FFmpeg libs statically, others dynamically
|
||||||
if lib_name.starts_with("av") || lib_name == "swresample" {
|
if lib_name.starts_with("av") || lib_name == "swresample" {
|
||||||
println!("cargo:rustc-link-lib=static={}", lib_name);
|
println!("cargo:rustc-link-lib=static={}", lib_name);
|
||||||
} else if lib_name == "rga"
|
|
||||||
&& link_paths
|
|
||||||
.iter()
|
|
||||||
.any(|path| Path::new(path).join("librga.a").exists())
|
|
||||||
{
|
|
||||||
println!("cargo:rustc-link-lib=static=rga");
|
|
||||||
} else {
|
} else {
|
||||||
// Runtime libraries (va, drm, etc.) must be dynamic
|
// Runtime libraries (va, drm, etc.) must be dynamic
|
||||||
println!("cargo:rustc-link-lib={}", lib_name);
|
println!("cargo:rustc-link-lib={}", lib_name);
|
||||||
@@ -538,11 +355,9 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
// ARM (aarch64, arm): no X11 needed, uses RKMPP/V4L2
|
// ARM (aarch64, arm): no X11 needed, uses RKMPP/V4L2
|
||||||
v
|
v
|
||||||
} else if target_os == "android" {
|
|
||||||
Vec::new()
|
|
||||||
} else {
|
} else {
|
||||||
panic!(
|
panic!(
|
||||||
"Unsupported OS: {}. Only Windows, Linux, and Android are supported.",
|
"Unsupported OS: {}. Only Windows and Linux are supported.",
|
||||||
target_os
|
target_os
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -553,18 +368,12 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ffmpeg_ffi() {
|
fn ffmpeg_ffi() {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("common");
|
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("common");
|
||||||
let ffi_header_path = ffmpeg_ram_dir.join("ffmpeg_ffi.h");
|
let ffi_header_path = ffmpeg_ram_dir.join("ffmpeg_ffi.h");
|
||||||
println!("cargo:rerun-if-changed={}", ffi_header_path.display());
|
println!("cargo:rerun-if-changed={}", ffi_header_path.display());
|
||||||
let ffi_header = ffi_header_path.to_string_lossy().to_string();
|
let ffi_header = ffi_header_path.to_string_lossy().to_string();
|
||||||
let mut bindings = bindgen::builder()
|
let bindings = bindgen::builder().header(ffi_header).rustified_enum(".*");
|
||||||
.header(ffi_header)
|
|
||||||
.rustified_enum(".*");
|
|
||||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") {
|
|
||||||
print_android_bindgen_env();
|
|
||||||
bindings = bindings.clang_args(android_clang_args());
|
|
||||||
}
|
|
||||||
bindings
|
bindings
|
||||||
.generate()
|
.generate()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -573,19 +382,13 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_ffmpeg_ram(builder: &mut Build) {
|
fn build_ffmpeg_ram(builder: &mut Build) {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("ffmpeg_ram");
|
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("ffmpeg_ram");
|
||||||
let ffi_header = ffmpeg_ram_dir
|
let ffi_header = ffmpeg_ram_dir
|
||||||
.join("ffmpeg_ram_ffi.h")
|
.join("ffmpeg_ram_ffi.h")
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
let mut bindings = bindgen::builder()
|
let bindings = bindgen::builder().header(ffi_header).rustified_enum(".*");
|
||||||
.header(ffi_header)
|
|
||||||
.rustified_enum(".*");
|
|
||||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android") {
|
|
||||||
print_android_bindgen_env();
|
|
||||||
bindings = bindings.clang_args(android_clang_args());
|
|
||||||
}
|
|
||||||
bindings
|
bindings
|
||||||
.generate()
|
.generate()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -597,12 +400,13 @@ mod ffmpeg {
|
|||||||
// RKMPP decode only exists on ARM builds where FFmpeg is compiled with RKMPP support.
|
// RKMPP decode only exists on ARM builds where FFmpeg is compiled with RKMPP support.
|
||||||
// Avoid compiling this file on x86/x64 where `AV_HWDEVICE_TYPE_RKMPP` doesn't exist.
|
// Avoid compiling this file on x86/x64 where `AV_HWDEVICE_TYPE_RKMPP` doesn't exist.
|
||||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
let enable_rkmpp = matches!(target_arch.as_str(), "aarch64" | "arm")
|
||||||
let enable_rkmpp = target_os != "android"
|
|
||||||
&& matches!(target_arch.as_str(), "aarch64" | "arm")
|
|
||||||
|| std::env::var_os("CARGO_FEATURE_RKMPP").is_some();
|
|| std::env::var_os("CARGO_FEATURE_RKMPP").is_some();
|
||||||
if enable_rkmpp {
|
if enable_rkmpp {
|
||||||
builder.file(ffmpeg_ram_dir.join("ffmpeg_ram_decode.cpp"));
|
builder.file(ffmpeg_ram_dir.join("ffmpeg_ram_decode.cpp"));
|
||||||
|
if enable_rkmpp {
|
||||||
|
builder.define("ONE_KVM_FFMPEG_RKMPP", None);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"cargo:info=Skipping ffmpeg_ram_decode.cpp (RKMPP) for arch {}",
|
"cargo:info=Skipping ffmpeg_ram_decode.cpp (RKMPP) for arch {}",
|
||||||
@@ -617,7 +421,7 @@ mod ffmpeg {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let capture_header = manifest_dir
|
let capture_header = manifest_dir
|
||||||
.join("cpp")
|
.join("cpp")
|
||||||
.join("ffmpeg_capture_ffi.h")
|
.join("ffmpeg_capture_ffi.h")
|
||||||
@@ -640,7 +444,7 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_ffmpeg_hw(builder: &mut Build) {
|
fn build_ffmpeg_hw(builder: &mut Build) {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
|
let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
|
||||||
let ffi_header = ffmpeg_hw_dir
|
let ffi_header = ffmpeg_hw_dir
|
||||||
.join("ffmpeg_hw_ffi.h")
|
.join("ffmpeg_hw_ffi.h")
|
||||||
@@ -655,9 +459,7 @@ mod ffmpeg {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
let enable_rkmpp = matches!(target_arch.as_str(), "aarch64" | "arm")
|
||||||
let enable_rkmpp = target_os != "android"
|
|
||||||
&& matches!(target_arch.as_str(), "aarch64" | "arm")
|
|
||||||
|| std::env::var_os("CARGO_FEATURE_RKMPP").is_some();
|
|| std::env::var_os("CARGO_FEATURE_RKMPP").is_some();
|
||||||
if enable_rkmpp {
|
if enable_rkmpp {
|
||||||
// Include RGA headers for NV16->NV12 conversion (RGA im2d API)
|
// Include RGA headers for NV16->NV12 conversion (RGA im2d API)
|
||||||
@@ -686,6 +488,7 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.file(ffmpeg_hw_dir.join("ffmpeg_hw_mjpeg_h26x.cpp"));
|
builder.file(ffmpeg_hw_dir.join("ffmpeg_hw_mjpeg_h26x.cpp"));
|
||||||
|
builder.file(ffmpeg_hw_dir.join("rkmpp_dmabuf.cpp"));
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"cargo:info=Skipping ffmpeg_hw_mjpeg_h26x.cpp (RKMPP) for arch {}",
|
"cargo:info=Skipping ffmpeg_hw_mjpeg_h26x.cpp (RKMPP) for arch {}",
|
||||||
|
|||||||
@@ -146,6 +146,67 @@ int linux_support_v4l2m2m() {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto is_qcom_platform = [&]() -> bool {
|
||||||
|
const char *platform_hints[] = {
|
||||||
|
"qcom",
|
||||||
|
"qualcomm",
|
||||||
|
"venus",
|
||||||
|
"iris",
|
||||||
|
"sc7280",
|
||||||
|
"qcm6490",
|
||||||
|
"qcs6490",
|
||||||
|
};
|
||||||
|
|
||||||
|
const char *platform_files[] = {
|
||||||
|
"/proc/device-tree/compatible",
|
||||||
|
"/proc/device-tree/model",
|
||||||
|
"/sys/firmware/devicetree/base/compatible",
|
||||||
|
"/sys/firmware/devicetree/base/model",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(platform_files) / sizeof(platform_files[0]); i++) {
|
||||||
|
std::string value;
|
||||||
|
if (read_text_file(platform_files[i], &value) &&
|
||||||
|
contains_any(to_lower(value), platform_hints,
|
||||||
|
sizeof(platform_hints) / sizeof(platform_hints[0]))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *video_nodes[] = {
|
||||||
|
"video0",
|
||||||
|
"video1",
|
||||||
|
"video2",
|
||||||
|
"video3",
|
||||||
|
"video10",
|
||||||
|
"video11",
|
||||||
|
"video32",
|
||||||
|
};
|
||||||
|
const char *video_hints[] = {
|
||||||
|
"qcom-iris",
|
||||||
|
"qcom,",
|
||||||
|
"venus",
|
||||||
|
"iris",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(video_nodes) / sizeof(video_nodes[0]); i++) {
|
||||||
|
std::string name;
|
||||||
|
std::string modalias;
|
||||||
|
const std::string base = std::string("/sys/class/video4linux/") + video_nodes[i];
|
||||||
|
if (read_text_file((base + "/name").c_str(), &name) &&
|
||||||
|
contains_any(to_lower(name), video_hints, sizeof(video_hints) / sizeof(video_hints[0]))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (read_text_file((base + "/device/modalias").c_str(), &modalias) &&
|
||||||
|
contains_any(to_lower(modalias), video_hints,
|
||||||
|
sizeof(video_hints) / sizeof(video_hints[0]))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
auto is_amlogic_platform = [&]() -> bool {
|
auto is_amlogic_platform = [&]() -> bool {
|
||||||
const char *platform_hints[] = {
|
const char *platform_hints[] = {
|
||||||
"amlogic",
|
"amlogic",
|
||||||
@@ -210,7 +271,8 @@ int linux_support_v4l2m2m() {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const bool amlogic_platform = is_amlogic_platform();
|
const bool qcom_platform = is_qcom_platform();
|
||||||
|
const bool amlogic_platform = !qcom_platform && is_amlogic_platform();
|
||||||
if (amlogic_platform && !v4l2m2m_allowed()) {
|
if (amlogic_platform && !v4l2m2m_allowed()) {
|
||||||
LOG_WARN(std::string(
|
LOG_WARN(std::string(
|
||||||
"V4L2 M2M: skipped probe on Amlogic platform; set ONE_KVM_V4L2M2M_ALLOW=1 to enable"));
|
"V4L2 M2M: skipped probe on Amlogic platform; set ONE_KVM_V4L2M2M_ALLOW=1 to enable"));
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ extern "C" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <fstream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -24,7 +27,7 @@ bool is_software_h264(const std::string &name) {
|
|||||||
// Exclude all hardware encoders
|
// Exclude all hardware encoders
|
||||||
static const char* hw_suffixes[] = {
|
static const char* hw_suffixes[] = {
|
||||||
"nvenc", "amf", "qsv", "vaapi", "rkmpp",
|
"nvenc", "amf", "qsv", "vaapi", "rkmpp",
|
||||||
"v4l2m2m", "videotoolbox", "mediacodec", "_mf"
|
"v4l2m2m", "videotoolbox", "_mf"
|
||||||
};
|
};
|
||||||
for (const auto& suffix : hw_suffixes) {
|
for (const auto& suffix : hw_suffixes) {
|
||||||
if (name.find(suffix) != std::string::npos) return false;
|
if (name.find(suffix) != std::string::npos) return false;
|
||||||
@@ -37,7 +40,7 @@ bool is_software_hevc(const std::string &name) {
|
|||||||
if (name != "hevc" && name != "libx265") return false;
|
if (name != "hevc" && name != "libx265") return false;
|
||||||
static const char* hw_suffixes[] = {
|
static const char* hw_suffixes[] = {
|
||||||
"nvenc", "amf", "qsv", "vaapi", "rkmpp",
|
"nvenc", "amf", "qsv", "vaapi", "rkmpp",
|
||||||
"v4l2m2m", "videotoolbox", "mediacodec", "_mf"
|
"v4l2m2m", "videotoolbox", "_mf"
|
||||||
};
|
};
|
||||||
for (const auto& suffix : hw_suffixes) {
|
for (const auto& suffix : hw_suffixes) {
|
||||||
if (name.find(suffix) != std::string::npos) return false;
|
if (name.find(suffix) != std::string::npos) return false;
|
||||||
@@ -45,17 +48,46 @@ bool is_software_hevc(const std::string &name) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool is_qcom_iris_driver() {
|
||||||
|
const char *driver_path = "/sys/class/video4linux/video1/name";
|
||||||
|
std::ifstream file(driver_path);
|
||||||
|
if (!file.is_open()) return false;
|
||||||
|
|
||||||
|
std::string value;
|
||||||
|
std::getline(file, value, '\0');
|
||||||
|
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
return value.find("qcom-iris") != std::string::npos ||
|
||||||
|
value.find("iris-encoder") != std::string::npos ||
|
||||||
|
value.find("iris") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
namespace util_encode {
|
namespace util_encode {
|
||||||
|
|
||||||
|
bool is_qcom_iris_platform() {
|
||||||
|
return is_qcom_iris_driver();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool supports_forced_keyframe(const std::string &name) {
|
||||||
|
if (name.find("v4l2m2m") != std::string::npos && is_qcom_iris_platform()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
||||||
int gop, int fps, int thread_count) {
|
int gop, int fps, int thread_count) {
|
||||||
c->has_b_frames = 0;
|
c->has_b_frames = 0;
|
||||||
c->max_b_frames = 0;
|
c->max_b_frames = 0;
|
||||||
if (gop > 0 && gop < std::numeric_limits<int16_t>::max()) {
|
const bool qcom_iris_v4l2 =
|
||||||
c->gop_size = gop;
|
name.find("v4l2m2m") != std::string::npos && is_qcom_iris_platform();
|
||||||
c->keyint_min = gop; // Match keyint_min to gop for consistent keyframe interval
|
const int effective_gop = qcom_iris_v4l2 ? std::max(5, fps / 3) : gop;
|
||||||
|
if (effective_gop > 0 && effective_gop < std::numeric_limits<int16_t>::max()) {
|
||||||
|
c->gop_size = effective_gop;
|
||||||
|
c->keyint_min = effective_gop; // Match keyint_min to gop for consistent keyframe interval
|
||||||
} else if (name.find("vaapi") != std::string::npos) {
|
} else if (name.find("vaapi") != std::string::npos) {
|
||||||
c->gop_size = fps > 0 ? fps : 30; // Default to 1 second keyframe interval
|
c->gop_size = fps > 0 ? fps : 30; // Default to 1 second keyframe interval
|
||||||
c->keyint_min = c->gop_size;
|
c->keyint_min = c->gop_size;
|
||||||
@@ -100,13 +132,8 @@ void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
|||||||
c->color_primaries = AVCOL_PRI_SMPTE170M;
|
c->color_primaries = AVCOL_PRI_SMPTE170M;
|
||||||
c->color_trc = AVCOL_TRC_SMPTE170M;
|
c->color_trc = AVCOL_TRC_SMPTE170M;
|
||||||
|
|
||||||
// WebRTC SDP advertises constrained baseline. Keep most hardware and software
|
// WebRTC SDP advertises constrained baseline. Keep hardware and software
|
||||||
// encoders on the same browser-friendly H264 profile. Android MediaCodec is
|
// encoders on the same browser-friendly H264 profile.
|
||||||
// deliberately excluded because older vendor OMX encoders can reject explicit
|
|
||||||
// profile/level combinations during configure().
|
|
||||||
if (name.find("mediacodec") != std::string::npos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (name.find("h264") != std::string::npos) {
|
if (name.find("h264") != std::string::npos) {
|
||||||
c->profile = AV_PROFILE_H264_CONSTRAINED_BASELINE;
|
c->profile = AV_PROFILE_H264_CONSTRAINED_BASELINE;
|
||||||
} else if (name.find("hevc") != std::string::npos) {
|
} else if (name.find("hevc") != std::string::npos) {
|
||||||
@@ -125,7 +152,8 @@ bool set_lantency_free(void *priv_data, const std::string &name) {
|
|||||||
}
|
}
|
||||||
if (name.find("amf") != std::string::npos) {
|
if (name.find("amf") != std::string::npos) {
|
||||||
if ((ret = av_opt_set(priv_data, "query_timeout", "1000", 0)) < 0) {
|
if ((ret = av_opt_set(priv_data, "query_timeout", "1000", 0)) < 0) {
|
||||||
LOG_WARN(std::string("amf query_timeout option is unavailable, ret = ") + av_err2str(ret));
|
LOG_DEBUG(std::string("amf query_timeout option is unavailable, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (name.find("qsv") != std::string::npos) {
|
if (name.find("qsv") != std::string::npos) {
|
||||||
@@ -144,7 +172,8 @@ bool set_lantency_free(void *priv_data, const std::string &name) {
|
|||||||
if (name.find("rkmpp") != std::string::npos) {
|
if (name.find("rkmpp") != std::string::npos) {
|
||||||
// Set async_depth to 1 for minimal buffering (0 = synchronous, higher = more buffering)
|
// Set async_depth to 1 for minimal buffering (0 = synchronous, higher = more buffering)
|
||||||
if ((ret = av_opt_set(priv_data, "async_depth", "1", 0)) < 0) {
|
if ((ret = av_opt_set(priv_data, "async_depth", "1", 0)) < 0) {
|
||||||
LOG_WARN(std::string("rkmpp set async_depth failed, ret = ") + av_err2str(ret));
|
LOG_DEBUG(std::string("rkmpp async_depth option is unavailable, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
// Not fatal - older FFmpeg versions may not support this option
|
// Not fatal - older FFmpeg versions may not support this option
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,11 +181,14 @@ bool set_lantency_free(void *priv_data, const std::string &name) {
|
|||||||
if (name.find("v4l2m2m") != std::string::npos) {
|
if (name.find("v4l2m2m") != std::string::npos) {
|
||||||
// Minimize number of output buffers for lower latency
|
// Minimize number of output buffers for lower latency
|
||||||
if ((ret = av_opt_set_int(priv_data, "num_output_buffers", 4, 0)) < 0) {
|
if ((ret = av_opt_set_int(priv_data, "num_output_buffers", 4, 0)) < 0) {
|
||||||
LOG_WARN(std::string("v4l2m2m set num_output_buffers failed, ret = ") + av_err2str(ret));
|
LOG_DEBUG(std::string("v4l2m2m num_output_buffers option is unavailable, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
// Not fatal
|
// Not fatal
|
||||||
}
|
}
|
||||||
if ((ret = av_opt_set_int(priv_data, "num_capture_buffers", 4, 0)) < 0) {
|
const int capture_buffers = is_qcom_iris_driver() ? 12 : 8;
|
||||||
LOG_WARN(std::string("v4l2m2m set num_capture_buffers failed, ret = ") + av_err2str(ret));
|
if ((ret = av_opt_set_int(priv_data, "num_capture_buffers", capture_buffers, 0)) < 0) {
|
||||||
|
LOG_DEBUG(std::string("v4l2m2m num_capture_buffers option is unavailable, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
// Not fatal
|
// Not fatal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,9 +342,6 @@ bool set_quality(void *priv_data, const std::string &name, int quality) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Do not force MediaCodec level here. Some Android TV vendor encoders,
|
|
||||||
// including older Amlogic OMX implementations, reject explicit level values
|
|
||||||
// even when they support the requested resolution and bitrate.
|
|
||||||
// libx264 software encoder presets
|
// libx264 software encoder presets
|
||||||
if (is_software_h264(name)) {
|
if (is_software_h264(name)) {
|
||||||
const char* preset = nullptr;
|
const char* preset = nullptr;
|
||||||
@@ -368,6 +397,33 @@ struct CodecOptions {
|
|||||||
|
|
||||||
bool set_rate_control(AVCodecContext *c, const std::string &name, int rc,
|
bool set_rate_control(AVCodecContext *c, const std::string &name, int rc,
|
||||||
int q) {
|
int q) {
|
||||||
|
// Remote-desktop content is usually sparse. VBR avoids padding static
|
||||||
|
// frames up to the target bitrate while allowing short bursts for screen
|
||||||
|
// changes. Keep those bursts bounded at twice the target bitrate.
|
||||||
|
if (rc == RC_VBR && c->bit_rate > 0) {
|
||||||
|
c->rc_max_rate = c->bit_rate * 2;
|
||||||
|
c->rc_buffer_size = c->rc_max_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.find("vaapi") != std::string::npos && rc == RC_CQ) {
|
||||||
|
// Used only after the normal bitrate-based VAAPI initialization fails.
|
||||||
|
// Some drivers, including Intel iHD on Jasper Lake, expose CQP as their
|
||||||
|
// only compatible rate-control mode.
|
||||||
|
c->bit_rate = 0;
|
||||||
|
c->rc_min_rate = 0;
|
||||||
|
c->rc_max_rate = 0;
|
||||||
|
c->rc_buffer_size = 0;
|
||||||
|
c->rc_initial_buffer_occupancy = 0;
|
||||||
|
|
||||||
|
const int qp = q > 0 ? q : 23;
|
||||||
|
const int ret = av_opt_set_int(c->priv_data, "qp", qp, 0);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOG_ERROR(std::string("vaapi set qp failed, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (name.find("qsv") != std::string::npos) {
|
if (name.find("qsv") != std::string::npos) {
|
||||||
// https://github.com/LizardByte/Sunshine/blob/3e47cd3cc8fd37a7a88be82444ff4f3c0022856b/src/video.cpp#L1635
|
// https://github.com/LizardByte/Sunshine/blob/3e47cd3cc8fd37a7a88be82444ff4f3c0022856b/src/video.cpp#L1635
|
||||||
c->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
|
c->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
|
||||||
@@ -375,9 +431,6 @@ bool set_rate_control(AVCodecContext *c, const std::string &name, int rc,
|
|||||||
std::vector<CodecOptions> codecs = {
|
std::vector<CodecOptions> codecs = {
|
||||||
{"nvenc", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr"}}},
|
{"nvenc", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr"}}},
|
||||||
{"amf", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr_latency"}}},
|
{"amf", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr_latency"}}},
|
||||||
{"mediacodec",
|
|
||||||
"bitrate_mode",
|
|
||||||
{{RC_CBR, "cbr"}, {RC_VBR, "vbr"}, {RC_CQ, "cq"}}},
|
|
||||||
// {"videotoolbox", "constant_bit_rate", {{RC_CBR, "1"}}},
|
// {"videotoolbox", "constant_bit_rate", {{RC_CBR, "1"}}},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -392,13 +445,6 @@ bool set_rate_control(AVCodecContext *c, const std::string &name, int rc,
|
|||||||
it->second + " failed, ret = " + av_err2str(ret));
|
it->second + " failed, ret = " + av_err2str(ret));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (name.find("mediacodec") != std::string::npos) {
|
|
||||||
if (rc == RC_CQ) {
|
|
||||||
if (q >= 0 && q <= 51) {
|
|
||||||
c->global_quality = q;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -448,13 +494,6 @@ bool set_others(void *priv_data, const std::string &name) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (name.find("mediacodec") != std::string::npos) {
|
|
||||||
if ((ret = av_opt_set_int(priv_data, "ndk_codec", 1, 0)) < 0) {
|
|
||||||
LOG_ERROR(std::string("mediacodec set ndk_codec failed, ret = ") +
|
|
||||||
av_err2str(ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// NOTE: Removed idr_interval = INT_MAX for VAAPI.
|
// NOTE: Removed idr_interval = INT_MAX for VAAPI.
|
||||||
// This was disabling automatic keyframe generation.
|
// This was disabling automatic keyframe generation.
|
||||||
// The encoder should respect c->gop_size for keyframe interval.
|
// The encoder should respect c->gop_size for keyframe interval.
|
||||||
@@ -464,6 +503,10 @@ bool set_others(void *priv_data, const std::string &name) {
|
|||||||
bool change_bit_rate(AVCodecContext *c, const std::string &name, int kbs) {
|
bool change_bit_rate(AVCodecContext *c, const std::string &name, int kbs) {
|
||||||
if (kbs > 0) {
|
if (kbs > 0) {
|
||||||
c->bit_rate = kbs * 1000;
|
c->bit_rate = kbs * 1000;
|
||||||
|
if (c->rc_max_rate > 0 && name.find("qsv") == std::string::npos) {
|
||||||
|
c->rc_max_rate = c->bit_rate * 2;
|
||||||
|
c->rc_buffer_size = c->rc_max_rate;
|
||||||
|
}
|
||||||
if (name.find("qsv") != std::string::npos) {
|
if (name.find("qsv") != std::string::npos) {
|
||||||
c->rc_max_rate = c->bit_rate;
|
c->rc_max_rate = c->bit_rate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ extern "C" {
|
|||||||
|
|
||||||
namespace util_encode {
|
namespace util_encode {
|
||||||
|
|
||||||
|
bool is_qcom_iris_platform();
|
||||||
|
bool supports_forced_keyframe(const std::string &name);
|
||||||
|
|
||||||
void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
||||||
int gop, int fps, int thread_count);
|
int gop, int fps, int thread_count);
|
||||||
bool set_lantency_free(void *priv_data, const std::string &name);
|
bool set_lantency_free(void *priv_data, const std::string &name);
|
||||||
|
|||||||
38
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dma_jpeg.h
Normal file
38
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dma_jpeg.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Validate bounded JPEG headers before giving hardware a fixed-size output
|
||||||
|
// buffer. Only baseline 8-bit JPEG is accepted; other streams use copy fallback.
|
||||||
|
// No scan-data traversal or full-packet copy is needed.
|
||||||
|
inline bool rkmpp_dma_jpeg_header(const uint8_t *data, size_t size, int width, int height) {
|
||||||
|
if (!data || size < 4 || data[0] != 0xff || data[1] != 0xd8) return false;
|
||||||
|
size_t pos = 2;
|
||||||
|
bool sof = false;
|
||||||
|
while (pos < size) {
|
||||||
|
if (data[pos++] != 0xff) return false;
|
||||||
|
while (pos < size && data[pos] == 0xff) ++pos;
|
||||||
|
if (pos == size) return false;
|
||||||
|
const unsigned marker = data[pos++];
|
||||||
|
if (!marker || marker == 0xd8 || marker == 0xd9 || marker == 1 ||
|
||||||
|
(marker >= 0xd0 && marker <= 0xd7)) return false;
|
||||||
|
if (size - pos < 2) return false;
|
||||||
|
const size_t length = (size_t(data[pos]) << 8) | data[pos + 1];
|
||||||
|
if (length < 2 || length > size - pos) return false;
|
||||||
|
if (marker == 0xc0) {
|
||||||
|
if (sof || length < 8 || data[pos + 2] != 8) return false;
|
||||||
|
const unsigned h = (unsigned(data[pos + 3]) << 8) | data[pos + 4];
|
||||||
|
const unsigned w = (unsigned(data[pos + 5]) << 8) | data[pos + 6];
|
||||||
|
const unsigned components = data[pos + 7];
|
||||||
|
if (w != unsigned(width) || h != unsigned(height) ||
|
||||||
|
(components != 1 && components != 3) || length != 8 + 3 * components) return false;
|
||||||
|
sof = true;
|
||||||
|
} else if (marker >= 0xc0 && marker <= 0xcf && marker != 0xc4 && marker != 0xcc) {
|
||||||
|
return false; // Progressive, lossless, extended or differential SOF.
|
||||||
|
} else if (marker == 0xda) {
|
||||||
|
return sof && length >= 6 && size - pos > length;
|
||||||
|
}
|
||||||
|
pos += length;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
329
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dmabuf.cpp
Normal file
329
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dmabuf.cpp
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
#include "rkmpp_dmabuf_ffi.h"
|
||||||
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <limits>
|
||||||
|
#include <new>
|
||||||
|
#include "rkmpp_dma_jpeg.h"
|
||||||
|
|
||||||
|
// Native MPP is already linked by the ARM FFmpeg/RKMPP build. Keep this
|
||||||
|
// optional for toolchains which only supply the FFmpeg headers.
|
||||||
|
#if defined(__linux__) && __has_include(<rockchip/rk_mpi.h>)
|
||||||
|
#define HAVE_MPP_DMA 1
|
||||||
|
#include <cerrno>
|
||||||
|
#include <linux/dma-buf.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
extern "C" {
|
||||||
|
#include <rockchip/rk_mpi.h>
|
||||||
|
#include <rockchip/mpp_buffer.h>
|
||||||
|
#include <rockchip/mpp_frame.h>
|
||||||
|
#include <rockchip/mpp_packet.h>
|
||||||
|
#include <rockchip/mpp_task.h>
|
||||||
|
#include <rockchip/rk_venc_cfg.h>
|
||||||
|
#include <rockchip/rk_venc_rc.h>
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static thread_local char dma_error[192] = {};
|
||||||
|
static int fail(const char *operation, int code) {
|
||||||
|
std::snprintf(dma_error, sizeof(dma_error), "%s (ret=%d)", operation, code);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_MPP_DMA
|
||||||
|
struct RkmppDmaEncoder {
|
||||||
|
MppCtx ctx = nullptr;
|
||||||
|
MppApi *api = nullptr;
|
||||||
|
MppEncCfg cfg = nullptr;
|
||||||
|
MppPacket packet = nullptr;
|
||||||
|
std::array<MppBuffer, 16> buffers{};
|
||||||
|
std::array<size_t, 16> capacities{};
|
||||||
|
size_t count = 0;
|
||||||
|
size_t minimum = 0;
|
||||||
|
int width = 0, height = 0, stride = 0;
|
||||||
|
MppFrameFormat format = MPP_FMT_YUV420SP;
|
||||||
|
bool jpeg = false;
|
||||||
|
MppCtx decoder = nullptr;
|
||||||
|
MppApi *dec_api = nullptr;
|
||||||
|
MppBufferGroup decoded_group = nullptr;
|
||||||
|
MppBuffer decoded_buffer = nullptr;
|
||||||
|
MppFrame decoded_frame = nullptr;
|
||||||
|
MppPacket input_packet = nullptr;
|
||||||
|
bool decoded_layout_set = false;
|
||||||
|
int decoded_stride = 0, decoded_vstride = 0;
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
if (packet) mpp_packet_deinit(&packet);
|
||||||
|
if (ctx) {
|
||||||
|
// A timeout must not expose still-in-use input to V4L2 QBUF.
|
||||||
|
api->reset(ctx);
|
||||||
|
mpp_destroy(ctx);
|
||||||
|
ctx = nullptr;
|
||||||
|
}
|
||||||
|
// On any decoder failure, end hardware access before releasing the
|
||||||
|
// input packet, exported buffers, or allowing the caller's QBUF.
|
||||||
|
if (decoder) {
|
||||||
|
dec_api->reset(decoder);
|
||||||
|
mpp_destroy(decoder);
|
||||||
|
decoder = nullptr;
|
||||||
|
}
|
||||||
|
if (input_packet) mpp_packet_deinit(&input_packet);
|
||||||
|
if (decoded_frame) mpp_frame_deinit(&decoded_frame);
|
||||||
|
if (decoded_buffer) { mpp_buffer_put(decoded_buffer); decoded_buffer = nullptr; }
|
||||||
|
if (decoded_group) { mpp_buffer_group_put(decoded_group); decoded_group = nullptr; }
|
||||||
|
for (auto &buffer : buffers) {
|
||||||
|
if (buffer) { mpp_buffer_put(buffer); buffer = nullptr; }
|
||||||
|
}
|
||||||
|
if (cfg) { mpp_enc_cfg_deinit(cfg); cfg = nullptr; }
|
||||||
|
}
|
||||||
|
~RkmppDmaEncoder() { close(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool set_cfg(RkmppDmaEncoder *e, const char *key, int value) {
|
||||||
|
int ret = mpp_enc_cfg_set_s32(e->cfg, key, value);
|
||||||
|
if (ret) fail(key, ret);
|
||||||
|
return ret == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int rkmpp_dma_reconfigure(RkmppDmaEncoder *e, int kbps, int gop) {
|
||||||
|
if (!e || !e->ctx || kbps <= 0 || kbps > 1000000 || gop <= 0)
|
||||||
|
return fail("invalid DMA encoder configuration", -1);
|
||||||
|
const int bps = kbps * 1000;
|
||||||
|
if (!set_cfg(e, "rc:bps_target", bps) ||
|
||||||
|
!set_cfg(e, "rc:bps_max", bps + bps / 16) ||
|
||||||
|
!set_cfg(e, "rc:bps_min", bps - bps / 16) ||
|
||||||
|
!set_cfg(e, "rc:gop", gop)) return -1;
|
||||||
|
int ret = e->api->control(e->ctx, MPP_ENC_SET_CFG, e->cfg);
|
||||||
|
return ret ? fail("MPP_ENC_SET_CFG", ret) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" RkmppDmaEncoder *rkmpp_dma_new(
|
||||||
|
int width, int height, int stride, int format, int codec, int fps,
|
||||||
|
int kbps, int gop, const int *fds, const size_t *sizes, size_t count) {
|
||||||
|
if (width <= 0 || height <= 0 || width > 8192 || height > 8192 ||
|
||||||
|
(width & 1) || (height & 1) || format < 0 || format > 4 ||
|
||||||
|
codec < 0 || codec > 1 || fps <= 0 || fps > 240 ||
|
||||||
|
(format != 4 && stride < width * (format == 1 || format == 3 ? 3 : format == 2 ? 2 : 1)) ||
|
||||||
|
(format == 2 && stride % 16 != 0) || !fds || !sizes || !count || count > 16) {
|
||||||
|
fail("invalid DMA frame layout", -1); return nullptr;
|
||||||
|
}
|
||||||
|
auto *e = new (std::nothrow) RkmppDmaEncoder;
|
||||||
|
if (!e) { fail("allocate DMA encoder", -1); return nullptr; }
|
||||||
|
e->width = width; e->height = height; e->stride = stride; e->count = count;
|
||||||
|
e->jpeg = format == 4;
|
||||||
|
if (e->jpeg) e->stride = stride = (width + 15) & ~15;
|
||||||
|
const int vstride = e->jpeg ? (height + 15) & ~15 : height;
|
||||||
|
e->format = format == 3 ? MPP_FMT_RGB888 : format == 2 ? MPP_FMT_YUV422_YUYV : format == 1 ? MPP_FMT_BGR888 : MPP_FMT_YUV420SP;
|
||||||
|
auto abort_init = [e](const char *op, int ret) -> RkmppDmaEncoder * {
|
||||||
|
fail(op, ret); delete e; return nullptr;
|
||||||
|
};
|
||||||
|
int ret = mpp_create(&e->ctx, &e->api);
|
||||||
|
if (ret) return abort_init("mpp_create", ret);
|
||||||
|
RK_S64 timeout = 2000;
|
||||||
|
ret = e->api->control(e->ctx, MPP_SET_OUTPUT_TIMEOUT, &timeout);
|
||||||
|
if (ret) return abort_init("MPP_SET_OUTPUT_TIMEOUT", ret);
|
||||||
|
ret = e->api->control(e->ctx, MPP_SET_INPUT_TIMEOUT, &timeout);
|
||||||
|
if (ret) return abort_init("MPP_SET_INPUT_TIMEOUT", ret);
|
||||||
|
ret = mpp_init(e->ctx, MPP_CTX_ENC, codec ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC);
|
||||||
|
if (ret) return abort_init("mpp_init", ret);
|
||||||
|
ret = mpp_enc_cfg_init(&e->cfg);
|
||||||
|
if (ret) return abort_init("mpp_enc_cfg_init", ret);
|
||||||
|
ret = e->api->control(e->ctx, MPP_ENC_GET_CFG, e->cfg);
|
||||||
|
if (ret) return abort_init("MPP_ENC_GET_CFG", ret);
|
||||||
|
if (!set_cfg(e, "prep:width", width) || !set_cfg(e, "prep:height", height) ||
|
||||||
|
!set_cfg(e, "prep:hor_stride", stride) || !set_cfg(e, "prep:ver_stride", vstride) ||
|
||||||
|
!set_cfg(e, "prep:format", e->format) || !set_cfg(e, "rc:mode", MPP_ENC_RC_MODE_CBR) ||
|
||||||
|
!set_cfg(e, "rc:fps_in_flex", 0) || !set_cfg(e, "rc:fps_in_num", fps) ||
|
||||||
|
!set_cfg(e, "rc:fps_in_denorm", 1) || !set_cfg(e, "rc:fps_out_flex", 0) ||
|
||||||
|
!set_cfg(e, "rc:fps_out_num", fps) || !set_cfg(e, "rc:fps_out_denorm", 1) ||
|
||||||
|
!set_cfg(e, "codec:type", codec ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC)) {
|
||||||
|
delete e; return nullptr;
|
||||||
|
}
|
||||||
|
// Match the browser-friendly baseline profile used by the existing RKMPP
|
||||||
|
// byte encoder, rather than inheriting MPP's High-profile default.
|
||||||
|
const int level = int64_t(width) * height * fps <= int64_t(1920) * 1080 * 60 ? 42 : 52;
|
||||||
|
if (!codec && (!set_cfg(e, "h264:profile", 66) || !set_cfg(e, "h264:level", level) ||
|
||||||
|
!set_cfg(e, "h264:cabac_en", 0) || !set_cfg(e, "h264:trans8x8", 0))) {
|
||||||
|
delete e; return nullptr;
|
||||||
|
}
|
||||||
|
if (rkmpp_dma_reconfigure(e, kbps, gop)) { delete e; return nullptr; }
|
||||||
|
MppEncHeaderMode mode = MPP_ENC_HEADER_MODE_EACH_IDR;
|
||||||
|
ret = e->api->control(e->ctx, MPP_ENC_SET_HEADER_MODE, &mode);
|
||||||
|
if (ret) return abort_init("MPP_ENC_SET_HEADER_MODE", ret);
|
||||||
|
// Reject arithmetic overflow even on 32-bit ARM; stride is supplied by a driver.
|
||||||
|
if (size_t(stride) > std::numeric_limits<size_t>::max() / size_t(height))
|
||||||
|
return abort_init("DMA buffer size overflow", -1);
|
||||||
|
size_t minimum = size_t(stride) * height;
|
||||||
|
if (format == 0) {
|
||||||
|
if (minimum > std::numeric_limits<size_t>::max() / 3)
|
||||||
|
return abort_init("DMA buffer size overflow", -1);
|
||||||
|
minimum = minimum * 3 / 2;
|
||||||
|
}
|
||||||
|
if (e->jpeg) minimum = 68; // SOI/payload plus bounded hardware read headroom.
|
||||||
|
e->minimum = minimum;
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
if (fds[i] < 0 || sizes[i] < minimum) return abort_init("short DMA buffer", -1);
|
||||||
|
MppBufferInfo info{};
|
||||||
|
info.type = MPP_BUFFER_TYPE_EXT_DMA; info.fd = fds[i];
|
||||||
|
info.size = sizes[i]; info.index = static_cast<int>(i);
|
||||||
|
ret = mpp_buffer_import(&e->buffers[i], &info);
|
||||||
|
if (ret) return abort_init("mpp_buffer_import", ret);
|
||||||
|
e->capacities[i] = sizes[i];
|
||||||
|
}
|
||||||
|
if (e->jpeg) {
|
||||||
|
ret = mpp_create(&e->decoder, &e->dec_api);
|
||||||
|
if (ret) return abort_init("mpp_create JPEG decoder", ret);
|
||||||
|
ret = mpp_init(e->decoder, MPP_CTX_DEC, MPP_VIDEO_CodingMJPEG);
|
||||||
|
if (ret) return abort_init("mpp_init JPEG decoder", ret);
|
||||||
|
MppFrameFormat output = MPP_FMT_YUV420SP;
|
||||||
|
ret = e->dec_api->control(e->decoder, MPP_DEC_SET_OUTPUT_FORMAT, &output);
|
||||||
|
if (ret) return abort_init("JPEG NV12 output", ret);
|
||||||
|
ret = mpp_buffer_group_get_internal(&e->decoded_group, MPP_BUFFER_TYPE_DRM);
|
||||||
|
if (ret) return abort_init("JPEG output buffer group", ret);
|
||||||
|
// MPP JPEG requires aligned storage; reserve the conservative size used
|
||||||
|
// by its advanced-task decoder demo. One output reused after encode.
|
||||||
|
ret = mpp_buffer_get(e->decoded_group, &e->decoded_buffer, size_t(stride) * vstride * 4);
|
||||||
|
if (ret) return abort_init("JPEG output buffer", ret);
|
||||||
|
ret = mpp_frame_init(&e->decoded_frame);
|
||||||
|
if (ret) return abort_init("JPEG output frame", ret);
|
||||||
|
mpp_frame_set_buffer(e->decoded_frame, e->decoded_buffer);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int dma_read_sync(MppBuffer buffer, bool start) {
|
||||||
|
dma_buf_sync sync{};
|
||||||
|
sync.flags = DMA_BUF_SYNC_READ | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END);
|
||||||
|
int ret;
|
||||||
|
do { ret = ioctl(mpp_buffer_get_fd(buffer), DMA_BUF_IOCTL_SYNC, &sync); }
|
||||||
|
while (ret < 0 && errno == EINTR);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int decode_jpeg(RkmppDmaEncoder *e, size_t index, size_t bytes_used) {
|
||||||
|
MppBuffer input = e->buffers[index];
|
||||||
|
// The parser reads only header bytes with explicit DMA CPU-read ownership.
|
||||||
|
if (dma_read_sync(input, true)) return fail("JPEG DMA read sync start", errno);
|
||||||
|
const auto *data = static_cast<const uint8_t *>(mpp_buffer_get_ptr(input));
|
||||||
|
const bool valid = rkmpp_dma_jpeg_header(data, bytes_used, e->width, e->height);
|
||||||
|
if (dma_read_sync(input, false)) return fail("JPEG DMA read sync end", errno);
|
||||||
|
if (!valid) return fail("unsupported/mismatched JPEG header", -1);
|
||||||
|
|
||||||
|
int ret = mpp_packet_init_with_buffer(&e->input_packet, input);
|
||||||
|
if (ret) return fail("JPEG input packet", ret);
|
||||||
|
mpp_packet_set_length(e->input_packet, bytes_used);
|
||||||
|
MppTask task = nullptr;
|
||||||
|
ret = e->dec_api->poll(e->decoder, MPP_PORT_INPUT, static_cast<MppPollType>(2000));
|
||||||
|
if (ret) return fail("JPEG input poll", ret);
|
||||||
|
ret = e->dec_api->dequeue(e->decoder, MPP_PORT_INPUT, &task);
|
||||||
|
if (ret || !task) return fail("JPEG input task", ret);
|
||||||
|
ret = mpp_task_meta_set_packet(task, KEY_INPUT_PACKET, e->input_packet);
|
||||||
|
if (ret) return fail("JPEG input metadata", ret);
|
||||||
|
ret = mpp_task_meta_set_frame(task, KEY_OUTPUT_FRAME, e->decoded_frame);
|
||||||
|
if (ret) return fail("JPEG output metadata", ret);
|
||||||
|
ret = e->dec_api->enqueue(e->decoder, MPP_PORT_INPUT, task);
|
||||||
|
if (ret) return fail("JPEG submit", ret);
|
||||||
|
task = nullptr;
|
||||||
|
ret = e->dec_api->poll(e->decoder, MPP_PORT_OUTPUT, static_cast<MppPollType>(2000));
|
||||||
|
if (ret) return fail("JPEG output poll", ret);
|
||||||
|
ret = e->dec_api->dequeue(e->decoder, MPP_PORT_OUTPUT, &task);
|
||||||
|
if (ret || !task) return fail("JPEG output task", ret);
|
||||||
|
MppFrame result = nullptr;
|
||||||
|
ret = mpp_task_meta_get_frame(task, KEY_OUTPUT_FRAME, &result);
|
||||||
|
if (ret || result != e->decoded_frame) return fail("JPEG output frame mismatch", ret);
|
||||||
|
ret = e->dec_api->enqueue(e->decoder, MPP_PORT_OUTPUT, task);
|
||||||
|
if (ret) return fail("JPEG return output task", ret);
|
||||||
|
ret = e->dec_api->poll(e->decoder, MPP_PORT_INPUT, static_cast<MppPollType>(2000));
|
||||||
|
if (ret) return fail("JPEG input completion", ret);
|
||||||
|
mpp_packet_deinit(&e->input_packet);
|
||||||
|
if (mpp_frame_get_errinfo(result) || mpp_frame_get_discard(result) ||
|
||||||
|
mpp_frame_get_info_change(result) ||
|
||||||
|
mpp_frame_get_width(result) != unsigned(e->width) ||
|
||||||
|
mpp_frame_get_height(result) != unsigned(e->height) ||
|
||||||
|
mpp_frame_get_fmt(result) != MPP_FMT_YUV420SP ||
|
||||||
|
mpp_frame_get_buffer(result) != e->decoded_buffer) {
|
||||||
|
std::snprintf(dma_error, sizeof(dma_error),
|
||||||
|
"invalid JPEG decoded frame: err=%u discard=%u info_change=%u size=%ux%u fmt=%x buffer_match=%d",
|
||||||
|
mpp_frame_get_errinfo(result), mpp_frame_get_discard(result), mpp_frame_get_info_change(result),
|
||||||
|
mpp_frame_get_width(result), mpp_frame_get_height(result), unsigned(mpp_frame_get_fmt(result)),
|
||||||
|
int(mpp_frame_get_buffer(result) == e->decoded_buffer));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const int hs = mpp_frame_get_hor_stride(result), vs = mpp_frame_get_ver_stride(result);
|
||||||
|
if (hs < e->width || vs < e->height || hs > 8192 || vs > 8192 || (hs & 15) || (vs & 15) ||
|
||||||
|
size_t(hs) * vs * 3 / 2 > mpp_buffer_get_size(e->decoded_buffer))
|
||||||
|
return fail("invalid JPEG decoded stride", -1);
|
||||||
|
if (!e->decoded_layout_set) {
|
||||||
|
if (!set_cfg(e, "prep:hor_stride", hs) || !set_cfg(e, "prep:ver_stride", vs)) return -1;
|
||||||
|
ret = e->api->control(e->ctx, MPP_ENC_SET_CFG, e->cfg);
|
||||||
|
if (ret) return fail("JPEG encoder layout", ret);
|
||||||
|
e->decoded_stride = hs; e->decoded_vstride = vs; e->decoded_layout_set = true;
|
||||||
|
} else if (hs != e->decoded_stride || vs != e->decoded_vstride) {
|
||||||
|
return fail("JPEG decoded layout changed", -1);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int rkmpp_dma_encode(RkmppDmaEncoder *e, size_t index, size_t bytes_used, int fresh_fd, int64_t pts_us,
|
||||||
|
int force_idr, const uint8_t **data, size_t *size) {
|
||||||
|
if (!e || !e->ctx || index >= e->count || !data || !size)
|
||||||
|
return fail("invalid DMA encode call", -1);
|
||||||
|
*data = nullptr; *size = 0;
|
||||||
|
if (e->packet) mpp_packet_deinit(&e->packet);
|
||||||
|
auto abort_encode = [e](const char *op, int ret) {
|
||||||
|
fail(op, ret); e->close(); return -1;
|
||||||
|
};
|
||||||
|
if (bytes_used > e->capacities[index] ||
|
||||||
|
(e->jpeg ? (bytes_used < 4 || e->capacities[index] - bytes_used < 64) : bytes_used != e->minimum))
|
||||||
|
return abort_encode("invalid DMA payload length", -1);
|
||||||
|
if (fresh_fd >= 0) {
|
||||||
|
MppBuffer replacement = nullptr;
|
||||||
|
MppBufferInfo info{};
|
||||||
|
info.type = MPP_BUFFER_TYPE_EXT_DMA; info.fd = fresh_fd;
|
||||||
|
info.size = e->capacities[index]; info.index = static_cast<int>(index);
|
||||||
|
const int ret = mpp_buffer_import(&replacement, &info);
|
||||||
|
if (ret) return abort_encode("refresh USB DMA import", ret);
|
||||||
|
if (e->buffers[index]) mpp_buffer_put(e->buffers[index]);
|
||||||
|
e->buffers[index] = replacement;
|
||||||
|
}
|
||||||
|
if (e->jpeg && decode_jpeg(e, index, bytes_used)) {
|
||||||
|
// Preserve the detailed decoder failure while ending BOTH engines.
|
||||||
|
e->close(); return -1;
|
||||||
|
}
|
||||||
|
if (force_idr) {
|
||||||
|
int ret = e->api->control(e->ctx, MPP_ENC_SET_IDR_FRAME, nullptr);
|
||||||
|
if (ret) return abort_encode("MPP_ENC_SET_IDR_FRAME", ret);
|
||||||
|
}
|
||||||
|
MppFrame frame = e->jpeg ? e->decoded_frame : nullptr;
|
||||||
|
int ret = 0;
|
||||||
|
if (!e->jpeg) {
|
||||||
|
ret = mpp_frame_init(&frame);
|
||||||
|
if (ret) return abort_encode("mpp_frame_init", ret);
|
||||||
|
mpp_frame_set_width(frame, e->width); mpp_frame_set_height(frame, e->height);
|
||||||
|
mpp_frame_set_hor_stride(frame, e->stride); mpp_frame_set_ver_stride(frame, e->height);
|
||||||
|
mpp_frame_set_fmt(frame, e->format);
|
||||||
|
mpp_frame_set_buffer(frame, e->buffers[index]);
|
||||||
|
}
|
||||||
|
mpp_frame_set_pts(frame, pts_us);
|
||||||
|
ret = e->api->encode_put_frame(e->ctx, frame);
|
||||||
|
if (!e->jpeg) mpp_frame_deinit(&frame);
|
||||||
|
if (ret) return abort_encode("encode_put_frame", ret);
|
||||||
|
ret = e->api->encode_get_packet(e->ctx, &e->packet);
|
||||||
|
if (ret || !e->packet) return abort_encode("encode_get_packet", ret);
|
||||||
|
if (mpp_packet_is_partition(e->packet) || !mpp_packet_get_length(e->packet))
|
||||||
|
return abort_encode("incomplete DMA encoder output", -1);
|
||||||
|
// One synchronous input, no temporal scalability/reordering/split output.
|
||||||
|
// A completed packet is the input-consumption barrier for this mode.
|
||||||
|
*data = static_cast<const uint8_t *>(mpp_packet_get_pos(e->packet));
|
||||||
|
*size = mpp_packet_get_length(e->packet);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
extern "C" void rkmpp_dma_free(RkmppDmaEncoder *e) { delete e; }
|
||||||
|
#else
|
||||||
|
extern "C" RkmppDmaEncoder *rkmpp_dma_new(int,int,int,int,int,int,int,int,const int*,const size_t*,size_t) {
|
||||||
|
fail("RKMPP DMA support not built", -1); return nullptr;
|
||||||
|
}
|
||||||
|
extern "C" int rkmpp_dma_encode(RkmppDmaEncoder*,size_t,size_t,int,int64_t,int,const uint8_t**,size_t*) { return -1; }
|
||||||
|
extern "C" int rkmpp_dma_reconfigure(RkmppDmaEncoder*,int,int) { return -1; }
|
||||||
|
extern "C" void rkmpp_dma_free(RkmppDmaEncoder*) {}
|
||||||
|
#endif
|
||||||
|
extern "C" const char *rkmpp_dma_error(void) { return dma_error; }
|
||||||
31
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dmabuf_ffi.h
Normal file
31
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dmabuf_ffi.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct RkmppDmaEncoder RkmppDmaEncoder;
|
||||||
|
// format: 0=NV12, 1=BGR24, 2=YUYV, 3=RGB24 (byte stride), 4=MJPEG (stride ignored).
|
||||||
|
// codec: 0=H264, 1=HEVC. MJPEG is decoded to hardware NV12, then encoded.
|
||||||
|
RkmppDmaEncoder *rkmpp_dma_new(int width, int height, int stride, int format,
|
||||||
|
int codec, int fps, int kbps, int gop,
|
||||||
|
const int *fds, const size_t *sizes, size_t count);
|
||||||
|
// Synchronous input-completion boundary. On failure the encoder is destroyed
|
||||||
|
// internally BEFORE returning, so it cannot keep reading the capture buffer.
|
||||||
|
// Output is borrowed until the next call or destruction; copy before reusing it.
|
||||||
|
// bytes_used must be the actual captured payload. MJPEG needs 64 bytes of
|
||||||
|
// readable allocation headroom; never pass sizeimage as the compressed length.
|
||||||
|
// fresh_fd=-1 reuses the original import (native HDMI). UVC supplies a new
|
||||||
|
// export of this dequeued slot. Keep it open until replacement/free; the old
|
||||||
|
// export can be closed AFTER this call, including on failure.
|
||||||
|
int rkmpp_dma_encode(RkmppDmaEncoder *encoder, size_t index, size_t bytes_used, int fresh_fd, int64_t pts_us,
|
||||||
|
int force_idr, const uint8_t **data, size_t *size);
|
||||||
|
int rkmpp_dma_reconfigure(RkmppDmaEncoder *encoder, int kbps, int gop);
|
||||||
|
void rkmpp_dma_free(RkmppDmaEncoder *encoder);
|
||||||
|
const char *rkmpp_dma_error(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Minimal FFmpeg RAM MJPEG decoder (RKMPP only) -> NV12 in CPU memory.
|
// FFmpeg RAM decoder with optional RKMPP hardware-frame support.
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
@@ -54,9 +54,11 @@ public:
|
|||||||
thread_count_ = thread_count > 0 ? thread_count : 1;
|
thread_count_ = thread_count > 0 ? thread_count : 1;
|
||||||
callback_ = callback;
|
callback_ = callback;
|
||||||
|
|
||||||
|
#ifdef ONE_KVM_FFMPEG_RKMPP
|
||||||
if (name_.find("rkmpp") != std::string::npos) {
|
if (name_.find("rkmpp") != std::string::npos) {
|
||||||
hw_device_type_ = AV_HWDEVICE_TYPE_RKMPP;
|
hw_device_type_ = AV_HWDEVICE_TYPE_RKMPP;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
~FFmpegRamDecoder() {}
|
~FFmpegRamDecoder() {}
|
||||||
@@ -137,13 +139,6 @@ public:
|
|||||||
av_buffer_unref(&frames_ref);
|
av_buffer_unref(&frames_ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name_.find("mediacodec") != std::string::npos && c_->priv_data) {
|
|
||||||
if ((ret = av_opt_set_int(c_->priv_data, "ndk_codec", 1, 0)) < 0) {
|
|
||||||
LOG_WARN(std::string("mediacodec decoder ndk_codec option failed, ret = ") +
|
|
||||||
av_err2str(ret));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((ret = avcodec_open2(c_, codec, NULL)) < 0) {
|
if ((ret = avcodec_open2(c_, codec, NULL)) < 0) {
|
||||||
set_last_error(std::string("avcodec_open2 failed, ret = ") + av_err2str(ret));
|
set_last_error(std::string("avcodec_open2 failed, ret = ") + av_err2str(ret));
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ extern "C" {
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
@@ -21,6 +22,12 @@ extern "C" {
|
|||||||
#include "win.h"
|
#include "win.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static thread_local std::string g_encoder_last_error;
|
||||||
|
|
||||||
|
static void set_encoder_last_error(const std::string &message) {
|
||||||
|
g_encoder_last_error = message;
|
||||||
|
}
|
||||||
|
|
||||||
static int calculate_offset_length(int pix_fmt, int height, const int *linesize,
|
static int calculate_offset_length(int pix_fmt, int height, const int *linesize,
|
||||||
int *offset, int *length) {
|
int *offset, int *length) {
|
||||||
switch (pix_fmt) {
|
switch (pix_fmt) {
|
||||||
@@ -122,7 +129,6 @@ public:
|
|||||||
AVFrame *frame_ = NULL;
|
AVFrame *frame_ = NULL;
|
||||||
AVPacket *pkt_ = NULL;
|
AVPacket *pkt_ = NULL;
|
||||||
std::string name_;
|
std::string name_;
|
||||||
std::string mc_name_; // for mediacodec
|
|
||||||
|
|
||||||
int width_ = 0;
|
int width_ = 0;
|
||||||
int height_ = 0;
|
int height_ = 0;
|
||||||
@@ -145,14 +151,12 @@ public:
|
|||||||
AVPixelFormat hw_pixfmt_ = AV_PIX_FMT_NONE;
|
AVPixelFormat hw_pixfmt_ = AV_PIX_FMT_NONE;
|
||||||
AVBufferRef *hw_device_ctx_ = NULL;
|
AVBufferRef *hw_device_ctx_ = NULL;
|
||||||
AVFrame *hw_frame_ = NULL;
|
AVFrame *hw_frame_ = NULL;
|
||||||
AVFrame *borrowed_frame_ = NULL;
|
|
||||||
|
|
||||||
FFmpegRamEncoder(const char *name, const char *mc_name, int width, int height,
|
FFmpegRamEncoder(const char *name, int width, int height,
|
||||||
int pixfmt, int align, int fps, int gop, int rc, int quality,
|
int pixfmt, int align, int fps, int gop, int rc, int quality,
|
||||||
int kbs, int q, int thread_count, int gpu,
|
int kbs, int q, int thread_count, int gpu,
|
||||||
RamEncodeCallback callback) {
|
RamEncodeCallback callback) {
|
||||||
name_ = name;
|
name_ = name;
|
||||||
mc_name_ = mc_name ? mc_name : "";
|
|
||||||
width_ = width;
|
width_ = width;
|
||||||
height_ = height;
|
height_ = height;
|
||||||
pixfmt_ = (AVPixelFormat)pixfmt;
|
pixfmt_ = (AVPixelFormat)pixfmt;
|
||||||
@@ -184,12 +188,13 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool init(int *linesize, int *offset, int *length) {
|
bool init(int *linesize, int *offset, int *length) {
|
||||||
|
g_encoder_last_error.clear();
|
||||||
const AVCodec *codec = NULL;
|
const AVCodec *codec = NULL;
|
||||||
|
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
if (!(codec = avcodec_find_encoder_by_name(name_.c_str()))) {
|
if (!(codec = avcodec_find_encoder_by_name(name_.c_str()))) {
|
||||||
LOG_ERROR(std::string("Codec ") + name_ + " not found");
|
set_encoder_last_error(std::string("Codec ") + name_ + " not found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,12 +257,6 @@ public:
|
|||||||
LOG_ERROR(std::string("Could not allocate video packet"));
|
LOG_ERROR(std::string("Could not allocate video packet"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
borrowed_frame_ = av_frame_alloc();
|
|
||||||
if (!borrowed_frame_) {
|
|
||||||
LOG_ERROR(std::string("Could not allocate borrowed video frame"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* resolution must be a multiple of two */
|
/* resolution must be a multiple of two */
|
||||||
c_->width = width_;
|
c_->width = width_;
|
||||||
c_->height = height_;
|
c_->height = height_;
|
||||||
@@ -273,23 +272,16 @@ public:
|
|||||||
LOG_ERROR(std::string("set_quality failed, name: ") + name_);
|
LOG_ERROR(std::string("set_quality failed, name: ") + name_);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
util_encode::set_rate_control(c_, name_, rc_, q_);
|
if (!util_encode::set_rate_control(c_, name_, rc_, q_)) {
|
||||||
|
LOG_ERROR(std::string("set_rate_control failed, name: ") + name_);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
util_encode::set_gpu(c_->priv_data, name_, gpu_);
|
util_encode::set_gpu(c_->priv_data, name_, gpu_);
|
||||||
util_encode::force_hw(c_->priv_data, name_);
|
util_encode::force_hw(c_->priv_data, name_);
|
||||||
util_encode::set_others(c_->priv_data, name_);
|
util_encode::set_others(c_->priv_data, name_);
|
||||||
if (name_.find("mediacodec") != std::string::npos) {
|
|
||||||
if (mc_name_.length() > 0) {
|
|
||||||
LOG_INFO(std::string("mediacodec codec_name: ") + mc_name_);
|
|
||||||
if ((ret = av_opt_set(c_->priv_data, "codec_name", mc_name_.c_str(),
|
|
||||||
0)) < 0) {
|
|
||||||
LOG_ERROR(std::string("mediacodec codec_name failed, ret = ") + av_err2str(ret));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((ret = avcodec_open2(c_, codec, NULL)) < 0) {
|
if ((ret = avcodec_open2(c_, codec, NULL)) < 0) {
|
||||||
LOG_ERROR(std::string("avcodec_open2 failed, ret = ") + av_err2str(ret) +
|
set_encoder_last_error(std::string("avcodec_open2 failed, ret = ") +
|
||||||
", name: " + name_);
|
av_err2str(ret) + ", name: " + name_);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,14 +299,6 @@ public:
|
|||||||
int encode(const uint8_t *data, int length, const void *obj, uint64_t ms) {
|
int encode(const uint8_t *data, int length, const void *obj, uint64_t ms) {
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
if (can_borrow_input(length)) {
|
|
||||||
AVFrame *borrowed = wrap_borrowed_frame(data, length);
|
|
||||||
if (!borrowed) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return do_encode(borrowed, obj, ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((ret = av_frame_make_writable(frame_)) != 0) {
|
if ((ret = av_frame_make_writable(frame_)) != 0) {
|
||||||
LOG_ERROR(std::string("av_frame_make_writable failed, ret = ") + av_err2str(ret));
|
LOG_ERROR(std::string("av_frame_make_writable failed, ret = ") + av_err2str(ret));
|
||||||
return ret;
|
return ret;
|
||||||
@@ -350,8 +334,6 @@ public:
|
|||||||
av_frame_free(&frame_);
|
av_frame_free(&frame_);
|
||||||
if (hw_frame_)
|
if (hw_frame_)
|
||||||
av_frame_free(&hw_frame_);
|
av_frame_free(&hw_frame_);
|
||||||
if (borrowed_frame_)
|
|
||||||
av_frame_free(&borrowed_frame_);
|
|
||||||
if (hw_device_ctx_)
|
if (hw_device_ctx_)
|
||||||
av_buffer_unref(&hw_device_ctx_);
|
av_buffer_unref(&hw_device_ctx_);
|
||||||
if (c_)
|
if (c_)
|
||||||
@@ -397,12 +379,12 @@ private:
|
|||||||
frame->pts = ms;
|
frame->pts = ms;
|
||||||
|
|
||||||
// Force keyframe if requested
|
// Force keyframe if requested
|
||||||
if (force_keyframe_) {
|
if (force_keyframe_ && util_encode::supports_forced_keyframe(name_)) {
|
||||||
frame->pict_type = AV_PICTURE_TYPE_I;
|
frame->pict_type = AV_PICTURE_TYPE_I;
|
||||||
force_keyframe_ = false;
|
|
||||||
} else {
|
} else {
|
||||||
frame->pict_type = AV_PICTURE_TYPE_NONE;
|
frame->pict_type = AV_PICTURE_TYPE_NONE;
|
||||||
}
|
}
|
||||||
|
force_keyframe_ = false;
|
||||||
|
|
||||||
ret = avcodec_send_frame(c_, frame);
|
ret = avcodec_send_frame(c_, frame);
|
||||||
if (ret == AVERROR(EAGAIN)) {
|
if (ret == AVERROR(EAGAIN)) {
|
||||||
@@ -610,65 +592,6 @@ private:
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool can_borrow_input(int data_length) const {
|
|
||||||
if (hw_device_type_ != AV_HWDEVICE_TYPE_NONE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (name_.find("mediacodec") == std::string::npos) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
switch (pixfmt_) {
|
|
||||||
case AV_PIX_FMT_NV12:
|
|
||||||
case AV_PIX_FMT_NV21:
|
|
||||||
return data_length >= width_ * height_ * 3 / 2;
|
|
||||||
case AV_PIX_FMT_YUV420P:
|
|
||||||
return data_length >= width_ * height_ * 3 / 2;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AVFrame *wrap_borrowed_frame(const uint8_t *data, int data_length) {
|
|
||||||
if (!borrowed_frame_) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
av_frame_unref(borrowed_frame_);
|
|
||||||
borrowed_frame_->format = pixfmt_;
|
|
||||||
borrowed_frame_->width = width_;
|
|
||||||
borrowed_frame_->height = height_;
|
|
||||||
|
|
||||||
const int y_size = width_ * height_;
|
|
||||||
const int uv_size = y_size / 4;
|
|
||||||
switch (pixfmt_) {
|
|
||||||
case AV_PIX_FMT_NV12:
|
|
||||||
case AV_PIX_FMT_NV21:
|
|
||||||
if (data_length < y_size + y_size / 2) {
|
|
||||||
LOG_ERROR("wrap_borrowed_frame: NV12/NV21 data length error");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
borrowed_frame_->data[0] = const_cast<uint8_t *>(data);
|
|
||||||
borrowed_frame_->data[1] = const_cast<uint8_t *>(data + y_size);
|
|
||||||
borrowed_frame_->linesize[0] = width_;
|
|
||||||
borrowed_frame_->linesize[1] = width_;
|
|
||||||
break;
|
|
||||||
case AV_PIX_FMT_YUV420P:
|
|
||||||
if (data_length < y_size + uv_size * 2) {
|
|
||||||
LOG_ERROR("wrap_borrowed_frame: YUV420P data length error");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
borrowed_frame_->data[0] = const_cast<uint8_t *>(data);
|
|
||||||
borrowed_frame_->data[1] = const_cast<uint8_t *>(data + y_size);
|
|
||||||
borrowed_frame_->data[2] = const_cast<uint8_t *>(data + y_size + uv_size);
|
|
||||||
borrowed_frame_->linesize[0] = width_;
|
|
||||||
borrowed_frame_->linesize[1] = width_ / 2;
|
|
||||||
borrowed_frame_->linesize[2] = width_ / 2;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return borrowed_frame_;
|
|
||||||
}
|
|
||||||
|
|
||||||
int bytes_per_pixel(int pix_fmt) {
|
int bytes_per_pixel(int pix_fmt) {
|
||||||
switch (pix_fmt) {
|
switch (pix_fmt) {
|
||||||
case AV_PIX_FMT_YUYV422:
|
case AV_PIX_FMT_YUYV422:
|
||||||
@@ -687,18 +610,45 @@ private:
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
extern "C" FFmpegRamEncoder *
|
extern "C" FFmpegRamEncoder *
|
||||||
ffmpeg_ram_new_encoder(const char *name, const char *mc_name, int width,
|
ffmpeg_ram_new_encoder(const char *name, int width,
|
||||||
int height, int pixfmt, int align, int fps, int gop,
|
int height, int pixfmt, int align, int fps, int gop,
|
||||||
int rc, int quality, int kbs, int q, int thread_count,
|
int rc, int quality, int kbs, int q, int thread_count,
|
||||||
int gpu, int *linesize, int *offset, int *length,
|
int gpu, int *linesize, int *offset, int *length,
|
||||||
RamEncodeCallback callback) {
|
RamEncodeCallback callback) {
|
||||||
FFmpegRamEncoder *encoder = NULL;
|
FFmpegRamEncoder *encoder = NULL;
|
||||||
try {
|
try {
|
||||||
encoder = new FFmpegRamEncoder(name, mc_name, width, height, pixfmt, align,
|
auto try_create = [&](int attempt_rc, int attempt_kbs) {
|
||||||
fps, gop, rc, quality, kbs, q, thread_count,
|
FFmpegRamEncoder *candidate = new FFmpegRamEncoder(
|
||||||
gpu, callback);
|
name, width, height, pixfmt, align, fps, gop, attempt_rc, quality,
|
||||||
|
attempt_kbs, q, thread_count, gpu, callback);
|
||||||
|
if (candidate && candidate->init(linesize, offset, length)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate) {
|
||||||
|
candidate->free_encoder();
|
||||||
|
delete candidate;
|
||||||
|
}
|
||||||
|
return static_cast<FFmpegRamEncoder *>(NULL);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve the existing VAAPI path first. With a target bitrate and no
|
||||||
|
// explicit QP, FFmpeg negotiates a supported bitrate-based mode such as
|
||||||
|
// AVBR, VBR or CBR with the driver.
|
||||||
|
encoder = try_create(rc, kbs);
|
||||||
|
if (encoder) {
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry only VAAPI with a fresh context in explicit-QP mode. This avoids
|
||||||
|
// changing rate control on hardware which already supports CBR/VBR while
|
||||||
|
// allowing CQP-only drivers to pass probing and normal encoder creation.
|
||||||
|
if (name && std::string(name).find("vaapi") != std::string::npos &&
|
||||||
|
rc != RC_CQ) {
|
||||||
|
LOG_DEBUG(std::string("VAAPI bitrate-based rate control failed for ") +
|
||||||
|
name + ", retrying with CQP");
|
||||||
|
encoder = try_create(RC_CQ, 0);
|
||||||
if (encoder) {
|
if (encoder) {
|
||||||
if (encoder->init(linesize, offset, length)) {
|
|
||||||
return encoder;
|
return encoder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -772,3 +722,7 @@ extern "C" void ffmpeg_ram_request_keyframe(FFmpegRamEncoder *encoder) {
|
|||||||
LOG_ERROR(std::string("ffmpeg_ram_request_keyframe failed, ") + std::string(e.what()));
|
LOG_ERROR(std::string("ffmpeg_ram_request_keyframe failed, ") + std::string(e.what()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" const char *ffmpeg_ram_encoder_last_error(void) {
|
||||||
|
return g_encoder_last_error.c_str();
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ typedef void (*RamEncodePacketCallback)(void *packet, const uint8_t *data,
|
|||||||
typedef void (*RamDecodeCallback)(const uint8_t *data, int len, int width,
|
typedef void (*RamDecodeCallback)(const uint8_t *data, int len, int width,
|
||||||
int height, int pixfmt, const void *obj);
|
int height, int pixfmt, const void *obj);
|
||||||
|
|
||||||
void *ffmpeg_ram_new_encoder(const char *name, const char *mc_name, int width,
|
void *ffmpeg_ram_new_encoder(const char *name, int width,
|
||||||
int height, int pixfmt, int align, int fps,
|
int height, int pixfmt, int align, int fps,
|
||||||
int gop, int rc, int quality, int kbs, int q,
|
int gop, int rc, int quality, int kbs, int q,
|
||||||
int thread_count, int gpu, int *linesize,
|
int thread_count, int gpu, int *linesize,
|
||||||
@@ -31,6 +31,7 @@ int ffmpeg_ram_get_linesize_offset_length(int pix_fmt, int width, int height,
|
|||||||
int *length);
|
int *length);
|
||||||
int ffmpeg_ram_set_bitrate(void *encoder, int kbs);
|
int ffmpeg_ram_set_bitrate(void *encoder, int kbs);
|
||||||
void ffmpeg_ram_request_keyframe(void *encoder);
|
void ffmpeg_ram_request_keyframe(void *encoder);
|
||||||
|
const char *ffmpeg_ram_encoder_last_error(void);
|
||||||
|
|
||||||
void *ffmpeg_ram_new_decoder(const char *name, int width, int height,
|
void *ffmpeg_ram_new_decoder(const char *name, int width, int height,
|
||||||
int sw_pixfmt, int thread_count,
|
int sw_pixfmt, int thread_count,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub enum Driver {
|
|||||||
FFMPEG,
|
FFMPEG,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(windows, target_os = "linux", target_os = "android"))]
|
#[cfg(any(windows, target_os = "linux"))]
|
||||||
pub(crate) fn supported_gpu(_encode: bool) -> (bool, bool, bool) {
|
pub(crate) fn supported_gpu(_encode: bool) -> (bool, bool, bool) {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use std::ffi::c_int;
|
use std::ffi::c_int;
|
||||||
@@ -39,8 +39,6 @@ pub(crate) fn supported_gpu(_encode: bool) -> (bool, bool, bool) {
|
|||||||
linux_support_amd() == 0,
|
linux_support_amd() == 0,
|
||||||
linux_support_intel() == 0,
|
linux_support_intel() == 0,
|
||||||
);
|
);
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
return (false, false, false);
|
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
(false, false, false)
|
(false, false, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ pub extern "C" fn hwcodec_av_log_callback(level: i32, message: *const std::os::r
|
|||||||
if let Ok(str_slice) = c_str.to_str() {
|
if let Ok(str_slice) = c_str.to_str() {
|
||||||
let string = String::from(str_slice);
|
let string = String::from(str_slice);
|
||||||
if level == AV_LOG_ERROR as i32 {
|
if level == AV_LOG_ERROR as i32 {
|
||||||
log::error!("{}", string);
|
|
||||||
if string.contains(could_not_find_ref_with_poc) {
|
if string.contains(could_not_find_ref_with_poc) {
|
||||||
hwcodec_set_flag_could_not_find_ref_with_poc();
|
hwcodec_set_flag_could_not_find_ref_with_poc();
|
||||||
}
|
}
|
||||||
|
log::debug!("{}", string);
|
||||||
} else if level == AV_LOG_PANIC as i32 || level == AV_LOG_FATAL as i32 {
|
} else if level == AV_LOG_PANIC as i32 || level == AV_LOG_FATAL as i32 {
|
||||||
log::error!("{}", string);
|
log::error!("{}", string);
|
||||||
} else if level == AV_LOG_WARNING as i32 {
|
} else if level == AV_LOG_WARNING as i32 {
|
||||||
log::warn!("{}", string);
|
log::debug!("{}", string);
|
||||||
} else if level == AV_LOG_INFO as i32 {
|
} else if level == AV_LOG_INFO as i32 {
|
||||||
log::info!("{}", string);
|
log::info!("{}", string);
|
||||||
} else if level == AV_LOG_VERBOSE as i32 || level == AV_LOG_DEBUG as i32 {
|
} else if level == AV_LOG_VERBOSE as i32 || level == AV_LOG_DEBUG as i32 {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ impl Drop for Decoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_error_message() -> String {
|
pub fn last_error_message() -> String {
|
||||||
unsafe {
|
unsafe {
|
||||||
let ptr = ffmpeg_ram_last_error();
|
let ptr = ffmpeg_ram_last_error();
|
||||||
if ptr.is_null() {
|
if ptr.is_null() {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
use crate::ffmpeg_ram::{ffmpeg_ram_encode_packet, ffmpeg_ram_free_packet};
|
||||||
use crate::{
|
use crate::{
|
||||||
common::DataFormat::{self, *},
|
common::DataFormat::{self, *},
|
||||||
ffmpeg::{init_av_log, AVPixelFormat},
|
ffmpeg::{init_av_log, AVPixelFormat},
|
||||||
ffmpeg_ram::{
|
ffmpeg_ram::{
|
||||||
ffmpeg_linesize_offset_length, ffmpeg_ram_encode, ffmpeg_ram_encode_packet,
|
ffmpeg_linesize_offset_length, ffmpeg_ram_encode, ffmpeg_ram_encoder_last_error,
|
||||||
ffmpeg_ram_free_encoder, ffmpeg_ram_free_packet, ffmpeg_ram_new_encoder,
|
ffmpeg_ram_free_encoder, ffmpeg_ram_new_encoder, ffmpeg_ram_request_keyframe,
|
||||||
ffmpeg_ram_request_keyframe, ffmpeg_ram_set_bitrate, CodecInfo, AV_NUM_DATA_POINTERS,
|
ffmpeg_ram_set_bitrate, CodecInfo, AV_NUM_DATA_POINTERS,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "bytes")]
|
#[cfg(feature = "bytes")]
|
||||||
@@ -17,7 +19,7 @@ use std::{
|
|||||||
slice,
|
slice,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(windows, target_os = "linux", target_os = "android"))]
|
#[cfg(any(windows, target_os = "linux"))]
|
||||||
use crate::common::Driver;
|
use crate::common::Driver;
|
||||||
|
|
||||||
/// Timeout for encoder test in milliseconds
|
/// Timeout for encoder test in milliseconds
|
||||||
@@ -28,7 +30,6 @@ const PRIORITY_AMF: i32 = 2;
|
|||||||
const PRIORITY_RKMPP: i32 = 3;
|
const PRIORITY_RKMPP: i32 = 3;
|
||||||
const PRIORITY_VAAPI: i32 = 4;
|
const PRIORITY_VAAPI: i32 = 4;
|
||||||
const PRIORITY_V4L2M2M: i32 = 5;
|
const PRIORITY_V4L2M2M: i32 = 5;
|
||||||
const PRIORITY_MEDIACODEC: i32 = 2;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct CandidateCodecSpec {
|
struct CandidateCodecSpec {
|
||||||
@@ -95,32 +96,12 @@ fn linux_support_v4l2m2m() -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(windows, target_os = "linux", target_os = "android"))]
|
#[cfg(any(windows, target_os = "linux"))]
|
||||||
fn enumerate_candidate_codecs(ctx: &EncodeContext) -> Vec<CodecInfo> {
|
fn enumerate_candidate_codecs(ctx: &EncodeContext) -> Vec<CodecInfo> {
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
let mut codecs = Vec::new();
|
let mut codecs = Vec::new();
|
||||||
|
|
||||||
if cfg!(target_os = "android") {
|
|
||||||
push_candidate(
|
|
||||||
&mut codecs,
|
|
||||||
CandidateCodecSpec {
|
|
||||||
name: "h264_mediacodec",
|
|
||||||
format: H264,
|
|
||||||
priority: PRIORITY_MEDIACODEC,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
push_candidate(
|
|
||||||
&mut codecs,
|
|
||||||
CandidateCodecSpec {
|
|
||||||
name: "hevc_mediacodec",
|
|
||||||
format: H265,
|
|
||||||
priority: PRIORITY_MEDIACODEC,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return codecs;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contains = |_vendor: Driver, _format: DataFormat| {
|
let contains = |_vendor: Driver, _format: DataFormat| {
|
||||||
// Without VRAM feature, we can't check SDK availability.
|
// Without VRAM feature, we can't check SDK availability.
|
||||||
// Keep the prefilter coarse and let FFmpeg validation do the real check.
|
// Keep the prefilter coarse and let FFmpeg validation do the real check.
|
||||||
@@ -281,13 +262,7 @@ struct ProbePolicy {
|
|||||||
|
|
||||||
impl ProbePolicy {
|
impl ProbePolicy {
|
||||||
fn for_codec(codec_name: &str) -> Self {
|
fn for_codec(codec_name: &str) -> Self {
|
||||||
if codec_name.contains("mediacodec") {
|
if codec_name.contains("amf") {
|
||||||
Self {
|
|
||||||
max_attempts: 30,
|
|
||||||
request_keyframe: true,
|
|
||||||
accept_any_output: true,
|
|
||||||
}
|
|
||||||
} else if codec_name.contains("amf") {
|
|
||||||
Self {
|
Self {
|
||||||
max_attempts: 5,
|
max_attempts: 5,
|
||||||
request_keyframe: true,
|
request_keyframe: true,
|
||||||
@@ -340,7 +315,8 @@ fn log_failed_probe_attempt(
|
|||||||
if frames.is_empty() {
|
if frames.is_empty() {
|
||||||
trace!(
|
trace!(
|
||||||
"Encoder {} test produced no output on attempt {}",
|
"Encoder {} test produced no output on attempt {}",
|
||||||
codec_name, attempt
|
codec_name,
|
||||||
|
attempt
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
@@ -367,13 +343,12 @@ fn log_failed_probe_attempt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> bool {
|
fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> bool {
|
||||||
use log::{debug, warn};
|
use log::debug;
|
||||||
|
|
||||||
debug!("Testing encoder: {}", codec.name);
|
debug!("Testing encoder: {}", codec.name);
|
||||||
|
|
||||||
let test_ctx = EncodeContext {
|
let test_ctx = EncodeContext {
|
||||||
name: codec.name.clone(),
|
name: codec.name.clone(),
|
||||||
mc_name: codec.mc_name.clone(),
|
|
||||||
..ctx.clone()
|
..ctx.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -420,7 +395,7 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
last_err = Some(err);
|
last_err = Some(err);
|
||||||
warn!(
|
debug!(
|
||||||
"Encoder {} test attempt {} returned error: {}",
|
"Encoder {} test attempt {} returned error: {}",
|
||||||
codec.name, attempt_no, err
|
codec.name, attempt_no, err
|
||||||
);
|
);
|
||||||
@@ -437,20 +412,13 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo
|
|||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => false,
|
||||||
warn!("Failed to create encoder {}", codec.name);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_software_fallback(codecs: &mut Vec<CodecInfo>) {
|
fn add_software_fallback(codecs: &mut Vec<CodecInfo>) {
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
if cfg!(target_os = "android") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for fallback in CodecInfo::soft().into_vec() {
|
for fallback in CodecInfo::soft().into_vec() {
|
||||||
if !codecs.iter().any(|codec| codec.format == fallback.format) {
|
if !codecs.iter().any(|codec| codec.format == fallback.format) {
|
||||||
debug!(
|
debug!(
|
||||||
@@ -465,7 +433,6 @@ fn add_software_fallback(codecs: &mut Vec<CodecInfo>) {
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct EncodeContext {
|
pub struct EncodeContext {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub mc_name: Option<String>,
|
|
||||||
pub width: i32,
|
pub width: i32,
|
||||||
pub height: i32,
|
pub height: i32,
|
||||||
pub pixfmt: i32,
|
pub pixfmt: i32,
|
||||||
@@ -550,10 +517,8 @@ impl Encoder {
|
|||||||
.unwrap_or("-1".to_owned())
|
.unwrap_or("-1".to_owned())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(-1);
|
.unwrap_or(-1);
|
||||||
let mc_name = ctx.mc_name.clone().unwrap_or_default();
|
|
||||||
let codec = ffmpeg_ram_new_encoder(
|
let codec = ffmpeg_ram_new_encoder(
|
||||||
CString::new(ctx.name.as_str()).map_err(|_| ())?.as_ptr(),
|
CString::new(ctx.name.as_str()).map_err(|_| ())?.as_ptr(),
|
||||||
CString::new(mc_name.as_str()).map_err(|_| ())?.as_ptr(),
|
|
||||||
ctx.width,
|
ctx.width,
|
||||||
ctx.height,
|
ctx.height,
|
||||||
ctx.pixfmt,
|
ctx.pixfmt,
|
||||||
@@ -573,6 +538,10 @@ impl Encoder {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if codec.is_null() {
|
if codec.is_null() {
|
||||||
|
let message = encoder_last_error_message();
|
||||||
|
if !message.is_empty() {
|
||||||
|
log::debug!("ffmpeg_ram_new_encoder failed: {}", message);
|
||||||
|
}
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,6 +592,16 @@ impl Encoder {
|
|||||||
Some(Encoder::packet_callback),
|
Some(Encoder::packet_callback),
|
||||||
);
|
);
|
||||||
if result == -11 || result == 0 {
|
if result == -11 || result == 0 {
|
||||||
|
if self.ctx.name.contains("v4l2m2m") {
|
||||||
|
return Ok(frames
|
||||||
|
.into_iter()
|
||||||
|
.map(|frame| EncodeBytesFrame {
|
||||||
|
data: Bytes::copy_from_slice(frame.data.as_ref()),
|
||||||
|
pts: frame.pts,
|
||||||
|
key: frame.key,
|
||||||
|
})
|
||||||
|
.collect());
|
||||||
|
}
|
||||||
return Ok(frames);
|
return Ok(frames);
|
||||||
}
|
}
|
||||||
Err(result)
|
Err(result)
|
||||||
@@ -698,11 +677,11 @@ impl Encoder {
|
|||||||
pub fn available_encoders(ctx: EncodeContext, _sdk: Option<String>) -> Vec<CodecInfo> {
|
pub fn available_encoders(ctx: EncodeContext, _sdk: Option<String>) -> Vec<CodecInfo> {
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
if !(cfg!(windows) || cfg!(target_os = "linux") || cfg!(target_os = "android")) {
|
if !(cfg!(windows) || cfg!(target_os = "linux")) {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let mut res = vec![];
|
let mut res = vec![];
|
||||||
#[cfg(any(windows, target_os = "linux", target_os = "android"))]
|
#[cfg(any(windows, target_os = "linux"))]
|
||||||
let codecs = enumerate_candidate_codecs(&ctx);
|
let codecs = enumerate_candidate_codecs(&ctx);
|
||||||
|
|
||||||
if let Ok(yuv) = Encoder::dummy_yuv(ctx.clone()) {
|
if let Ok(yuv) = Encoder::dummy_yuv(ctx.clone()) {
|
||||||
@@ -736,6 +715,16 @@ impl Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn encoder_last_error_message() -> String {
|
||||||
|
unsafe {
|
||||||
|
let ptr = ffmpeg_ram_encoder_last_error();
|
||||||
|
if ptr.is_null() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
std::ffi::CStr::from_ptr(ptr).to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for Encoder {
|
impl Drop for Encoder {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
@@ -9,18 +9,12 @@ use std::ffi::c_int;
|
|||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/ffmpeg_ram_ffi.rs"));
|
include!(concat!(env!("OUT_DIR"), "/ffmpeg_ram_ffi.rs"));
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))]
|
||||||
any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"),
|
|
||||||
not(target_os = "android")
|
|
||||||
))]
|
|
||||||
pub mod decode;
|
pub mod decode;
|
||||||
|
|
||||||
// Provide a small stub on non-ARM builds so dependents can still compile, but decoder
|
// Provide a small stub on non-ARM builds so dependents can still compile, but decoder
|
||||||
// construction will fail (since the C++ RKMPP decoder isn't built/linked).
|
// construction will fail (since the C++ RKMPP decoder isn't built/linked).
|
||||||
#[cfg(any(
|
#[cfg(not(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp")))]
|
||||||
not(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp")),
|
|
||||||
target_os = "android"
|
|
||||||
))]
|
|
||||||
pub mod decode {
|
pub mod decode {
|
||||||
use crate::ffmpeg::AVPixelFormat;
|
use crate::ffmpeg::AVPixelFormat;
|
||||||
|
|
||||||
@@ -69,8 +63,6 @@ pub enum Priority {
|
|||||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
pub struct CodecInfo {
|
pub struct CodecInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(skip)]
|
|
||||||
pub mc_name: Option<String>,
|
|
||||||
pub format: DataFormat,
|
pub format: DataFormat,
|
||||||
pub priority: i32,
|
pub priority: i32,
|
||||||
pub hwdevice: AVHWDeviceType,
|
pub hwdevice: AVHWDeviceType,
|
||||||
@@ -80,7 +72,6 @@ impl Default for CodecInfo {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: Default::default(),
|
name: Default::default(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: DataFormat::H264,
|
format: DataFormat::H264,
|
||||||
priority: Default::default(),
|
priority: Default::default(),
|
||||||
hwdevice: AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
|
||||||
@@ -93,28 +84,24 @@ impl CodecInfo {
|
|||||||
match format {
|
match format {
|
||||||
H264 => Some(CodecInfo {
|
H264 => Some(CodecInfo {
|
||||||
name: "libx264".to_owned(),
|
name: "libx264".to_owned(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: H264,
|
format: H264,
|
||||||
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
||||||
priority: Priority::Soft as _,
|
priority: Priority::Soft as _,
|
||||||
}),
|
}),
|
||||||
H265 => Some(CodecInfo {
|
H265 => Some(CodecInfo {
|
||||||
name: "libx265".to_owned(),
|
name: "libx265".to_owned(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: H265,
|
format: H265,
|
||||||
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
||||||
priority: Priority::Soft as _,
|
priority: Priority::Soft as _,
|
||||||
}),
|
}),
|
||||||
VP8 => Some(CodecInfo {
|
VP8 => Some(CodecInfo {
|
||||||
name: "libvpx".to_owned(),
|
name: "libvpx".to_owned(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: VP8,
|
format: VP8,
|
||||||
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
||||||
priority: Priority::Soft as _,
|
priority: Priority::Soft as _,
|
||||||
}),
|
}),
|
||||||
VP9 => Some(CodecInfo {
|
VP9 => Some(CodecInfo {
|
||||||
name: "libvpx-vp9".to_owned(),
|
name: "libvpx-vp9".to_owned(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: VP9,
|
format: VP9,
|
||||||
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
||||||
priority: Priority::Soft as _,
|
priority: Priority::Soft as _,
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
pub mod capture;
|
pub mod capture;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod ffmpeg;
|
pub mod ffmpeg;
|
||||||
#[cfg(all(
|
#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))]
|
||||||
any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"),
|
|
||||||
not(target_os = "android")
|
|
||||||
))]
|
|
||||||
pub mod ffmpeg_hw;
|
pub mod ffmpeg_hw;
|
||||||
pub mod ffmpeg_ram;
|
pub mod ffmpeg_ram;
|
||||||
|
#[cfg(all(
|
||||||
|
target_os = "linux",
|
||||||
|
any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp")
|
||||||
|
))]
|
||||||
|
pub mod rkmpp_dmabuf;
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn hwcodec_log(level: i32, message: *const std::os::raw::c_char) {
|
pub extern "C" fn hwcodec_log(level: i32, message: *const std::os::raw::c_char) {
|
||||||
|
|||||||
193
libs/hwcodec/src/rkmpp_dmabuf.rs
Normal file
193
libs/hwcodec/src/rkmpp_dmabuf.rs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
//! Synchronous RKMPP encoder for pre-exported V4L2 DMA buffers.
|
||||||
|
//! Unlike the byte-slice encoder this never reads raw pixels on the CPU.
|
||||||
|
|
||||||
|
use std::ffi::{c_char, c_int, c_void, CStr};
|
||||||
|
use std::os::fd::{AsRawFd, OwnedFd};
|
||||||
|
use std::ptr::NonNull;
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn rkmpp_dma_new(
|
||||||
|
width: c_int,
|
||||||
|
height: c_int,
|
||||||
|
stride: c_int,
|
||||||
|
format: c_int,
|
||||||
|
codec: c_int,
|
||||||
|
fps: c_int,
|
||||||
|
kbps: c_int,
|
||||||
|
gop: c_int,
|
||||||
|
fds: *const c_int,
|
||||||
|
sizes: *const usize,
|
||||||
|
count: usize,
|
||||||
|
) -> *mut c_void;
|
||||||
|
fn rkmpp_dma_encode(
|
||||||
|
encoder: *mut c_void,
|
||||||
|
index: usize,
|
||||||
|
bytes_used: usize,
|
||||||
|
fresh_fd: c_int,
|
||||||
|
pts_us: i64,
|
||||||
|
force_idr: c_int,
|
||||||
|
data: *mut *const u8,
|
||||||
|
size: *mut usize,
|
||||||
|
) -> c_int;
|
||||||
|
fn rkmpp_dma_reconfigure(encoder: *mut c_void, kbps: c_int, gop: c_int) -> c_int;
|
||||||
|
fn rkmpp_dma_free(encoder: *mut c_void);
|
||||||
|
fn rkmpp_dma_error() -> *const c_char;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum DmaFormat {
|
||||||
|
Nv12 = 0,
|
||||||
|
Bgr24 = 1,
|
||||||
|
Yuyv = 2,
|
||||||
|
Rgb24 = 3,
|
||||||
|
Mjpeg = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DmaEncoderConfig {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub stride: u32,
|
||||||
|
pub format: DmaFormat,
|
||||||
|
pub hevc: bool,
|
||||||
|
pub fps: u32,
|
||||||
|
pub bitrate_kbps: u32,
|
||||||
|
pub gop: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DmaEncoder {
|
||||||
|
ctx: NonNull<c_void>,
|
||||||
|
// Export FDs remain open until AFTER mpp_destroy and imported buffer release.
|
||||||
|
_buffers: Vec<(OwnedFd, usize)>,
|
||||||
|
// Keep refreshed exports alive until native replacement/release has ended
|
||||||
|
// all references to the previous import. At most one FD per capture slot.
|
||||||
|
fresh_buffers: Vec<Option<OwnedFd>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclusive ownership: the context can move between threads but all calls are sequential.
|
||||||
|
unsafe impl Send for DmaEncoder {}
|
||||||
|
|
||||||
|
fn last_error() -> String {
|
||||||
|
unsafe {
|
||||||
|
CStr::from_ptr(rkmpp_dma_error())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DmaEncoder {
|
||||||
|
pub fn new(config: DmaEncoderConfig, buffers: Vec<(OwnedFd, usize)>) -> Result<Self, String> {
|
||||||
|
let fds: Vec<_> = buffers.iter().map(|(fd, _)| fd.as_raw_fd()).collect();
|
||||||
|
let sizes: Vec<_> = buffers.iter().map(|(_, size)| *size).collect();
|
||||||
|
for value in [
|
||||||
|
config.width,
|
||||||
|
config.height,
|
||||||
|
config.stride,
|
||||||
|
config.fps,
|
||||||
|
config.bitrate_kbps,
|
||||||
|
config.gop,
|
||||||
|
] {
|
||||||
|
if value > c_int::MAX as u32 {
|
||||||
|
return Err("DMA encoder parameter overflow".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ptr = unsafe {
|
||||||
|
rkmpp_dma_new(
|
||||||
|
config.width as _,
|
||||||
|
config.height as _,
|
||||||
|
config.stride as _,
|
||||||
|
config.format as c_int,
|
||||||
|
config.hevc as _,
|
||||||
|
config.fps as _,
|
||||||
|
config.bitrate_kbps as _,
|
||||||
|
config.gop as _,
|
||||||
|
fds.as_ptr(),
|
||||||
|
sizes.as_ptr(),
|
||||||
|
buffers.len(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
ctx: NonNull::new(ptr).ok_or_else(last_error)?,
|
||||||
|
fresh_buffers: (0..buffers.len()).map(|_| None).collect(),
|
||||||
|
_buffers: buffers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// The indexed buffer must be dequeued and exclusively leased to this call.
|
||||||
|
/// Do not requeue/write it until this function returns. On failure native MPP
|
||||||
|
/// is synchronously destroyed before returning, ending all input access.
|
||||||
|
/// `bytes_used` is the actual DQBUF payload length, not the buffer capacity.
|
||||||
|
/// A refreshed FD, if supplied, must refer to the same leased capture slot
|
||||||
|
/// with the capacity registered at construction. Ownership is retained here.
|
||||||
|
pub unsafe fn encode(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
bytes_used: usize,
|
||||||
|
fresh_fd: Option<OwnedFd>,
|
||||||
|
pts_ms: i64,
|
||||||
|
force_idr: bool,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let mut data = std::ptr::null();
|
||||||
|
let mut size = 0;
|
||||||
|
if index >= self.fresh_buffers.len() {
|
||||||
|
return Err("Invalid DMA capture index".into());
|
||||||
|
}
|
||||||
|
let ret = unsafe {
|
||||||
|
rkmpp_dma_encode(
|
||||||
|
self.ctx.as_ptr(),
|
||||||
|
index,
|
||||||
|
bytes_used,
|
||||||
|
fresh_fd.as_ref().map_or(-1, AsRawFd::as_raw_fd),
|
||||||
|
pts_ms.saturating_mul(1000),
|
||||||
|
force_idr as _,
|
||||||
|
&mut data,
|
||||||
|
&mut size,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if fresh_fd.is_some() {
|
||||||
|
// Native has now released the previous import, or destroyed both
|
||||||
|
// hardware contexts on error. Only now may its old FD be closed.
|
||||||
|
self.fresh_buffers[index] = fresh_fd;
|
||||||
|
}
|
||||||
|
if ret != 0 {
|
||||||
|
return Err(last_error());
|
||||||
|
}
|
||||||
|
if data.is_null() || size == 0 {
|
||||||
|
return Err("Empty RKMPP DMA packet".into());
|
||||||
|
}
|
||||||
|
// Copy only the compressed output, releasing the driver's packet promptly
|
||||||
|
// regardless of how long a network subscriber retains its Bytes.
|
||||||
|
Ok(unsafe { std::slice::from_raw_parts(data, size) }.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reconfigure(&mut self, kbps: u32, gop: u32) -> Result<(), String> {
|
||||||
|
if kbps > c_int::MAX as u32 || gop > c_int::MAX as u32 {
|
||||||
|
return Err("DMA encoder parameter overflow".into());
|
||||||
|
}
|
||||||
|
if unsafe { rkmpp_dma_reconfigure(self.ctx.as_ptr(), kbps as _, gop as _) } != 0 {
|
||||||
|
return Err(last_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DmaEncoder {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { rkmpp_dma_free(self.ctx.as_ptr()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_discriminants_match_native_abi() {
|
||||||
|
assert_eq!(DmaFormat::Nv12 as c_int, 0);
|
||||||
|
assert_eq!(DmaFormat::Bgr24 as c_int, 1);
|
||||||
|
assert_eq!(DmaFormat::Yuyv as c_int, 2);
|
||||||
|
assert_eq!(DmaFormat::Rgb24 as c_int, 3);
|
||||||
|
assert_eq!(DmaFormat::Mjpeg as c_int, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// This file is generated by cargo_embargo.
|
|
||||||
// Do not modify this file because the changes will be overridden on upgrade.
|
|
||||||
|
|
||||||
package {
|
|
||||||
default_applicable_licenses: ["external_rust_crates_v4l2r_license"],
|
|
||||||
}
|
|
||||||
|
|
||||||
rust_library {
|
|
||||||
name: "libv4l2r",
|
|
||||||
crate_name: "v4l2r",
|
|
||||||
cargo_env_compat: true,
|
|
||||||
cargo_pkg_version: "0.0.7",
|
|
||||||
crate_root: "src/lib.rs",
|
|
||||||
edition: "2021",
|
|
||||||
rustlibs: [
|
|
||||||
"libbitflags",
|
|
||||||
"liblog_rust",
|
|
||||||
"libnix",
|
|
||||||
"libthiserror",
|
|
||||||
],
|
|
||||||
proc_macros: ["libenumn"],
|
|
||||||
apex_available: [
|
|
||||||
"//apex_available:platform",
|
|
||||||
"//apex_available:anyapex",
|
|
||||||
],
|
|
||||||
product_available: true,
|
|
||||||
vendor_available: true,
|
|
||||||
// Bindgen-generated bindings of our local videodev2.h.
|
|
||||||
srcs: [":libv4l2r_bindgen"],
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
rust_test {
|
|
||||||
name: "v4l2r_test_src_lib",
|
|
||||||
crate_name: "v4l2r",
|
|
||||||
cargo_env_compat: true,
|
|
||||||
cargo_pkg_version: "0.0.7",
|
|
||||||
crate_root: "src/lib.rs",
|
|
||||||
test_suites: ["general-tests"],
|
|
||||||
auto_gen_config: true,
|
|
||||||
edition: "2021",
|
|
||||||
rustlibs: [
|
|
||||||
"libbitflags",
|
|
||||||
"liblog_rust",
|
|
||||||
"libnix",
|
|
||||||
"libthiserror",
|
|
||||||
],
|
|
||||||
proc_macros: ["libenumn"],
|
|
||||||
// Bindgen-generated bindings of our local videodev2.h.
|
|
||||||
srcs: [":libv4l2r_bindgen"],
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,7 @@ version = "0.1.6"
|
|||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
|
||||||
[dependencies.nix]
|
[dependencies.nix]
|
||||||
version = "0.28"
|
version = "0.31"
|
||||||
features = [
|
features = [
|
||||||
"ioctl",
|
"ioctl",
|
||||||
"mman",
|
"mman",
|
||||||
@@ -59,7 +59,7 @@ features = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[dependencies.thiserror]
|
[dependencies.thiserror]
|
||||||
version = "1.0"
|
version = "2"
|
||||||
|
|
||||||
[build-dependencies.bindgen]
|
[build-dependencies.bindgen]
|
||||||
version = "0.70.1"
|
version = "0.72"
|
||||||
|
|||||||
6
libs/v4l2r/Cargo.toml.orig
generated
6
libs/v4l2r/Cargo.toml.orig
generated
@@ -18,11 +18,11 @@ arch64 = []
|
|||||||
arch32 = []
|
arch32 = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nix = { version = "0.28", features = ["ioctl", "mman", "poll", "fs", "event"] }
|
nix = { version = "0.31", features = ["ioctl", "mman", "poll", "fs", "event"] }
|
||||||
bitflags = "2.4"
|
bitflags = "2.4"
|
||||||
thiserror = "1.0"
|
thiserror = "2"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
enumn = "0.1.6"
|
enumn = "0.1.6"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
bindgen = "0.70.1"
|
bindgen = "0.72"
|
||||||
|
|||||||
@@ -15,10 +15,5 @@ parts are intentionally removed here so this dependency stays scoped to capture.
|
|||||||
|
|
||||||
## Build options
|
## Build options
|
||||||
|
|
||||||
`cargo build` generates V4L2 bindings from `/usr/include/linux/videodev2.h` by
|
`cargo build` generates V4L2 bindings from the vendored Linux UAPI headers in
|
||||||
default. Set `V4L2R_VIDEODEV2_H_PATH` to a directory containing `videodev2.h` to
|
`include/`.
|
||||||
generate bindings from a different header.
|
|
||||||
|
|
||||||
For Android targets, the build script uses the Android NDK sysroot. Set one of
|
|
||||||
`ANDROID_NDK_HOME`, `ANDROID_NDK_ROOT`, `NDK_HOME`, `ANDROID_HOME`, or
|
|
||||||
`ANDROID_SDK_ROOT` if the NDK cannot be found automatically.
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
// This file defines the customizations to the bindgen builder used to generate the v4l2r
|
// This file defines the customizations to the bindgen builder used to generate the v4l2r
|
||||||
// bindings.
|
// bindings.
|
||||||
//
|
//
|
||||||
// It is meant to be included from `lib/build.rs` and `android/build.rs`.
|
// It is meant to be included from `build.rs`.
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// Workaround for https://github.com/rust-lang/rust-bindgen/issues/753.
|
/// Workaround for https://github.com/rust-lang/rust-bindgen/issues/753.
|
||||||
pub struct Fix753;
|
pub struct Fix753;
|
||||||
|
|
||||||
impl bindgen::callbacks::ParseCallbacks for Fix753 {
|
impl bindgen::callbacks::ParseCallbacks for Fix753 {
|
||||||
fn item_name(&self, original_item_name: &str) -> Option<String> {
|
fn item_name(&self, item_info: bindgen::callbacks::ItemInfo<'_>) -> Option<String> {
|
||||||
Some(original_item_name.trim_start_matches("Fix753_").to_owned())
|
Some(item_info.name.trim_start_matches("Fix753_").to_owned())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
use std::env::{self, VarError};
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
include!("bindgen.rs");
|
include!("bindgen.rs");
|
||||||
|
|
||||||
/// Environment variable that can be set to point to the directory containing the `videodev2.h`
|
/// Vendored Linux UAPI include root.
|
||||||
/// file to use to generate the bindings.
|
const VENDORED_INCLUDE_DIR: &str = "include";
|
||||||
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";
|
|
||||||
|
|
||||||
/// Wrapper file to use as input of bindgen.
|
/// Wrapper file to use as input of bindgen.
|
||||||
const WRAPPER_H: &str = "v4l2r_wrapper.h";
|
const WRAPPER_H: &str = "v4l2r_wrapper.h";
|
||||||
@@ -17,54 +13,23 @@ const WRAPPER_H: &str = "v4l2r_wrapper.h";
|
|||||||
const FIX753_H: &str = "fix753.h";
|
const FIX753_H: &str = "fix753.h";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let target = env::var("TARGET").unwrap_or_default();
|
let include_root =
|
||||||
let is_android = target.contains("android");
|
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 default_videodev2_h_path = if is_android {
|
|
||||||
android_sysroot().join("usr/include").display().to_string()
|
|
||||||
} else {
|
|
||||||
DEFAULT_VIDEODEV2_H_PATH.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
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");
|
|
||||||
println!("cargo::rerun-if-env-changed=ANDROID_HOME");
|
|
||||||
println!("cargo::rerun-if-env-changed=ANDROID_SDK_ROOT");
|
|
||||||
println!("cargo::rerun-if-env-changed=CARGO_NDK_PLATFORM");
|
|
||||||
println!("cargo::rerun-if-changed={}", videodev2_h.display());
|
println!("cargo::rerun-if-changed={}", videodev2_h.display());
|
||||||
println!("cargo::rerun-if-changed={}", FIX753_H);
|
println!("cargo::rerun-if-changed={}", FIX753_H);
|
||||||
println!("cargo::rerun-if-changed={}", WRAPPER_H);
|
println!("cargo::rerun-if-changed={}", WRAPPER_H);
|
||||||
|
|
||||||
let mut clang_args = vec![
|
let clang_args = vec![
|
||||||
format!("-I{videodev2_h_path}"),
|
format!("-I{}", include_root.display()),
|
||||||
#[cfg(all(feature = "arch64", not(feature = "arch32")))]
|
#[cfg(all(feature = "arch64", not(feature = "arch32")))]
|
||||||
"--target=x86_64-linux-gnu".into(),
|
"--target=x86_64-linux-gnu".into(),
|
||||||
#[cfg(all(feature = "arch32", not(feature = "arch64")))]
|
#[cfg(all(feature = "arch32", not(feature = "arch64")))]
|
||||||
"--target=i686-linux-gnu".into(),
|
"--target=i686-linux-gnu".into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if is_android {
|
|
||||||
clang_args.extend(android_clang_args(&target));
|
|
||||||
}
|
|
||||||
|
|
||||||
let bindings = v4l2r_bindgen_builder(bindgen::Builder::default())
|
let bindings = v4l2r_bindgen_builder(bindgen::Builder::default())
|
||||||
.header(WRAPPER_H)
|
.header(WRAPPER_H)
|
||||||
.clang_args(clang_args)
|
.clang_args(clang_args)
|
||||||
@@ -76,105 +41,3 @@ fn main() {
|
|||||||
.write_to_file(out_path.join("bindings.rs"))
|
.write_to_file(out_path.join("bindings.rs"))
|
||||||
.expect("Couldn't write bindings!");
|
.expect("Couldn't write bindings!");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn android_clang_args(target: &str) -> Vec<String> {
|
|
||||||
let ndk = android_ndk_home();
|
|
||||||
let toolchain = ndk.join("toolchains/llvm/prebuilt").join(host_tag());
|
|
||||||
let sysroot = toolchain.join("sysroot");
|
|
||||||
let clang_include = toolchain
|
|
||||||
.join("lib/clang")
|
|
||||||
.join(clang_version(&toolchain))
|
|
||||||
.join("include");
|
|
||||||
let api = env::var("CARGO_NDK_PLATFORM")
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse::<u32>().ok())
|
|
||||||
.unwrap_or(21);
|
|
||||||
let clang_target = android_clang_target(target);
|
|
||||||
|
|
||||||
vec![
|
|
||||||
format!("--target={clang_target}"),
|
|
||||||
format!("--sysroot={}", sysroot.display()),
|
|
||||||
format!("-D__ANDROID_API__={api}"),
|
|
||||||
format!("-isystem{}", clang_include.display()),
|
|
||||||
format!("-isystem{}", sysroot.join("usr/include").display()),
|
|
||||||
format!(
|
|
||||||
"-isystem{}",
|
|
||||||
sysroot.join("usr/include").join(clang_target).display()
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn android_clang_target(target: &str) -> &'static str {
|
|
||||||
match target {
|
|
||||||
"aarch64-linux-android" => "aarch64-linux-android",
|
|
||||||
"armv7-linux-androideabi" => "armv7a-linux-androideabi",
|
|
||||||
"i686-linux-android" => "i686-linux-android",
|
|
||||||
"x86_64-linux-android" => "x86_64-linux-android",
|
|
||||||
other => panic!("unsupported Android target for v4l2r bindgen: {other}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn android_sysroot() -> PathBuf {
|
|
||||||
android_ndk_home()
|
|
||||||
.join("toolchains/llvm/prebuilt")
|
|
||||||
.join(host_tag())
|
|
||||||
.join("sysroot")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn android_ndk_home() -> PathBuf {
|
|
||||||
for key in ["ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "NDK_HOME"] {
|
|
||||||
if let Ok(value) = env::var(key) {
|
|
||||||
return PathBuf::from(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key in ["ANDROID_HOME", "ANDROID_SDK_ROOT"] {
|
|
||||||
if let Ok(value) = env::var(key) {
|
|
||||||
let ndk_dir = PathBuf::from(value).join("ndk");
|
|
||||||
if let Some(newest) = newest_child_dir(&ndk_dir) {
|
|
||||||
return newest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panic!(
|
|
||||||
"v4l2r Android bindgen requires ANDROID_NDK_HOME, ANDROID_NDK_ROOT, NDK_HOME, \
|
|
||||||
or ANDROID_HOME/ANDROID_SDK_ROOT with an ndk directory"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn newest_child_dir(path: &PathBuf) -> Option<PathBuf> {
|
|
||||||
let mut entries = std::fs::read_dir(path)
|
|
||||||
.ok()?
|
|
||||||
.filter_map(|entry| entry.ok())
|
|
||||||
.map(|entry| entry.path())
|
|
||||||
.filter(|path| path.is_dir())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
entries.sort();
|
|
||||||
entries.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn host_tag() -> &'static str {
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
"linux-x86_64"
|
|
||||||
} else if cfg!(target_os = "macos") {
|
|
||||||
"darwin-x86_64"
|
|
||||||
} else if cfg!(target_os = "windows") {
|
|
||||||
"windows-x86_64"
|
|
||||||
} else {
|
|
||||||
panic!("unsupported host OS for Android NDK");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clang_version(toolchain: &PathBuf) -> String {
|
|
||||||
let clang_dir = toolchain.join("lib/clang");
|
|
||||||
let mut entries = std::fs::read_dir(&clang_dir)
|
|
||||||
.unwrap_or_else(|err| panic!("failed to read {}: {err}", clang_dir.display()))
|
|
||||||
.filter_map(|entry| entry.ok())
|
|
||||||
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
entries.sort();
|
|
||||||
entries
|
|
||||||
.pop()
|
|
||||||
.unwrap_or_else(|| panic!("no clang resource directory in {}", clang_dir.display()))
|
|
||||||
}
|
|
||||||
|
|||||||
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
@@ -2,6 +2,7 @@ use crate::ioctl::ioctl_and_convert;
|
|||||||
use crate::ioctl::IoctlConvertError;
|
use crate::ioctl::IoctlConvertError;
|
||||||
use crate::ioctl::IoctlConvertResult;
|
use crate::ioctl::IoctlConvertResult;
|
||||||
use crate::ioctl::UncheckedV4l2Buffer;
|
use crate::ioctl::UncheckedV4l2Buffer;
|
||||||
|
use crate::memory::MemoryType;
|
||||||
use crate::QueueType;
|
use crate::QueueType;
|
||||||
|
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
@@ -51,12 +52,13 @@ pub type DqBufError<CE> = IoctlConvertError<DqBufIoctlError, CE>;
|
|||||||
pub type DqBufResult<O, CE> = IoctlConvertResult<O, DqBufIoctlError, CE>;
|
pub type DqBufResult<O, CE> = IoctlConvertResult<O, DqBufIoctlError, CE>;
|
||||||
|
|
||||||
/// Safe wrapper around the `VIDIOC_DQBUF` ioctl.
|
/// Safe wrapper around the `VIDIOC_DQBUF` ioctl.
|
||||||
pub fn dqbuf<O>(fd: &impl AsRawFd, queue: QueueType) -> DqBufResult<O, O::Error>
|
pub fn dqbuf<O>(fd: &impl AsRawFd, queue: QueueType, memory: MemoryType) -> DqBufResult<O, O::Error>
|
||||||
where
|
where
|
||||||
O: TryFrom<UncheckedV4l2Buffer>,
|
O: TryFrom<UncheckedV4l2Buffer>,
|
||||||
O::Error: std::fmt::Debug,
|
O::Error: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let mut v4l2_buf = UncheckedV4l2Buffer::new_for_querybuf(queue, None);
|
let mut v4l2_buf = UncheckedV4l2Buffer::new_for_querybuf(queue, None);
|
||||||
|
v4l2_buf.0.memory = memory as u32;
|
||||||
|
|
||||||
ioctl_and_convert(
|
ioctl_and_convert(
|
||||||
unsafe { ioctl::vidioc_dqbuf(fd.as_raw_fd(), v4l2_buf.as_mut()) }
|
unsafe { ioctl::vidioc_dqbuf(fd.as_raw_fd(), v4l2_buf.as_mut()) }
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
#ifdef __ANDROID__
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <linux/videodev2.h>
|
#include <linux/videodev2.h>
|
||||||
|
|
||||||
#define MARK_FIX_753(name) const unsigned long int Fix753_##name = name;
|
#define MARK_FIX_753(name) const unsigned long int Fix753_##name = name;
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ license = "GPL-2.0"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
thiserror = "1"
|
thiserror = "2"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -97,14 +97,14 @@ impl ExfatBootSector {
|
|||||||
let heap_sectors = volume_length as u32 - cluster_heap_offset;
|
let heap_sectors = volume_length as u32 - cluster_heap_offset;
|
||||||
let cluster_count = heap_sectors / sectors_per_cluster;
|
let cluster_count = heap_sectors / sectors_per_cluster;
|
||||||
|
|
||||||
// Calculate root directory cluster based on upcase table size
|
// Calculate root directory cluster based on bitmap and upcase table size.
|
||||||
// Cluster 2: Bitmap (1 cluster)
|
|
||||||
// Cluster 3...: Upcase table (128KB, may span multiple clusters)
|
|
||||||
// Next available: Root directory
|
|
||||||
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
|
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
|
||||||
|
let bitmap_size = ((cluster_count + 7) / 8) as u64;
|
||||||
|
let bitmap_clusters =
|
||||||
|
((bitmap_size + cluster_size as u64 - 1) / cluster_size as u64).max(1) as u32;
|
||||||
let upcase_clusters =
|
let upcase_clusters =
|
||||||
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
|
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
|
||||||
let first_cluster_of_root = 3 + upcase_clusters;
|
let first_cluster_of_root = 2 + bitmap_clusters + upcase_clusters;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
jump_boot: [0xEB, 0x76, 0x90],
|
jump_boot: [0xEB, 0x76, 0x90],
|
||||||
@@ -211,6 +211,15 @@ const ENTRY_TYPE_VOLUME_LABEL: u8 = 0x83;
|
|||||||
const ENTRY_TYPE_BITMAP: u8 = 0x81;
|
const ENTRY_TYPE_BITMAP: u8 = 0x81;
|
||||||
const ENTRY_TYPE_UPCASE: u8 = 0x82;
|
const ENTRY_TYPE_UPCASE: u8 = 0x82;
|
||||||
|
|
||||||
|
fn set_cluster_allocated(bitmap: &mut [u8], cluster: u32) {
|
||||||
|
let index = (cluster - 2) as usize;
|
||||||
|
let byte_idx = index / 8;
|
||||||
|
let bit_idx = index % 8;
|
||||||
|
if byte_idx < bitmap.len() {
|
||||||
|
bitmap[byte_idx] |= 1 << bit_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create volume label directory entry
|
/// Create volume label directory entry
|
||||||
fn create_volume_label_entry(label: &str) -> [u8; 32] {
|
fn create_volume_label_entry(label: &str) -> [u8; 32] {
|
||||||
let mut entry = [0u8; 32];
|
let mut entry = [0u8; 32];
|
||||||
@@ -301,27 +310,42 @@ pub fn format_exfat<W: Write + Seek>(
|
|||||||
let fat_offset = partition_offset + boot_sector.fat_offset as u64 * 512;
|
let fat_offset = partition_offset + boot_sector.fat_offset as u64 * 512;
|
||||||
writer.seek(SeekFrom::Start(fat_offset))?;
|
writer.seek(SeekFrom::Start(fat_offset))?;
|
||||||
|
|
||||||
|
let bitmap_size = (boot_sector.cluster_count + 7) / 8;
|
||||||
|
let bitmap_clusters =
|
||||||
|
((bitmap_size as u64 + cluster_size as u64 - 1) / cluster_size as u64).max(1) as u32;
|
||||||
|
|
||||||
// Calculate how many clusters the upcase table needs (128KB)
|
// Calculate how many clusters the upcase table needs (128KB)
|
||||||
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
|
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
|
||||||
let upcase_clusters =
|
let upcase_clusters =
|
||||||
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
|
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
|
||||||
let root_cluster = 3 + upcase_clusters; // Root comes after bitmap and upcase
|
let bitmap_start_cluster = 2;
|
||||||
|
let upcase_start_cluster = bitmap_start_cluster + bitmap_clusters;
|
||||||
|
let root_cluster = upcase_start_cluster + upcase_clusters;
|
||||||
|
|
||||||
// FAT entries: cluster 0 and 1 are reserved
|
// FAT entries: cluster 0 and 1 are reserved
|
||||||
// 0: Media type (0xFFFFFFF8)
|
// 0: Media type (0xFFFFFFF8)
|
||||||
// 1: Reserved (0xFFFFFFFF)
|
// 1: Reserved (0xFFFFFFFF)
|
||||||
// 2: Bitmap cluster (single cluster, end of chain)
|
// 2..2+bitmap_clusters-1: Bitmap cluster chain
|
||||||
// 3..3+upcase_clusters-1: Upcase table cluster chain
|
// upcase_start_cluster..upcase_start_cluster+upcase_clusters-1: Upcase table cluster chain
|
||||||
// 3+upcase_clusters: Root directory cluster (end of chain)
|
// root_cluster: Root directory cluster (end of chain)
|
||||||
let mut fat_entries = vec![
|
let mut fat_entries = vec![
|
||||||
0xFFFFFFF8, // Media type
|
0xFFFFFFF8, // Media type
|
||||||
0xFFFFFFFF, // Reserved
|
0xFFFFFFFF, // Reserved
|
||||||
0xFFFFFFFF, // Bitmap (single cluster, end of chain)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Build allocation bitmap cluster chain
|
||||||
|
for i in 0..bitmap_clusters {
|
||||||
|
let cluster_num = bitmap_start_cluster + i;
|
||||||
|
if i == bitmap_clusters - 1 {
|
||||||
|
fat_entries.push(0xFFFFFFFF);
|
||||||
|
} else {
|
||||||
|
fat_entries.push(cluster_num + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build upcase table cluster chain
|
// Build upcase table cluster chain
|
||||||
for i in 0..upcase_clusters {
|
for i in 0..upcase_clusters {
|
||||||
let cluster_num = 3 + i;
|
let cluster_num = upcase_start_cluster + i;
|
||||||
if i == upcase_clusters - 1 {
|
if i == upcase_clusters - 1 {
|
||||||
// Last cluster in chain
|
// Last cluster in chain
|
||||||
fat_entries.push(0xFFFFFFFF);
|
fat_entries.push(0xFFFFFFFF);
|
||||||
@@ -345,38 +369,26 @@ pub fn format_exfat<W: Write + Seek>(
|
|||||||
// Calculate cluster heap offset
|
// Calculate cluster heap offset
|
||||||
let heap_offset = partition_offset + boot_sector.cluster_heap_offset as u64 * 512;
|
let heap_offset = partition_offset + boot_sector.cluster_heap_offset as u64 * 512;
|
||||||
|
|
||||||
// Cluster 2: Allocation Bitmap
|
// Allocation Bitmap
|
||||||
let bitmap_size = (boot_sector.cluster_count + 7) / 8;
|
let mut bitmap = vec![0u8; bitmap_clusters as usize * cluster_size as usize];
|
||||||
let _bitmap_clusters =
|
|
||||||
((bitmap_size as u64 + cluster_size as u64 - 1) / cluster_size as u64).max(1);
|
|
||||||
let mut bitmap = vec![0u8; cluster_size as usize];
|
|
||||||
|
|
||||||
// Mark clusters 2, 3..3+upcase_clusters-1, root_cluster as used
|
// Mark bitmap, upcase, and root directory clusters as used.
|
||||||
// Cluster 2: bitmap
|
// exFAT allocation bitmap bit 0 describes cluster 2.
|
||||||
bitmap[0] |= 0b00000100; // Bit 2
|
for i in 0..bitmap_clusters {
|
||||||
// Clusters 3..3+upcase_clusters-1: upcase table
|
set_cluster_allocated(&mut bitmap, bitmap_start_cluster + i);
|
||||||
|
}
|
||||||
for i in 0..upcase_clusters {
|
for i in 0..upcase_clusters {
|
||||||
let cluster = 3 + i;
|
set_cluster_allocated(&mut bitmap, upcase_start_cluster + i);
|
||||||
let byte_idx = (cluster / 8) as usize;
|
|
||||||
let bit_idx = cluster % 8;
|
|
||||||
if byte_idx < bitmap.len() {
|
|
||||||
bitmap[byte_idx] |= 1 << bit_idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Root directory cluster
|
|
||||||
let byte_idx = (root_cluster / 8) as usize;
|
|
||||||
let bit_idx = root_cluster % 8;
|
|
||||||
if byte_idx < bitmap.len() {
|
|
||||||
bitmap[byte_idx] |= 1 << bit_idx;
|
|
||||||
}
|
}
|
||||||
|
set_cluster_allocated(&mut bitmap, root_cluster);
|
||||||
|
|
||||||
writer.seek(SeekFrom::Start(heap_offset))?;
|
writer.seek(SeekFrom::Start(heap_offset))?;
|
||||||
writer.write_all(&bitmap)?;
|
writer.write_all(&bitmap)?;
|
||||||
|
|
||||||
// Cluster 3..3+upcase_clusters-1: Upcase table
|
// Upcase table
|
||||||
let upcase_data = generate_upcase_table();
|
let upcase_data = generate_upcase_table();
|
||||||
let upcase_checksum = calculate_upcase_checksum(&upcase_data);
|
let upcase_checksum = calculate_upcase_checksum(&upcase_data);
|
||||||
let upcase_offset = heap_offset + cluster_size as u64; // Start at cluster 3
|
let upcase_offset = heap_offset + bitmap_clusters as u64 * cluster_size as u64;
|
||||||
writer.seek(SeekFrom::Start(upcase_offset))?;
|
writer.seek(SeekFrom::Start(upcase_offset))?;
|
||||||
writer.write_all(&upcase_data)?;
|
writer.write_all(&upcase_data)?;
|
||||||
|
|
||||||
@@ -388,13 +400,18 @@ pub fn format_exfat<W: Write + Seek>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Root directory cluster
|
// Root directory cluster
|
||||||
let root_offset = heap_offset + (1 + upcase_clusters as u64) * cluster_size as u64;
|
let root_offset =
|
||||||
|
heap_offset + (bitmap_clusters as u64 + upcase_clusters as u64) * cluster_size as u64;
|
||||||
writer.seek(SeekFrom::Start(root_offset))?;
|
writer.seek(SeekFrom::Start(root_offset))?;
|
||||||
|
|
||||||
// Write directory entries
|
// Write directory entries
|
||||||
let volume_label_entry = create_volume_label_entry(label);
|
let volume_label_entry = create_volume_label_entry(label);
|
||||||
let bitmap_entry = create_bitmap_entry(2, bitmap_size as u64);
|
let bitmap_entry = create_bitmap_entry(bitmap_start_cluster, bitmap_size as u64);
|
||||||
let upcase_entry = create_upcase_entry(3, upcase_data.len() as u64, upcase_checksum);
|
let upcase_entry = create_upcase_entry(
|
||||||
|
upcase_start_cluster,
|
||||||
|
upcase_data.len() as u64,
|
||||||
|
upcase_checksum,
|
||||||
|
);
|
||||||
|
|
||||||
writer.write_all(&volume_label_entry)?;
|
writer.write_all(&volume_label_entry)?;
|
||||||
writer.write_all(&bitmap_entry)?;
|
writer.write_all(&bitmap_entry)?;
|
||||||
@@ -413,6 +430,9 @@ pub fn format_exfat<W: Write + Seek>(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::partition::PartitionLayout;
|
||||||
|
use std::io::Read;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cluster_size() {
|
fn test_cluster_size() {
|
||||||
@@ -428,4 +448,120 @@ mod tests {
|
|||||||
assert_eq!(get_cluster_size(8 * 1024 * 1024 * 1024 / 512), 131072); // 8GB → 128KB
|
assert_eq!(get_cluster_size(8 * 1024 * 1024 * 1024 / 512), 131072); // 8GB → 128KB
|
||||||
assert_eq!(get_cluster_size(16 * 1024 * 1024 * 1024 / 512), 131072); // 16GB → 128KB
|
assert_eq!(get_cluster_size(16 * 1024 * 1024 * 1024 / 512), 131072); // 16GB → 128KB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_bitmap_uses_cluster_heap_bit_indices() {
|
||||||
|
let temp_file = NamedTempFile::new().unwrap();
|
||||||
|
let path = temp_file.path();
|
||||||
|
let size = 64 * 1024 * 1024u64;
|
||||||
|
let layout = PartitionLayout::calculate(size).unwrap();
|
||||||
|
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(path)
|
||||||
|
.unwrap();
|
||||||
|
file.set_len(size).unwrap();
|
||||||
|
format_exfat(&mut file, layout.data_offset(), layout.data_size(), "TEST").unwrap();
|
||||||
|
|
||||||
|
let mut boot_sector = [0u8; 512];
|
||||||
|
file.seek(SeekFrom::Start(layout.data_offset())).unwrap();
|
||||||
|
file.read_exact(&mut boot_sector).unwrap();
|
||||||
|
let cluster_heap_offset = u32::from_le_bytes(boot_sector[88..92].try_into().unwrap());
|
||||||
|
let first_cluster_of_root = u32::from_le_bytes(boot_sector[96..100].try_into().unwrap());
|
||||||
|
let sectors_per_cluster = 1u64 << boot_sector[109];
|
||||||
|
let cluster_size = sectors_per_cluster * 512;
|
||||||
|
|
||||||
|
let bitmap_offset = layout.data_offset() + cluster_heap_offset as u64 * 512;
|
||||||
|
let mut bitmap = vec![0u8; cluster_size as usize];
|
||||||
|
file.seek(SeekFrom::Start(bitmap_offset)).unwrap();
|
||||||
|
file.read_exact(&mut bitmap).unwrap();
|
||||||
|
|
||||||
|
let is_allocated = |cluster: u32| {
|
||||||
|
let index = (cluster - 2) as usize;
|
||||||
|
(bitmap[index / 8] & (1 << (index % 8))) != 0
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
is_allocated(2),
|
||||||
|
"allocation bitmap cluster must be allocated"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
is_allocated(3),
|
||||||
|
"upcase table first cluster must be allocated"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
is_allocated(first_cluster_of_root),
|
||||||
|
"root directory cluster must be allocated"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!is_allocated(first_cluster_of_root + 1),
|
||||||
|
"first data cluster after root should be free after formatting"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_supports_multi_cluster_allocation_bitmap() {
|
||||||
|
let temp_file = NamedTempFile::new().unwrap();
|
||||||
|
let path = temp_file.path();
|
||||||
|
let size = 240 * 1024 * 1024u64;
|
||||||
|
let layout = PartitionLayout::calculate(size).unwrap();
|
||||||
|
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(path)
|
||||||
|
.unwrap();
|
||||||
|
file.set_len(size).unwrap();
|
||||||
|
format_exfat(&mut file, layout.data_offset(), layout.data_size(), "TEST").unwrap();
|
||||||
|
|
||||||
|
let mut boot_sector = [0u8; 512];
|
||||||
|
file.seek(SeekFrom::Start(layout.data_offset())).unwrap();
|
||||||
|
file.read_exact(&mut boot_sector).unwrap();
|
||||||
|
let fat_offset = u32::from_le_bytes(boot_sector[80..84].try_into().unwrap());
|
||||||
|
let cluster_heap_offset = u32::from_le_bytes(boot_sector[88..92].try_into().unwrap());
|
||||||
|
let cluster_count = u32::from_le_bytes(boot_sector[92..96].try_into().unwrap());
|
||||||
|
let first_cluster_of_root = u32::from_le_bytes(boot_sector[96..100].try_into().unwrap());
|
||||||
|
let sectors_per_cluster = 1u64 << boot_sector[109];
|
||||||
|
let cluster_size = sectors_per_cluster * 512;
|
||||||
|
|
||||||
|
let bitmap_size = ((cluster_count + 7) / 8) as u64;
|
||||||
|
let bitmap_clusters = bitmap_size.div_ceil(cluster_size) as u32;
|
||||||
|
assert!(
|
||||||
|
bitmap_clusters > 1,
|
||||||
|
"test volume should require a multi-cluster allocation bitmap"
|
||||||
|
);
|
||||||
|
|
||||||
|
let upcase_clusters = (128 * 1024u64).div_ceil(cluster_size) as u32;
|
||||||
|
assert_eq!(first_cluster_of_root, 2 + bitmap_clusters + upcase_clusters);
|
||||||
|
|
||||||
|
let read_fat = |file: &mut std::fs::File, cluster: u32| -> u32 {
|
||||||
|
let offset = layout.data_offset() + fat_offset as u64 * 512 + cluster as u64 * 4;
|
||||||
|
let mut bytes = [0u8; 4];
|
||||||
|
file.seek(SeekFrom::Start(offset)).unwrap();
|
||||||
|
file.read_exact(&mut bytes).unwrap();
|
||||||
|
u32::from_le_bytes(bytes)
|
||||||
|
};
|
||||||
|
assert_eq!(read_fat(&mut file, 2), 3);
|
||||||
|
assert_eq!(read_fat(&mut file, 2 + bitmap_clusters - 1), 0xFFFFFFFF);
|
||||||
|
|
||||||
|
let root_offset = layout.data_offset()
|
||||||
|
+ cluster_heap_offset as u64 * 512
|
||||||
|
+ (first_cluster_of_root - 2) as u64 * cluster_size;
|
||||||
|
let mut root = vec![0u8; cluster_size as usize];
|
||||||
|
file.seek(SeekFrom::Start(root_offset)).unwrap();
|
||||||
|
file.read_exact(&mut root).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(root[32], ENTRY_TYPE_BITMAP);
|
||||||
|
assert_eq!(u32::from_le_bytes(root[52..56].try_into().unwrap()), 2);
|
||||||
|
assert_eq!(
|
||||||
|
u64::from_le_bytes(root[56..64].try_into().unwrap()),
|
||||||
|
bitmap_size
|
||||||
|
);
|
||||||
|
assert_eq!(root[64], ENTRY_TYPE_UPCASE);
|
||||||
|
assert_eq!(
|
||||||
|
u32::from_le_bytes(root[84..88].try_into().unwrap()),
|
||||||
|
2 + bitmap_clusters
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -28,8 +28,15 @@ impl VentoyImage {
|
|||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create sparse file
|
// Build beside the destination so a failed create cannot leave a partial image.
|
||||||
let mut file = File::create(path)?;
|
let parent = path
|
||||||
|
.parent()
|
||||||
|
.filter(|parent| !parent.as_os_str().is_empty())
|
||||||
|
.unwrap_or_else(|| Path::new("."));
|
||||||
|
let mut temp = tempfile::Builder::new()
|
||||||
|
.prefix(".ventoy-")
|
||||||
|
.tempfile_in(parent)?;
|
||||||
|
let mut file = temp.as_file_mut();
|
||||||
file.set_len(size)?;
|
file.set_len(size)?;
|
||||||
|
|
||||||
// Write boot code
|
// Write boot code
|
||||||
@@ -64,6 +71,8 @@ impl VentoyImage {
|
|||||||
format_exfat(&mut file, layout.data_offset(), layout.data_size(), label)?;
|
format_exfat(&mut file, layout.data_offset(), layout.data_size(), label)?;
|
||||||
|
|
||||||
file.flush()?;
|
file.flush()?;
|
||||||
|
temp.persist(path)
|
||||||
|
.map_err(|error| VentoyError::Io(error.error))?;
|
||||||
|
|
||||||
println!("[INFO] Ventoy IMG created successfully!");
|
println!("[INFO] Ventoy IMG created successfully!");
|
||||||
|
|
||||||
@@ -274,3 +283,20 @@ impl VentoyImage {
|
|||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_create_does_not_publish_partial_image() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("ventoy.img");
|
||||||
|
|
||||||
|
let error = VentoyImage::create(&path, "64M", "TEST").err().unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(error, VentoyError::ResourceNotFound(_)));
|
||||||
|
assert!(!path.exists());
|
||||||
|
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ license = "BSD-3-Clause"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
bindgen = "0.70.1"
|
bindgen = "0.72"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user