mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 11:42:02 +08:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
198552014b | ||
|
|
3e3937605e | ||
|
|
4a85fbfab8 | ||
|
|
87d1110a87 | ||
|
|
b31aae284d | ||
|
|
dc6475776e | ||
|
|
3de72677e6 | ||
|
|
6a1616c32a | ||
|
|
032f47a891 | ||
|
|
2e0ca89943 | ||
|
|
1f7cfb373c | ||
|
|
da05656a89 | ||
|
|
265852b312 | ||
|
|
02bf04ed7f | ||
|
|
8915d36bcf | ||
|
|
3ea15e37a4 | ||
|
|
cb0c66af96 | ||
|
|
a3ebcded34 | ||
|
|
f7c2cd1b90 | ||
|
|
e774210ae3 | ||
|
|
935fa823f2 | ||
|
|
dd3f73ae54 | ||
|
|
0b9d94f53f | ||
|
|
e5d6279a54 | ||
|
|
57d4091497 | ||
|
|
4e8c342905 | ||
|
|
17cd74f64c | ||
|
|
9923670426 | ||
|
|
3ee3df77b8 | ||
|
|
8ec2f25e82 | ||
|
|
c27d3a6703 | ||
|
|
6723f432a3 | ||
|
|
12a3f1c947 | ||
|
|
52754c862b | ||
|
|
e51d243324 | ||
|
|
a1ebd34083 | ||
|
|
89b19ea7dd | ||
|
|
0d47d8395d | ||
|
|
d82c863f40 | ||
|
|
d8e7de74a6 | ||
|
|
74035f8e12 | ||
|
|
8d45186eba | ||
|
|
c484580b8f | ||
|
|
56bce7937c | ||
|
|
07b982d1d2 | ||
|
|
9065e01225 | ||
|
|
cc3cc15774 | ||
|
|
fcb39c73fc | ||
|
|
7c703b8b4b | ||
|
|
8eac31f69f | ||
|
|
9653e16a68 | ||
|
|
d0c0852fbb | ||
|
|
c0a0c90cbd | ||
|
|
9e3483b836 | ||
|
|
132f445c29 | ||
|
|
4952cbaf19 | ||
|
|
099f0b1ca2 | ||
|
|
eecbc0fc13 | ||
|
|
2d81a071e5 | ||
|
|
3e35181583 | ||
|
|
c3a3f41a2c |
301
.github/workflows/build.yml
vendored
Normal file
301
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
publish_release:
|
||||||
|
description: Publish GitHub Release
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
release_tag:
|
||||||
|
description: Release tag name when publishing
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
frontend:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
working-directory: web
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Upload frontend dist
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: web-dist
|
||||||
|
path: web/dist
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
deb:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: frontend
|
||||||
|
timeout-minutes: 120
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download frontend dist
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: web-dist
|
||||||
|
path: web/dist
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install cross
|
||||||
|
run: cargo install cross --locked
|
||||||
|
|
||||||
|
- name: Build linux binary
|
||||||
|
run: bash build/build-images.sh
|
||||||
|
|
||||||
|
- name: Package deb
|
||||||
|
run: bash build/package-deb.sh
|
||||||
|
|
||||||
|
- name: Upload deb
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: one-kvm-deb
|
||||||
|
path: target/debian/*.deb
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
windows:
|
||||||
|
runs-on: windows-2022
|
||||||
|
needs: frontend
|
||||||
|
timeout-minutes: 120
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download frontend dist
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: web-dist
|
||||||
|
path: web/dist
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Set up MSVC
|
||||||
|
uses: ilammy/msvc-dev-cmd@v1
|
||||||
|
|
||||||
|
- name: Prepare vcpkg and dependencies
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$env:VCPKG_ROOT = "C:\vcpkg"
|
||||||
|
$env:VCPKG_DEFAULT_TRIPLET = "x64-windows-static"
|
||||||
|
$env:VCPKG_INSTALLED_DIR = Join-Path $pwd "vcpkg_installed"
|
||||||
|
|
||||||
|
if (-not (Test-Path $env:VCPKG_ROOT)) {
|
||||||
|
git clone https://github.com/microsoft/vcpkg $env:VCPKG_ROOT
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$env:VCPKG_ROOT\bootstrap-vcpkg.bat" -disableMetrics
|
||||||
|
& "$env:VCPKG_ROOT\vcpkg.exe" install --triplet $env:VCPKG_DEFAULT_TRIPLET --x-install-root="$env:VCPKG_INSTALLED_DIR"
|
||||||
|
|
||||||
|
$tripletRoot = Join-Path $env:VCPKG_INSTALLED_DIR $env:VCPKG_DEFAULT_TRIPLET
|
||||||
|
$env:TURBOJPEG_SOURCE = "explicit"
|
||||||
|
$env:TURBOJPEG_LIB_DIR = Join-Path $tripletRoot "lib"
|
||||||
|
$env:TURBOJPEG_INCLUDE_DIR = Join-Path $tripletRoot "include"
|
||||||
|
|
||||||
|
"VCPKG_ROOT=$env:VCPKG_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
|
"VCPKG_DEFAULT_TRIPLET=$env:VCPKG_DEFAULT_TRIPLET" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
|
"VCPKG_INSTALLED_DIR=$env:VCPKG_INSTALLED_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
|
"TURBOJPEG_SOURCE=$env:TURBOJPEG_SOURCE" | 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
|
||||||
|
|
||||||
|
- name: Build Windows exe
|
||||||
|
shell: pwsh
|
||||||
|
run: .\build\windows\build.ps1 -Configuration release -Package
|
||||||
|
|
||||||
|
- name: Upload exe
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: one-kvm-windows-exe
|
||||||
|
path: target/x86_64-pc-windows-msvc/release/one-kvm_*.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
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:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: [deb, windows, android]
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Validate release tag
|
||||||
|
run: |
|
||||||
|
if [ -z "${{ inputs.release_tag }}" ]; then
|
||||||
|
echo "release_tag is required when publish_release is true"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Download deb artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: one-kvm-deb
|
||||||
|
path: release-artifacts/deb
|
||||||
|
|
||||||
|
- name: Download exe artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: one-kvm-windows-exe
|
||||||
|
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
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ inputs.release_tag }}
|
||||||
|
prerelease: true
|
||||||
|
generate_release_notes: true
|
||||||
|
files: |
|
||||||
|
release-artifacts/deb/*.deb
|
||||||
|
release-artifacts/windows/*.exe
|
||||||
|
release-artifacts/android/*.apk
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
# Rust
|
# Rust
|
||||||
/target/
|
target/
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
@@ -30,6 +30,8 @@ Thumbs.db
|
|||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
/dist/
|
/dist/
|
||||||
|
/build-staging
|
||||||
|
/.tmp/
|
||||||
|
|
||||||
# Frontend (built files)
|
# Frontend (built files)
|
||||||
/web/node_modules/
|
/web/node_modules/
|
||||||
@@ -41,3 +43,4 @@ CLAUDE.md
|
|||||||
secrets.toml
|
secrets.toml
|
||||||
.env
|
.env
|
||||||
/docs/
|
/docs/
|
||||||
|
web/package-lock.json
|
||||||
|
|||||||
276
Cargo.toml
276
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "one-kvm"
|
name = "one-kvm"
|
||||||
version = "0.1.8"
|
version = "0.2.2"
|
||||||
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"
|
||||||
@@ -9,129 +9,259 @@ repository = "https://github.com/mofeng-git/One-KVM"
|
|||||||
keywords = ["kvm", "ipkvm", "remote-management", "embedded"]
|
keywords = ["kvm", "ipkvm", "remote-management", "embedded"]
|
||||||
categories = ["embedded", "network-programming"]
|
categories = ["embedded", "network-programming"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["desktop"]
|
||||||
|
desktop = [
|
||||||
|
"dep:tokio",
|
||||||
|
"dep:tokio-util",
|
||||||
|
"dep:axum",
|
||||||
|
"dep:axum-extra",
|
||||||
|
"dep:tower-http",
|
||||||
|
"dep:sqlx",
|
||||||
|
"dep:serde",
|
||||||
|
"dep:serde_json",
|
||||||
|
"dep:tracing",
|
||||||
|
"dep:tracing-subscriber",
|
||||||
|
"dep:thiserror",
|
||||||
|
"dep:anyhow",
|
||||||
|
"dep:argon2",
|
||||||
|
"dep:rand",
|
||||||
|
"dep:uuid",
|
||||||
|
"dep:base64",
|
||||||
|
"dep:nix",
|
||||||
|
"dep:reqwest",
|
||||||
|
"dep:urlencoding",
|
||||||
|
"dep:rust-embed",
|
||||||
|
"dep:mime_guess",
|
||||||
|
"dep:rustls",
|
||||||
|
"dep:rcgen",
|
||||||
|
"dep:axum-server",
|
||||||
|
"dep:clap",
|
||||||
|
"dep:time",
|
||||||
|
"dep:bytes",
|
||||||
|
"dep:bytemuck",
|
||||||
|
"dep:xxhash-rust",
|
||||||
|
"dep:async-stream",
|
||||||
|
"dep:futures",
|
||||||
|
"dep:tokio-tungstenite",
|
||||||
|
"dep:parking_lot",
|
||||||
|
"dep:arc-swap",
|
||||||
|
"dep:webrtc",
|
||||||
|
"dep:rtp",
|
||||||
|
"dep:rtsp-types",
|
||||||
|
"dep:sdp-types",
|
||||||
|
"dep:serialport",
|
||||||
|
"dep:async-trait",
|
||||||
|
"dep:libc",
|
||||||
|
"dep:ventoy-img",
|
||||||
|
"dep:protobuf",
|
||||||
|
"dep:sodiumoxide",
|
||||||
|
"dep:sha2",
|
||||||
|
"dep:typeshare",
|
||||||
|
"dep:hwcodec",
|
||||||
|
"dep:libyuv",
|
||||||
|
"dep:turbojpeg",
|
||||||
|
"dep:audiopus",
|
||||||
|
"dep:v4l2r",
|
||||||
|
"dep:alsa",
|
||||||
|
"dep:gpio-cdev",
|
||||||
|
"dep:cpal",
|
||||||
|
"dep:windows-sys",
|
||||||
|
]
|
||||||
|
android = [
|
||||||
|
"dep:anyhow",
|
||||||
|
"dep:argon2",
|
||||||
|
"dep:arc-swap",
|
||||||
|
"dep:async-stream",
|
||||||
|
"dep:async-trait",
|
||||||
|
"dep:axum",
|
||||||
|
"dep:axum-extra",
|
||||||
|
"dep:base64",
|
||||||
|
"dep:bytemuck",
|
||||||
|
"dep:bytes",
|
||||||
|
"dep:futures",
|
||||||
|
"dep:gpio-cdev",
|
||||||
|
"dep:hwcodec",
|
||||||
|
"dep:libc",
|
||||||
|
"dep:libyuv",
|
||||||
|
"dep:mime_guess",
|
||||||
|
"dep:nix",
|
||||||
|
"dep:parking_lot",
|
||||||
|
"dep:protobuf",
|
||||||
|
"dep:rand",
|
||||||
|
"dep:rcgen",
|
||||||
|
"dep:reqwest",
|
||||||
|
"dep:rtp",
|
||||||
|
"dep:rtsp-types",
|
||||||
|
"dep:rust-embed",
|
||||||
|
"dep:rustls",
|
||||||
|
"dep:sdp-types",
|
||||||
|
"dep:serde",
|
||||||
|
"dep:serde_json",
|
||||||
|
"dep: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"] }
|
tokio = { version = "1", features = ["full"], optional = true }
|
||||||
tokio-util = { version = "0.7", features = ["rt"] }
|
tokio-util = { version = "0.7", features = ["rt"], optional = true }
|
||||||
|
|
||||||
# Web framework
|
# Web framework
|
||||||
axum = { version = "0.8", features = ["ws", "multipart", "tokio"] }
|
axum = { version = "0.8", features = ["ws", "multipart", "tokio"], optional = true }
|
||||||
axum-extra = { version = "0.12", features = ["typed-header", "cookie"] }
|
axum-extra = { version = "0.12", features = ["cookie"], optional = true }
|
||||||
tower-http = { version = "0.6", features = ["fs", "cors", "trace", "compression-gzip"] }
|
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"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"], optional = true }
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
serde_json = "1"
|
serde_json = { version = "1", optional = true }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
tracing = "0.1"
|
tracing = { version = "0.1", optional = true }
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "tracing-log"] }
|
tracing-log = { version = "0.2", optional = true }
|
||||||
tracing-log = "0.2"
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "tracing-log"], optional = true }
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
thiserror = "2"
|
thiserror = { version = "2", optional = true }
|
||||||
anyhow = "1"
|
anyhow = { version = "1", optional = true }
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
argon2 = "0.5"
|
argon2 = { version = "0.5", optional = true }
|
||||||
rand = "0.9"
|
rand = { version = "0.9", optional = true }
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"], optional = true }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
base64 = { version = "0.22", optional = true }
|
||||||
base64 = "0.22"
|
|
||||||
nix = { version = "0.30", features = ["fs", "net", "hostname", "poll"] }
|
|
||||||
|
|
||||||
# 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 }
|
reqwest = { version = "0.13", features = ["stream", "rustls", "json"], default-features = false, optional = true }
|
||||||
urlencoding = "2"
|
urlencoding = { version = "2", optional = true }
|
||||||
|
|
||||||
# Static file embedding
|
# Static file embedding
|
||||||
rust-embed = { version = "8", features = ["compression"] }
|
rust-embed = { version = "8", features = ["compression", "debug-embed"], optional = true }
|
||||||
mime_guess = "2"
|
mime_guess = { version = "2", optional = true }
|
||||||
|
|
||||||
# TLS/HTTPS
|
# TLS/HTTPS
|
||||||
rustls = { version = "0.23", features = ["ring"] }
|
rustls = { version = "0.23", features = ["ring"], optional = true }
|
||||||
rcgen = "0.14"
|
rcgen = { version = "0.14", optional = true }
|
||||||
axum-server = { version = "0.8", features = ["tls-rustls"] }
|
axum-server = { version = "0.8", features = ["tls-rustls"], optional = true }
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"], optional = true }
|
||||||
|
|
||||||
# Time
|
# Time (cookie max_age + RFC3339 timestamps)
|
||||||
time = "0.3"
|
time = { version = "0.3", features = ["serde", "formatting", "parsing"], optional = true }
|
||||||
|
|
||||||
# Video capture (V4L2)
|
|
||||||
v4l2r = "0.0.7"
|
|
||||||
|
|
||||||
# JPEG encoding (libjpeg-turbo, SIMD accelerated)
|
|
||||||
turbojpeg = "1.3"
|
|
||||||
|
|
||||||
# Bytes handling
|
# Bytes handling
|
||||||
bytes = "1"
|
bytes = { version = "1", optional = true }
|
||||||
bytemuck = { version = "1.24", features = ["derive"] }
|
bytemuck = { version = "1.24", features = ["derive"], optional = true }
|
||||||
|
|
||||||
# Frame deduplication (hash-based comparison)
|
# Frame deduplication (hash-based comparison)
|
||||||
xxhash-rust = { version = "0.8", features = ["xxh64"] }
|
xxhash-rust = { version = "0.8", features = ["xxh64"], optional = true }
|
||||||
|
|
||||||
# Async channels
|
# Async channels
|
||||||
async-stream = "0.3"
|
async-stream = { version = "0.3", optional = true }
|
||||||
futures = "0.3"
|
futures = { version = "0.3", optional = true }
|
||||||
|
|
||||||
# WebSocket client (for ttyd proxy)
|
# WebSocket client (for ttyd proxy)
|
||||||
tokio-tungstenite = "0.28"
|
tokio-tungstenite = { version = "0.28", optional = true }
|
||||||
|
|
||||||
# High-performance synchronization
|
# High-performance synchronization
|
||||||
parking_lot = "0.12"
|
parking_lot = { version = "0.12", optional = true }
|
||||||
arc-swap = "1.8"
|
arc-swap = { version = "1.8", optional = true }
|
||||||
|
|
||||||
# WebRTC
|
# WebRTC
|
||||||
webrtc = "0.14"
|
webrtc = { version = "0.14", optional = true }
|
||||||
rtp = "0.14"
|
rtp = { version = "0.14", optional = true }
|
||||||
rtsp-types = "0.1"
|
rtsp-types = { version = "0.1", optional = true }
|
||||||
sdp-types = "0.1"
|
sdp-types = { version = "0.1", optional = true }
|
||||||
|
|
||||||
# Audio (ALSA capture + Opus encoding)
|
|
||||||
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
|
||||||
alsa = "0.11"
|
|
||||||
audiopus = "0.2"
|
|
||||||
|
|
||||||
# HID (serial port for CH9329)
|
# HID (serial port for CH9329)
|
||||||
serialport = "4"
|
serialport = { version = "4", optional = true }
|
||||||
async-trait = "0.1"
|
async-trait = { version = "0.1", optional = true }
|
||||||
libc = "0.2"
|
libc = { version = "0.2", optional = true }
|
||||||
|
|
||||||
# Ventoy bootable image support
|
# Ventoy bootable image support
|
||||||
ventoy-img = { path = "libs/ventoy-img-rs" }
|
ventoy-img = { path = "libs/ventoy-img-rs", optional = true }
|
||||||
|
|
||||||
# ATX (GPIO control)
|
|
||||||
gpio-cdev = "0.6"
|
|
||||||
|
|
||||||
# H264 hardware/software encoding (hwcodec from rustdesk)
|
|
||||||
hwcodec = { path = "libs/hwcodec" }
|
|
||||||
|
|
||||||
# RustDesk protocol support
|
# RustDesk protocol support
|
||||||
protobuf = { version = "3.7", features = ["with-bytes"] }
|
protobuf = { version = "3.7", features = ["with-bytes"], optional = true }
|
||||||
sodiumoxide = "0.2"
|
sodiumoxide = { version = "0.2", optional = true }
|
||||||
sha2 = "0.10"
|
sha2 = { version = "0.10", optional = true }
|
||||||
# High-performance pixel format conversion (libyuv)
|
|
||||||
libyuv = { path = "res/vcpkg/libyuv" }
|
|
||||||
|
|
||||||
# TypeScript type generation
|
# TypeScript type generation
|
||||||
typeshare = "1.0"
|
typeshare = { version = "1.0", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(any(unix, windows))'.dependencies]
|
||||||
|
# Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux)
|
||||||
|
hwcodec = { path = "libs/hwcodec", features = ["bytes"], optional = true }
|
||||||
|
libyuv = { path = "res/vcpkg/libyuv", optional = true }
|
||||||
|
turbojpeg = { version = "1.3", optional = true }
|
||||||
|
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
||||||
|
audiopus = { version = "0.2", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(all(unix, not(target_os = "android")))'.dependencies]
|
||||||
|
# Utilities
|
||||||
|
nix = { version = "0.30", default-features = false, features = ["fs", "socket", "net", "hostname", "poll"], optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
# Utilities
|
||||||
|
nix = { version = "0.30", default-features = false, features = ["fs", "socket", "hostname", "poll"], optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
# Video capture (V4L2)
|
||||||
|
v4l2r = { path = "libs/v4l2r", optional = true }
|
||||||
|
|
||||||
|
# Audio (ALSA capture)
|
||||||
|
alsa = { version = "0.11", optional = true }
|
||||||
|
|
||||||
|
# ATX (GPIO control)
|
||||||
|
gpio-cdev = { version = "0.6", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
cpal = { version = "0.17", default-features = false, optional = true }
|
||||||
|
windows-sys = { version = "0.61", features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_NetworkManagement_IpHelper",
|
||||||
|
"Win32_NetworkManagement_Ndis",
|
||||||
|
"Win32_Networking_WinSock",
|
||||||
|
"Win32_System_SystemInformation",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
protobuf-codegen = "3.7"
|
protobuf-codegen = "3.7"
|
||||||
toml = "0.9"
|
|
||||||
cc = "1"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|||||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
238
README.en.md
Normal file
238
README.en.md
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<div align="center">
|
||||||
|
<h1>One-KVM</h1>
|
||||||
|
<p><strong>An open, lightweight IP-KVM stack in Rust — remote management down to BIOS level</strong></p>
|
||||||
|
|
||||||
|
<p><a href="README.md">简体中文</a> · <a href="README.en.md">English</a></p>
|
||||||
|
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/releases)
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/stargazers)
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/network/members)
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/issues)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**One-KVM (Rust)** is a lightweight IP-KVM solution written in Rust. It lets you manage servers and workstations over the network, including at BIOS level.
|
||||||
|
|
||||||
|
Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
||||||
|
|
||||||
|
- **Open**: not tied to one hardware recipe; runs across many setups.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
> **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>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Core
|
||||||
|
|
||||||
|
| Area | Capabilities |
|
||||||
|
|------|----------------|
|
||||||
|
| Video capture | HDMI USB / MIPI CSI / RK3588 HDMI IN; MJPEG and WebRTC (H.264 / H.265 / VP8 / VP9) |
|
||||||
|
| Video encoding | VAAPI / QSV / RKMPP / V4L2 M2M hardware paths, with software fallback |
|
||||||
|
| 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 |
|
||||||
|
| ATX power | GPIO or USB relay; power and reset control |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
### Debian / Ubuntu (.deb)
|
||||||
|
|
||||||
|
Download a `one-kvm_*.deb` matching your CPU architecture from Releases, then from the directory containing the package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install ./one-kvm_0.x.x_<arch>.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the version and architecture in the filename with your actual file name.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Images:
|
||||||
|
|
||||||
|
- **one-kvm** — main app + ttyd
|
||||||
|
- **one-kvm-full** — same plus optional extras (e.g. gostc, easytier-core)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --name one-kvm -itd \
|
||||||
|
--privileged=true --restart unless-stopped \
|
||||||
|
-v /dev:/dev -v /sys:/sys \
|
||||||
|
--net=host \
|
||||||
|
silentwind0/one-kvm-full
|
||||||
|
```
|
||||||
|
|
||||||
|
If pulls are slow, use the Aliyun mirror, e.g. `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm-full` (and `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm` for the slim image).
|
||||||
|
|
||||||
|
### fnOS NAS (Feiniu / 飞牛)
|
||||||
|
|
||||||
|
One-KVM is listed in the fnOS **app store**; search and install on your NAS.
|
||||||
|
|
||||||
|
### Web UI and first run
|
||||||
|
|
||||||
|
Open `http://<device-ip>:8080` in a browser (**8420** after fnOS install). The first visit runs initial setup.
|
||||||
|
|
||||||
|
## Reporting issues
|
||||||
|
|
||||||
|
If something breaks:
|
||||||
|
|
||||||
|
1. Open [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) or report in the project QQ group.
|
||||||
|
2. Include **useful** error messages and steps to reproduce.
|
||||||
|
3. Mention software version, hardware, and OS details.
|
||||||
|
|
||||||
|
## Sponsorship
|
||||||
|
|
||||||
|
One-KVM builds on many great open-source projects; a lot of time goes into testing and maintenance. If you find it useful, you can support development on **[Afdian (为爱发电)](https://afdian.com/a/silentwind)**.
|
||||||
|
|
||||||
|
### Thanks
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Supporter list</strong></summary>
|
||||||
|
|
||||||
|
- 浩龙的电子嵌入式之路
|
||||||
|
|
||||||
|
- Tsuki
|
||||||
|
|
||||||
|
- H_xiaoming
|
||||||
|
|
||||||
|
- 0蓝蓝0
|
||||||
|
|
||||||
|
- fairybl
|
||||||
|
|
||||||
|
- Will
|
||||||
|
|
||||||
|
- 自.知
|
||||||
|
|
||||||
|
- 观棋不语٩ ི۶
|
||||||
|
|
||||||
|
- 爱发电用户_a57a4
|
||||||
|
|
||||||
|
- 爱发电用户_2c769
|
||||||
|
|
||||||
|
- 霜序
|
||||||
|
|
||||||
|
- 远方(闲鱼用户名:小远技术店铺)
|
||||||
|
|
||||||
|
- 爱发电用户_399fc
|
||||||
|
|
||||||
|
- 斐斐の
|
||||||
|
|
||||||
|
- 爱发电用户_09451
|
||||||
|
|
||||||
|
- 超高校级的錆鱼
|
||||||
|
|
||||||
|
- 爱发电用户_08cff
|
||||||
|
|
||||||
|
- guoke
|
||||||
|
|
||||||
|
- mgt
|
||||||
|
|
||||||
|
- 姜沢掵
|
||||||
|
|
||||||
|
- ui_beam
|
||||||
|
|
||||||
|
- 爱发电用户_c0dd7
|
||||||
|
|
||||||
|
- 爱发电用户_dnjK
|
||||||
|
|
||||||
|
- 忍者胖猪
|
||||||
|
|
||||||
|
- 永遠の願い
|
||||||
|
|
||||||
|
- 爱发电用户_GBrF
|
||||||
|
|
||||||
|
- 爱发电用户_fd65c
|
||||||
|
|
||||||
|
- 爱发电用户_vhNa
|
||||||
|
|
||||||
|
- 爱发电用户_Xu6S
|
||||||
|
|
||||||
|
- moss
|
||||||
|
|
||||||
|
- woshididi
|
||||||
|
|
||||||
|
- 爱发电用户_a0fd1
|
||||||
|
|
||||||
|
- 爱发电用户_f6bH
|
||||||
|
|
||||||
|
- 码农
|
||||||
|
|
||||||
|
- 爱发电用户_6639f
|
||||||
|
|
||||||
|
- jeron
|
||||||
|
|
||||||
|
- 爱发电用户_CN7y
|
||||||
|
|
||||||
|
- 爱发电用户_Up6w
|
||||||
|
|
||||||
|
- 爱发电用户_e3202
|
||||||
|
|
||||||
|
- 一语念白
|
||||||
|
|
||||||
|
- 云边
|
||||||
|
|
||||||
|
- 爱发电用户_5a711
|
||||||
|
|
||||||
|
- 爱发电用户_9a706
|
||||||
|
|
||||||
|
- T0m9ir1SUKI
|
||||||
|
|
||||||
|
- 爱发电用户_56d52
|
||||||
|
|
||||||
|
- 爱发电用户_3N6F
|
||||||
|
|
||||||
|
- DUSK
|
||||||
|
|
||||||
|
- 飘零
|
||||||
|
|
||||||
|
- .
|
||||||
|
|
||||||
|
- 饭太稀
|
||||||
|
|
||||||
|
- 葱
|
||||||
|
|
||||||
|
- MaxZ
|
||||||
|
|
||||||
|
- 爱发电用户_c5f33
|
||||||
|
|
||||||
|
- 爱发电用户_09386
|
||||||
|
|
||||||
|
- 爱发电用户_JT6c
|
||||||
|
|
||||||
|
- 爱发电用户_d3d9c
|
||||||
|
|
||||||
|
- ......
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Sponsors
|
||||||
|
|
||||||
|
**Mirror Download Services:**
|
||||||
|
- **[Chongqing University Open Source Software Mirror](https://mirrors.cqu.edu.cn/)** — provides mirror download services
|
||||||
|
|
||||||
|
**File hosting**
|
||||||
|
|
||||||
|
- **[Huang1111 public-interest program](https://pan.huang1111.cn/s/mxkx3T1)** — login-free downloads
|
||||||
|
|
||||||
|
**Cloud**
|
||||||
|
|
||||||
|
- **[林枫云](https://www.dkdun.cn)** — project server sponsorship
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
林枫云 offers premium network routes, high-frequency game servers, and high-bandwidth servers in China and abroad.
|
||||||
99
README.md
99
README.md
@@ -2,8 +2,9 @@
|
|||||||
<h1>One-KVM</h1>
|
<h1>One-KVM</h1>
|
||||||
<p><strong>Rust 编写的开放轻量 IP-KVM 解决方案,实现 BIOS 级远程管理</strong></p>
|
<p><strong>Rust 编写的开放轻量 IP-KVM 解决方案,实现 BIOS 级远程管理</strong></p>
|
||||||
|
|
||||||
<p><a href="README.md">简体中文</a></p>
|
<p><a href="README.md">简体中文</a> · <a href="README.en.md">English</a></p>
|
||||||
|
|
||||||
|
[](https://github.com/mofeng-git/One-KVM/releases)
|
||||||
[](https://github.com/mofeng-git/One-KVM/stargazers)
|
[](https://github.com/mofeng-git/One-KVM/stargazers)
|
||||||
[](https://github.com/mofeng-git/One-KVM/network/members)
|
[](https://github.com/mofeng-git/One-KVM/network/members)
|
||||||
[](https://github.com/mofeng-git/One-KVM/issues)
|
[](https://github.com/mofeng-git/One-KVM/issues)
|
||||||
@@ -15,57 +16,78 @@
|
|||||||
|
|
||||||
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
|
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
|
||||||
|
|
||||||
项目目标:
|
项目目标:提供一个开放、轻量、易用的 IPKVM 解决方案。
|
||||||
|
|
||||||
- **开放**:不绑定特定硬件配置,尽量适配常见 Linux 设备
|
- **开放**:不绑定特定硬件配置,可在各类硬件环境中稳定运行。
|
||||||
- **轻量**:单二进制分发,部署过程更简单
|
- **轻量**:以二进制文件形式分发,无繁杂的依赖项,部署过程简单。
|
||||||
- **易用**:网页界面完成设备与参数配置,无需手动改配置文件
|
- **易用**:无需手动编辑配置文件,参数设置均可通过网页界面完成。
|
||||||
|
|
||||||
> **注意:** One-KVM Rust 目前仍处于开发早期阶段,功能与细节会快速迭代,欢迎体验与反馈。
|
> **One-KVM Python** 已停止开发,如有需要可访问 <https://github.com/mofeng-git/One-KVM/tree/python>。
|
||||||
|
|
||||||
## 🔁 迁移说明
|
<div align="center">
|
||||||
|
|
||||||
开发重心正在从 **One-KVM Python** 逐步转向 **One-KVM Rust**。
|

|
||||||
|
|
||||||
- 如果你在使用 **One-KVM Python(基于 PiKVM)**,请查看 [One-KVM Python 文档](https://docs.one-kvm.cn/python/)
|
</div>
|
||||||
- One-KVM Rust 相较于 One-KVM Python:**尚未完全适配 CSI HDMI 采集卡**、**不支持 VNC 访问**,仍处于开发早期阶段
|
|
||||||
|
|
||||||
## 📊 功能介绍
|
## 📊 功能介绍
|
||||||
|
|
||||||
### 核心功能
|
### 核心功能
|
||||||
|
|
||||||
| 功能 | 说明 |
|
| 功能 | 能力说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 视频采集 | HDMI USB 采集卡支持,提供 MJPEG / WebRTC(H.264/H.265/VP8/VP9) |
|
| 视频采集 | HDMI USB /MIPI CSI/RK3588 HDMI IN 采集支持,提供 MJPEG / WebRTC(H.264/H.265/VP8/VP9) 视频流|
|
||||||
|
| 视频编码 | 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 控制电源/重启按钮 |
|
| ATX 电源控制 | GPIO /USB 继电器,支持控制电源、重启按钮 |
|
||||||
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
|
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
|
||||||
|
|
||||||
### 硬件编码
|
此外提供基于 Web UI 的可视化配置与中英文界面;并集成 Web 终端(ttyd)、内网穿透(gostc)、P2P 组网(EasyTier)、RustDesk 协议(扩展跨平台远程访问)以及 RTSP 推流等能力。
|
||||||
|
|
||||||
支持自动检测和选择硬件加速:
|
|
||||||
|
|
||||||
- **VAAPI**:Intel/AMD GPU
|
|
||||||
- **RKMPP**:Rockchip SoC
|
|
||||||
- **V4L2 M2M**:通用硬件编码器
|
|
||||||
- **软件编码**:CPU 编码
|
|
||||||
|
|
||||||
### 扩展能力
|
|
||||||
|
|
||||||
- Web UI 配置,多语言支持(中文/英文)
|
|
||||||
- 内置 Web 终端(ttyd)、内网穿透支持(gostc)、P2P 组网支持(EasyTier)、RustDesk 协议集成(用于跨平台远程访问能力扩展)和 RTSP 视频流(用于视频推流)
|
|
||||||
|
|
||||||
## ⚡ 安装使用
|
## ⚡ 安装使用
|
||||||
|
|
||||||
可以访问 [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/)。
|
||||||
|
|
||||||
|
### 使用 deb 安装(Debian / Ubuntu)
|
||||||
|
|
||||||
|
从 Releases 下载与本机架构匹配的 `one-kvm_*.deb`,在包所在目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install ./one-kvm_0.x.x_<arch>.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
将文件名中的版本号与架构替换为实际下载的包名。
|
||||||
|
|
||||||
|
### 使用 Docker
|
||||||
|
|
||||||
|
镜像分为 **one-kvm**(One-KVM 主程序 + ttyd)与 **one-kvm-full**(另含 gostc、easytier-core 等可选扩展),按需选用。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --name one-kvm -itd \
|
||||||
|
--privileged=true --restart unless-stopped \
|
||||||
|
-v /dev:/dev -v /sys:/sys \
|
||||||
|
--net=host \
|
||||||
|
silentwind0/one-kvm-full
|
||||||
|
```
|
||||||
|
|
||||||
|
拉取较慢时,可将镜像名替换为阿里云加速,例如 `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm-full`(`one-kvm` 镜像同理,将 `silentwind0/one-kvm` 换为 `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm`)。
|
||||||
|
|
||||||
|
### 飞牛 NAS
|
||||||
|
|
||||||
|
One-KVM 已上架飞牛 **应用市场**,在 NAS 上直接搜索安装即可。
|
||||||
|
|
||||||
|
### 访问 Web 与首次配置
|
||||||
|
|
||||||
|
浏览器访问 `http://<设备 IP>:8080`(飞牛 NAS 安装后为 8420 端口)。首次访问将引导完成初始配置。
|
||||||
|
|
||||||
## 报告问题
|
## 报告问题
|
||||||
|
|
||||||
如果您发现了问题,请:
|
如果您发现了问题,请:
|
||||||
1. 使用 [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) 报告,或加入 QQ 群聊反馈。
|
1. 使用 [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) 报告,或加入 QQ 群聊反馈。
|
||||||
2. 提供详细的错误信息和复现步骤
|
2. 提供有帮助的错误信息和复现步骤
|
||||||
3. 包含您的硬件配置和系统信息
|
3. 包含您使用的软件版本、硬件配置和系统信息
|
||||||
|
|
||||||
## 赞助支持
|
## 赞助支持
|
||||||
|
|
||||||
@@ -88,8 +110,6 @@
|
|||||||
|
|
||||||
- Will
|
- Will
|
||||||
|
|
||||||
- 浩龙的电子嵌入式之路
|
|
||||||
|
|
||||||
- 自.知
|
- 自.知
|
||||||
|
|
||||||
- 观棋不语٩ ི۶
|
- 观棋不语٩ ི۶
|
||||||
@@ -188,8 +208,6 @@
|
|||||||
|
|
||||||
- 爱发电用户_JT6c
|
- 爱发电用户_JT6c
|
||||||
|
|
||||||
- MaxZ
|
|
||||||
|
|
||||||
- 爱发电用户_d3d9c
|
- 爱发电用户_d3d9c
|
||||||
|
|
||||||
- ......
|
- ......
|
||||||
@@ -200,13 +218,22 @@
|
|||||||
|
|
||||||
本项目得到以下赞助商的支持:
|
本项目得到以下赞助商的支持:
|
||||||
|
|
||||||
|
**镜像下载服务:**
|
||||||
|
- **[重庆大学开源软件镜像站](https://mirrors.cqu.edu.cn/)** - 提供镜像站下载服务
|
||||||
|
|
||||||
**文件存储服务:**
|
**文件存储服务:**
|
||||||
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
|
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
|
||||||
|
|
||||||
**云服务商**
|
**云服务商**
|
||||||
|
|
||||||
- **[林枫云](https://www.dkdun.cn)** - 赞助了本项目宁波大带宽服务器
|
- **[林枫云](https://www.dkdun.cn)** - 赞助了本项目服务器
|
||||||
|
|
||||||

|
<img height="128" alt="林枫云" src="https://docs.one-kvm.cn/img/36076FEFF0898A80EBD5756D28F4076C.png" />
|
||||||
|
|
||||||
林枫云主营国内外地域的精品线路业务服务器、高主频游戏服务器和大带宽服务器。
|
林枫云主营国内外地域的精品线路业务服务器、高主频游戏服务器和大带宽服务器。
|
||||||
|
|
||||||
|
- **[贝塔网络](https://my.beita.cc/?ref=github_onekvm)** - 赞助了本项目服务器
|
||||||
|
|
||||||
|
<img height="128" alt="BTBT" src="https://github.com/user-attachments/assets/c442d5f5-d72f-4a07-b9f4-400a6a0c3f1e" />
|
||||||
|
|
||||||
|
远程电脑、消费级GPU服务器、独服物理机,全自动在线交付。
|
||||||
|
|||||||
14
agents.md
Normal file
14
agents.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 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
Normal file
7
android/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.gradle/
|
||||||
|
.kotlin/
|
||||||
|
build/
|
||||||
|
local.properties
|
||||||
|
app/build/
|
||||||
|
app/src/main/jniLibs/
|
||||||
|
native/target/
|
||||||
571
android/app/build.gradle.kts
Normal file
571
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
36
android/app/src/main/AndroidManifest.xml
Normal file
36
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
71
android/app/src/main/java/cn/one_kvm/androidhost/LogStore.kt
Normal file
71
android/app/src/main/java/cn/one_kvm/androidhost/LogStore.kt
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
452
android/app/src/main/java/cn/one_kvm/androidhost/MainActivity.kt
Normal file
452
android/app/src/main/java/cn/one_kvm/androidhost/MainActivity.kt
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
38
android/app/src/main/res/drawable/ic_launcher_one_kvm.xml
Normal file
38
android/app/src/main/res/drawable/ic_launcher_one_kvm.xml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?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>
|
||||||
13
android/app/src/main/res/drawable/ic_stat_one_kvm.xml
Normal file
13
android/app/src/main/res/drawable/ic_stat_one_kvm.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?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>
|
||||||
4
android/app/src/main/res/values/strings.xml
Normal file
4
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">One-KVM Android Host</string>
|
||||||
|
</resources>
|
||||||
7
android/app/src/main/res/values/styles.xml
Normal file
7
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?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>
|
||||||
3
android/build.gradle.kts
Normal file
3
android/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application") version "9.0.0" apply false
|
||||||
|
}
|
||||||
3
android/gradle.properties
Normal file
3
android/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
android.useAndroidX=true
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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
Executable file
251
android/gradlew
vendored
Executable file
@@ -0,0 +1,251 @@
|
|||||||
|
#!/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
Normal file
94
android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
@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
|
||||||
21
android/native/Cargo.toml
Normal file
21
android/native/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[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"]
|
||||||
24
android/native/src/bin/one-kvm-android-host.rs
Normal file
24
android/native/src/bin/one-kvm-android-host.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
182
android/native/src/lib.rs
Normal file
182
android/native/src/lib.rs
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
18
android/settings.gradle.kts
Normal file
18
android/settings.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "OneKvmAndroidHost"
|
||||||
|
include(":app")
|
||||||
19
build.rs
19
build.rs
@@ -64,25 +64,6 @@ fn generate_secrets() {
|
|||||||
pub mod ice {
|
pub mod ice {
|
||||||
/// Google public STUN server URL (hardcoded)
|
/// Google public STUN server URL (hardcoded)
|
||||||
pub const STUN_SERVER: &str = "stun:stun.l.google.com:19302";
|
pub const STUN_SERVER: &str = "stun:stun.l.google.com:19302";
|
||||||
|
|
||||||
/// TURN server URLs - not provided, users must configure their own
|
|
||||||
pub const TURN_URLS: &str = "";
|
|
||||||
|
|
||||||
/// TURN authentication username
|
|
||||||
pub const TURN_USERNAME: &str = "";
|
|
||||||
|
|
||||||
/// TURN authentication password
|
|
||||||
pub const TURN_PASSWORD: &str = "";
|
|
||||||
|
|
||||||
/// Always returns true since we have STUN
|
|
||||||
pub const fn is_configured() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Always returns false since TURN is not provided
|
|
||||||
pub const fn has_turn() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RustDesk public server configuration - NOT PROVIDED
|
/// RustDesk public server configuration - NOT PROVIDED
|
||||||
|
|||||||
117
build/build-android.sh
Normal file
117
build/build-android.sh
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#!/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
|
||||||
@@ -19,6 +19,23 @@ ARCH_MAP=(
|
|||||||
build_arch() {
|
build_arch() {
|
||||||
local rust_target="$1"
|
local rust_target="$1"
|
||||||
|
|
||||||
|
case "${CHINAMIRRO:-}" in
|
||||||
|
1|true|TRUE|yes|YES|on|ON)
|
||||||
|
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--build-arg CHINAMIRRO=1"
|
||||||
|
echo "=== China mirror acceleration: enabled (Tsinghua) ==="
|
||||||
|
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
||||||
|
env \
|
||||||
|
CROSS_BUILD_OPTS="$cross_build_opts" \
|
||||||
|
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=tuna \
|
||||||
|
CARGO_SOURCE_TUNA_REGISTRY=sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/ \
|
||||||
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
|
RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup \
|
||||||
|
RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup \
|
||||||
|
cross build --release --target "$rust_target"
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
||||||
cross build --release --target "$rust_target"
|
cross build --release --target "$rust_target"
|
||||||
}
|
}
|
||||||
@@ -49,6 +66,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"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
309
build/cross/Dockerfile.android
Normal file
309
build/cross/Dockerfile.android
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# 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"]
|
||||||
@@ -6,16 +6,36 @@ FROM debian:11
|
|||||||
# Linux headers used by v4l2r bindgen
|
# Linux headers used by v4l2r bindgen
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
ARG LINUX_HEADERS_VERSION=6.6
|
||||||
ARG LINUX_HEADERS_SHA256=
|
ARG LINUX_HEADERS_SHA256=
|
||||||
|
ARG CHINAMIRRO=0
|
||||||
|
|
||||||
# Set Rustup mirrors (Aliyun)
|
# Optionally use Tsinghua mirrors for builds in China.
|
||||||
#ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
# RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup
|
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
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
|
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||||
|
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||||
|
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 \
|
||||||
|
mkdir -p /root/.cargo; \
|
||||||
|
printf '%s\n' \
|
||||||
|
'[source.crates-io]' \
|
||||||
|
"replace-with = 'tuna'" \
|
||||||
|
'[source.tuna]' \
|
||||||
|
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||||
|
'[registries.tuna]' \
|
||||||
|
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||||
|
> /root/.cargo/config.toml; \
|
||||||
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
@@ -75,6 +95,7 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& 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 \
|
||||||
|
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||||
-DCMAKE_SYSTEM_NAME=Linux \
|
-DCMAKE_SYSTEM_NAME=Linux \
|
||||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||||
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
|
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
|
||||||
@@ -93,6 +114,11 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
|||||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||||
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
|
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
|
||||||
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
|
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
|
||||||
|
-DJPEG_FOUND=TRUE \
|
||||||
|
-DJPEG_INCLUDE_DIR=/usr/aarch64-linux-gnu/include \
|
||||||
|
-DJPEG_LIBRARY=/usr/aarch64-linux-gnu/lib/libjpeg.a \
|
||||||
|
-DCMAKE_C_FLAGS="-DHAVE_JPEG -I/usr/aarch64-linux-gnu/include" \
|
||||||
|
-DCMAKE_CXX_FLAGS="-DHAVE_JPEG -I/usr/aarch64-linux-gnu/include" \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/libyuv
|
&& rm -rf /tmp/libyuv
|
||||||
@@ -327,7 +353,11 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& rm -rf /tmp/ffmpeg-build /tmp/aarch64-cross.txt /tmp/aarch64-pkg-config
|
&& rm -rf /tmp/ffmpeg-build /tmp/aarch64-cross.txt /tmp/aarch64-pkg-config
|
||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN rustup target add aarch64-unknown-linux-gnu
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
|
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||||
|
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||||
|
fi \
|
||||||
|
&& rustup target add aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
# Configure environment for cross-compilation
|
# Configure environment for cross-compilation
|
||||||
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
||||||
|
|||||||
@@ -6,16 +6,36 @@ FROM debian:11
|
|||||||
# Linux headers used by v4l2r bindgen
|
# Linux headers used by v4l2r bindgen
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
ARG LINUX_HEADERS_VERSION=6.6
|
||||||
ARG LINUX_HEADERS_SHA256=
|
ARG LINUX_HEADERS_SHA256=
|
||||||
|
ARG CHINAMIRRO=0
|
||||||
|
|
||||||
# Set Rustup mirrors (Aliyun)
|
# Optionally use Tsinghua mirrors for builds in China.
|
||||||
#ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
# RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup
|
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
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
|
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||||
|
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||||
|
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 \
|
||||||
|
mkdir -p /root/.cargo; \
|
||||||
|
printf '%s\n' \
|
||||||
|
'[source.crates-io]' \
|
||||||
|
"replace-with = 'tuna'" \
|
||||||
|
'[source.tuna]' \
|
||||||
|
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||||
|
'[registries.tuna]' \
|
||||||
|
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||||
|
> /root/.cargo/config.toml; \
|
||||||
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
@@ -74,6 +94,7 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& 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 \
|
||||||
|
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||||
-DCMAKE_SYSTEM_NAME=Linux \
|
-DCMAKE_SYSTEM_NAME=Linux \
|
||||||
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||||
-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
|
-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
|
||||||
@@ -92,6 +113,11 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
|||||||
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||||
-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
|
-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
|
||||||
-DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++ \
|
-DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++ \
|
||||||
|
-DJPEG_FOUND=TRUE \
|
||||||
|
-DJPEG_INCLUDE_DIR=/usr/arm-linux-gnueabihf/include \
|
||||||
|
-DJPEG_LIBRARY=/usr/arm-linux-gnueabihf/lib/libjpeg.a \
|
||||||
|
-DCMAKE_C_FLAGS="-DHAVE_JPEG -I/usr/arm-linux-gnueabihf/include" \
|
||||||
|
-DCMAKE_CXX_FLAGS="-DHAVE_JPEG -I/usr/arm-linux-gnueabihf/include" \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/libyuv
|
&& rm -rf /tmp/libyuv
|
||||||
@@ -316,7 +342,11 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& rm -rf /tmp/ffmpeg-build /tmp/armhf-cross.txt /tmp/armhf-pkg-config
|
&& rm -rf /tmp/ffmpeg-build /tmp/armhf-cross.txt /tmp/armhf-pkg-config
|
||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
|
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||||
|
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||||
|
fi \
|
||||||
|
&& rustup target add armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
# Configure environment for cross-compilation
|
# Configure environment for cross-compilation
|
||||||
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
|
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
|
||||||
|
|||||||
@@ -6,16 +6,36 @@ FROM debian:11
|
|||||||
# Linux headers used by v4l2r bindgen
|
# Linux headers used by v4l2r bindgen
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
ARG LINUX_HEADERS_VERSION=6.6
|
||||||
ARG LINUX_HEADERS_SHA256=
|
ARG LINUX_HEADERS_SHA256=
|
||||||
|
ARG CHINAMIRRO=0
|
||||||
|
|
||||||
# Set Rustup mirrors (Aliyun)
|
# Optionally use Tsinghua mirrors for builds in China.
|
||||||
#ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
# RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup
|
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
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
|
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||||
|
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||||
|
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 \
|
||||||
|
mkdir -p /root/.cargo; \
|
||||||
|
printf '%s\n' \
|
||||||
|
'[source.crates-io]' \
|
||||||
|
"replace-with = 'tuna'" \
|
||||||
|
'[source.tuna]' \
|
||||||
|
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||||
|
'[registries.tuna]' \
|
||||||
|
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
||||||
|
> /root/.cargo/config.toml; \
|
||||||
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
@@ -72,6 +92,8 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& cd /tmp/libjpeg-turbo \
|
&& cd /tmp/libjpeg-turbo \
|
||||||
&& 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_LIBDIR=lib \
|
||||||
-DENABLE_SHARED=OFF -DENABLE_STATIC=ON \
|
-DENABLE_SHARED=OFF -DENABLE_STATIC=ON \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
@@ -82,6 +104,11 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
|||||||
&& cd /tmp/libyuv \
|
&& cd /tmp/libyuv \
|
||||||
&& 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_INCLUDE_DIR=/usr/local/include \
|
||||||
|
-DJPEG_LIBRARY=/usr/local/lib/libjpeg.a \
|
||||||
|
-DCMAKE_C_FLAGS="-DHAVE_JPEG -I/usr/local/include" \
|
||||||
|
-DCMAKE_CXX_FLAGS="-DHAVE_JPEG -I/usr/local/include" \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/libyuv
|
&& rm -rf /tmp/libyuv
|
||||||
@@ -221,7 +248,11 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& rm -rf /tmp/ffmpeg-build
|
&& rm -rf /tmp/ffmpeg-build
|
||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN rustup target add x86_64-unknown-linux-gnu
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
|
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
||||||
|
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
||||||
|
fi \
|
||||||
|
&& rustup target add x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
# Configure environment for static linking
|
# Configure environment for static linking
|
||||||
ENV PKG_CONFIG_ALLOW_CROSS=1\
|
ENV PKG_CONFIG_ALLOW_CROSS=1\
|
||||||
|
|||||||
87
build/windows/build.ps1
Normal file
87
build/windows/build.ps1
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
param(
|
||||||
|
[string]$Configuration = "debug",
|
||||||
|
[string]$Target = "x86_64-pc-windows-msvc",
|
||||||
|
[string]$Triplet = "x64-windows-static",
|
||||||
|
[string]$VcpkgRoot = $env:VCPKG_ROOT,
|
||||||
|
[string]$VcpkgInstalledRoot = $env:VCPKG_INSTALLED_DIR,
|
||||||
|
[switch]$NoDefaultFeatures,
|
||||||
|
[string[]]$Features = @(),
|
||||||
|
[switch]$Package,
|
||||||
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
|
[string[]]$CargoArgs = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
||||||
|
Set-Location $repoRoot
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($VcpkgRoot)) {
|
||||||
|
$VcpkgRoot = Join-Path (Split-Path $repoRoot -Parent) "vcpkg"
|
||||||
|
}
|
||||||
|
|
||||||
|
$VcpkgRoot = [System.IO.Path]::GetFullPath($VcpkgRoot)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($VcpkgInstalledRoot)) {
|
||||||
|
$VcpkgInstalledRoot = Join-Path $VcpkgRoot "installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
$VcpkgInstalledRoot = [System.IO.Path]::GetFullPath($VcpkgInstalledRoot)
|
||||||
|
$vcpkgTripletRoot = Join-Path $VcpkgInstalledRoot $Triplet
|
||||||
|
$turbojpegLibDir = Join-Path $vcpkgTripletRoot "lib"
|
||||||
|
$turbojpegIncludeDir = Join-Path $vcpkgTripletRoot "include"
|
||||||
|
|
||||||
|
if (-not (Test-Path $VcpkgRoot)) {
|
||||||
|
throw "VCPKG_ROOT does not exist: $VcpkgRoot. Run build/windows/bootstrap-vcpkg.ps1 first."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $turbojpegLibDir) -or -not (Test-Path $turbojpegIncludeDir)) {
|
||||||
|
throw "vcpkg triplet is not installed at $vcpkgTripletRoot. Run build/windows/bootstrap-vcpkg.ps1 first."
|
||||||
|
}
|
||||||
|
|
||||||
|
$env:VCPKG_ROOT = $VcpkgRoot
|
||||||
|
$env:VCPKG_DEFAULT_TRIPLET = $Triplet
|
||||||
|
$env:VCPKG_INSTALLED_DIR = $VcpkgInstalledRoot
|
||||||
|
$env:TURBOJPEG_SOURCE = "explicit"
|
||||||
|
$env:TURBOJPEG_LIB_DIR = $turbojpegLibDir
|
||||||
|
$env:TURBOJPEG_INCLUDE_DIR = $turbojpegIncludeDir
|
||||||
|
|
||||||
|
$cargoCommand = @("build", "--target", $Target)
|
||||||
|
|
||||||
|
if ($Configuration -eq "release") {
|
||||||
|
$cargoCommand += "--release"
|
||||||
|
} elseif ($Configuration -ne "debug") {
|
||||||
|
throw "Unsupported configuration '$Configuration'. Use 'debug' or 'release'."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($NoDefaultFeatures) {
|
||||||
|
$cargoCommand += "--no-default-features"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Features.Count -gt 0) {
|
||||||
|
$cargoCommand += "--features"
|
||||||
|
$cargoCommand += ($Features -join ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
$cargoCommand += $CargoArgs
|
||||||
|
|
||||||
|
cargo @cargoCommand
|
||||||
|
|
||||||
|
if ($Package) {
|
||||||
|
$metadata = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json
|
||||||
|
$packageInfo = $metadata.packages | Where-Object { $_.name -eq "one-kvm" } | Select-Object -First 1
|
||||||
|
|
||||||
|
if ($null -eq $packageInfo -or [string]::IsNullOrWhiteSpace($packageInfo.version)) {
|
||||||
|
throw "Failed to resolve version from Cargo metadata"
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourcePath = Join-Path $repoRoot "target/$Target/release/one-kvm.exe"
|
||||||
|
$targetName = "one-kvm_{0}_amd64.exe" -f $packageInfo.version
|
||||||
|
$targetPath = Join-Path $repoRoot "target/$Target/release/$targetName"
|
||||||
|
|
||||||
|
if (-not (Test-Path $sourcePath)) {
|
||||||
|
throw "Windows binary not found: $sourcePath"
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item $sourcePath $targetPath
|
||||||
|
Write-Host $targetPath
|
||||||
|
}
|
||||||
@@ -4,19 +4,21 @@ version = "0.8.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Hardware video codec for IP-KVM (Windows/Linux)"
|
description = "Hardware video codec for IP-KVM (Windows/Linux)"
|
||||||
|
|
||||||
|
[package.metadata.cargo-machete]
|
||||||
|
ignored = ["serde"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
bytes = ["dep:bytes"]
|
||||||
rkmpp = []
|
rkmpp = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
bytes = { version = "1", optional = true }
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cc = "1.0"
|
cc = "1.0"
|
||||||
bindgen = "0.59"
|
bindgen = "0.70.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
env_logger = "0.10"
|
|
||||||
|
|||||||
@@ -21,11 +21,16 @@ fn build_common(builder: &mut Build) {
|
|||||||
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");
|
||||||
|
|
||||||
bindgen::builder()
|
let mut 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
|
||||||
.generate()
|
.generate()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("common_ffi.rs"))
|
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("common_ffi.rs"))
|
||||||
@@ -34,7 +39,9 @@ fn build_common(builder: &mut Build) {
|
|||||||
// system
|
// system
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
["d3d11", "dxgi"].map(|lib| println!("cargo:rustc-link-lib={}", lib));
|
for lib in ["d3d11", "dxgi"] {
|
||||||
|
println!("cargo:rustc-link-lib={}", lib);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.include(&common_dir);
|
builder.include(&common_dir);
|
||||||
@@ -55,9 +62,9 @@ fn build_common(builder: &mut Build) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unsupported platforms
|
// Unsupported platforms
|
||||||
if target_os != "windows" && target_os != "linux" {
|
if target_os != "windows" && target_os != "linux" && target_os != "android" {
|
||||||
panic!(
|
panic!(
|
||||||
"Unsupported OS: {}. Only Windows and Linux are supported.",
|
"Unsupported OS: {}. Only Windows, Linux, and Android are supported.",
|
||||||
target_os
|
target_os
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,9 +76,9 @@ fn build_common(builder: &mut Build) {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct CommonCallbacks;
|
struct CommonCallbacks;
|
||||||
impl bindgen::callbacks::ParseCallbacks for CommonCallbacks {
|
impl bindgen::callbacks::ParseCallbacks for CommonCallbacks {
|
||||||
fn add_derives(&self, name: &str) -> Vec<String> {
|
fn add_derives(&self, info: &bindgen::callbacks::DeriveInfo<'_>) -> Vec<String> {
|
||||||
let names = vec!["DataFormat", "SurfaceFormat", "API"];
|
let names = vec!["DataFormat", "SurfaceFormat", "API"];
|
||||||
if names.contains(&name) {
|
if names.contains(&info.name) {
|
||||||
vec!["Serialize", "Deserialize"]
|
vec!["Serialize", "Deserialize"]
|
||||||
.drain(..)
|
.drain(..)
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
@@ -82,15 +89,126 @@ 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 Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") {
|
if let Some(vcpkg_installed) = vcpkg_installed_root() {
|
||||||
link_vcpkg(builder, vcpkg_root.into());
|
link_vcpkg(builder, vcpkg_installed);
|
||||||
} else {
|
} else {
|
||||||
// Use system FFmpeg via pkg-config
|
// Use system FFmpeg via pkg-config
|
||||||
link_system_ffmpeg(builder);
|
link_system_ffmpeg(builder);
|
||||||
@@ -99,6 +217,84 @@ mod ffmpeg {
|
|||||||
link_os();
|
link_os();
|
||||||
build_ffmpeg_ram(builder);
|
build_ffmpeg_ram(builder);
|
||||||
build_ffmpeg_hw(builder);
|
build_ffmpeg_hw(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> {
|
||||||
|
println!("cargo:rerun-if-env-changed=VCPKG_INSTALLED_DIR");
|
||||||
|
println!("cargo:rerun-if-env-changed=VCPKG_ROOT");
|
||||||
|
|
||||||
|
if let Ok(path) = std::env::var("VCPKG_INSTALLED_DIR") {
|
||||||
|
if !path.trim().is_empty() {
|
||||||
|
return Some(PathBuf::from(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::env::var("VCPKG_ROOT")
|
||||||
|
.ok()
|
||||||
|
.filter(|path| !path.trim().is_empty())
|
||||||
|
.map(|path| PathBuf::from(path).join("installed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Link system FFmpeg using pkg-config or custom path
|
/// Link system FFmpeg using pkg-config or custom path
|
||||||
@@ -271,7 +467,6 @@ mod ffmpeg {
|
|||||||
target = target.replace("x64", "x86");
|
target = target.replace("x64", "x86");
|
||||||
}
|
}
|
||||||
println!("cargo:info={}", target);
|
println!("cargo:info={}", target);
|
||||||
path.push("installed");
|
|
||||||
path.push(target);
|
path.push(target);
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
@@ -282,15 +477,26 @@ mod ffmpeg {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
// Only need avcodec and avutil for encoding
|
// avdevice/avformat are needed by the Windows DirectShow capture bridge.
|
||||||
let mut static_libs = vec!["avcodec", "avutil"];
|
let mut static_libs = vec!["avcodec", "avutil"];
|
||||||
if target_os == "windows" {
|
if target_os == "windows" {
|
||||||
static_libs.push("libmfx");
|
static_libs.extend([
|
||||||
|
"avformat",
|
||||||
|
"avdevice",
|
||||||
|
"avfilter",
|
||||||
|
"swresample",
|
||||||
|
"swscale",
|
||||||
|
"vpx",
|
||||||
|
"libx264",
|
||||||
|
"x265-static",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
for lib in static_libs {
|
||||||
|
println!("cargo:rustc-link-lib=static={}", lib);
|
||||||
|
}
|
||||||
|
if target_os == "windows" {
|
||||||
|
link_windows_qsv_lib(&path.join("lib"));
|
||||||
}
|
}
|
||||||
static_libs
|
|
||||||
.iter()
|
|
||||||
.map(|lib| println!("cargo:rustc-link-lib=static={}", lib))
|
|
||||||
.count();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let include = path.join("include");
|
let include = path.join("include");
|
||||||
@@ -299,12 +505,28 @@ mod ffmpeg {
|
|||||||
include
|
include
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn link_windows_qsv_lib(lib_dir: &Path) {
|
||||||
|
if lib_dir.join("libmfx.lib").exists() {
|
||||||
|
println!("cargo:rustc-link-lib=static=libmfx");
|
||||||
|
println!("cargo:info=Using Windows QSV support library libmfx.lib");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:warning=Windows QSV support library not found in {}",
|
||||||
|
lib_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn link_os() {
|
fn link_os() {
|
||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
||||||
|
|
||||||
let dyn_libs: Vec<&str> = if target_os == "windows" {
|
let dyn_libs: Vec<&str> = if target_os == "windows" {
|
||||||
["User32", "bcrypt", "ole32", "advapi32"].to_vec()
|
[
|
||||||
|
"User32", "bcrypt", "ole32", "advapi32", "mfuuid", "strmiids",
|
||||||
|
]
|
||||||
|
.to_vec()
|
||||||
} else if target_os == "linux" {
|
} else if target_os == "linux" {
|
||||||
// Base libraries for all Linux platforms
|
// Base libraries for all Linux platforms
|
||||||
let mut v = vec!["drm", "stdc++"];
|
let mut v = vec!["drm", "stdc++"];
|
||||||
@@ -316,9 +538,11 @@ 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 and Linux are supported.",
|
"Unsupported OS: {}. Only Windows, Linux, and Android are supported.",
|
||||||
target_os
|
target_os
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -334,9 +558,14 @@ mod ffmpeg {
|
|||||||
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();
|
||||||
bindgen::builder()
|
let mut bindings = bindgen::builder()
|
||||||
.header(ffi_header)
|
.header(ffi_header)
|
||||||
.rustified_enum("*")
|
.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
|
||||||
.generate()
|
.generate()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_ffi.rs"))
|
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_ffi.rs"))
|
||||||
@@ -350,9 +579,14 @@ mod ffmpeg {
|
|||||||
.join("ffmpeg_ram_ffi.h")
|
.join("ffmpeg_ram_ffi.h")
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
bindgen::builder()
|
let mut bindings = bindgen::builder()
|
||||||
.header(ffi_header)
|
.header(ffi_header)
|
||||||
.rustified_enum("*")
|
.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
|
||||||
.generate()
|
.generate()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_ram_ffi.rs"))
|
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_ram_ffi.rs"))
|
||||||
@@ -363,7 +597,9 @@ 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 enable_rkmpp = matches!(target_arch.as_str(), "aarch64" | "arm")
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
let enable_rkmpp = target_os != "android"
|
||||||
|
&& matches!(target_arch.as_str(), "aarch64" | "arm")
|
||||||
|| 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"));
|
||||||
@@ -375,6 +611,34 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_ffmpeg_capture(builder: &mut Build) {
|
||||||
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
if target_os != "windows" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let capture_header = manifest_dir
|
||||||
|
.join("cpp")
|
||||||
|
.join("ffmpeg_capture_ffi.h")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
bindgen::builder()
|
||||||
|
.header(capture_header)
|
||||||
|
.rustified_enum(".*")
|
||||||
|
.generate()
|
||||||
|
.unwrap()
|
||||||
|
.write_to_file(
|
||||||
|
Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_capture_ffi.rs"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
builder.file(manifest_dir.join("cpp").join("ffmpeg_capture.cpp"));
|
||||||
|
println!("cargo:rustc-link-lib=strmiids");
|
||||||
|
println!("cargo:rustc-link-lib=oleaut32");
|
||||||
|
println!("cargo:rustc-link-lib=quartz");
|
||||||
|
}
|
||||||
|
|
||||||
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!("CARGO_MANIFEST_DIR"));
|
||||||
let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
|
let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
|
||||||
@@ -384,14 +648,16 @@ mod ffmpeg {
|
|||||||
.to_string();
|
.to_string();
|
||||||
bindgen::builder()
|
bindgen::builder()
|
||||||
.header(ffi_header)
|
.header(ffi_header)
|
||||||
.rustified_enum("*")
|
.rustified_enum(".*")
|
||||||
.generate()
|
.generate()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_hw_ffi.rs"))
|
.write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_hw_ffi.rs"))
|
||||||
.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 enable_rkmpp = matches!(target_arch.as_str(), "aarch64" | "arm")
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
let enable_rkmpp = target_os != "android"
|
||||||
|
&& matches!(target_arch.as_str(), "aarch64" | "arm")
|
||||||
|| 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)
|
||||||
|
|||||||
@@ -122,12 +122,12 @@ int linux_support_v4l2m2m() {
|
|||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::getline(file, *out);
|
std::getline(file, *out, '\0');
|
||||||
return !out->empty();
|
return !out->empty();
|
||||||
};
|
};
|
||||||
|
|
||||||
auto allow_video0_probe = []() -> bool {
|
auto v4l2m2m_allowed = []() -> bool {
|
||||||
const char *env = std::getenv("ONE_KVM_V4L2M2M_ALLOW_VIDEO0");
|
const char *env = std::getenv("ONE_KVM_V4L2M2M_ALLOW");
|
||||||
if (env == nullptr) {
|
if (env == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -137,30 +137,90 @@ int linux_support_v4l2m2m() {
|
|||||||
return std::strcmp(env, "0") != 0;
|
return std::strcmp(env, "0") != 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto is_amlogic_vdec = [&]() -> bool {
|
auto contains_any = [](const std::string &value, const char *const *needles, size_t len) -> bool {
|
||||||
std::string name;
|
for (size_t i = 0; i < len; i++) {
|
||||||
std::string modalias;
|
if (value.find(needles[i]) != std::string::npos) {
|
||||||
if (read_text_file("/sys/class/video4linux/video0/name", &name)) {
|
|
||||||
const std::string lowered = to_lower(name);
|
|
||||||
if (lowered.find("meson") != std::string::npos ||
|
|
||||||
lowered.find("vdec") != std::string::npos ||
|
|
||||||
lowered.find("decoder") != std::string::npos ||
|
|
||||||
lowered.find("video-decoder") != std::string::npos) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (read_text_file("/sys/class/video4linux/video0/device/modalias", &modalias)) {
|
|
||||||
const std::string lowered = to_lower(modalias);
|
|
||||||
if (lowered.find("amlogic") != std::string::npos ||
|
|
||||||
lowered.find("meson") != std::string::npos ||
|
|
||||||
lowered.find("gxl-vdec") != std::string::npos ||
|
|
||||||
lowered.find("gx-vdec") != std::string::npos) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto is_amlogic_platform = [&]() -> bool {
|
||||||
|
const char *platform_hints[] = {
|
||||||
|
"amlogic",
|
||||||
|
"meson",
|
||||||
|
"gxl",
|
||||||
|
"gxbb",
|
||||||
|
"gxm",
|
||||||
|
"g12a",
|
||||||
|
"g12b",
|
||||||
|
"sm1",
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
"video10",
|
||||||
|
"video11",
|
||||||
|
"video32",
|
||||||
|
};
|
||||||
|
const char *vdec_hints[] = {
|
||||||
|
"meson",
|
||||||
|
"amlogic",
|
||||||
|
"vdec",
|
||||||
|
"decoder",
|
||||||
|
"video-decoder",
|
||||||
|
"gxl-vdec",
|
||||||
|
"gx-vdec",
|
||||||
|
};
|
||||||
|
|
||||||
|
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), vdec_hints, sizeof(vdec_hints) / sizeof(vdec_hints[0]))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (read_text_file((base + "/device/modalias").c_str(), &modalias) &&
|
||||||
|
contains_any(to_lower(modalias), vdec_hints,
|
||||||
|
sizeof(vdec_hints) / sizeof(vdec_hints[0]))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bool amlogic_platform = is_amlogic_platform();
|
||||||
|
if (amlogic_platform && !v4l2m2m_allowed()) {
|
||||||
|
LOG_WARN(std::string(
|
||||||
|
"V4L2 M2M: skipped probe on Amlogic platform; set ONE_KVM_V4L2M2M_ALLOW=1 to enable"));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amlogic_platform) {
|
||||||
|
LOG_WARN(std::string("V4L2 M2M: ONE_KVM_V4L2M2M_ALLOW is set; probing Amlogic video nodes"));
|
||||||
|
}
|
||||||
|
|
||||||
// Check common V4L2 M2M device paths used by various ARM SoCs
|
// Check common V4L2 M2M device paths used by various ARM SoCs
|
||||||
// /dev/video10 - Standard on many SoCs
|
// /dev/video10 - Standard on many SoCs
|
||||||
// /dev/video11 - Standard on many SoCs (often decoder)
|
// /dev/video11 - Standard on many SoCs (often decoder)
|
||||||
@@ -179,13 +239,6 @@ int linux_support_v4l2m2m() {
|
|||||||
|
|
||||||
for (size_t i = 0; i < sizeof(m2m_devices) / sizeof(m2m_devices[0]); i++) {
|
for (size_t i = 0; i < sizeof(m2m_devices) / sizeof(m2m_devices[0]); i++) {
|
||||||
if (access(m2m_devices[i], F_OK) == 0) {
|
if (access(m2m_devices[i], F_OK) == 0) {
|
||||||
if (std::strcmp(m2m_devices[i], "/dev/video0") == 0) {
|
|
||||||
if (!allow_video0_probe() && is_amlogic_vdec()) {
|
|
||||||
LOG_TRACE(std::string("V4L2 M2M: Skipping /dev/video0 (Amlogic vdec)"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device exists, check if it's an M2M device by trying to open it
|
// Device exists, check if it's an M2M device by trying to open it
|
||||||
int fd = open(m2m_devices[i], O_RDWR | O_NONBLOCK);
|
int fd = open(m2m_devices[i], O_RDWR | O_NONBLOCK);
|
||||||
if (fd >= 0) {
|
if (fd >= 0) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavutil/opt.h>
|
#include <libavutil/opt.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,13 +100,17 @@ 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;
|
||||||
|
|
||||||
// Profile selection: use BASELINE for software H264 (faster, simpler)
|
// WebRTC SDP advertises constrained baseline. Keep most hardware and software
|
||||||
if (is_software_h264(name)) {
|
// encoders on the same browser-friendly H264 profile. Android MediaCodec is
|
||||||
c->profile = FF_PROFILE_H264_BASELINE; // Simpler profile for real-time
|
// deliberately excluded because older vendor OMX encoders can reject explicit
|
||||||
} else if (name.find("h264") != std::string::npos) {
|
// profile/level combinations during configure().
|
||||||
c->profile = FF_PROFILE_H264_HIGH;
|
if (name.find("mediacodec") != std::string::npos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name.find("h264") != std::string::npos) {
|
||||||
|
c->profile = AV_PROFILE_H264_CONSTRAINED_BASELINE;
|
||||||
} else if (name.find("hevc") != std::string::npos) {
|
} else if (name.find("hevc") != std::string::npos) {
|
||||||
c->profile = FF_PROFILE_HEVC_MAIN;
|
c->profile = AV_PROFILE_HEVC_MAIN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +125,7 @@ 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_ERROR(std::string("amf set_lantency_free failed, ret = ") + av_err2str(ret));
|
LOG_WARN(std::string("amf query_timeout option is unavailable, ret = ") + av_err2str(ret));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (name.find("qsv") != std::string::npos) {
|
if (name.find("qsv") != std::string::npos) {
|
||||||
@@ -306,23 +310,9 @@ bool set_quality(void *priv_data, const std::string &name, int quality) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (name.find("mediacodec") != std::string::npos) {
|
// Do not force MediaCodec level here. Some Android TV vendor encoders,
|
||||||
if (name.find("h264") != std::string::npos) {
|
// including older Amlogic OMX implementations, reject explicit level values
|
||||||
if ((ret = av_opt_set(priv_data, "level", "5.1", 0)) < 0) {
|
// even when they support the requested resolution and bitrate.
|
||||||
LOG_ERROR(std::string("mediacodec set opt level 5.1 failed, ret = ") +
|
|
||||||
av_err2str(ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (name.find("hevc") != std::string::npos) {
|
|
||||||
// https:en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
|
||||||
if ((ret = av_opt_set(priv_data, "level", "h5.1", 0)) < 0) {
|
|
||||||
LOG_ERROR(std::string("mediacodec set opt level h5.1 failed, ret = ") +
|
|
||||||
av_err2str(ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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;
|
||||||
@@ -458,6 +448,13 @@ 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.
|
||||||
|
|||||||
879
libs/hwcodec/cpp/ffmpeg_capture.cpp
Normal file
879
libs/hwcodec/cpp/ffmpeg_capture.cpp
Normal file
@@ -0,0 +1,879 @@
|
|||||||
|
#define NOMINMAX
|
||||||
|
#include "ffmpeg_capture_ffi.h"
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <dshow.h>
|
||||||
|
#include <dvdmedia.h>
|
||||||
|
extern "C" {
|
||||||
|
#include <libavcodec/codec_id.h>
|
||||||
|
#include <libavdevice/avdevice.h>
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
#include <libavutil/avutil.h>
|
||||||
|
#include <libavutil/error.h>
|
||||||
|
#include <libavutil/pixfmt.h>
|
||||||
|
}
|
||||||
|
#include <atomic>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
|
||||||
|
#pragma comment(lib, "strmiids")
|
||||||
|
|
||||||
|
thread_local std::string g_last_error;
|
||||||
|
|
||||||
|
struct HwcodecDshowCaptureContext {
|
||||||
|
AVFormatContext* format_ctx = nullptr;
|
||||||
|
int stream_index = -1;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
int pixel_format = HWCODEC_CAPTURE_FMT_UNKNOWN;
|
||||||
|
int stride = 0;
|
||||||
|
int timeout_ms = 2000;
|
||||||
|
std::atomic<long long> deadline_ms{0};
|
||||||
|
std::atomic<int> timed_out{0};
|
||||||
|
uint64_t sequence = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct DshowCapabilityEntry {
|
||||||
|
std::string format;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
std::vector<int> fps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* requested_pixel_format_name(int requested_format);
|
||||||
|
|
||||||
|
void set_last_error(const std::string& message) {
|
||||||
|
g_last_error = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ffmpeg_error(int errnum) {
|
||||||
|
char buffer[AV_ERROR_MAX_STRING_SIZE] = {0};
|
||||||
|
av_make_error_string(buffer, sizeof(buffer), errnum);
|
||||||
|
return std::string(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
long long now_ms() {
|
||||||
|
return static_cast<long long>(GetTickCount64());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string wide_to_utf8(const wchar_t* value) {
|
||||||
|
if (!value) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
int size = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr);
|
||||||
|
if (size <= 1) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
std::string result(static_cast<size_t>(size - 1), '\0');
|
||||||
|
WideCharToMultiByte(
|
||||||
|
CP_UTF8,
|
||||||
|
0,
|
||||||
|
value,
|
||||||
|
-1,
|
||||||
|
result.empty() ? nullptr : &result[0],
|
||||||
|
size,
|
||||||
|
nullptr,
|
||||||
|
nullptr);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_fps_candidate(std::vector<int>* fps, LONGLONG interval_100ns) {
|
||||||
|
if (!fps || interval_100ns <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double fps_value = 10000000.0 / static_cast<double>(interval_100ns);
|
||||||
|
int rounded = static_cast<int>(fps_value + 0.5);
|
||||||
|
if (rounded <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (std::find(fps->begin(), fps->end(), rounded) == fps->end()) {
|
||||||
|
fps->push_back(rounded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void normalize_fps(std::vector<int>* fps) {
|
||||||
|
if (!fps) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::sort(fps->begin(), fps->end(), std::greater<int>());
|
||||||
|
fps->erase(std::unique(fps->begin(), fps->end()), fps->end());
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* media_subtype_to_format(const GUID& subtype) {
|
||||||
|
if (subtype == MEDIASUBTYPE_MJPG) {
|
||||||
|
return "MJPEG";
|
||||||
|
}
|
||||||
|
if (subtype == MEDIASUBTYPE_YUY2) {
|
||||||
|
return "YUYV";
|
||||||
|
}
|
||||||
|
if (subtype == MEDIASUBTYPE_UYVY) {
|
||||||
|
return "UYVY";
|
||||||
|
}
|
||||||
|
if (subtype == MEDIASUBTYPE_YVYU) {
|
||||||
|
return "YVYU";
|
||||||
|
}
|
||||||
|
if (subtype == MEDIASUBTYPE_NV12) {
|
||||||
|
return "NV12";
|
||||||
|
}
|
||||||
|
if (subtype == MEDIASUBTYPE_RGB24) {
|
||||||
|
return "RGB24";
|
||||||
|
}
|
||||||
|
if (subtype == MEDIASUBTYPE_RGB32) {
|
||||||
|
return "BGR24";
|
||||||
|
}
|
||||||
|
if (subtype == MEDIASUBTYPE_IYUV) {
|
||||||
|
return "YUV420";
|
||||||
|
}
|
||||||
|
if (subtype == MEDIASUBTYPE_YV12) {
|
||||||
|
return "YVU420";
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void free_media_type(AM_MEDIA_TYPE* media_type) {
|
||||||
|
if (!media_type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (media_type->cbFormat != 0) {
|
||||||
|
CoTaskMemFree(media_type->pbFormat);
|
||||||
|
media_type->cbFormat = 0;
|
||||||
|
media_type->pbFormat = nullptr;
|
||||||
|
}
|
||||||
|
if (media_type->pUnk != nullptr) {
|
||||||
|
media_type->pUnk->Release();
|
||||||
|
media_type->pUnk = nullptr;
|
||||||
|
}
|
||||||
|
CoTaskMemFree(media_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fill_capability_entry(
|
||||||
|
const AM_MEDIA_TYPE* media_type,
|
||||||
|
const VIDEO_STREAM_CONFIG_CAPS* caps,
|
||||||
|
DshowCapabilityEntry* out_entry) {
|
||||||
|
if (!media_type || !out_entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* format = media_subtype_to_format(media_type->subtype);
|
||||||
|
if (!format) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LONG width = 0;
|
||||||
|
LONG height = 0;
|
||||||
|
REFERENCE_TIME avg_time_per_frame = 0;
|
||||||
|
|
||||||
|
if (media_type->formattype == FORMAT_VideoInfo && media_type->pbFormat &&
|
||||||
|
media_type->cbFormat >= sizeof(VIDEOINFOHEADER)) {
|
||||||
|
const auto* info = reinterpret_cast<const VIDEOINFOHEADER*>(media_type->pbFormat);
|
||||||
|
width = info->bmiHeader.biWidth;
|
||||||
|
height = std::abs(info->bmiHeader.biHeight);
|
||||||
|
avg_time_per_frame = info->AvgTimePerFrame;
|
||||||
|
} else if (media_type->formattype == FORMAT_VideoInfo2 && media_type->pbFormat &&
|
||||||
|
media_type->cbFormat >= sizeof(VIDEOINFOHEADER2)) {
|
||||||
|
const auto* info = reinterpret_cast<const VIDEOINFOHEADER2*>(media_type->pbFormat);
|
||||||
|
width = info->bmiHeader.biWidth;
|
||||||
|
height = std::abs(info->bmiHeader.biHeight);
|
||||||
|
avg_time_per_frame = info->AvgTimePerFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((width <= 0 || height <= 0) && caps) {
|
||||||
|
width = std::max<LONG>(caps->InputSize.cx, caps->MinOutputSize.cx);
|
||||||
|
height = std::max<LONG>(caps->InputSize.cy, caps->MinOutputSize.cy);
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
width = caps->MaxOutputSize.cx;
|
||||||
|
height = caps->MaxOutputSize.cy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_entry->format = format;
|
||||||
|
out_entry->width = static_cast<int>(width);
|
||||||
|
out_entry->height = static_cast<int>(height);
|
||||||
|
out_entry->fps.clear();
|
||||||
|
|
||||||
|
add_fps_candidate(&out_entry->fps, avg_time_per_frame);
|
||||||
|
if (caps) {
|
||||||
|
add_fps_candidate(&out_entry->fps, caps->MinFrameInterval);
|
||||||
|
add_fps_candidate(&out_entry->fps, caps->MaxFrameInterval);
|
||||||
|
}
|
||||||
|
normalize_fps(&out_entry->fps);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_stream_capabilities(IAMStreamConfig* stream_config, std::vector<DshowCapabilityEntry>* entries) {
|
||||||
|
if (!stream_config || !entries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cap_count = 0;
|
||||||
|
int cap_size = 0;
|
||||||
|
HRESULT hr = stream_config->GetNumberOfCapabilities(&cap_count, &cap_size);
|
||||||
|
if (FAILED(hr) || cap_count <= 0 || cap_size < static_cast<int>(sizeof(VIDEO_STREAM_CONFIG_CAPS))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BYTE> caps_buffer(static_cast<size_t>(cap_size));
|
||||||
|
for (int index = 0; index < cap_count; ++index) {
|
||||||
|
AM_MEDIA_TYPE* media_type = nullptr;
|
||||||
|
hr = stream_config->GetStreamCaps(index, &media_type, caps_buffer.data());
|
||||||
|
if (FAILED(hr) || !media_type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DshowCapabilityEntry entry;
|
||||||
|
const auto* caps = reinterpret_cast<const VIDEO_STREAM_CONFIG_CAPS*>(caps_buffer.data());
|
||||||
|
if (fill_capability_entry(media_type, caps, &entry)) {
|
||||||
|
entries->push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
free_media_type(media_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool find_device_filter(const std::string& device_name, IBaseFilter** out_filter) {
|
||||||
|
if (!out_filter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*out_filter = nullptr;
|
||||||
|
|
||||||
|
ICreateDevEnum* dev_enum = nullptr;
|
||||||
|
IEnumMoniker* enum_moniker = nullptr;
|
||||||
|
HRESULT hr = CoCreateInstance(
|
||||||
|
CLSID_SystemDeviceEnum,
|
||||||
|
nullptr,
|
||||||
|
CLSCTX_INPROC_SERVER,
|
||||||
|
IID_ICreateDevEnum,
|
||||||
|
reinterpret_cast<void**>(&dev_enum));
|
||||||
|
if (FAILED(hr) || !dev_enum) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = dev_enum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enum_moniker, 0);
|
||||||
|
dev_enum->Release();
|
||||||
|
if (hr != S_OK || !enum_moniker) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
IMoniker* moniker = nullptr;
|
||||||
|
ULONG fetched = 0;
|
||||||
|
while (!found && enum_moniker->Next(1, &moniker, &fetched) == S_OK) {
|
||||||
|
IPropertyBag* bag = nullptr;
|
||||||
|
hr = moniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, reinterpret_cast<void**>(&bag));
|
||||||
|
if (SUCCEEDED(hr) && bag) {
|
||||||
|
VARIANT name;
|
||||||
|
VariantInit(&name);
|
||||||
|
if (SUCCEEDED(bag->Read(L"FriendlyName", &name, nullptr)) && name.vt == VT_BSTR) {
|
||||||
|
auto utf8_name = wide_to_utf8(name.bstrVal);
|
||||||
|
if (utf8_name == device_name) {
|
||||||
|
hr = moniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, reinterpret_cast<void**>(out_filter));
|
||||||
|
found = SUCCEEDED(hr) && *out_filter != nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VariantClear(&name);
|
||||||
|
bag->Release();
|
||||||
|
}
|
||||||
|
moniker->Release();
|
||||||
|
}
|
||||||
|
enum_moniker->Release();
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string build_capabilities_payload(const std::vector<DshowCapabilityEntry>& entries) {
|
||||||
|
std::string payload;
|
||||||
|
for (size_t i = 0; i < entries.size(); ++i) {
|
||||||
|
const auto& entry = entries[i];
|
||||||
|
payload += entry.format;
|
||||||
|
payload.push_back('|');
|
||||||
|
payload += std::to_string(entry.width);
|
||||||
|
payload.push_back('|');
|
||||||
|
payload += std::to_string(entry.height);
|
||||||
|
payload.push_back('|');
|
||||||
|
for (size_t fps_index = 0; fps_index < entry.fps.size(); ++fps_index) {
|
||||||
|
payload += std::to_string(entry.fps[fps_index]);
|
||||||
|
if (fps_index + 1 < entry.fps.size()) {
|
||||||
|
payload.push_back(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i + 1 < entries.size()) {
|
||||||
|
payload.push_back('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* copy_payload(const std::string& payload) {
|
||||||
|
char* out = reinterpret_cast<char*>(std::malloc(payload.size() + 1));
|
||||||
|
if (!out) {
|
||||||
|
set_last_error("Failed to allocate capture payload buffer");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
std::memcpy(out, payload.c_str(), payload.size() + 1);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
int open_dshow_input_with_options(
|
||||||
|
AVFormatContext** format_ctx,
|
||||||
|
const AVInputFormat* input,
|
||||||
|
const std::string& device_name,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int fps,
|
||||||
|
int requested_format,
|
||||||
|
bool use_video_size,
|
||||||
|
bool use_framerate,
|
||||||
|
bool use_pixel_format,
|
||||||
|
std::string* attempt_desc) {
|
||||||
|
if (!format_ctx || !input) {
|
||||||
|
return AVERROR(EINVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
AVDictionary* options = nullptr;
|
||||||
|
std::vector<std::string> parts;
|
||||||
|
|
||||||
|
if (use_video_size && width > 0 && height > 0) {
|
||||||
|
std::string video_size = std::to_string(width) + "x" + std::to_string(height);
|
||||||
|
av_dict_set(&options, "video_size", video_size.c_str(), 0);
|
||||||
|
parts.push_back("video_size=" + video_size);
|
||||||
|
}
|
||||||
|
if (use_framerate && fps > 0) {
|
||||||
|
std::string framerate = std::to_string(fps);
|
||||||
|
av_dict_set(&options, "framerate", framerate.c_str(), 0);
|
||||||
|
parts.push_back("framerate=" + framerate);
|
||||||
|
}
|
||||||
|
|
||||||
|
av_dict_set(&options, "rtbufsize", "64M", 0);
|
||||||
|
parts.push_back("rtbufsize=64M");
|
||||||
|
|
||||||
|
const char* pixel_format_name = requested_pixel_format_name(requested_format);
|
||||||
|
if (use_pixel_format && pixel_format_name) {
|
||||||
|
av_dict_set(&options, "pixel_format", pixel_format_name, 0);
|
||||||
|
parts.push_back(std::string("pixel_format=") + pixel_format_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt_desc) {
|
||||||
|
*attempt_desc = parts.empty() ? "default options" : "options{";
|
||||||
|
if (!parts.empty()) {
|
||||||
|
for (size_t i = 0; i < parts.size(); ++i) {
|
||||||
|
if (i > 0) {
|
||||||
|
attempt_desc->append(", ");
|
||||||
|
}
|
||||||
|
attempt_desc->append(parts[i]);
|
||||||
|
}
|
||||||
|
attempt_desc->append("}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string input_name = "video=" + device_name;
|
||||||
|
int ret = avformat_open_input(format_ctx, input_name.c_str(), input, &options);
|
||||||
|
av_dict_free(&options);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScopedComInit {
|
||||||
|
public:
|
||||||
|
ScopedComInit() {
|
||||||
|
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||||
|
initialized_ = hr == S_OK || hr == S_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedComInit() {
|
||||||
|
if (initialized_) {
|
||||||
|
CoUninitialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool initialized_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
int capture_stride(int pixel_format, int width) {
|
||||||
|
switch (pixel_format) {
|
||||||
|
case HWCODEC_CAPTURE_FMT_YUYV:
|
||||||
|
case HWCODEC_CAPTURE_FMT_YVYU:
|
||||||
|
case HWCODEC_CAPTURE_FMT_UYVY:
|
||||||
|
return width * 2;
|
||||||
|
case HWCODEC_CAPTURE_FMT_RGB24:
|
||||||
|
case HWCODEC_CAPTURE_FMT_BGR24:
|
||||||
|
return width * 3;
|
||||||
|
case HWCODEC_CAPTURE_FMT_NV24:
|
||||||
|
return width * 2;
|
||||||
|
case HWCODEC_CAPTURE_FMT_NV12:
|
||||||
|
case HWCODEC_CAPTURE_FMT_NV21:
|
||||||
|
case HWCODEC_CAPTURE_FMT_NV16:
|
||||||
|
case HWCODEC_CAPTURE_FMT_YUV420:
|
||||||
|
case HWCODEC_CAPTURE_FMT_YVU420:
|
||||||
|
case HWCODEC_CAPTURE_FMT_GREY:
|
||||||
|
case HWCODEC_CAPTURE_FMT_MJPEG:
|
||||||
|
case HWCODEC_CAPTURE_FMT_JPEG:
|
||||||
|
default:
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int map_raw_pixfmt(int format) {
|
||||||
|
switch (format) {
|
||||||
|
case AV_PIX_FMT_YUYV422:
|
||||||
|
return HWCODEC_CAPTURE_FMT_YUYV;
|
||||||
|
case AV_PIX_FMT_UYVY422:
|
||||||
|
return HWCODEC_CAPTURE_FMT_UYVY;
|
||||||
|
#ifdef AV_PIX_FMT_YVYU422
|
||||||
|
case AV_PIX_FMT_YVYU422:
|
||||||
|
return HWCODEC_CAPTURE_FMT_YVYU;
|
||||||
|
#endif
|
||||||
|
case AV_PIX_FMT_NV12:
|
||||||
|
return HWCODEC_CAPTURE_FMT_NV12;
|
||||||
|
case AV_PIX_FMT_NV21:
|
||||||
|
return HWCODEC_CAPTURE_FMT_NV21;
|
||||||
|
#ifdef AV_PIX_FMT_NV16
|
||||||
|
case AV_PIX_FMT_NV16:
|
||||||
|
return HWCODEC_CAPTURE_FMT_NV16;
|
||||||
|
#endif
|
||||||
|
#ifdef AV_PIX_FMT_NV24
|
||||||
|
case AV_PIX_FMT_NV24:
|
||||||
|
return HWCODEC_CAPTURE_FMT_NV24;
|
||||||
|
#endif
|
||||||
|
case AV_PIX_FMT_YUV420P:
|
||||||
|
return HWCODEC_CAPTURE_FMT_YUV420;
|
||||||
|
#ifdef AV_PIX_FMT_YVU420P
|
||||||
|
case AV_PIX_FMT_YVU420P:
|
||||||
|
return HWCODEC_CAPTURE_FMT_YVU420;
|
||||||
|
#endif
|
||||||
|
case AV_PIX_FMT_RGB24:
|
||||||
|
return HWCODEC_CAPTURE_FMT_RGB24;
|
||||||
|
case AV_PIX_FMT_BGR24:
|
||||||
|
return HWCODEC_CAPTURE_FMT_BGR24;
|
||||||
|
case AV_PIX_FMT_GRAY8:
|
||||||
|
return HWCODEC_CAPTURE_FMT_GREY;
|
||||||
|
default:
|
||||||
|
return HWCODEC_CAPTURE_FMT_UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int map_codec_to_capture_format(const AVCodecParameters* codecpar) {
|
||||||
|
if (!codecpar) {
|
||||||
|
return HWCODEC_CAPTURE_FMT_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (codecpar->codec_id) {
|
||||||
|
case AV_CODEC_ID_MJPEG:
|
||||||
|
return HWCODEC_CAPTURE_FMT_MJPEG;
|
||||||
|
case AV_CODEC_ID_JPEG2000:
|
||||||
|
return HWCODEC_CAPTURE_FMT_JPEG;
|
||||||
|
case AV_CODEC_ID_RAWVIDEO:
|
||||||
|
return map_raw_pixfmt(codecpar->format);
|
||||||
|
default:
|
||||||
|
return HWCODEC_CAPTURE_FMT_UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int interrupt_callback(void* opaque) {
|
||||||
|
auto* ctx = reinterpret_cast<HwcodecDshowCaptureContext*>(opaque);
|
||||||
|
if (!ctx) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
auto deadline = ctx->deadline_ms.load();
|
||||||
|
if (deadline <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (now_ms() > deadline) {
|
||||||
|
ctx->timed_out.store(1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* requested_pixel_format_name(int requested_format) {
|
||||||
|
switch (requested_format) {
|
||||||
|
case HWCODEC_CAPTURE_FMT_YUYV:
|
||||||
|
return "yuyv422";
|
||||||
|
case HWCODEC_CAPTURE_FMT_UYVY:
|
||||||
|
return "uyvy422";
|
||||||
|
case HWCODEC_CAPTURE_FMT_NV12:
|
||||||
|
return "nv12";
|
||||||
|
case HWCODEC_CAPTURE_FMT_NV21:
|
||||||
|
return "nv21";
|
||||||
|
case HWCODEC_CAPTURE_FMT_RGB24:
|
||||||
|
return "rgb24";
|
||||||
|
case HWCODEC_CAPTURE_FMT_BGR24:
|
||||||
|
return "bgr24";
|
||||||
|
case HWCODEC_CAPTURE_FMT_GREY:
|
||||||
|
return "gray";
|
||||||
|
default:
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" const char* hwcodec_capture_last_error(void) {
|
||||||
|
return g_last_error.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" char* hwcodec_dshow_list_video_devices(void) {
|
||||||
|
ScopedComInit com;
|
||||||
|
|
||||||
|
ICreateDevEnum* dev_enum = nullptr;
|
||||||
|
IEnumMoniker* enum_moniker = nullptr;
|
||||||
|
HRESULT hr = CoCreateInstance(
|
||||||
|
CLSID_SystemDeviceEnum,
|
||||||
|
nullptr,
|
||||||
|
CLSCTX_INPROC_SERVER,
|
||||||
|
IID_ICreateDevEnum,
|
||||||
|
reinterpret_cast<void**>(&dev_enum));
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
set_last_error("Failed to create DirectShow device enumerator");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = dev_enum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enum_moniker, 0);
|
||||||
|
dev_enum->Release();
|
||||||
|
if (hr != S_OK || !enum_moniker) {
|
||||||
|
char* out = reinterpret_cast<char*>(std::malloc(1));
|
||||||
|
if (out) {
|
||||||
|
out[0] = '\0';
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> devices;
|
||||||
|
IMoniker* moniker = nullptr;
|
||||||
|
ULONG fetched = 0;
|
||||||
|
while (enum_moniker->Next(1, &moniker, &fetched) == S_OK) {
|
||||||
|
IPropertyBag* bag = nullptr;
|
||||||
|
hr = moniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, reinterpret_cast<void**>(&bag));
|
||||||
|
if (SUCCEEDED(hr) && bag) {
|
||||||
|
VARIANT name;
|
||||||
|
VariantInit(&name);
|
||||||
|
if (SUCCEEDED(bag->Read(L"FriendlyName", &name, nullptr)) && name.vt == VT_BSTR) {
|
||||||
|
auto utf8_name = wide_to_utf8(name.bstrVal);
|
||||||
|
if (!utf8_name.empty()) {
|
||||||
|
devices.push_back(utf8_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VariantClear(&name);
|
||||||
|
bag->Release();
|
||||||
|
}
|
||||||
|
moniker->Release();
|
||||||
|
}
|
||||||
|
enum_moniker->Release();
|
||||||
|
|
||||||
|
std::string payload;
|
||||||
|
for (size_t i = 0; i < devices.size(); ++i) {
|
||||||
|
payload += devices[i];
|
||||||
|
if (i + 1 < devices.size()) {
|
||||||
|
payload.push_back('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy_payload(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" char* hwcodec_dshow_list_device_capabilities(const char* device_name) {
|
||||||
|
if (!device_name || device_name[0] == '\0') {
|
||||||
|
set_last_error("DirectShow device name is empty");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedComInit com;
|
||||||
|
IBaseFilter* filter = nullptr;
|
||||||
|
if (!find_device_filter(device_name, &filter) || !filter) {
|
||||||
|
set_last_error("Failed to find DirectShow device filter");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DshowCapabilityEntry> entries;
|
||||||
|
IEnumPins* enum_pins = nullptr;
|
||||||
|
HRESULT hr = filter->EnumPins(&enum_pins);
|
||||||
|
if (SUCCEEDED(hr) && enum_pins) {
|
||||||
|
IPin* pin = nullptr;
|
||||||
|
ULONG fetched = 0;
|
||||||
|
while (enum_pins->Next(1, &pin, &fetched) == S_OK) {
|
||||||
|
PIN_DIRECTION direction = PINDIR_INPUT;
|
||||||
|
if (SUCCEEDED(pin->QueryDirection(&direction)) && direction == PINDIR_OUTPUT) {
|
||||||
|
IAMStreamConfig* stream_config = nullptr;
|
||||||
|
if (SUCCEEDED(pin->QueryInterface(IID_IAMStreamConfig, reinterpret_cast<void**>(&stream_config))) &&
|
||||||
|
stream_config) {
|
||||||
|
append_stream_capabilities(stream_config, &entries);
|
||||||
|
stream_config->Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pin->Release();
|
||||||
|
}
|
||||||
|
enum_pins->Release();
|
||||||
|
}
|
||||||
|
filter->Release();
|
||||||
|
|
||||||
|
std::sort(entries.begin(), entries.end(), [](const DshowCapabilityEntry& left, const DshowCapabilityEntry& right) {
|
||||||
|
if (left.format != right.format) {
|
||||||
|
return left.format < right.format;
|
||||||
|
}
|
||||||
|
if (left.width != right.width) {
|
||||||
|
return left.width < right.width;
|
||||||
|
}
|
||||||
|
if (left.height != right.height) {
|
||||||
|
return left.height < right.height;
|
||||||
|
}
|
||||||
|
return left.fps > right.fps;
|
||||||
|
});
|
||||||
|
entries.erase(
|
||||||
|
std::unique(entries.begin(), entries.end(), [](const DshowCapabilityEntry& left, const DshowCapabilityEntry& right) {
|
||||||
|
return left.format == right.format && left.width == right.width && left.height == right.height && left.fps == right.fps;
|
||||||
|
}),
|
||||||
|
entries.end());
|
||||||
|
|
||||||
|
return copy_payload(build_capabilities_payload(entries));
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void hwcodec_capture_string_free(char* ptr) {
|
||||||
|
if (ptr) {
|
||||||
|
std::free(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" HwcodecDshowCaptureContext* hwcodec_dshow_capture_open(
|
||||||
|
const char* device_name,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int fps,
|
||||||
|
int requested_format,
|
||||||
|
int timeout_ms) {
|
||||||
|
if (!device_name || device_name[0] == '\0') {
|
||||||
|
set_last_error("Device name is empty");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
avdevice_register_all();
|
||||||
|
|
||||||
|
const AVInputFormat* input = av_find_input_format("dshow");
|
||||||
|
if (!input) {
|
||||||
|
set_last_error("FFmpeg dshow input format is unavailable");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* ctx = new HwcodecDshowCaptureContext();
|
||||||
|
ctx->timeout_ms = timeout_ms > 0 ? timeout_ms : 2000;
|
||||||
|
ctx->format_ctx = avformat_alloc_context();
|
||||||
|
if (!ctx->format_ctx) {
|
||||||
|
delete ctx;
|
||||||
|
set_last_error("Failed to allocate FFmpeg format context");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
ctx->format_ctx->interrupt_callback.callback = interrupt_callback;
|
||||||
|
ctx->format_ctx->interrupt_callback.opaque = ctx;
|
||||||
|
|
||||||
|
std::string open_attempt;
|
||||||
|
int ret = open_dshow_input_with_options(
|
||||||
|
&ctx->format_ctx,
|
||||||
|
input,
|
||||||
|
device_name,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
requested_format,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
&open_attempt);
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
avformat_free_context(ctx->format_ctx);
|
||||||
|
ctx->format_ctx = avformat_alloc_context();
|
||||||
|
if (!ctx->format_ctx) {
|
||||||
|
delete ctx;
|
||||||
|
set_last_error("Failed to allocate FFmpeg format context for fallback open");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
ctx->format_ctx->interrupt_callback.callback = interrupt_callback;
|
||||||
|
ctx->format_ctx->interrupt_callback.opaque = ctx;
|
||||||
|
|
||||||
|
std::string fallback_attempt;
|
||||||
|
ret = open_dshow_input_with_options(
|
||||||
|
&ctx->format_ctx,
|
||||||
|
input,
|
||||||
|
device_name,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
requested_format,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
&fallback_attempt);
|
||||||
|
if (ret >= 0) {
|
||||||
|
open_attempt = fallback_attempt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
avformat_free_context(ctx->format_ctx);
|
||||||
|
ctx->format_ctx = avformat_alloc_context();
|
||||||
|
if (!ctx->format_ctx) {
|
||||||
|
delete ctx;
|
||||||
|
set_last_error("Failed to allocate FFmpeg format context for final fallback open");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
ctx->format_ctx->interrupt_callback.callback = interrupt_callback;
|
||||||
|
ctx->format_ctx->interrupt_callback.opaque = ctx;
|
||||||
|
|
||||||
|
std::string fallback_attempt;
|
||||||
|
ret = open_dshow_input_with_options(
|
||||||
|
&ctx->format_ctx,
|
||||||
|
input,
|
||||||
|
device_name,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
requested_format,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
&fallback_attempt);
|
||||||
|
if (ret >= 0) {
|
||||||
|
open_attempt = fallback_attempt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
set_last_error("Failed to open dshow input (" + open_attempt + "): " + ffmpeg_error(ret));
|
||||||
|
avformat_free_context(ctx->format_ctx);
|
||||||
|
delete ctx;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = avformat_find_stream_info(ctx->format_ctx, nullptr);
|
||||||
|
if (ret < 0) {
|
||||||
|
set_last_error("Failed to read stream info: " + ffmpeg_error(ret));
|
||||||
|
avformat_close_input(&ctx->format_ctx);
|
||||||
|
delete ctx;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < ctx->format_ctx->nb_streams; ++i) {
|
||||||
|
AVStream* stream = ctx->format_ctx->streams[i];
|
||||||
|
if (stream && stream->codecpar && stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||||
|
ctx->stream_index = static_cast<int>(i);
|
||||||
|
ctx->width = stream->codecpar->width > 0 ? stream->codecpar->width : width;
|
||||||
|
ctx->height = stream->codecpar->height > 0 ? stream->codecpar->height : height;
|
||||||
|
ctx->pixel_format = map_codec_to_capture_format(stream->codecpar);
|
||||||
|
ctx->stride = capture_stride(ctx->pixel_format, ctx->width);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->stream_index < 0) {
|
||||||
|
set_last_error("No video stream found on DirectShow device");
|
||||||
|
avformat_close_input(&ctx->format_ctx);
|
||||||
|
delete ctx;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->pixel_format == HWCODEC_CAPTURE_FMT_UNKNOWN) {
|
||||||
|
set_last_error("DirectShow stream format is unsupported in current Windows backend");
|
||||||
|
avformat_close_input(&ctx->format_ctx);
|
||||||
|
delete ctx;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int hwcodec_dshow_capture_info(
|
||||||
|
HwcodecDshowCaptureContext* ctx,
|
||||||
|
HwcodecCaptureStreamInfo* out_info) {
|
||||||
|
if (!ctx || !out_info) {
|
||||||
|
set_last_error("Invalid capture context");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_info->width = ctx->width;
|
||||||
|
out_info->height = ctx->height;
|
||||||
|
out_info->pixel_format = ctx->pixel_format;
|
||||||
|
out_info->stride = ctx->stride;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int hwcodec_dshow_capture_read(
|
||||||
|
HwcodecDshowCaptureContext* ctx,
|
||||||
|
uint8_t** out_data,
|
||||||
|
int* out_len,
|
||||||
|
uint64_t* out_sequence) {
|
||||||
|
if (!ctx || !out_data || !out_len || !out_sequence) {
|
||||||
|
set_last_error("Invalid capture read arguments");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_data = nullptr;
|
||||||
|
*out_len = 0;
|
||||||
|
*out_sequence = 0;
|
||||||
|
|
||||||
|
AVPacket packet;
|
||||||
|
av_init_packet(&packet);
|
||||||
|
packet.data = nullptr;
|
||||||
|
packet.size = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
ctx->timed_out.store(0);
|
||||||
|
ctx->deadline_ms.store(now_ms() + ctx->timeout_ms);
|
||||||
|
int ret = av_read_frame(ctx->format_ctx, &packet);
|
||||||
|
ctx->deadline_ms.store(0);
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
if (ctx->timed_out.load() != 0) {
|
||||||
|
set_last_error("Timed out waiting for frame");
|
||||||
|
return -110;
|
||||||
|
}
|
||||||
|
set_last_error("Failed to read frame: " + ffmpeg_error(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.stream_index != ctx->stream_index) {
|
||||||
|
av_packet_unref(&packet);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.size <= 0 || !packet.data) {
|
||||||
|
av_packet_unref(&packet);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* buffer = reinterpret_cast<uint8_t*>(std::malloc(static_cast<size_t>(packet.size)));
|
||||||
|
if (!buffer) {
|
||||||
|
av_packet_unref(&packet);
|
||||||
|
set_last_error("Failed to allocate packet buffer");
|
||||||
|
return -12;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(buffer, packet.data, static_cast<size_t>(packet.size));
|
||||||
|
*out_data = buffer;
|
||||||
|
*out_len = packet.size;
|
||||||
|
*out_sequence = ctx->sequence++;
|
||||||
|
av_packet_unref(&packet);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void hwcodec_dshow_capture_packet_free(uint8_t* data) {
|
||||||
|
if (data) {
|
||||||
|
std::free(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void hwcodec_dshow_capture_close(HwcodecDshowCaptureContext* ctx) {
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ctx->format_ctx) {
|
||||||
|
avformat_close_input(&ctx->format_ctx);
|
||||||
|
}
|
||||||
|
delete ctx;
|
||||||
|
}
|
||||||
64
libs/hwcodec/cpp/ffmpeg_capture_ffi.h
Normal file
64
libs/hwcodec/cpp/ffmpeg_capture_ffi.h
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#ifndef HWCODEC_FFMPEG_CAPTURE_FFI_H
|
||||||
|
#define HWCODEC_FFMPEG_CAPTURE_FFI_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct HwcodecDshowCaptureContext HwcodecDshowCaptureContext;
|
||||||
|
|
||||||
|
enum HwcodecCapturePixelFormat {
|
||||||
|
HWCODEC_CAPTURE_FMT_UNKNOWN = 0,
|
||||||
|
HWCODEC_CAPTURE_FMT_MJPEG = 1,
|
||||||
|
HWCODEC_CAPTURE_FMT_JPEG = 2,
|
||||||
|
HWCODEC_CAPTURE_FMT_YUYV = 3,
|
||||||
|
HWCODEC_CAPTURE_FMT_YVYU = 4,
|
||||||
|
HWCODEC_CAPTURE_FMT_UYVY = 5,
|
||||||
|
HWCODEC_CAPTURE_FMT_NV12 = 6,
|
||||||
|
HWCODEC_CAPTURE_FMT_NV21 = 7,
|
||||||
|
HWCODEC_CAPTURE_FMT_NV16 = 8,
|
||||||
|
HWCODEC_CAPTURE_FMT_NV24 = 9,
|
||||||
|
HWCODEC_CAPTURE_FMT_YUV420 = 10,
|
||||||
|
HWCODEC_CAPTURE_FMT_YVU420 = 11,
|
||||||
|
HWCODEC_CAPTURE_FMT_RGB24 = 12,
|
||||||
|
HWCODEC_CAPTURE_FMT_BGR24 = 13,
|
||||||
|
HWCODEC_CAPTURE_FMT_GREY = 14,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct HwcodecCaptureStreamInfo {
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int pixel_format;
|
||||||
|
int stride;
|
||||||
|
} HwcodecCaptureStreamInfo;
|
||||||
|
|
||||||
|
const char* hwcodec_capture_last_error(void);
|
||||||
|
char* hwcodec_dshow_list_video_devices(void);
|
||||||
|
char* hwcodec_dshow_list_device_capabilities(const char* device_name);
|
||||||
|
void hwcodec_capture_string_free(char* ptr);
|
||||||
|
|
||||||
|
HwcodecDshowCaptureContext* hwcodec_dshow_capture_open(
|
||||||
|
const char* device_name,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int fps,
|
||||||
|
int requested_format,
|
||||||
|
int timeout_ms);
|
||||||
|
int hwcodec_dshow_capture_info(
|
||||||
|
HwcodecDshowCaptureContext* ctx,
|
||||||
|
HwcodecCaptureStreamInfo* out_info);
|
||||||
|
int hwcodec_dshow_capture_read(
|
||||||
|
HwcodecDshowCaptureContext* ctx,
|
||||||
|
uint8_t** out_data,
|
||||||
|
int* out_len,
|
||||||
|
uint64_t* out_sequence);
|
||||||
|
void hwcodec_dshow_capture_packet_free(uint8_t* data);
|
||||||
|
void hwcodec_dshow_capture_close(HwcodecDshowCaptureContext* ctx);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -137,6 +137,13 @@ 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;
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ _exit:
|
|||||||
namespace {
|
namespace {
|
||||||
typedef void (*RamEncodeCallback)(const uint8_t *data, int len, int64_t pts,
|
typedef void (*RamEncodeCallback)(const uint8_t *data, int len, int64_t pts,
|
||||||
int key, const void *obj);
|
int key, const void *obj);
|
||||||
|
typedef void (*RamEncodePacketCallback)(void *packet, const uint8_t *data,
|
||||||
|
int len, int64_t pts, int key,
|
||||||
|
const void *obj);
|
||||||
|
|
||||||
class FFmpegRamEncoder {
|
class FFmpegRamEncoder {
|
||||||
public:
|
public:
|
||||||
@@ -134,6 +137,7 @@ public:
|
|||||||
int thread_count_ = 1;
|
int thread_count_ = 1;
|
||||||
int gpu_ = 0;
|
int gpu_ = 0;
|
||||||
RamEncodeCallback callback_ = NULL;
|
RamEncodeCallback callback_ = NULL;
|
||||||
|
RamEncodePacketCallback packet_callback_ = NULL;
|
||||||
int offset_[AV_NUM_DATA_POINTERS] = {0};
|
int offset_[AV_NUM_DATA_POINTERS] = {0};
|
||||||
bool force_keyframe_ = false; // Force next frame to be a keyframe
|
bool force_keyframe_ = false; // Force next frame to be a keyframe
|
||||||
|
|
||||||
@@ -141,6 +145,7 @@ 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, const char *mc_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,
|
||||||
@@ -247,6 +252,11 @@ 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_;
|
||||||
@@ -297,11 +307,19 @@ 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;
|
||||||
}
|
}
|
||||||
if ((ret = fill_frame(frame_, (uint8_t *)data, length, offset_)) != 0)
|
if ((ret = fill_frame(frame_, data, length, offset_)) != 0)
|
||||||
return ret;
|
return ret;
|
||||||
AVFrame *tmp_frame;
|
AVFrame *tmp_frame;
|
||||||
if (hw_device_type_ != AV_HWDEVICE_TYPE_NONE) {
|
if (hw_device_type_ != AV_HWDEVICE_TYPE_NONE) {
|
||||||
@@ -317,6 +335,14 @@ public:
|
|||||||
return do_encode(tmp_frame, obj, ms);
|
return do_encode(tmp_frame, obj, ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int encode_packet(const uint8_t *data, int length, const void *obj,
|
||||||
|
uint64_t ms, RamEncodePacketCallback callback) {
|
||||||
|
packet_callback_ = callback;
|
||||||
|
int ret = encode(data, length, obj, ms);
|
||||||
|
packet_callback_ = NULL;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
void free_encoder() {
|
void free_encoder() {
|
||||||
if (pkt_)
|
if (pkt_)
|
||||||
av_packet_free(&pkt_);
|
av_packet_free(&pkt_);
|
||||||
@@ -324,6 +350,8 @@ 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_)
|
||||||
@@ -376,101 +404,203 @@ private:
|
|||||||
frame->pict_type = AV_PICTURE_TYPE_NONE;
|
frame->pict_type = AV_PICTURE_TYPE_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((ret = avcodec_send_frame(c_, frame)) < 0) {
|
ret = avcodec_send_frame(c_, frame);
|
||||||
|
if (ret == AVERROR(EAGAIN)) {
|
||||||
|
int drain_ret = receive_available_packets(obj, encoded);
|
||||||
|
if (drain_ret < 0) {
|
||||||
|
return drain_ret;
|
||||||
|
}
|
||||||
|
ret = avcodec_send_frame(c_, frame);
|
||||||
|
}
|
||||||
|
if (ret == AVERROR(EAGAIN)) {
|
||||||
|
return encoded ? 0 : AVERROR(EAGAIN);
|
||||||
|
}
|
||||||
|
if (ret < 0) {
|
||||||
LOG_ERROR(std::string("avcodec_send_frame failed, ret = ") + av_err2str(ret));
|
LOG_ERROR(std::string("avcodec_send_frame failed, ret = ") + av_err2str(ret));
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto start = util::now();
|
ret = receive_available_packets(obj, encoded);
|
||||||
while (ret >= 0 && util::elapsed_ms(start) < DECODE_TIMEOUT_MS) {
|
if (ret < 0) {
|
||||||
if ((ret = avcodec_receive_packet(c_, pkt_)) < 0) {
|
return ret;
|
||||||
if (ret != AVERROR(EAGAIN)) {
|
|
||||||
LOG_ERROR(std::string("avcodec_receive_packet failed, ret = ") + av_err2str(ret));
|
|
||||||
}
|
|
||||||
goto _exit;
|
|
||||||
}
|
|
||||||
if (!pkt_->data || !pkt_->size) {
|
|
||||||
LOG_ERROR(std::string("avcodec_receive_packet failed, pkt size is 0"));
|
|
||||||
goto _exit;
|
|
||||||
}
|
|
||||||
encoded = true;
|
|
||||||
callback_(pkt_->data, pkt_->size, pkt_->pts,
|
|
||||||
pkt_->flags & AV_PKT_FLAG_KEY, obj);
|
|
||||||
}
|
}
|
||||||
_exit:
|
|
||||||
av_packet_unref(pkt_);
|
|
||||||
// If no packet is produced for this input frame, treat it as EAGAIN.
|
// If no packet is produced for this input frame, treat it as EAGAIN.
|
||||||
// This is not a fatal error: encoders may buffer internally (e.g., startup delay).
|
// This is not a fatal error: encoders may buffer internally (e.g., startup delay).
|
||||||
return encoded ? 0 : AVERROR(EAGAIN);
|
return encoded ? 0 : AVERROR(EAGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
int fill_frame(AVFrame *frame, uint8_t *data, int data_length,
|
int receive_available_packets(const void *obj, bool &encoded) {
|
||||||
const int *const offset) {
|
int ret = 0;
|
||||||
|
auto start = util::now();
|
||||||
|
|
||||||
|
while (util::elapsed_ms(start) < DECODE_TIMEOUT_MS) {
|
||||||
|
ret = avcodec_receive_packet(c_, pkt_);
|
||||||
|
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (ret < 0) {
|
||||||
|
LOG_ERROR(std::string("avcodec_receive_packet failed, ret = ") + av_err2str(ret));
|
||||||
|
av_packet_unref(pkt_);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
if (!pkt_->data || !pkt_->size) {
|
||||||
|
LOG_WARN(std::string("avcodec_receive_packet returned empty packet"));
|
||||||
|
av_packet_unref(pkt_);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
encoded = true;
|
||||||
|
if (packet_callback_) {
|
||||||
|
AVPacket *owned_pkt = av_packet_clone(pkt_);
|
||||||
|
if (!owned_pkt) {
|
||||||
|
LOG_ERROR("av_packet_clone failed");
|
||||||
|
av_packet_unref(pkt_);
|
||||||
|
return AVERROR(ENOMEM);
|
||||||
|
}
|
||||||
|
packet_callback_(owned_pkt, owned_pkt->data, owned_pkt->size,
|
||||||
|
owned_pkt->pts, owned_pkt->flags & AV_PKT_FLAG_KEY,
|
||||||
|
obj);
|
||||||
|
} else {
|
||||||
|
callback_(pkt_->data, pkt_->size, pkt_->pts,
|
||||||
|
pkt_->flags & AV_PKT_FLAG_KEY, obj);
|
||||||
|
}
|
||||||
|
av_packet_unref(pkt_);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int copy_plane(uint8_t *dst, int dst_stride, const uint8_t *src,
|
||||||
|
int src_stride, int row_bytes, int rows) {
|
||||||
|
if (!dst || !src || dst_stride < row_bytes || src_stride < row_bytes) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (rows <= 0 || row_bytes <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (dst_stride == row_bytes && src_stride == row_bytes) {
|
||||||
|
memcpy(dst, src, static_cast<size_t>(row_bytes) * rows);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (int y = 0; y < rows; y++) {
|
||||||
|
memcpy(dst + y * dst_stride, src + y * src_stride, row_bytes);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fill_frame(AVFrame *frame, const uint8_t *data, int data_length,
|
||||||
|
const int *const) {
|
||||||
|
const int src_y_stride = width_;
|
||||||
|
const int src_packed_stride = width_ * bytes_per_pixel(frame->format);
|
||||||
|
const int src_uv_stride = width_;
|
||||||
|
const int src_y_size = width_ * frame->height;
|
||||||
|
const int src_420_chroma_size = (width_ / 2) * (frame->height / 2);
|
||||||
switch (frame->format) {
|
switch (frame->format) {
|
||||||
case AV_PIX_FMT_NV12:
|
case AV_PIX_FMT_NV12:
|
||||||
case AV_PIX_FMT_NV21:
|
case AV_PIX_FMT_NV21:
|
||||||
if (data_length <
|
if (data_length <
|
||||||
frame->height * (frame->linesize[0] + frame->linesize[1] / 2)) {
|
frame->height * src_y_stride + frame->height / 2 * src_uv_stride) {
|
||||||
LOG_ERROR(std::string("fill_frame: NV12/NV21 data length error. data_length:") +
|
LOG_ERROR(std::string("fill_frame: NV12/NV21 data length error. data_length:") +
|
||||||
std::to_string(data_length) +
|
std::to_string(data_length) +
|
||||||
", linesize[0]:" + std::to_string(frame->linesize[0]) +
|
", width:" + std::to_string(width_) +
|
||||||
", linesize[1]:" + std::to_string(frame->linesize[1]));
|
", height:" + std::to_string(frame->height));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (copy_plane(frame->data[0], frame->linesize[0], data, src_y_stride,
|
||||||
|
width_, frame->height) != 0 ||
|
||||||
|
copy_plane(frame->data[1], frame->linesize[1], data + src_y_size,
|
||||||
|
src_uv_stride, width_, frame->height / 2) != 0) {
|
||||||
|
LOG_ERROR("fill_frame: NV12/NV21 copy failed");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
frame->data[0] = data;
|
|
||||||
frame->data[1] = data + offset[0];
|
|
||||||
break;
|
break;
|
||||||
case AV_PIX_FMT_NV16:
|
case AV_PIX_FMT_NV16:
|
||||||
case AV_PIX_FMT_NV24:
|
|
||||||
if (data_length <
|
if (data_length <
|
||||||
frame->height * (frame->linesize[0] + frame->linesize[1])) {
|
frame->height * src_y_stride + frame->height * src_uv_stride) {
|
||||||
LOG_ERROR(std::string("fill_frame: NV16/NV24 data length error. data_length:") +
|
LOG_ERROR(std::string("fill_frame: NV16 data length error. data_length:") +
|
||||||
std::to_string(data_length) +
|
std::to_string(data_length) +
|
||||||
", linesize[0]:" + std::to_string(frame->linesize[0]) +
|
", width:" + std::to_string(width_) +
|
||||||
", linesize[1]:" + std::to_string(frame->linesize[1]));
|
", height:" + std::to_string(frame->height));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (copy_plane(frame->data[0], frame->linesize[0], data, src_y_stride,
|
||||||
|
width_, frame->height) != 0 ||
|
||||||
|
copy_plane(frame->data[1], frame->linesize[1], data + src_y_size,
|
||||||
|
src_uv_stride, width_, frame->height) != 0) {
|
||||||
|
LOG_ERROR("fill_frame: NV16 copy failed");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
frame->data[0] = data;
|
|
||||||
frame->data[1] = data + offset[0];
|
|
||||||
break;
|
break;
|
||||||
|
case AV_PIX_FMT_NV24: {
|
||||||
|
const int src_nv24_uv_stride = width_ * 2;
|
||||||
|
if (data_length <
|
||||||
|
frame->height * src_y_stride + frame->height * src_nv24_uv_stride) {
|
||||||
|
LOG_ERROR(std::string("fill_frame: NV24 data length error. data_length:") +
|
||||||
|
std::to_string(data_length) +
|
||||||
|
", width:" + std::to_string(width_) +
|
||||||
|
", height:" + std::to_string(frame->height));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (copy_plane(frame->data[0], frame->linesize[0], data, src_y_stride,
|
||||||
|
width_, frame->height) != 0 ||
|
||||||
|
copy_plane(frame->data[1], frame->linesize[1], data + src_y_size,
|
||||||
|
src_nv24_uv_stride, width_ * 2, frame->height) != 0) {
|
||||||
|
LOG_ERROR("fill_frame: NV24 copy failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case AV_PIX_FMT_YUV420P:
|
case AV_PIX_FMT_YUV420P:
|
||||||
if (data_length <
|
if (data_length <
|
||||||
frame->height * (frame->linesize[0] + frame->linesize[1] / 2 +
|
width_ * frame->height + (width_ / 2) * (frame->height / 2) * 2) {
|
||||||
frame->linesize[2] / 2)) {
|
|
||||||
LOG_ERROR(std::string("fill_frame: 420P data length error. data_length:") +
|
LOG_ERROR(std::string("fill_frame: 420P data length error. data_length:") +
|
||||||
std::to_string(data_length) +
|
std::to_string(data_length) +
|
||||||
", linesize[0]:" + std::to_string(frame->linesize[0]) +
|
", width:" + std::to_string(width_) +
|
||||||
", linesize[1]:" + std::to_string(frame->linesize[1]) +
|
", height:" + std::to_string(frame->height));
|
||||||
", linesize[2]:" + std::to_string(frame->linesize[2]));
|
return -1;
|
||||||
|
}
|
||||||
|
if (copy_plane(frame->data[0], frame->linesize[0], data, width_,
|
||||||
|
width_, frame->height) != 0 ||
|
||||||
|
copy_plane(frame->data[1], frame->linesize[1], data + src_y_size,
|
||||||
|
width_ / 2, width_ / 2, frame->height / 2) != 0 ||
|
||||||
|
copy_plane(frame->data[2], frame->linesize[2],
|
||||||
|
data + src_y_size + src_420_chroma_size,
|
||||||
|
width_ / 2, width_ / 2, frame->height / 2) != 0) {
|
||||||
|
LOG_ERROR("fill_frame: 420P copy failed");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
frame->data[0] = data;
|
|
||||||
frame->data[1] = data + offset[0];
|
|
||||||
frame->data[2] = data + offset[1];
|
|
||||||
break;
|
break;
|
||||||
case AV_PIX_FMT_YUYV422:
|
case AV_PIX_FMT_YUYV422:
|
||||||
case AV_PIX_FMT_YVYU422:
|
case AV_PIX_FMT_YVYU422:
|
||||||
case AV_PIX_FMT_UYVY422:
|
case AV_PIX_FMT_UYVY422:
|
||||||
// Packed YUV 4:2:2 formats: single plane, linesize[0] = width * 2
|
// Packed YUV 4:2:2 formats: single plane, linesize[0] = width * 2
|
||||||
if (data_length < frame->height * frame->linesize[0]) {
|
if (data_length < frame->height * src_packed_stride) {
|
||||||
LOG_ERROR(std::string("fill_frame: YUYV422 data length error. data_length:") +
|
LOG_ERROR(std::string("fill_frame: YUYV422 data length error. data_length:") +
|
||||||
std::to_string(data_length) +
|
std::to_string(data_length) +
|
||||||
", linesize[0]:" + std::to_string(frame->linesize[0]) +
|
", stride:" + std::to_string(src_packed_stride) +
|
||||||
", height:" + std::to_string(frame->height));
|
", height:" + std::to_string(frame->height));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
frame->data[0] = data;
|
if (copy_plane(frame->data[0], frame->linesize[0], data,
|
||||||
|
src_packed_stride, src_packed_stride, frame->height) != 0) {
|
||||||
|
LOG_ERROR("fill_frame: YUYV422 copy failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case AV_PIX_FMT_RGB24:
|
case AV_PIX_FMT_RGB24:
|
||||||
case AV_PIX_FMT_BGR24:
|
case AV_PIX_FMT_BGR24:
|
||||||
if (data_length < frame->height * frame->linesize[0]) {
|
if (data_length < frame->height * src_packed_stride) {
|
||||||
LOG_ERROR(std::string("fill_frame: RGB24/BGR24 data length error. data_length:") +
|
LOG_ERROR(std::string("fill_frame: RGB24/BGR24 data length error. data_length:") +
|
||||||
std::to_string(data_length) +
|
std::to_string(data_length) +
|
||||||
", linesize[0]:" + std::to_string(frame->linesize[0]) +
|
", stride:" + std::to_string(src_packed_stride) +
|
||||||
", height:" + std::to_string(frame->height));
|
", height:" + std::to_string(frame->height));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
frame->data[0] = data;
|
if (copy_plane(frame->data[0], frame->linesize[0], data,
|
||||||
|
src_packed_stride, src_packed_stride, frame->height) != 0) {
|
||||||
|
LOG_ERROR("fill_frame: RGB24/BGR24 copy failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LOG_ERROR(std::string("fill_frame: unsupported format, ") +
|
LOG_ERROR(std::string("fill_frame: unsupported format, ") +
|
||||||
@@ -479,6 +609,79 @@ 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) {
|
||||||
|
switch (pix_fmt) {
|
||||||
|
case AV_PIX_FMT_YUYV422:
|
||||||
|
case AV_PIX_FMT_YVYU422:
|
||||||
|
case AV_PIX_FMT_UYVY422:
|
||||||
|
return 2;
|
||||||
|
case AV_PIX_FMT_RGB24:
|
||||||
|
case AV_PIX_FMT_BGR24:
|
||||||
|
return 3;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -532,6 +735,25 @@ extern "C" void ffmpeg_ram_free_encoder(FFmpegRamEncoder *encoder) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" int ffmpeg_ram_encode_packet(FFmpegRamEncoder *encoder,
|
||||||
|
const uint8_t *data, int length,
|
||||||
|
const void *obj, uint64_t ms,
|
||||||
|
RamEncodePacketCallback callback) {
|
||||||
|
try {
|
||||||
|
return encoder->encode_packet(data, length, obj, ms, callback);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
LOG_ERROR(std::string("encode_packet failed, ") + std::string(e.what()));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void ffmpeg_ram_free_packet(void *packet) {
|
||||||
|
AVPacket *pkt = reinterpret_cast<AVPacket *>(packet);
|
||||||
|
if (pkt) {
|
||||||
|
av_packet_free(&pkt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" int ffmpeg_ram_set_bitrate(FFmpegRamEncoder *encoder, int kbs) {
|
extern "C" int ffmpeg_ram_set_bitrate(FFmpegRamEncoder *encoder, int kbs) {
|
||||||
try {
|
try {
|
||||||
return encoder->set_bitrate(kbs);
|
return encoder->set_bitrate(kbs);
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
typedef void (*RamEncodeCallback)(const uint8_t *data, int len, int64_t pts,
|
typedef void (*RamEncodeCallback)(const uint8_t *data, int len, int64_t pts,
|
||||||
int key, const void *obj);
|
int key, const void *obj);
|
||||||
|
typedef void (*RamEncodePacketCallback)(void *packet, const uint8_t *data,
|
||||||
|
int len, int64_t pts, int key,
|
||||||
|
const void *obj);
|
||||||
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);
|
||||||
|
|
||||||
@@ -18,7 +21,11 @@ void *ffmpeg_ram_new_encoder(const char *name, const char *mc_name, int width,
|
|||||||
RamEncodeCallback callback);
|
RamEncodeCallback callback);
|
||||||
int ffmpeg_ram_encode(void *encoder, const uint8_t *data, int length,
|
int ffmpeg_ram_encode(void *encoder, const uint8_t *data, int length,
|
||||||
const void *obj, int64_t ms);
|
const void *obj, int64_t ms);
|
||||||
|
int ffmpeg_ram_encode_packet(void *encoder, const uint8_t *data, int length,
|
||||||
|
const void *obj, int64_t ms,
|
||||||
|
RamEncodePacketCallback callback);
|
||||||
void ffmpeg_ram_free_encoder(void *encoder);
|
void ffmpeg_ram_free_encoder(void *encoder);
|
||||||
|
void ffmpeg_ram_free_packet(void *packet);
|
||||||
int ffmpeg_ram_get_linesize_offset_length(int pix_fmt, int width, int height,
|
int ffmpeg_ram_get_linesize_offset_length(int pix_fmt, int width, int height,
|
||||||
int align, int *linesize, int *offset,
|
int align, int *linesize, int *offset,
|
||||||
int *length);
|
int *length);
|
||||||
|
|||||||
297
libs/hwcodec/src/capture.rs
Normal file
297
libs/hwcodec/src/capture.rs
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
#![allow(non_upper_case_globals)]
|
||||||
|
#![allow(non_camel_case_types)]
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::os::raw::c_int;
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/ffmpeg_capture_ffi.rs"));
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CapturePixelFormat {
|
||||||
|
Unknown,
|
||||||
|
Mjpeg,
|
||||||
|
Jpeg,
|
||||||
|
Yuyv,
|
||||||
|
Yvyu,
|
||||||
|
Uyvy,
|
||||||
|
Nv12,
|
||||||
|
Nv21,
|
||||||
|
Nv16,
|
||||||
|
Nv24,
|
||||||
|
Yuv420,
|
||||||
|
Yvu420,
|
||||||
|
Rgb24,
|
||||||
|
Bgr24,
|
||||||
|
Grey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapturePixelFormat {
|
||||||
|
pub fn to_ffi(self) -> c_int {
|
||||||
|
match self {
|
||||||
|
Self::Unknown => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UNKNOWN as c_int,
|
||||||
|
Self::Mjpeg => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_MJPEG as c_int,
|
||||||
|
Self::Jpeg => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_JPEG as c_int,
|
||||||
|
Self::Yuyv => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUYV as c_int,
|
||||||
|
Self::Yvyu => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVYU as c_int,
|
||||||
|
Self::Uyvy => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UYVY as c_int,
|
||||||
|
Self::Nv12 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV12 as c_int,
|
||||||
|
Self::Nv21 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV21 as c_int,
|
||||||
|
Self::Nv16 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV16 as c_int,
|
||||||
|
Self::Nv24 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV24 as c_int,
|
||||||
|
Self::Yuv420 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUV420 as c_int,
|
||||||
|
Self::Yvu420 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVU420 as c_int,
|
||||||
|
Self::Rgb24 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_RGB24 as c_int,
|
||||||
|
Self::Bgr24 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_BGR24 as c_int,
|
||||||
|
Self::Grey => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_GREY as c_int,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_ffi(value: c_int) -> Self {
|
||||||
|
match value {
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_MJPEG as c_int => Self::Mjpeg,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_JPEG as c_int => Self::Jpeg,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUYV as c_int => Self::Yuyv,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVYU as c_int => Self::Yvyu,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UYVY as c_int => Self::Uyvy,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV12 as c_int => Self::Nv12,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV21 as c_int => Self::Nv21,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV16 as c_int => Self::Nv16,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV24 as c_int => Self::Nv24,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUV420 as c_int => {
|
||||||
|
Self::Yuv420
|
||||||
|
}
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVU420 as c_int => {
|
||||||
|
Self::Yvu420
|
||||||
|
}
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_RGB24 as c_int => Self::Rgb24,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_BGR24 as c_int => Self::Bgr24,
|
||||||
|
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_GREY as c_int => Self::Grey,
|
||||||
|
_ => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_name(name: &str) -> Option<Self> {
|
||||||
|
match name.trim().to_ascii_uppercase().as_str() {
|
||||||
|
"MJPEG" | "MJPG" => Some(Self::Mjpeg),
|
||||||
|
"JPEG" => Some(Self::Jpeg),
|
||||||
|
"YUYV" => Some(Self::Yuyv),
|
||||||
|
"YVYU" => Some(Self::Yvyu),
|
||||||
|
"UYVY" => Some(Self::Uyvy),
|
||||||
|
"NV12" => Some(Self::Nv12),
|
||||||
|
"NV21" => Some(Self::Nv21),
|
||||||
|
"NV16" => Some(Self::Nv16),
|
||||||
|
"NV24" => Some(Self::Nv24),
|
||||||
|
"YUV420" | "I420" | "IYUV" => Some(Self::Yuv420),
|
||||||
|
"YVU420" | "YV12" => Some(Self::Yvu420),
|
||||||
|
"RGB24" => Some(Self::Rgb24),
|
||||||
|
"BGR24" => Some(Self::Bgr24),
|
||||||
|
"GREY" | "GRAY" | "Y800" => Some(Self::Grey),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DshowCapability {
|
||||||
|
pub format: CapturePixelFormat,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub fps: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct CaptureStreamInfo {
|
||||||
|
pub width: i32,
|
||||||
|
pub height: i32,
|
||||||
|
pub pixel_format: CapturePixelFormat,
|
||||||
|
pub stride: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CaptureError {
|
||||||
|
pub code: i32,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for CaptureError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CaptureError {}
|
||||||
|
|
||||||
|
fn last_error_message() -> String {
|
||||||
|
unsafe {
|
||||||
|
let ptr = hwcodec_capture_last_error();
|
||||||
|
if ptr.is_null() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
CStr::from_ptr(ptr).to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_dshow_video_devices() -> Result<Vec<String>, CaptureError> {
|
||||||
|
unsafe {
|
||||||
|
let ptr = hwcodec_dshow_list_video_devices();
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Err(CaptureError {
|
||||||
|
code: -1,
|
||||||
|
message: last_error_message(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let payload = CStr::from_ptr(ptr).to_string_lossy().to_string();
|
||||||
|
hwcodec_capture_string_free(ptr as *mut _);
|
||||||
|
Ok(payload
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_dshow_device_capabilities(device_name: &str) -> Result<Vec<DshowCapability>, CaptureError> {
|
||||||
|
let device_name = CString::new(device_name).map_err(|_| CaptureError {
|
||||||
|
code: -1,
|
||||||
|
message: "device name contains NUL byte".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let ptr = hwcodec_dshow_list_device_capabilities(device_name.as_ptr());
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Err(CaptureError {
|
||||||
|
code: -1,
|
||||||
|
message: last_error_message(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = CStr::from_ptr(ptr).to_string_lossy().to_string();
|
||||||
|
hwcodec_capture_string_free(ptr as *mut _);
|
||||||
|
|
||||||
|
let capabilities = payload
|
||||||
|
.lines()
|
||||||
|
.filter_map(parse_dshow_capability_line)
|
||||||
|
.collect();
|
||||||
|
Ok(capabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_dshow_capability_line(line: &str) -> Option<DshowCapability> {
|
||||||
|
let mut parts = line.split('|');
|
||||||
|
let format = CapturePixelFormat::from_name(parts.next()?.trim())?;
|
||||||
|
let width = parts.next()?.trim().parse::<u32>().ok()?;
|
||||||
|
let height = parts.next()?.trim().parse::<u32>().ok()?;
|
||||||
|
let fps = parts
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|value| value.trim().parse::<u32>().ok())
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Some(DshowCapability {
|
||||||
|
format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DshowCapture {
|
||||||
|
ctx: *mut HwcodecDshowCaptureContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for DshowCapture {}
|
||||||
|
|
||||||
|
impl DshowCapture {
|
||||||
|
pub fn open(
|
||||||
|
device_name: &str,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
fps: i32,
|
||||||
|
requested_format: CapturePixelFormat,
|
||||||
|
timeout_ms: i32,
|
||||||
|
) -> Result<Self, CaptureError> {
|
||||||
|
let device_name = CString::new(device_name).map_err(|_| CaptureError {
|
||||||
|
code: -1,
|
||||||
|
message: "device name contains NUL byte".to_string(),
|
||||||
|
})?;
|
||||||
|
unsafe {
|
||||||
|
let ctx = hwcodec_dshow_capture_open(
|
||||||
|
device_name.as_ptr(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fps,
|
||||||
|
requested_format.to_ffi(),
|
||||||
|
timeout_ms,
|
||||||
|
);
|
||||||
|
if ctx.is_null() {
|
||||||
|
return Err(CaptureError {
|
||||||
|
code: -1,
|
||||||
|
message: last_error_message(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Self { ctx })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(&self) -> Result<CaptureStreamInfo, CaptureError> {
|
||||||
|
unsafe {
|
||||||
|
let mut info = HwcodecCaptureStreamInfo {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
pixel_format: HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UNKNOWN as c_int,
|
||||||
|
stride: 0,
|
||||||
|
};
|
||||||
|
let ret = hwcodec_dshow_capture_info(self.ctx, &mut info);
|
||||||
|
if ret != 0 {
|
||||||
|
return Err(CaptureError {
|
||||||
|
code: ret,
|
||||||
|
message: last_error_message(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(CaptureStreamInfo {
|
||||||
|
width: info.width,
|
||||||
|
height: info.height,
|
||||||
|
pixel_format: CapturePixelFormat::from_ffi(info.pixel_format),
|
||||||
|
stride: info.stride,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_packet(&mut self) -> Result<(Vec<u8>, u64), CaptureError> {
|
||||||
|
unsafe {
|
||||||
|
let mut data = std::ptr::null_mut();
|
||||||
|
let mut len = 0;
|
||||||
|
let mut sequence = 0u64;
|
||||||
|
let ret = hwcodec_dshow_capture_read(self.ctx, &mut data, &mut len, &mut sequence);
|
||||||
|
if ret != 0 {
|
||||||
|
return Err(CaptureError {
|
||||||
|
code: ret,
|
||||||
|
message: last_error_message(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if data.is_null() || len <= 0 {
|
||||||
|
return Err(CaptureError {
|
||||||
|
code: -1,
|
||||||
|
message: "empty packet returned by capture backend".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let slice = std::slice::from_raw_parts(data, len as usize);
|
||||||
|
let vec = slice.to_vec();
|
||||||
|
hwcodec_dshow_capture_packet_free(data);
|
||||||
|
Ok((vec, sequence))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DshowCapture {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
hwcodec_dshow_capture_close(self.ctx);
|
||||||
|
}
|
||||||
|
self.ctx = std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ pub enum Driver {
|
|||||||
FFMPEG,
|
FFMPEG,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(windows, target_os = "linux"))]
|
#[cfg(any(windows, target_os = "linux", target_os = "android"))]
|
||||||
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,6 +39,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ 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_free_encoder,
|
ffmpeg_linesize_offset_length, ffmpeg_ram_encode, ffmpeg_ram_encode_packet,
|
||||||
ffmpeg_ram_new_encoder, ffmpeg_ram_request_keyframe, ffmpeg_ram_set_bitrate, CodecInfo,
|
ffmpeg_ram_free_encoder, ffmpeg_ram_free_packet, ffmpeg_ram_new_encoder,
|
||||||
AV_NUM_DATA_POINTERS,
|
ffmpeg_ram_request_keyframe, ffmpeg_ram_set_bitrate, CodecInfo, AV_NUM_DATA_POINTERS,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
use bytes::Bytes;
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use std::{
|
use std::{
|
||||||
ffi::{c_void, CString},
|
ffi::{c_void, CString},
|
||||||
@@ -15,7 +17,7 @@ use std::{
|
|||||||
slice,
|
slice,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(windows, target_os = "linux"))]
|
#[cfg(any(windows, target_os = "linux", target_os = "android"))]
|
||||||
use crate::common::Driver;
|
use crate::common::Driver;
|
||||||
|
|
||||||
/// Timeout for encoder test in milliseconds
|
/// Timeout for encoder test in milliseconds
|
||||||
@@ -26,6 +28,7 @@ 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 {
|
||||||
@@ -92,11 +95,32 @@ fn linux_support_v4l2m2m() -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(windows, target_os = "linux"))]
|
#[cfg(any(windows, target_os = "linux", target_os = "android"))]
|
||||||
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.
|
||||||
@@ -243,8 +267,7 @@ fn enumerate_candidate_codecs(ctx: &EncodeContext) -> Vec<CodecInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
codecs.retain(|codec| {
|
codecs.retain(|codec| {
|
||||||
!(ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_YUV420P as i32
|
!(ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_YUV420P as i32 && codec.name.contains("qsv"))
|
||||||
&& codec.name.contains("qsv"))
|
|
||||||
});
|
});
|
||||||
codecs
|
codecs
|
||||||
}
|
}
|
||||||
@@ -258,7 +281,19 @@ struct ProbePolicy {
|
|||||||
|
|
||||||
impl ProbePolicy {
|
impl ProbePolicy {
|
||||||
fn for_codec(codec_name: &str) -> Self {
|
fn for_codec(codec_name: &str) -> Self {
|
||||||
if codec_name.contains("v4l2m2m") {
|
if codec_name.contains("mediacodec") {
|
||||||
|
Self {
|
||||||
|
max_attempts: 30,
|
||||||
|
request_keyframe: true,
|
||||||
|
accept_any_output: true,
|
||||||
|
}
|
||||||
|
} else if codec_name.contains("amf") {
|
||||||
|
Self {
|
||||||
|
max_attempts: 5,
|
||||||
|
request_keyframe: true,
|
||||||
|
accept_any_output: true,
|
||||||
|
}
|
||||||
|
} else if codec_name.contains("v4l2m2m") {
|
||||||
Self {
|
Self {
|
||||||
max_attempts: 5,
|
max_attempts: 5,
|
||||||
request_keyframe: true,
|
request_keyframe: true,
|
||||||
@@ -299,11 +334,11 @@ fn log_failed_probe_attempt(
|
|||||||
frames: &[EncodeFrame],
|
frames: &[EncodeFrame],
|
||||||
elapsed_ms: u128,
|
elapsed_ms: u128,
|
||||||
) {
|
) {
|
||||||
use log::debug;
|
use log::{debug, trace};
|
||||||
|
|
||||||
if policy.accept_any_output {
|
if policy.accept_any_output {
|
||||||
if frames.is_empty() {
|
if frames.is_empty() {
|
||||||
debug!(
|
trace!(
|
||||||
"Encoder {} test produced no output on attempt {}",
|
"Encoder {} test produced no output on attempt {}",
|
||||||
codec_name, attempt
|
codec_name, attempt
|
||||||
);
|
);
|
||||||
@@ -332,7 +367,7 @@ 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;
|
use log::{debug, warn};
|
||||||
|
|
||||||
debug!("Testing encoder: {}", codec.name);
|
debug!("Testing encoder: {}", codec.name);
|
||||||
|
|
||||||
@@ -383,13 +418,13 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
last_err = Some(err);
|
last_err = Some(err);
|
||||||
debug!(
|
warn!(
|
||||||
"Encoder {} test attempt {} returned error: {}",
|
"Encoder {} test attempt {} returned error: {}",
|
||||||
codec.name, attempt_no, err
|
codec.name, attempt_no, err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,16 +437,20 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo
|
|||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!("Failed to create encoder {}", codec.name);
|
warn!("Failed to create encoder {}", codec.name);
|
||||||
false
|
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!(
|
||||||
@@ -446,6 +485,39 @@ pub struct EncodeFrame {
|
|||||||
pub key: i32,
|
pub key: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
pub struct EncodeBytesFrame {
|
||||||
|
pub data: Bytes,
|
||||||
|
pub pts: i64,
|
||||||
|
pub key: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
struct FfmpegPacketOwner {
|
||||||
|
packet: *mut c_void,
|
||||||
|
data: *const u8,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
unsafe impl Send for FfmpegPacketOwner {}
|
||||||
|
|
||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
impl AsRef<[u8]> for FfmpegPacketOwner {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
unsafe { slice::from_raw_parts(self.data, self.len) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
impl Drop for FfmpegPacketOwner {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
ffmpeg_ram_free_packet(self.packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for EncodeFrame {
|
impl Display for EncodeFrame {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "encode len:{}, pts:{}", self.data.len(), self.pts)
|
write!(f, "encode len:{}, pts:{}", self.data.len(), self.pts)
|
||||||
@@ -538,6 +610,25 @@ impl Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
pub fn encode_bytes(&mut self, data: &[u8], ms: i64) -> Result<Vec<EncodeBytesFrame>, i32> {
|
||||||
|
unsafe {
|
||||||
|
let mut frames = Vec::<EncodeBytesFrame>::new();
|
||||||
|
let result = ffmpeg_ram_encode_packet(
|
||||||
|
self.codec,
|
||||||
|
data.as_ptr(),
|
||||||
|
data.len() as _,
|
||||||
|
&mut frames as *mut _ as *const c_void,
|
||||||
|
ms,
|
||||||
|
Some(Encoder::packet_callback),
|
||||||
|
);
|
||||||
|
if result == -11 || result == 0 {
|
||||||
|
return Ok(frames);
|
||||||
|
}
|
||||||
|
Err(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" fn callback(data: *const u8, size: c_int, pts: i64, key: i32, obj: *const c_void) {
|
extern "C" fn callback(data: *const u8, size: c_int, pts: i64, key: i32, obj: *const c_void) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let frames = &mut *(obj as *mut Vec<EncodeFrame>);
|
let frames = &mut *(obj as *mut Vec<EncodeFrame>);
|
||||||
@@ -549,6 +640,30 @@ impl Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
extern "C" fn packet_callback(
|
||||||
|
packet: *mut c_void,
|
||||||
|
data: *const u8,
|
||||||
|
size: c_int,
|
||||||
|
pts: i64,
|
||||||
|
key: i32,
|
||||||
|
obj: *const c_void,
|
||||||
|
) {
|
||||||
|
unsafe {
|
||||||
|
let frames = &mut *(obj as *mut Vec<EncodeBytesFrame>);
|
||||||
|
let owner = FfmpegPacketOwner {
|
||||||
|
packet,
|
||||||
|
data,
|
||||||
|
len: size as usize,
|
||||||
|
};
|
||||||
|
frames.push(EncodeBytesFrame {
|
||||||
|
data: Bytes::from_owner(owner),
|
||||||
|
pts,
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_bitrate(&mut self, kbs: i32) -> Result<(), ()> {
|
pub fn set_bitrate(&mut self, kbs: i32) -> Result<(), ()> {
|
||||||
let ret = unsafe { ffmpeg_ram_set_bitrate(self.codec, kbs) };
|
let ret = unsafe { ffmpeg_ram_set_bitrate(self.codec, kbs) };
|
||||||
if ret == 0 {
|
if ret == 0 {
|
||||||
@@ -583,11 +698,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")) {
|
if !(cfg!(windows) || cfg!(target_os = "linux") || cfg!(target_os = "android")) {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let mut res = vec![];
|
let mut res = vec![];
|
||||||
#[cfg(any(windows, target_os = "linux"))]
|
#[cfg(any(windows, target_os = "linux", target_os = "android"))]
|
||||||
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()) {
|
||||||
|
|||||||
@@ -3,20 +3,24 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use crate::common::DataFormat::{self, *};
|
use crate::common::DataFormat::{self, *};
|
||||||
use crate::ffmpeg::{
|
use crate::ffmpeg::AVHWDeviceType::{self, *};
|
||||||
AVHWDeviceType::{self, *},
|
|
||||||
};
|
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::ffi::c_int;
|
use std::ffi::c_int;
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/ffmpeg_ram_ffi.rs"));
|
include!(concat!(env!("OUT_DIR"), "/ffmpeg_ram_ffi.rs"));
|
||||||
|
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))]
|
#[cfg(all(
|
||||||
|
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(not(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp")))]
|
#[cfg(any(
|
||||||
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
#[cfg(windows)]
|
||||||
|
pub mod capture;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod ffmpeg;
|
pub mod ffmpeg;
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))]
|
#[cfg(all(
|
||||||
|
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;
|
||||||
|
|
||||||
|
|||||||
1
libs/v4l2r/.cargo-ok
Normal file
1
libs/v4l2r/.cargo-ok
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"v":1}
|
||||||
6
libs/v4l2r/.cargo_vcs_info.json
Normal file
6
libs/v4l2r/.cargo_vcs_info.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"sha1": "7b441383125ae583017a1c18b3fc9ec6c88ddbe8"
|
||||||
|
},
|
||||||
|
"path_in_vcs": "lib"
|
||||||
|
}
|
||||||
52
libs/v4l2r/Android.bp
Normal file
52
libs/v4l2r/Android.bp
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// 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"],
|
||||||
|
|
||||||
|
}
|
||||||
65
libs/v4l2r/Cargo.toml
Normal file
65
libs/v4l2r/Cargo.toml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||||
|
#
|
||||||
|
# When uploading crates to the registry Cargo will automatically
|
||||||
|
# "normalize" Cargo.toml files for maximal compatibility
|
||||||
|
# with all versions of Cargo and also rewrite `path` dependencies
|
||||||
|
# to registry (e.g., crates.io) dependencies.
|
||||||
|
#
|
||||||
|
# If you are reading this file be aware that the original Cargo.toml
|
||||||
|
# will likely look very different (and much more reasonable).
|
||||||
|
# See Cargo.toml.orig for the original contents.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
edition = "2021"
|
||||||
|
name = "v4l2r"
|
||||||
|
version = "0.0.7"
|
||||||
|
authors = ["Alexandre Courbot <gnurou@gmail.com>"]
|
||||||
|
build = "build.rs"
|
||||||
|
autolib = false
|
||||||
|
autobins = false
|
||||||
|
autoexamples = false
|
||||||
|
autotests = false
|
||||||
|
autobenches = false
|
||||||
|
description = "Safe and flexible abstraction over V4L2"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = [
|
||||||
|
"v4l2",
|
||||||
|
"video",
|
||||||
|
"linux",
|
||||||
|
]
|
||||||
|
categories = ["os"]
|
||||||
|
license-file = "LICENSE"
|
||||||
|
repository = "https://github.com/Gnurou/v4l2r"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
arch32 = []
|
||||||
|
arch64 = []
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "v4l2r"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies.bitflags]
|
||||||
|
version = "2.4"
|
||||||
|
|
||||||
|
[dependencies.enumn]
|
||||||
|
version = "0.1.6"
|
||||||
|
|
||||||
|
[dependencies.log]
|
||||||
|
version = "0.4.14"
|
||||||
|
|
||||||
|
[dependencies.nix]
|
||||||
|
version = "0.28"
|
||||||
|
features = [
|
||||||
|
"ioctl",
|
||||||
|
"mman",
|
||||||
|
"poll",
|
||||||
|
"fs",
|
||||||
|
"event",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.thiserror]
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
|
[build-dependencies.bindgen]
|
||||||
|
version = "0.70.1"
|
||||||
28
libs/v4l2r/Cargo.toml.orig
generated
Normal file
28
libs/v4l2r/Cargo.toml.orig
generated
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "v4l2r"
|
||||||
|
version = "0.0.7"
|
||||||
|
authors = ["Alexandre Courbot <gnurou@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
description = "Safe and flexible abstraction over V4L2"
|
||||||
|
repository = "https://github.com/Gnurou/v4l2r"
|
||||||
|
categories = ["os"]
|
||||||
|
keywords = ["v4l2", "video", "linux"]
|
||||||
|
|
||||||
|
license-file.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# Generate the bindings for 64-bit even if the host is 32-bit.
|
||||||
|
arch64 = []
|
||||||
|
# Generate the bindings for 32-bit even if the host is 64-bit.
|
||||||
|
arch32 = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nix = { version = "0.28", features = ["ioctl", "mman", "poll", "fs", "event"] }
|
||||||
|
bitflags = "2.4"
|
||||||
|
thiserror = "1.0"
|
||||||
|
log = "0.4.14"
|
||||||
|
enumn = "0.1.6"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
bindgen = "0.70.1"
|
||||||
23
libs/v4l2r/LICENSE
Normal file
23
libs/v4l2r/LICENSE
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Permission is hereby granted, free of charge, to any
|
||||||
|
person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice
|
||||||
|
shall be included in all copies or substantial portions
|
||||||
|
of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
24
libs/v4l2r/README.md
Normal file
24
libs/v4l2r/README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Rust bindings for V4L2
|
||||||
|
|
||||||
|
This is a vendored, One-KVM-specific subset of `v4l2r`.
|
||||||
|
|
||||||
|
It keeps only the pieces needed for video capture:
|
||||||
|
|
||||||
|
- generated Linux V4L2 bindings,
|
||||||
|
- safe low-level ioctl wrappers used by capture and device probing,
|
||||||
|
- memory handle helpers for `MMAP`, `USERPTR`, and `DMABUF`,
|
||||||
|
- core V4L2 types such as `Format`, `PixelFormat`, and `QueueType`.
|
||||||
|
|
||||||
|
The upstream crate also contains high-level device abstractions, stateful
|
||||||
|
decoder/encoder helpers, stateless codec controls, examples, and C FFI. Those
|
||||||
|
parts are intentionally removed here so this dependency stays scoped to capture.
|
||||||
|
|
||||||
|
## Build options
|
||||||
|
|
||||||
|
`cargo build` generates V4L2 bindings from `/usr/include/linux/videodev2.h` by
|
||||||
|
default. Set `V4L2R_VIDEODEV2_H_PATH` to a directory containing `videodev2.h` to
|
||||||
|
generate bindings from a different header.
|
||||||
|
|
||||||
|
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.
|
||||||
20
libs/v4l2r/bindgen.rs
Normal file
20
libs/v4l2r/bindgen.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// This file defines the customizations to the bindgen builder used to generate the v4l2r
|
||||||
|
// bindings.
|
||||||
|
//
|
||||||
|
// It is meant to be included from `lib/build.rs` and `android/build.rs`.
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// Workaround for https://github.com/rust-lang/rust-bindgen/issues/753.
|
||||||
|
pub struct Fix753;
|
||||||
|
|
||||||
|
impl bindgen::callbacks::ParseCallbacks for Fix753 {
|
||||||
|
fn item_name(&self, original_item_name: &str) -> Option<String> {
|
||||||
|
Some(original_item_name.trim_start_matches("Fix753_").to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v4l2r_bindgen_builder(builder: bindgen::Builder) -> bindgen::Builder {
|
||||||
|
builder
|
||||||
|
.parse_callbacks(Box::new(Fix753))
|
||||||
|
.derive_default(true)
|
||||||
|
}
|
||||||
180
libs/v4l2r/build.rs
Normal file
180
libs/v4l2r/build.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
use std::env::{self, VarError};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
include!("bindgen.rs");
|
||||||
|
|
||||||
|
/// Environment variable that can be set to point to the directory containing the `videodev2.h`
|
||||||
|
/// file to use to generate the bindings.
|
||||||
|
const V4L2R_VIDEODEV_ENV: &str = "V4L2R_VIDEODEV2_H_PATH";
|
||||||
|
|
||||||
|
/// Default header file to parse if the `V4L2R_VIDEODEV2_H_PATH` environment variable is not set.
|
||||||
|
const DEFAULT_VIDEODEV2_H_PATH: &str = "/usr/include/linux";
|
||||||
|
|
||||||
|
/// Wrapper file to use as input of bindgen.
|
||||||
|
const WRAPPER_H: &str = "v4l2r_wrapper.h";
|
||||||
|
|
||||||
|
// Fix for https://github.com/rust-lang/rust-bindgen/issues/753
|
||||||
|
const FIX753_H: &str = "fix753.h";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let target = env::var("TARGET").unwrap_or_default();
|
||||||
|
let is_android = target.contains("android");
|
||||||
|
|
||||||
|
let default_videodev2_h_path = if is_android {
|
||||||
|
android_sysroot().join("usr/include").display().to_string()
|
||||||
|
} 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={}", FIX753_H);
|
||||||
|
println!("cargo::rerun-if-changed={}", WRAPPER_H);
|
||||||
|
|
||||||
|
let mut clang_args = vec![
|
||||||
|
format!("-I{videodev2_h_path}"),
|
||||||
|
#[cfg(all(feature = "arch64", not(feature = "arch32")))]
|
||||||
|
"--target=x86_64-linux-gnu".into(),
|
||||||
|
#[cfg(all(feature = "arch32", not(feature = "arch64")))]
|
||||||
|
"--target=i686-linux-gnu".into(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if is_android {
|
||||||
|
clang_args.extend(android_clang_args(&target));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bindings = v4l2r_bindgen_builder(bindgen::Builder::default())
|
||||||
|
.header(WRAPPER_H)
|
||||||
|
.clang_args(clang_args)
|
||||||
|
.generate()
|
||||||
|
.expect("unable to generate bindings");
|
||||||
|
|
||||||
|
let out_path = PathBuf::from(env::var("OUT_DIR").expect("`OUT_DIR` is not set"));
|
||||||
|
bindings
|
||||||
|
.write_to_file(out_path.join("bindings.rs"))
|
||||||
|
.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()))
|
||||||
|
}
|
||||||
55
libs/v4l2r/fix753.h
Normal file
55
libs/v4l2r/fix753.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#undef V4L2_FWHT_FL_COMPONENTS_NUM_MSK
|
||||||
|
#undef V4L2_FWHT_FL_PIXENC_MSK
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_IS_INTERLACED
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_IS_INTERLACED);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_IS_BOTTOM_FIRST
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_IS_BOTTOM_FIRST);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_IS_ALTERNATE
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_IS_ALTERNATE);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_IS_BOTTOM_FIELD
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_IS_BOTTOM_FIELD);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_LUMA_IS_UNCOMPRESSED
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_LUMA_IS_UNCOMPRESSED);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_CB_IS_UNCOMPRESSED
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_CB_IS_UNCOMPRESSED);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_CR_IS_UNCOMPRESSED
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_CR_IS_UNCOMPRESSED);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_CHROMA_FULL_HEIGHT
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_CHROMA_FULL_HEIGHT);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_CHROMA_FULL_WIDTH
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_CHROMA_FULL_WIDTH);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_ALPHA_IS_UNCOMPRESSED
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_ALPHA_IS_UNCOMPRESSED);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_I_FRAME
|
||||||
|
MARK_FIX_753(V4L2_FWHT_FL_I_FRAME);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_COMPONENTS_NUM_OFFSET
|
||||||
|
#define V4L2_FWHT_FL_COMPONENTS_NUM_MSK \
|
||||||
|
(7 << V4L2_FWHT_FL_COMPONENTS_NUM_OFFSET)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef V4L2_FWHT_FL_PIXENC_OFFSET
|
||||||
|
#define V4L2_FWHT_FL_PIXENC_MSK (3 << V4L2_FWHT_FL_PIXENC_OFFSET)
|
||||||
|
#endif
|
||||||
8
libs/v4l2r/src/bindings.rs
Normal file
8
libs/v4l2r/src/bindings.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
#![allow(non_upper_case_globals)]
|
||||||
|
#![allow(non_camel_case_types)]
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
#![allow(deref_nullptr)]
|
||||||
|
#![allow(clippy::all)]
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||||
1177
libs/v4l2r/src/ioctl.rs
Normal file
1177
libs/v4l2r/src/ioctl.rs
Normal file
File diff suppressed because it is too large
Load Diff
66
libs/v4l2r/src/ioctl/dqbuf.rs
Normal file
66
libs/v4l2r/src/ioctl/dqbuf.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::ioctl::ioctl_and_convert;
|
||||||
|
use crate::ioctl::IoctlConvertError;
|
||||||
|
use crate::ioctl::IoctlConvertResult;
|
||||||
|
use crate::ioctl::UncheckedV4l2Buffer;
|
||||||
|
use crate::QueueType;
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_buffer;
|
||||||
|
nix::ioctl_readwrite!(vidioc_dqbuf, b'V', 17, v4l2_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DqBufIoctlError {
|
||||||
|
#[error("end-of-stream reached")]
|
||||||
|
Eos,
|
||||||
|
#[error("no buffer ready for dequeue")]
|
||||||
|
NotReady,
|
||||||
|
#[error("unexpected ioctl error: {0}")]
|
||||||
|
Other(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Errno> for DqBufIoctlError {
|
||||||
|
fn from(error: Errno) -> Self {
|
||||||
|
match error {
|
||||||
|
Errno::EAGAIN => Self::NotReady,
|
||||||
|
Errno::EPIPE => Self::Eos,
|
||||||
|
error => Self::Other(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DqBufIoctlError> for Errno {
|
||||||
|
fn from(err: DqBufIoctlError) -> Self {
|
||||||
|
match err {
|
||||||
|
DqBufIoctlError::Eos => Errno::EPIPE,
|
||||||
|
DqBufIoctlError::NotReady => Errno::EAGAIN,
|
||||||
|
DqBufIoctlError::Other(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DqBufError<CE> = IoctlConvertError<DqBufIoctlError, CE>;
|
||||||
|
pub type DqBufResult<O, CE> = IoctlConvertResult<O, DqBufIoctlError, CE>;
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_DQBUF` ioctl.
|
||||||
|
pub fn dqbuf<O>(fd: &impl AsRawFd, queue: QueueType) -> DqBufResult<O, O::Error>
|
||||||
|
where
|
||||||
|
O: TryFrom<UncheckedV4l2Buffer>,
|
||||||
|
O::Error: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let mut v4l2_buf = UncheckedV4l2Buffer::new_for_querybuf(queue, None);
|
||||||
|
|
||||||
|
ioctl_and_convert(
|
||||||
|
unsafe { ioctl::vidioc_dqbuf(fd.as_raw_fd(), v4l2_buf.as_mut()) }
|
||||||
|
.map(|_| v4l2_buf)
|
||||||
|
.map_err(Into::into),
|
||||||
|
)
|
||||||
|
}
|
||||||
137
libs/v4l2r/src/ioctl/enum_fmt.rs
Normal file
137
libs/v4l2r/src/ioctl/enum_fmt.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
//! Safe wrapper for the `VIDIOC_ENUM_FMT` ioctl.
|
||||||
|
use super::string_from_cstr;
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::bindings::v4l2_fmtdesc;
|
||||||
|
use crate::{PixelFormat, QueueType};
|
||||||
|
use bitflags::bitflags;
|
||||||
|
use log::error;
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use std::fmt;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Flags returned by the `VIDIOC_ENUM_FMT` ioctl into the `flags` field of
|
||||||
|
/// `struct v4l2_fmtdesc`.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct FormatFlags: u32 {
|
||||||
|
const COMPRESSED = bindings::V4L2_FMT_FLAG_COMPRESSED;
|
||||||
|
const EMULATED = bindings::V4L2_FMT_FLAG_EMULATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Quickly get the Fourcc code of a format.
|
||||||
|
impl From<v4l2_fmtdesc> for PixelFormat {
|
||||||
|
fn from(fmtdesc: v4l2_fmtdesc) -> Self {
|
||||||
|
fmtdesc.pixelformat.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe variant of the `v4l2_fmtdesc` struct, to be used with `enum_fmt`.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FmtDesc {
|
||||||
|
pub flags: FormatFlags,
|
||||||
|
pub description: String,
|
||||||
|
pub pixelformat: PixelFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FmtDesc {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}: {} {}",
|
||||||
|
self.pixelformat,
|
||||||
|
self.description,
|
||||||
|
if self.flags.is_empty() {
|
||||||
|
"".into()
|
||||||
|
} else {
|
||||||
|
format!("({:?})", self.flags)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<v4l2_fmtdesc> for FmtDesc {
|
||||||
|
fn from(fmtdesc: v4l2_fmtdesc) -> Self {
|
||||||
|
FmtDesc {
|
||||||
|
flags: FormatFlags::from_bits_truncate(fmtdesc.flags),
|
||||||
|
description: string_from_cstr(&fmtdesc.description).unwrap_or_else(|_| "".into()),
|
||||||
|
pixelformat: fmtdesc.pixelformat.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_fmtdesc;
|
||||||
|
nix::ioctl_readwrite!(vidioc_enum_fmt, b'V', 2, v4l2_fmtdesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum EnumFmtError {
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(#[from] nix::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EnumFmtError> for Errno {
|
||||||
|
fn from(err: EnumFmtError) -> Self {
|
||||||
|
match err {
|
||||||
|
EnumFmtError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_ENUM_FMT` ioctl.
|
||||||
|
pub fn enum_fmt<T: From<v4l2_fmtdesc>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
queue: QueueType,
|
||||||
|
index: u32,
|
||||||
|
) -> Result<T, EnumFmtError> {
|
||||||
|
let mut fmtdesc = v4l2_fmtdesc {
|
||||||
|
type_: queue as u32,
|
||||||
|
index,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
unsafe { ioctl::vidioc_enum_fmt(fd.as_raw_fd(), &mut fmtdesc) }?;
|
||||||
|
|
||||||
|
Ok(T::from(fmtdesc))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over the formats of the given queue. This takes a reference to the
|
||||||
|
/// device's file descriptor so no operation that could affect the format
|
||||||
|
/// enumeration can take place while the iterator exists.
|
||||||
|
pub struct FormatIterator<'a, F: AsRawFd> {
|
||||||
|
fd: &'a F,
|
||||||
|
queue: QueueType,
|
||||||
|
index: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, F: AsRawFd> FormatIterator<'a, F> {
|
||||||
|
/// Create a new iterator listing all the currently valid formats on
|
||||||
|
/// `queue`.
|
||||||
|
pub fn new(fd: &'a F, queue: QueueType) -> Self {
|
||||||
|
FormatIterator {
|
||||||
|
fd,
|
||||||
|
queue,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, F: AsRawFd> Iterator for FormatIterator<'a, F> {
|
||||||
|
type Item = FmtDesc;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match enum_fmt(self.fd, self.queue, self.index) {
|
||||||
|
Ok(fmtdesc) => {
|
||||||
|
self.index += 1;
|
||||||
|
Some(fmtdesc)
|
||||||
|
}
|
||||||
|
// EINVAL means we have reached the last format.
|
||||||
|
Err(EnumFmtError::IoctlError(Errno::EINVAL)) => None,
|
||||||
|
_ => {
|
||||||
|
error!("Unexpected return value for VIDIOC_ENUM_FMT!");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
libs/v4l2r/src/ioctl/expbuf.rs
Normal file
61
libs/v4l2r/src/ioctl/expbuf.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//! Safe wrapper for the `VIDIOC_EXPBUF` ioctl.
|
||||||
|
use bitflags::bitflags;
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use nix::fcntl::OFlag;
|
||||||
|
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bindings::v4l2_exportbuffer;
|
||||||
|
use crate::QueueType;
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Flags that can be passed when exporting the buffer.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct ExpbufFlags: u32 {
|
||||||
|
const CLOEXEC = OFlag::O_CLOEXEC.bits() as u32;
|
||||||
|
const RDONLY = OFlag::O_RDONLY.bits() as u32;
|
||||||
|
const WRONLY = OFlag::O_WRONLY.bits() as u32;
|
||||||
|
const RDWR = OFlag::O_RDWR.bits() as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_exportbuffer;
|
||||||
|
nix::ioctl_readwrite!(vidioc_expbuf, b'V', 16, v4l2_exportbuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ExpbufError {
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(#[from] Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExpbufError> for Errno {
|
||||||
|
fn from(err: ExpbufError) -> Self {
|
||||||
|
match err {
|
||||||
|
ExpbufError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_EXPBUF` ioctl.
|
||||||
|
pub fn expbuf<R: FromRawFd>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
queue: QueueType,
|
||||||
|
index: usize,
|
||||||
|
plane: usize,
|
||||||
|
flags: ExpbufFlags,
|
||||||
|
) -> Result<R, ExpbufError> {
|
||||||
|
let mut v4l2_expbuf = v4l2_exportbuffer {
|
||||||
|
type_: queue as u32,
|
||||||
|
index: index as u32,
|
||||||
|
plane: plane as u32,
|
||||||
|
flags: flags.bits(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe { ioctl::vidioc_expbuf(fd.as_raw_fd(), &mut v4l2_expbuf) }?;
|
||||||
|
|
||||||
|
Ok(unsafe { R::from_raw_fd(v4l2_expbuf.fd) })
|
||||||
|
}
|
||||||
82
libs/v4l2r/src/ioctl/frameintervals.rs
Normal file
82
libs/v4l2r/src/ioctl/frameintervals.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use nix::errno::Errno;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::bindings::v4l2_frmivalenum;
|
||||||
|
use crate::PixelFormat;
|
||||||
|
|
||||||
|
/// A wrapper for the 'v4l2_frmivalenum' union member types
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FrmIvalTypes<'a> {
|
||||||
|
Discrete(&'a bindings::v4l2_fract),
|
||||||
|
StepWise(&'a bindings::v4l2_frmival_stepwise),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl v4l2_frmivalenum {
|
||||||
|
/// Safely access the intervals member of the struct based on the
|
||||||
|
/// returned type.
|
||||||
|
pub fn intervals(&self) -> Option<FrmIvalTypes<'_>> {
|
||||||
|
match self.type_ {
|
||||||
|
// SAFETY: the member of the union that gets used by the driver
|
||||||
|
// is determined by the type
|
||||||
|
bindings::v4l2_frmivaltypes_V4L2_FRMIVAL_TYPE_DISCRETE => {
|
||||||
|
Some(FrmIvalTypes::Discrete(unsafe {
|
||||||
|
&self.__bindgen_anon_1.discrete
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: the member of the union that gets used by the driver
|
||||||
|
// is determined by the type
|
||||||
|
bindings::v4l2_frmivaltypes_V4L2_FRMIVAL_TYPE_CONTINUOUS
|
||||||
|
| bindings::v4l2_frmivaltypes_V4L2_FRMIVAL_TYPE_STEPWISE => {
|
||||||
|
Some(FrmIvalTypes::StepWise(unsafe {
|
||||||
|
&self.__bindgen_anon_1.stepwise
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_frmivalenum;
|
||||||
|
nix::ioctl_readwrite!(vidioc_enum_frameintervals, b'V', 75, v4l2_frmivalenum);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum FrameIntervalsError {
|
||||||
|
#[error("Unexpected ioctl error: {0}")]
|
||||||
|
IoctlError(nix::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FrameIntervalsError> for Errno {
|
||||||
|
fn from(err: FrameIntervalsError) -> Self {
|
||||||
|
match err {
|
||||||
|
FrameIntervalsError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Safe wrapper around the `VIDIOC_ENUM_FRAMEINTERVALS` ioctl.
|
||||||
|
pub fn enum_frame_intervals<O: From<v4l2_frmivalenum>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
index: u32,
|
||||||
|
pixel_format: PixelFormat,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> Result<O, FrameIntervalsError> {
|
||||||
|
let mut frame_interval = v4l2_frmivalenum {
|
||||||
|
index,
|
||||||
|
pixel_format: pixel_format.into(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_enum_frameintervals(fd.as_raw_fd(), &mut frame_interval) } {
|
||||||
|
Ok(_) => Ok(O::from(frame_interval)),
|
||||||
|
Err(e) => Err(FrameIntervalsError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
79
libs/v4l2r/src/ioctl/framesizes.rs
Normal file
79
libs/v4l2r/src/ioctl/framesizes.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use nix::errno::Errno;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::bindings::v4l2_frmsizeenum;
|
||||||
|
use crate::PixelFormat;
|
||||||
|
|
||||||
|
/// A wrapper for the 'v4l2_frmsizeenum' union member types
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FrmSizeTypes<'a> {
|
||||||
|
Discrete(&'a bindings::v4l2_frmsize_discrete),
|
||||||
|
StepWise(&'a bindings::v4l2_frmsize_stepwise),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl v4l2_frmsizeenum {
|
||||||
|
/// Safely access the size member of the struct based on the
|
||||||
|
/// returned type.
|
||||||
|
pub fn size(&self) -> Option<FrmSizeTypes<'_>> {
|
||||||
|
match self.type_ {
|
||||||
|
// SAFETY: the member of the union that gets used by the driver
|
||||||
|
// is determined by the type
|
||||||
|
bindings::v4l2_frmsizetypes_V4L2_FRMSIZE_TYPE_DISCRETE => {
|
||||||
|
Some(FrmSizeTypes::Discrete(unsafe {
|
||||||
|
&self.__bindgen_anon_1.discrete
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: the member of the union that gets used by the driver
|
||||||
|
// is determined by the type
|
||||||
|
bindings::v4l2_frmsizetypes_V4L2_FRMSIZE_TYPE_CONTINUOUS
|
||||||
|
| bindings::v4l2_frmsizetypes_V4L2_FRMSIZE_TYPE_STEPWISE => {
|
||||||
|
Some(FrmSizeTypes::StepWise(unsafe {
|
||||||
|
&self.__bindgen_anon_1.stepwise
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_frmsizeenum;
|
||||||
|
nix::ioctl_readwrite!(vidioc_enum_framesizes, b'V', 74, v4l2_frmsizeenum);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum FrameSizeError {
|
||||||
|
#[error("Unexpected ioctl error: {0}")]
|
||||||
|
IoctlError(nix::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FrameSizeError> for Errno {
|
||||||
|
fn from(err: FrameSizeError) -> Self {
|
||||||
|
match err {
|
||||||
|
FrameSizeError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_ENUM_FRAMESIZES` ioctl.
|
||||||
|
pub fn enum_frame_sizes<O: From<v4l2_frmsizeenum>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
index: u32,
|
||||||
|
pixel_format: PixelFormat,
|
||||||
|
) -> Result<O, FrameSizeError> {
|
||||||
|
let mut frame_size = v4l2_frmsizeenum {
|
||||||
|
index,
|
||||||
|
pixel_format: pixel_format.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_enum_framesizes(fd.as_raw_fd(), &mut frame_size) } {
|
||||||
|
Ok(_) => Ok(O::from(frame_size)),
|
||||||
|
Err(e) => Err(FrameSizeError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
189
libs/v4l2r/src/ioctl/g_dv_timings.rs
Normal file
189
libs/v4l2r/src/ioctl/g_dv_timings.rs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
use enumn::N;
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::bindings::v4l2_dv_timings;
|
||||||
|
use crate::bindings::v4l2_dv_timings_cap;
|
||||||
|
use crate::bindings::v4l2_enum_dv_timings;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_dv_timings;
|
||||||
|
use crate::bindings::v4l2_dv_timings_cap;
|
||||||
|
use crate::bindings::v4l2_enum_dv_timings;
|
||||||
|
|
||||||
|
nix::ioctl_readwrite!(vidioc_s_dv_timings, b'V', 87, v4l2_dv_timings);
|
||||||
|
nix::ioctl_readwrite!(vidioc_g_dv_timings, b'V', 88, v4l2_dv_timings);
|
||||||
|
nix::ioctl_readwrite!(vidioc_enum_dv_timings, b'V', 98, v4l2_enum_dv_timings);
|
||||||
|
nix::ioctl_read!(vidioc_query_dv_timings, b'V', 99, v4l2_dv_timings);
|
||||||
|
nix::ioctl_readwrite!(vidioc_dv_timings_cap, b'V', 100, v4l2_dv_timings_cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, N)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum DvTimingsType {
|
||||||
|
Bt6561120 = bindings::V4L2_DV_BT_656_1120,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GDvTimingsError {
|
||||||
|
#[error("ioctl not supported or invalid parameters")]
|
||||||
|
Invalid,
|
||||||
|
#[error("Digital video timings are not supported on this input or output")]
|
||||||
|
Unsupported,
|
||||||
|
#[error("Device is busy and cannot change timings")]
|
||||||
|
Busy,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GDvTimingsError> for Errno {
|
||||||
|
fn from(err: GDvTimingsError) -> Self {
|
||||||
|
match err {
|
||||||
|
GDvTimingsError::Invalid => Errno::EINVAL,
|
||||||
|
GDvTimingsError::Unsupported => Errno::ENODATA,
|
||||||
|
GDvTimingsError::Busy => Errno::EBUSY,
|
||||||
|
GDvTimingsError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_S_DV_TIMINGS` ioctl.
|
||||||
|
pub fn s_dv_timings<I: Into<v4l2_dv_timings>, O: From<v4l2_dv_timings>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
timings: I,
|
||||||
|
) -> Result<O, GDvTimingsError> {
|
||||||
|
let mut timings: v4l2_dv_timings = timings.into();
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_s_dv_timings(fd.as_raw_fd(), &mut timings) } {
|
||||||
|
Ok(_) => Ok(O::from(timings)),
|
||||||
|
Err(Errno::EINVAL) => Err(GDvTimingsError::Invalid),
|
||||||
|
Err(Errno::ENODATA) => Err(GDvTimingsError::Unsupported),
|
||||||
|
Err(Errno::EBUSY) => Err(GDvTimingsError::Busy),
|
||||||
|
Err(e) => Err(GDvTimingsError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_G_DV_TIMINGS` ioctl.
|
||||||
|
pub fn g_dv_timings<O: From<v4l2_dv_timings>>(fd: &impl AsRawFd) -> Result<O, GDvTimingsError> {
|
||||||
|
let mut timings = v4l2_dv_timings {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_g_dv_timings(fd.as_raw_fd(), &mut timings) } {
|
||||||
|
Ok(_) => Ok(O::from(timings)),
|
||||||
|
Err(Errno::EINVAL) => Err(GDvTimingsError::Invalid),
|
||||||
|
Err(Errno::ENODATA) => Err(GDvTimingsError::Unsupported),
|
||||||
|
Err(Errno::EBUSY) => Err(GDvTimingsError::Busy),
|
||||||
|
Err(e) => Err(GDvTimingsError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum EnumDvTimingsError {
|
||||||
|
#[error("timing index is out of bounds")]
|
||||||
|
Invalid,
|
||||||
|
#[error("Digital video timings are not supported on this input or output")]
|
||||||
|
Unsupported,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EnumDvTimingsError> for Errno {
|
||||||
|
fn from(err: EnumDvTimingsError) -> Self {
|
||||||
|
match err {
|
||||||
|
EnumDvTimingsError::Invalid => Errno::EINVAL,
|
||||||
|
EnumDvTimingsError::Unsupported => Errno::ENODATA,
|
||||||
|
EnumDvTimingsError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_ENUM_DV_TIMINGS` ioctl.
|
||||||
|
pub fn enum_dv_timings<O: From<v4l2_dv_timings>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
index: u32,
|
||||||
|
) -> Result<O, EnumDvTimingsError> {
|
||||||
|
let mut timings = v4l2_enum_dv_timings {
|
||||||
|
index,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_enum_dv_timings(fd.as_raw_fd(), &mut timings) } {
|
||||||
|
Ok(_) => Ok(O::from(timings.timings)),
|
||||||
|
Err(Errno::EINVAL) => Err(EnumDvTimingsError::Invalid),
|
||||||
|
Err(Errno::ENODATA) => Err(EnumDvTimingsError::Unsupported),
|
||||||
|
Err(e) => Err(EnumDvTimingsError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum QueryDvTimingsError {
|
||||||
|
#[error("Digital video timings are not supported on this input or output")]
|
||||||
|
Unsupported,
|
||||||
|
#[error("No timings could be detected because no signal was found")]
|
||||||
|
NoLink,
|
||||||
|
#[error("Unstable signal")]
|
||||||
|
UnstableSignal,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<QueryDvTimingsError> for Errno {
|
||||||
|
fn from(err: QueryDvTimingsError) -> Self {
|
||||||
|
match err {
|
||||||
|
QueryDvTimingsError::Unsupported => Errno::ENODATA,
|
||||||
|
QueryDvTimingsError::NoLink => Errno::ENOLINK,
|
||||||
|
QueryDvTimingsError::UnstableSignal => Errno::ENOLCK,
|
||||||
|
QueryDvTimingsError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_QUERY_DV_TIMINGS` ioctl.
|
||||||
|
pub fn query_dv_timings<O: From<v4l2_dv_timings>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
) -> Result<O, QueryDvTimingsError> {
|
||||||
|
let mut timings = v4l2_dv_timings {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_query_dv_timings(fd.as_raw_fd(), &mut timings) } {
|
||||||
|
Ok(_) => Ok(O::from(timings)),
|
||||||
|
Err(Errno::ENODATA) => Err(QueryDvTimingsError::Unsupported),
|
||||||
|
Err(Errno::ENOLINK) => Err(QueryDvTimingsError::NoLink),
|
||||||
|
Err(Errno::ENOLCK) => Err(QueryDvTimingsError::UnstableSignal),
|
||||||
|
Err(e) => Err(QueryDvTimingsError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DvTimingsCapError {
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DvTimingsCapError> for Errno {
|
||||||
|
fn from(err: DvTimingsCapError) -> Self {
|
||||||
|
match err {
|
||||||
|
DvTimingsCapError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_DV_TIMINGS_CAP` ioctl.
|
||||||
|
pub fn dv_timings_cap<O: From<v4l2_dv_timings_cap>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
) -> Result<O, DvTimingsCapError> {
|
||||||
|
let mut caps = v4l2_dv_timings_cap {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_dv_timings_cap(fd.as_raw_fd(), &mut caps) } {
|
||||||
|
Ok(_) => Ok(O::from(caps)),
|
||||||
|
Err(e) => Err(DvTimingsCapError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
300
libs/v4l2r/src/ioctl/g_fmt.rs
Normal file
300
libs/v4l2r/src/ioctl/g_fmt.rs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
//! Safe wrapper for the `VIDIOC_(G|S|TRY)_FMT` ioctls.
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use std::convert::{From, Into, TryFrom, TryInto};
|
||||||
|
use std::default::Default;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::bindings::v4l2_format;
|
||||||
|
use crate::Format;
|
||||||
|
use crate::FormatConversionError;
|
||||||
|
use crate::PlaneLayout;
|
||||||
|
use crate::QueueType;
|
||||||
|
|
||||||
|
impl TryFrom<(QueueType, &Format)> for v4l2_format {
|
||||||
|
type Error = FormatConversionError;
|
||||||
|
|
||||||
|
fn try_from((queue, format): (QueueType, &Format)) -> Result<Self, Self::Error> {
|
||||||
|
Ok(v4l2_format {
|
||||||
|
type_: queue as u32,
|
||||||
|
fmt: match queue {
|
||||||
|
QueueType::VideoCaptureMplane | QueueType::VideoOutputMplane => {
|
||||||
|
bindings::v4l2_format__bindgen_ty_1 {
|
||||||
|
pix_mp: {
|
||||||
|
if format.plane_fmt.len() > bindings::VIDEO_MAX_PLANES as usize {
|
||||||
|
return Err(Self::Error::TooManyPlanes(format.plane_fmt.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pix_mp = bindings::v4l2_pix_format_mplane {
|
||||||
|
width: format.width,
|
||||||
|
height: format.height,
|
||||||
|
pixelformat: format.pixelformat.into(),
|
||||||
|
num_planes: format.plane_fmt.len() as u8,
|
||||||
|
plane_fmt: Default::default(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for (plane, v4l2_plane) in
|
||||||
|
format.plane_fmt.iter().zip(pix_mp.plane_fmt.iter_mut())
|
||||||
|
{
|
||||||
|
*v4l2_plane = plane.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
pix_mp
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => bindings::v4l2_format__bindgen_ty_1 {
|
||||||
|
pix: {
|
||||||
|
if format.plane_fmt.len() > 1 {
|
||||||
|
return Err(Self::Error::TooManyPlanes(format.plane_fmt.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (bytesperline, sizeimage) = if !format.plane_fmt.is_empty() {
|
||||||
|
(
|
||||||
|
format.plane_fmt[0].bytesperline,
|
||||||
|
format.plane_fmt[0].sizeimage,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
bindings::v4l2_pix_format {
|
||||||
|
width: format.width,
|
||||||
|
height: format.height,
|
||||||
|
pixelformat: format.pixelformat.into(),
|
||||||
|
bytesperline,
|
||||||
|
sizeimage,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PlaneLayout> for bindings::v4l2_plane_pix_format {
|
||||||
|
fn from(plane: &PlaneLayout) -> Self {
|
||||||
|
bindings::v4l2_plane_pix_format {
|
||||||
|
sizeimage: plane.sizeimage,
|
||||||
|
bytesperline: plane.bytesperline,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_format;
|
||||||
|
nix::ioctl_readwrite!(vidioc_g_fmt, b'V', 4, v4l2_format);
|
||||||
|
nix::ioctl_readwrite!(vidioc_s_fmt, b'V', 5, v4l2_format);
|
||||||
|
nix::ioctl_readwrite!(vidioc_try_fmt, b'V', 64, v4l2_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GFmtError {
|
||||||
|
#[error("error while converting from V4L2 format")]
|
||||||
|
FromV4L2FormatConversionError,
|
||||||
|
#[error("invalid buffer type requested")]
|
||||||
|
InvalidBufferType,
|
||||||
|
#[error("unexpected ioctl error: {0}")]
|
||||||
|
IoctlError(nix::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GFmtError> for Errno {
|
||||||
|
fn from(err: GFmtError) -> Self {
|
||||||
|
match err {
|
||||||
|
GFmtError::FromV4L2FormatConversionError => Errno::EINVAL,
|
||||||
|
GFmtError::InvalidBufferType => Errno::EINVAL,
|
||||||
|
GFmtError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_G_FMT` ioctl.
|
||||||
|
pub fn g_fmt<O: TryFrom<v4l2_format>>(fd: &impl AsRawFd, queue: QueueType) -> Result<O, GFmtError> {
|
||||||
|
let mut fmt = v4l2_format {
|
||||||
|
type_: queue as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_g_fmt(fd.as_raw_fd(), &mut fmt) } {
|
||||||
|
Ok(_) => Ok(fmt
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| GFmtError::FromV4L2FormatConversionError)?),
|
||||||
|
Err(Errno::EINVAL) => Err(GFmtError::InvalidBufferType),
|
||||||
|
Err(e) => Err(GFmtError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SFmtError {
|
||||||
|
#[error("error while converting from V4L2 format")]
|
||||||
|
FromV4L2FormatConversionError,
|
||||||
|
#[error("error while converting to V4L2 format")]
|
||||||
|
ToV4L2FormatConversionError,
|
||||||
|
#[error("invalid buffer type requested")]
|
||||||
|
InvalidBufferType,
|
||||||
|
#[error("device currently busy")]
|
||||||
|
DeviceBusy,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(nix::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SFmtError> for Errno {
|
||||||
|
fn from(err: SFmtError) -> Self {
|
||||||
|
match err {
|
||||||
|
SFmtError::FromV4L2FormatConversionError => Errno::EINVAL,
|
||||||
|
SFmtError::ToV4L2FormatConversionError => Errno::EINVAL,
|
||||||
|
SFmtError::InvalidBufferType => Errno::EINVAL,
|
||||||
|
SFmtError::DeviceBusy => Errno::EBUSY,
|
||||||
|
SFmtError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_S_FMT` ioctl.
|
||||||
|
pub fn s_fmt<I: TryInto<v4l2_format>, O: TryFrom<v4l2_format>>(
|
||||||
|
fd: &mut impl AsRawFd,
|
||||||
|
format: I,
|
||||||
|
) -> Result<O, SFmtError> {
|
||||||
|
let mut fmt: v4l2_format = format
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| SFmtError::ToV4L2FormatConversionError)?;
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_s_fmt(fd.as_raw_fd(), &mut fmt) } {
|
||||||
|
Ok(_) => Ok(fmt
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| SFmtError::FromV4L2FormatConversionError)?),
|
||||||
|
Err(Errno::EINVAL) => Err(SFmtError::InvalidBufferType),
|
||||||
|
Err(Errno::EBUSY) => Err(SFmtError::DeviceBusy),
|
||||||
|
Err(e) => Err(SFmtError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum TryFmtError {
|
||||||
|
#[error("error while converting from V4L2 format")]
|
||||||
|
FromV4L2FormatConversionError,
|
||||||
|
#[error("error while converting to V4L2 format")]
|
||||||
|
ToV4L2FormatConversionError,
|
||||||
|
#[error("invalid buffer type requested")]
|
||||||
|
InvalidBufferType,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(nix::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TryFmtError> for Errno {
|
||||||
|
fn from(err: TryFmtError) -> Self {
|
||||||
|
match err {
|
||||||
|
TryFmtError::FromV4L2FormatConversionError => Errno::EINVAL,
|
||||||
|
TryFmtError::ToV4L2FormatConversionError => Errno::EINVAL,
|
||||||
|
TryFmtError::InvalidBufferType => Errno::EINVAL,
|
||||||
|
TryFmtError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_TRY_FMT` ioctl.
|
||||||
|
pub fn try_fmt<I: TryInto<v4l2_format>, O: TryFrom<v4l2_format>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
format: I,
|
||||||
|
) -> Result<O, TryFmtError> {
|
||||||
|
let mut fmt: v4l2_format = format
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| TryFmtError::ToV4L2FormatConversionError)?;
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_try_fmt(fd.as_raw_fd(), &mut fmt) } {
|
||||||
|
Ok(_) => Ok(fmt
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| TryFmtError::FromV4L2FormatConversionError)?),
|
||||||
|
Err(Errno::EINVAL) => Err(TryFmtError::InvalidBufferType),
|
||||||
|
Err(e) => Err(TryFmtError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Convert from Format to multi-planar v4l2_format and back.
|
||||||
|
fn mplane_to_v4l2_format() {
|
||||||
|
// This is not a real format but let us use unique values per field.
|
||||||
|
let mplane = Format {
|
||||||
|
width: 632,
|
||||||
|
height: 480,
|
||||||
|
pixelformat: b"NM12".into(),
|
||||||
|
plane_fmt: vec![
|
||||||
|
PlaneLayout {
|
||||||
|
sizeimage: 307200,
|
||||||
|
bytesperline: 640,
|
||||||
|
},
|
||||||
|
PlaneLayout {
|
||||||
|
sizeimage: 153600,
|
||||||
|
bytesperline: 320,
|
||||||
|
},
|
||||||
|
PlaneLayout {
|
||||||
|
sizeimage: 76800,
|
||||||
|
bytesperline: 160,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let v4l2_format = v4l2_format {
|
||||||
|
..(QueueType::VideoCaptureMplane, &mplane).try_into().unwrap()
|
||||||
|
};
|
||||||
|
let mplane2: Format = v4l2_format.try_into().unwrap();
|
||||||
|
assert_eq!(mplane, mplane2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Convert from Format to single-planar v4l2_format and back.
|
||||||
|
fn splane_to_v4l2_format() {
|
||||||
|
// This is not a real format but let us use unique values per field.
|
||||||
|
let splane = Format {
|
||||||
|
width: 632,
|
||||||
|
height: 480,
|
||||||
|
pixelformat: b"NV12".into(),
|
||||||
|
plane_fmt: vec![PlaneLayout {
|
||||||
|
sizeimage: 307200,
|
||||||
|
bytesperline: 640,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
// Conversion to/from single-planar format.
|
||||||
|
let v4l2_format = v4l2_format {
|
||||||
|
..(QueueType::VideoCapture, &splane).try_into().unwrap()
|
||||||
|
};
|
||||||
|
let splane2: Format = v4l2_format.try_into().unwrap();
|
||||||
|
assert_eq!(splane, splane2);
|
||||||
|
|
||||||
|
// Trying to use a multi-planar format with the single-planar API should
|
||||||
|
// fail.
|
||||||
|
let mplane = Format {
|
||||||
|
width: 632,
|
||||||
|
height: 480,
|
||||||
|
pixelformat: b"NM12".into(),
|
||||||
|
// This is not a real format but let us use unique values per field.
|
||||||
|
plane_fmt: vec![
|
||||||
|
PlaneLayout {
|
||||||
|
sizeimage: 307200,
|
||||||
|
bytesperline: 640,
|
||||||
|
},
|
||||||
|
PlaneLayout {
|
||||||
|
sizeimage: 153600,
|
||||||
|
bytesperline: 320,
|
||||||
|
},
|
||||||
|
PlaneLayout {
|
||||||
|
sizeimage: 76800,
|
||||||
|
bytesperline: 160,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
TryInto::<v4l2_format>::try_into((QueueType::VideoCapture, &mplane)).err(),
|
||||||
|
Some(FormatConversionError::TooManyPlanes(3))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
149
libs/v4l2r/src/ioctl/g_parm.rs
Normal file
149
libs/v4l2r/src/ioctl/g_parm.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bindings::v4l2_standard;
|
||||||
|
use crate::bindings::v4l2_std_id;
|
||||||
|
use crate::bindings::v4l2_streamparm;
|
||||||
|
use crate::QueueType;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_standard;
|
||||||
|
use crate::bindings::v4l2_std_id;
|
||||||
|
use crate::bindings::v4l2_streamparm;
|
||||||
|
|
||||||
|
nix::ioctl_readwrite!(vidioc_g_parm, b'V', 21, v4l2_streamparm);
|
||||||
|
nix::ioctl_readwrite!(vidioc_s_parm, b'V', 22, v4l2_streamparm);
|
||||||
|
nix::ioctl_read!(vidioc_g_std, b'V', 23, v4l2_std_id);
|
||||||
|
nix::ioctl_write_ptr!(vidioc_s_std, b'V', 24, v4l2_std_id);
|
||||||
|
nix::ioctl_readwrite!(vidioc_enumstd, b'V', 25, v4l2_standard);
|
||||||
|
nix::ioctl_read!(vidioc_querystd, b'V', 63, v4l2_std_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GParmError {
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GParmError> for Errno {
|
||||||
|
fn from(err: GParmError) -> Self {
|
||||||
|
match err {
|
||||||
|
GParmError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_G_PARM` ioctl.
|
||||||
|
pub fn g_parm<O: From<v4l2_streamparm>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
queue: QueueType,
|
||||||
|
) -> Result<O, GParmError> {
|
||||||
|
let mut parm = v4l2_streamparm {
|
||||||
|
type_: queue as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_g_parm(fd.as_raw_fd(), &mut parm) } {
|
||||||
|
Ok(_) => Ok(O::from(parm)),
|
||||||
|
Err(e) => Err(GParmError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_S_PARM` ioctl.
|
||||||
|
pub fn s_parm<I: Into<v4l2_streamparm>, O: From<v4l2_streamparm>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
parm: I,
|
||||||
|
) -> Result<O, GParmError> {
|
||||||
|
let mut parm = parm.into();
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_s_parm(fd.as_raw_fd(), &mut parm) } {
|
||||||
|
Ok(_) => Ok(O::from(parm)),
|
||||||
|
Err(e) => Err(GParmError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_G_STD` ioctl.
|
||||||
|
pub fn g_std<O: From<v4l2_std_id>>(fd: &impl AsRawFd) -> Result<O, GParmError> {
|
||||||
|
let mut std_id: v4l2_std_id = 0;
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_g_std(fd.as_raw_fd(), &mut std_id) } {
|
||||||
|
Ok(_) => Ok(O::from(std_id)),
|
||||||
|
Err(e) => Err(GParmError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SStdError {
|
||||||
|
#[error("unsupported standard requested")]
|
||||||
|
Unsupported,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SStdError> for Errno {
|
||||||
|
fn from(err: SStdError) -> Self {
|
||||||
|
match err {
|
||||||
|
SStdError::Unsupported => Errno::EINVAL,
|
||||||
|
SStdError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_S_STD` ioctl.
|
||||||
|
pub fn s_std<I: Into<v4l2_std_id>>(fd: &impl AsRawFd, std_id: I) -> Result<(), SStdError> {
|
||||||
|
let std_id = std_id.into();
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_s_std(fd.as_raw_fd(), &std_id) } {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(Errno::EINVAL) => Err(SStdError::Unsupported),
|
||||||
|
Err(e) => Err(SStdError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum EnumStdError {
|
||||||
|
#[error("requested index is out of bounds")]
|
||||||
|
OutOfBounds,
|
||||||
|
#[error("standard video timings are not supported for this input or output")]
|
||||||
|
Unsupported,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EnumStdError> for Errno {
|
||||||
|
fn from(err: EnumStdError) -> Self {
|
||||||
|
match err {
|
||||||
|
EnumStdError::OutOfBounds => Errno::EINVAL,
|
||||||
|
EnumStdError::Unsupported => Errno::ENODATA,
|
||||||
|
EnumStdError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_ENUMSTD` ioctl.
|
||||||
|
pub fn enumstd<O: From<v4l2_standard>>(fd: &impl AsRawFd, index: u32) -> Result<O, EnumStdError> {
|
||||||
|
let mut standard = v4l2_standard {
|
||||||
|
index,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_enumstd(fd.as_raw_fd(), &mut standard) } {
|
||||||
|
Ok(_) => Ok(O::from(standard)),
|
||||||
|
Err(Errno::EINVAL) => Err(EnumStdError::OutOfBounds),
|
||||||
|
Err(Errno::ENODATA) => Err(EnumStdError::Unsupported),
|
||||||
|
Err(e) => Err(EnumStdError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_QUERYSTD` ioctl.
|
||||||
|
pub fn querystd<O: From<v4l2_std_id>>(fd: &impl AsRawFd) -> Result<O, GParmError> {
|
||||||
|
let mut std_id: v4l2_std_id = 0;
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_querystd(fd.as_raw_fd(), &mut std_id) } {
|
||||||
|
Ok(_) => Ok(O::from(std_id)),
|
||||||
|
Err(e) => Err(GParmError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
130
libs/v4l2r/src/ioctl/g_selection.rs
Normal file
130
libs/v4l2r/src/ioctl/g_selection.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
use bitflags::bitflags;
|
||||||
|
use enumn::N;
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::bindings::v4l2_rect;
|
||||||
|
use crate::bindings::v4l2_selection;
|
||||||
|
|
||||||
|
#[derive(Debug, N, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum SelectionType {
|
||||||
|
Capture = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_CAPTURE,
|
||||||
|
Output = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_OUTPUT,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, N, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum SelectionTarget {
|
||||||
|
Crop = bindings::V4L2_SEL_TGT_CROP,
|
||||||
|
CropDefault = bindings::V4L2_SEL_TGT_CROP_DEFAULT,
|
||||||
|
CropBounds = bindings::V4L2_SEL_TGT_CROP_BOUNDS,
|
||||||
|
NativeSize = bindings::V4L2_SEL_TGT_NATIVE_SIZE,
|
||||||
|
Compose = bindings::V4L2_SEL_TGT_COMPOSE,
|
||||||
|
ComposeDefault = bindings::V4L2_SEL_TGT_COMPOSE_DEFAULT,
|
||||||
|
ComposeBounds = bindings::V4L2_SEL_TGT_COMPOSE_BOUNDS,
|
||||||
|
ComposePadded = bindings::V4L2_SEL_TGT_COMPOSE_PADDED,
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct SelectionFlags: u32 {
|
||||||
|
const GE = bindings::V4L2_SEL_FLAG_GE;
|
||||||
|
const LE = bindings::V4L2_SEL_FLAG_LE;
|
||||||
|
const KEEP_CONFIG = bindings::V4L2_SEL_FLAG_KEEP_CONFIG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_selection;
|
||||||
|
nix::ioctl_readwrite!(vidioc_g_selection, b'V', 94, v4l2_selection);
|
||||||
|
nix::ioctl_readwrite!(vidioc_s_selection, b'V', 95, v4l2_selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GSelectionError {
|
||||||
|
#[error("invalid type or target requested")]
|
||||||
|
Invalid,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GSelectionError> for Errno {
|
||||||
|
fn from(err: GSelectionError) -> Self {
|
||||||
|
match err {
|
||||||
|
GSelectionError::Invalid => Errno::EINVAL,
|
||||||
|
GSelectionError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_G_SELECTION` ioctl.
|
||||||
|
pub fn g_selection<R: From<v4l2_rect>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
selection: SelectionType,
|
||||||
|
target: SelectionTarget,
|
||||||
|
) -> Result<R, GSelectionError> {
|
||||||
|
let mut sel = v4l2_selection {
|
||||||
|
type_: selection as u32,
|
||||||
|
target: target as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_g_selection(fd.as_raw_fd(), &mut sel) } {
|
||||||
|
Ok(_) => Ok(R::from(sel.r)),
|
||||||
|
Err(Errno::EINVAL) => Err(GSelectionError::Invalid),
|
||||||
|
Err(e) => Err(GSelectionError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SSelectionError {
|
||||||
|
#[error("invalid type or target requested")]
|
||||||
|
Invalid,
|
||||||
|
#[error("invalid range requested")]
|
||||||
|
InvalidRange,
|
||||||
|
#[error("cannot change selection rectangle currently")]
|
||||||
|
Busy,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(nix::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SSelectionError> for Errno {
|
||||||
|
fn from(err: SSelectionError) -> Self {
|
||||||
|
match err {
|
||||||
|
SSelectionError::Invalid => Errno::EINVAL,
|
||||||
|
SSelectionError::InvalidRange => Errno::ERANGE,
|
||||||
|
SSelectionError::Busy => Errno::EBUSY,
|
||||||
|
SSelectionError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_S_SELECTION` ioctl.
|
||||||
|
pub fn s_selection<RI: Into<v4l2_rect>, RO: From<v4l2_rect>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
selection: SelectionType,
|
||||||
|
target: SelectionTarget,
|
||||||
|
rect: RI,
|
||||||
|
flags: SelectionFlags,
|
||||||
|
) -> Result<RO, SSelectionError> {
|
||||||
|
let mut sel = v4l2_selection {
|
||||||
|
type_: selection as u32,
|
||||||
|
target: target as u32,
|
||||||
|
flags: flags.bits(),
|
||||||
|
r: rect.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_s_selection(fd.as_raw_fd(), &mut sel) } {
|
||||||
|
Ok(_) => Ok(RO::from(sel.r)),
|
||||||
|
Err(Errno::EINVAL) => Err(SSelectionError::Invalid),
|
||||||
|
Err(Errno::ERANGE) => Err(SSelectionError::InvalidRange),
|
||||||
|
Err(Errno::EBUSY) => Err(SSelectionError::Busy),
|
||||||
|
Err(e) => Err(SSelectionError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
117
libs/v4l2r/src/ioctl/mmap.rs
Normal file
117
libs/v4l2r/src/ioctl/mmap.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use core::num::NonZeroUsize;
|
||||||
|
use std::{
|
||||||
|
cmp::{max, min},
|
||||||
|
ops::Deref,
|
||||||
|
ptr::NonNull,
|
||||||
|
slice,
|
||||||
|
};
|
||||||
|
use std::{ops::DerefMut, os::unix::io::AsFd};
|
||||||
|
|
||||||
|
use log::error;
|
||||||
|
use nix::{errno::Errno, libc::off_t, sys::mman};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub struct PlaneMapping {
|
||||||
|
// A mapping remains valid until we munmap it, that is, until the
|
||||||
|
// PlaneMapping object is deleted. Hence the static lifetime.
|
||||||
|
pub data: &'static mut [u8],
|
||||||
|
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaneMapping {
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
self.end - self.start
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restrict(mut self, start: usize, end: usize) -> Self {
|
||||||
|
self.start = max(self.start, start);
|
||||||
|
self.end = min(self.end, end);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for PlaneMapping {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.data[self.start..self.end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsMut<[u8]> for PlaneMapping {
|
||||||
|
fn as_mut(&mut self) -> &mut [u8] {
|
||||||
|
&mut self.data[self.start..self.end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for PlaneMapping {
|
||||||
|
type Target = [u8];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.data[self.start..self.end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for PlaneMapping {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.data[self.start..self.end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PlaneMapping {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Safe because the pointer and length were constructed in mmap() and
|
||||||
|
// are always valid.
|
||||||
|
unsafe {
|
||||||
|
mman::munmap(
|
||||||
|
NonNull::new_unchecked(self.data.as_mut_ptr().cast()),
|
||||||
|
self.data.len(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
error!("Error while unmapping plane: {}", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum MmapError {
|
||||||
|
#[error("provided length was 0")]
|
||||||
|
ZeroLength,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(#[from] Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MmapError> for Errno {
|
||||||
|
fn from(err: MmapError) -> Self {
|
||||||
|
match err {
|
||||||
|
MmapError::ZeroLength => Errno::EINVAL,
|
||||||
|
MmapError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO should be unsafe because the mapping can be used after a buffer is queued?
|
||||||
|
// Or not, since this cannot cause a crash...
|
||||||
|
pub fn mmap(fd: &impl AsFd, mem_offset: u32, length: u32) -> Result<PlaneMapping, MmapError> {
|
||||||
|
let non_zero_length = NonZeroUsize::new(length as usize).ok_or(MmapError::ZeroLength)?;
|
||||||
|
let data = unsafe {
|
||||||
|
mman::mmap(
|
||||||
|
None,
|
||||||
|
non_zero_length,
|
||||||
|
mman::ProtFlags::PROT_READ | mman::ProtFlags::PROT_WRITE,
|
||||||
|
mman::MapFlags::MAP_SHARED,
|
||||||
|
fd,
|
||||||
|
mem_offset as off_t,
|
||||||
|
)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(PlaneMapping {
|
||||||
|
// Safe because we know the pointer is valid and has enough data mapped
|
||||||
|
// to cover the length.
|
||||||
|
data: unsafe { slice::from_raw_parts_mut(data.as_ptr().cast(), length as usize) },
|
||||||
|
start: 0,
|
||||||
|
end: length as usize,
|
||||||
|
})
|
||||||
|
}
|
||||||
197
libs/v4l2r/src/ioctl/qbuf.rs
Normal file
197
libs/v4l2r/src/ioctl/qbuf.rs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
//! Safe wrapper for the VIDIOC_(D)QBUF and VIDIOC_QUERYBUF ioctls.
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use nix::libc::{suseconds_t, time_t};
|
||||||
|
use nix::sys::time::{TimeVal, TimeValLike};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::ioctl::ioctl_and_convert;
|
||||||
|
use crate::ioctl::BufferFlags;
|
||||||
|
use crate::ioctl::IoctlConvertError;
|
||||||
|
use crate::ioctl::IoctlConvertResult;
|
||||||
|
use crate::ioctl::UncheckedV4l2Buffer;
|
||||||
|
use crate::memory::Memory;
|
||||||
|
use crate::memory::PlaneHandle;
|
||||||
|
use crate::QueueType;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum QBufIoctlError {
|
||||||
|
#[error("invalid number of planes specified for the buffer: got {0}, expected {1}")]
|
||||||
|
NumPlanesMismatch(usize, usize),
|
||||||
|
#[error("data offset specified while using the single-planar API")]
|
||||||
|
DataOffsetNotSupported,
|
||||||
|
#[error("unexpected ioctl error: {0}")]
|
||||||
|
Other(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Errno> for QBufIoctlError {
|
||||||
|
fn from(errno: Errno) -> Self {
|
||||||
|
Self::Other(errno)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<QBufIoctlError> for Errno {
|
||||||
|
fn from(err: QBufIoctlError) -> Self {
|
||||||
|
match err {
|
||||||
|
QBufIoctlError::NumPlanesMismatch(_, _) => Errno::EINVAL,
|
||||||
|
QBufIoctlError::DataOffsetNotSupported => Errno::EINVAL,
|
||||||
|
QBufIoctlError::Other(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Representation of a single plane of a V4L2 buffer.
|
||||||
|
pub struct QBufPlane(pub bindings::v4l2_plane);
|
||||||
|
|
||||||
|
impl QBufPlane {
|
||||||
|
// TODO remove as this is not safe - we should always specify a handle.
|
||||||
|
pub fn new(bytes_used: usize) -> Self {
|
||||||
|
QBufPlane(bindings::v4l2_plane {
|
||||||
|
bytesused: bytes_used as u32,
|
||||||
|
data_offset: 0,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_from_handle<H: PlaneHandle>(handle: &H, bytes_used: usize) -> Self {
|
||||||
|
let mut plane = Self::new(bytes_used);
|
||||||
|
handle.fill_v4l2_plane(&mut plane.0);
|
||||||
|
plane
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for QBufPlane {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("QBufPlane")
|
||||||
|
.field("bytesused", &self.0.bytesused)
|
||||||
|
.field("data_offset", &self.0.data_offset)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains all the information that can be passed to the `qbuf` ioctl.
|
||||||
|
// TODO Change this to contain a v4l2_buffer, and create constructors/methods
|
||||||
|
// to change it? Then during qbuf we just need to set m.planes to planes
|
||||||
|
// (after resizing it to 8) and we are good to use it as-is.
|
||||||
|
// We could even turn the trait into AsRef<v4l2_buffer> for good measure.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct QBuffer<H: PlaneHandle> {
|
||||||
|
index: u32,
|
||||||
|
queue: QueueType,
|
||||||
|
pub flags: BufferFlags,
|
||||||
|
pub field: u32,
|
||||||
|
pub sequence: u32,
|
||||||
|
pub timestamp: TimeVal,
|
||||||
|
pub planes: Vec<QBufPlane>,
|
||||||
|
pub _h: std::marker::PhantomData<H>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H: PlaneHandle> QBuffer<H> {
|
||||||
|
pub fn new(queue: QueueType, index: u32) -> Self {
|
||||||
|
QBuffer {
|
||||||
|
index,
|
||||||
|
queue,
|
||||||
|
flags: Default::default(),
|
||||||
|
field: Default::default(),
|
||||||
|
sequence: Default::default(),
|
||||||
|
timestamp: TimeVal::zero(),
|
||||||
|
planes: Vec::new(),
|
||||||
|
_h: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H: PlaneHandle> QBuffer<H> {
|
||||||
|
pub fn set_timestamp(mut self, sec: time_t, usec: suseconds_t) -> Self {
|
||||||
|
self.timestamp = TimeVal::new(sec, usec);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H: PlaneHandle> From<QBuffer<H>> for UncheckedV4l2Buffer {
|
||||||
|
fn from(qbuf: QBuffer<H>) -> Self {
|
||||||
|
let mut v4l2_buf = UncheckedV4l2Buffer::new_for_querybuf(qbuf.queue, Some(qbuf.index));
|
||||||
|
v4l2_buf.0.index = qbuf.index;
|
||||||
|
v4l2_buf.0.type_ = qbuf.queue as u32;
|
||||||
|
v4l2_buf.0.memory = H::Memory::MEMORY_TYPE as u32;
|
||||||
|
v4l2_buf.0.flags = qbuf.flags.bits();
|
||||||
|
v4l2_buf.0.field = qbuf.field;
|
||||||
|
v4l2_buf.0.sequence = qbuf.sequence;
|
||||||
|
v4l2_buf.0.timestamp.tv_sec = qbuf.timestamp.tv_sec();
|
||||||
|
v4l2_buf.0.timestamp.tv_usec = qbuf.timestamp.tv_usec();
|
||||||
|
if let Some(planes) = &mut v4l2_buf.1 {
|
||||||
|
for (dst_plane, src_plane) in planes.iter_mut().zip(qbuf.planes.into_iter()) {
|
||||||
|
*dst_plane = src_plane.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let plane = &qbuf.planes[0];
|
||||||
|
|
||||||
|
v4l2_buf.0.length = plane.0.length;
|
||||||
|
v4l2_buf.0.bytesused = plane.0.bytesused;
|
||||||
|
v4l2_buf.0.m = (&plane.0.m, H::Memory::MEMORY_TYPE).into();
|
||||||
|
}
|
||||||
|
|
||||||
|
v4l2_buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_buffer;
|
||||||
|
nix::ioctl_readwrite!(vidioc_querybuf, b'V', 9, v4l2_buffer);
|
||||||
|
nix::ioctl_readwrite!(vidioc_qbuf, b'V', 15, v4l2_buffer);
|
||||||
|
nix::ioctl_readwrite!(vidioc_dqbuf, b'V', 17, v4l2_buffer);
|
||||||
|
nix::ioctl_readwrite!(vidioc_prepare_buf, b'V', 93, v4l2_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type QBufError<CE> = IoctlConvertError<QBufIoctlError, CE>;
|
||||||
|
pub type QBufResult<O, CE> = IoctlConvertResult<O, QBufIoctlError, CE>;
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_QBUF` ioctl.
|
||||||
|
///
|
||||||
|
/// TODO: `qbuf` should be unsafe! The following invariants need to be guaranteed
|
||||||
|
/// by the caller:
|
||||||
|
///
|
||||||
|
/// For MMAP buffers, any mapping must not be accessed by the caller (or any
|
||||||
|
/// mapping must be unmapped before queueing?). Also if the buffer has been
|
||||||
|
/// DMABUF-exported, its consumers must likewise not access it.
|
||||||
|
///
|
||||||
|
/// For DMABUF buffers, the FD must not be duplicated and accessed anywhere else.
|
||||||
|
///
|
||||||
|
/// For USERPTR buffers, things are most tricky. Not only must the data not be
|
||||||
|
/// accessed by anyone else, the caller also needs to guarantee that the backing
|
||||||
|
/// memory won't be freed until the corresponding buffer is returned by either
|
||||||
|
/// `dqbuf` or `streamoff`.
|
||||||
|
pub fn qbuf<I, O>(fd: &impl AsRawFd, buffer: I) -> QBufResult<O, O::Error>
|
||||||
|
where
|
||||||
|
I: Into<UncheckedV4l2Buffer>,
|
||||||
|
O: TryFrom<UncheckedV4l2Buffer>,
|
||||||
|
O::Error: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let mut v4l2_buf: UncheckedV4l2Buffer = buffer.into();
|
||||||
|
|
||||||
|
ioctl_and_convert(
|
||||||
|
unsafe { ioctl::vidioc_qbuf(fd.as_raw_fd(), v4l2_buf.as_mut()) }
|
||||||
|
.map(|_| v4l2_buf)
|
||||||
|
.map_err(Into::into),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_PREPARE_BUF` ioctl.
|
||||||
|
pub fn prepare_buf<I, O>(fd: &impl AsRawFd, buffer: I) -> QBufResult<O, O::Error>
|
||||||
|
where
|
||||||
|
I: Into<UncheckedV4l2Buffer>,
|
||||||
|
O: TryFrom<UncheckedV4l2Buffer>,
|
||||||
|
O::Error: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let mut v4l2_buf: UncheckedV4l2Buffer = buffer.into();
|
||||||
|
|
||||||
|
ioctl_and_convert(
|
||||||
|
unsafe { ioctl::vidioc_prepare_buf(fd.as_raw_fd(), v4l2_buf.as_mut()) }
|
||||||
|
.map(|_| v4l2_buf)
|
||||||
|
.map_err(Into::into),
|
||||||
|
)
|
||||||
|
}
|
||||||
113
libs/v4l2r/src/ioctl/querybuf.rs
Normal file
113
libs/v4l2r/src/ioctl/querybuf.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
use std::convert::Infallible;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::ioctl::ioctl_and_convert;
|
||||||
|
use crate::ioctl::BufferFlags;
|
||||||
|
use crate::ioctl::IoctlConvertError;
|
||||||
|
use crate::ioctl::IoctlConvertResult;
|
||||||
|
use crate::ioctl::UncheckedV4l2Buffer;
|
||||||
|
use crate::QueueType;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct QueryBufPlane {
|
||||||
|
/// Offset to pass to `mmap()` in order to obtain a mapping for this plane.
|
||||||
|
pub mem_offset: u32,
|
||||||
|
/// Length of this plane.
|
||||||
|
pub length: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains information about a buffer's layout, as obtained from [`crate::ioctl::querybuf`].
|
||||||
|
///
|
||||||
|
/// It is a subset of [`crate::ioctl::V4l2Buffer`], only more convenient on occasion because its
|
||||||
|
/// conversion from an unchecked v4l2_buffer cannot fail.
|
||||||
|
///
|
||||||
|
/// Single-planar buffers have one entry in [`planes`] representing the layout of their unique
|
||||||
|
/// plane.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct QueryBuffer {
|
||||||
|
pub index: usize,
|
||||||
|
pub flags: BufferFlags,
|
||||||
|
pub planes: Vec<QueryBufPlane>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<UncheckedV4l2Buffer> for QueryBuffer {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn try_from(buffer: UncheckedV4l2Buffer) -> Result<Self, Self::Error> {
|
||||||
|
let v4l2_buf = buffer.0;
|
||||||
|
let planes = match buffer.1 {
|
||||||
|
None => vec![QueryBufPlane {
|
||||||
|
mem_offset: unsafe { v4l2_buf.m.offset },
|
||||||
|
length: v4l2_buf.length,
|
||||||
|
}],
|
||||||
|
Some(v4l2_planes) => v4l2_planes
|
||||||
|
.iter()
|
||||||
|
.take(v4l2_buf.length as usize)
|
||||||
|
.map(|v4l2_plane| QueryBufPlane {
|
||||||
|
mem_offset: unsafe { v4l2_plane.m.mem_offset },
|
||||||
|
length: v4l2_plane.length,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(QueryBuffer {
|
||||||
|
index: v4l2_buf.index as usize,
|
||||||
|
flags: BufferFlags::from_bits_truncate(v4l2_buf.flags),
|
||||||
|
planes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_buffer;
|
||||||
|
nix::ioctl_readwrite!(vidioc_querybuf, b'V', 9, v4l2_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum QueryBufIoctlError {
|
||||||
|
#[error("unsupported queue or out-of-bounds index")]
|
||||||
|
InvalidInput,
|
||||||
|
#[error("unexpected ioctl error: {0}")]
|
||||||
|
Other(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Errno> for QueryBufIoctlError {
|
||||||
|
fn from(err: Errno) -> Self {
|
||||||
|
match err {
|
||||||
|
Errno::EINVAL => QueryBufIoctlError::InvalidInput,
|
||||||
|
e => QueryBufIoctlError::Other(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<QueryBufIoctlError> for Errno {
|
||||||
|
fn from(err: QueryBufIoctlError) -> Self {
|
||||||
|
match err {
|
||||||
|
QueryBufIoctlError::InvalidInput => Errno::EINVAL,
|
||||||
|
QueryBufIoctlError::Other(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type QueryBufError<CE> = IoctlConvertError<QueryBufIoctlError, CE>;
|
||||||
|
pub type QueryBufResult<O, CE> = IoctlConvertResult<O, QueryBufIoctlError, CE>;
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_QUERYBUF` ioctl.
|
||||||
|
pub fn querybuf<O>(fd: &impl AsRawFd, queue: QueueType, index: usize) -> QueryBufResult<O, O::Error>
|
||||||
|
where
|
||||||
|
O: TryFrom<UncheckedV4l2Buffer>,
|
||||||
|
O::Error: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let mut v4l2_buf = UncheckedV4l2Buffer::new_for_querybuf(queue, Some(index as u32));
|
||||||
|
|
||||||
|
ioctl_and_convert(
|
||||||
|
unsafe { ioctl::vidioc_querybuf(fd.as_raw_fd(), v4l2_buf.as_mut()) }
|
||||||
|
.map(|_| v4l2_buf)
|
||||||
|
.map_err(Into::into),
|
||||||
|
)
|
||||||
|
}
|
||||||
136
libs/v4l2r/src/ioctl/querycap.rs
Normal file
136
libs/v4l2r/src/ioctl/querycap.rs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
//! Safe wrapper for the `VIDIOC_QUERYCAP` ioctl.
|
||||||
|
use super::string_from_cstr;
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::bindings::v4l2_capability;
|
||||||
|
use bitflags::bitflags;
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use std::fmt;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Flags returned by the `VIDIOC_QUERYCAP` ioctl into the `capabilities`
|
||||||
|
/// or `device_capabilities` field of `v4l2_capability`.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Capabilities: u32 {
|
||||||
|
const VIDEO_CAPTURE = bindings::V4L2_CAP_VIDEO_CAPTURE;
|
||||||
|
const VIDEO_OUTPUT = bindings::V4L2_CAP_VIDEO_OUTPUT;
|
||||||
|
const VIDEO_OVERLAY = bindings::V4L2_CAP_VIDEO_OVERLAY;
|
||||||
|
const VBI_CAPTURE = bindings::V4L2_CAP_VBI_CAPTURE;
|
||||||
|
const VBI_OUTPUT = bindings::V4L2_CAP_VBI_OUTPUT;
|
||||||
|
const SLICED_VBI_CAPTURE = bindings::V4L2_CAP_SLICED_VBI_CAPTURE;
|
||||||
|
const SLICED_VBI_OUTPUT = bindings::V4L2_CAP_SLICED_VBI_OUTPUT;
|
||||||
|
const RDS_CAPTURE = bindings::V4L2_CAP_RDS_CAPTURE;
|
||||||
|
const VIDEO_OUTPUT_OVERLAY = bindings::V4L2_CAP_VIDEO_OUTPUT_OVERLAY;
|
||||||
|
const HW_FREQ_SEEK = bindings::V4L2_CAP_HW_FREQ_SEEK;
|
||||||
|
const RDS_OUTPUT = bindings::V4L2_CAP_RDS_OUTPUT;
|
||||||
|
|
||||||
|
const VIDEO_CAPTURE_MPLANE = bindings::V4L2_CAP_VIDEO_CAPTURE_MPLANE;
|
||||||
|
const VIDEO_OUTPUT_MPLANE = bindings::V4L2_CAP_VIDEO_OUTPUT_MPLANE;
|
||||||
|
const VIDEO_M2M_MPLANE = bindings::V4L2_CAP_VIDEO_M2M_MPLANE;
|
||||||
|
const VIDEO_M2M = bindings::V4L2_CAP_VIDEO_M2M;
|
||||||
|
|
||||||
|
const TUNER = bindings::V4L2_CAP_TUNER;
|
||||||
|
const AUDIO = bindings::V4L2_CAP_AUDIO;
|
||||||
|
const RADIO = bindings::V4L2_CAP_RADIO;
|
||||||
|
const MODULATOR = bindings::V4L2_CAP_MODULATOR;
|
||||||
|
|
||||||
|
const SDR_CAPTURE = bindings::V4L2_CAP_SDR_CAPTURE;
|
||||||
|
const EXT_PIX_FORMAT = bindings::V4L2_CAP_EXT_PIX_FORMAT;
|
||||||
|
const SDR_OUTPUT = bindings::V4L2_CAP_SDR_OUTPUT;
|
||||||
|
const META_CAPTURE = bindings::V4L2_CAP_META_CAPTURE;
|
||||||
|
|
||||||
|
const READWRITE = bindings::V4L2_CAP_READWRITE;
|
||||||
|
const ASYNCIO = bindings::V4L2_CAP_ASYNCIO;
|
||||||
|
const STREAMING = bindings::V4L2_CAP_STREAMING;
|
||||||
|
const META_OUTPUT = bindings::V4L2_CAP_META_OUTPUT;
|
||||||
|
|
||||||
|
const TOUCH = bindings::V4L2_CAP_TOUCH;
|
||||||
|
|
||||||
|
const DEVICE_CAPS = bindings::V4L2_CAP_DEVICE_CAPS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Capabilities {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
fmt::Debug::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to get the capability flags from a `VIDIOC_QUERYCAP` ioctl.
|
||||||
|
impl From<v4l2_capability> for Capabilities {
|
||||||
|
fn from(qcap: v4l2_capability) -> Self {
|
||||||
|
Capabilities::from_bits_truncate(qcap.capabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe variant of the `v4l2_capability` struct, to be used with `querycap`.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Capability {
|
||||||
|
pub driver: String,
|
||||||
|
pub card: String,
|
||||||
|
pub bus_info: String,
|
||||||
|
pub version: u32,
|
||||||
|
pub capabilities: Capabilities,
|
||||||
|
pub device_caps: Option<Capabilities>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Capability {
|
||||||
|
/// Returns the set of capabilities of the hardware as a whole.
|
||||||
|
pub fn capabilities(&self) -> Capabilities {
|
||||||
|
self.capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the capabilities that apply to the currently opened V4L2 node.
|
||||||
|
pub fn device_caps(&self) -> Capabilities {
|
||||||
|
self.device_caps
|
||||||
|
.unwrap_or_else(|| self.capabilities.difference(Capabilities::DEVICE_CAPS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<v4l2_capability> for Capability {
|
||||||
|
fn from(qcap: v4l2_capability) -> Self {
|
||||||
|
Capability {
|
||||||
|
driver: string_from_cstr(&qcap.driver).unwrap_or_else(|_| "".into()),
|
||||||
|
card: string_from_cstr(&qcap.card).unwrap_or_else(|_| "".into()),
|
||||||
|
bus_info: string_from_cstr(&qcap.bus_info).unwrap_or_else(|_| "".into()),
|
||||||
|
version: qcap.version,
|
||||||
|
capabilities: Capabilities::from_bits_truncate(qcap.capabilities),
|
||||||
|
device_caps: if qcap.capabilities & bindings::V4L2_CAP_DEVICE_CAPS != 0 {
|
||||||
|
Some(Capabilities::from_bits_truncate(qcap.device_caps))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_capability;
|
||||||
|
nix::ioctl_read!(vidioc_querycap, b'V', 0, v4l2_capability);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum QueryCapError {
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<QueryCapError> for Errno {
|
||||||
|
fn from(err: QueryCapError) -> Self {
|
||||||
|
match err {
|
||||||
|
QueryCapError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_QUERYCAP` ioctl.
|
||||||
|
pub fn querycap<T: From<v4l2_capability>>(fd: &impl AsRawFd) -> Result<T, QueryCapError> {
|
||||||
|
let mut qcap: v4l2_capability = Default::default();
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_querycap(fd.as_raw_fd(), &mut qcap) } {
|
||||||
|
Ok(_) => Ok(T::from(qcap)),
|
||||||
|
Err(e) => Err(QueryCapError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
162
libs/v4l2r/src/ioctl/reqbufs.rs
Normal file
162
libs/v4l2r/src/ioctl/reqbufs.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
//! Safe wrapper for the `VIDIOC_REQBUFS` ioctl.
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::bindings::v4l2_create_buffers;
|
||||||
|
use crate::bindings::v4l2_format;
|
||||||
|
use crate::bindings::v4l2_requestbuffers;
|
||||||
|
use crate::memory::MemoryType;
|
||||||
|
use crate::QueueType;
|
||||||
|
use bitflags::bitflags;
|
||||||
|
use nix::{self, errno::Errno};
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Flags returned by the `VIDIOC_REQBUFS` ioctl into the `capabilities`
|
||||||
|
/// field of `struct v4l2_requestbuffers`.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct BufferCapabilities: u32 {
|
||||||
|
const SUPPORTS_MMAP = bindings::V4L2_BUF_CAP_SUPPORTS_MMAP;
|
||||||
|
const SUPPORTS_USERPTR = bindings::V4L2_BUF_CAP_SUPPORTS_USERPTR;
|
||||||
|
const SUPPORTS_DMABUF = bindings::V4L2_BUF_CAP_SUPPORTS_DMABUF;
|
||||||
|
const SUPPORTS_REQUESTS = bindings::V4L2_BUF_CAP_SUPPORTS_REQUESTS;
|
||||||
|
const SUPPORTS_ORPHANED_BUFS = bindings::V4L2_BUF_CAP_SUPPORTS_ORPHANED_BUFS;
|
||||||
|
const SUPPORTS_M2M_HOLD_CAPTURE_BUF = bindings::V4L2_BUF_CAP_SUPPORTS_M2M_HOLD_CAPTURE_BUF;
|
||||||
|
const SUPPORTS_MMAP_CACHE_HINTS = bindings::V4L2_BUF_CAP_SUPPORTS_MMAP_CACHE_HINTS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Memory Consistency Flags passed to the `VIDIOC_REQBUFS` ioctl in the `flags`
|
||||||
|
/// field of `struct v4l2_requestbuffers`.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct MemoryConsistency: u8 {
|
||||||
|
const MEMORY_FLAG_NON_COHERENT = bindings::V4L2_MEMORY_FLAG_NON_COHERENT as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<v4l2_requestbuffers> for () {
|
||||||
|
fn from(_reqbufs: v4l2_requestbuffers) -> Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In case we are just interested in the number of buffers that `reqbufs`
|
||||||
|
/// created.
|
||||||
|
impl From<v4l2_requestbuffers> for usize {
|
||||||
|
fn from(reqbufs: v4l2_requestbuffers) -> Self {
|
||||||
|
reqbufs.count as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If we just want to query the buffer capabilities.
|
||||||
|
impl From<v4l2_requestbuffers> for BufferCapabilities {
|
||||||
|
fn from(reqbufs: v4l2_requestbuffers) -> Self {
|
||||||
|
BufferCapabilities::from_bits_truncate(reqbufs.capabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full result of the `reqbufs` ioctl.
|
||||||
|
pub struct RequestBuffers {
|
||||||
|
pub count: u32,
|
||||||
|
pub capabilities: BufferCapabilities,
|
||||||
|
pub flags: MemoryConsistency,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<v4l2_requestbuffers> for RequestBuffers {
|
||||||
|
fn from(reqbufs: v4l2_requestbuffers) -> Self {
|
||||||
|
RequestBuffers {
|
||||||
|
count: reqbufs.count,
|
||||||
|
capabilities: BufferCapabilities::from_bits_truncate(reqbufs.capabilities),
|
||||||
|
flags: MemoryConsistency::from_bits_truncate(reqbufs.flags),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::v4l2_create_buffers;
|
||||||
|
use crate::bindings::v4l2_requestbuffers;
|
||||||
|
|
||||||
|
nix::ioctl_readwrite!(vidioc_reqbufs, b'V', 8, v4l2_requestbuffers);
|
||||||
|
nix::ioctl_readwrite!(vidioc_create_bufs, b'V', 92, v4l2_create_buffers);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ReqbufsError {
|
||||||
|
#[error("invalid buffer ({0}) or memory type ({1:?}) requested")]
|
||||||
|
InvalidBufferType(QueueType, MemoryType),
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(nix::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReqbufsError> for Errno {
|
||||||
|
fn from(err: ReqbufsError) -> Self {
|
||||||
|
match err {
|
||||||
|
ReqbufsError::InvalidBufferType(_, _) => Errno::EINVAL,
|
||||||
|
ReqbufsError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_REQBUFS` ioctl.
|
||||||
|
pub fn reqbufs<O: From<v4l2_requestbuffers>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
queue: QueueType,
|
||||||
|
memory: MemoryType,
|
||||||
|
count: u32,
|
||||||
|
flags: MemoryConsistency,
|
||||||
|
) -> Result<O, ReqbufsError> {
|
||||||
|
let mut reqbufs = v4l2_requestbuffers {
|
||||||
|
count,
|
||||||
|
type_: queue as u32,
|
||||||
|
memory: memory as u32,
|
||||||
|
flags: flags.bits(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_reqbufs(fd.as_raw_fd(), &mut reqbufs) } {
|
||||||
|
Ok(_) => Ok(O::from(reqbufs)),
|
||||||
|
Err(Errno::EINVAL) => Err(ReqbufsError::InvalidBufferType(queue, memory)),
|
||||||
|
Err(e) => Err(ReqbufsError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CreateBufsError {
|
||||||
|
#[error("no memory available to allocate MMAP buffers")]
|
||||||
|
NoMem,
|
||||||
|
#[error("invalid format or memory type requested")]
|
||||||
|
Invalid,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(nix::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreateBufsError> for Errno {
|
||||||
|
fn from(err: CreateBufsError) -> Self {
|
||||||
|
match err {
|
||||||
|
CreateBufsError::NoMem => Errno::ENOMEM,
|
||||||
|
CreateBufsError::Invalid => Errno::EINVAL,
|
||||||
|
CreateBufsError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_CREATE_BUFS` ioctl.
|
||||||
|
pub fn create_bufs<F: Into<v4l2_format>, O: From<v4l2_create_buffers>>(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
count: u32,
|
||||||
|
memory: MemoryType,
|
||||||
|
format: F,
|
||||||
|
) -> Result<O, CreateBufsError> {
|
||||||
|
let mut create_bufs = v4l2_create_buffers {
|
||||||
|
count,
|
||||||
|
memory: memory as u32,
|
||||||
|
format: format.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_create_bufs(fd.as_raw_fd(), &mut create_bufs) } {
|
||||||
|
Ok(_) => Ok(O::from(create_bufs)),
|
||||||
|
Err(Errno::ENOMEM) => Err(CreateBufsError::NoMem),
|
||||||
|
Err(Errno::EINVAL) => Err(CreateBufsError::Invalid),
|
||||||
|
Err(e) => Err(CreateBufsError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
71
libs/v4l2r/src/ioctl/streamon.rs
Normal file
71
libs/v4l2r/src/ioctl/streamon.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//! Safe wrapper for the `VIDIOC_STREAM(ON|OFF)` ioctls.
|
||||||
|
use crate::QueueType;
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
nix::ioctl_write_ptr!(vidioc_streamon, b'V', 18, u32);
|
||||||
|
nix::ioctl_write_ptr!(vidioc_streamoff, b'V', 19, u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum StreamOnError {
|
||||||
|
#[error("queue type ({0}) not supported, or no buffers allocated or enqueued")]
|
||||||
|
InvalidQueue(QueueType),
|
||||||
|
#[error("invalid pad configuration")]
|
||||||
|
InvalidPadConfig,
|
||||||
|
#[error("invalid pipeline link configuration")]
|
||||||
|
InvalidPipelineConfig,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StreamOnError> for Errno {
|
||||||
|
fn from(err: StreamOnError) -> Self {
|
||||||
|
match err {
|
||||||
|
StreamOnError::InvalidQueue(_) => Errno::EINVAL,
|
||||||
|
StreamOnError::InvalidPadConfig => Errno::EPIPE,
|
||||||
|
StreamOnError::InvalidPipelineConfig => Errno::ENOLINK,
|
||||||
|
StreamOnError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_STREAMON` ioctl.
|
||||||
|
pub fn streamon(fd: &impl AsRawFd, queue: QueueType) -> Result<(), StreamOnError> {
|
||||||
|
match unsafe { ioctl::vidioc_streamon(fd.as_raw_fd(), &(queue as u32)) } {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(Errno::EINVAL) => Err(StreamOnError::InvalidQueue(queue)),
|
||||||
|
Err(Errno::EPIPE) => Err(StreamOnError::InvalidPadConfig),
|
||||||
|
Err(Errno::ENOLINK) => Err(StreamOnError::InvalidPipelineConfig),
|
||||||
|
Err(e) => Err(StreamOnError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum StreamOffError {
|
||||||
|
#[error("queue type not supported")]
|
||||||
|
InvalidQueue,
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StreamOffError> for Errno {
|
||||||
|
fn from(err: StreamOffError) -> Self {
|
||||||
|
match err {
|
||||||
|
StreamOffError::InvalidQueue => Errno::EINVAL,
|
||||||
|
StreamOffError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_STREAMOFF` ioctl.
|
||||||
|
pub fn streamoff(fd: &impl AsRawFd, queue: QueueType) -> Result<(), StreamOffError> {
|
||||||
|
match unsafe { ioctl::vidioc_streamoff(fd.as_raw_fd(), &(queue as u32)) } {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(Errno::EINVAL) => Err(StreamOffError::InvalidQueue),
|
||||||
|
Err(e) => Err(StreamOffError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
210
libs/v4l2r/src/ioctl/subscribe_event.rs
Normal file
210
libs/v4l2r/src/ioctl/subscribe_event.rs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
//! Safe wrapper for the `VIDIOC_SUBSCRIBE_EVENT` and `VIDIOC_UNSUBSCRIBE_EVENT
|
||||||
|
//! ioctls.
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
use bitflags::bitflags;
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bindings;
|
||||||
|
use crate::bindings::v4l2_event;
|
||||||
|
use crate::bindings::v4l2_event_subscription;
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct SubscribeEventFlags: u32 {
|
||||||
|
const SEND_INITIAL = bindings::V4L2_EVENT_SUB_FL_SEND_INITIAL;
|
||||||
|
const ALLOW_FEEDBACK = bindings::V4L2_EVENT_SUB_FL_ALLOW_FEEDBACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum EventType {
|
||||||
|
VSync,
|
||||||
|
Eos,
|
||||||
|
Ctrl(u32),
|
||||||
|
FrameSync,
|
||||||
|
SourceChange(u32),
|
||||||
|
MotionDet,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum EventConversionError {
|
||||||
|
#[error("unrecognized event {0}")]
|
||||||
|
UnrecognizedEvent(u32),
|
||||||
|
#[error("unrecognized source change {0}")]
|
||||||
|
UnrecognizedSourceChange(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&v4l2_event_subscription> for EventType {
|
||||||
|
type Error = EventConversionError;
|
||||||
|
|
||||||
|
fn try_from(event: &v4l2_event_subscription) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match event.type_ {
|
||||||
|
bindings::V4L2_EVENT_VSYNC => EventType::VSync,
|
||||||
|
bindings::V4L2_EVENT_EOS => EventType::Eos,
|
||||||
|
bindings::V4L2_EVENT_CTRL => EventType::Ctrl(event.id),
|
||||||
|
bindings::V4L2_EVENT_FRAME_SYNC => EventType::FrameSync,
|
||||||
|
bindings::V4L2_EVENT_SOURCE_CHANGE => EventType::SourceChange(event.id),
|
||||||
|
bindings::V4L2_EVENT_MOTION_DET => EventType::MotionDet,
|
||||||
|
e => return Err(EventConversionError::UnrecognizedEvent(e)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct SrcChanges: u32 {
|
||||||
|
const RESOLUTION = bindings::V4L2_EVENT_SRC_CH_RESOLUTION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
SrcChangeEvent(SrcChanges),
|
||||||
|
Eos,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<v4l2_event> for Event {
|
||||||
|
type Error = EventConversionError;
|
||||||
|
|
||||||
|
fn try_from(value: v4l2_event) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value.type_ {
|
||||||
|
bindings::V4L2_EVENT_VSYNC => todo!(),
|
||||||
|
bindings::V4L2_EVENT_EOS => Event::Eos,
|
||||||
|
bindings::V4L2_EVENT_CTRL => todo!(),
|
||||||
|
bindings::V4L2_EVENT_FRAME_SYNC => todo!(),
|
||||||
|
bindings::V4L2_EVENT_SOURCE_CHANGE => {
|
||||||
|
let changes = unsafe { value.u.src_change.changes };
|
||||||
|
Event::SrcChangeEvent(
|
||||||
|
SrcChanges::from_bits(changes)
|
||||||
|
.ok_or(EventConversionError::UnrecognizedSourceChange(changes))?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
bindings::V4L2_EVENT_MOTION_DET => todo!(),
|
||||||
|
t => return Err(EventConversionError::UnrecognizedEvent(t)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_v4l2_event_subscription(
|
||||||
|
event: EventType,
|
||||||
|
flags: SubscribeEventFlags,
|
||||||
|
) -> v4l2_event_subscription {
|
||||||
|
v4l2_event_subscription {
|
||||||
|
type_: match event {
|
||||||
|
EventType::VSync => bindings::V4L2_EVENT_VSYNC,
|
||||||
|
EventType::Eos => bindings::V4L2_EVENT_EOS,
|
||||||
|
EventType::Ctrl(_) => bindings::V4L2_EVENT_CTRL,
|
||||||
|
EventType::FrameSync => bindings::V4L2_EVENT_FRAME_SYNC,
|
||||||
|
EventType::SourceChange(_) => bindings::V4L2_EVENT_SOURCE_CHANGE,
|
||||||
|
EventType::MotionDet => bindings::V4L2_EVENT_MOTION_DET,
|
||||||
|
},
|
||||||
|
id: match event {
|
||||||
|
EventType::Ctrl(id) => id,
|
||||||
|
EventType::SourceChange(id) => id,
|
||||||
|
_ => 0,
|
||||||
|
},
|
||||||
|
flags: flags.bits(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod ioctl {
|
||||||
|
use crate::bindings::{v4l2_event, v4l2_event_subscription};
|
||||||
|
|
||||||
|
nix::ioctl_read!(vidioc_dqevent, b'V', 89, v4l2_event);
|
||||||
|
nix::ioctl_write_ptr!(vidioc_subscribe_event, b'V', 90, v4l2_event_subscription);
|
||||||
|
nix::ioctl_write_ptr!(vidioc_unsubscribe_event, b'V', 91, v4l2_event_subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SubscribeEventError {
|
||||||
|
#[error("ioctl error: {0}")]
|
||||||
|
IoctlError(#[from] Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SubscribeEventError> for Errno {
|
||||||
|
fn from(err: SubscribeEventError) -> Self {
|
||||||
|
match err {
|
||||||
|
SubscribeEventError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_SUBSCRIBE_EVENT` ioctl.
|
||||||
|
pub fn subscribe_event(
|
||||||
|
fd: &impl AsRawFd,
|
||||||
|
event: EventType,
|
||||||
|
flags: SubscribeEventFlags,
|
||||||
|
) -> Result<(), SubscribeEventError> {
|
||||||
|
let subscription = build_v4l2_event_subscription(event, flags);
|
||||||
|
|
||||||
|
unsafe { ioctl::vidioc_subscribe_event(fd.as_raw_fd(), &subscription) }?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_UNSUBSCRIBE_EVENT` ioctl.
|
||||||
|
pub fn unsubscribe_event(fd: &impl AsRawFd, event: EventType) -> Result<(), SubscribeEventError> {
|
||||||
|
let subscription = build_v4l2_event_subscription(event, SubscribeEventFlags::empty());
|
||||||
|
|
||||||
|
unsafe { ioctl::vidioc_unsubscribe_event(fd.as_raw_fd(), &subscription) }?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe wrapper around the `VIDIOC_UNSUBSCRIBE_EVENT` ioctl to unsubscribe from all events.
|
||||||
|
pub fn unsubscribe_all_events(fd: &impl AsRawFd) -> Result<(), SubscribeEventError> {
|
||||||
|
let subscription = v4l2_event_subscription {
|
||||||
|
type_: bindings::V4L2_EVENT_ALL,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe { ioctl::vidioc_unsubscribe_event(fd.as_raw_fd(), &subscription) }?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DqEventError {
|
||||||
|
#[error("no event ready for dequeue")]
|
||||||
|
NotReady,
|
||||||
|
#[error("error while converting event")]
|
||||||
|
EventConversionError,
|
||||||
|
#[error("unexpected ioctl error: {0}")]
|
||||||
|
IoctlError(Errno),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Errno> for DqEventError {
|
||||||
|
fn from(error: Errno) -> Self {
|
||||||
|
match error {
|
||||||
|
Errno::ENOENT => Self::NotReady,
|
||||||
|
error => Self::IoctlError(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DqEventError> for Errno {
|
||||||
|
fn from(err: DqEventError) -> Self {
|
||||||
|
match err {
|
||||||
|
DqEventError::NotReady => Errno::ENOENT,
|
||||||
|
DqEventError::EventConversionError => Errno::EINVAL,
|
||||||
|
DqEventError::IoctlError(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dqevent<O: TryFrom<v4l2_event>>(fd: &impl AsRawFd) -> Result<O, DqEventError> {
|
||||||
|
let mut event: v4l2_event = Default::default();
|
||||||
|
|
||||||
|
match unsafe { ioctl::vidioc_dqevent(fd.as_raw_fd(), &mut event) } {
|
||||||
|
Ok(_) => Ok(event
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| DqEventError::EventConversionError)?),
|
||||||
|
Err(Errno::ENOENT) => Err(DqEventError::NotReady),
|
||||||
|
Err(e) => Err(DqEventError::IoctlError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
496
libs/v4l2r/src/lib.rs
Normal file
496
libs/v4l2r/src/lib.rs
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
//! This library provides the V4L2 pieces One-KVM needs for video capture:
|
||||||
|
//!
|
||||||
|
//! * The `ioctl` module provides direct, thin wrappers over the V4L2 ioctls
|
||||||
|
//! with added safety. Note that "safety" here is in terms of memory safety:
|
||||||
|
//! this layer won't guard against passing invalid data that the ioctls will
|
||||||
|
//! reject - it just makes sure that data passed from and to the kernel can
|
||||||
|
//! be accessed safely. Since this is a 1:1 mapping over the V4L2 ioctls,
|
||||||
|
//! working at this level is a bit laborious, although more comfortable than
|
||||||
|
//! doing the same in C.
|
||||||
|
//!
|
||||||
|
//! The upstream v4l2r crate also contains high-level decoder/encoder and C FFI
|
||||||
|
//! layers. This vendored copy intentionally excludes those pieces and keeps the
|
||||||
|
//! capture-oriented ioctl/memory surface only.
|
||||||
|
//!
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod bindings;
|
||||||
|
pub mod ioctl;
|
||||||
|
pub mod memory;
|
||||||
|
|
||||||
|
// This can be needed to match nix errors that we expose.
|
||||||
|
pub use nix;
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
use enumn::N;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
// The goal of this library is to provide two layers of abstraction:
|
||||||
|
// ioctl: direct, safe counterparts of the V4L2 ioctls.
|
||||||
|
// device/queue/buffer: higher abstraction, still mapping to core V4L2 mechanics.
|
||||||
|
|
||||||
|
/// Possible directions for the queue
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum QueueDirection {
|
||||||
|
Output,
|
||||||
|
Capture,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Possible classes for this queue.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum QueueClass {
|
||||||
|
Video,
|
||||||
|
Vbi,
|
||||||
|
SlicedVbi,
|
||||||
|
VideoOverlay,
|
||||||
|
VideoMplane,
|
||||||
|
Sdr,
|
||||||
|
Meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Types of queues currently supported by this library.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, N)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum QueueType {
|
||||||
|
VideoCapture = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_CAPTURE,
|
||||||
|
VideoOutput = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_OUTPUT,
|
||||||
|
VideoOverlay = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_OVERLAY,
|
||||||
|
VbiCapture = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VBI_CAPTURE,
|
||||||
|
VbiOutput = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VBI_OUTPUT,
|
||||||
|
SlicedVbiCapture = bindings::v4l2_buf_type_V4L2_BUF_TYPE_SLICED_VBI_CAPTURE,
|
||||||
|
SlicedVbiOutput = bindings::v4l2_buf_type_V4L2_BUF_TYPE_SLICED_VBI_OUTPUT,
|
||||||
|
VideoOutputOverlay = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY,
|
||||||
|
VideoCaptureMplane = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
|
||||||
|
VideoOutputMplane = bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
|
||||||
|
SdrCapture = bindings::v4l2_buf_type_V4L2_BUF_TYPE_SDR_CAPTURE,
|
||||||
|
SdrOutput = bindings::v4l2_buf_type_V4L2_BUF_TYPE_SDR_OUTPUT,
|
||||||
|
MetaCapture = bindings::v4l2_buf_type_V4L2_BUF_TYPE_META_CAPTURE,
|
||||||
|
MetaOutput = bindings::v4l2_buf_type_V4L2_BUF_TYPE_META_OUTPUT,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueueType {
|
||||||
|
/// Returns the queue corresponding to the passed `direction` and `class`.
|
||||||
|
pub fn from_dir_and_class(direction: QueueDirection, class: QueueClass) -> Self {
|
||||||
|
match (direction, class) {
|
||||||
|
(QueueDirection::Capture, QueueClass::Video) => Self::VideoCapture,
|
||||||
|
(QueueDirection::Output, QueueClass::Video) => Self::VideoOutput,
|
||||||
|
(QueueDirection::Capture, QueueClass::VideoOverlay) => Self::VideoOverlay,
|
||||||
|
(QueueDirection::Output, QueueClass::VideoOverlay) => Self::VideoOutputOverlay,
|
||||||
|
(QueueDirection::Capture, QueueClass::Vbi) => Self::VbiCapture,
|
||||||
|
(QueueDirection::Output, QueueClass::Vbi) => Self::VbiOutput,
|
||||||
|
(QueueDirection::Capture, QueueClass::SlicedVbi) => Self::SlicedVbiCapture,
|
||||||
|
(QueueDirection::Output, QueueClass::SlicedVbi) => Self::SlicedVbiOutput,
|
||||||
|
(QueueDirection::Capture, QueueClass::VideoMplane) => Self::VideoCaptureMplane,
|
||||||
|
(QueueDirection::Output, QueueClass::VideoMplane) => Self::VideoOutputMplane,
|
||||||
|
(QueueDirection::Capture, QueueClass::Sdr) => Self::SdrCapture,
|
||||||
|
(QueueDirection::Output, QueueClass::Sdr) => Self::SdrOutput,
|
||||||
|
(QueueDirection::Capture, QueueClass::Meta) => Self::MetaCapture,
|
||||||
|
(QueueDirection::Output, QueueClass::Meta) => Self::MetaOutput,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the queue type is multiplanar.
|
||||||
|
pub fn is_multiplanar(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
QueueType::VideoCaptureMplane | QueueType::VideoOutputMplane
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the direction of the queue type (Output or Capture).
|
||||||
|
pub fn direction(&self) -> QueueDirection {
|
||||||
|
match self {
|
||||||
|
QueueType::VideoOutput
|
||||||
|
| QueueType::VideoOutputMplane
|
||||||
|
| QueueType::VideoOverlay
|
||||||
|
| QueueType::VideoOutputOverlay
|
||||||
|
| QueueType::VbiOutput
|
||||||
|
| QueueType::SlicedVbiOutput
|
||||||
|
| QueueType::SdrOutput
|
||||||
|
| QueueType::MetaOutput => QueueDirection::Output,
|
||||||
|
|
||||||
|
QueueType::VideoCapture
|
||||||
|
| QueueType::VbiCapture
|
||||||
|
| QueueType::SlicedVbiCapture
|
||||||
|
| QueueType::VideoCaptureMplane
|
||||||
|
| QueueType::SdrCapture
|
||||||
|
| QueueType::MetaCapture => QueueDirection::Capture,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class(&self) -> QueueClass {
|
||||||
|
match self {
|
||||||
|
QueueType::VideoCapture | QueueType::VideoOutput => QueueClass::Video,
|
||||||
|
QueueType::VideoOverlay | QueueType::VideoOutputOverlay => QueueClass::VideoOverlay,
|
||||||
|
QueueType::VbiCapture | QueueType::VbiOutput => QueueClass::Vbi,
|
||||||
|
QueueType::SlicedVbiCapture | QueueType::SlicedVbiOutput => QueueClass::SlicedVbi,
|
||||||
|
QueueType::VideoCaptureMplane | QueueType::VideoOutputMplane => QueueClass::VideoMplane,
|
||||||
|
QueueType::SdrCapture | QueueType::SdrOutput => QueueClass::Sdr,
|
||||||
|
QueueType::MetaCapture | QueueType::MetaOutput => QueueClass::Meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for QueueType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
Debug::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Fourcc pixel format, used to pass formats to V4L2. It can be converted
|
||||||
|
/// back and forth from a 32-bit integer, or a 4-bytes string.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||||
|
pub struct PixelFormat(u32);
|
||||||
|
|
||||||
|
impl PixelFormat {
|
||||||
|
pub const fn from_u32(v: u32) -> Self {
|
||||||
|
Self(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_u32(self) -> u32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_fourcc(n: &[u8; 4]) -> Self {
|
||||||
|
Self(n[0] as u32 | (n[1] as u32) << 8 | (n[2] as u32) << 16 | (n[3] as u32) << 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_fourcc(self) -> [u8; 4] {
|
||||||
|
self.0.to_le_bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a Fourcc in 32-bit integer format (like the ones passed in V4L2
|
||||||
|
/// structures) into the matching pixel format.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use v4l2r::PixelFormat;
|
||||||
|
/// // Fourcc representation of NV12.
|
||||||
|
/// let nv12 = u32::from_le(0x3231564e);
|
||||||
|
/// let f = PixelFormat::from(nv12);
|
||||||
|
/// assert_eq!(u32::from(f), nv12);
|
||||||
|
/// ```
|
||||||
|
impl From<u32> for PixelFormat {
|
||||||
|
fn from(i: u32) -> Self {
|
||||||
|
Self::from_u32(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a pixel format back to its 32-bit representation.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use v4l2r::PixelFormat;
|
||||||
|
/// // Fourcc representation of NV12.
|
||||||
|
/// let nv12 = u32::from_le(0x3231564e);
|
||||||
|
/// let f = PixelFormat::from(nv12);
|
||||||
|
/// assert_eq!(u32::from(f), nv12);
|
||||||
|
/// ```
|
||||||
|
impl From<PixelFormat> for u32 {
|
||||||
|
fn from(format: PixelFormat) -> Self {
|
||||||
|
format.to_u32()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple way to convert a string litteral (e.g. b"NV12") into a pixel
|
||||||
|
/// format that can be passed to V4L2.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use v4l2r::PixelFormat;
|
||||||
|
/// let nv12 = b"NV12";
|
||||||
|
/// let f = PixelFormat::from(nv12);
|
||||||
|
/// assert_eq!(&<[u8; 4]>::from(f), nv12);
|
||||||
|
/// ```
|
||||||
|
impl From<&[u8; 4]> for PixelFormat {
|
||||||
|
fn from(n: &[u8; 4]) -> Self {
|
||||||
|
Self::from_fourcc(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a pixel format back to its 4-character representation.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use v4l2r::PixelFormat;
|
||||||
|
/// let nv12 = b"NV12";
|
||||||
|
/// let f = PixelFormat::from(nv12);
|
||||||
|
/// assert_eq!(&<[u8; 4]>::from(f), nv12);
|
||||||
|
/// ```
|
||||||
|
impl From<PixelFormat> for [u8; 4] {
|
||||||
|
fn from(format: PixelFormat) -> Self {
|
||||||
|
format.to_fourcc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces a debug string for this PixelFormat, including its hexadecimal
|
||||||
|
/// and string representation.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use v4l2r::PixelFormat;
|
||||||
|
/// // Fourcc representation of NV12.
|
||||||
|
/// let nv12 = u32::from_le(0x3231564e);
|
||||||
|
/// let f = PixelFormat::from(nv12);
|
||||||
|
/// assert_eq!(format!("{:?}", f), "0x3231564e (NV12)");
|
||||||
|
/// ```
|
||||||
|
impl fmt::Debug for PixelFormat {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_fmt(format_args!("0x{:08x} ({})", self.0, self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces a displayable form of this PixelFormat.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use v4l2r::PixelFormat;
|
||||||
|
/// // Fourcc representation of NV12.
|
||||||
|
/// let nv12 = u32::from_le(0x3231564e);
|
||||||
|
/// let f = PixelFormat::from(nv12);
|
||||||
|
/// assert_eq!(f.to_string(), "NV12");
|
||||||
|
/// ```
|
||||||
|
impl fmt::Display for PixelFormat {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let fourcc = self
|
||||||
|
.0
|
||||||
|
.to_le_bytes()
|
||||||
|
.iter()
|
||||||
|
.map(|&x| x as char)
|
||||||
|
.collect::<String>();
|
||||||
|
f.write_str(fourcc.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Description of a single plane in a format.
|
||||||
|
#[derive(Debug, PartialEq, Clone, Default)]
|
||||||
|
pub struct PlaneLayout {
|
||||||
|
/// Useful size of the plane ; the backing memory must be at least that large.
|
||||||
|
pub sizeimage: u32,
|
||||||
|
/// Bytes per line of data. Only meaningful for image formats.
|
||||||
|
pub bytesperline: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unified representation of a V4L2 format capable of handling both single
|
||||||
|
/// and multi-planar formats. When the single-planar API is used, only
|
||||||
|
/// one plane shall be used - attempts to have more will be rejected by the
|
||||||
|
/// ioctl wrappers.
|
||||||
|
#[derive(Debug, PartialEq, Clone, Default)]
|
||||||
|
pub struct Format {
|
||||||
|
/// Width of the image in pixels.
|
||||||
|
pub width: u32,
|
||||||
|
/// Height of the image in pixels.
|
||||||
|
pub height: u32,
|
||||||
|
/// Format each pixel is encoded in.
|
||||||
|
pub pixelformat: PixelFormat,
|
||||||
|
/// Individual layout of each plane in this format. The exact number of planes
|
||||||
|
/// is defined by `pixelformat`.
|
||||||
|
pub plane_fmt: Vec<PlaneLayout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, PartialEq)]
|
||||||
|
pub enum FormatConversionError {
|
||||||
|
#[error("too many planes ({0}) specified,")]
|
||||||
|
TooManyPlanes(usize),
|
||||||
|
#[error("invalid buffer type requested")]
|
||||||
|
InvalidBufferType(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<bindings::v4l2_format> for Format {
|
||||||
|
type Error = FormatConversionError;
|
||||||
|
|
||||||
|
fn try_from(fmt: bindings::v4l2_format) -> std::result::Result<Self, Self::Error> {
|
||||||
|
match fmt.type_ {
|
||||||
|
bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_CAPTURE
|
||||||
|
| bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_OUTPUT => {
|
||||||
|
let pix = unsafe { &fmt.fmt.pix };
|
||||||
|
Ok(Format {
|
||||||
|
width: pix.width,
|
||||||
|
height: pix.height,
|
||||||
|
pixelformat: PixelFormat::from(pix.pixelformat),
|
||||||
|
plane_fmt: vec![PlaneLayout {
|
||||||
|
bytesperline: pix.bytesperline,
|
||||||
|
sizeimage: pix.sizeimage,
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
|
||||||
|
| bindings::v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE => {
|
||||||
|
let pix_mp = unsafe { &fmt.fmt.pix_mp };
|
||||||
|
|
||||||
|
// Can only happen if we passed a malformed v4l2_format.
|
||||||
|
if pix_mp.num_planes as usize > pix_mp.plane_fmt.len() {
|
||||||
|
return Err(Self::Error::TooManyPlanes(pix_mp.num_planes as usize));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut plane_fmt = Vec::new();
|
||||||
|
for i in 0..pix_mp.num_planes as usize {
|
||||||
|
let plane = &pix_mp.plane_fmt[i];
|
||||||
|
plane_fmt.push(PlaneLayout {
|
||||||
|
sizeimage: plane.sizeimage,
|
||||||
|
bytesperline: plane.bytesperline,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Format {
|
||||||
|
width: pix_mp.width,
|
||||||
|
height: pix_mp.height,
|
||||||
|
pixelformat: PixelFormat::from(pix_mp.pixelformat),
|
||||||
|
plane_fmt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t => Err(Self::Error::InvalidBufferType(t)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quickly build a usable `Format` from a pixel format and resolution.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use v4l2r::Format;
|
||||||
|
/// let f = Format::from((b"NV12", (640, 480)));
|
||||||
|
/// assert_eq!(f.width, 640);
|
||||||
|
/// assert_eq!(f.height, 480);
|
||||||
|
/// assert_eq!(f.pixelformat.to_string(), "NV12");
|
||||||
|
/// assert_eq!(f.plane_fmt.len(), 0);
|
||||||
|
/// ```
|
||||||
|
impl<T: Into<PixelFormat>> From<(T, (usize, usize))> for Format {
|
||||||
|
fn from((pixel_format, (width, height)): (T, (usize, usize))) -> Self {
|
||||||
|
Format {
|
||||||
|
width: width as u32,
|
||||||
|
height: height as u32,
|
||||||
|
pixelformat: pixel_format.into(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A more elegant representation for `v4l2_rect`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Rect {
|
||||||
|
pub left: i32,
|
||||||
|
pub top: i32,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rect {
|
||||||
|
pub fn new(left: i32, top: i32, width: u32, height: u32) -> Rect {
|
||||||
|
Rect {
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bindings::v4l2_rect> for Rect {
|
||||||
|
fn from(rect: bindings::v4l2_rect) -> Self {
|
||||||
|
Rect {
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bindings::v4l2_selection> for Rect {
|
||||||
|
fn from(selection: bindings::v4l2_selection) -> Self {
|
||||||
|
Self::from(selection.r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rect> for bindings::v4l2_rect {
|
||||||
|
fn from(rect: Rect) -> Self {
|
||||||
|
bindings::v4l2_rect {
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Rect {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"({}, {}), {}x{}",
|
||||||
|
self.left, self.top, self.width, self.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Equivalent of `enum v4l2_colorspace`.
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, N)]
|
||||||
|
pub enum Colorspace {
|
||||||
|
#[default]
|
||||||
|
Default = bindings::v4l2_colorspace_V4L2_COLORSPACE_DEFAULT,
|
||||||
|
Smpte170M = bindings::v4l2_colorspace_V4L2_COLORSPACE_SMPTE170M,
|
||||||
|
Smpte240M = bindings::v4l2_colorspace_V4L2_COLORSPACE_SMPTE240M,
|
||||||
|
Rec709 = bindings::v4l2_colorspace_V4L2_COLORSPACE_REC709,
|
||||||
|
Bt878 = bindings::v4l2_colorspace_V4L2_COLORSPACE_BT878,
|
||||||
|
SystemM470 = bindings::v4l2_colorspace_V4L2_COLORSPACE_470_SYSTEM_M,
|
||||||
|
SystemBG470 = bindings::v4l2_colorspace_V4L2_COLORSPACE_470_SYSTEM_BG,
|
||||||
|
Jpeg = bindings::v4l2_colorspace_V4L2_COLORSPACE_JPEG,
|
||||||
|
Srgb = bindings::v4l2_colorspace_V4L2_COLORSPACE_SRGB,
|
||||||
|
OpRgb = bindings::v4l2_colorspace_V4L2_COLORSPACE_OPRGB,
|
||||||
|
Bt2020 = bindings::v4l2_colorspace_V4L2_COLORSPACE_BT2020,
|
||||||
|
Raw = bindings::v4l2_colorspace_V4L2_COLORSPACE_RAW,
|
||||||
|
DciP3 = bindings::v4l2_colorspace_V4L2_COLORSPACE_DCI_P3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Equivalent of `enum v4l2_xfer_func`.
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, N)]
|
||||||
|
pub enum XferFunc {
|
||||||
|
#[default]
|
||||||
|
Default = bindings::v4l2_xfer_func_V4L2_XFER_FUNC_DEFAULT,
|
||||||
|
F709 = bindings::v4l2_xfer_func_V4L2_XFER_FUNC_709,
|
||||||
|
Srgb = bindings::v4l2_xfer_func_V4L2_XFER_FUNC_SRGB,
|
||||||
|
OpRgb = bindings::v4l2_xfer_func_V4L2_XFER_FUNC_OPRGB,
|
||||||
|
Smpte240M = bindings::v4l2_xfer_func_V4L2_XFER_FUNC_SMPTE240M,
|
||||||
|
None = bindings::v4l2_xfer_func_V4L2_XFER_FUNC_NONE,
|
||||||
|
DciP3 = bindings::v4l2_xfer_func_V4L2_XFER_FUNC_DCI_P3,
|
||||||
|
Smpte2084 = bindings::v4l2_xfer_func_V4L2_XFER_FUNC_SMPTE2084,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Equivalent of `enum v4l2_ycbcr_encoding`.
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, N)]
|
||||||
|
pub enum YCbCrEncoding {
|
||||||
|
#[default]
|
||||||
|
Default = bindings::v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_DEFAULT,
|
||||||
|
E601 = bindings::v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_601,
|
||||||
|
E709 = bindings::v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_709,
|
||||||
|
Xv601 = bindings::v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_XV601,
|
||||||
|
Xv709 = bindings::v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_XV709,
|
||||||
|
Sycc = bindings::v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_SYCC,
|
||||||
|
Bt2020 = bindings::v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_BT2020,
|
||||||
|
Bt2020ConstLum = bindings::v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_BT2020_CONST_LUM,
|
||||||
|
Smpte240M = bindings::v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_SMPTE240M,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Equivalent of `enum v4l2_quantization`.
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, N)]
|
||||||
|
pub enum Quantization {
|
||||||
|
#[default]
|
||||||
|
Default = bindings::v4l2_quantization_V4L2_QUANTIZATION_DEFAULT,
|
||||||
|
FullRange = bindings::v4l2_quantization_V4L2_QUANTIZATION_FULL_RANGE,
|
||||||
|
LimRange = bindings::v4l2_quantization_V4L2_QUANTIZATION_LIM_RANGE,
|
||||||
|
}
|
||||||
279
libs/v4l2r/src/memory.rs
Normal file
279
libs/v4l2r/src/memory.rs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
//! Abstracts the different kinds of backing memory (`MMAP`, `USERPTR`,
|
||||||
|
//! `DMABUF`) supported by V4L2.
|
||||||
|
//!
|
||||||
|
//! V4L2 allows to use either memory that is provided by the device itself
|
||||||
|
//! (MMAP) or memory imported via user allocation (USERPTR) or the dma-buf
|
||||||
|
//! subsystem (DMABUF). This results in 2 very different behaviors and 3 memory
|
||||||
|
//! types that we need to model.
|
||||||
|
//!
|
||||||
|
//! The `Memory` trait represents these memory types and is thus implemented
|
||||||
|
//! by exacly 3 types: `MMAP`, `UserPtr`, and `DMABuf`. These types do very
|
||||||
|
//! little apart from providing a constant with the corresponding V4L2 memory
|
||||||
|
//! type they model, and implement the `SelfBacked` (for MMAP) or `Imported`
|
||||||
|
//! (for `UserPtr` and `DMABuf`) traits to indicate where their memory comes
|
||||||
|
//! from.
|
||||||
|
//!
|
||||||
|
//! The `PlaneHandle` trait is used by types which can bind to one of these
|
||||||
|
//! memory types, i.e. a type that can represent a single memory plane of a
|
||||||
|
//! buffer. For `MMAP` memory this is a void type (since `MMAP` provides its
|
||||||
|
//! own memory). `UserPtr`, a `Vec<u8>` can adequately be used as backing
|
||||||
|
//! memory, and for `DMABuf` we will use a file descriptor. For handles that
|
||||||
|
//! can be mapped into the user address-space (and indeed for `MMAP` this is
|
||||||
|
//! the only way to access the memory), the `Mappable` trait can be implemented.
|
||||||
|
//!
|
||||||
|
//! The set of handles that make all the planes for a given buffer is
|
||||||
|
//! represented by the `BufferHandles` trait. This trait is more abstract since
|
||||||
|
//! we may want to decide at runtime the kind of memory we want to use ;
|
||||||
|
//! therefore this trait does not have any particular kind of memory attached to
|
||||||
|
//! it. `PrimitiveBufferHandles` is used to represent plane handles which memory
|
||||||
|
//! type is known at compilation time, and thus includes a reference to a
|
||||||
|
//! `PlaneHandle` type and by transition its `Memory` type.
|
||||||
|
mod dmabuf;
|
||||||
|
mod mmap;
|
||||||
|
mod userptr;
|
||||||
|
|
||||||
|
pub use dmabuf::*;
|
||||||
|
pub use mmap::*;
|
||||||
|
pub use userptr::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
bindings::{self, v4l2_buffer__bindgen_ty_1, v4l2_plane__bindgen_ty_1},
|
||||||
|
ioctl::{PlaneMapping, QueryBufPlane},
|
||||||
|
};
|
||||||
|
use enumn::N;
|
||||||
|
use std::os::unix::io::AsFd;
|
||||||
|
use std::{fmt::Debug, ops::Deref};
|
||||||
|
|
||||||
|
/// All the supported V4L2 memory types.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, N)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum MemoryType {
|
||||||
|
Mmap = bindings::v4l2_memory_V4L2_MEMORY_MMAP,
|
||||||
|
UserPtr = bindings::v4l2_memory_V4L2_MEMORY_USERPTR,
|
||||||
|
Overlay = bindings::v4l2_memory_V4L2_MEMORY_OVERLAY,
|
||||||
|
DmaBuf = bindings::v4l2_memory_V4L2_MEMORY_DMABUF,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait describing a memory type that can be used to back V4L2 buffers.
|
||||||
|
pub trait Memory: 'static {
|
||||||
|
/// The memory type represented.
|
||||||
|
const MEMORY_TYPE: MemoryType;
|
||||||
|
/// The final type of the memory backing information in `struct v4l2_buffer` or `struct
|
||||||
|
/// v4l2_plane`.
|
||||||
|
type RawBacking;
|
||||||
|
|
||||||
|
/// Returns a reference to the memory backing information for `m` that is relevant for this
|
||||||
|
/// memory type.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// The caller must ensure that `m` indeed belongs to a buffer of this memory type.
|
||||||
|
unsafe fn get_plane_buffer_backing(m: &bindings::v4l2_plane__bindgen_ty_1)
|
||||||
|
-> &Self::RawBacking;
|
||||||
|
|
||||||
|
/// Returns a reference to the memory backing information for `m` that is relevant for this memory type.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// The caller must ensure that `m` indeed belongs to a buffer of this memory type.
|
||||||
|
unsafe fn get_single_planar_buffer_backing(
|
||||||
|
m: &bindings::v4l2_buffer__bindgen_ty_1,
|
||||||
|
) -> &Self::RawBacking;
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the memory backing information for `m` that is relevant for
|
||||||
|
/// this memory type.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// The caller must ensure that `m` indeed belongs to a buffer of this memory type.
|
||||||
|
unsafe fn get_plane_buffer_backing_mut(
|
||||||
|
m: &mut bindings::v4l2_plane__bindgen_ty_1,
|
||||||
|
) -> &mut Self::RawBacking;
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the memory backing information for `m` that is relevant for
|
||||||
|
/// this memory type.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// The caller must ensure that `m` indeed belongs to a buffer of this memory type.
|
||||||
|
unsafe fn get_single_planar_buffer_backing_mut(
|
||||||
|
m: &mut bindings::v4l2_buffer__bindgen_ty_1,
|
||||||
|
) -> &mut Self::RawBacking;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for memory types that provide their own memory, i.e. MMAP.
|
||||||
|
pub trait SelfBacked: Memory + Default {}
|
||||||
|
|
||||||
|
/// Trait for memory types to which external memory must be attached to, i.e. UserPtr and
|
||||||
|
/// DMABuf.
|
||||||
|
pub trait Imported: Memory {}
|
||||||
|
|
||||||
|
/// Trait for a handle that represents actual data for a single place. A buffer
|
||||||
|
/// will have as many of these as it has planes.
|
||||||
|
pub trait PlaneHandle: Debug + Send + 'static {
|
||||||
|
/// The kind of memory the handle attaches to.
|
||||||
|
type Memory: Memory;
|
||||||
|
|
||||||
|
/// Fill a plane of a multi-planar V4L2 buffer with the handle's information.
|
||||||
|
fn fill_v4l2_plane(&self, plane: &mut bindings::v4l2_plane);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trait for plane handles that provide access to their content through a map()
|
||||||
|
// method (typically, MMAP buffers).
|
||||||
|
pub trait Mappable: PlaneHandle {
|
||||||
|
/// Return a `PlaneMapping` enabling access to the memory of this handle.
|
||||||
|
fn map<D: AsFd>(device: &D, plane_info: &QueryBufPlane) -> Option<PlaneMapping>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for structures providing all the handles of a single buffer.
|
||||||
|
pub trait BufferHandles: Send + Debug + 'static {
|
||||||
|
/// Enumeration of all the `MemoryType` supported by this type. Typically
|
||||||
|
/// a subset of `MemoryType` or `MemoryType` itself.
|
||||||
|
type SupportedMemoryType: Into<MemoryType> + Send + Clone + Copy;
|
||||||
|
|
||||||
|
/// Number of planes.
|
||||||
|
fn len(&self) -> usize;
|
||||||
|
/// Fill a plane of a multi-planar V4L2 buffer with the `index` handle's information.
|
||||||
|
fn fill_v4l2_plane(&self, index: usize, plane: &mut bindings::v4l2_plane);
|
||||||
|
|
||||||
|
/// Returns true if there are no handles here (unlikely).
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of `BufferHandles` for all indexables of `PlaneHandle` (e.g. [`std::vec::Vec`]).
|
||||||
|
///
|
||||||
|
/// This is The simplest way to use primitive handles.
|
||||||
|
impl<P, Q> BufferHandles for Q
|
||||||
|
where
|
||||||
|
P: PlaneHandle,
|
||||||
|
Q: Send + Debug + 'static + Deref<Target = [P]>,
|
||||||
|
{
|
||||||
|
type SupportedMemoryType = MemoryType;
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.deref().len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_v4l2_plane(&self, index: usize, plane: &mut bindings::v4l2_plane) {
|
||||||
|
self.deref()[index].fill_v4l2_plane(plane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for plane handles for which the final memory type is known at compile
|
||||||
|
/// time.
|
||||||
|
pub trait PrimitiveBufferHandles: BufferHandles {
|
||||||
|
type HandleType: PlaneHandle;
|
||||||
|
const MEMORY_TYPE: Self::SupportedMemoryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of `PrimitiveBufferHandles` for all indexables of `PlaneHandle` (e.g.
|
||||||
|
/// [`std::vec::Vec`]).
|
||||||
|
impl<P, Q> PrimitiveBufferHandles for Q
|
||||||
|
where
|
||||||
|
P: PlaneHandle,
|
||||||
|
Q: Send + Debug + 'static + Deref<Target = [P]>,
|
||||||
|
{
|
||||||
|
type HandleType = P;
|
||||||
|
const MEMORY_TYPE: Self::SupportedMemoryType = P::Memory::MEMORY_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Conversion from `v4l2_buffer`'s backing information to `v4l2_plane`'s.
|
||||||
|
impl From<(&v4l2_buffer__bindgen_ty_1, MemoryType)> for v4l2_plane__bindgen_ty_1 {
|
||||||
|
fn from((m, memory): (&v4l2_buffer__bindgen_ty_1, MemoryType)) -> Self {
|
||||||
|
match memory {
|
||||||
|
MemoryType::Mmap => v4l2_plane__bindgen_ty_1 {
|
||||||
|
// Safe because the buffer type is determined to be MMAP.
|
||||||
|
mem_offset: unsafe { m.offset },
|
||||||
|
},
|
||||||
|
MemoryType::UserPtr => v4l2_plane__bindgen_ty_1 {
|
||||||
|
// Safe because the buffer type is determined to be USERPTR.
|
||||||
|
userptr: unsafe { m.userptr },
|
||||||
|
},
|
||||||
|
MemoryType::DmaBuf => v4l2_plane__bindgen_ty_1 {
|
||||||
|
// Safe because the buffer type is determined to be DMABUF.
|
||||||
|
fd: unsafe { m.fd },
|
||||||
|
},
|
||||||
|
MemoryType::Overlay => Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Conversion from `v4l2_plane`'s backing information to `v4l2_buffer`'s.
|
||||||
|
impl From<(&v4l2_plane__bindgen_ty_1, MemoryType)> for v4l2_buffer__bindgen_ty_1 {
|
||||||
|
fn from((m, memory): (&v4l2_plane__bindgen_ty_1, MemoryType)) -> Self {
|
||||||
|
match memory {
|
||||||
|
MemoryType::Mmap => v4l2_buffer__bindgen_ty_1 {
|
||||||
|
// Safe because the buffer type is determined to be MMAP.
|
||||||
|
offset: unsafe { m.mem_offset },
|
||||||
|
},
|
||||||
|
MemoryType::UserPtr => v4l2_buffer__bindgen_ty_1 {
|
||||||
|
// Safe because the buffer type is determined to be USERPTR.
|
||||||
|
userptr: unsafe { m.userptr },
|
||||||
|
},
|
||||||
|
MemoryType::DmaBuf => v4l2_buffer__bindgen_ty_1 {
|
||||||
|
// Safe because the buffer type is determined to be DMABUF.
|
||||||
|
fd: unsafe { m.fd },
|
||||||
|
},
|
||||||
|
MemoryType::Overlay => Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::bindings::v4l2_buffer__bindgen_ty_1;
|
||||||
|
use crate::bindings::v4l2_plane__bindgen_ty_1;
|
||||||
|
use crate::memory::MemoryType;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Purpose of this test is dubious as the members are overlapping anyway.
|
||||||
|
fn plane_m_to_buffer_m() {
|
||||||
|
let plane_m = v4l2_plane__bindgen_ty_1 {
|
||||||
|
mem_offset: 0xfeedc0fe,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { v4l2_buffer__bindgen_ty_1::from((&plane_m, MemoryType::Mmap)).offset },
|
||||||
|
0xfeedc0fe
|
||||||
|
);
|
||||||
|
|
||||||
|
let plane_m = v4l2_plane__bindgen_ty_1 {
|
||||||
|
userptr: 0xfeedc0fe,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { v4l2_buffer__bindgen_ty_1::from((&plane_m, MemoryType::UserPtr)).userptr },
|
||||||
|
0xfeedc0fe
|
||||||
|
);
|
||||||
|
|
||||||
|
let plane_m = v4l2_plane__bindgen_ty_1 { fd: 0x76543210 };
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { v4l2_buffer__bindgen_ty_1::from((&plane_m, MemoryType::DmaBuf)).fd },
|
||||||
|
0x76543210
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Purpose of this test is dubious as the members are overlapping anyway.
|
||||||
|
fn buffer_m_to_plane_m() {
|
||||||
|
let buffer_m = v4l2_buffer__bindgen_ty_1 { offset: 0xfeedc0fe };
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { v4l2_plane__bindgen_ty_1::from((&buffer_m, MemoryType::Mmap)).mem_offset },
|
||||||
|
0xfeedc0fe
|
||||||
|
);
|
||||||
|
|
||||||
|
let buffer_m = v4l2_buffer__bindgen_ty_1 {
|
||||||
|
userptr: 0xfeedc0fe,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { v4l2_plane__bindgen_ty_1::from((&buffer_m, MemoryType::UserPtr)).userptr },
|
||||||
|
0xfeedc0fe
|
||||||
|
);
|
||||||
|
|
||||||
|
let buffer_m = v4l2_buffer__bindgen_ty_1 { fd: 0x76543210 };
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { v4l2_plane__bindgen_ty_1::from((&buffer_m, MemoryType::DmaBuf)).fd },
|
||||||
|
0x76543210
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
91
libs/v4l2r/src/memory/dmabuf.rs
Normal file
91
libs/v4l2r/src/memory/dmabuf.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
//! Operations specific to DMABuf-type buffers.
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{bindings, ioctl};
|
||||||
|
use std::os::fd::RawFd;
|
||||||
|
use std::os::unix::io::{AsFd, AsRawFd};
|
||||||
|
|
||||||
|
pub struct DmaBuf;
|
||||||
|
|
||||||
|
pub type DmaBufferHandles<T> = Vec<DmaBufHandle<T>>;
|
||||||
|
|
||||||
|
impl Memory for DmaBuf {
|
||||||
|
const MEMORY_TYPE: MemoryType = MemoryType::DmaBuf;
|
||||||
|
type RawBacking = RawFd;
|
||||||
|
|
||||||
|
unsafe fn get_plane_buffer_backing(
|
||||||
|
m: &bindings::v4l2_plane__bindgen_ty_1,
|
||||||
|
) -> &Self::RawBacking {
|
||||||
|
&m.fd
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_single_planar_buffer_backing(
|
||||||
|
m: &bindings::v4l2_buffer__bindgen_ty_1,
|
||||||
|
) -> &Self::RawBacking {
|
||||||
|
&m.fd
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_plane_buffer_backing_mut(
|
||||||
|
m: &mut bindings::v4l2_plane__bindgen_ty_1,
|
||||||
|
) -> &mut Self::RawBacking {
|
||||||
|
&mut m.fd
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_single_planar_buffer_backing_mut(
|
||||||
|
m: &mut bindings::v4l2_buffer__bindgen_ty_1,
|
||||||
|
) -> &mut Self::RawBacking {
|
||||||
|
&mut m.fd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Imported for DmaBuf {}
|
||||||
|
|
||||||
|
pub trait DmaBufSource: AsRawFd + AsFd + Debug + Send {
|
||||||
|
fn len(&self) -> u64;
|
||||||
|
|
||||||
|
/// Make Clippy happy.
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DmaBufSource for std::fs::File {
|
||||||
|
fn len(&self) -> u64 {
|
||||||
|
match self.metadata() {
|
||||||
|
Err(_) => {
|
||||||
|
warn!("Failed to compute File size for use as DMABuf, using 0...");
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Ok(m) => m.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle for a DMABUF plane. Any type that can provide a file descriptor is
|
||||||
|
/// valid.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DmaBufHandle<T: DmaBufSource>(pub T);
|
||||||
|
|
||||||
|
impl<T: DmaBufSource> From<T> for DmaBufHandle<T> {
|
||||||
|
fn from(dmabuf: T) -> Self {
|
||||||
|
DmaBufHandle(dmabuf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DmaBufSource + 'static> PlaneHandle for DmaBufHandle<T> {
|
||||||
|
type Memory = DmaBuf;
|
||||||
|
|
||||||
|
fn fill_v4l2_plane(&self, plane: &mut bindings::v4l2_plane) {
|
||||||
|
plane.m.fd = self.0.as_raw_fd();
|
||||||
|
plane.length = self.0.len() as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DmaBufSource> DmaBufHandle<T> {
|
||||||
|
pub fn map(&self) -> Result<PlaneMapping, ioctl::MmapError> {
|
||||||
|
let len = self.0.len();
|
||||||
|
|
||||||
|
ioctl::mmap(&self.0, 0, len as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
libs/v4l2r/src/memory/mmap.rs
Normal file
58
libs/v4l2r/src/memory/mmap.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
//! Operations specific to MMAP-type buffers.
|
||||||
|
use super::*;
|
||||||
|
use crate::{bindings, ioctl};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::os::fd::AsFd;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Mmap;
|
||||||
|
|
||||||
|
impl Memory for Mmap {
|
||||||
|
const MEMORY_TYPE: MemoryType = MemoryType::Mmap;
|
||||||
|
type RawBacking = u32;
|
||||||
|
|
||||||
|
unsafe fn get_plane_buffer_backing(
|
||||||
|
m: &bindings::v4l2_plane__bindgen_ty_1,
|
||||||
|
) -> &Self::RawBacking {
|
||||||
|
&m.mem_offset
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_single_planar_buffer_backing(
|
||||||
|
m: &bindings::v4l2_buffer__bindgen_ty_1,
|
||||||
|
) -> &Self::RawBacking {
|
||||||
|
&m.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_plane_buffer_backing_mut(
|
||||||
|
m: &mut bindings::v4l2_plane__bindgen_ty_1,
|
||||||
|
) -> &mut Self::RawBacking {
|
||||||
|
&mut m.mem_offset
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_single_planar_buffer_backing_mut(
|
||||||
|
m: &mut bindings::v4l2_buffer__bindgen_ty_1,
|
||||||
|
) -> &mut Self::RawBacking {
|
||||||
|
&mut m.offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelfBacked for Mmap {}
|
||||||
|
|
||||||
|
/// Dummy handle for a MMAP plane, to use with APIs that require handles. MMAP
|
||||||
|
/// buffers are backed by the device, and thus we don't need to attach any extra
|
||||||
|
/// information to them.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct MmapHandle;
|
||||||
|
|
||||||
|
// There is no information to fill with MMAP buffers ; the index is enough.
|
||||||
|
impl PlaneHandle for MmapHandle {
|
||||||
|
type Memory = Mmap;
|
||||||
|
|
||||||
|
fn fill_v4l2_plane(&self, _plane: &mut bindings::v4l2_plane) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mappable for MmapHandle {
|
||||||
|
fn map<D: AsFd>(device: &D, plane_info: &QueryBufPlane) -> Option<PlaneMapping> {
|
||||||
|
ioctl::mmap(device, plane_info.mem_offset, plane_info.length).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
77
libs/v4l2r/src/memory/userptr.rs
Normal file
77
libs/v4l2r/src/memory/userptr.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//! Operations specific to UserPtr-type buffers.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::bindings;
|
||||||
|
|
||||||
|
pub struct UserPtr;
|
||||||
|
|
||||||
|
impl Memory for UserPtr {
|
||||||
|
const MEMORY_TYPE: MemoryType = MemoryType::UserPtr;
|
||||||
|
type RawBacking = core::ffi::c_ulong;
|
||||||
|
|
||||||
|
unsafe fn get_plane_buffer_backing(
|
||||||
|
m: &bindings::v4l2_plane__bindgen_ty_1,
|
||||||
|
) -> &Self::RawBacking {
|
||||||
|
&m.userptr
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_single_planar_buffer_backing(
|
||||||
|
m: &bindings::v4l2_buffer__bindgen_ty_1,
|
||||||
|
) -> &Self::RawBacking {
|
||||||
|
&m.userptr
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_plane_buffer_backing_mut(
|
||||||
|
m: &mut bindings::v4l2_plane__bindgen_ty_1,
|
||||||
|
) -> &mut Self::RawBacking {
|
||||||
|
&mut m.userptr
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_single_planar_buffer_backing_mut(
|
||||||
|
m: &mut bindings::v4l2_buffer__bindgen_ty_1,
|
||||||
|
) -> &mut Self::RawBacking {
|
||||||
|
&mut m.userptr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Imported for UserPtr {}
|
||||||
|
|
||||||
|
/// Handle for a USERPTR plane. These buffers are backed by userspace-allocated
|
||||||
|
/// memory, which translates well into Rust's slice of `u8`s. Since slices also
|
||||||
|
/// carry size information, we know that we are not passing unallocated areas
|
||||||
|
/// of the address-space to the kernel.
|
||||||
|
///
|
||||||
|
/// USERPTR buffers have the particularity that the `length` field of `struct
|
||||||
|
/// v4l2_buffer` must be set before doing a `QBUF` ioctl. This handle struct
|
||||||
|
/// also takes care of that.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UserPtrHandle<T: AsRef<[u8]> + Debug + Send + 'static>(pub T);
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]> + Debug + Send + Clone> Clone for UserPtrHandle<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
UserPtrHandle(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]> + Debug + Send + 'static> AsRef<[u8]> for UserPtrHandle<T> {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]> + Debug + Send> From<T> for UserPtrHandle<T> {
|
||||||
|
fn from(buffer: T) -> Self {
|
||||||
|
UserPtrHandle(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]> + Debug + Send + 'static> PlaneHandle for UserPtrHandle<T> {
|
||||||
|
type Memory = UserPtr;
|
||||||
|
|
||||||
|
fn fill_v4l2_plane(&self, plane: &mut bindings::v4l2_plane) {
|
||||||
|
let slice = AsRef::<[u8]>::as_ref(&self.0);
|
||||||
|
|
||||||
|
plane.m.userptr = slice.as_ptr() as std::os::raw::c_ulong;
|
||||||
|
plane.length = slice.len() as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
libs/v4l2r/v4l2r_wrapper.h
Normal file
10
libs/v4l2r/v4l2r_wrapper.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#ifdef __ANDROID__
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <linux/videodev2.h>
|
||||||
|
|
||||||
|
#define MARK_FIX_753(name) const unsigned long int Fix753_##name = name;
|
||||||
|
#include "fix753.h"
|
||||||
@@ -45,4 +45,4 @@ pub use error::{Result, VentoyError};
|
|||||||
pub use exfat::FileInfo;
|
pub use exfat::FileInfo;
|
||||||
pub use image::VentoyImage;
|
pub use image::VentoyImage;
|
||||||
pub use partition::{parse_size, PartitionLayout};
|
pub use partition::{parse_size, PartitionLayout};
|
||||||
pub use resources::{get_resource_dir, init_resources, is_initialized, required_files};
|
pub use resources::{init_resources, is_initialized, required_files};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use crate::error::{Result, VentoyError};
|
use crate::error::{Result, VentoyError};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
/// Resource file names
|
/// Resource file names
|
||||||
@@ -151,13 +151,6 @@ pub fn get_ventoy_disk_img() -> Result<&'static [u8]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the resource directory path for a given data directory
|
|
||||||
///
|
|
||||||
/// Returns `{data_dir}/ventoy`
|
|
||||||
pub fn get_resource_dir(data_dir: &Path) -> PathBuf {
|
|
||||||
data_dir.join("ventoy")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List required resource files
|
/// List required resource files
|
||||||
pub fn required_files() -> &'static [&'static str] {
|
pub fn required_files() -> &'static [&'static str] {
|
||||||
&[BOOT_IMG_NAME, CORE_IMG_NAME, VENTOY_DISK_IMG_NAME]
|
&[BOOT_IMG_NAME, CORE_IMG_NAME, VENTOY_DISK_IMG_NAME]
|
||||||
@@ -166,22 +159,6 @@ pub fn required_files() -> &'static [&'static str] {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::io::Write;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
fn create_test_resources(dir: &Path) {
|
|
||||||
// Create boot.img (512 bytes)
|
|
||||||
let mut boot = std::fs::File::create(dir.join(BOOT_IMG_NAME)).unwrap();
|
|
||||||
boot.write_all(&[0u8; 512]).unwrap();
|
|
||||||
|
|
||||||
// Create core.img (fake, 1KB)
|
|
||||||
let mut core = std::fs::File::create(dir.join(CORE_IMG_NAME)).unwrap();
|
|
||||||
core.write_all(&[0u8; 1024]).unwrap();
|
|
||||||
|
|
||||||
// Create ventoy.disk.img (fake, 1KB)
|
|
||||||
let mut ventoy = std::fs::File::create(dir.join(VENTOY_DISK_IMG_NAME)).unwrap();
|
|
||||||
ventoy.write_all(&[0u8; 1024]).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_files() {
|
fn test_required_files() {
|
||||||
@@ -191,11 +168,4 @@ mod tests {
|
|||||||
assert!(files.contains(&"core.img"));
|
assert!(files.contains(&"core.img"));
|
||||||
assert!(files.contains(&"ventoy.disk.img"));
|
assert!(files.contains(&"ventoy.disk.img"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_resource_dir() {
|
|
||||||
let data_dir = Path::new("/var/lib/one-kvm");
|
|
||||||
let resource_dir = get_resource_dir(data_dir);
|
|
||||||
assert_eq!(resource_dir, PathBuf::from("/var/lib/one-kvm/ventoy"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,4 @@ license = "BSD-3-Clause"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cc = "1.0"
|
bindgen = "0.70.1"
|
||||||
bindgen = "0.59"
|
|
||||||
|
|||||||
@@ -19,9 +19,19 @@ fn main() {
|
|||||||
|
|
||||||
fn generate_bindings(cpp_dir: &Path) {
|
fn generate_bindings(cpp_dir: &Path) {
|
||||||
let ffi_header = cpp_dir.join("yuv_ffi.h");
|
let ffi_header = cpp_dir.join("yuv_ffi.h");
|
||||||
|
let mut builder = bindgen::builder().header(ffi_header.to_string_lossy().to_string());
|
||||||
|
|
||||||
bindgen::builder()
|
if env::var("CARGO_CFG_TARGET_OS").ok().as_deref() == Some("android") {
|
||||||
.header(ffi_header.to_string_lossy().to_string())
|
println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME");
|
||||||
|
println!("cargo:rerun-if-env-changed=ANDROID_NDK_ROOT");
|
||||||
|
println!("cargo:rerun-if-env-changed=NDK_HOME");
|
||||||
|
println!("cargo:rerun-if-env-changed=ANDROID_HOME");
|
||||||
|
println!("cargo:rerun-if-env-changed=ANDROID_SDK_ROOT");
|
||||||
|
println!("cargo:rerun-if-env-changed=CARGO_NDK_PLATFORM");
|
||||||
|
builder = builder.clang_args(android_clang_args());
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
// YUYV conversions
|
// YUYV conversions
|
||||||
.allowlist_function("YUY2ToI420")
|
.allowlist_function("YUY2ToI420")
|
||||||
.allowlist_function("YUY2ToNV12")
|
.allowlist_function("YUY2ToNV12")
|
||||||
@@ -38,6 +48,7 @@ fn generate_bindings(cpp_dir: &Path) {
|
|||||||
// NV12/NV21 conversions
|
// NV12/NV21 conversions
|
||||||
.allowlist_function("NV12ToI420")
|
.allowlist_function("NV12ToI420")
|
||||||
.allowlist_function("NV21ToI420")
|
.allowlist_function("NV21ToI420")
|
||||||
|
.allowlist_function("NV21ToNV12")
|
||||||
.allowlist_function("NV12Copy")
|
.allowlist_function("NV12Copy")
|
||||||
.allowlist_function("SplitUVPlane")
|
.allowlist_function("SplitUVPlane")
|
||||||
// ARGB/BGRA conversions
|
// ARGB/BGRA conversions
|
||||||
@@ -49,6 +60,7 @@ fn generate_bindings(cpp_dir: &Path) {
|
|||||||
.allowlist_function("ABGRToARGB")
|
.allowlist_function("ABGRToARGB")
|
||||||
// RGB24/BGR24 conversions
|
// RGB24/BGR24 conversions
|
||||||
.allowlist_function("RGB24ToI420")
|
.allowlist_function("RGB24ToI420")
|
||||||
|
.allowlist_function("RGB24ToNV12")
|
||||||
.allowlist_function("RAWToI420")
|
.allowlist_function("RAWToI420")
|
||||||
.allowlist_function("RGB24ToARGB")
|
.allowlist_function("RGB24ToARGB")
|
||||||
.allowlist_function("RAWToARGB")
|
.allowlist_function("RAWToARGB")
|
||||||
@@ -62,6 +74,9 @@ fn generate_bindings(cpp_dir: &Path) {
|
|||||||
.allowlist_function("UYVYToARGB")
|
.allowlist_function("UYVYToARGB")
|
||||||
.allowlist_function("ARGBToRGB24")
|
.allowlist_function("ARGBToRGB24")
|
||||||
.allowlist_function("ARGBToRAW")
|
.allowlist_function("ARGBToRAW")
|
||||||
|
// MJPEG decoding
|
||||||
|
.allowlist_function("MJPGToNV12")
|
||||||
|
.allowlist_function("MJPGSize")
|
||||||
// Scaling
|
// Scaling
|
||||||
.allowlist_function("I420Scale")
|
.allowlist_function("I420Scale")
|
||||||
.allowlist_function("NV12Scale")
|
.allowlist_function("NV12Scale")
|
||||||
@@ -81,9 +96,33 @@ fn generate_bindings(cpp_dir: &Path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn link_libyuv() {
|
fn link_libyuv() {
|
||||||
|
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
|
||||||
|
if target_os == "android" {
|
||||||
|
if link_android_libyuv() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(vcpkg_installed) = vcpkg_installed_root() {
|
||||||
|
if link_vcpkg(vcpkg_installed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!(
|
||||||
|
"Android libyuv not found!\n\
|
||||||
|
\n\
|
||||||
|
Build it with scripts/build-android-libyuv.sh and set:\n\
|
||||||
|
export ONE_KVM_ANDROID_LIBYUV_ROOT=/path/to/android-libyuv\n\
|
||||||
|
\n\
|
||||||
|
Expected layout:\n\
|
||||||
|
$ONE_KVM_ANDROID_LIBYUV_ROOT/<abi>/include\n\
|
||||||
|
$ONE_KVM_ANDROID_LIBYUV_ROOT/<abi>/lib/libyuv.a"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Try vcpkg first
|
// Try vcpkg first
|
||||||
if let Ok(vcpkg_root) = env::var("VCPKG_ROOT") {
|
if let Some(vcpkg_installed) = vcpkg_installed_root() {
|
||||||
if link_vcpkg(vcpkg_root.into()) {
|
if link_vcpkg(vcpkg_installed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +148,233 @@ fn link_libyuv() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn link_android_libyuv() -> bool {
|
||||||
|
println!("cargo:rerun-if-env-changed=ONE_KVM_ANDROID_LIBYUV_ROOT");
|
||||||
|
println!("cargo:rerun-if-env-changed=ONE_KVM_ANDROID_LIBYUV_STATIC");
|
||||||
|
|
||||||
|
let root = match env::var("ONE_KVM_ANDROID_LIBYUV_ROOT")
|
||||||
|
.ok()
|
||||||
|
.filter(|path| !path.trim().is_empty())
|
||||||
|
{
|
||||||
|
Some(path) => PathBuf::from(path),
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||||
|
let abi = android_abi(&target_arch);
|
||||||
|
let abi_root = root.join(abi);
|
||||||
|
let lib_dir = if abi_root.join("lib").exists() {
|
||||||
|
abi_root.join("lib")
|
||||||
|
} else {
|
||||||
|
root.join("lib")
|
||||||
|
};
|
||||||
|
let include_dir = if abi_root.join("include").exists() {
|
||||||
|
abi_root.join("include")
|
||||||
|
} else {
|
||||||
|
root.join("include")
|
||||||
|
};
|
||||||
|
|
||||||
|
let static_lib = lib_dir.join("libyuv.a");
|
||||||
|
let shared_lib = lib_dir.join("libyuv.so");
|
||||||
|
let use_static = env::var("ONE_KVM_ANDROID_LIBYUV_STATIC")
|
||||||
|
.or_else(|_| env::var("LIBYUV_STATIC"))
|
||||||
|
.map(|value| value != "0")
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if use_static && static_lib.exists() {
|
||||||
|
println!("cargo:rustc-link-search=native={}", lib_dir.display());
|
||||||
|
println!("cargo:rustc-link-lib=static=yuv");
|
||||||
|
link_android_libjpeg(&root, abi);
|
||||||
|
println!("cargo:rustc-link-lib=c++_shared");
|
||||||
|
println!(
|
||||||
|
"cargo:info=Using Android libyuv from {} (static linking)",
|
||||||
|
root.display()
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if shared_lib.exists() {
|
||||||
|
println!("cargo:rustc-link-search=native={}", lib_dir.display());
|
||||||
|
println!("cargo:rustc-link-lib=yuv");
|
||||||
|
println!("cargo:rustc-link-lib=c++_shared");
|
||||||
|
println!(
|
||||||
|
"cargo:info=Using Android libyuv from {} (dynamic linking)",
|
||||||
|
root.display()
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:warning=Android libyuv not found under {} for ABI {} (checked {}, {})",
|
||||||
|
root.display(),
|
||||||
|
abi,
|
||||||
|
static_lib.display(),
|
||||||
|
shared_lib.display()
|
||||||
|
);
|
||||||
|
if !include_dir.exists() {
|
||||||
|
println!(
|
||||||
|
"cargo:warning=Android libyuv include directory not found: {}",
|
||||||
|
include_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_android_libjpeg(libyuv_root: &Path, abi: &str) {
|
||||||
|
println!("cargo:rerun-if-env-changed=ONE_KVM_ANDROID_TURBOJPEG_ROOT");
|
||||||
|
|
||||||
|
let mut roots = Vec::new();
|
||||||
|
if let Ok(root) = env::var("ONE_KVM_ANDROID_TURBOJPEG_ROOT") {
|
||||||
|
if !root.trim().is_empty() {
|
||||||
|
roots.push(PathBuf::from(root));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roots.push(libyuv_root.with_file_name("android-turbojpeg"));
|
||||||
|
|
||||||
|
for root in roots {
|
||||||
|
let abi_lib_dir = root.join(abi).join("lib");
|
||||||
|
let lib_dir = if abi_lib_dir.exists() {
|
||||||
|
abi_lib_dir
|
||||||
|
} else {
|
||||||
|
root.join("lib")
|
||||||
|
};
|
||||||
|
let jpeg_lib = lib_dir.join("libjpeg.a");
|
||||||
|
if jpeg_lib.exists() {
|
||||||
|
println!("cargo:rustc-link-search=native={}", lib_dir.display());
|
||||||
|
println!("cargo:rustc-link-lib=static=jpeg");
|
||||||
|
println!(
|
||||||
|
"cargo:info=Using Android libjpeg for libyuv MJPEG from {}",
|
||||||
|
root.display()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:warning=Android libjpeg.a not found; libyuv MJPEG symbols may fail to link");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn android_abi(target_arch: &str) -> &'static str {
|
||||||
|
match target_arch {
|
||||||
|
"aarch64" => "arm64-v8a",
|
||||||
|
"arm" => "armeabi-v7a",
|
||||||
|
"x86" => "x86",
|
||||||
|
"x86_64" => "x86_64",
|
||||||
|
_ => "unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn android_clang_args() -> Vec<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 libyuv bindgen: {other}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn android_ndk_home() -> PathBuf {
|
||||||
|
for key in ["ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "NDK_HOME"] {
|
||||||
|
if let Ok(value) = env::var(key) {
|
||||||
|
return PathBuf::from(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in ["ANDROID_HOME", "ANDROID_SDK_ROOT"] {
|
||||||
|
if let Ok(value) = env::var(key) {
|
||||||
|
let ndk_dir = PathBuf::from(value).join("ndk");
|
||||||
|
if let Some(newest) = newest_child_dir(&ndk_dir) {
|
||||||
|
return newest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!(
|
||||||
|
"libyuv Android bindgen requires ANDROID_NDK_HOME, ANDROID_NDK_ROOT, NDK_HOME, \
|
||||||
|
or ANDROID_HOME/ANDROID_SDK_ROOT with an ndk directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn newest_child_dir(path: &Path) -> Option<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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vcpkg_installed_root() -> Option<PathBuf> {
|
||||||
|
println!("cargo:rerun-if-env-changed=VCPKG_INSTALLED_DIR");
|
||||||
|
println!("cargo:rerun-if-env-changed=VCPKG_ROOT");
|
||||||
|
|
||||||
|
if let Ok(path) = env::var("VCPKG_INSTALLED_DIR") {
|
||||||
|
if !path.trim().is_empty() {
|
||||||
|
return Some(PathBuf::from(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env::var("VCPKG_ROOT")
|
||||||
|
.ok()
|
||||||
|
.filter(|path| !path.trim().is_empty())
|
||||||
|
.map(|path| PathBuf::from(path).join("installed"))
|
||||||
|
}
|
||||||
|
|
||||||
fn link_vcpkg(mut path: PathBuf) -> bool {
|
fn link_vcpkg(mut path: PathBuf) -> bool {
|
||||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
@@ -117,6 +383,10 @@ fn link_vcpkg(mut path: PathBuf) -> bool {
|
|||||||
("linux", "x86_64") => "x64-linux",
|
("linux", "x86_64") => "x64-linux",
|
||||||
("linux", "aarch64") => "arm64-linux",
|
("linux", "aarch64") => "arm64-linux",
|
||||||
("linux", "arm") => "arm-linux",
|
("linux", "arm") => "arm-linux",
|
||||||
|
("android", "x86_64") => "x64-android",
|
||||||
|
("android", "x86") => "x86-android",
|
||||||
|
("android", "aarch64") => "arm64-android",
|
||||||
|
("android", "arm") => "arm-neon-android",
|
||||||
("windows", "x86_64") => "x64-windows-static",
|
("windows", "x86_64") => "x64-windows-static",
|
||||||
("windows", "x86") => "x86-windows-static",
|
("windows", "x86") => "x86-windows-static",
|
||||||
("macos", "x86_64") => "x64-osx",
|
("macos", "x86_64") => "x64-osx",
|
||||||
@@ -130,7 +400,6 @@ fn link_vcpkg(mut path: PathBuf) -> bool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
path.push("installed");
|
|
||||||
path.push(triplet);
|
path.push(triplet);
|
||||||
|
|
||||||
let include_path = path.join("include");
|
let include_path = path.join("include");
|
||||||
@@ -154,12 +423,21 @@ fn link_vcpkg(mut path: PathBuf) -> bool {
|
|||||||
if use_static && static_lib.exists() {
|
if use_static && static_lib.exists() {
|
||||||
// Static linking (for deb packaging)
|
// Static linking (for deb packaging)
|
||||||
println!("cargo:rustc-link-lib=static=yuv");
|
println!("cargo:rustc-link-lib=static=yuv");
|
||||||
println!("cargo:rustc-link-lib=stdc++");
|
link_libjpeg_for_static_libyuv(&[lib_path.clone()], &target_os);
|
||||||
|
if target_os == "linux" {
|
||||||
|
println!("cargo:rustc-link-lib=stdc++");
|
||||||
|
} else if target_os == "android" {
|
||||||
|
println!("cargo:rustc-link-lib=c++_shared");
|
||||||
|
}
|
||||||
println!("cargo:info=Using libyuv from vcpkg (static linking)");
|
println!("cargo:info=Using libyuv from vcpkg (static linking)");
|
||||||
} else {
|
} else {
|
||||||
// Dynamic linking (default for development)
|
// Dynamic linking (default for development)
|
||||||
println!("cargo:rustc-link-lib=yuv");
|
println!("cargo:rustc-link-lib=yuv");
|
||||||
println!("cargo:rustc-link-lib=stdc++");
|
if target_os == "linux" {
|
||||||
|
println!("cargo:rustc-link-lib=stdc++");
|
||||||
|
} else if target_os == "android" {
|
||||||
|
println!("cargo:rustc-link-lib=c++_shared");
|
||||||
|
}
|
||||||
println!("cargo:info=Using libyuv from vcpkg (dynamic linking)");
|
println!("cargo:info=Using libyuv from vcpkg (dynamic linking)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +529,7 @@ fn link_system() -> bool {
|
|||||||
if use_static && libyuv_static.exists() {
|
if use_static && libyuv_static.exists() {
|
||||||
println!("cargo:rustc-link-search=native={}", path);
|
println!("cargo:rustc-link-search=native={}", path);
|
||||||
println!("cargo:rustc-link-lib=static=yuv");
|
println!("cargo:rustc-link-lib=static=yuv");
|
||||||
|
link_libjpeg_for_static_libyuv(&[lib_path.to_path_buf()], "linux");
|
||||||
println!("cargo:rustc-link-lib=stdc++");
|
println!("cargo:rustc-link-lib=stdc++");
|
||||||
println!(
|
println!(
|
||||||
"cargo:info=Using system libyuv from {} (static linking)",
|
"cargo:info=Using system libyuv from {} (static linking)",
|
||||||
@@ -277,3 +556,58 @@ fn link_system() -> bool {
|
|||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn link_libjpeg_for_static_libyuv(preferred_lib_dirs: &[PathBuf], target_os: &str) {
|
||||||
|
if target_os != "linux" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-env-changed=ONE_KVM_LIBJPEG_DIR");
|
||||||
|
|
||||||
|
let mut lib_dirs = Vec::new();
|
||||||
|
if let Ok(path) = env::var("ONE_KVM_LIBJPEG_DIR") {
|
||||||
|
if !path.trim().is_empty() {
|
||||||
|
lib_dirs.push(PathBuf::from(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lib_dirs.extend(preferred_lib_dirs.iter().cloned());
|
||||||
|
lib_dirs.extend(
|
||||||
|
[
|
||||||
|
"/usr/local/lib",
|
||||||
|
"/usr/local/lib64",
|
||||||
|
"/usr/lib",
|
||||||
|
"/usr/lib64",
|
||||||
|
"/usr/lib/x86_64-linux-gnu",
|
||||||
|
"/usr/lib/aarch64-linux-gnu",
|
||||||
|
"/usr/lib/arm-linux-gnueabihf",
|
||||||
|
"/usr/aarch64-linux-gnu/lib",
|
||||||
|
"/usr/arm-linux-gnueabihf/lib",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(PathBuf::from),
|
||||||
|
);
|
||||||
|
|
||||||
|
for lib_dir in dedupe_paths(lib_dirs) {
|
||||||
|
if lib_dir.join("libjpeg.a").exists() {
|
||||||
|
println!("cargo:rustc-link-search=native={}", lib_dir.display());
|
||||||
|
println!("cargo:rustc-link-lib=static=jpeg");
|
||||||
|
println!(
|
||||||
|
"cargo:info=Using libjpeg for static libyuv MJPEG from {}",
|
||||||
|
lib_dir.display()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:warning=libjpeg.a not found; static libyuv built with MJPEG may fail to link");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dedupe_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
|
||||||
|
let mut deduped = Vec::new();
|
||||||
|
for path in paths {
|
||||||
|
if !deduped.iter().any(|existing| existing == &path) {
|
||||||
|
deduped.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deduped
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ int NV21ToI420(const uint8_t* src_y, int src_stride_y,
|
|||||||
uint8_t* dst_v, int dst_stride_v,
|
uint8_t* dst_v, int dst_stride_v,
|
||||||
int width, int height);
|
int width, int height);
|
||||||
|
|
||||||
|
// NV21 -> NV12
|
||||||
|
int NV21ToNV12(const uint8_t* src_y, int src_stride_y,
|
||||||
|
const uint8_t* src_vu, int src_stride_vu,
|
||||||
|
uint8_t* dst_y, int dst_stride_y,
|
||||||
|
uint8_t* dst_uv, int dst_stride_uv,
|
||||||
|
int width, int height);
|
||||||
|
|
||||||
// Split interleaved UV plane into separate U and V planes
|
// Split interleaved UV plane into separate U and V planes
|
||||||
void SplitUVPlane(const uint8_t* src_uv, int src_stride_uv,
|
void SplitUVPlane(const uint8_t* src_uv, int src_stride_uv,
|
||||||
uint8_t* dst_u, int dst_stride_u,
|
uint8_t* dst_u, int dst_stride_u,
|
||||||
@@ -167,6 +174,12 @@ int RAWToI420(const uint8_t* src_raw, int src_stride_raw,
|
|||||||
uint8_t* dst_v, int dst_stride_v,
|
uint8_t* dst_v, int dst_stride_v,
|
||||||
int width, int height);
|
int width, int height);
|
||||||
|
|
||||||
|
// BGR24 -> NV12
|
||||||
|
int RGB24ToNV12(const uint8_t* src_rgb24, int src_stride_rgb24,
|
||||||
|
uint8_t* dst_y, int dst_stride_y,
|
||||||
|
uint8_t* dst_uv, int dst_stride_uv,
|
||||||
|
int width, int height);
|
||||||
|
|
||||||
// RGB24 -> ARGB
|
// RGB24 -> ARGB
|
||||||
int RGB24ToARGB(const uint8_t* src_rgb24, int src_stride_rgb24,
|
int RGB24ToARGB(const uint8_t* src_rgb24, int src_stride_rgb24,
|
||||||
uint8_t* dst_argb, int dst_stride_argb,
|
uint8_t* dst_argb, int dst_stride_argb,
|
||||||
@@ -253,12 +266,6 @@ int MJPGToNV12(const uint8_t* sample, size_t sample_size,
|
|||||||
int src_width, int src_height,
|
int src_width, int src_height,
|
||||||
int dst_width, int dst_height);
|
int dst_width, int dst_height);
|
||||||
|
|
||||||
// MJPEG -> ARGB
|
|
||||||
int MJPGToARGB(const uint8_t* sample, size_t sample_size,
|
|
||||||
uint8_t* dst_argb, int dst_stride_argb,
|
|
||||||
int src_width, int src_height,
|
|
||||||
int dst_width, int dst_height);
|
|
||||||
|
|
||||||
// Get MJPEG dimensions without decoding
|
// Get MJPEG dimensions without decoding
|
||||||
int MJPGSize(const uint8_t* sample, size_t sample_size,
|
int MJPGSize(const uint8_t* sample, size_t sample_size,
|
||||||
int* width, int* height);
|
int* width, int* height);
|
||||||
|
|||||||
@@ -32,17 +32,9 @@ use std::fmt;
|
|||||||
// Include auto-generated FFI bindings
|
// Include auto-generated FFI bindings
|
||||||
include!(concat!(env!("OUT_DIR"), "/yuv_ffi.rs"));
|
include!(concat!(env!("OUT_DIR"), "/yuv_ffi.rs"));
|
||||||
|
|
||||||
// Type alias for C's size_t - adapts to platform pointer width
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
type SizeT = u32;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
type SizeT = u64;
|
|
||||||
|
|
||||||
// Helper function to convert usize to C's size_t type
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn usize_to_size_t(val: usize) -> SizeT {
|
fn usize_to_size_t(val: usize) -> usize {
|
||||||
val as SizeT
|
val
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -364,8 +356,7 @@ pub fn split_uv_plane(
|
|||||||
let dst_u_required = (dst_stride_u as usize).saturating_mul(height);
|
let dst_u_required = (dst_stride_u as usize).saturating_mul(height);
|
||||||
let dst_v_required = (dst_stride_v as usize).saturating_mul(height);
|
let dst_v_required = (dst_stride_v as usize).saturating_mul(height);
|
||||||
|
|
||||||
if src_uv.len() < src_required || dst_u.len() < dst_u_required || dst_v.len() < dst_v_required
|
if src_uv.len() < src_required || dst_u.len() < dst_u_required || dst_v.len() < dst_v_required {
|
||||||
{
|
|
||||||
return Err(YuvError::BufferTooSmall);
|
return Err(YuvError::BufferTooSmall);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,6 +514,34 @@ pub fn nv21_to_i420(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Resu
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert NV21 to NV12 by swapping interleaved chroma bytes.
|
||||||
|
pub fn nv21_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> {
|
||||||
|
if width % 2 != 0 || height % 2 != 0 {
|
||||||
|
return Err(YuvError::InvalidDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
let w = width as usize;
|
||||||
|
let h = height as usize;
|
||||||
|
let y_size = w * h;
|
||||||
|
|
||||||
|
if src.len() < nv12_size(w, h) || dst.len() < nv12_size(w, h) {
|
||||||
|
return Err(YuvError::BufferTooSmall);
|
||||||
|
}
|
||||||
|
|
||||||
|
call_yuv!(NV21ToNV12(
|
||||||
|
src.as_ptr(),
|
||||||
|
width,
|
||||||
|
src[y_size..].as_ptr(),
|
||||||
|
width,
|
||||||
|
dst.as_mut_ptr(),
|
||||||
|
width,
|
||||||
|
dst[y_size..].as_mut_ptr(),
|
||||||
|
width,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ARGB/BGRA conversions (32-bit)
|
// ARGB/BGRA conversions (32-bit)
|
||||||
// Note: libyuv ARGB = BGRA in memory on little-endian systems
|
// Note: libyuv ARGB = BGRA in memory on little-endian systems
|
||||||
@@ -1047,7 +1066,7 @@ pub fn rgb24_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Res
|
|||||||
i420_to_nv12(&i420_buffer, dst, width, height)
|
i420_to_nv12(&i420_buffer, dst, width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert BGR24 to NV12 (via two-step conversion: BGR24 → I420 → NV12)
|
/// Convert BGR24 to NV12.
|
||||||
pub fn bgr24_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> {
|
pub fn bgr24_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> {
|
||||||
if width % 2 != 0 || height % 2 != 0 {
|
if width % 2 != 0 || height % 2 != 0 {
|
||||||
return Err(YuvError::InvalidDimensions);
|
return Err(YuvError::InvalidDimensions);
|
||||||
@@ -1060,10 +1079,71 @@ pub fn bgr24_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Res
|
|||||||
return Err(YuvError::BufferTooSmall);
|
return Err(YuvError::BufferTooSmall);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two-step conversion: BGR24 → I420 → NV12
|
#[cfg(windows)]
|
||||||
let mut i420_buffer = vec![0u8; i420_size(w, h)];
|
{
|
||||||
bgr24_to_i420(src, &mut i420_buffer, width, height)?;
|
let mut i420_buffer = vec![0u8; i420_size(w, h)];
|
||||||
i420_to_nv12(&i420_buffer, dst, width, height)
|
bgr24_to_i420(src, &mut i420_buffer, width, height)?;
|
||||||
|
return i420_to_nv12(&i420_buffer, dst, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let y_size = w * h;
|
||||||
|
call_yuv!(RGB24ToNV12(
|
||||||
|
src.as_ptr(),
|
||||||
|
width * 3,
|
||||||
|
dst.as_mut_ptr(),
|
||||||
|
width,
|
||||||
|
dst[y_size..].as_mut_ptr(),
|
||||||
|
width,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read MJPEG dimensions without decoding the frame.
|
||||||
|
pub fn mjpg_size(src: &[u8]) -> Result<(i32, i32)> {
|
||||||
|
let mut width = 0;
|
||||||
|
let mut height = 0;
|
||||||
|
|
||||||
|
call_yuv!(MJPGSize(
|
||||||
|
src.as_ptr(),
|
||||||
|
usize_to_size_t(src.len()),
|
||||||
|
&mut width,
|
||||||
|
&mut height,
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok((width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode MJPEG directly to NV12.
|
||||||
|
pub fn mjpg_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> {
|
||||||
|
if width % 2 != 0 || height % 2 != 0 {
|
||||||
|
return Err(YuvError::InvalidDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
let w = width as usize;
|
||||||
|
let h = height as usize;
|
||||||
|
if dst.len() < nv12_size(w, h) {
|
||||||
|
return Err(YuvError::BufferTooSmall);
|
||||||
|
}
|
||||||
|
|
||||||
|
let y_size = w * h;
|
||||||
|
let (dst_y, dst_uv) = dst.split_at_mut(y_size);
|
||||||
|
|
||||||
|
call_yuv!(MJPGToNV12(
|
||||||
|
src.as_ptr(),
|
||||||
|
usize_to_size_t(src.len()),
|
||||||
|
dst_y.as_mut_ptr(),
|
||||||
|
width,
|
||||||
|
dst_uv.as_mut_ptr(),
|
||||||
|
width,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
219
scripts/build-android-alsa.sh
Normal file
219
scripts/build-android-alsa.sh
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
SOURCE_DIR=""
|
||||||
|
OUTPUT_DIR="${PROJECT_ROOT}/dist/android-alsa"
|
||||||
|
ANDROID_API="${ANDROID_API:-21}"
|
||||||
|
NDK_ROOT="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}"
|
||||||
|
BUILD_ABIS="arm64-v8a armeabi-v7a"
|
||||||
|
JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}"
|
||||||
|
ALSA_REPO="${ALSA_REPO:-https://github.com/alsa-project/alsa-lib.git}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
scripts/build-android-alsa.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--source <dir> Existing alsa-lib source checkout. If omitted, the
|
||||||
|
script clones it into .tmp/android-alsa-src.
|
||||||
|
--output <dir> Output root. Default: dist/android-alsa
|
||||||
|
--ndk <dir> Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT.
|
||||||
|
--api <level> Android API level. Default: 21.
|
||||||
|
--abis <list> Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a.
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
The output layout is compatible with ONE_KVM_ANDROID_ALSA_ROOT:
|
||||||
|
<output>/arm64-v8a/include/alsa/asoundlib.h
|
||||||
|
<output>/arm64-v8a/lib/libasound.so
|
||||||
|
<output>/arm64-v8a/lib/pkgconfig/alsa.pc
|
||||||
|
<output>/armeabi-v7a/include/alsa/asoundlib.h
|
||||||
|
<output>/armeabi-v7a/lib/libasound.so
|
||||||
|
<output>/armeabi-v7a/lib/pkgconfig/alsa.pc
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--source)
|
||||||
|
SOURCE_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
OUTPUT_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ndk)
|
||||||
|
NDK_ROOT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--api)
|
||||||
|
ANDROID_API="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--abis)
|
||||||
|
BUILD_ABIS="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required"
|
||||||
|
[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT"
|
||||||
|
|
||||||
|
if [[ -z "$SOURCE_DIR" ]]; then
|
||||||
|
SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-alsa-src"
|
||||||
|
if [[ ! -d "$SOURCE_DIR/.git" ]]; then
|
||||||
|
rm -rf "$SOURCE_DIR"
|
||||||
|
git clone --depth 1 "$ALSA_REPO" "$SOURCE_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -d "$SOURCE_DIR" ]] || fail "alsa-lib source not found: $SOURCE_DIR"
|
||||||
|
[[ -f "$SOURCE_DIR/configure.ac" || -f "$SOURCE_DIR/configure" ]] || fail "alsa-lib source layout not recognized under: $SOURCE_DIR"
|
||||||
|
|
||||||
|
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
||||||
|
|
||||||
|
HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64"
|
||||||
|
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||||
|
ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake"
|
||||||
|
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||||
|
[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE"
|
||||||
|
command -v cmake >/dev/null 2>&1 || fail "cmake is required"
|
||||||
|
command -v autoreconf >/dev/null 2>&1 || fail "autoreconf is required"
|
||||||
|
|
||||||
|
normalize_abis() {
|
||||||
|
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_generated_source_headers() {
|
||||||
|
rm -f \
|
||||||
|
"$SOURCE_DIR/include/asoundlib.h" \
|
||||||
|
"$SOURCE_DIR/include/version.h" \
|
||||||
|
"$SOURCE_DIR/include/stamp-vh" \
|
||||||
|
"$SOURCE_DIR/include/alsa"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_one() {
|
||||||
|
local abi="$1"
|
||||||
|
local prefix build_dir
|
||||||
|
|
||||||
|
case "$abi" in
|
||||||
|
arm64-v8a | armeabi-v7a) ;;
|
||||||
|
*) fail "Unsupported ABI: $abi" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
prefix="${OUTPUT_DIR}/${abi}"
|
||||||
|
build_dir="${PROJECT_ROOT}/.tmp/alsa-android-build/${abi}"
|
||||||
|
|
||||||
|
rm -rf "$build_dir"
|
||||||
|
mkdir -p "$build_dir" "$prefix"
|
||||||
|
|
||||||
|
case "$abi" in
|
||||||
|
arm64-v8a)
|
||||||
|
export CC="${TOOLCHAIN}/bin/aarch64-linux-android${ANDROID_API}-clang"
|
||||||
|
export CXX="${TOOLCHAIN}/bin/aarch64-linux-android${ANDROID_API}-clang++"
|
||||||
|
export HOST_TRIPLE="aarch64-linux-android"
|
||||||
|
;;
|
||||||
|
armeabi-v7a)
|
||||||
|
export CC="${TOOLCHAIN}/bin/armv7a-linux-androideabi${ANDROID_API}-clang"
|
||||||
|
export CXX="${TOOLCHAIN}/bin/armv7a-linux-androideabi${ANDROID_API}-clang++"
|
||||||
|
export HOST_TRIPLE="arm-linux-androideabi"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
export AR="${TOOLCHAIN}/bin/llvm-ar"
|
||||||
|
export RANLIB="${TOOLCHAIN}/bin/llvm-ranlib"
|
||||||
|
export STRIP="${TOOLCHAIN}/bin/llvm-strip"
|
||||||
|
export CFLAGS="-fPIC"
|
||||||
|
export CXXFLAGS="-fPIC"
|
||||||
|
|
||||||
|
clean_generated_source_headers
|
||||||
|
|
||||||
|
if [[ -f "$SOURCE_DIR/config.status" || -f "$SOURCE_DIR/Makefile" ]]; then
|
||||||
|
(
|
||||||
|
cd "$SOURCE_DIR"
|
||||||
|
make distclean >/dev/null 2>&1 || true
|
||||||
|
)
|
||||||
|
clean_generated_source_headers
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$SOURCE_DIR/configure" ]]; then
|
||||||
|
(
|
||||||
|
cd "$SOURCE_DIR"
|
||||||
|
autoreconf -fi
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$build_dir"
|
||||||
|
pcm_plugins="copy linear route mulaw alaw adpcm rate plug multi file null empty meter hooks lfloat ladspa asym iec958 softvol extplug ioplug mmap_emul"
|
||||||
|
ctl_plugins="remap ext"
|
||||||
|
ac_cv_header_sys_shm_h=no \
|
||||||
|
"$SOURCE_DIR/configure" \
|
||||||
|
--host="$HOST_TRIPLE" \
|
||||||
|
--prefix="$prefix" \
|
||||||
|
--enable-shared \
|
||||||
|
--disable-static \
|
||||||
|
--disable-python \
|
||||||
|
--with-pcm-plugins="$pcm_plugins" \
|
||||||
|
--with-ctl-plugins="$ctl_plugins" \
|
||||||
|
--disable-doc \
|
||||||
|
--disable-oss \
|
||||||
|
--disable-seq \
|
||||||
|
--disable-rawmidi \
|
||||||
|
--disable-hwdep \
|
||||||
|
--disable-usb \
|
||||||
|
--disable-firewire \
|
||||||
|
--disable-instr \
|
||||||
|
--disable-alisp
|
||||||
|
make -j"$JOBS"
|
||||||
|
make install
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "$prefix/lib/pkgconfig"
|
||||||
|
cat > "$prefix/lib/pkgconfig/alsa.pc" <<EOF
|
||||||
|
prefix=\${pcfiledir}/../..
|
||||||
|
exec_prefix=\${prefix}
|
||||||
|
libdir=\${exec_prefix}/lib
|
||||||
|
includedir=\${prefix}/include
|
||||||
|
|
||||||
|
Name: alsa
|
||||||
|
Description: ALSA sound library
|
||||||
|
Version: 1.2.15
|
||||||
|
Libs: -L\${libdir} -lasound
|
||||||
|
Cflags: -I\${includedir}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Built ALSA for ${abi}: ${prefix}"
|
||||||
|
}
|
||||||
|
|
||||||
|
for abi in $(normalize_abis); do
|
||||||
|
build_one "$abi"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Done.
|
||||||
|
|
||||||
|
Use this when building the Android APK:
|
||||||
|
export ONE_KVM_ANDROID_ALSA_ROOT="${OUTPUT_DIR}"
|
||||||
|
cd android && ./gradlew :app:assembleDebug
|
||||||
|
EOF
|
||||||
296
scripts/build-android-ffmpeg-mediacodec.sh
Normal file
296
scripts/build-android-ffmpeg-mediacodec.sh
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
SOURCE_DIR=""
|
||||||
|
OUTPUT_DIR="${PROJECT_ROOT}/dist/android-ffmpeg-mediacodec"
|
||||||
|
ANDROID_API="${ANDROID_API:-21}"
|
||||||
|
NDK_ROOT="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}"
|
||||||
|
BUILD_ABIS="arm64-v8a armeabi-v7a"
|
||||||
|
JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
scripts/build-android-ffmpeg-mediacodec.sh --source <ffmpeg-source-dir> [options]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--source <dir> FFmpeg source directory. For the downloaded package,
|
||||||
|
use the extracted ffmpeg-rockchip directory.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--output <dir> Output root. Default: dist/android-ffmpeg-mediacodec
|
||||||
|
--ndk <dir> Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT.
|
||||||
|
--api <level> Android API level. Default: 21.
|
||||||
|
--abis <list> Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a.
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
The output layout is compatible with ONE_KVM_ANDROID_FFMPEG_ROOT:
|
||||||
|
<output>/arm64-v8a/include
|
||||||
|
<output>/arm64-v8a/lib
|
||||||
|
<output>/armeabi-v7a/include
|
||||||
|
<output>/armeabi-v7a/lib
|
||||||
|
|
||||||
|
Example:
|
||||||
|
scripts/build-android-ffmpeg-mediacodec.sh \
|
||||||
|
--source .tmp/android-ffmpeg-check/src/ffmpeg-rockchip \
|
||||||
|
--output /opt/one-kvm/android-ffmpeg
|
||||||
|
|
||||||
|
export ONE_KVM_ANDROID_FFMPEG_ROOT=/opt/one-kvm/android-ffmpeg
|
||||||
|
cd android && ./gradlew :app:assembleDebug
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--source)
|
||||||
|
SOURCE_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
OUTPUT_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ndk)
|
||||||
|
NDK_ROOT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--api)
|
||||||
|
ANDROID_API="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--abis)
|
||||||
|
BUILD_ABIS="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$SOURCE_DIR" ]] || fail "--source is required"
|
||||||
|
[[ -d "$SOURCE_DIR" ]] || fail "FFmpeg source not found: $SOURCE_DIR"
|
||||||
|
[[ -x "$SOURCE_DIR/configure" ]] || fail "FFmpeg configure script not found under: $SOURCE_DIR"
|
||||||
|
[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required"
|
||||||
|
[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT"
|
||||||
|
|
||||||
|
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
||||||
|
|
||||||
|
HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64"
|
||||||
|
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||||
|
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||||
|
|
||||||
|
normalize_abis() {
|
||||||
|
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
patch_android_ffmpeg_mjpeg_mediacodec() {
|
||||||
|
local avcodec_dir="${SOURCE_DIR}/libavcodec"
|
||||||
|
local configure_file="${SOURCE_DIR}/configure"
|
||||||
|
local mediacodecdec="${avcodec_dir}/mediacodecdec.c"
|
||||||
|
local allcodecs="${avcodec_dir}/allcodecs.c"
|
||||||
|
local makefile="${avcodec_dir}/Makefile"
|
||||||
|
|
||||||
|
[[ -f "$mediacodecdec" ]] || fail "FFmpeg mediacodecdec.c not found: $mediacodecdec"
|
||||||
|
[[ -f "$allcodecs" ]] || fail "FFmpeg allcodecs.c not found: $allcodecs"
|
||||||
|
[[ -f "$makefile" ]] || fail "FFmpeg libavcodec Makefile not found: $makefile"
|
||||||
|
[[ -f "$configure_file" ]] || fail "FFmpeg configure not found: $configure_file"
|
||||||
|
|
||||||
|
python3 - "$mediacodecdec" "$allcodecs" "$configure_file" "$makefile" <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
mediacodecdec, allcodecs, configure_file, makefile = map(Path, sys.argv[1:])
|
||||||
|
|
||||||
|
def replace_once(path: Path, old: str, new: str) -> None:
|
||||||
|
text = path.read_text()
|
||||||
|
if new in text:
|
||||||
|
return
|
||||||
|
if old not in text:
|
||||||
|
raise SystemExit(f"patch anchor not found in {path}: {old!r}")
|
||||||
|
path.write_text(text.replace(old, new, 1))
|
||||||
|
|
||||||
|
replace_once(
|
||||||
|
mediacodecdec,
|
||||||
|
"CONFIG_MPEG2_MEDIACODEC_DECODER || \\\n",
|
||||||
|
"CONFIG_MJPEG_MEDIACODEC_DECODER || \\\n"
|
||||||
|
" CONFIG_MPEG2_MEDIACODEC_DECODER || \\\n",
|
||||||
|
)
|
||||||
|
replace_once(
|
||||||
|
mediacodecdec,
|
||||||
|
"#if CONFIG_MPEG2_MEDIACODEC_DECODER\n"
|
||||||
|
" case AV_CODEC_ID_MPEG2VIDEO:",
|
||||||
|
"#if CONFIG_MJPEG_MEDIACODEC_DECODER\n"
|
||||||
|
" case AV_CODEC_ID_MJPEG:\n"
|
||||||
|
" codec_mime = \"video/mjpeg\";\n\n"
|
||||||
|
" ret = common_set_extradata(avctx, format);\n"
|
||||||
|
" if (ret < 0)\n"
|
||||||
|
" goto done;\n"
|
||||||
|
" break;\n"
|
||||||
|
"#endif\n"
|
||||||
|
"#if CONFIG_MPEG2_MEDIACODEC_DECODER\n"
|
||||||
|
" case AV_CODEC_ID_MPEG2VIDEO:",
|
||||||
|
)
|
||||||
|
replace_once(
|
||||||
|
mediacodecdec,
|
||||||
|
"#if CONFIG_MPEG2_MEDIACODEC_DECODER\n"
|
||||||
|
"DECLARE_MEDIACODEC_VDEC(mpeg2, \"MPEG-2\", AV_CODEC_ID_MPEG2VIDEO, NULL)",
|
||||||
|
"#if CONFIG_MJPEG_MEDIACODEC_DECODER\n"
|
||||||
|
"DECLARE_MEDIACODEC_VDEC(mjpeg, \"MJPEG\", AV_CODEC_ID_MJPEG, NULL)\n"
|
||||||
|
"#endif\n\n"
|
||||||
|
"#if CONFIG_MPEG2_MEDIACODEC_DECODER\n"
|
||||||
|
"DECLARE_MEDIACODEC_VDEC(mpeg2, \"MPEG-2\", AV_CODEC_ID_MPEG2VIDEO, NULL)",
|
||||||
|
)
|
||||||
|
replace_once(
|
||||||
|
allcodecs,
|
||||||
|
"extern const FFCodec ff_mjpeg_cuvid_decoder;",
|
||||||
|
"extern const FFCodec ff_mjpeg_cuvid_decoder;\n"
|
||||||
|
"extern const FFCodec ff_mjpeg_mediacodec_decoder;",
|
||||||
|
)
|
||||||
|
replace_once(
|
||||||
|
configure_file,
|
||||||
|
'mjpeg_cuvid_decoder_deps="cuvid"',
|
||||||
|
'mjpeg_cuvid_decoder_deps="cuvid"\n'
|
||||||
|
'mjpeg_mediacodec_decoder_deps="mediacodec"',
|
||||||
|
)
|
||||||
|
replace_once(
|
||||||
|
makefile,
|
||||||
|
"OBJS-$(CONFIG_MJPEG_RKMPP_DECODER)",
|
||||||
|
"OBJS-$(CONFIG_MJPEG_MEDIACODEC_DECODER) += mediacodecdec.o\n"
|
||||||
|
"OBJS-$(CONFIG_MJPEG_RKMPP_DECODER)",
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
abi_arch() {
|
||||||
|
case "$1" in
|
||||||
|
arm64-v8a) echo "aarch64" ;;
|
||||||
|
armeabi-v7a) echo "arm" ;;
|
||||||
|
*) fail "Unsupported ABI: $1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
abi_cpu() {
|
||||||
|
case "$1" in
|
||||||
|
arm64-v8a) echo "armv8-a" ;;
|
||||||
|
armeabi-v7a) echo "armv7-a" ;;
|
||||||
|
*) fail "Unsupported ABI: $1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
abi_target() {
|
||||||
|
case "$1" in
|
||||||
|
arm64-v8a) echo "aarch64-linux-android" ;;
|
||||||
|
armeabi-v7a) echo "armv7a-linux-androideabi" ;;
|
||||||
|
*) fail "Unsupported ABI: $1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
build_one() {
|
||||||
|
local abi="$1"
|
||||||
|
local arch cpu target prefix build_dir cc cxx ar ranlib strip extra_cflags extra_ldflags
|
||||||
|
|
||||||
|
arch="$(abi_arch "$abi")"
|
||||||
|
cpu="$(abi_cpu "$abi")"
|
||||||
|
target="$(abi_target "$abi")"
|
||||||
|
prefix="${OUTPUT_DIR}/${abi}"
|
||||||
|
build_dir="${PROJECT_ROOT}/.tmp/ffmpeg-android-build/${abi}"
|
||||||
|
cc="${TOOLCHAIN}/bin/${target}${ANDROID_API}-clang"
|
||||||
|
cxx="${TOOLCHAIN}/bin/${target}${ANDROID_API}-clang++"
|
||||||
|
ar="${TOOLCHAIN}/bin/llvm-ar"
|
||||||
|
ranlib="${TOOLCHAIN}/bin/llvm-ranlib"
|
||||||
|
strip="${TOOLCHAIN}/bin/llvm-strip"
|
||||||
|
extra_cflags="-fPIC"
|
||||||
|
extra_ldflags=""
|
||||||
|
|
||||||
|
[[ -x "$cc" ]] || fail "Missing compiler: $cc"
|
||||||
|
|
||||||
|
if [[ "$abi" == "armeabi-v7a" ]]; then
|
||||||
|
extra_cflags="${extra_cflags} -march=armv7-a -mfloat-abi=softfp -mfpu=neon"
|
||||||
|
extra_ldflags="${extra_ldflags} -Wl,--fix-cortex-a8"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$build_dir"
|
||||||
|
mkdir -p "$build_dir" "$prefix"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$build_dir"
|
||||||
|
"${SOURCE_DIR}/configure" \
|
||||||
|
--prefix="$prefix" \
|
||||||
|
--target-os=android \
|
||||||
|
--arch="$arch" \
|
||||||
|
--cpu="$cpu" \
|
||||||
|
--cc="$cc" \
|
||||||
|
--cxx="$cxx" \
|
||||||
|
--ar="$ar" \
|
||||||
|
--ranlib="$ranlib" \
|
||||||
|
--strip="$strip" \
|
||||||
|
--cross-prefix="${TOOLCHAIN}/bin/llvm-" \
|
||||||
|
--sysroot="${TOOLCHAIN}/sysroot" \
|
||||||
|
--enable-cross-compile \
|
||||||
|
--enable-static \
|
||||||
|
--disable-shared \
|
||||||
|
--disable-programs \
|
||||||
|
--disable-doc \
|
||||||
|
--disable-avdevice \
|
||||||
|
--disable-avformat \
|
||||||
|
--disable-avfilter \
|
||||||
|
--disable-swscale \
|
||||||
|
--disable-swresample \
|
||||||
|
--disable-postproc \
|
||||||
|
--disable-network \
|
||||||
|
--disable-everything \
|
||||||
|
--disable-hwaccels \
|
||||||
|
--disable-cuda-llvm \
|
||||||
|
--disable-v4l2-m2m \
|
||||||
|
--disable-vulkan \
|
||||||
|
--enable-pthreads \
|
||||||
|
--enable-jni \
|
||||||
|
--enable-mediacodec \
|
||||||
|
--enable-decoder=mjpeg_mediacodec \
|
||||||
|
--enable-decoder=mjpeg \
|
||||||
|
--enable-encoder=h264_mediacodec \
|
||||||
|
--enable-encoder=hevc_mediacodec \
|
||||||
|
--enable-parser=mjpeg \
|
||||||
|
--enable-bsf=h264_metadata \
|
||||||
|
--enable-bsf=hevc_metadata \
|
||||||
|
--enable-protocol=file \
|
||||||
|
--extra-cflags="$extra_cflags" \
|
||||||
|
--extra-ldflags="$extra_ldflags"
|
||||||
|
|
||||||
|
make -j"$JOBS"
|
||||||
|
make install
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Built FFmpeg MediaCodec for ${abi}: ${prefix}"
|
||||||
|
}
|
||||||
|
|
||||||
|
patch_android_ffmpeg_mjpeg_mediacodec
|
||||||
|
|
||||||
|
for abi in $(normalize_abis); do
|
||||||
|
build_one "$abi"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Done.
|
||||||
|
|
||||||
|
Use this when building the Android APK:
|
||||||
|
export ONE_KVM_ANDROID_FFMPEG_ROOT="${OUTPUT_DIR}"
|
||||||
|
cd android && ./gradlew :app:assembleDebug
|
||||||
|
EOF
|
||||||
189
scripts/build-android-libyuv.sh
Normal file
189
scripts/build-android-libyuv.sh
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
SOURCE_DIR=""
|
||||||
|
OUTPUT_DIR="${PROJECT_ROOT}/dist/android-libyuv"
|
||||||
|
JPEG_ROOT="${ONE_KVM_ANDROID_TURBOJPEG_ROOT:-${PROJECT_ROOT}/dist/android-turbojpeg}"
|
||||||
|
ANDROID_API="${ANDROID_API:-21}"
|
||||||
|
NDK_ROOT="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}"
|
||||||
|
BUILD_ABIS="arm64-v8a armeabi-v7a"
|
||||||
|
JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}"
|
||||||
|
LIBYUV_REPO="${LIBYUV_REPO:-https://github.com/lemenkov/libyuv.git}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
scripts/build-android-libyuv.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--source <dir> Existing libyuv source checkout. If omitted, the script
|
||||||
|
clones libyuv into .tmp/android-libyuv-src.
|
||||||
|
--output <dir> Output root. Default: dist/android-libyuv
|
||||||
|
--ndk <dir> Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT.
|
||||||
|
--api <level> Android API level. Default: 21.
|
||||||
|
--abis <list> Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a.
|
||||||
|
--jpeg-root <dir> Android libjpeg root. Defaults to ONE_KVM_ANDROID_TURBOJPEG_ROOT
|
||||||
|
or dist/android-turbojpeg when present. Enables libyuv HAVE_JPEG.
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
The output layout is compatible with ONE_KVM_ANDROID_LIBYUV_ROOT:
|
||||||
|
<output>/arm64-v8a/include
|
||||||
|
<output>/arm64-v8a/lib/libyuv.a
|
||||||
|
<output>/armeabi-v7a/include
|
||||||
|
<output>/armeabi-v7a/lib/libyuv.a
|
||||||
|
|
||||||
|
Example:
|
||||||
|
scripts/build-android-libyuv.sh --output /opt/one-kvm/android-libyuv
|
||||||
|
|
||||||
|
export ONE_KVM_ANDROID_LIBYUV_ROOT=/opt/one-kvm/android-libyuv
|
||||||
|
cd android && ./gradlew :app:assembleDebug
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--source)
|
||||||
|
SOURCE_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
OUTPUT_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ndk)
|
||||||
|
NDK_ROOT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--api)
|
||||||
|
ANDROID_API="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--abis)
|
||||||
|
BUILD_ABIS="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--jpeg-root)
|
||||||
|
JPEG_ROOT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required"
|
||||||
|
[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT"
|
||||||
|
|
||||||
|
if [[ -z "$SOURCE_DIR" ]]; then
|
||||||
|
SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-libyuv-src"
|
||||||
|
if [[ ! -d "$SOURCE_DIR/.git" ]]; then
|
||||||
|
rm -rf "$SOURCE_DIR"
|
||||||
|
git clone --depth 1 "$LIBYUV_REPO" "$SOURCE_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -d "$SOURCE_DIR" ]] || fail "libyuv source not found: $SOURCE_DIR"
|
||||||
|
[[ -f "$SOURCE_DIR/CMakeLists.txt" ]] || fail "libyuv CMakeLists.txt not found under: $SOURCE_DIR"
|
||||||
|
|
||||||
|
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
||||||
|
|
||||||
|
HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64"
|
||||||
|
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||||
|
ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake"
|
||||||
|
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||||
|
[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE"
|
||||||
|
command -v cmake >/dev/null 2>&1 || fail "cmake is required"
|
||||||
|
|
||||||
|
normalize_abis() {
|
||||||
|
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
build_one() {
|
||||||
|
local abi="$1"
|
||||||
|
local prefix build_dir jpeg_include jpeg_library
|
||||||
|
local -a jpeg_args
|
||||||
|
|
||||||
|
case "$abi" in
|
||||||
|
arm64-v8a | armeabi-v7a | x86 | x86_64) ;;
|
||||||
|
*) fail "Unsupported ABI: $abi" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
prefix="${OUTPUT_DIR}/${abi}"
|
||||||
|
build_dir="${PROJECT_ROOT}/.tmp/libyuv-android-build/${abi}"
|
||||||
|
|
||||||
|
rm -rf "$build_dir"
|
||||||
|
mkdir -p "$build_dir" "$prefix"
|
||||||
|
|
||||||
|
jpeg_include="$JPEG_ROOT/$abi/include"
|
||||||
|
jpeg_library="$JPEG_ROOT/$abi/lib/libjpeg.a"
|
||||||
|
jpeg_args=()
|
||||||
|
if [[ -f "$jpeg_library" && -f "$jpeg_include/jpeglib.h" ]]; then
|
||||||
|
jpeg_args=(
|
||||||
|
-DJPEG_FOUND=TRUE
|
||||||
|
-DJPEG_INCLUDE_DIR="$jpeg_include"
|
||||||
|
-DJPEG_LIBRARY="$jpeg_library"
|
||||||
|
-DCMAKE_C_FLAGS="-DHAVE_JPEG"
|
||||||
|
-DCMAKE_CXX_FLAGS="-DHAVE_JPEG"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
echo "Warning: Android libjpeg not found for ${abi}; libyuv MJPEG APIs will be disabled." >&2
|
||||||
|
echo " Checked: $jpeg_library and $jpeg_include/jpeglib.h" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmake -S "$SOURCE_DIR" -B "$build_dir" \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$ANDROID_TOOLCHAIN_FILE" \
|
||||||
|
-DANDROID_ABI="$abi" \
|
||||||
|
-DANDROID_PLATFORM="android-${ANDROID_API}" \
|
||||||
|
-DANDROID_STL=c++_shared \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_INSTALL_PREFIX="$prefix" \
|
||||||
|
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DUNIT_TEST=OFF \
|
||||||
|
-DTEST=OFF \
|
||||||
|
"${jpeg_args[@]}"
|
||||||
|
|
||||||
|
cmake --build "$build_dir" --target yuv --parallel "$JOBS"
|
||||||
|
|
||||||
|
mkdir -p "$prefix/lib" "$prefix/include"
|
||||||
|
if [[ -f "$build_dir/libyuv.a" ]]; then
|
||||||
|
cp "$build_dir/libyuv.a" "$prefix/lib/libyuv.a"
|
||||||
|
elif [[ -f "$build_dir/lib/libyuv.a" ]]; then
|
||||||
|
cp "$build_dir/lib/libyuv.a" "$prefix/lib/libyuv.a"
|
||||||
|
else
|
||||||
|
fail "Built libyuv.a was not found under: $build_dir"
|
||||||
|
fi
|
||||||
|
cp -R "$SOURCE_DIR/include/." "$prefix/include/"
|
||||||
|
|
||||||
|
echo "Built libyuv for ${abi}: ${prefix}"
|
||||||
|
}
|
||||||
|
|
||||||
|
for abi in $(normalize_abis); do
|
||||||
|
build_one "$abi"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Done.
|
||||||
|
|
||||||
|
Use this when building the Android APK:
|
||||||
|
export ONE_KVM_ANDROID_LIBYUV_ROOT="${OUTPUT_DIR}"
|
||||||
|
export ONE_KVM_ANDROID_TURBOJPEG_ROOT="${JPEG_ROOT}"
|
||||||
|
cd android && ./gradlew :app:assembleDebug
|
||||||
|
EOF
|
||||||
186
scripts/build-android-opus.sh
Normal file
186
scripts/build-android-opus.sh
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
SOURCE_DIR=""
|
||||||
|
OUTPUT_DIR="${PROJECT_ROOT}/dist/android-opus"
|
||||||
|
ANDROID_API="${ANDROID_API:-21}"
|
||||||
|
NDK_ROOT="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}"
|
||||||
|
BUILD_ABIS="arm64-v8a armeabi-v7a"
|
||||||
|
JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}"
|
||||||
|
OPUS_VERSION="${OPUS_VERSION:-1.5.2}"
|
||||||
|
OPUS_TARBALL_URL="${OPUS_TARBALL_URL:-https://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz}"
|
||||||
|
OPUS_TARBALL_SHA256="${OPUS_TARBALL_SHA256:-65c1d2f78b9f2fb20082c38cbe47c951ad5839345876e46941612ee87f9a7ce1}"
|
||||||
|
LOCAL_OPUS_TARBALL="${LOCAL_OPUS_TARBALL:-${PROJECT_ROOT}/opus-${OPUS_VERSION}.tar.gz}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
scripts/build-android-opus.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--source <dir> Existing opus source checkout. If omitted, the script
|
||||||
|
downloads and extracts the official source tarball
|
||||||
|
into .tmp/android-opus-src.
|
||||||
|
--output <dir> Output root. Default: dist/android-opus
|
||||||
|
--ndk <dir> Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT.
|
||||||
|
--api <level> Android API level. Default: 21.
|
||||||
|
--abis <list> Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a.
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
The output layout is compatible with ONE_KVM_ANDROID_OPUS_ROOT:
|
||||||
|
<output>/arm64-v8a/include/opus/opus.h
|
||||||
|
<output>/arm64-v8a/lib/libopus.so
|
||||||
|
<output>/armeabi-v7a/include/opus/opus.h
|
||||||
|
<output>/armeabi-v7a/lib/libopus.so
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--source)
|
||||||
|
SOURCE_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
OUTPUT_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ndk)
|
||||||
|
NDK_ROOT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--api)
|
||||||
|
ANDROID_API="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--abis)
|
||||||
|
BUILD_ABIS="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required"
|
||||||
|
[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT"
|
||||||
|
|
||||||
|
if [[ -z "$SOURCE_DIR" ]]; then
|
||||||
|
SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-opus-src"
|
||||||
|
if [[ ! -f "$SOURCE_DIR/configure" ]]; then
|
||||||
|
rm -rf "$SOURCE_DIR"
|
||||||
|
mkdir -p "$SOURCE_DIR"
|
||||||
|
tarball="${PROJECT_ROOT}/.tmp/opus-${OPUS_VERSION}.tar.gz"
|
||||||
|
if [[ -f "$LOCAL_OPUS_TARBALL" ]]; then
|
||||||
|
cp "$LOCAL_OPUS_TARBALL" "$tarball"
|
||||||
|
else
|
||||||
|
command -v curl >/dev/null 2>&1 || fail "curl is required to download opus source"
|
||||||
|
curl -fsSL "$OPUS_TARBALL_URL" -o "$tarball"
|
||||||
|
fi
|
||||||
|
echo "${OPUS_TARBALL_SHA256} ${tarball}" | sha256sum -c -
|
||||||
|
tar -xzf "$tarball" -C "$SOURCE_DIR" --strip-components=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -d "$SOURCE_DIR" ]] || fail "opus source not found: $SOURCE_DIR"
|
||||||
|
[[ -x "$SOURCE_DIR/configure" || -f "$SOURCE_DIR/configure.ac" ]] || fail "opus source layout not recognized under: $SOURCE_DIR"
|
||||||
|
|
||||||
|
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
||||||
|
|
||||||
|
HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64"
|
||||||
|
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||||
|
ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake"
|
||||||
|
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||||
|
[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE"
|
||||||
|
command -v cmake >/dev/null 2>&1 || fail "cmake is required"
|
||||||
|
|
||||||
|
normalize_abis() {
|
||||||
|
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
build_one() {
|
||||||
|
local abi="$1"
|
||||||
|
local prefix build_dir
|
||||||
|
|
||||||
|
case "$abi" in
|
||||||
|
arm64-v8a | armeabi-v7a) ;;
|
||||||
|
*) fail "Unsupported ABI: $abi" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
prefix="${OUTPUT_DIR}/${abi}"
|
||||||
|
build_dir="${PROJECT_ROOT}/.tmp/opus-android-build/${abi}"
|
||||||
|
|
||||||
|
rm -rf "$build_dir"
|
||||||
|
mkdir -p "$build_dir" "$prefix"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$build_dir"
|
||||||
|
case "$abi" in
|
||||||
|
arm64-v8a)
|
||||||
|
export CC="${TOOLCHAIN}/bin/aarch64-linux-android${ANDROID_API}-clang"
|
||||||
|
export CXX="${TOOLCHAIN}/bin/aarch64-linux-android${ANDROID_API}-clang++"
|
||||||
|
export HOST_TRIPLE="aarch64-linux-android"
|
||||||
|
;;
|
||||||
|
armeabi-v7a)
|
||||||
|
export CC="${TOOLCHAIN}/bin/armv7a-linux-androideabi${ANDROID_API}-clang"
|
||||||
|
export CXX="${TOOLCHAIN}/bin/armv7a-linux-androideabi${ANDROID_API}-clang++"
|
||||||
|
export HOST_TRIPLE="arm-linux-androideabi"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
export AR="${TOOLCHAIN}/bin/llvm-ar"
|
||||||
|
export RANLIB="${TOOLCHAIN}/bin/llvm-ranlib"
|
||||||
|
export STRIP="${TOOLCHAIN}/bin/llvm-strip"
|
||||||
|
export CFLAGS="-fPIC"
|
||||||
|
export CXXFLAGS="-fPIC"
|
||||||
|
export LDFLAGS=""
|
||||||
|
"$SOURCE_DIR/configure" \
|
||||||
|
--prefix="$prefix" \
|
||||||
|
--host="$HOST_TRIPLE" \
|
||||||
|
--disable-static \
|
||||||
|
--enable-shared \
|
||||||
|
--disable-doc \
|
||||||
|
--disable-extra-programs \
|
||||||
|
--with-pic
|
||||||
|
make -j"$JOBS"
|
||||||
|
make install
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "$prefix/lib" "$prefix/include"
|
||||||
|
if [[ -f "$prefix/include/opus/opus.h" ]]; then
|
||||||
|
:
|
||||||
|
elif [[ -f "$SOURCE_DIR/include/opus/opus.h" ]]; then
|
||||||
|
mkdir -p "$prefix/include/opus"
|
||||||
|
cp "$SOURCE_DIR/include/opus/opus.h" "$prefix/include/opus/opus.h"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Built Opus for ${abi}: ${prefix}"
|
||||||
|
}
|
||||||
|
|
||||||
|
for abi in $(normalize_abis); do
|
||||||
|
build_one "$abi"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Done.
|
||||||
|
|
||||||
|
Use this when building the Android APK:
|
||||||
|
export ONE_KVM_ANDROID_OPUS_ROOT="${OUTPUT_DIR}"
|
||||||
|
cd android && ./gradlew :app:assembleDebug
|
||||||
|
EOF
|
||||||
178
scripts/build-android-turbojpeg.sh
Normal file
178
scripts/build-android-turbojpeg.sh
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
SOURCE_DIR=""
|
||||||
|
OUTPUT_DIR="${PROJECT_ROOT}/dist/android-turbojpeg"
|
||||||
|
ANDROID_API="${ANDROID_API:-21}"
|
||||||
|
NDK_ROOT="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}"
|
||||||
|
BUILD_ABIS="arm64-v8a armeabi-v7a"
|
||||||
|
JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}"
|
||||||
|
LIBJPEG_TURBO_REPO="${LIBJPEG_TURBO_REPO:-https://github.com/libjpeg-turbo/libjpeg-turbo.git}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
scripts/build-android-turbojpeg.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--source <dir> Existing libjpeg-turbo source checkout. If omitted,
|
||||||
|
the script clones it into .tmp/android-turbojpeg-src.
|
||||||
|
--output <dir> Output root. Default: dist/android-turbojpeg
|
||||||
|
--ndk <dir> Android NDK root. Defaults to ANDROID_NDK_HOME or ANDROID_NDK_ROOT.
|
||||||
|
--api <level> Android API level. Default: 21.
|
||||||
|
--abis <list> Space/comma separated ABI list. Default: arm64-v8a armeabi-v7a.
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
The output layout is compatible with ONE_KVM_ANDROID_TURBOJPEG_ROOT:
|
||||||
|
<output>/arm64-v8a/include/turbojpeg.h
|
||||||
|
<output>/arm64-v8a/lib/libturbojpeg.a
|
||||||
|
<output>/arm64-v8a/include/jpeglib.h
|
||||||
|
<output>/arm64-v8a/lib/libjpeg.a
|
||||||
|
<output>/armeabi-v7a/include/turbojpeg.h
|
||||||
|
<output>/armeabi-v7a/lib/libturbojpeg.a
|
||||||
|
<output>/armeabi-v7a/include/jpeglib.h
|
||||||
|
<output>/armeabi-v7a/lib/libjpeg.a
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--source)
|
||||||
|
SOURCE_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output)
|
||||||
|
OUTPUT_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ndk)
|
||||||
|
NDK_ROOT="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--api)
|
||||||
|
ANDROID_API="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--abis)
|
||||||
|
BUILD_ABIS="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$NDK_ROOT" ]] || fail "--ndk or ANDROID_NDK_HOME/ANDROID_NDK_ROOT is required"
|
||||||
|
[[ -d "$NDK_ROOT/toolchains/llvm/prebuilt" ]] || fail "Invalid NDK root: $NDK_ROOT"
|
||||||
|
|
||||||
|
if [[ -z "$SOURCE_DIR" ]]; then
|
||||||
|
SOURCE_DIR="${PROJECT_ROOT}/.tmp/android-turbojpeg-src"
|
||||||
|
if [[ ! -d "$SOURCE_DIR/.git" ]]; then
|
||||||
|
rm -rf "$SOURCE_DIR"
|
||||||
|
git clone --depth 1 "$LIBJPEG_TURBO_REPO" "$SOURCE_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -d "$SOURCE_DIR" ]] || fail "libjpeg-turbo source not found: $SOURCE_DIR"
|
||||||
|
[[ -f "$SOURCE_DIR/CMakeLists.txt" ]] || fail "libjpeg-turbo CMakeLists.txt not found under: $SOURCE_DIR"
|
||||||
|
|
||||||
|
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
|
||||||
|
|
||||||
|
HOST_TAG="$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64"
|
||||||
|
TOOLCHAIN="${NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
|
||||||
|
ANDROID_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake"
|
||||||
|
[[ -d "$TOOLCHAIN/bin" ]] || fail "NDK LLVM toolchain not found: $TOOLCHAIN"
|
||||||
|
[[ -f "$ANDROID_TOOLCHAIN_FILE" ]] || fail "NDK CMake toolchain not found: $ANDROID_TOOLCHAIN_FILE"
|
||||||
|
command -v cmake >/dev/null 2>&1 || fail "cmake is required"
|
||||||
|
|
||||||
|
normalize_abis() {
|
||||||
|
printf '%s\n' "$BUILD_ABIS" | tr ',' ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
build_one() {
|
||||||
|
local abi="$1"
|
||||||
|
local prefix build_dir lib_path
|
||||||
|
|
||||||
|
case "$abi" in
|
||||||
|
arm64-v8a | armeabi-v7a | x86 | x86_64) ;;
|
||||||
|
*) fail "Unsupported ABI: $abi" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
prefix="${OUTPUT_DIR}/${abi}"
|
||||||
|
build_dir="${PROJECT_ROOT}/.tmp/turbojpeg-android-build/${abi}"
|
||||||
|
|
||||||
|
rm -rf "$build_dir"
|
||||||
|
mkdir -p "$build_dir" "$prefix"
|
||||||
|
|
||||||
|
cmake -S "$SOURCE_DIR" -B "$build_dir" \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$ANDROID_TOOLCHAIN_FILE" \
|
||||||
|
-DANDROID_ABI="$abi" \
|
||||||
|
-DANDROID_PLATFORM="android-${ANDROID_API}" \
|
||||||
|
-DANDROID_STL=c++_shared \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_INSTALL_PREFIX="$prefix" \
|
||||||
|
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
|
-DCMAKE_C_FLAGS="-DANDROID -Dstderr=__sF+2" \
|
||||||
|
-DCMAKE_CXX_FLAGS="-DANDROID -Dstderr=__sF+2" \
|
||||||
|
-DENABLE_SHARED=OFF \
|
||||||
|
-DENABLE_STATIC=ON \
|
||||||
|
-DWITH_TURBOJPEG=ON \
|
||||||
|
-DWITH_JAVA=OFF \
|
||||||
|
-DWITH_12BIT=OFF \
|
||||||
|
-DWITH_ARITH_DEC=ON \
|
||||||
|
-DWITH_ARITH_ENC=ON
|
||||||
|
|
||||||
|
cmake --build "$build_dir" --target turbojpeg-static jpeg-static --parallel "$JOBS"
|
||||||
|
|
||||||
|
mkdir -p "$prefix/lib" "$prefix/include"
|
||||||
|
lib_path="$build_dir/libturbojpeg.a"
|
||||||
|
if [[ ! -f "$lib_path" ]]; then
|
||||||
|
lib_path="$build_dir/lib/libturbojpeg.a"
|
||||||
|
fi
|
||||||
|
[[ -f "$lib_path" ]] || fail "Built libturbojpeg.a was not found under: $build_dir"
|
||||||
|
|
||||||
|
cp "$lib_path" "$prefix/lib/libturbojpeg.a"
|
||||||
|
lib_path="$build_dir/libjpeg.a"
|
||||||
|
if [[ ! -f "$lib_path" ]]; then
|
||||||
|
lib_path="$build_dir/lib/libjpeg.a"
|
||||||
|
fi
|
||||||
|
[[ -f "$lib_path" ]] || fail "Built libjpeg.a was not found under: $build_dir"
|
||||||
|
|
||||||
|
cp "$lib_path" "$prefix/lib/libjpeg.a"
|
||||||
|
cp "$SOURCE_DIR/src/turbojpeg.h" "$prefix/include/turbojpeg.h"
|
||||||
|
cp "$SOURCE_DIR/src/jerror.h" "$prefix/include/jerror.h"
|
||||||
|
cp "$SOURCE_DIR/src/jmorecfg.h" "$prefix/include/jmorecfg.h"
|
||||||
|
cp "$SOURCE_DIR/src/jpeglib.h" "$prefix/include/jpeglib.h"
|
||||||
|
cp "$build_dir/jconfig.h" "$prefix/include/jconfig.h"
|
||||||
|
|
||||||
|
echo "Built TurboJPEG for ${abi}: ${prefix}"
|
||||||
|
}
|
||||||
|
|
||||||
|
for abi in $(normalize_abis); do
|
||||||
|
build_one "$abi"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Done.
|
||||||
|
|
||||||
|
Use this when building the Android APK:
|
||||||
|
export ONE_KVM_ANDROID_TURBOJPEG_ROOT="${OUTPUT_DIR}"
|
||||||
|
cd android && ./gradlew :app:assembleDebug
|
||||||
|
EOF
|
||||||
@@ -11,20 +11,14 @@ use super::led::LedSensor;
|
|||||||
use super::types::{AtxAction, AtxKeyConfig, AtxLedConfig, AtxState, PowerStatus};
|
use super::types::{AtxAction, AtxKeyConfig, AtxLedConfig, AtxState, PowerStatus};
|
||||||
use crate::error::{AppError, Result};
|
use crate::error::{AppError, Result};
|
||||||
|
|
||||||
/// ATX power control configuration
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct AtxControllerConfig {
|
pub struct AtxControllerConfig {
|
||||||
/// Whether ATX is enabled
|
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// Power button configuration (used for both short and long press)
|
|
||||||
pub power: AtxKeyConfig,
|
pub power: AtxKeyConfig,
|
||||||
/// Reset button configuration
|
|
||||||
pub reset: AtxKeyConfig,
|
pub reset: AtxKeyConfig,
|
||||||
/// LED sensing configuration
|
|
||||||
pub led: AtxLedConfig,
|
pub led: AtxLedConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal state holding all ATX components
|
|
||||||
/// Grouped together to reduce lock acquisitions
|
/// Grouped together to reduce lock acquisitions
|
||||||
struct AtxInner {
|
struct AtxInner {
|
||||||
config: AtxControllerConfig,
|
config: AtxControllerConfig,
|
||||||
@@ -33,46 +27,99 @@ struct AtxInner {
|
|||||||
led_sensor: Option<LedSensor>,
|
led_sensor: Option<LedSensor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ATX Controller
|
|
||||||
///
|
|
||||||
/// Manages ATX power control through independent executors for each action.
|
/// Manages ATX power control through independent executors for each action.
|
||||||
/// Supports hot-reload of configuration.
|
/// Supports hot-reload of configuration.
|
||||||
pub struct AtxController {
|
pub struct AtxController {
|
||||||
/// Single lock for all internal state to reduce lock contention
|
|
||||||
inner: RwLock<AtxInner>,
|
inner: RwLock<AtxInner>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AtxController {
|
impl AtxController {
|
||||||
|
fn should_share_serial_device(power: &AtxKeyConfig, reset: &AtxKeyConfig) -> bool {
|
||||||
|
power.is_configured()
|
||||||
|
&& reset.is_configured()
|
||||||
|
&& power.driver == super::types::AtxDriverType::Serial
|
||||||
|
&& reset.driver == super::types::AtxDriverType::Serial
|
||||||
|
&& !power.device.is_empty()
|
||||||
|
&& power.device == reset.device
|
||||||
|
&& power.baud_rate == reset.baud_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_key_executor(
|
||||||
|
warn_label: &str,
|
||||||
|
info_label: &str,
|
||||||
|
config: AtxKeyConfig,
|
||||||
|
mut executor: AtxKeyExecutor,
|
||||||
|
) -> Option<AtxKeyExecutor> {
|
||||||
|
if let Err(e) = executor.init().await {
|
||||||
|
warn!("Failed to initialize {} executor: {}", warn_label, e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"{} executor initialized: {:?} on {} pin {}",
|
||||||
|
info_label, config.driver, config.device, config.pin
|
||||||
|
);
|
||||||
|
Some(executor)
|
||||||
|
}
|
||||||
|
|
||||||
async fn init_components(inner: &mut AtxInner) {
|
async fn init_components(inner: &mut AtxInner) {
|
||||||
// Initialize power executor
|
if Self::should_share_serial_device(&inner.config.power, &inner.config.reset) {
|
||||||
if inner.config.power.is_configured() {
|
match AtxKeyExecutor::open_shared_serial(
|
||||||
let mut executor = AtxKeyExecutor::new(inner.config.power.clone());
|
&inner.config.power.device,
|
||||||
if let Err(e) = executor.init().await {
|
inner.config.power.baud_rate,
|
||||||
warn!("Failed to initialize power executor: {}", e);
|
) {
|
||||||
} else {
|
Ok(shared_serial) => {
|
||||||
info!(
|
for (slot, warn_label, info_label, config, serial) in [
|
||||||
"Power executor initialized: {:?} on {} pin {}",
|
(
|
||||||
inner.config.power.driver, inner.config.power.device, inner.config.power.pin
|
&mut inner.power_executor,
|
||||||
);
|
"power",
|
||||||
inner.power_executor = Some(executor);
|
"Power",
|
||||||
|
inner.config.power.clone(),
|
||||||
|
shared_serial.clone(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&mut inner.reset_executor,
|
||||||
|
"reset",
|
||||||
|
"Reset",
|
||||||
|
inner.config.reset.clone(),
|
||||||
|
shared_serial,
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let executor =
|
||||||
|
AtxKeyExecutor::new_with_shared_serial(config.clone(), serial);
|
||||||
|
*slot =
|
||||||
|
Self::init_key_executor(warn_label, info_label, config, executor).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Failed to open shared serial device {} for ATX power/reset: {}",
|
||||||
|
inner.config.power.device, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (slot, warn_label, info_label, config) in [
|
||||||
|
(
|
||||||
|
&mut inner.power_executor,
|
||||||
|
"power",
|
||||||
|
"Power",
|
||||||
|
inner.config.power.clone(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&mut inner.reset_executor,
|
||||||
|
"reset",
|
||||||
|
"Reset",
|
||||||
|
inner.config.reset.clone(),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
if config.is_configured() {
|
||||||
|
let executor = AtxKeyExecutor::new(config.clone());
|
||||||
|
*slot = Self::init_key_executor(warn_label, info_label, config, executor).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize reset executor
|
|
||||||
if inner.config.reset.is_configured() {
|
|
||||||
let mut executor = AtxKeyExecutor::new(inner.config.reset.clone());
|
|
||||||
if let Err(e) = executor.init().await {
|
|
||||||
warn!("Failed to initialize reset executor: {}", e);
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
"Reset executor initialized: {:?} on {} pin {}",
|
|
||||||
inner.config.reset.driver, inner.config.reset.device, inner.config.reset.pin
|
|
||||||
);
|
|
||||||
inner.reset_executor = Some(executor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize LED sensor
|
|
||||||
if inner.config.led.is_configured() {
|
if inner.config.led.is_configured() {
|
||||||
let mut sensor = LedSensor::new(inner.config.led.clone());
|
let mut sensor = LedSensor::new(inner.config.led.clone());
|
||||||
if let Err(e) = sensor.init().await {
|
if let Err(e) = sensor.init().await {
|
||||||
@@ -88,19 +135,17 @@ impl AtxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown_components(inner: &mut AtxInner) {
|
async fn shutdown_components(inner: &mut AtxInner) {
|
||||||
if let Some(executor) = inner.power_executor.as_mut() {
|
for (slot, label) in [
|
||||||
if let Err(e) = executor.shutdown().await {
|
(&mut inner.power_executor, "power"),
|
||||||
warn!("Failed to shutdown power executor: {}", e);
|
(&mut inner.reset_executor, "reset"),
|
||||||
|
] {
|
||||||
|
if let Some(executor) = slot.as_mut() {
|
||||||
|
if let Err(e) = executor.shutdown().await {
|
||||||
|
warn!("Failed to shutdown {} executor: {}", label, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
*slot = None;
|
||||||
}
|
}
|
||||||
inner.power_executor = None;
|
|
||||||
|
|
||||||
if let Some(executor) = inner.reset_executor.as_mut() {
|
|
||||||
if let Err(e) = executor.shutdown().await {
|
|
||||||
warn!("Failed to shutdown reset executor: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inner.reset_executor = None;
|
|
||||||
|
|
||||||
if let Some(sensor) = inner.led_sensor.as_mut() {
|
if let Some(sensor) = inner.led_sensor.as_mut() {
|
||||||
if let Err(e) = sensor.shutdown().await {
|
if let Err(e) = sensor.shutdown().await {
|
||||||
@@ -110,7 +155,20 @@ impl AtxController {
|
|||||||
inner.led_sensor = None;
|
inner.led_sensor = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new ATX controller with the specified configuration
|
async fn read_power_status(sensor: Option<&LedSensor>) -> PowerStatus {
|
||||||
|
let Some(sensor) = sensor else {
|
||||||
|
return PowerStatus::Unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
match sensor.read().await {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to read ATX LED sensor: {}", e);
|
||||||
|
PowerStatus::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(config: AtxControllerConfig) -> Self {
|
pub fn new(config: AtxControllerConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: RwLock::new(AtxInner {
|
inner: RwLock::new(AtxInner {
|
||||||
@@ -122,12 +180,10 @@ impl AtxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a disabled ATX controller
|
|
||||||
pub fn disabled() -> Self {
|
pub fn disabled() -> Self {
|
||||||
Self::new(AtxControllerConfig::default())
|
Self::new(AtxControllerConfig::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the ATX controller and its executors
|
|
||||||
pub async fn init(&self) -> Result<()> {
|
pub async fn init(&self) -> Result<()> {
|
||||||
let mut inner = self.inner.write().await;
|
let mut inner = self.inner.write().await;
|
||||||
|
|
||||||
@@ -143,7 +199,6 @@ impl AtxController {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reload ATX controller configuration
|
|
||||||
pub async fn reload(&self, config: AtxControllerConfig) -> Result<()> {
|
pub async fn reload(&self, config: AtxControllerConfig) -> Result<()> {
|
||||||
let mut inner = self.inner.write().await;
|
let mut inner = self.inner.write().await;
|
||||||
|
|
||||||
@@ -164,7 +219,6 @@ impl AtxController {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shutdown ATX controller and release all resources
|
|
||||||
pub async fn shutdown(&self) -> Result<()> {
|
pub async fn shutdown(&self) -> Result<()> {
|
||||||
let mut inner = self.inner.write().await;
|
let mut inner = self.inner.write().await;
|
||||||
Self::shutdown_components(&mut inner).await;
|
Self::shutdown_components(&mut inner).await;
|
||||||
@@ -172,86 +226,50 @@ impl AtxController {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trigger a power action (short/long/reset)
|
|
||||||
pub async fn trigger_power_action(&self, action: AtxAction) -> Result<()> {
|
pub async fn trigger_power_action(&self, action: AtxAction) -> Result<()> {
|
||||||
let inner = self.inner.read().await;
|
let inner = self.inner.read().await;
|
||||||
|
|
||||||
match action {
|
let (executor, duration) = match action {
|
||||||
AtxAction::Short | AtxAction::Long => {
|
AtxAction::Short => (inner.power_executor.as_ref(), timing::SHORT_PRESS),
|
||||||
if let Some(executor) = &inner.power_executor {
|
AtxAction::Long => (inner.power_executor.as_ref(), timing::LONG_PRESS),
|
||||||
let duration = match action {
|
AtxAction::Reset => (inner.reset_executor.as_ref(), timing::RESET_PRESS),
|
||||||
AtxAction::Short => timing::SHORT_PRESS,
|
};
|
||||||
AtxAction::Long => timing::LONG_PRESS,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
executor.pulse(duration).await?;
|
|
||||||
} else {
|
|
||||||
return Err(AppError::Config(
|
|
||||||
"Power button not configured for ATX controller".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AtxAction::Reset => {
|
|
||||||
if let Some(executor) = &inner.reset_executor {
|
|
||||||
executor.pulse(timing::RESET_PRESS).await?;
|
|
||||||
} else {
|
|
||||||
return Err(AppError::Config(
|
|
||||||
"Reset button not configured for ATX controller".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let Some(executor) = executor else {
|
||||||
|
return Err(AppError::Config(
|
||||||
|
match action {
|
||||||
|
AtxAction::Reset => "Reset button not configured for ATX controller",
|
||||||
|
_ => "Power button not configured for ATX controller",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
executor.pulse(duration).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trigger a short power button press
|
|
||||||
pub async fn power_short(&self) -> Result<()> {
|
pub async fn power_short(&self) -> Result<()> {
|
||||||
self.trigger_power_action(AtxAction::Short).await
|
self.trigger_power_action(AtxAction::Short).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trigger a long power button press
|
|
||||||
pub async fn power_long(&self) -> Result<()> {
|
pub async fn power_long(&self) -> Result<()> {
|
||||||
self.trigger_power_action(AtxAction::Long).await
|
self.trigger_power_action(AtxAction::Long).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trigger a reset button press
|
|
||||||
pub async fn reset(&self) -> Result<()> {
|
pub async fn reset(&self) -> Result<()> {
|
||||||
self.trigger_power_action(AtxAction::Reset).await
|
self.trigger_power_action(AtxAction::Reset).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current power status using the LED sensor (if configured)
|
|
||||||
pub async fn power_status(&self) -> PowerStatus {
|
pub async fn power_status(&self) -> PowerStatus {
|
||||||
let inner = self.inner.read().await;
|
let inner = self.inner.read().await;
|
||||||
|
Self::read_power_status(inner.led_sensor.as_ref()).await
|
||||||
if let Some(sensor) = &inner.led_sensor {
|
|
||||||
match sensor.read().await {
|
|
||||||
Ok(status) => status,
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to read ATX LED sensor: {}", e);
|
|
||||||
PowerStatus::Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PowerStatus::Unknown
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a snapshot of the ATX state for API responses
|
|
||||||
pub async fn state(&self) -> AtxState {
|
pub async fn state(&self) -> AtxState {
|
||||||
let inner = self.inner.read().await;
|
let inner = self.inner.read().await;
|
||||||
|
|
||||||
let power_status = if let Some(sensor) = &inner.led_sensor {
|
let power_status = Self::read_power_status(inner.led_sensor.as_ref()).await;
|
||||||
match sensor.read().await {
|
|
||||||
Ok(status) => status,
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to read ATX LED sensor: {}", e);
|
|
||||||
PowerStatus::Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PowerStatus::Unknown
|
|
||||||
};
|
|
||||||
|
|
||||||
AtxState {
|
AtxState {
|
||||||
available: inner.config.enabled,
|
available: inner.config.enabled,
|
||||||
@@ -262,3 +280,49 @@ impl AtxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::atx::AtxDriverType;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_share_serial_device_true() {
|
||||||
|
let power = AtxKeyConfig {
|
||||||
|
driver: AtxDriverType::Serial,
|
||||||
|
device: "/dev/ttyUSB0".to_string(),
|
||||||
|
pin: 1,
|
||||||
|
active_level: super::super::types::ActiveLevel::High,
|
||||||
|
baud_rate: 9600,
|
||||||
|
};
|
||||||
|
let reset = AtxKeyConfig {
|
||||||
|
driver: AtxDriverType::Serial,
|
||||||
|
device: "/dev/ttyUSB0".to_string(),
|
||||||
|
pin: 2,
|
||||||
|
active_level: super::super::types::ActiveLevel::High,
|
||||||
|
baud_rate: 9600,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(AtxController::should_share_serial_device(&power, &reset));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_share_serial_device_false_on_different_baud() {
|
||||||
|
let power = AtxKeyConfig {
|
||||||
|
driver: AtxDriverType::Serial,
|
||||||
|
device: "/dev/ttyUSB0".to_string(),
|
||||||
|
pin: 1,
|
||||||
|
active_level: super::super::types::ActiveLevel::High,
|
||||||
|
baud_rate: 9600,
|
||||||
|
};
|
||||||
|
let reset = AtxKeyConfig {
|
||||||
|
driver: AtxDriverType::Serial,
|
||||||
|
device: "/dev/ttyUSB0".to_string(),
|
||||||
|
pin: 2,
|
||||||
|
active_level: super::super::types::ActiveLevel::High,
|
||||||
|
baud_rate: 115200,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!AtxController::should_share_serial_device(&power, &reset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user