mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-21 23:13:21 +08:00
Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ef5011760 | ||
|
|
d71112380c | ||
|
|
491bfbecb5 | ||
|
|
8a6cc98806 | ||
|
|
728373b067 | ||
|
|
c62ff2da87 | ||
|
|
572bf859ba | ||
|
|
aef136241a | ||
|
|
78af9f67de | ||
|
|
9f34b4109b | ||
|
|
1967d22dff | ||
|
|
0cba8ab127 | ||
|
|
d002226acb | ||
|
|
1db572e020 | ||
|
|
5de5ee42c2 | ||
|
|
2c19208094 | ||
|
|
3014edffbb | ||
|
|
ece4f303ce | ||
|
|
db9d79554a | ||
|
|
620fe0be54 | ||
|
|
79a4dcf2b0 | ||
|
|
a8ee9552a0 | ||
|
|
3749bafae0 | ||
|
|
b573f478fc | ||
|
|
d67a3ab7f8 | ||
|
|
dbe5672df4 | ||
|
|
48fd64319e | ||
|
|
ccf821ab22 | ||
|
|
b074aa5779 | ||
|
|
95a1fdf42d | ||
|
|
e678ec394d | ||
|
|
628909a192 | ||
|
|
5cab585115 | ||
|
|
dcfa3eadaf | ||
|
|
1647e70243 | ||
|
|
9c6d8a614c | ||
|
|
db7a845d3b | ||
|
|
1d48d45592 | ||
|
|
d613739b06 | ||
|
|
6e1194fe4c | ||
|
|
f101ddb918 | ||
|
|
0054100414 | ||
|
|
486f3887c2 | ||
|
|
37af369b43 | ||
|
|
827d24fde4 | ||
|
|
b8ea38b5f3 | ||
|
|
4530d8663d | ||
|
|
a4073d64cb | ||
|
|
16400df182 | ||
|
|
47af17bebc | ||
|
|
814f23a27c | ||
|
|
6bbe137f09 | ||
|
|
6fdcf5c7c9 | ||
|
|
ce1712ff2e | ||
|
|
8b09824e04 | ||
|
|
9fb23476ac | ||
|
|
971c263bf8 | ||
|
|
cb3fd3e646 | ||
|
|
d414557d74 | ||
|
|
fbcd95b617 | ||
|
|
887f29096f | ||
|
|
e0bddc2faa | ||
|
|
4f770a1eea | ||
|
|
376dc97134 | ||
|
|
27c8da9a75 | ||
|
|
5963dfa01a | ||
|
|
f86cba6ce5 | ||
|
|
e1b82519b9 | ||
|
|
32d8a62fec | ||
|
|
4cd622ff07 | ||
|
|
6ec4361f43 | ||
|
|
e9bed3688f | ||
|
|
8d5366444b | ||
|
|
4e32b05124 | ||
|
|
e0055bf491 | ||
|
|
d60ddb21dc | ||
|
|
219d1ccd08 | ||
|
|
6f697e56c8 | ||
|
|
05535b9bb2 | ||
|
|
5ce2cb658c | ||
|
|
a86df52d9a | ||
|
|
275c2ca343 | ||
|
|
5d871e8b21 | ||
|
|
694afdd07f | ||
|
|
d31e70515a | ||
|
|
701b141347 | ||
|
|
eaa114f40c | ||
|
|
563c808836 | ||
|
|
79b6683817 | ||
|
|
8bd16df6f8 | ||
|
|
fdea52d59d | ||
|
|
4031abd8d6 | ||
|
|
cf2bfaf6a7 | ||
|
|
d35021ee5d | ||
|
|
57d1597264 | ||
|
|
aca847e76c | ||
|
|
8036c164b4 | ||
|
|
0482987bf1 | ||
|
|
50dd419e5a | ||
|
|
213359b6c8 | ||
|
|
63c2bb4ca2 | ||
|
|
6693020e6d | ||
|
|
05c5f45995 | ||
|
|
1ded7a8a66 | ||
|
|
809d0ca65c | ||
|
|
139064de35 | ||
|
|
54e0cbc84e | ||
|
|
7c9166a8cf | ||
|
|
a9a16f2218 | ||
|
|
c9ae9dd893 | ||
|
|
010d50b727 | ||
|
|
c88fb03243 | ||
|
|
7bae9b633c | ||
|
|
fc3f1d6aac | ||
|
|
b719be3e38 | ||
|
|
db2410cc7f | ||
|
|
d9c2854911 | ||
|
|
99c051a874 | ||
|
|
a0ad151fbc | ||
|
|
4806a5e0a9 | ||
|
|
e60152d38b | ||
|
|
f6e97a06f5 | ||
|
|
618599266e | ||
|
|
696c1b4bb2 | ||
|
|
b346af35d3 | ||
|
|
16a65289f2 | ||
|
|
bb2691fe8f | ||
|
|
7753c83e27 | ||
|
|
1d6fb2e3c5 | ||
|
|
6c1bff5d0c | ||
|
|
f1e362a820 | ||
|
|
e2c19d550c | ||
|
|
c101ef1c80 | ||
|
|
4b7be20fe0 | ||
|
|
5c98aea7e3 | ||
|
|
da61644dbc | ||
|
|
5de7ecd4c5 | ||
|
|
3fc6b055a0 | ||
|
|
4b65eebd5d | ||
|
|
921c00c472 | ||
|
|
b6bd76534f | ||
|
|
780cfb4cca | ||
|
|
8f9272d985 | ||
|
|
f679647ec9 | ||
|
|
198552014b | ||
|
|
3e3937605e | ||
|
|
4a85fbfab8 | ||
|
|
87d1110a87 | ||
|
|
b31aae284d | ||
|
|
dc6475776e | ||
|
|
3de72677e6 | ||
|
|
6a1616c32a | ||
|
|
032f47a891 |
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -34,13 +34,11 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: web/package-lock.json
|
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
working-directory: web
|
working-directory: web
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Upload frontend dist
|
- name: Upload frontend dist
|
||||||
@@ -66,8 +64,16 @@ jobs:
|
|||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
# The Docker workflow builds the same targets with the same Cross images.
|
||||||
|
shared-key: linux-cross-release
|
||||||
|
key: ${{ hashFiles('Cross.toml', 'build/cross/Dockerfile.*') }}
|
||||||
|
cache-all-crates: true
|
||||||
|
|
||||||
- name: Install cross
|
- name: Install cross
|
||||||
run: cargo install cross --locked
|
run: cargo install cross --version 0.2.5 --locked
|
||||||
|
|
||||||
- name: Build linux binary
|
- name: Build linux binary
|
||||||
run: bash build/build-images.sh
|
run: bash build/build-images.sh
|
||||||
@@ -127,6 +133,12 @@ jobs:
|
|||||||
"TURBOJPEG_LIB_DIR=$env:TURBOJPEG_LIB_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
"TURBOJPEG_LIB_DIR=$env:TURBOJPEG_LIB_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
"TURBOJPEG_INCLUDE_DIR=$env:TURBOJPEG_INCLUDE_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
"TURBOJPEG_INCLUDE_DIR=$env:TURBOJPEG_INCLUDE_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
shared-key: windows-msvc-release
|
||||||
|
key: ${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }}
|
||||||
|
|
||||||
- name: Build Windows exe
|
- name: Build Windows exe
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: .\build\windows\build.ps1 -Configuration release -Package
|
run: .\build\windows\build.ps1 -Configuration release -Package
|
||||||
|
|||||||
91
.github/workflows/docker.yml
vendored
Normal file
91
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: Docker image tag
|
||||||
|
required: false
|
||||||
|
default: latest
|
||||||
|
platforms:
|
||||||
|
description: Docker platforms
|
||||||
|
required: false
|
||||||
|
default: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
DOCKER_PLATFORMS: ${{ github.event_name == 'workflow_dispatch' && inputs.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
||||||
|
DOCKER_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 360
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
# Reuse dependency artifacts produced by the deb job and vice versa.
|
||||||
|
shared-key: linux-cross-release
|
||||||
|
key: ${{ hashFiles('Cross.toml', 'build/cross/Dockerfile.*') }}
|
||||||
|
cache-all-crates: true
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y unzip xz-utils
|
||||||
|
cargo install cross --version 0.2.5 --locked
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
working-directory: web
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build linux binaries
|
||||||
|
run: bash build/build-images.sh
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: docker.io
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to Aliyun Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: registry.cn-hangzhou.aliyuncs.com
|
||||||
|
username: ${{ secrets.ALIYUN_USERNAME }}
|
||||||
|
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Publish Docker images
|
||||||
|
run: |
|
||||||
|
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry docker.io/silentwind0 --tag "$DOCKER_TAG" --push
|
||||||
|
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry registry.cn-hangzhou.aliyuncs.com/silentwind --tag "$DOCKER_TAG" --push
|
||||||
|
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry docker.io/silentwind0 --variant full --tag "$DOCKER_TAG" --push
|
||||||
|
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry registry.cn-hangzhou.aliyuncs.com/silentwind --variant full --tag "$DOCKER_TAG" --push
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
# Rust
|
# Rust
|
||||||
/target/
|
target/
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
@@ -31,6 +31,7 @@ Thumbs.db
|
|||||||
# Build artifacts
|
# Build artifacts
|
||||||
/dist/
|
/dist/
|
||||||
/build-staging
|
/build-staging
|
||||||
|
/.tmp/
|
||||||
|
|
||||||
# Frontend (built files)
|
# Frontend (built files)
|
||||||
/web/node_modules/
|
/web/node_modules/
|
||||||
|
|||||||
193
Cargo.toml
193
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "one-kvm"
|
name = "one-kvm"
|
||||||
version = "0.2.1"
|
version = "0.2.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["SilentWind"]
|
authors = ["SilentWind"]
|
||||||
description = "A open and lightweight IP-KVM solution written in Rust"
|
description = "A open and lightweight IP-KVM solution written in Rust"
|
||||||
@@ -9,119 +9,193 @@ 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:toml_edit",
|
||||||
|
"dep:tracing",
|
||||||
|
"dep:tracing-subscriber",
|
||||||
|
"dep:thiserror",
|
||||||
|
"dep:anyhow",
|
||||||
|
"dep:argon2",
|
||||||
|
"dep:rand",
|
||||||
|
"dep:totp-rs",
|
||||||
|
"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:tempfile",
|
||||||
|
"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:libloading",
|
||||||
|
"dep:ventoy-img",
|
||||||
|
"dep:protobuf",
|
||||||
|
"dep:sodiumoxide",
|
||||||
|
"dep:des",
|
||||||
|
"dep:sha2",
|
||||||
|
"dep:typeshare",
|
||||||
|
"dep:hwcodec",
|
||||||
|
"dep:libyuv",
|
||||||
|
"dep:turbojpeg",
|
||||||
|
"dep:audiopus",
|
||||||
|
"dep:v4l2r",
|
||||||
|
"dep:alsa",
|
||||||
|
"dep:gpio-cdev",
|
||||||
|
"dep:one-kvm-bluetooth-hid",
|
||||||
|
"dep:cpal",
|
||||||
|
"dep:windows-sys",
|
||||||
|
]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"], optional = true }
|
||||||
tokio-util = { version = "0.7", features = ["rt"] }
|
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 = ["cookie"] }
|
axum-extra = { version = "0.12", features = ["cookie"], optional = true }
|
||||||
tower-http = { version = "0.6", features = ["cors", "trace", "set-header"] }
|
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.9", default-features = false, features = ["runtime-tokio", "sqlite-bundled"], 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 }
|
||||||
|
toml_edit = { version = "0.25", 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-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.10", optional = true }
|
||||||
|
totp-rs = { version = "5.7", features = ["gen_secret", "otpauth", "zeroize"], optional = true }
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"], optional = true }
|
||||||
base64 = "0.22"
|
base64 = { version = "0.22", optional = true }
|
||||||
nix = { version = "0.30", features = ["fs", "net", "hostname", "poll"] }
|
tempfile = { version = "3", optional = true }
|
||||||
|
|
||||||
# HTTP client (for URL downloads)
|
# HTTP client (for URL downloads)
|
||||||
# Use rustls by default, but allow native-tls for systems with older GLIBC
|
# Use rustls by default, but allow native-tls for systems with older GLIBC
|
||||||
reqwest = { version = "0.13", features = ["stream", "rustls", "json"], default-features = false }
|
reqwest = { version = "0.13", features = ["stream", "rustls-no-provider", "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", "debug-embed"] }
|
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", default-features = false, features = ["ring", "std", "tls12"], 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-no-provider"], optional = true }
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"], optional = true }
|
||||||
|
|
||||||
# Time (cookie max_age + RFC3339 timestamps)
|
# Time (cookie max_age + RFC3339 timestamps)
|
||||||
time = { version = "0.3", features = ["serde", "formatting", "parsing"] }
|
time = { version = "0.3", features = ["serde", "formatting", "parsing"], optional = true }
|
||||||
|
|
||||||
# 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.29", 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.17", optional = true }
|
||||||
rtp = "0.14"
|
rtp = { version = "0.17", optional = true }
|
||||||
rtsp-types = "0.1"
|
rtsp-types = { version = "0.1", optional = true }
|
||||||
sdp-types = "0.1"
|
sdp-types = { version = "0.1", optional = true }
|
||||||
|
|
||||||
# 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 }
|
||||||
|
libloading = { version = "0.8", 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 }
|
||||||
|
|
||||||
# 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"
|
des = { version = "0.9", optional = true }
|
||||||
|
sha2 = { version = "0.11", optional = true }
|
||||||
# TypeScript type generation
|
# TypeScript type generation
|
||||||
typeshare = "1.0"
|
typeshare = { version = "1.0", optional = true }
|
||||||
|
|
||||||
[target.'cfg(any(unix, windows))'.dependencies]
|
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
||||||
# Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux)
|
# Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux)
|
||||||
hwcodec = { path = "libs/hwcodec" }
|
hwcodec = { path = "libs/hwcodec", features = ["bytes"], optional = true }
|
||||||
libyuv = { path = "res/vcpkg/libyuv" }
|
libyuv = { path = "res/vcpkg/libyuv", optional = true }
|
||||||
turbojpeg = "1.3"
|
turbojpeg = { version = "1.3", optional = true }
|
||||||
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
||||||
audiopus = "0.2"
|
audiopus = { version = "0.2", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
one-kvm-bluetooth-hid = { path = "libs/bluetooth-hid", optional = true }
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
nix = { version = "0.31", default-features = false, features = ["fs", "socket", "net", "hostname", "poll"], optional = true }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
|
||||||
# Video capture (V4L2)
|
# Video capture (V4L2)
|
||||||
v4l2r = "0.0.7"
|
v4l2r = { path = "libs/v4l2r", optional = true }
|
||||||
|
|
||||||
# Audio (ALSA capture)
|
# Audio (ALSA capture)
|
||||||
alsa = "0.11"
|
alsa = { version = "0.11", optional = true }
|
||||||
|
|
||||||
# ATX (GPIO control)
|
# ATX (GPIO control)
|
||||||
gpio-cdev = "0.6"
|
gpio-cdev = { version = "0.6", optional = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
cpal = { version = "0.17", default-features = false }
|
cpal = { version = "0.18", default-features = false, optional = true }
|
||||||
windows-sys = { version = "0.61", features = [
|
windows-sys = { version = "0.61", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_NetworkManagement_IpHelper",
|
"Win32_NetworkManagement_IpHelper",
|
||||||
@@ -129,10 +203,7 @@ windows-sys = { version = "0.61", features = [
|
|||||||
"Win32_Networking_WinSock",
|
"Win32_Networking_WinSock",
|
||||||
"Win32_System_SystemInformation",
|
"Win32_System_SystemInformation",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
] }
|
], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
protobuf-codegen = "3.7"
|
protobuf-codegen = "3.7"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Cross-compilation configuration for One-KVM
|
# Cross-compilation configuration for One-KVM
|
||||||
# Uses pre-built custom Debian 11 based images for consistent build/runtime environment
|
# Uses pre-built custom Debian 12 based images for consistent build/runtime environment
|
||||||
# See: https://github.com/cross-rs/cross
|
# See: https://github.com/cross-rs/cross
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
|
|||||||
27
README.en.md
27
README.en.md
@@ -22,6 +22,8 @@ Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
|||||||
- **Lightweight**: shipped as a binary with minimal moving parts for deployment.
|
- **Lightweight**: shipped as a binary with minimal moving parts for deployment.
|
||||||
- **Easy to use**: no hand-edited config files required; settings are done in the web UI.
|
- **Easy to use**: no hand-edited config files required; settings are done in the web UI.
|
||||||
|
|
||||||
|
For more information, see the [One-KVM Rust documentation](https://docs.one-kvm.cn/).
|
||||||
|
|
||||||
> **One-KVM (Python)** is no longer maintained. If you still need it, see <https://github.com/mofeng-git/One-KVM/tree/python>.
|
> **One-KVM (Python)** is no longer maintained. If you still need it, see <https://github.com/mofeng-git/One-KVM/tree/python>.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -32,7 +34,7 @@ Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Core
|
**Core**
|
||||||
|
|
||||||
| Area | Capabilities |
|
| Area | Capabilities |
|
||||||
|------|----------------|
|
|------|----------------|
|
||||||
@@ -40,11 +42,9 @@ Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
|||||||
| Video encoding | VAAPI / QSV / RKMPP / V4L2 M2M hardware paths, with software fallback |
|
| Video encoding | VAAPI / QSV / RKMPP / V4L2 M2M hardware paths, with software fallback |
|
||||||
| Keyboard & mouse | USB OTG HID or CH340 + CH9329 HID; absolute / relative mouse |
|
| Keyboard & mouse | USB OTG HID or CH340 + CH9329 HID; absolute / relative mouse |
|
||||||
| Virtual media | USB mass storage; ISO/IMG mount and Ventoy-style virtual USB |
|
| Virtual media | USB mass storage; ISO/IMG mount and Ventoy-style virtual USB |
|
||||||
| ATX power | GPIO or USB relay; power and reset control |
|
| ATX power | GPIO or USB relay; power control |
|
||||||
| Audio | ALSA capture + Opus (HTTP / WebRTC) |
|
| Audio | ALSA capture + Opus (HTTP / WebRTC) |
|
||||||
|
|
||||||
The web UI supports visual configuration and Chinese/English locales. Built-ins include a web terminal (ttyd), intranet tunnel (gostc), P2P (EasyTier), RustDesk protocol (optional cross-platform remote access), and RTSP streaming.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Release artifacts are on [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases). Below are short paths for common setups. For **system requirements, hardware, Docker env vars, USB OTG**, and full troubleshooting, see the [One-KVM documentation](https://docs.one-kvm.cn/) (Chinese; use a translator if needed).
|
Release artifacts are on [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases). Below are short paths for common setups. For **system requirements, hardware, Docker env vars, USB OTG**, and full troubleshooting, see the [One-KVM documentation](https://docs.one-kvm.cn/) (Chinese; use a translator if needed).
|
||||||
@@ -216,15 +216,22 @@ One-KVM builds on many great open-source projects; a lot of time goes into testi
|
|||||||
|
|
||||||
- 爱发电用户_d3d9c
|
- 爱发电用户_d3d9c
|
||||||
|
|
||||||
|
- 爱发电用户_97b41
|
||||||
|
|
||||||
|
- 偶然
|
||||||
|
|
||||||
|
- 爱发电用户_dba45
|
||||||
|
|
||||||
|
- 爱发电用户_d4f8b
|
||||||
|
|
||||||
|
- 故人。
|
||||||
|
|
||||||
- ......
|
- ......
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Sponsors
|
### Sponsors
|
||||||
|
|
||||||
**Mirror Download Services:**
|
|
||||||
- **[Chongqing University Open Source Software Mirror](https://mirrors.cqu.edu.cn/)** — provides mirror download services
|
|
||||||
|
|
||||||
**File hosting**
|
**File hosting**
|
||||||
|
|
||||||
- **[Huang1111 public-interest program](https://pan.huang1111.cn/s/mxkx3T1)** — login-free downloads
|
- **[Huang1111 public-interest program](https://pan.huang1111.cn/s/mxkx3T1)** — login-free downloads
|
||||||
@@ -236,3 +243,9 @@ One-KVM builds on many great open-source projects; a lot of time goes into testi
|
|||||||

|

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

|
||||||
|
|
||||||
|
Remote computers, consumer GPU servers, and dedicated physical machines with fully automated online delivery.
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 项目概述
|
## 项目概述
|
||||||
|
|
||||||
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
|
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
- **轻量**:以二进制文件形式分发,无繁杂的依赖项,部署过程简单。
|
- **轻量**:以二进制文件形式分发,无繁杂的依赖项,部署过程简单。
|
||||||
- **易用**:无需手动编辑配置文件,参数设置均可通过网页界面完成。
|
- **易用**:无需手动编辑配置文件,参数设置均可通过网页界面完成。
|
||||||
|
|
||||||
|
更多内容可访问 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
||||||
|
|
||||||
> **One-KVM Python** 已停止开发,如有需要可访问 <https://github.com/mofeng-git/One-KVM/tree/python>。
|
> **One-KVM Python** 已停止开发,如有需要可访问 <https://github.com/mofeng-git/One-KVM/tree/python>。
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -30,9 +32,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 📊 功能介绍
|
## 功能介绍
|
||||||
|
|
||||||
### 核心功能
|
**核心功能**
|
||||||
|
|
||||||
| 功能 | 能力说明 |
|
| 功能 | 能力说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -40,12 +42,10 @@
|
|||||||
| 视频编码 | VAAPI/QSV/RKMPP/V4L2M2M 硬件编码支持,以及软件编码兜底 |
|
| 视频编码 | VAAPI/QSV/RKMPP/V4L2M2M 硬件编码支持,以及软件编码兜底 |
|
||||||
| 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID,支持绝对/相对鼠标模式 |
|
| 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID,支持绝对/相对鼠标模式 |
|
||||||
| 虚拟媒体 | USB Mass Storage,支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
|
| 虚拟媒体 | USB Mass Storage,支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
|
||||||
| ATX 电源控制 | GPIO /USB 继电器,支持控制电源、重启按钮 |
|
| ATX 电源控制 | GPIO/USB 继电器,支持电源控制 |
|
||||||
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
|
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
|
||||||
|
|
||||||
此外提供基于 Web UI 的可视化配置与中英文界面;并集成 Web 终端(ttyd)、内网穿透(gostc)、P2P 组网(EasyTier)、RustDesk 协议(扩展跨平台远程访问)以及 RTSP 推流等能力。
|
## 安装使用
|
||||||
|
|
||||||
## ⚡ 安装使用
|
|
||||||
|
|
||||||
构建产物见 [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases)。以下为常见安装方式的简要步骤;**系统要求、硬件准备、Docker 环境变量与 USB OTG 等完整说明**请查阅 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
构建产物见 [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases)。以下为常见安装方式的简要步骤;**系统要求、硬件准备、Docker 环境变量与 USB OTG 等完整说明**请查阅 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
||||||
|
|
||||||
@@ -210,6 +210,16 @@ One-KVM 已上架飞牛 **应用市场**,在 NAS 上直接搜索安装即可
|
|||||||
|
|
||||||
- 爱发电用户_d3d9c
|
- 爱发电用户_d3d9c
|
||||||
|
|
||||||
|
- 爱发电用户_97b41
|
||||||
|
|
||||||
|
- 偶然
|
||||||
|
|
||||||
|
- 爱发电用户_dba45
|
||||||
|
|
||||||
|
- 爱发电用户_d4f8b
|
||||||
|
|
||||||
|
- 故人。
|
||||||
|
|
||||||
- ......
|
- ......
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -218,9 +228,6 @@ One-KVM 已上架飞牛 **应用市场**,在 NAS 上直接搜索安装即可
|
|||||||
|
|
||||||
本项目得到以下赞助商的支持:
|
本项目得到以下赞助商的支持:
|
||||||
|
|
||||||
**镜像下载服务:**
|
|
||||||
- **[重庆大学开源软件镜像站](https://mirrors.cqu.edu.cn/)** - 提供镜像站下载服务
|
|
||||||
|
|
||||||
**文件存储服务:**
|
**文件存储服务:**
|
||||||
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
|
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
|
||||||
|
|
||||||
|
|||||||
14
agents.md
14
agents.md
@@ -1,14 +0,0 @@
|
|||||||
# Agents Notes
|
|
||||||
|
|
||||||
## Windows MSVC Build
|
|
||||||
|
|
||||||
Run from the repository root in PowerShell:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:VCPKG_ROOT='C:\Users\mofen\code\vcpkg'
|
|
||||||
$env:TURBOJPEG_SOURCE='explicit'
|
|
||||||
$env:TURBOJPEG_LIB_DIR='C:\Users\mofen\code\vcpkg\installed\x64-windows-static\lib'
|
|
||||||
$env:TURBOJPEG_INCLUDE_DIR='C:\Users\mofen\code\vcpkg\installed\x64-windows-static\include'
|
|
||||||
|
|
||||||
cargo build --target x86_64-pc-windows-msvc
|
|
||||||
```
|
|
||||||
31
build.rs
31
build.rs
@@ -2,21 +2,6 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Set BUILD_DATE environment variable for compile-time access
|
|
||||||
// Use system time to avoid adding chrono as a build dependency
|
|
||||||
let now = std::time::SystemTime::now();
|
|
||||||
let duration = now.duration_since(std::time::UNIX_EPOCH).unwrap();
|
|
||||||
let secs = duration.as_secs();
|
|
||||||
|
|
||||||
// Convert Unix timestamp to date (simplified calculation)
|
|
||||||
// Days since epoch
|
|
||||||
let days = secs / 86400;
|
|
||||||
// Calculate year, month, day from days since 1970-01-01
|
|
||||||
let (year, month, day) = days_to_ymd(days as i64);
|
|
||||||
let build_date = format!("{:04}-{:02}-{:02}", year, month, day);
|
|
||||||
|
|
||||||
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
|
|
||||||
|
|
||||||
// Compile protobuf files for RustDesk protocol
|
// Compile protobuf files for RustDesk protocol
|
||||||
compile_protos();
|
compile_protos();
|
||||||
|
|
||||||
@@ -86,19 +71,3 @@ pub mod rustdesk {
|
|||||||
|
|
||||||
fs::write(&dest_path, code).expect("Failed to write secrets_generated.rs");
|
fs::write(&dest_path, code).expect("Failed to write secrets_generated.rs");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert days since Unix epoch to year-month-day
|
|
||||||
fn days_to_ymd(days: i64) -> (i32, u32, u32) {
|
|
||||||
// Algorithm from http://howardhinnant.github.io/date_algorithms.html
|
|
||||||
let z = days + 719468;
|
|
||||||
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
|
||||||
let doe = (z - era * 146097) as u32;
|
|
||||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
||||||
let y = yoe as i64 + era * 400;
|
|
||||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
||||||
let mp = (5 * doy + 2) / 153;
|
|
||||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
|
||||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
||||||
let year = if m <= 2 { y + 1 } else { y };
|
|
||||||
(year as i32, m, d)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
# One-KVM Runtime Image
|
# One-KVM Runtime Image
|
||||||
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
||||||
# Used after cross-compiling with `cross build`
|
# Used after cross-compiling with `cross build`
|
||||||
# Using Debian 11 for maximum compatibility (GLIBC 2.31)
|
# Using Debian 12 to match the build images (GLIBC 2.36)
|
||||||
|
|
||||||
ARG TARGETPLATFORM=linux/amd64
|
ARG TARGETPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM debian:11-slim
|
FROM debian:12-slim
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
# Install runtime dependencies in a single layer
|
# Install runtime dependencies in a single layer
|
||||||
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
||||||
# Only hardware acceleration drivers and core system libraries remain dynamic
|
# Only hardware acceleration drivers and core system libraries remain dynamic
|
||||||
RUN sed -i 's/ main$/ main contrib non-free/' /etc/apt/sources.list && \
|
RUN sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
# Core runtime (all platforms) - no codec libs needed
|
# Core runtime (all platforms) - no codec libs needed
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
# Bluetooth HID uses the host BlueZ system bus; btmgmt manages a dedicated adapter.
|
||||||
|
bluez \
|
||||||
libudev1 \
|
libudev1 \
|
||||||
libasound2 \
|
libasound2 \
|
||||||
|
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
|
||||||
|
network-manager \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping \
|
||||||
# v4l2 is handled by kernel, minimal userspace needed
|
# v4l2 is handled by kernel, minimal userspace needed
|
||||||
libv4l-0 \
|
libv4l-0 \
|
||||||
&& \
|
&& \
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
# One-KVM Runtime Image (full)
|
# One-KVM Runtime Image (full)
|
||||||
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
||||||
# Used after cross-compiling with `cross build`
|
# Used after cross-compiling with `cross build`
|
||||||
# Using Debian 11 for maximum compatibility (GLIBC 2.31)
|
# Using Debian 12 to match the build images (GLIBC 2.36)
|
||||||
|
|
||||||
ARG TARGETPLATFORM=linux/amd64
|
ARG TARGETPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM debian:11-slim
|
FROM debian:12-slim
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
# Install runtime dependencies in a single layer
|
# Install runtime dependencies in a single layer
|
||||||
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
||||||
# Only hardware acceleration drivers and core system libraries remain dynamic
|
# Only hardware acceleration drivers and core system libraries remain dynamic
|
||||||
RUN sed -i 's/ main$/ main contrib non-free/' /etc/apt/sources.list && \
|
RUN sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
# Core runtime (all platforms) - no codec libs needed
|
# Core runtime (all platforms) - no codec libs needed
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
# Bluetooth HID uses the host BlueZ system bus; btmgmt manages a dedicated adapter.
|
||||||
|
bluez \
|
||||||
libudev1 \
|
libudev1 \
|
||||||
libasound2 \
|
libasound2 \
|
||||||
|
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
|
||||||
|
network-manager \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping \
|
||||||
# v4l2 is handled by kernel, minimal userspace needed
|
# v4l2 is handled by kernel, minimal userspace needed
|
||||||
libv4l-0 \
|
libv4l-0 \
|
||||||
&& \
|
&& \
|
||||||
|
|||||||
@@ -21,16 +21,21 @@ build_arch() {
|
|||||||
|
|
||||||
case "${CHINAMIRRO:-}" in
|
case "${CHINAMIRRO:-}" in
|
||||||
1|true|TRUE|yes|YES|on|ON)
|
1|true|TRUE|yes|YES|on|ON)
|
||||||
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--build-arg CHINAMIRRO=1"
|
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--progress=plain --build-arg CHINAMIRRO=1 --build-arg GH_PROXY=${GH_PROXY:-https://gh-proxy.com/} --build-arg DEBIAN_IMAGE=${DEBIAN_IMAGE:-docker.1ms.run/library/debian:12}"
|
||||||
echo "=== China mirror acceleration: enabled (Tsinghua) ==="
|
cross_build_opts="$cross_build_opts --build-arg HTTP_PROXY= --build-arg HTTPS_PROXY= --build-arg ALL_PROXY= --build-arg NO_PROXY="
|
||||||
|
cross_build_opts="$cross_build_opts --build-arg http_proxy= --build-arg https_proxy= --build-arg all_proxy= --build-arg no_proxy="
|
||||||
|
echo "=== China mirror acceleration: enabled ==="
|
||||||
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
||||||
env \
|
env \
|
||||||
CROSS_BUILD_OPTS="$cross_build_opts" \
|
CROSS_BUILD_OPTS="$cross_build_opts" \
|
||||||
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=tuna \
|
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=rsproxy-sparse \
|
||||||
CARGO_SOURCE_TUNA_REGISTRY=sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/ \
|
CARGO_SOURCE_RSPROXY_REGISTRY=https://rsproxy.cn/crates.io-index \
|
||||||
|
CARGO_SOURCE_RSPROXY_SPARSE_REGISTRY=sparse+https://rsproxy.cn/index/ \
|
||||||
|
CARGO_REGISTRIES_RSPROXY_INDEX=https://rsproxy.cn/crates.io-index \
|
||||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup \
|
CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||||
RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup \
|
RUSTUP_DIST_SERVER=https://rsproxy.cn \
|
||||||
|
RUSTUP_UPDATE_ROOT=https://rsproxy.cn/rustup \
|
||||||
cross build --release --target "$rust_target"
|
cross build --release --target "$rust_target"
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
@@ -66,7 +71,7 @@ case "${1:-all}" in
|
|||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 # Build all"
|
echo " $0 # Build all"
|
||||||
echo " $0 x86_64 # Build x86_64 only"
|
echo " $0 x86_64 # Build x86_64 only"
|
||||||
echo " CHINAMIRRO=1 $0 arm64 # Build with Tsinghua mirrors"
|
echo " CHINAMIRRO=1 $0 arm64 # Build with China mirrors"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -83,16 +88,4 @@ for target in "${ARCH_MAP[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
echo "Static libraries:"
|
|
||||||
for target in "${ARCH_MAP[@]}"; do
|
|
||||||
case "$target" in
|
|
||||||
x86_64-unknown-linux-gnu) gnu_target="x86_64-linux-gnu" ;;
|
|
||||||
aarch64-unknown-linux-gnu) gnu_target="aarch64-linux-gnu" ;;
|
|
||||||
armv7-unknown-linux-gnueabihf) gnu_target="armv7-linux-gnueabihf" ;;
|
|
||||||
esac
|
|
||||||
if [ -d "$PROJECT_DIR/target/one-kvm-libs/$gnu_target/lib" ]; then
|
|
||||||
echo " $gnu_target: OK"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
echo "Next step: ./build/package-docker.sh or ./build/package-deb.sh"
|
echo "Next step: ./build/package-docker.sh or ./build/package-deb.sh"
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
# Cross-compilation image for ARM64 based on Debian 11
|
# Cross-compilation image for ARM64 based on Debian 12
|
||||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||||
|
|
||||||
FROM debian:11
|
ARG DEBIAN_IMAGE=debian:12
|
||||||
|
FROM ${DEBIAN_IMAGE}
|
||||||
|
|
||||||
# Linux headers used by v4l2r bindgen
|
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
|
||||||
ARG LINUX_HEADERS_SHA256=
|
|
||||||
ARG CHINAMIRRO=0
|
ARG CHINAMIRRO=0
|
||||||
|
ARG GH_PROXY=https://gh-proxy.com/
|
||||||
|
ARG RUSTUP_DIST_SERVER_CN=https://rsproxy.cn
|
||||||
|
ARG RUSTUP_UPDATE_ROOT_CN=https://rsproxy.cn/rustup
|
||||||
|
ARG CARGO_INDEX_CN=https://rsproxy.cn/crates.io-index
|
||||||
|
ARG CARGO_REGISTRY_CN=sparse+https://rsproxy.cn/index/
|
||||||
|
ARG LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||||
|
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||||
|
ARG LIBVPX_VERSION=1.16.0
|
||||||
|
ARG X265_VERSION=3.4
|
||||||
|
ARG OPUS_VERSION=1.5.2
|
||||||
|
ARG RKMPP_BRANCH=jellyfin-mpp
|
||||||
|
ARG RKRGA_BRANCH=jellyfin-rga
|
||||||
|
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||||
|
|
||||||
# Optionally use Tsinghua mirrors for builds in China.
|
# Optionally use China mirrors for builds in China.
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
sed -i \
|
sed -i -E \
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
/etc/apt/sources.list.d/debian.sources; \
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
@@ -21,19 +31,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
mkdir -p /root/.cargo; \
|
mkdir -p /root/.cargo; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[source.crates-io]' \
|
'[source.crates-io]' \
|
||||||
"replace-with = 'tuna'" \
|
"replace-with = 'rsproxy-sparse'" \
|
||||||
'[source.tuna]' \
|
'[source.rsproxy]' \
|
||||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_INDEX_CN}'" \
|
||||||
'[registries.tuna]' \
|
'[source.rsproxy-sparse]' \
|
||||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||||
|
'[registries.rsproxy]' \
|
||||||
|
"index = '${CARGO_INDEX_CN}'" \
|
||||||
|
'[net]' \
|
||||||
|
'git-fetch-with-cli = true' \
|
||||||
> /root/.cargo/config.toml; \
|
> /root/.cargo/config.toml; \
|
||||||
fi \
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -69,6 +83,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
# Install ARM64 development libraries (without VAAPI/X11 for ARM)
|
# Install ARM64 development libraries (without VAAPI/X11 for ARM)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libssl3 \
|
||||||
|
libssl3:arm64 \
|
||||||
libasound2-dev:arm64 \
|
libasound2-dev:arm64 \
|
||||||
libv4l-dev:arm64 \
|
libv4l-dev:arm64 \
|
||||||
libudev-dev:arm64 \
|
libudev-dev:arm64 \
|
||||||
@@ -78,23 +94,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libdrm-dev:arm64 \
|
libdrm-dev:arm64 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install newer V4L2 headers for v4l2r bindgen
|
|
||||||
RUN mkdir -p /opt/v4l2-headers \
|
|
||||||
&& wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \
|
|
||||||
&& if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \
|
|
||||||
&& tar -xf /tmp/linux-headers.tar.xz -C /tmp \
|
|
||||||
&& cd /tmp/linux-${LINUX_HEADERS_VERSION} \
|
|
||||||
&& make ARCH=arm64 headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \
|
|
||||||
&& rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz
|
|
||||||
|
|
||||||
ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include
|
|
||||||
|
|
||||||
# Build static libjpeg-turbo from source (cross-compile for ARM64)
|
# Build static libjpeg-turbo from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libjpeg-turbo \
|
||||||
&& cd /tmp/libjpeg-turbo \
|
&& cd /tmp/libjpeg-turbo \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||||
|
-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 \
|
||||||
@@ -104,8 +115,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& rm -rf /tmp/libjpeg-turbo
|
&& rm -rf /tmp/libjpeg-turbo
|
||||||
|
|
||||||
# Build static libyuv from source (cross-compile for ARM64)
|
# Build static libyuv from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
RUN git init /tmp/libyuv \
|
||||||
&& cd /tmp/libyuv \
|
&& cd /tmp/libyuv \
|
||||||
|
&& github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||||
|
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||||
@@ -113,14 +129,24 @@ 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
|
||||||
|
|
||||||
# Build static libvpx from source (cross-compile for ARM64)
|
# Build static libvpx from source (cross-compile for ARM64)
|
||||||
# CC/CXX/LD/AR must be environment variables, not configure arguments
|
# CC/CXX/LD/AR must be environment variables, not configure arguments
|
||||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libvpx \
|
||||||
&& cd /tmp/libvpx \
|
&& cd /tmp/libvpx \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& echo "=== libvpx: Configuring for ARM64 ===" \
|
&& echo "=== libvpx: Configuring for ARM64 ===" \
|
||||||
&& export CC=aarch64-linux-gnu-gcc \
|
&& export CC=aarch64-linux-gnu-gcc \
|
||||||
&& export CXX=aarch64-linux-gnu-g++ \
|
&& export CXX=aarch64-linux-gnu-g++ \
|
||||||
@@ -143,7 +169,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
|||||||
&& rm -rf /tmp/libvpx
|
&& rm -rf /tmp/libvpx
|
||||||
|
|
||||||
# Build static libx264 from source (cross-compile for ARM64)
|
# Build static libx264 from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||||
&& cd /tmp/x264 \
|
&& cd /tmp/x264 \
|
||||||
&& export CC=aarch64-linux-gnu-gcc \
|
&& export CC=aarch64-linux-gnu-gcc \
|
||||||
&& export AR=aarch64-linux-gnu-ar \
|
&& export AR=aarch64-linux-gnu-ar \
|
||||||
@@ -159,12 +187,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
|||||||
&& rm -rf /tmp/x264
|
&& rm -rf /tmp/x264
|
||||||
|
|
||||||
# Build static libx265 from source (cross-compile for ARM64)
|
# Build static libx265 from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/x265 \
|
||||||
&& cd /tmp/x265 \
|
&& cd /tmp/x265 \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& cd source \
|
&& cd source \
|
||||||
&& mkdir -p build \
|
&& mkdir -p build \
|
||||||
&& cd build \
|
&& cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||||
-DCMAKE_SYSTEM_NAME=Linux \
|
-DCMAKE_SYSTEM_NAME=Linux \
|
||||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||||
@@ -198,12 +232,15 @@ Cflags: -I\${includedir}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build static libopus from source (cross-compile for ARM64)
|
# Build static libopus from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||||
|
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||||
|
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||||
&& cd /tmp/opus \
|
&& cd /tmp/opus \
|
||||||
&& export CC=aarch64-linux-gnu-gcc \
|
&& export CC=aarch64-linux-gnu-gcc \
|
||||||
&& export AR=aarch64-linux-gnu-ar \
|
&& export AR=aarch64-linux-gnu-ar \
|
||||||
&& export RANLIB=aarch64-linux-gnu-ranlib \
|
&& export RANLIB=aarch64-linux-gnu-ranlib \
|
||||||
&& ./autogen.sh \
|
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--prefix=/usr/aarch64-linux-gnu \
|
--prefix=/usr/aarch64-linux-gnu \
|
||||||
--host=aarch64-linux-gnu \
|
--host=aarch64-linux-gnu \
|
||||||
@@ -211,13 +248,20 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
|||||||
--disable-doc \
|
--disable-doc \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/opus
|
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||||
|
|
||||||
# Download and build FFmpeg with RKMPP support
|
# Download and build FFmpeg with RKMPP support
|
||||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
&& github_prefix="" \
|
||||||
&& tar -xzf ffmpeg.tar.gz \
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
&& cd ffmpeg \
|
&& git clone --depth 1 --branch ${RKMPP_BRANCH} "${github_prefix}https://github.com/nyanmisaka/mpp.git" rkmpp \
|
||||||
|
&& git clone --depth 1 --branch ${RKRGA_BRANCH} "${github_prefix}https://github.com/nyanmisaka/rk-mirrors.git" rkrga \
|
||||||
|
&& git init ffmpeg-rockchip \
|
||||||
|
&& cd ffmpeg-rockchip \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||||
|
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
|
&& cd .. \
|
||||||
# Build RKMPP
|
# Build RKMPP
|
||||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||||
&& cmake .. \
|
&& cmake .. \
|
||||||
@@ -232,6 +276,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/rockchip_mpp.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/rockchip_mpp.pc \
|
||||||
|
&& rm -f /usr/aarch64-linux-gnu/lib/librockchip_mpp.so* \
|
||||||
&& cd ../.. \
|
&& cd ../.. \
|
||||||
# Build RKRGA - create cross file for meson
|
# Build RKRGA - create cross file for meson
|
||||||
&& echo '[binaries]' > /tmp/aarch64-cross.txt \
|
&& echo '[binaries]' > /tmp/aarch64-cross.txt \
|
||||||
@@ -259,6 +304,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& ar rcs /usr/aarch64-linux-gnu/lib/librga.a $(find build -name '*.o') \
|
&& ar rcs /usr/aarch64-linux-gnu/lib/librga.a $(find build -name '*.o') \
|
||||||
&& ranlib /usr/aarch64-linux-gnu/lib/librga.a \
|
&& ranlib /usr/aarch64-linux-gnu/lib/librga.a \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/librga.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/librga.pc \
|
||||||
|
&& rm -f /usr/aarch64-linux-gnu/lib/librga.so* \
|
||||||
&& cd .. \
|
&& cd .. \
|
||||||
# Create pkg-config wrapper for cross-compilation
|
# Create pkg-config wrapper for cross-compilation
|
||||||
&& echo '#!/bin/sh' > /tmp/aarch64-pkg-config \
|
&& echo '#!/bin/sh' > /tmp/aarch64-pkg-config \
|
||||||
@@ -348,8 +394,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& rustup target add aarch64-unknown-linux-gnu
|
&& rustup target add aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
@@ -365,4 +411,4 @@ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
|||||||
LIBYUV_STATIC=1 \
|
LIBYUV_STATIC=1 \
|
||||||
OPUS_STATIC=1 \
|
OPUS_STATIC=1 \
|
||||||
PKG_CONFIG_ALL_STATIC=1 \
|
PKG_CONFIG_ALL_STATIC=1 \
|
||||||
RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc"
|
RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc -C link-arg=-Wl,--allow-multiple-definition"
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
# Cross-compilation image for ARMv7 based on Debian 11
|
# Cross-compilation image for ARMv7 based on Debian 12
|
||||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||||
|
|
||||||
FROM debian:11
|
ARG DEBIAN_IMAGE=debian:12
|
||||||
|
FROM ${DEBIAN_IMAGE}
|
||||||
|
|
||||||
# Linux headers used by v4l2r bindgen
|
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
|
||||||
ARG LINUX_HEADERS_SHA256=
|
|
||||||
ARG CHINAMIRRO=0
|
ARG CHINAMIRRO=0
|
||||||
|
ARG GH_PROXY=https://gh-proxy.com/
|
||||||
|
ARG RUSTUP_DIST_SERVER_CN=https://rsproxy.cn
|
||||||
|
ARG RUSTUP_UPDATE_ROOT_CN=https://rsproxy.cn/rustup
|
||||||
|
ARG CARGO_INDEX_CN=https://rsproxy.cn/crates.io-index
|
||||||
|
ARG CARGO_REGISTRY_CN=sparse+https://rsproxy.cn/index/
|
||||||
|
ARG LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||||
|
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||||
|
ARG LIBVPX_VERSION=1.16.0
|
||||||
|
ARG X265_VERSION=3.4
|
||||||
|
ARG OPUS_VERSION=1.5.2
|
||||||
|
ARG RKMPP_BRANCH=jellyfin-mpp
|
||||||
|
ARG RKRGA_BRANCH=jellyfin-rga
|
||||||
|
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||||
|
|
||||||
# Optionally use Tsinghua mirrors for builds in China.
|
# Optionally use China mirrors for builds in China.
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
sed -i \
|
sed -i -E \
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
/etc/apt/sources.list.d/debian.sources; \
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
@@ -21,19 +31,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
mkdir -p /root/.cargo; \
|
mkdir -p /root/.cargo; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[source.crates-io]' \
|
'[source.crates-io]' \
|
||||||
"replace-with = 'tuna'" \
|
"replace-with = 'rsproxy-sparse'" \
|
||||||
'[source.tuna]' \
|
'[source.rsproxy]' \
|
||||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_INDEX_CN}'" \
|
||||||
'[registries.tuna]' \
|
'[source.rsproxy-sparse]' \
|
||||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||||
|
'[registries.rsproxy]' \
|
||||||
|
"index = '${CARGO_INDEX_CN}'" \
|
||||||
|
'[net]' \
|
||||||
|
'git-fetch-with-cli = true' \
|
||||||
> /root/.cargo/config.toml; \
|
> /root/.cargo/config.toml; \
|
||||||
fi \
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -69,6 +83,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
# Install ARMv7 development libraries (without VAAPI/X11 for ARM)
|
# Install ARMv7 development libraries (without VAAPI/X11 for ARM)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libssl3 \
|
||||||
|
libssl3:armhf \
|
||||||
libasound2-dev:armhf \
|
libasound2-dev:armhf \
|
||||||
libv4l-dev:armhf \
|
libv4l-dev:armhf \
|
||||||
libudev-dev:armhf \
|
libudev-dev:armhf \
|
||||||
@@ -77,23 +93,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libdrm-dev:armhf \
|
libdrm-dev:armhf \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install newer V4L2 headers for v4l2r bindgen
|
|
||||||
RUN mkdir -p /opt/v4l2-headers \
|
|
||||||
&& wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \
|
|
||||||
&& if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \
|
|
||||||
&& tar -xf /tmp/linux-headers.tar.xz -C /tmp \
|
|
||||||
&& cd /tmp/linux-${LINUX_HEADERS_VERSION} \
|
|
||||||
&& make ARCH=arm headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \
|
|
||||||
&& rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz
|
|
||||||
|
|
||||||
ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include
|
|
||||||
|
|
||||||
# Build static libjpeg-turbo from source (cross-compile for ARMv7)
|
# Build static libjpeg-turbo from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libjpeg-turbo \
|
||||||
&& cd /tmp/libjpeg-turbo \
|
&& cd /tmp/libjpeg-turbo \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||||
|
-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 \
|
||||||
@@ -103,8 +114,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& rm -rf /tmp/libjpeg-turbo
|
&& rm -rf /tmp/libjpeg-turbo
|
||||||
|
|
||||||
# Build static libyuv from source (cross-compile for ARMv7)
|
# Build static libyuv from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
RUN git init /tmp/libyuv \
|
||||||
&& cd /tmp/libyuv \
|
&& cd /tmp/libyuv \
|
||||||
|
&& github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||||
|
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||||
@@ -112,13 +128,23 @@ 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
|
||||||
|
|
||||||
# Build static libvpx from source (cross-compile for ARMv7)
|
# Build static libvpx from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libvpx \
|
||||||
&& cd /tmp/libvpx \
|
&& cd /tmp/libvpx \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& export CC=arm-linux-gnueabihf-gcc \
|
&& export CC=arm-linux-gnueabihf-gcc \
|
||||||
&& export CXX=arm-linux-gnueabihf-g++ \
|
&& export CXX=arm-linux-gnueabihf-g++ \
|
||||||
&& export LD=arm-linux-gnueabihf-ld \
|
&& export LD=arm-linux-gnueabihf-ld \
|
||||||
@@ -135,7 +161,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
|||||||
&& rm -rf /tmp/libvpx
|
&& rm -rf /tmp/libvpx
|
||||||
|
|
||||||
# Build static libx264 from source (cross-compile for ARMv7)
|
# Build static libx264 from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||||
&& cd /tmp/x264 \
|
&& cd /tmp/x264 \
|
||||||
&& export CC=arm-linux-gnueabihf-gcc \
|
&& export CC=arm-linux-gnueabihf-gcc \
|
||||||
&& export AR=arm-linux-gnueabihf-ar \
|
&& export AR=arm-linux-gnueabihf-ar \
|
||||||
@@ -152,12 +180,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
|||||||
&& rm -rf /tmp/x264
|
&& rm -rf /tmp/x264
|
||||||
|
|
||||||
# Build static libx265 from source (cross-compile for ARMv7)
|
# Build static libx265 from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/x265 \
|
||||||
&& cd /tmp/x265 \
|
&& cd /tmp/x265 \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& cd source \
|
&& cd source \
|
||||||
&& mkdir -p build \
|
&& mkdir -p build \
|
||||||
&& cd build \
|
&& cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||||
-DCMAKE_SYSTEM_NAME=Linux \
|
-DCMAKE_SYSTEM_NAME=Linux \
|
||||||
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||||
@@ -187,12 +221,15 @@ Cflags: -I\${includedir}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build static libopus from source (cross-compile for ARMv7)
|
# Build static libopus from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||||
|
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||||
|
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||||
&& cd /tmp/opus \
|
&& cd /tmp/opus \
|
||||||
&& export CC=arm-linux-gnueabihf-gcc \
|
&& export CC=arm-linux-gnueabihf-gcc \
|
||||||
&& export AR=arm-linux-gnueabihf-ar \
|
&& export AR=arm-linux-gnueabihf-ar \
|
||||||
&& export RANLIB=arm-linux-gnueabihf-ranlib \
|
&& export RANLIB=arm-linux-gnueabihf-ranlib \
|
||||||
&& ./autogen.sh \
|
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--prefix=/usr/arm-linux-gnueabihf \
|
--prefix=/usr/arm-linux-gnueabihf \
|
||||||
--host=arm-linux-gnueabihf \
|
--host=arm-linux-gnueabihf \
|
||||||
@@ -200,13 +237,20 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
|||||||
--disable-doc \
|
--disable-doc \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/opus
|
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||||
|
|
||||||
# Download and build FFmpeg with RKMPP support
|
# Download and build FFmpeg with RKMPP support
|
||||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
&& github_prefix="" \
|
||||||
&& tar -xzf ffmpeg.tar.gz \
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
&& cd ffmpeg \
|
&& git clone --depth 1 --branch ${RKMPP_BRANCH} "${github_prefix}https://github.com/nyanmisaka/mpp.git" rkmpp \
|
||||||
|
&& git clone --depth 1 --branch ${RKRGA_BRANCH} "${github_prefix}https://github.com/nyanmisaka/rk-mirrors.git" rkrga \
|
||||||
|
&& git init ffmpeg-rockchip \
|
||||||
|
&& cd ffmpeg-rockchip \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||||
|
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
|
&& cd .. \
|
||||||
# Build RKMPP
|
# Build RKMPP
|
||||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||||
&& cmake .. \
|
&& cmake .. \
|
||||||
@@ -221,6 +265,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/rockchip_mpp.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/rockchip_mpp.pc \
|
||||||
|
&& rm -f /usr/arm-linux-gnueabihf/lib/librockchip_mpp.so* \
|
||||||
&& cd ../.. \
|
&& cd ../.. \
|
||||||
# Build RKRGA - create cross file for meson
|
# Build RKRGA - create cross file for meson
|
||||||
&& echo '[binaries]' > /tmp/armhf-cross.txt \
|
&& echo '[binaries]' > /tmp/armhf-cross.txt \
|
||||||
@@ -248,6 +293,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& ar rcs /usr/arm-linux-gnueabihf/lib/librga.a $(find build -name '*.o') \
|
&& ar rcs /usr/arm-linux-gnueabihf/lib/librga.a $(find build -name '*.o') \
|
||||||
&& ranlib /usr/arm-linux-gnueabihf/lib/librga.a \
|
&& ranlib /usr/arm-linux-gnueabihf/lib/librga.a \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/librga.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/librga.pc \
|
||||||
|
&& rm -f /usr/arm-linux-gnueabihf/lib/librga.so* \
|
||||||
&& cd .. \
|
&& cd .. \
|
||||||
# Create pkg-config wrapper for cross-compilation
|
# Create pkg-config wrapper for cross-compilation
|
||||||
&& echo '#!/bin/sh' > /tmp/armhf-pkg-config \
|
&& echo '#!/bin/sh' > /tmp/armhf-pkg-config \
|
||||||
@@ -337,8 +383,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& rustup target add armv7-unknown-linux-gnueabihf
|
&& rustup target add armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
@@ -354,7 +400,7 @@ ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
|
|||||||
LIBYUV_STATIC=1 \
|
LIBYUV_STATIC=1 \
|
||||||
OPUS_STATIC=1 \
|
OPUS_STATIC=1 \
|
||||||
PKG_CONFIG_ALL_STATIC=1 \
|
PKG_CONFIG_ALL_STATIC=1 \
|
||||||
RUSTFLAGS="-C linker=arm-linux-gnueabihf-gcc"
|
RUSTFLAGS="-C linker=arm-linux-gnueabihf-gcc -C link-arg=-Wl,--allow-multiple-definition"
|
||||||
|
|
||||||
# Default command
|
# Default command
|
||||||
CMD ["bash"]
|
CMD ["bash"]
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
# Cross-compilation image for x86_64 based on Debian 11
|
# Cross-compilation image for x86_64 based on Debian 12
|
||||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||||
|
|
||||||
FROM debian:11
|
ARG DEBIAN_IMAGE=debian:12
|
||||||
|
FROM ${DEBIAN_IMAGE}
|
||||||
|
|
||||||
# Linux headers used by v4l2r bindgen
|
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
|
||||||
ARG LINUX_HEADERS_SHA256=
|
|
||||||
ARG CHINAMIRRO=0
|
ARG CHINAMIRRO=0
|
||||||
|
ARG GH_PROXY=https://gh-proxy.com/
|
||||||
|
ARG RUSTUP_DIST_SERVER_CN=https://rsproxy.cn
|
||||||
|
ARG RUSTUP_UPDATE_ROOT_CN=https://rsproxy.cn/rustup
|
||||||
|
ARG CARGO_INDEX_CN=https://rsproxy.cn/crates.io-index
|
||||||
|
ARG CARGO_REGISTRY_CN=sparse+https://rsproxy.cn/index/
|
||||||
|
ARG LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||||
|
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||||
|
ARG LIBVPX_VERSION=1.16.0
|
||||||
|
ARG X265_VERSION=3.4
|
||||||
|
ARG OPUS_VERSION=1.5.2
|
||||||
|
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||||
|
|
||||||
# Optionally use Tsinghua mirrors for builds in China.
|
# Optionally use China mirrors for builds in China.
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
sed -i \
|
sed -i -E \
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
/etc/apt/sources.list.d/debian.sources; \
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
@@ -21,19 +29,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
mkdir -p /root/.cargo; \
|
mkdir -p /root/.cargo; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[source.crates-io]' \
|
'[source.crates-io]' \
|
||||||
"replace-with = 'tuna'" \
|
"replace-with = 'rsproxy-sparse'" \
|
||||||
'[source.tuna]' \
|
'[source.rsproxy]' \
|
||||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_INDEX_CN}'" \
|
||||||
'[registries.tuna]' \
|
'[source.rsproxy-sparse]' \
|
||||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||||
|
'[registries.rsproxy]' \
|
||||||
|
"index = '${CARGO_INDEX_CN}'" \
|
||||||
|
'[net]' \
|
||||||
|
'git-fetch-with-cli = true' \
|
||||||
> /root/.cargo/config.toml; \
|
> /root/.cargo/config.toml; \
|
||||||
fi \
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -76,48 +88,62 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libxdmcp-dev \
|
libxdmcp-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install newer V4L2 headers for v4l2r bindgen
|
|
||||||
RUN mkdir -p /opt/v4l2-headers \
|
|
||||||
&& wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \
|
|
||||||
&& if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \
|
|
||||||
&& tar -xf /tmp/linux-headers.tar.xz -C /tmp \
|
|
||||||
&& cd /tmp/linux-${LINUX_HEADERS_VERSION} \
|
|
||||||
&& make ARCH=x86 headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \
|
|
||||||
&& rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz
|
|
||||||
|
|
||||||
ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include
|
|
||||||
|
|
||||||
# Build static libjpeg-turbo from source (needed by libyuv)
|
# Build static libjpeg-turbo from source (needed by libyuv)
|
||||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libjpeg-turbo \
|
||||||
&& cd /tmp/libjpeg-turbo \
|
&& cd /tmp/libjpeg-turbo \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
|
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||||
|
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||||
-DENABLE_SHARED=OFF -DENABLE_STATIC=ON \
|
-DENABLE_SHARED=OFF -DENABLE_STATIC=ON \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/libjpeg-turbo
|
&& rm -rf /tmp/libjpeg-turbo
|
||||||
|
|
||||||
# Build static libyuv from source (uses libjpeg-turbo headers)
|
# Build static libyuv from source (uses libjpeg-turbo headers)
|
||||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
RUN git init /tmp/libyuv \
|
||||||
&& cd /tmp/libyuv \
|
&& cd /tmp/libyuv \
|
||||||
|
&& github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||||
|
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
|
-DJPEG_FOUND=TRUE \
|
||||||
|
-DJPEG_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
|
||||||
|
|
||||||
# Build static libvpx from source
|
# Build static libvpx from source
|
||||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libvpx \
|
||||||
&& cd /tmp/libvpx \
|
&& cd /tmp/libvpx \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--enable-static --disable-shared --enable-pic \
|
--enable-static --disable-shared --enable-pic \
|
||||||
--disable-examples --disable-tools --disable-docs \
|
--disable-examples --disable-tools --disable-docs \
|
||||||
|
--disable-unit-tests \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/libvpx
|
&& rm -rf /tmp/libvpx
|
||||||
|
|
||||||
# Build static libx264 from source
|
# Build static libx264 from source
|
||||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||||
&& cd /tmp/x264 \
|
&& cd /tmp/x264 \
|
||||||
&& ./configure --enable-static --disable-cli \
|
&& ./configure --enable-static --disable-cli \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
@@ -125,12 +151,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
|||||||
&& rm -rf /tmp/x264
|
&& rm -rf /tmp/x264
|
||||||
|
|
||||||
# Build static libx265 from source
|
# Build static libx265 from source
|
||||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/x265 \
|
||||||
&& cd /tmp/x265 \
|
&& cd /tmp/x265 \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& cd source \
|
&& cd source \
|
||||||
&& mkdir -p build \
|
&& mkdir -p build \
|
||||||
&& cd build \
|
&& cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||||
-DENABLE_SHARED=OFF \
|
-DENABLE_SHARED=OFF \
|
||||||
-DENABLE_CLI=OFF \
|
-DENABLE_CLI=OFF \
|
||||||
-DBUILD_SHARED_LIBS=OFF \
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
@@ -155,21 +187,28 @@ Cflags: -I\${includedir}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build static libopus from source
|
# Build static libopus from source
|
||||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||||
|
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||||
|
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||||
&& cd /tmp/opus \
|
&& cd /tmp/opus \
|
||||||
&& ./autogen.sh \
|
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--enable-static --disable-shared \
|
--enable-static --disable-shared \
|
||||||
--disable-doc \
|
--disable-doc \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/opus
|
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||||
|
|
||||||
# Download and build FFmpeg with minimal configuration for encoding only
|
# Download and build FFmpeg with minimal configuration for encoding only
|
||||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
&& github_prefix="" \
|
||||||
&& tar -xzf ffmpeg.tar.gz \
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
&& cd ffmpeg/ffmpeg-rockchip \
|
&& git init ffmpeg-rockchip \
|
||||||
|
&& cd ffmpeg-rockchip \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||||
|
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--enable-gpl \
|
--enable-gpl \
|
||||||
--enable-version3 \
|
--enable-version3 \
|
||||||
@@ -242,8 +281,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& rustup target add x86_64-unknown-linux-gnu
|
&& rustup target add x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Maintainer: SilentWind <admin@mofeng.run>
|
|||||||
Package: one-kvm
|
Package: one-kvm
|
||||||
Architecture: {arch}
|
Architecture: {arch}
|
||||||
Depends: ${{auto}}, ca-certificates{distsuffix}
|
Depends: ${{auto}}, ca-certificates{distsuffix}
|
||||||
|
Recommends: bluez
|
||||||
Description: A open and lightweight IP-KVM solution written in Rust
|
Description: A open and lightweight IP-KVM solution written in Rust
|
||||||
Enables BIOS-level remote management of servers and workstations.
|
Enables BIOS-level remote management of servers and workstations.
|
||||||
.
|
.
|
||||||
@@ -17,6 +18,7 @@ Description: A open and lightweight IP-KVM solution written in Rust
|
|||||||
* Hardware-accelerated video encoding (VAAPI, QSV, RKMPP)
|
* Hardware-accelerated video encoding (VAAPI, QSV, RKMPP)
|
||||||
* WebRTC and MJPEG streaming with low latency
|
* WebRTC and MJPEG streaming with low latency
|
||||||
* USB HID emulation via OTG gadget
|
* USB HID emulation via OTG gadget
|
||||||
|
* Classic Bluetooth keyboard and mouse via BlueZ
|
||||||
* Mass storage device for ISO/IMG mounting
|
* Mass storage device for ISO/IMG mounting
|
||||||
* ATX power control via GPIO or USB relay
|
* ATX power control via GPIO or USB relay
|
||||||
Homepage: https://github.com/mofeng-git/One-KVM
|
Homepage: https://github.com/mofeng-git/One-KVM
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=One-KVM IP-KVM Service
|
Description=One-KVM IP-KVM Service
|
||||||
Documentation=https://github.com/mofeng-git/One-KVM
|
Documentation=https://github.com/mofeng-git/One-KVM
|
||||||
After=network.target
|
After=network-online.target bluetooth.service
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Build deb packages from pre-compiled binaries
|
# Build deb packages from pre-compiled binaries
|
||||||
# Binaries are compiled once on Debian 11 (GLIBC 2.31) via build-images.sh
|
# Binaries are compiled once on Debian 12 (GLIBC 2.36) via build-images.sh
|
||||||
# This script packages them directly on the host using dpkg-deb
|
# This script packages them directly on the host using dpkg-deb
|
||||||
# Usage: ./build/build-deb.sh [arch]
|
# Usage: ./build/build-deb.sh [arch]
|
||||||
# Example: ./build/build-deb.sh aarch64
|
# Example: ./build/build-deb.sh aarch64
|
||||||
@@ -125,8 +125,8 @@ EOF
|
|||||||
chmod 755 "$PKG_DIR/DEBIAN/prerm"
|
chmod 755 "$PKG_DIR/DEBIAN/prerm"
|
||||||
|
|
||||||
# Create control file
|
# Create control file
|
||||||
BASE_DEPS="libc6 (>= 2.31), libgcc-s1, libstdc++6, libasound2 (>= 1.1), libdrm2 (>= 2.4)"
|
BASE_DEPS="libc6 (>= 2.36), libgcc-s1, libstdc++6, libasound2t64 (>= 1.1) | libasound2 (>= 1.1), libdrm2 (>= 2.4)"
|
||||||
AMD64_DEPS="libva2 (>= 2.0), libva-drm2 (>= 2.10), libva-x11-2 (>= 2.10), libmfx1 (>= 21.1), libx11-6 (>= 1.6), libxcb1 (>= 1.14), i965-va-driver-shaders (>= 2.4), intel-media-va-driver-non-free (>= 21.1)"
|
AMD64_DEPS="libva2 (>= 2.0), libva-drm2 (>= 2.10), libva-x11-2 (>= 2.10), libmfx-gen1.2 (>= 22.0) | libmfx1 (>= 21.1), libx11-6 (>= 1.6), libxcb1 (>= 1.14), i965-va-driver-shaders (>= 2.4), intel-media-va-driver-non-free (>= 21.1)"
|
||||||
DEPS="$BASE_DEPS"
|
DEPS="$BASE_DEPS"
|
||||||
if [ "$DEB_ARCH" = "amd64" ]; then
|
if [ "$DEB_ARCH" = "amd64" ]; then
|
||||||
DEPS="$DEPS, $AMD64_DEPS"
|
DEPS="$DEPS, $AMD64_DEPS"
|
||||||
@@ -139,10 +139,11 @@ Section: admin
|
|||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: $DEB_ARCH
|
Architecture: $DEB_ARCH
|
||||||
Depends: $DEPS
|
Depends: $DEPS
|
||||||
|
Recommends: bluez
|
||||||
Maintainer: SilentWind <admin@mofeng.run>
|
Maintainer: SilentWind <admin@mofeng.run>
|
||||||
Description: A open and lightweight IP-KVM solution
|
Description: A open and lightweight IP-KVM solution
|
||||||
Enables BIOS-level remote management of servers and workstations.
|
Enables BIOS-level remote management of servers and workstations.
|
||||||
Built on Debian 11, compatible with Debian 11+, Ubuntu 20.04+.
|
Built on Debian 12, compatible with Debian 12+, Ubuntu 24.04+.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build deb directly on host
|
# Build deb directly on host
|
||||||
|
|||||||
@@ -314,6 +314,10 @@ build_for_platform() {
|
|||||||
|
|
||||||
if [ "$PUSH" = true ]; then
|
if [ "$PUSH" = true ]; then
|
||||||
build_cmd="$build_cmd --push"
|
build_cmd="$build_cmd --push"
|
||||||
|
# Aliyun ACR rejects the OCI artifact manifests used by BuildKit attestations.
|
||||||
|
if [[ "$REGISTRY" == registry.cn-*.aliyuncs.com/* ]]; then
|
||||||
|
build_cmd="$build_cmd --provenance=false --sbom=false"
|
||||||
|
fi
|
||||||
elif [ "$LOAD" = true ]; then
|
elif [ "$LOAD" = true ]; then
|
||||||
build_cmd="$build_cmd --load"
|
build_cmd="$build_cmd --load"
|
||||||
fi
|
fi
|
||||||
|
|||||||
24
docs/macos-drag-compatibility.md
Normal file
24
docs/macos-drag-compatibility.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# macOS 拖拽兼容模式(实验性)
|
||||||
|
|
||||||
|
适用于 OTG 和 CH9329 后端的绝对鼠标输入。遇到目标 Mac 上拖拽只能移动一小段便停止时,可在设置页的 HID 配置中开启「macOS 拖拽兼容模式」。默认关闭。
|
||||||
|
|
||||||
|
配置字段为 `hid.mouse_macos_drag`,旧 PR 的 `ch9329_macos_drag` 字段仍可作为读取别名。OTG 必须同时启用相对鼠标和绝对鼠标接口,否则配置保存会被拒绝。CH9329 同时启用 Linux 兼容开关时,此模式优先。
|
||||||
|
|
||||||
|
## 报告行为
|
||||||
|
|
||||||
|
- 未按键时仍使用绝对坐标定位。
|
||||||
|
- 从绝对输入开始的拖拽:按下使用绝对报告,移动和滚轮使用相对报告,释放时先更新相对按钮状态,再发送绝对按钮释放。
|
||||||
|
- 按键报告的通道保持到所有按钮释放,不随拖拽期间的输入模式切换而改变。
|
||||||
|
- 从相对输入开始的点击、拖拽继续使用相对按钮报告;此选项不修复原生相对模式的点击兼容问题。
|
||||||
|
- 位移从原始 15 位输入坐标换算,累计小数余量,大位移按单包范围分包并保留总量及方向。
|
||||||
|
- 运行时使用当前视频采集尺寸,并在 HID 重载后恢复尺寸;视频尚未提供尺寸时使用配置尺寸。
|
||||||
|
|
||||||
|
## 验证范围与限制
|
||||||
|
|
||||||
|
本次 OTG 扩展及位移换算修改没有 macOS 实机验证。原 PR 作者对旧 CH9329 实现的实机反馈不能视为本版本已经验证。
|
||||||
|
|
||||||
|
本地自动化覆盖位移累计/分包、1080p/4K 换算、往返移动、滚轮、输入模式切换、复位、CH9329 命令队列、OTG 临时文件模拟端点及配置校验。临时文件测试仅验证写出的字节,不验证 USB 枚举或目标系统的事件解释。
|
||||||
|
|
||||||
|
相对位移受 macOS 鼠标速度、加速、显示缩放及多屏布局影响,无法保证与绝对定位逐像素一致。松手报告携带最后的客户端绝对坐标,仍可能出现落点偏移或光标跳位;大位移分包也可能增加低波特率串口延迟。
|
||||||
|
|
||||||
|
后续实机验证应分别覆盖两个后端:窗口/文件/文本拖拽,快速及慢速移动,滚轮和多按钮组合,1080p/4K 与缩放显示,输入模式切换、断开重连、精确落点。异常时关闭此选项恢复默认行为。
|
||||||
16
libs/bluetooth-hid/Cargo.toml
Normal file
16
libs/bluetooth-hid/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "one-kvm-bluetooth-hid"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] }
|
||||||
|
dbus = { version = "0.9", features = ["vendored"] }
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "process"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
dbus-crossroads = "0.5"
|
||||||
|
dbus-tokio = "0.7"
|
||||||
|
libc = "0.2"
|
||||||
|
tracing = "0.1"
|
||||||
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//! Explicit hardware regression: opens a 10-second pairing window without
|
||||||
|
//! removing existing bonds or sending input. Stop the production HID first.
|
||||||
|
use one_kvm_bluetooth_hid::{Action, Config, Peripheral};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), String> {
|
||||||
|
let peripheral = Peripheral::start(Config {
|
||||||
|
adapter: "hci0".into(),
|
||||||
|
name: "One-KVM HID".into(),
|
||||||
|
peer: None,
|
||||||
|
})?;
|
||||||
|
let result = async {
|
||||||
|
for _ in 0..50 {
|
||||||
|
if peripheral.status().initialized {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
let before = peripheral.status();
|
||||||
|
if !before.initialized {
|
||||||
|
return Err(format!("Initialization failed: {:?}", before.error));
|
||||||
|
}
|
||||||
|
if !before
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.any(|d| Some(&d.address) == before.peer.as_ref() && d.paired)
|
||||||
|
{
|
||||||
|
return Err("This regression needs an existing bonded computer".into());
|
||||||
|
}
|
||||||
|
peripheral.action(Action::Pair(10)).await?;
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
let open = peripheral.status();
|
||||||
|
println!("open: {}", serde_json::to_string(&open).unwrap());
|
||||||
|
if open.pairing_seconds == 0 {
|
||||||
|
return Err("Existing bond prematurely closed pairing".into());
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||||
|
let expired = peripheral.status();
|
||||||
|
println!("expired: {}", serde_json::to_string(&expired).unwrap());
|
||||||
|
if expired.pairing_seconds != 0 || expired.peer != before.peer {
|
||||||
|
return Err("Window did not expire while preserving the bonded peer".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
let cleanup = peripheral.shutdown().await;
|
||||||
|
result.and(cleanup)
|
||||||
|
}
|
||||||
24
libs/bluetooth-hid/examples/probe.rs
Normal file
24
libs/bluetooth-hid/examples/probe.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//! Native hardware smoke check. Registers for 12 seconds, sends no input,
|
||||||
|
//! opens no pairing window, then restores the adapter configuration.
|
||||||
|
use one_kvm_bluetooth_hid::{Config, Peripheral};
|
||||||
|
use std::time::Duration;
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), String> {
|
||||||
|
let peripheral = Peripheral::start(Config {
|
||||||
|
adapter: "hci0".into(),
|
||||||
|
name: "One-KVM HID".into(),
|
||||||
|
peer: None,
|
||||||
|
})?;
|
||||||
|
let mut initialized = false;
|
||||||
|
for _ in 0..12 {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
let status = peripheral.status();
|
||||||
|
initialized |= status.initialized;
|
||||||
|
println!("{}", serde_json::to_string(&status).unwrap());
|
||||||
|
}
|
||||||
|
peripheral.shutdown().await?;
|
||||||
|
if !initialized {
|
||||||
|
return Err("Native BlueZ peripheral never initialized".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
236
libs/bluetooth-hid/src/agent.rs
Normal file
236
libs/bluetooth-hid/src/agent.rs
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
use crate::Shared;
|
||||||
|
use dbus::{channel::MatchingReceiver, message::MatchRule, nonblock::Proxy, Path};
|
||||||
|
use dbus_crossroads::Crossroads;
|
||||||
|
use std::{
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PATH: &str = "/org/onekvm/bluetooth/agent";
|
||||||
|
pub struct Agent {
|
||||||
|
connection: Arc<dbus::nonblock::SyncConnection>,
|
||||||
|
task: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
impl Agent {
|
||||||
|
pub async fn register(shared: Arc<Shared>, adapter: String) -> Result<Self, String> {
|
||||||
|
let (resource, connection) =
|
||||||
|
dbus_tokio::connection::new_system_sync().map_err(|e| e.to_string())?;
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let _ = resource.await;
|
||||||
|
});
|
||||||
|
let mut cr = Crossroads::new();
|
||||||
|
let iface = cr.register("org.bluez.Agent1", |b| {
|
||||||
|
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||||
|
b.method("Cancel", (), (), |_, _: &mut (), ()| Ok(()));
|
||||||
|
let state = shared.clone();
|
||||||
|
let name = adapter.clone();
|
||||||
|
b.method(
|
||||||
|
"RequestAuthorization",
|
||||||
|
("device",),
|
||||||
|
(),
|
||||||
|
move |_, _: &mut (), (path,): (Path<'static>,)| {
|
||||||
|
authorize(&state, &name, &path, true)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let state = shared.clone();
|
||||||
|
let name = adapter.clone();
|
||||||
|
b.method(
|
||||||
|
"RequestConfirmation",
|
||||||
|
("device", "passkey"),
|
||||||
|
(),
|
||||||
|
move |_, _: &mut (), (path, _): (Path<'static>, u32)| {
|
||||||
|
authorize(&state, &name, &path, true)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
b.method(
|
||||||
|
"AuthorizeService",
|
||||||
|
("device", "uuid"),
|
||||||
|
(),
|
||||||
|
move |_, _: &mut (), (path, uuid): (Path<'static>, String)| {
|
||||||
|
if !matches!(uuid.as_str(), "00001124-0000-1000-8000-00805f9b34fb") {
|
||||||
|
return Err(dbus::MethodErr::from((
|
||||||
|
"org.bluez.Error.Rejected",
|
||||||
|
"Not an HID service",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
authorize(&shared, &adapter, &path, false)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cr.insert(PATH, &[iface], ());
|
||||||
|
let profile = cr.register("org.bluez.Profile1", |b| {
|
||||||
|
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||||
|
b.method(
|
||||||
|
"RequestDisconnection",
|
||||||
|
("device",),
|
||||||
|
(),
|
||||||
|
|_, _: &mut (), (_device,): (Path<'static>,)| Ok(()),
|
||||||
|
);
|
||||||
|
b.method(
|
||||||
|
"NewConnection",
|
||||||
|
("device", "fd", "properties"),
|
||||||
|
(),
|
||||||
|
|_,
|
||||||
|
_: &mut (),
|
||||||
|
(_device, _fd, _props): (
|
||||||
|
Path<'static>,
|
||||||
|
dbus::arg::OwnedFd,
|
||||||
|
dbus::arg::PropMap,
|
||||||
|
)| {
|
||||||
|
// This profile only publishes SDP. Our L2CAP sockets own both HID channels.
|
||||||
|
Err::<(), _>(dbus::MethodErr::from((
|
||||||
|
"org.bluez.Error.Rejected",
|
||||||
|
"Unexpected profile connection",
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cr.insert("/org/onekvm/bluetooth/profile", &[profile], ());
|
||||||
|
connection.start_receive(
|
||||||
|
MatchRule::new_method_call(),
|
||||||
|
Box::new(move |msg, conn| {
|
||||||
|
let _ = cr.handle_message(msg, conn);
|
||||||
|
true
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let agent = Self { connection, task };
|
||||||
|
let proxy = Proxy::new(
|
||||||
|
"org.bluez",
|
||||||
|
"/org/bluez",
|
||||||
|
Duration::from_secs(5),
|
||||||
|
agent.connection.clone(),
|
||||||
|
);
|
||||||
|
let result: Result<(), dbus::Error> = proxy
|
||||||
|
.method_call(
|
||||||
|
"org.bluez.AgentManager1",
|
||||||
|
"RegisterAgent",
|
||||||
|
(Path::from(PATH), "NoInputNoOutput"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Err(error) = result {
|
||||||
|
return Err(error.to_string());
|
||||||
|
}
|
||||||
|
let result: Result<(), dbus::Error> = proxy
|
||||||
|
.method_call(
|
||||||
|
"org.bluez.AgentManager1",
|
||||||
|
"RequestDefaultAgent",
|
||||||
|
(Path::from(PATH),),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
result.map_err(|e| e.to_string())?;
|
||||||
|
let mut options: dbus::arg::PropMap = std::collections::HashMap::new();
|
||||||
|
options.insert(
|
||||||
|
"ServiceRecord".into(),
|
||||||
|
dbus::arg::Variant(Box::new(crate::protocol::sdp())),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"Role".into(),
|
||||||
|
dbus::arg::Variant(Box::new("server".to_string())),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"RequireAuthentication".into(),
|
||||||
|
dbus::arg::Variant(Box::new(true)),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"RequireAuthorization".into(),
|
||||||
|
dbus::arg::Variant(Box::new(false)),
|
||||||
|
);
|
||||||
|
let result: Result<(), dbus::Error> = proxy
|
||||||
|
.method_call(
|
||||||
|
"org.bluez.ProfileManager1",
|
||||||
|
"RegisterProfile",
|
||||||
|
(
|
||||||
|
Path::from("/org/onekvm/bluetooth/profile"),
|
||||||
|
crate::protocol::HID_UUID,
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
result.map_err(|e| format!("Register HID SDP: {e}"))?;
|
||||||
|
Ok(agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Agent {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.task.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn authorize(
|
||||||
|
shared: &Shared,
|
||||||
|
adapter: &str,
|
||||||
|
path: &str,
|
||||||
|
pairing: bool,
|
||||||
|
) -> Result<(), dbus::MethodErr> {
|
||||||
|
let rejected = || {
|
||||||
|
dbus::MethodErr::from((
|
||||||
|
"org.bluez.Error.Rejected",
|
||||||
|
"Open pairing in One-KVM or select the paired computer",
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let prefix = format!("/org/bluez/{adapter}/dev_");
|
||||||
|
let address = path
|
||||||
|
.strip_prefix(&prefix)
|
||||||
|
.ok_or_else(rejected)?
|
||||||
|
.replace('_', ":");
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
if pairing
|
||||||
|
&& !state
|
||||||
|
.pairing_until
|
||||||
|
.is_some_and(|until| until > Instant::now())
|
||||||
|
{
|
||||||
|
return Err(rejected());
|
||||||
|
}
|
||||||
|
if state.peer.as_ref().is_some_and(|peer| peer != &address) {
|
||||||
|
return Err(rejected());
|
||||||
|
}
|
||||||
|
if state.peer.is_none() {
|
||||||
|
if !pairing || state.bonded_before_pairing.contains(&address) {
|
||||||
|
return Err(rejected());
|
||||||
|
}
|
||||||
|
state.peer = Some(address);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn existing_unrelated_bond_cannot_be_adopted_by_agent_callback() {
|
||||||
|
let shared = Shared::new(None);
|
||||||
|
{
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state
|
||||||
|
.bonded_before_pairing
|
||||||
|
.insert("10:6F:D9:66:97:88".into());
|
||||||
|
}
|
||||||
|
assert!(authorize(
|
||||||
|
&shared,
|
||||||
|
"hci0",
|
||||||
|
"/org/bluez/hci0/dev_10_6F_D9_66_97_88",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(shared.state.lock().unwrap().peer.is_none());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn only_pair_during_window_and_pin_first_peer() {
|
||||||
|
let state = Shared::new(None);
|
||||||
|
let first = "/org/bluez/hci0/dev_10_6F_D9_66_97_88";
|
||||||
|
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||||
|
state.state.lock().unwrap().pairing_until = Some(Instant::now() + Duration::from_secs(60));
|
||||||
|
assert!(authorize(&state, "hci0", first, true).is_ok());
|
||||||
|
assert!(authorize(
|
||||||
|
&state,
|
||||||
|
"hci0",
|
||||||
|
"/org/bluez/hci0/dev_10_6F_D9_66_97_89",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(authorize(&state, "hci1", first, true).is_err());
|
||||||
|
state.state.lock().unwrap().pairing_until = None;
|
||||||
|
assert!(authorize(&state, "hci0", first, false).is_ok());
|
||||||
|
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
145
libs/bluetooth-hid/src/bonds.rs
Normal file
145
libs/bluetooth-hid/src/bonds.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
//! Only bonds explicitly owned by this HID peripheral may be removed.
|
||||||
|
use bluer::{Adapter, Session};
|
||||||
|
use std::{future::Future, pin::Pin};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Bond {
|
||||||
|
pub adapter: String,
|
||||||
|
pub peer: String,
|
||||||
|
pub pending: bool,
|
||||||
|
}
|
||||||
|
pub type Operation<'a, T> = Pin<Box<dyn Future<Output = Result<T, String>> + Send + 'a>>;
|
||||||
|
pub trait BondStore: Send + Sync {
|
||||||
|
fn list(&self) -> Operation<'_, Vec<Bond>>;
|
||||||
|
fn save(&self, bond: Bond) -> Operation<'_, ()>;
|
||||||
|
fn remove(&self, bond: Bond) -> Operation<'_, ()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clean_adapter(store: &dyn BondStore, adapter: &Adapter) -> Result<(), String> {
|
||||||
|
let address = adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string();
|
||||||
|
clean_owned(store, &address, |peer| async move {
|
||||||
|
let peer = peer.parse().map_err(|_| "Invalid stored HID peer")?;
|
||||||
|
if adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.contains(&peer)
|
||||||
|
{
|
||||||
|
adapter
|
||||||
|
.remove_device(peer)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clean_owned<F, Fut>(
|
||||||
|
store: &dyn BondStore,
|
||||||
|
address: &str,
|
||||||
|
mut remove: F,
|
||||||
|
) -> Result<(), String>
|
||||||
|
where
|
||||||
|
F: FnMut(String) -> Fut,
|
||||||
|
Fut: Future<Output = Result<(), String>>,
|
||||||
|
{
|
||||||
|
for bond in store
|
||||||
|
.list()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|b| b.pending && b.adapter == address)
|
||||||
|
{
|
||||||
|
remove(bond.peer.clone()).await?;
|
||||||
|
// Keep the tombstone until BlueZ confirms removal; retry safely after interruption.
|
||||||
|
store.remove(bond).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reset(store: &dyn BondStore, explicit: Vec<Bond>) -> Result<(), String> {
|
||||||
|
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||||
|
let mut owned = store.list().await?;
|
||||||
|
owned.extend(explicit);
|
||||||
|
for mut bond in owned {
|
||||||
|
bond.pending = true;
|
||||||
|
store.save(bond).await?;
|
||||||
|
}
|
||||||
|
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||||
|
clean_adapter(store, &session.adapter(&name).map_err(|e| e.to_string())?).await?;
|
||||||
|
}
|
||||||
|
// Records for absent adapters remain pending and are cleaned before their next use.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Memory(Mutex<Vec<Bond>>);
|
||||||
|
impl BondStore for Memory {
|
||||||
|
fn list(&self) -> Operation<'_, Vec<Bond>> {
|
||||||
|
Box::pin(async { Ok(self.0.lock().unwrap().clone()) })
|
||||||
|
}
|
||||||
|
fn save(&self, bond: Bond) -> Operation<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut rows = self.0.lock().unwrap();
|
||||||
|
rows.retain(|b| b.adapter != bond.adapter || b.peer != bond.peer);
|
||||||
|
rows.push(bond);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn remove(&self, bond: Bond) -> Operation<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.0.lock().unwrap().retain(|b| b != &bond);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn bond(adapter: &str, peer: &str, pending: bool) -> Bond {
|
||||||
|
Bond {
|
||||||
|
adapter: adapter.into(),
|
||||||
|
peer: peer.into(),
|
||||||
|
pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_only_removes_owned_pending_bonds_on_the_selected_hardware() {
|
||||||
|
let current = bond("adapter-A", "host-1", true);
|
||||||
|
let absent = bond("adapter-B", "host-2", true);
|
||||||
|
let retained = bond("adapter-A", "host-3", false);
|
||||||
|
let store = Memory(Mutex::new(vec![current, absent.clone(), retained.clone()]));
|
||||||
|
let removed = Mutex::new(Vec::new());
|
||||||
|
clean_owned(&store, "adapter-A", |peer| {
|
||||||
|
removed.lock().unwrap().push(peer);
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(*removed.lock().unwrap(), vec!["host-1"]);
|
||||||
|
assert_eq!(store.list().await.unwrap(), vec![absent, retained]);
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_failure_keeps_tombstone_and_is_reported() {
|
||||||
|
let record = bond("adapter-A", "host-1", true);
|
||||||
|
let store = Memory(Mutex::new(vec![record.clone()]));
|
||||||
|
assert_eq!(
|
||||||
|
clean_owned(&store, "adapter-A", |_| async {
|
||||||
|
Err("BlueZ denied removal".into())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_err(),
|
||||||
|
"BlueZ denied removal"
|
||||||
|
);
|
||||||
|
assert_eq!(store.list().await.unwrap(), vec![record]);
|
||||||
|
clean_owned(&store, "adapter-A", |_| async { Ok(()) })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(store.list().await.unwrap().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
135
libs/bluetooth-hid/src/controller.rs
Normal file
135
libs/bluetooth-hid/src/controller.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
//! Exclusive controller ownership. No BLE switching or power cycling.
|
||||||
|
use std::{
|
||||||
|
fs::{File, OpenOptions},
|
||||||
|
os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
|
||||||
|
process::Stdio,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tokio::process::Command;
|
||||||
|
pub struct Controller {
|
||||||
|
_lock: File,
|
||||||
|
adapter: String,
|
||||||
|
old_class: (u8, u8),
|
||||||
|
old_connectable: bool,
|
||||||
|
}
|
||||||
|
async fn run_once(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
let mut cmd = Command::new("btmgmt");
|
||||||
|
cmd.arg("--index")
|
||||||
|
.arg(adapter.trim_start_matches("hci"))
|
||||||
|
.args(args)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.kill_on_drop(true);
|
||||||
|
let mut child = cmd
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||||
|
// BlueZ 5.55's btmgmt exits on stdin EOF, even with a command argument.
|
||||||
|
// Keep a pipe open until it finishes; systemd normally supplies /dev/null.
|
||||||
|
let stdin = child.stdin.take();
|
||||||
|
let result = tokio::time::timeout(Duration::from_secs(5), child.wait_with_output())
|
||||||
|
.await
|
||||||
|
.map_err(|_| "btmgmt timeout")?
|
||||||
|
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||||
|
drop(stdin);
|
||||||
|
let text = String::from_utf8_lossy(&result.stdout).into_owned();
|
||||||
|
if !result.status.success()
|
||||||
|
|| !result.stderr.is_empty()
|
||||||
|
|| text.to_lowercase().contains("failed")
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"btmgmt {}: {text} {}",
|
||||||
|
args.join(" "),
|
||||||
|
String::from_utf8_lossy(&result.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
async fn run(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
// BlueZ updates the EIR/class asynchronously after registering SDP.
|
||||||
|
// Kernel management rejects a simultaneous class change with Busy.
|
||||||
|
for attempt in 0..5 {
|
||||||
|
match run_once(adapter, args).await {
|
||||||
|
Err(error) if error.contains("(Busy)") && attempt < 4 => {
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
}
|
||||||
|
result => return result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
impl Controller {
|
||||||
|
pub async fn acquire(adapter: &str) -> Result<Self, String> {
|
||||||
|
let lock = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(false)
|
||||||
|
.mode(0o600)
|
||||||
|
.custom_flags(libc::O_NOFOLLOW)
|
||||||
|
.open(format!("/run/one-kvm-bluetooth-{adapter}.lock"))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if unsafe { libc::flock(lock.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
|
||||||
|
return Err("Bluetooth adapter already owned by One-KVM".into());
|
||||||
|
}
|
||||||
|
let info = run(adapter, &["info"]).await?;
|
||||||
|
let settings = info
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.trim().strip_prefix("current settings:"))
|
||||||
|
.ok_or("Cannot read Bluetooth settings")?;
|
||||||
|
if !settings.split_whitespace().any(|s| s == "br/edr") {
|
||||||
|
return Err(
|
||||||
|
"Classic Bluetooth disabled; enable BR/EDR with btmgmt before using HID".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let class = info
|
||||||
|
.split_whitespace()
|
||||||
|
.skip_while(|s| *s != "class")
|
||||||
|
.nth(1)
|
||||||
|
.ok_or("Missing Bluetooth class")?;
|
||||||
|
let class =
|
||||||
|
u32::from_str_radix(class.trim_start_matches("0x"), 16).map_err(|e| e.to_string())?;
|
||||||
|
Ok(Self {
|
||||||
|
_lock: lock,
|
||||||
|
adapter: adapter.into(),
|
||||||
|
old_class: (((class >> 8) & 0x1f) as u8, (class & 0xfc) as u8),
|
||||||
|
old_connectable: settings.split_whitespace().any(|s| s == "connectable"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub async fn configure(&self) -> Result<(), String> {
|
||||||
|
run(&self.adapter, &["class", "5", "192"]).await?;
|
||||||
|
run(&self.adapter, &["connectable", "on"]).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub async fn restore(&self) -> Result<(), String> {
|
||||||
|
let mut errors = vec![];
|
||||||
|
if let Err(e) = run(
|
||||||
|
&self.adapter,
|
||||||
|
&[
|
||||||
|
"class",
|
||||||
|
&self.old_class.0.to_string(),
|
||||||
|
&self.old_class.1.to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
if let Err(e) = run(
|
||||||
|
&self.adapter,
|
||||||
|
&[
|
||||||
|
"connectable",
|
||||||
|
if self.old_connectable { "on" } else { "off" },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(errors.join("; "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
964
libs/bluetooth-hid/src/lib.rs
Normal file
964
libs/bluetooth-hid/src/lib.rs
Normal file
@@ -0,0 +1,964 @@
|
|||||||
|
//! Classic Bluetooth HID peripheral: SDP + L2CAP. Linux/BlueZ only.
|
||||||
|
mod agent;
|
||||||
|
pub mod bonds;
|
||||||
|
mod controller;
|
||||||
|
mod protocol;
|
||||||
|
|
||||||
|
use bluer::{
|
||||||
|
l2cap::{Security, SecurityLevel, SeqPacket, SeqPacketListener, Socket, SocketAddr},
|
||||||
|
Adapter, Address, AddressType, Session,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use tokio::sync::{mpsc, oneshot, watch, Mutex as AsyncMutex};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub adapter: String,
|
||||||
|
pub name: String,
|
||||||
|
pub peer: Option<String>,
|
||||||
|
}
|
||||||
|
impl Config {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if !self
|
||||||
|
.adapter
|
||||||
|
.strip_prefix("hci")
|
||||||
|
.is_some_and(|n| !n.is_empty() && n.bytes().all(|c| c.is_ascii_digit()))
|
||||||
|
{
|
||||||
|
return Err("Bluetooth adapter must be hci followed by an index".into());
|
||||||
|
}
|
||||||
|
if self.name.is_empty() || self.name.len() > 64 || self.name.chars().any(char::is_control) {
|
||||||
|
return Err(
|
||||||
|
"Bluetooth name must contain 1–64 UTF-8 bytes without control characters".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(peer) = &self.peer {
|
||||||
|
peer.parse::<Address>()
|
||||||
|
.map_err(|_| "Invalid Bluetooth peer address")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Status {
|
||||||
|
pub initialized: bool,
|
||||||
|
pub connected: bool,
|
||||||
|
pub ready: bool,
|
||||||
|
pub adapter: String,
|
||||||
|
pub adapter_address: String,
|
||||||
|
pub peer: Option<String>,
|
||||||
|
pub pairing_seconds: u32,
|
||||||
|
pub control_connected: bool,
|
||||||
|
pub interrupt_connected: bool,
|
||||||
|
pub leds: u8,
|
||||||
|
pub generation: u64,
|
||||||
|
pub error: Option<String>,
|
||||||
|
pub devices: Vec<Device>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Device {
|
||||||
|
pub address: String,
|
||||||
|
pub name: String,
|
||||||
|
pub paired: bool,
|
||||||
|
pub connected: bool,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Report {
|
||||||
|
Keyboard,
|
||||||
|
Mouse,
|
||||||
|
Consumer,
|
||||||
|
}
|
||||||
|
impl Report {
|
||||||
|
fn len(self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Keyboard => 8,
|
||||||
|
Self::Mouse => 4,
|
||||||
|
Self::Consumer => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn id(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Keyboard => 1,
|
||||||
|
Self::Mouse => 2,
|
||||||
|
Self::Consumer => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct State {
|
||||||
|
peer: Option<String>,
|
||||||
|
pairing_until: Option<Instant>,
|
||||||
|
bonded_before_pairing: HashSet<String>,
|
||||||
|
}
|
||||||
|
impl State {
|
||||||
|
fn observe_bond(&mut self, address: &str, paired: bool) {
|
||||||
|
// Just Works may complete without an Agent1 authorization callback.
|
||||||
|
// Adopt only a new bond from our explicit window, respecting a pinned peer.
|
||||||
|
if paired
|
||||||
|
&& self.peer.is_none()
|
||||||
|
&& self
|
||||||
|
.pairing_until
|
||||||
|
.is_some_and(|until| until > Instant::now())
|
||||||
|
&& !self.bonded_before_pairing.contains(address)
|
||||||
|
{
|
||||||
|
self.peer = Some(address.to_owned());
|
||||||
|
tracing::info!(
|
||||||
|
peer = address,
|
||||||
|
"Bluetooth HID selected newly bonded computer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn close_pairing(&mut self, paired: bool, fallback: Option<String>) {
|
||||||
|
self.pairing_until = None;
|
||||||
|
if !paired {
|
||||||
|
self.peer = fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn pairing_completed(&self, selected_paired: bool) -> bool {
|
||||||
|
selected_paired
|
||||||
|
&& self
|
||||||
|
.peer
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|peer| !self.bonded_before_pairing.contains(peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Shared {
|
||||||
|
state: Mutex<State>,
|
||||||
|
}
|
||||||
|
impl Shared {
|
||||||
|
fn new(peer: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Mutex::new(State {
|
||||||
|
peer,
|
||||||
|
pairing_until: None,
|
||||||
|
bonded_before_pairing: HashSet::new(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn allowed(&self, address: Address) -> bool {
|
||||||
|
self.state.lock().unwrap().peer.as_deref() == Some(address.to_string().as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub enum Action {
|
||||||
|
Pair(u32),
|
||||||
|
ClosePairing,
|
||||||
|
Forget,
|
||||||
|
Disconnect,
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
enum Command {
|
||||||
|
Report(Report, Vec<u8>),
|
||||||
|
Action(Action),
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
struct Request {
|
||||||
|
command: Command,
|
||||||
|
result: oneshot::Sender<Result<(), String>>,
|
||||||
|
expires: Instant,
|
||||||
|
}
|
||||||
|
pub struct Peripheral {
|
||||||
|
tx: mpsc::Sender<Request>,
|
||||||
|
status: watch::Receiver<Status>,
|
||||||
|
task: AsyncMutex<Option<tokio::task::JoinHandle<()>>>,
|
||||||
|
}
|
||||||
|
impl Peripheral {
|
||||||
|
pub fn start(config: Config) -> Result<Self, String> {
|
||||||
|
Self::start_with_store(config, None)
|
||||||
|
}
|
||||||
|
pub fn start_with_store(
|
||||||
|
config: Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
config.validate()?;
|
||||||
|
let (tx, rx) = mpsc::channel(64);
|
||||||
|
let (state_tx, status) = watch::channel(Status {
|
||||||
|
adapter: config.adapter.clone(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let task = tokio::spawn(supervise(config, bonds, rx, state_tx));
|
||||||
|
Ok(Self {
|
||||||
|
tx,
|
||||||
|
status,
|
||||||
|
task: AsyncMutex::new(Some(task)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn status(&self) -> Status {
|
||||||
|
self.status.borrow().clone()
|
||||||
|
}
|
||||||
|
pub fn subscribe(&self) -> watch::Receiver<Status> {
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
async fn request(&self, command: Command, duration: Duration) -> Result<(), String> {
|
||||||
|
let (result, rx) = oneshot::channel();
|
||||||
|
let request = Request {
|
||||||
|
command,
|
||||||
|
result,
|
||||||
|
expires: Instant::now() + duration,
|
||||||
|
};
|
||||||
|
self.tx
|
||||||
|
.try_send(request)
|
||||||
|
.map_err(|_| "Bluetooth input queue unavailable or full".to_string())?;
|
||||||
|
tokio::time::timeout(duration, rx)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Bluetooth operation timed out".to_string())?
|
||||||
|
.map_err(|_| "Bluetooth worker stopped".to_string())?
|
||||||
|
}
|
||||||
|
pub async fn send(&self, report: Report, value: Vec<u8>) -> Result<(), String> {
|
||||||
|
if value.len() != report.len() {
|
||||||
|
return Err("Invalid HID report size".into());
|
||||||
|
}
|
||||||
|
self.request(Command::Report(report, value), Duration::from_millis(500))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub async fn action(&self, action: Action) -> Result<(), String> {
|
||||||
|
self.request(Command::Action(action), Duration::from_secs(15))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub async fn shutdown(&self) -> Result<(), String> {
|
||||||
|
if let Some(task) = self.task.lock().await.take() {
|
||||||
|
let result = self.request(Command::Stop, Duration::from_secs(45)).await;
|
||||||
|
// Do not abort: the worker must restore the radio before a backend switch.
|
||||||
|
task.await.map_err(|e| e.to_string())?;
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listen(address: Address, psm: u16) -> Result<SeqPacketListener, String> {
|
||||||
|
let socket = Socket::<SeqPacket>::new_seq_packet().map_err(|e| e.to_string())?;
|
||||||
|
socket
|
||||||
|
.set_security(Security {
|
||||||
|
level: SecurityLevel::Medium,
|
||||||
|
key_size: 0,
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
socket.bind(SocketAddr::new(address,AddressType::BrEdr,psm))
|
||||||
|
.map_err(|e|format!("Cannot bind classic HID PSM 0x{psm:02x}: {e}. Disable BlueZ input plugin; see Bluetooth HID setup."))?;
|
||||||
|
socket.listen(2).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
struct Runtime {
|
||||||
|
adapter: Adapter,
|
||||||
|
_session: Session,
|
||||||
|
shared: Arc<Shared>,
|
||||||
|
agent: Option<agent::Agent>,
|
||||||
|
controller: controller::Controller,
|
||||||
|
listeners: [SeqPacketListener; 2],
|
||||||
|
channels: [Option<Arc<SeqPacket>>; 2],
|
||||||
|
channel_peer: Option<Address>,
|
||||||
|
partial_since: Option<Instant>,
|
||||||
|
hid: protocol::HidProtocol,
|
||||||
|
generation: u64,
|
||||||
|
old_pairable: bool,
|
||||||
|
old_powered: bool,
|
||||||
|
old_alias: String,
|
||||||
|
old_discoverable: bool,
|
||||||
|
old_discoverable_timeout: u32,
|
||||||
|
old_pairable_timeout: u32,
|
||||||
|
config: Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
recorded_peer: Option<String>,
|
||||||
|
}
|
||||||
|
impl Runtime {
|
||||||
|
async fn open(
|
||||||
|
config: &Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
let controller = controller::Controller::acquire(&config.adapter).await?;
|
||||||
|
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||||
|
let adapter = session
|
||||||
|
.adapter(&config.adapter)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let address = adapter.address().await.map_err(|e| e.to_string())?;
|
||||||
|
let mut config = config.clone();
|
||||||
|
if let Some(store) = &bonds {
|
||||||
|
bonds::clean_adapter(store.as_ref(), &adapter).await?;
|
||||||
|
if config.peer.is_none() {
|
||||||
|
config.peer = store
|
||||||
|
.list()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.find(|bond| bond.adapter == address.to_string() && !bond.pending)
|
||||||
|
.map(|bond| bond.peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Acquire BOTH channels before changing adapter state. Conflict is a startup error.
|
||||||
|
let listeners = [listen(address, 0x11)?, listen(address, 0x13)?];
|
||||||
|
let old_powered = adapter.is_powered().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_pairable = adapter.is_pairable().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_alias = adapter.alias().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_discoverable = adapter.is_discoverable().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_discoverable_timeout = adapter
|
||||||
|
.discoverable_timeout()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let old_pairable_timeout = adapter
|
||||||
|
.pairable_timeout()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let shared = Arc::new(Shared::new(
|
||||||
|
config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||||
|
));
|
||||||
|
let mut runtime = Self {
|
||||||
|
adapter,
|
||||||
|
_session: session,
|
||||||
|
shared,
|
||||||
|
agent: None,
|
||||||
|
controller,
|
||||||
|
listeners,
|
||||||
|
channels: [None, None],
|
||||||
|
channel_peer: None,
|
||||||
|
partial_since: None,
|
||||||
|
hid: Default::default(),
|
||||||
|
generation: 0,
|
||||||
|
old_pairable,
|
||||||
|
old_powered,
|
||||||
|
old_alias,
|
||||||
|
old_discoverable,
|
||||||
|
old_discoverable_timeout,
|
||||||
|
old_pairable_timeout,
|
||||||
|
config: config.clone(),
|
||||||
|
bonds,
|
||||||
|
recorded_peer: None,
|
||||||
|
};
|
||||||
|
if let Err(e) = runtime.setup().await {
|
||||||
|
let restore = runtime.close().await;
|
||||||
|
return Err(format!("{e}; cleanup: {restore:?}"));
|
||||||
|
}
|
||||||
|
Ok(runtime)
|
||||||
|
}
|
||||||
|
async fn setup(&mut self) -> Result<(), String> {
|
||||||
|
self.adapter
|
||||||
|
.set_powered(true)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_discoverable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_pairable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_alias(self.config.name.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.agent =
|
||||||
|
Some(agent::Agent::register(self.shared.clone(), self.config.adapter.clone()).await?);
|
||||||
|
self.controller.configure().await?;
|
||||||
|
self.public(None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn public(&self, seconds: Option<u32>) -> Result<(), String> {
|
||||||
|
if let Some(seconds) = seconds {
|
||||||
|
self.adapter
|
||||||
|
.set_pairable_timeout(seconds)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_discoverable_timeout(seconds)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_pairable(true)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if let Err(e) = self.adapter.set_discoverable(true).await {
|
||||||
|
let _ = self.adapter.set_pairable(false).await;
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.adapter
|
||||||
|
.set_discoverable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_pairable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn drop_channels(&mut self) {
|
||||||
|
for socket in self.channels.iter_mut().filter_map(Option::take) {
|
||||||
|
let _ = socket.shutdown(std::net::Shutdown::Both);
|
||||||
|
}
|
||||||
|
self.channel_peer = None;
|
||||||
|
self.partial_since = None;
|
||||||
|
self.hid = Default::default();
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
async fn packet(&self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||||
|
let socket = self.channels[index]
|
||||||
|
.as_ref()
|
||||||
|
.ok_or("Classic HID channel not connected")?;
|
||||||
|
let sent = tokio::time::timeout(Duration::from_millis(250), socket.send(data))
|
||||||
|
.await
|
||||||
|
.map_err(|_| "HID send timeout")?
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if sent != data.len() {
|
||||||
|
return Err("Short HID packet write".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn release(&mut self) {
|
||||||
|
for packet in self.hid.release() {
|
||||||
|
if self.channels[1].is_some() && self.packet(1, &packet).await.is_err() {
|
||||||
|
self.drop_channels();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
async fn accept(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
socket: SeqPacket,
|
||||||
|
peer: SocketAddr,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if peer.addr_type != AddressType::BrEdr {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// A host can open HID channels before the next status poll.
|
||||||
|
let paired = self
|
||||||
|
.adapter
|
||||||
|
.device(peer.addr)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_paired()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.observe_bond(&peer.addr.to_string(), paired);
|
||||||
|
if !self.shared.allowed(peer.addr) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if self.channel_peer.is_some_and(|p| p != peer.addr) || self.channels[index].is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// Kernel BT_SECURITY_MEDIUM negotiates encryption before accept completes.
|
||||||
|
if socket
|
||||||
|
.as_ref()
|
||||||
|
.security()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.key_size
|
||||||
|
< 7
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.channel_peer = Some(peer.addr);
|
||||||
|
self.channels[index] = Some(Arc::new(socket));
|
||||||
|
if self.channels.iter().all(Option::is_some) {
|
||||||
|
self.partial_since = None;
|
||||||
|
self.release().await;
|
||||||
|
} else {
|
||||||
|
self.partial_since = Some(Instant::now());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn status(&mut self) -> Result<Status, String> {
|
||||||
|
if !self.adapter.is_powered().await.map_err(|e| e.to_string())? {
|
||||||
|
return Err("Bluetooth adapter powered off".into());
|
||||||
|
}
|
||||||
|
// A daemon restart loses the SDP registration even if the adapter comes back powered.
|
||||||
|
if !self
|
||||||
|
.adapter
|
||||||
|
.uuids()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.any(|id| id.to_string() == protocol::HID_UUID)
|
||||||
|
{
|
||||||
|
return Err("Classic HID SDP registration lost; restarting Bluetooth backend".into());
|
||||||
|
}
|
||||||
|
let mut status = Status {
|
||||||
|
initialized: true,
|
||||||
|
adapter: self.config.adapter.clone(),
|
||||||
|
adapter_address: self
|
||||||
|
.adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for address in self
|
||||||
|
.adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
{
|
||||||
|
let device = self.adapter.device(address).map_err(|e| e.to_string())?;
|
||||||
|
let connected = device.is_connected().await.unwrap_or(false);
|
||||||
|
let paired = device.is_paired().await.unwrap_or(false);
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.observe_bond(&address.to_string(), paired);
|
||||||
|
if paired || connected {
|
||||||
|
status.devices.push(Device {
|
||||||
|
address: address.to_string(),
|
||||||
|
name: device.alias().await.unwrap_or_default(),
|
||||||
|
paired,
|
||||||
|
connected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.devices.sort_by(|a, b| a.address.cmp(&b.address));
|
||||||
|
let (peer, until) = {
|
||||||
|
let state = self.shared.state.lock().unwrap();
|
||||||
|
(state.peer.clone(), state.pairing_until)
|
||||||
|
};
|
||||||
|
status.peer = peer.clone();
|
||||||
|
let selected = status
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.find(|device| Some(&device.address) == peer.as_ref());
|
||||||
|
let selected_paired = selected.is_some_and(|device| device.paired);
|
||||||
|
if selected_paired && self.recorded_peer != peer {
|
||||||
|
if let (Some(store), Some(peer)) = (&self.bonds, &peer) {
|
||||||
|
store
|
||||||
|
.save(bonds::Bond {
|
||||||
|
adapter: status.adapter_address.clone(),
|
||||||
|
peer: peer.clone(),
|
||||||
|
pending: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
self.recorded_peer = Some(peer.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.connected = selected.is_some_and(|device| device.paired && device.connected);
|
||||||
|
if self.channels.iter().any(Option::is_some)
|
||||||
|
&& (!status.connected
|
||||||
|
|| self
|
||||||
|
.partial_since
|
||||||
|
.is_some_and(|time| time.elapsed() > Duration::from_secs(10)))
|
||||||
|
{
|
||||||
|
self.drop_channels();
|
||||||
|
}
|
||||||
|
if let Some(until) = until {
|
||||||
|
let completed = self
|
||||||
|
.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.pairing_completed(selected_paired);
|
||||||
|
if until <= Instant::now() || completed {
|
||||||
|
{
|
||||||
|
let mut state = self.shared.state.lock().unwrap();
|
||||||
|
state.close_pairing(
|
||||||
|
selected_paired,
|
||||||
|
self.config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||||
|
);
|
||||||
|
status.peer = state.peer.clone();
|
||||||
|
}
|
||||||
|
self.public(None).await?;
|
||||||
|
tracing::info!(reason = if completed { "bonded" } else { "expired" }, peer = ?status.peer, "Bluetooth HID pairing window closed");
|
||||||
|
} else {
|
||||||
|
status.pairing_seconds =
|
||||||
|
until.saturating_duration_since(Instant::now()).as_secs() as u32 + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.control_connected = self.channels[0].is_some();
|
||||||
|
status.interrupt_connected = self.channels[1].is_some();
|
||||||
|
status.ready = status.connected
|
||||||
|
&& status.control_connected
|
||||||
|
&& status.interrupt_connected
|
||||||
|
&& !self.hid.suspended;
|
||||||
|
status.leds = self.hid.leds;
|
||||||
|
status.generation = self.generation;
|
||||||
|
Ok(status)
|
||||||
|
}
|
||||||
|
async fn forget(&mut self) -> Result<(), String> {
|
||||||
|
self.release().await;
|
||||||
|
self.drop_channels();
|
||||||
|
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||||
|
if let Some(peer) = &peer {
|
||||||
|
let address = peer.parse().map_err(|_| "Invalid peer")?;
|
||||||
|
if self
|
||||||
|
.adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.contains(&address)
|
||||||
|
{
|
||||||
|
self.adapter
|
||||||
|
.remove_device(address)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.bonded_before_pairing
|
||||||
|
.remove(peer);
|
||||||
|
tracing::info!(%peer, "Bluetooth HID forgot computer");
|
||||||
|
}
|
||||||
|
if let Some(store) = &self.bonds {
|
||||||
|
let address = self
|
||||||
|
.adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string();
|
||||||
|
for bond in store
|
||||||
|
.list()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|b| b.adapter == address && Some(&b.peer) == peer.as_ref())
|
||||||
|
{
|
||||||
|
store.remove(bond).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.recorded_peer = None;
|
||||||
|
self.config.peer = None;
|
||||||
|
self.shared.state.lock().unwrap().peer = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn incoming(&mut self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||||
|
if index == 1 {
|
||||||
|
self.hid.output(data);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let result = self.hid.control(data);
|
||||||
|
if let Some(reply) = result.reply {
|
||||||
|
self.packet(0, &reply).await?;
|
||||||
|
}
|
||||||
|
if result.reset {
|
||||||
|
self.release().await;
|
||||||
|
}
|
||||||
|
if result.unplug {
|
||||||
|
self.forget().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn execute(&mut self, command: Command) -> Result<(), String> {
|
||||||
|
match command {
|
||||||
|
Command::Report(kind, value) => {
|
||||||
|
let peer = self.channel_peer.ok_or("Classic HID is not connected")?;
|
||||||
|
if !self.shared.allowed(peer) || self.channels.iter().any(Option::is_none) {
|
||||||
|
return Err("Classic HID channels are not ready".into());
|
||||||
|
}
|
||||||
|
let device = self.adapter.device(peer).map_err(|e| e.to_string())?;
|
||||||
|
if !device.is_paired().await.unwrap_or(false)
|
||||||
|
|| !device.is_connected().await.unwrap_or(false)
|
||||||
|
{
|
||||||
|
self.drop_channels();
|
||||||
|
return Err("Selected Bluetooth computer disconnected".into());
|
||||||
|
}
|
||||||
|
let packet = self.hid.input(kind, &value)?;
|
||||||
|
if let Err(e) = self.packet(1, &packet).await {
|
||||||
|
self.drop_channels();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Action(Action::Pair(seconds)) => {
|
||||||
|
if !(10..=300).contains(&seconds) {
|
||||||
|
return Err("Pairing window must be 10–300 seconds".into());
|
||||||
|
}
|
||||||
|
let mut bonded = HashSet::new();
|
||||||
|
for address in self
|
||||||
|
.adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
{
|
||||||
|
if self
|
||||||
|
.adapter
|
||||||
|
.device(address)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_paired()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
{
|
||||||
|
bonded.insert(address.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut state = self.shared.state.lock().unwrap();
|
||||||
|
// Preserve the original baseline when extending an open window.
|
||||||
|
if !state
|
||||||
|
.pairing_until
|
||||||
|
.is_some_and(|until| until > Instant::now())
|
||||||
|
{
|
||||||
|
state.bonded_before_pairing = bonded;
|
||||||
|
}
|
||||||
|
state.pairing_until =
|
||||||
|
Some(Instant::now() + Duration::from_secs(seconds.into()));
|
||||||
|
}
|
||||||
|
if let Err(e) = self.public(Some(seconds)).await {
|
||||||
|
self.shared.state.lock().unwrap().pairing_until = None;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
tracing::info!(seconds, "Bluetooth HID pairing window opened");
|
||||||
|
}
|
||||||
|
Command::Action(Action::ClosePairing) => {
|
||||||
|
self.public(None).await?;
|
||||||
|
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||||
|
let paired = if let Some(peer) = peer {
|
||||||
|
self.adapter
|
||||||
|
.device(peer.parse().map_err(|_| "Invalid HID peer")?)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_paired()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.close_pairing(paired, self.config.peer.clone());
|
||||||
|
tracing::info!(reason = "manual", "Bluetooth HID pairing window closed");
|
||||||
|
}
|
||||||
|
Command::Action(Action::Forget) => self.forget().await?,
|
||||||
|
Command::Action(Action::Disconnect) => {
|
||||||
|
self.release().await;
|
||||||
|
self.drop_channels();
|
||||||
|
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||||
|
if let Some(peer) = peer {
|
||||||
|
self.adapter
|
||||||
|
.device(peer.parse().map_err(|_| "Invalid peer")?)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.disconnect()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Action(Action::Reset) => self.release().await,
|
||||||
|
Command::Stop => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn close(&mut self) -> Result<(), String> {
|
||||||
|
self.release().await;
|
||||||
|
self.drop_channels();
|
||||||
|
self.agent.take();
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
let mut errors = vec![];
|
||||||
|
if let Err(e) = self.adapter.set_discoverable(false).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.controller.restore().await {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_alias(self.old_alias.clone()).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self
|
||||||
|
.adapter
|
||||||
|
.set_discoverable_timeout(self.old_discoverable_timeout)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self
|
||||||
|
.adapter
|
||||||
|
.set_pairable_timeout(self.old_pairable_timeout)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_pairable(self.old_pairable).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_discoverable(self.old_discoverable).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_powered(self.old_powered).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(errors.join("; "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn receive(socket: Option<Arc<SeqPacket>>, buffer: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
match socket {
|
||||||
|
Some(socket) => socket.recv(buffer).await,
|
||||||
|
None => std::future::pending().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn supervise(
|
||||||
|
config: Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
mut rx: mpsc::Receiver<Request>,
|
||||||
|
tx: watch::Sender<Status>,
|
||||||
|
) {
|
||||||
|
let mut generation = 0;
|
||||||
|
loop {
|
||||||
|
let mut runtime = match Runtime::open(&config, bonds.clone()).await {
|
||||||
|
Ok(runtime) => runtime,
|
||||||
|
Err(error) => {
|
||||||
|
tx.send_replace(Status {
|
||||||
|
adapter: config.adapter.clone(),
|
||||||
|
error: Some(error.clone()),
|
||||||
|
generation,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let delay = tokio::time::sleep(Duration::from_secs(5));
|
||||||
|
tokio::pin!(delay);
|
||||||
|
loop {
|
||||||
|
tokio::select! {_=&mut delay=>break,request=rx.recv()=>{
|
||||||
|
let Some(request)=request else{return;};
|
||||||
|
if matches!(request.command,Command::Stop){let _=request.result.send(Ok(()));return;}
|
||||||
|
let _=request.result.send(Err(error.clone()));
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
runtime.generation = generation;
|
||||||
|
let mut timer = tokio::time::interval(Duration::from_millis(500));
|
||||||
|
let failure = loop {
|
||||||
|
let ctl = runtime.channels[0].clone();
|
||||||
|
let intr = runtime.channels[1].clone();
|
||||||
|
let mut ctl_buffer = [0; 1024];
|
||||||
|
let mut intr_buffer = [0; 1024];
|
||||||
|
tokio::select! {
|
||||||
|
accepted=runtime.listeners[0].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(0,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||||
|
accepted=runtime.listeners[1].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(1,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||||
|
read=receive(ctl,&mut ctl_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(0,&ctl_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||||
|
read=receive(intr,&mut intr_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(1,&intr_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||||
|
_=timer.tick()=>{match runtime.status().await{Ok(status)=>{tx.send_if_modified(|old|{if *old!=status{*old=status;true}else{false}});},Err(e)=>break e}}
|
||||||
|
request=rx.recv()=>{
|
||||||
|
let Some(request)=request else{let _=runtime.close().await;return;};
|
||||||
|
if matches!(request.command,Command::Stop){let result=runtime.close().await;tx.send_replace(Status{adapter:config.adapter.clone(),..Default::default()});let _=request.result.send(result);return;}
|
||||||
|
if request.expires<=Instant::now()||request.result.is_closed(){runtime.release().await;continue;}
|
||||||
|
let action = matches!(&request.command, Command::Action(_));
|
||||||
|
let result=runtime.execute(request.command).await;
|
||||||
|
if result.is_err(){runtime.release().await;}
|
||||||
|
if action {
|
||||||
|
if let Ok(status) = runtime.status().await { tx.send_replace(status); }
|
||||||
|
}
|
||||||
|
let _=request.result.send(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let restore = runtime.close().await;
|
||||||
|
generation = runtime.generation.wrapping_add(1);
|
||||||
|
tx.send_replace(Status {
|
||||||
|
adapter: config.adapter.clone(),
|
||||||
|
error: Some(format!("{failure}; cleanup: {restore:?}")),
|
||||||
|
generation,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
drop(runtime);
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn closing_pairing_preserves_completed_bonds_but_releases_unpaired_targets() {
|
||||||
|
let shared = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state.close_pairing(true, None);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:88"));
|
||||||
|
assert!(state.pairing_until.is_none());
|
||||||
|
state.close_pairing(false, None);
|
||||||
|
assert!(state.peer.is_none());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn configuration_validation() {
|
||||||
|
let mut c = Config {
|
||||||
|
adapter: "hci0".into(),
|
||||||
|
name: "One-KVM".into(),
|
||||||
|
peer: None,
|
||||||
|
};
|
||||||
|
assert!(c.validate().is_ok());
|
||||||
|
c.adapter = "hci0;reboot".into();
|
||||||
|
assert!(c.validate().is_err());
|
||||||
|
c.adapter = "hci1".into();
|
||||||
|
c.peer = Some("bogus".into());
|
||||||
|
assert!(c.validate().is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn peer_isolation() {
|
||||||
|
let state = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||||
|
assert!(state.allowed("10:6F:D9:66:97:88".parse().unwrap()));
|
||||||
|
assert!(!state.allowed("10:6F:D9:66:97:89".parse().unwrap()));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn new_bond_without_agent_callback_completes_pairing() {
|
||||||
|
let shared = Shared::new(None);
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
let peer = "10:6F:D9:66:97:88";
|
||||||
|
state.observe_bond(peer, false);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||||
|
assert!(state.pairing_completed(true));
|
||||||
|
// A second computer cannot replace the one selected in this window.
|
||||||
|
state.observe_bond("10:6F:D9:66:97:89", true);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn old_bond_does_not_finish_a_new_window() {
|
||||||
|
let peer = "10:6F:D9:66:97:88";
|
||||||
|
let shared = Shared::new(Some(peer.into()));
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state.bonded_before_pairing.insert(peer.into());
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert!(!state.pairing_completed(true));
|
||||||
|
state.peer = None;
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn bond_observation_respects_closed_window_and_pinned_peer() {
|
||||||
|
let peer = "10:6F:D9:66:97:88";
|
||||||
|
let shared = Shared::new(None);
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
state.pairing_until = Some(Instant::now() - Duration::from_secs(1));
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state.peer = Some("10:6F:D9:66:97:89".into());
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:89"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct AdapterInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub address: String,
|
||||||
|
pub powered: bool,
|
||||||
|
}
|
||||||
|
pub async fn adapters() -> Result<Vec<AdapterInfo>, String> {
|
||||||
|
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||||
|
let mut output = vec![];
|
||||||
|
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||||
|
let adapter = session.adapter(&name).map_err(|e| e.to_string())?;
|
||||||
|
output.push(AdapterInfo {
|
||||||
|
name,
|
||||||
|
address: adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string(),
|
||||||
|
powered: adapter.is_powered().await.map_err(|e| e.to_string())?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
267
libs/bluetooth-hid/src/protocol.rs
Normal file
267
libs/bluetooth-hid/src/protocol.rs
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
//! Classic HIDP framing and report descriptor.
|
||||||
|
use crate::Report;
|
||||||
|
pub const MAP: &[u8] = &[
|
||||||
|
0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00,
|
||||||
|
0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
|
||||||
|
0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01,
|
||||||
|
0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00,
|
||||||
|
0xc0, 0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x85, 0x02, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19,
|
||||||
|
0x01, 0x29, 0x05, 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75,
|
||||||
|
0x03, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, 0x15, 0x81, 0x25, 0x7f, 0x75,
|
||||||
|
0x08, 0x95, 0x03, 0x81, 0x06, 0xc0, 0xc0, 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x85, 0x03, 0x15,
|
||||||
|
0x00, 0x26, 0xff, 0x03, 0x19, 0x00, 0x2a, 0xff, 0x03, 0x75, 0x10, 0x95, 0x01, 0x81, 0x00, 0xc0,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const HID_UUID: &str = "00001124-0000-1000-8000-00805f9b34fb";
|
||||||
|
pub fn sdp() -> String {
|
||||||
|
let hex: String = MAP.iter().map(|byte| format!("{byte:02x}")).collect();
|
||||||
|
format!(
|
||||||
|
r#"<?xml version="1.0"?>
|
||||||
|
<record>
|
||||||
|
<attribute id="0x0001"><sequence><uuid value="0x1124"/></sequence></attribute>
|
||||||
|
<attribute id="0x0004"><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0011"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0005"><sequence><uuid value="0x1002"/></sequence></attribute>
|
||||||
|
<attribute id="0x0006"><sequence><uint16 value="0x656e"/><uint16 value="0x006a"/><uint16 value="0x0100"/></sequence></attribute>
|
||||||
|
<attribute id="0x0009"><sequence><sequence><uuid value="0x1124"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x000d"><sequence><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0013"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0100"><text value="One-KVM Keyboard and Mouse"/></attribute>
|
||||||
|
<attribute id="0x0101"><text value="Classic Bluetooth HID"/></attribute>
|
||||||
|
<attribute id="0x0102"><text value="One-KVM"/></attribute>
|
||||||
|
<attribute id="0x0200"><uint16 value="0x0100"/></attribute>
|
||||||
|
<attribute id="0x0201"><uint16 value="0x0111"/></attribute>
|
||||||
|
<attribute id="0x0202"><uint8 value="0xc0"/></attribute>
|
||||||
|
<attribute id="0x0203"><uint8 value="0x00"/></attribute>
|
||||||
|
<attribute id="0x0204"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x0205"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x0206"><sequence><sequence><uint8 value="0x22"/><text encoding="hex" value="{hex}"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0207"><sequence><sequence><uint16 value="0x0409"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0209"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x020a"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x020b"><uint16 value="0x0100"/></attribute>
|
||||||
|
<attribute id="0x020c"><uint16 value="0x0c80"/></attribute>
|
||||||
|
<attribute id="0x020d"><boolean value="true"/></attribute>
|
||||||
|
<attribute id="0x020e"><boolean value="true"/></attribute>
|
||||||
|
</record>"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct HidProtocol {
|
||||||
|
pub boot: bool,
|
||||||
|
pub suspended: bool,
|
||||||
|
pub leds: u8,
|
||||||
|
pub keyboard: [u8; 8],
|
||||||
|
pub buttons: u8,
|
||||||
|
pub consumer: [u8; 2],
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct ControlResult {
|
||||||
|
pub reply: Option<Vec<u8>>,
|
||||||
|
pub unplug: bool,
|
||||||
|
pub reset: bool,
|
||||||
|
}
|
||||||
|
impl HidProtocol {
|
||||||
|
pub fn input(&mut self, kind: Report, value: &[u8]) -> Result<Vec<u8>, String> {
|
||||||
|
if value.len() != kind.len() {
|
||||||
|
return Err("Invalid HID report length".into());
|
||||||
|
}
|
||||||
|
if self.suspended {
|
||||||
|
return Err("Bluetooth HID is suspended".into());
|
||||||
|
}
|
||||||
|
let mut packet = vec![0xa1, kind.id()];
|
||||||
|
match kind {
|
||||||
|
Report::Keyboard => {
|
||||||
|
self.keyboard.copy_from_slice(value);
|
||||||
|
packet.extend_from_slice(value);
|
||||||
|
}
|
||||||
|
Report::Mouse => {
|
||||||
|
self.buttons = value[0];
|
||||||
|
packet.extend_from_slice(&value[..if self.boot { 3 } else { 4 }]);
|
||||||
|
if self.boot {
|
||||||
|
packet[2] &= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Report::Consumer if !self.boot => {
|
||||||
|
self.consumer.copy_from_slice(value);
|
||||||
|
packet.extend_from_slice(value);
|
||||||
|
}
|
||||||
|
Report::Consumer => return Err("Consumer keys unavailable in boot protocol".into()),
|
||||||
|
}
|
||||||
|
Ok(packet)
|
||||||
|
}
|
||||||
|
pub fn release(&mut self) -> Vec<Vec<u8>> {
|
||||||
|
self.keyboard = [0; 8];
|
||||||
|
self.buttons = 0;
|
||||||
|
self.consumer = [0; 2];
|
||||||
|
let mut packets = vec![
|
||||||
|
vec![0xa1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
if self.boot {
|
||||||
|
vec![0xa1, 2, 0, 0, 0]
|
||||||
|
} else {
|
||||||
|
vec![0xa1, 2, 0, 0, 0, 0]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if !self.boot {
|
||||||
|
packets.push(vec![0xa1, 3, 0, 0]);
|
||||||
|
}
|
||||||
|
packets
|
||||||
|
}
|
||||||
|
pub fn output(&mut self, data: &[u8]) -> bool {
|
||||||
|
if data.len() == 3 && data[0] == 0xa2 && data[1] == 1 {
|
||||||
|
self.leds = data[2] & 0x1f;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn control(&mut self, data: &[u8]) -> ControlResult {
|
||||||
|
let mut result = ControlResult::default();
|
||||||
|
let handshake = |code| ControlResult {
|
||||||
|
reply: Some(vec![code]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let Some(&header) = data.first() else {
|
||||||
|
return handshake(4);
|
||||||
|
};
|
||||||
|
match header {
|
||||||
|
0x13 if data.len() == 1 => {
|
||||||
|
self.suspended = true;
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x14 if data.len() == 1 => {
|
||||||
|
self.suspended = false;
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x15 if data.len() == 1 => {
|
||||||
|
result.unplug = true;
|
||||||
|
}
|
||||||
|
0x11 | 0x12 if data.len() == 1 => {
|
||||||
|
self.suspended = false;
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x60 if data.len() == 1 => {
|
||||||
|
result.reply = Some(vec![0xa0, if self.boot { 0 } else { 1 }]);
|
||||||
|
}
|
||||||
|
0x70 | 0x71 if data.len() == 1 => {
|
||||||
|
self.boot = header == 0x70;
|
||||||
|
result.reply = Some(vec![0]);
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x52 if data.len() == 3 && data[1] == 1 => {
|
||||||
|
self.leds = data[2] & 0x1f;
|
||||||
|
return handshake(0);
|
||||||
|
}
|
||||||
|
0x41 | 0x49 | 0x42 | 0x4a => {
|
||||||
|
let sized = header & 8 != 0;
|
||||||
|
if data.len() != if sized { 4 } else { 2 } {
|
||||||
|
return handshake(4);
|
||||||
|
}
|
||||||
|
let mut report = vec![0xa0 | (header & 3), data[1]];
|
||||||
|
match (header & 3, data[1]) {
|
||||||
|
(1, 1) => report.extend(self.keyboard),
|
||||||
|
(1, 2) => report.extend(if self.boot {
|
||||||
|
vec![self.buttons & 7, 0, 0]
|
||||||
|
} else {
|
||||||
|
vec![self.buttons, 0, 0, 0]
|
||||||
|
}),
|
||||||
|
(1, 3) if !self.boot => report.extend(self.consumer),
|
||||||
|
(2, 1) => report.push(self.leds),
|
||||||
|
_ => return handshake(2),
|
||||||
|
}
|
||||||
|
if sized {
|
||||||
|
report.truncate(1 + u16::from_le_bytes([data[2], data[3]]) as usize);
|
||||||
|
}
|
||||||
|
result.reply = Some(report);
|
||||||
|
}
|
||||||
|
_ => return handshake(3), // ERR_UNSUPPORTED_REQUEST
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn classic_frames_and_boot_protocol() {
|
||||||
|
let mut hid = HidProtocol::default();
|
||||||
|
assert_eq!(
|
||||||
|
hid.input(Report::Keyboard, &[2, 0, 4, 0, 0, 0, 0, 0])
|
||||||
|
.unwrap(),
|
||||||
|
[0xa1, 1, 2, 0, 4, 0, 0, 0, 0, 0]
|
||||||
|
);
|
||||||
|
assert_eq!(hid.control(&[0x60]).reply.unwrap(), [0xa0, 1]);
|
||||||
|
assert_eq!(hid.control(&[0x70]).reply.unwrap(), [0]);
|
||||||
|
assert_eq!(
|
||||||
|
hid.input(Report::Mouse, &[0x1f, 127, 128, 1]).unwrap(),
|
||||||
|
[0xa1, 2, 7, 127, 128]
|
||||||
|
);
|
||||||
|
assert!(hid.input(Report::Consumer, &[0, 0]).is_err());
|
||||||
|
assert!(hid.input(Report::Keyboard, &[0]).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn led_read_write_and_errors() {
|
||||||
|
let mut hid = HidProtocol::default();
|
||||||
|
assert!(hid.output(&[0xa2, 1, 3]));
|
||||||
|
assert_eq!(hid.leds, 3);
|
||||||
|
assert_eq!(hid.control(&[0x42, 1]).reply.unwrap(), [0xa2, 1, 3]);
|
||||||
|
assert_eq!(hid.control(&[0x52, 1, 2]).reply.unwrap(), [0]);
|
||||||
|
assert_eq!(hid.leds, 2);
|
||||||
|
assert_eq!(hid.control(&[0x41, 99]).reply.unwrap(), [2]);
|
||||||
|
assert_eq!(hid.control(&[0x41]).reply.unwrap(), [4]);
|
||||||
|
assert_eq!(hid.control(&[0x90, 0]).reply.unwrap(), [3]);
|
||||||
|
assert_eq!(
|
||||||
|
hid.control(&[0x49, 1, 3, 0]).reply.unwrap(),
|
||||||
|
[0xa1, 1, 0, 0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn suspend_reset_and_virtual_unplug() {
|
||||||
|
let mut hid = HidProtocol::default();
|
||||||
|
assert!(hid.control(&[0x13]).reset);
|
||||||
|
assert!(hid.input(Report::Mouse, &[0; 4]).is_err());
|
||||||
|
assert!(hid.control(&[0x14]).reset);
|
||||||
|
assert!(hid.input(Report::Mouse, &[0; 4]).is_ok());
|
||||||
|
assert!(hid.control(&[0x15]).unplug);
|
||||||
|
hid.keyboard[2] = 4;
|
||||||
|
hid.buttons = 1;
|
||||||
|
assert_eq!(hid.release().len(), 3);
|
||||||
|
assert_eq!(hid.keyboard, [0; 8]);
|
||||||
|
assert_eq!(hid.buttons, 0);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn sdp_describes_both_classic_channels() {
|
||||||
|
let record = sdp();
|
||||||
|
assert!(record.contains("uuid value=\"0x1124\""));
|
||||||
|
assert!(record.contains("uint16 value=\"0x0011\""));
|
||||||
|
assert!(record.contains("uint16 value=\"0x0013\""));
|
||||||
|
assert!(!record.contains("0x1812"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn descriptor_report_sizes() {
|
||||||
|
let (mut pos, mut size, mut count, mut id, mut depth) = (0, 0, 0, 0, 0);
|
||||||
|
let mut input = [0; 4];
|
||||||
|
let mut output = [0; 4];
|
||||||
|
while pos < MAP.len() {
|
||||||
|
let prefix = MAP[pos];
|
||||||
|
pos += 1;
|
||||||
|
let length = [0, 1, 2, 4][(prefix & 3) as usize];
|
||||||
|
let mut value = 0;
|
||||||
|
for i in 0..length {
|
||||||
|
value |= (MAP[pos + i] as usize) << (8 * i);
|
||||||
|
}
|
||||||
|
pos += length;
|
||||||
|
match ((prefix >> 2) & 3, prefix >> 4) {
|
||||||
|
(1, 7) => size = value,
|
||||||
|
(1, 8) => id = value,
|
||||||
|
(1, 9) => count = value,
|
||||||
|
(0, 8) => input[id] += size * count,
|
||||||
|
(0, 9) => output[id] += size * count,
|
||||||
|
(0, 10) => depth += 1,
|
||||||
|
(0, 12) => depth -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(depth, 0);
|
||||||
|
assert_eq!(input, [0, 64, 32, 16]);
|
||||||
|
assert_eq!(output, [0, 8, 0, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
libs/hwcodec/.gitignore
vendored
2
libs/hwcodec/.gitignore
vendored
@@ -8,8 +8,6 @@
|
|||||||
/ffmpeg/linux/debug
|
/ffmpeg/linux/debug
|
||||||
!/ffmpeg/mac
|
!/ffmpeg/mac
|
||||||
/ffmpeg/mac/debug
|
/ffmpeg/mac/debug
|
||||||
!/ffmpeg/android
|
|
||||||
/ffmpeg/android/debug
|
|
||||||
!/ffmpeg/ios
|
!/ffmpeg/ios
|
||||||
/ffmpeg/ios/debug
|
/ffmpeg/ios/debug
|
||||||
/input
|
/input
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ 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.72"
|
||||||
|
|||||||
@@ -41,18 +41,6 @@ Based on the information above, there are several optimizations and changes made
|
|||||||
* remove hevc_vaapi because of possible poor quality
|
* remove hevc_vaapi because of possible poor quality
|
||||||
* amf: not tested, https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/378
|
* amf: not tested, https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/378
|
||||||
|
|
||||||
### MacOS
|
|
||||||
|
|
||||||
| FFmpeg ram encode | FFmpeg ram decode |
|
|
||||||
| ------------------ | ------------------ |
|
|
||||||
| h265 only | Y |
|
|
||||||
|
|
||||||
### Android
|
|
||||||
|
|
||||||
| FFmpeg ram encode |
|
|
||||||
| ------------------ |
|
|
||||||
| Y |
|
|
||||||
|
|
||||||
## System requirements
|
## System requirements
|
||||||
|
|
||||||
* intel
|
* intel
|
||||||
@@ -76,4 +64,3 @@ Based on the information above, there are several optimizations and changes made
|
|||||||
https://docs.nvidia.com/video-technologies/video-codec-sdk/11.1/read-me/index.html
|
https://docs.nvidia.com/video-technologies/video-codec-sdk/11.1/read-me/index.html
|
||||||
|
|
||||||
https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new?ncid=em-prod-816193
|
https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new?ncid=em-prod-816193
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
// Read the current source path when running, since build artifacts may move.
|
||||||
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let cpp_dir = manifest_dir.join("cpp");
|
let cpp_dir = manifest_dir.join("cpp");
|
||||||
println!("cargo:rerun-if-changed=src");
|
println!("cargo:rerun-if-changed=src");
|
||||||
println!("cargo:rerun-if-changed={}", cpp_dir.display());
|
println!("cargo:rerun-if-changed={}", cpp_dir.display());
|
||||||
@@ -17,15 +18,16 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_common(builder: &mut Build) {
|
fn build_common(builder: &mut Build) {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||||
let common_dir = manifest_dir.join("cpp").join("common");
|
let common_dir = manifest_dir.join("cpp").join("common");
|
||||||
|
|
||||||
bindgen::builder()
|
let bindings = bindgen::builder()
|
||||||
.header(common_dir.join("common.h").to_string_lossy().to_string())
|
.header(common_dir.join("common.h").to_string_lossy().to_string())
|
||||||
.header(common_dir.join("callback.h").to_string_lossy().to_string())
|
.header(common_dir.join("callback.h").to_string_lossy().to_string())
|
||||||
.rustified_enum("*")
|
.rustified_enum(".*")
|
||||||
.parse_callbacks(Box::new(CommonCallbacks))
|
.parse_callbacks(Box::new(CommonCallbacks));
|
||||||
|
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"))
|
||||||
@@ -71,9 +73,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())
|
||||||
@@ -161,7 +163,7 @@ mod ffmpeg {
|
|||||||
println!("cargo:rustc-link-lib=static=avcodec");
|
println!("cargo:rustc-link-lib=static=avcodec");
|
||||||
println!("cargo:rustc-link-lib=static=avutil");
|
println!("cargo:rustc-link-lib=static=avutil");
|
||||||
|
|
||||||
// Link hardware acceleration dependencies (dynamic)
|
// Link hardware acceleration dependencies
|
||||||
// These vary by architecture
|
// These vary by architecture
|
||||||
if target_arch == "x86_64" {
|
if target_arch == "x86_64" {
|
||||||
// VAAPI for x86_64
|
// VAAPI for x86_64
|
||||||
@@ -170,13 +172,11 @@ mod ffmpeg {
|
|||||||
println!("cargo:rustc-link-lib=va-x11"); // Required for vaGetDisplay
|
println!("cargo:rustc-link-lib=va-x11"); // Required for vaGetDisplay
|
||||||
println!("cargo:rustc-link-lib=mfx");
|
println!("cargo:rustc-link-lib=mfx");
|
||||||
} else {
|
} else {
|
||||||
// RKMPP for ARM
|
for lib in ["rockchip_mpp", "rga"] {
|
||||||
println!("cargo:rustc-link-lib=rockchip_mpp");
|
if !lib_dir.join(format!("lib{lib}.a")).exists() {
|
||||||
let rga_static = lib_dir.join("librga.a");
|
panic!("missing static library: lib{lib}.a");
|
||||||
if rga_static.exists() {
|
}
|
||||||
println!("cargo:rustc-link-lib=static=rga");
|
println!("cargo:rustc-link-lib=static={}", lib);
|
||||||
} else {
|
|
||||||
println!("cargo:rustc-link-lib=rga");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,12 +235,6 @@ mod ffmpeg {
|
|||||||
// For static linking, link FFmpeg libs statically, others dynamically
|
// For static linking, link FFmpeg libs statically, others dynamically
|
||||||
if lib_name.starts_with("av") || lib_name == "swresample" {
|
if lib_name.starts_with("av") || lib_name == "swresample" {
|
||||||
println!("cargo:rustc-link-lib=static={}", lib_name);
|
println!("cargo:rustc-link-lib=static={}", lib_name);
|
||||||
} else if lib_name == "rga"
|
|
||||||
&& link_paths
|
|
||||||
.iter()
|
|
||||||
.any(|path| Path::new(path).join("librga.a").exists())
|
|
||||||
{
|
|
||||||
println!("cargo:rustc-link-lib=static=rga");
|
|
||||||
} else {
|
} else {
|
||||||
// Runtime libraries (va, drm, etc.) must be dynamic
|
// Runtime libraries (va, drm, etc.) must be dynamic
|
||||||
println!("cargo:rustc-link-lib={}", lib_name);
|
println!("cargo:rustc-link-lib={}", lib_name);
|
||||||
@@ -335,7 +329,10 @@ mod ffmpeg {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("cargo:warning=Windows QSV support library not found in {}", lib_dir.display());
|
println!(
|
||||||
|
"cargo:warning=Windows QSV support library not found in {}",
|
||||||
|
lib_dir.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn link_os() {
|
fn link_os() {
|
||||||
@@ -371,14 +368,13 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ffmpeg_ffi() {
|
fn ffmpeg_ffi() {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("common");
|
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("common");
|
||||||
let ffi_header_path = ffmpeg_ram_dir.join("ffmpeg_ffi.h");
|
let ffi_header_path = ffmpeg_ram_dir.join("ffmpeg_ffi.h");
|
||||||
println!("cargo:rerun-if-changed={}", ffi_header_path.display());
|
println!("cargo:rerun-if-changed={}", ffi_header_path.display());
|
||||||
let ffi_header = ffi_header_path.to_string_lossy().to_string();
|
let ffi_header = ffi_header_path.to_string_lossy().to_string();
|
||||||
bindgen::builder()
|
let bindings = bindgen::builder().header(ffi_header).rustified_enum(".*");
|
||||||
.header(ffi_header)
|
bindings
|
||||||
.rustified_enum("*")
|
|
||||||
.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"))
|
||||||
@@ -386,15 +382,14 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_ffmpeg_ram(builder: &mut Build) {
|
fn build_ffmpeg_ram(builder: &mut Build) {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("ffmpeg_ram");
|
let ffmpeg_ram_dir = manifest_dir.join("cpp").join("ffmpeg_ram");
|
||||||
let ffi_header = ffmpeg_ram_dir
|
let ffi_header = ffmpeg_ram_dir
|
||||||
.join("ffmpeg_ram_ffi.h")
|
.join("ffmpeg_ram_ffi.h")
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
bindgen::builder()
|
let bindings = bindgen::builder().header(ffi_header).rustified_enum(".*");
|
||||||
.header(ffi_header)
|
bindings
|
||||||
.rustified_enum("*")
|
|
||||||
.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"))
|
||||||
@@ -409,6 +404,9 @@ mod ffmpeg {
|
|||||||
|| std::env::var_os("CARGO_FEATURE_RKMPP").is_some();
|
|| std::env::var_os("CARGO_FEATURE_RKMPP").is_some();
|
||||||
if enable_rkmpp {
|
if enable_rkmpp {
|
||||||
builder.file(ffmpeg_ram_dir.join("ffmpeg_ram_decode.cpp"));
|
builder.file(ffmpeg_ram_dir.join("ffmpeg_ram_decode.cpp"));
|
||||||
|
if enable_rkmpp {
|
||||||
|
builder.define("ONE_KVM_FFMPEG_RKMPP", None);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"cargo:info=Skipping ffmpeg_ram_decode.cpp (RKMPP) for arch {}",
|
"cargo:info=Skipping ffmpeg_ram_decode.cpp (RKMPP) for arch {}",
|
||||||
@@ -423,7 +421,7 @@ mod ffmpeg {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let capture_header = manifest_dir
|
let capture_header = manifest_dir
|
||||||
.join("cpp")
|
.join("cpp")
|
||||||
.join("ffmpeg_capture_ffi.h")
|
.join("ffmpeg_capture_ffi.h")
|
||||||
@@ -431,7 +429,7 @@ mod ffmpeg {
|
|||||||
.to_string();
|
.to_string();
|
||||||
bindgen::builder()
|
bindgen::builder()
|
||||||
.header(capture_header)
|
.header(capture_header)
|
||||||
.rustified_enum("*")
|
.rustified_enum(".*")
|
||||||
.generate()
|
.generate()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write_to_file(
|
.write_to_file(
|
||||||
@@ -446,7 +444,7 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_ffmpeg_hw(builder: &mut Build) {
|
fn build_ffmpeg_hw(builder: &mut Build) {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
|
let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
|
||||||
let ffi_header = ffmpeg_hw_dir
|
let ffi_header = ffmpeg_hw_dir
|
||||||
.join("ffmpeg_hw_ffi.h")
|
.join("ffmpeg_hw_ffi.h")
|
||||||
@@ -454,7 +452,7 @@ 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"))
|
||||||
@@ -490,6 +488,7 @@ mod ffmpeg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.file(ffmpeg_hw_dir.join("ffmpeg_hw_mjpeg_h26x.cpp"));
|
builder.file(ffmpeg_hw_dir.join("ffmpeg_hw_mjpeg_h26x.cpp"));
|
||||||
|
builder.file(ffmpeg_hw_dir.join("rkmpp_dmabuf.cpp"));
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"cargo:info=Skipping ffmpeg_hw_mjpeg_h26x.cpp (RKMPP) for arch {}",
|
"cargo:info=Skipping ffmpeg_hw_mjpeg_h26x.cpp (RKMPP) for arch {}",
|
||||||
|
|||||||
@@ -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,152 @@ 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_qcom_platform = [&]() -> bool {
|
||||||
|
const char *platform_hints[] = {
|
||||||
|
"qcom",
|
||||||
|
"qualcomm",
|
||||||
|
"venus",
|
||||||
|
"iris",
|
||||||
|
"sc7280",
|
||||||
|
"qcm6490",
|
||||||
|
"qcs6490",
|
||||||
|
};
|
||||||
|
|
||||||
|
const char *platform_files[] = {
|
||||||
|
"/proc/device-tree/compatible",
|
||||||
|
"/proc/device-tree/model",
|
||||||
|
"/sys/firmware/devicetree/base/compatible",
|
||||||
|
"/sys/firmware/devicetree/base/model",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(platform_files) / sizeof(platform_files[0]); i++) {
|
||||||
|
std::string value;
|
||||||
|
if (read_text_file(platform_files[i], &value) &&
|
||||||
|
contains_any(to_lower(value), platform_hints,
|
||||||
|
sizeof(platform_hints) / sizeof(platform_hints[0]))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *video_nodes[] = {
|
||||||
|
"video0",
|
||||||
|
"video1",
|
||||||
|
"video2",
|
||||||
|
"video3",
|
||||||
|
"video10",
|
||||||
|
"video11",
|
||||||
|
"video32",
|
||||||
|
};
|
||||||
|
const char *video_hints[] = {
|
||||||
|
"qcom-iris",
|
||||||
|
"qcom,",
|
||||||
|
"venus",
|
||||||
|
"iris",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(video_nodes) / sizeof(video_nodes[0]); i++) {
|
||||||
|
std::string name;
|
||||||
|
std::string modalias;
|
||||||
|
const std::string base = std::string("/sys/class/video4linux/") + video_nodes[i];
|
||||||
|
if (read_text_file((base + "/name").c_str(), &name) &&
|
||||||
|
contains_any(to_lower(name), video_hints, sizeof(video_hints) / sizeof(video_hints[0]))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (read_text_file((base + "/device/modalias").c_str(), &modalias) &&
|
||||||
|
contains_any(to_lower(modalias), video_hints,
|
||||||
|
sizeof(video_hints) / sizeof(video_hints[0]))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto is_amlogic_platform = [&]() -> bool {
|
||||||
|
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 qcom_platform = is_qcom_platform();
|
||||||
|
const bool amlogic_platform = !qcom_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 +301,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) {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ extern "C" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <fstream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -24,7 +27,7 @@ bool is_software_h264(const std::string &name) {
|
|||||||
// Exclude all hardware encoders
|
// Exclude all hardware encoders
|
||||||
static const char* hw_suffixes[] = {
|
static const char* hw_suffixes[] = {
|
||||||
"nvenc", "amf", "qsv", "vaapi", "rkmpp",
|
"nvenc", "amf", "qsv", "vaapi", "rkmpp",
|
||||||
"v4l2m2m", "videotoolbox", "mediacodec", "_mf"
|
"v4l2m2m", "videotoolbox", "_mf"
|
||||||
};
|
};
|
||||||
for (const auto& suffix : hw_suffixes) {
|
for (const auto& suffix : hw_suffixes) {
|
||||||
if (name.find(suffix) != std::string::npos) return false;
|
if (name.find(suffix) != std::string::npos) return false;
|
||||||
@@ -37,7 +40,7 @@ bool is_software_hevc(const std::string &name) {
|
|||||||
if (name != "hevc" && name != "libx265") return false;
|
if (name != "hevc" && name != "libx265") return false;
|
||||||
static const char* hw_suffixes[] = {
|
static const char* hw_suffixes[] = {
|
||||||
"nvenc", "amf", "qsv", "vaapi", "rkmpp",
|
"nvenc", "amf", "qsv", "vaapi", "rkmpp",
|
||||||
"v4l2m2m", "videotoolbox", "mediacodec", "_mf"
|
"v4l2m2m", "videotoolbox", "_mf"
|
||||||
};
|
};
|
||||||
for (const auto& suffix : hw_suffixes) {
|
for (const auto& suffix : hw_suffixes) {
|
||||||
if (name.find(suffix) != std::string::npos) return false;
|
if (name.find(suffix) != std::string::npos) return false;
|
||||||
@@ -45,17 +48,46 @@ bool is_software_hevc(const std::string &name) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool is_qcom_iris_driver() {
|
||||||
|
const char *driver_path = "/sys/class/video4linux/video1/name";
|
||||||
|
std::ifstream file(driver_path);
|
||||||
|
if (!file.is_open()) return false;
|
||||||
|
|
||||||
|
std::string value;
|
||||||
|
std::getline(file, value, '\0');
|
||||||
|
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
return value.find("qcom-iris") != std::string::npos ||
|
||||||
|
value.find("iris-encoder") != std::string::npos ||
|
||||||
|
value.find("iris") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
namespace util_encode {
|
namespace util_encode {
|
||||||
|
|
||||||
|
bool is_qcom_iris_platform() {
|
||||||
|
return is_qcom_iris_driver();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool supports_forced_keyframe(const std::string &name) {
|
||||||
|
if (name.find("v4l2m2m") != std::string::npos && is_qcom_iris_platform()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
||||||
int gop, int fps, int thread_count) {
|
int gop, int fps, int thread_count) {
|
||||||
c->has_b_frames = 0;
|
c->has_b_frames = 0;
|
||||||
c->max_b_frames = 0;
|
c->max_b_frames = 0;
|
||||||
if (gop > 0 && gop < std::numeric_limits<int16_t>::max()) {
|
const bool qcom_iris_v4l2 =
|
||||||
c->gop_size = gop;
|
name.find("v4l2m2m") != std::string::npos && is_qcom_iris_platform();
|
||||||
c->keyint_min = gop; // Match keyint_min to gop for consistent keyframe interval
|
const int effective_gop = qcom_iris_v4l2 ? std::max(5, fps / 3) : gop;
|
||||||
|
if (effective_gop > 0 && effective_gop < std::numeric_limits<int16_t>::max()) {
|
||||||
|
c->gop_size = effective_gop;
|
||||||
|
c->keyint_min = effective_gop; // Match keyint_min to gop for consistent keyframe interval
|
||||||
} else if (name.find("vaapi") != std::string::npos) {
|
} else if (name.find("vaapi") != std::string::npos) {
|
||||||
c->gop_size = fps > 0 ? fps : 30; // Default to 1 second keyframe interval
|
c->gop_size = fps > 0 ? fps : 30; // Default to 1 second keyframe interval
|
||||||
c->keyint_min = c->gop_size;
|
c->keyint_min = c->gop_size;
|
||||||
@@ -120,7 +152,8 @@ bool set_lantency_free(void *priv_data, const std::string &name) {
|
|||||||
}
|
}
|
||||||
if (name.find("amf") != std::string::npos) {
|
if (name.find("amf") != std::string::npos) {
|
||||||
if ((ret = av_opt_set(priv_data, "query_timeout", "1000", 0)) < 0) {
|
if ((ret = av_opt_set(priv_data, "query_timeout", "1000", 0)) < 0) {
|
||||||
LOG_WARN(std::string("amf query_timeout option is unavailable, ret = ") + av_err2str(ret));
|
LOG_DEBUG(std::string("amf query_timeout option is unavailable, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (name.find("qsv") != std::string::npos) {
|
if (name.find("qsv") != std::string::npos) {
|
||||||
@@ -139,7 +172,8 @@ bool set_lantency_free(void *priv_data, const std::string &name) {
|
|||||||
if (name.find("rkmpp") != std::string::npos) {
|
if (name.find("rkmpp") != std::string::npos) {
|
||||||
// Set async_depth to 1 for minimal buffering (0 = synchronous, higher = more buffering)
|
// Set async_depth to 1 for minimal buffering (0 = synchronous, higher = more buffering)
|
||||||
if ((ret = av_opt_set(priv_data, "async_depth", "1", 0)) < 0) {
|
if ((ret = av_opt_set(priv_data, "async_depth", "1", 0)) < 0) {
|
||||||
LOG_WARN(std::string("rkmpp set async_depth failed, ret = ") + av_err2str(ret));
|
LOG_DEBUG(std::string("rkmpp async_depth option is unavailable, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
// Not fatal - older FFmpeg versions may not support this option
|
// Not fatal - older FFmpeg versions may not support this option
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,11 +181,14 @@ bool set_lantency_free(void *priv_data, const std::string &name) {
|
|||||||
if (name.find("v4l2m2m") != std::string::npos) {
|
if (name.find("v4l2m2m") != std::string::npos) {
|
||||||
// Minimize number of output buffers for lower latency
|
// Minimize number of output buffers for lower latency
|
||||||
if ((ret = av_opt_set_int(priv_data, "num_output_buffers", 4, 0)) < 0) {
|
if ((ret = av_opt_set_int(priv_data, "num_output_buffers", 4, 0)) < 0) {
|
||||||
LOG_WARN(std::string("v4l2m2m set num_output_buffers failed, ret = ") + av_err2str(ret));
|
LOG_DEBUG(std::string("v4l2m2m num_output_buffers option is unavailable, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
// Not fatal
|
// Not fatal
|
||||||
}
|
}
|
||||||
if ((ret = av_opt_set_int(priv_data, "num_capture_buffers", 4, 0)) < 0) {
|
const int capture_buffers = is_qcom_iris_driver() ? 12 : 8;
|
||||||
LOG_WARN(std::string("v4l2m2m set num_capture_buffers failed, ret = ") + av_err2str(ret));
|
if ((ret = av_opt_set_int(priv_data, "num_capture_buffers", capture_buffers, 0)) < 0) {
|
||||||
|
LOG_DEBUG(std::string("v4l2m2m num_capture_buffers option is unavailable, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
// Not fatal
|
// Not fatal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,23 +342,6 @@ bool set_quality(void *priv_data, const std::string &name, int quality) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (name.find("mediacodec") != std::string::npos) {
|
|
||||||
if (name.find("h264") != std::string::npos) {
|
|
||||||
if ((ret = av_opt_set(priv_data, "level", "5.1", 0)) < 0) {
|
|
||||||
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;
|
||||||
@@ -377,6 +397,33 @@ struct CodecOptions {
|
|||||||
|
|
||||||
bool set_rate_control(AVCodecContext *c, const std::string &name, int rc,
|
bool set_rate_control(AVCodecContext *c, const std::string &name, int rc,
|
||||||
int q) {
|
int q) {
|
||||||
|
// Remote-desktop content is usually sparse. VBR avoids padding static
|
||||||
|
// frames up to the target bitrate while allowing short bursts for screen
|
||||||
|
// changes. Keep those bursts bounded at twice the target bitrate.
|
||||||
|
if (rc == RC_VBR && c->bit_rate > 0) {
|
||||||
|
c->rc_max_rate = c->bit_rate * 2;
|
||||||
|
c->rc_buffer_size = c->rc_max_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.find("vaapi") != std::string::npos && rc == RC_CQ) {
|
||||||
|
// Used only after the normal bitrate-based VAAPI initialization fails.
|
||||||
|
// Some drivers, including Intel iHD on Jasper Lake, expose CQP as their
|
||||||
|
// only compatible rate-control mode.
|
||||||
|
c->bit_rate = 0;
|
||||||
|
c->rc_min_rate = 0;
|
||||||
|
c->rc_max_rate = 0;
|
||||||
|
c->rc_buffer_size = 0;
|
||||||
|
c->rc_initial_buffer_occupancy = 0;
|
||||||
|
|
||||||
|
const int qp = q > 0 ? q : 23;
|
||||||
|
const int ret = av_opt_set_int(c->priv_data, "qp", qp, 0);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOG_ERROR(std::string("vaapi set qp failed, ret = ") +
|
||||||
|
av_err2str(ret));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (name.find("qsv") != std::string::npos) {
|
if (name.find("qsv") != std::string::npos) {
|
||||||
// https://github.com/LizardByte/Sunshine/blob/3e47cd3cc8fd37a7a88be82444ff4f3c0022856b/src/video.cpp#L1635
|
// https://github.com/LizardByte/Sunshine/blob/3e47cd3cc8fd37a7a88be82444ff4f3c0022856b/src/video.cpp#L1635
|
||||||
c->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
|
c->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
|
||||||
@@ -384,9 +431,6 @@ bool set_rate_control(AVCodecContext *c, const std::string &name, int rc,
|
|||||||
std::vector<CodecOptions> codecs = {
|
std::vector<CodecOptions> codecs = {
|
||||||
{"nvenc", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr"}}},
|
{"nvenc", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr"}}},
|
||||||
{"amf", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr_latency"}}},
|
{"amf", "rc", {{RC_CBR, "cbr"}, {RC_VBR, "vbr_latency"}}},
|
||||||
{"mediacodec",
|
|
||||||
"bitrate_mode",
|
|
||||||
{{RC_CBR, "cbr"}, {RC_VBR, "vbr"}, {RC_CQ, "cq"}}},
|
|
||||||
// {"videotoolbox", "constant_bit_rate", {{RC_CBR, "1"}}},
|
// {"videotoolbox", "constant_bit_rate", {{RC_CBR, "1"}}},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -401,13 +445,6 @@ bool set_rate_control(AVCodecContext *c, const std::string &name, int rc,
|
|||||||
it->second + " failed, ret = " + av_err2str(ret));
|
it->second + " failed, ret = " + av_err2str(ret));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (name.find("mediacodec") != std::string::npos) {
|
|
||||||
if (rc == RC_CQ) {
|
|
||||||
if (q >= 0 && q <= 51) {
|
|
||||||
c->global_quality = q;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -466,6 +503,10 @@ bool set_others(void *priv_data, const std::string &name) {
|
|||||||
bool change_bit_rate(AVCodecContext *c, const std::string &name, int kbs) {
|
bool change_bit_rate(AVCodecContext *c, const std::string &name, int kbs) {
|
||||||
if (kbs > 0) {
|
if (kbs > 0) {
|
||||||
c->bit_rate = kbs * 1000;
|
c->bit_rate = kbs * 1000;
|
||||||
|
if (c->rc_max_rate > 0 && name.find("qsv") == std::string::npos) {
|
||||||
|
c->rc_max_rate = c->bit_rate * 2;
|
||||||
|
c->rc_buffer_size = c->rc_max_rate;
|
||||||
|
}
|
||||||
if (name.find("qsv") != std::string::npos) {
|
if (name.find("qsv") != std::string::npos) {
|
||||||
c->rc_max_rate = c->bit_rate;
|
c->rc_max_rate = c->bit_rate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ extern "C" {
|
|||||||
|
|
||||||
namespace util_encode {
|
namespace util_encode {
|
||||||
|
|
||||||
|
bool is_qcom_iris_platform();
|
||||||
|
bool supports_forced_keyframe(const std::string &name);
|
||||||
|
|
||||||
void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
|
||||||
int gop, int fps, int thread_count);
|
int gop, int fps, int thread_count);
|
||||||
bool set_lantency_free(void *priv_data, const std::string &name);
|
bool set_lantency_free(void *priv_data, const std::string &name);
|
||||||
|
|||||||
38
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dma_jpeg.h
Normal file
38
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dma_jpeg.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Validate bounded JPEG headers before giving hardware a fixed-size output
|
||||||
|
// buffer. Only baseline 8-bit JPEG is accepted; other streams use copy fallback.
|
||||||
|
// No scan-data traversal or full-packet copy is needed.
|
||||||
|
inline bool rkmpp_dma_jpeg_header(const uint8_t *data, size_t size, int width, int height) {
|
||||||
|
if (!data || size < 4 || data[0] != 0xff || data[1] != 0xd8) return false;
|
||||||
|
size_t pos = 2;
|
||||||
|
bool sof = false;
|
||||||
|
while (pos < size) {
|
||||||
|
if (data[pos++] != 0xff) return false;
|
||||||
|
while (pos < size && data[pos] == 0xff) ++pos;
|
||||||
|
if (pos == size) return false;
|
||||||
|
const unsigned marker = data[pos++];
|
||||||
|
if (!marker || marker == 0xd8 || marker == 0xd9 || marker == 1 ||
|
||||||
|
(marker >= 0xd0 && marker <= 0xd7)) return false;
|
||||||
|
if (size - pos < 2) return false;
|
||||||
|
const size_t length = (size_t(data[pos]) << 8) | data[pos + 1];
|
||||||
|
if (length < 2 || length > size - pos) return false;
|
||||||
|
if (marker == 0xc0) {
|
||||||
|
if (sof || length < 8 || data[pos + 2] != 8) return false;
|
||||||
|
const unsigned h = (unsigned(data[pos + 3]) << 8) | data[pos + 4];
|
||||||
|
const unsigned w = (unsigned(data[pos + 5]) << 8) | data[pos + 6];
|
||||||
|
const unsigned components = data[pos + 7];
|
||||||
|
if (w != unsigned(width) || h != unsigned(height) ||
|
||||||
|
(components != 1 && components != 3) || length != 8 + 3 * components) return false;
|
||||||
|
sof = true;
|
||||||
|
} else if (marker >= 0xc0 && marker <= 0xcf && marker != 0xc4 && marker != 0xcc) {
|
||||||
|
return false; // Progressive, lossless, extended or differential SOF.
|
||||||
|
} else if (marker == 0xda) {
|
||||||
|
return sof && length >= 6 && size - pos > length;
|
||||||
|
}
|
||||||
|
pos += length;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
329
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dmabuf.cpp
Normal file
329
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dmabuf.cpp
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
#include "rkmpp_dmabuf_ffi.h"
|
||||||
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <limits>
|
||||||
|
#include <new>
|
||||||
|
#include "rkmpp_dma_jpeg.h"
|
||||||
|
|
||||||
|
// Native MPP is already linked by the ARM FFmpeg/RKMPP build. Keep this
|
||||||
|
// optional for toolchains which only supply the FFmpeg headers.
|
||||||
|
#if defined(__linux__) && __has_include(<rockchip/rk_mpi.h>)
|
||||||
|
#define HAVE_MPP_DMA 1
|
||||||
|
#include <cerrno>
|
||||||
|
#include <linux/dma-buf.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
extern "C" {
|
||||||
|
#include <rockchip/rk_mpi.h>
|
||||||
|
#include <rockchip/mpp_buffer.h>
|
||||||
|
#include <rockchip/mpp_frame.h>
|
||||||
|
#include <rockchip/mpp_packet.h>
|
||||||
|
#include <rockchip/mpp_task.h>
|
||||||
|
#include <rockchip/rk_venc_cfg.h>
|
||||||
|
#include <rockchip/rk_venc_rc.h>
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static thread_local char dma_error[192] = {};
|
||||||
|
static int fail(const char *operation, int code) {
|
||||||
|
std::snprintf(dma_error, sizeof(dma_error), "%s (ret=%d)", operation, code);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_MPP_DMA
|
||||||
|
struct RkmppDmaEncoder {
|
||||||
|
MppCtx ctx = nullptr;
|
||||||
|
MppApi *api = nullptr;
|
||||||
|
MppEncCfg cfg = nullptr;
|
||||||
|
MppPacket packet = nullptr;
|
||||||
|
std::array<MppBuffer, 16> buffers{};
|
||||||
|
std::array<size_t, 16> capacities{};
|
||||||
|
size_t count = 0;
|
||||||
|
size_t minimum = 0;
|
||||||
|
int width = 0, height = 0, stride = 0;
|
||||||
|
MppFrameFormat format = MPP_FMT_YUV420SP;
|
||||||
|
bool jpeg = false;
|
||||||
|
MppCtx decoder = nullptr;
|
||||||
|
MppApi *dec_api = nullptr;
|
||||||
|
MppBufferGroup decoded_group = nullptr;
|
||||||
|
MppBuffer decoded_buffer = nullptr;
|
||||||
|
MppFrame decoded_frame = nullptr;
|
||||||
|
MppPacket input_packet = nullptr;
|
||||||
|
bool decoded_layout_set = false;
|
||||||
|
int decoded_stride = 0, decoded_vstride = 0;
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
if (packet) mpp_packet_deinit(&packet);
|
||||||
|
if (ctx) {
|
||||||
|
// A timeout must not expose still-in-use input to V4L2 QBUF.
|
||||||
|
api->reset(ctx);
|
||||||
|
mpp_destroy(ctx);
|
||||||
|
ctx = nullptr;
|
||||||
|
}
|
||||||
|
// On any decoder failure, end hardware access before releasing the
|
||||||
|
// input packet, exported buffers, or allowing the caller's QBUF.
|
||||||
|
if (decoder) {
|
||||||
|
dec_api->reset(decoder);
|
||||||
|
mpp_destroy(decoder);
|
||||||
|
decoder = nullptr;
|
||||||
|
}
|
||||||
|
if (input_packet) mpp_packet_deinit(&input_packet);
|
||||||
|
if (decoded_frame) mpp_frame_deinit(&decoded_frame);
|
||||||
|
if (decoded_buffer) { mpp_buffer_put(decoded_buffer); decoded_buffer = nullptr; }
|
||||||
|
if (decoded_group) { mpp_buffer_group_put(decoded_group); decoded_group = nullptr; }
|
||||||
|
for (auto &buffer : buffers) {
|
||||||
|
if (buffer) { mpp_buffer_put(buffer); buffer = nullptr; }
|
||||||
|
}
|
||||||
|
if (cfg) { mpp_enc_cfg_deinit(cfg); cfg = nullptr; }
|
||||||
|
}
|
||||||
|
~RkmppDmaEncoder() { close(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool set_cfg(RkmppDmaEncoder *e, const char *key, int value) {
|
||||||
|
int ret = mpp_enc_cfg_set_s32(e->cfg, key, value);
|
||||||
|
if (ret) fail(key, ret);
|
||||||
|
return ret == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int rkmpp_dma_reconfigure(RkmppDmaEncoder *e, int kbps, int gop) {
|
||||||
|
if (!e || !e->ctx || kbps <= 0 || kbps > 1000000 || gop <= 0)
|
||||||
|
return fail("invalid DMA encoder configuration", -1);
|
||||||
|
const int bps = kbps * 1000;
|
||||||
|
if (!set_cfg(e, "rc:bps_target", bps) ||
|
||||||
|
!set_cfg(e, "rc:bps_max", bps + bps / 16) ||
|
||||||
|
!set_cfg(e, "rc:bps_min", bps - bps / 16) ||
|
||||||
|
!set_cfg(e, "rc:gop", gop)) return -1;
|
||||||
|
int ret = e->api->control(e->ctx, MPP_ENC_SET_CFG, e->cfg);
|
||||||
|
return ret ? fail("MPP_ENC_SET_CFG", ret) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" RkmppDmaEncoder *rkmpp_dma_new(
|
||||||
|
int width, int height, int stride, int format, int codec, int fps,
|
||||||
|
int kbps, int gop, const int *fds, const size_t *sizes, size_t count) {
|
||||||
|
if (width <= 0 || height <= 0 || width > 8192 || height > 8192 ||
|
||||||
|
(width & 1) || (height & 1) || format < 0 || format > 4 ||
|
||||||
|
codec < 0 || codec > 1 || fps <= 0 || fps > 240 ||
|
||||||
|
(format != 4 && stride < width * (format == 1 || format == 3 ? 3 : format == 2 ? 2 : 1)) ||
|
||||||
|
(format == 2 && stride % 16 != 0) || !fds || !sizes || !count || count > 16) {
|
||||||
|
fail("invalid DMA frame layout", -1); return nullptr;
|
||||||
|
}
|
||||||
|
auto *e = new (std::nothrow) RkmppDmaEncoder;
|
||||||
|
if (!e) { fail("allocate DMA encoder", -1); return nullptr; }
|
||||||
|
e->width = width; e->height = height; e->stride = stride; e->count = count;
|
||||||
|
e->jpeg = format == 4;
|
||||||
|
if (e->jpeg) e->stride = stride = (width + 15) & ~15;
|
||||||
|
const int vstride = e->jpeg ? (height + 15) & ~15 : height;
|
||||||
|
e->format = format == 3 ? MPP_FMT_RGB888 : format == 2 ? MPP_FMT_YUV422_YUYV : format == 1 ? MPP_FMT_BGR888 : MPP_FMT_YUV420SP;
|
||||||
|
auto abort_init = [e](const char *op, int ret) -> RkmppDmaEncoder * {
|
||||||
|
fail(op, ret); delete e; return nullptr;
|
||||||
|
};
|
||||||
|
int ret = mpp_create(&e->ctx, &e->api);
|
||||||
|
if (ret) return abort_init("mpp_create", ret);
|
||||||
|
RK_S64 timeout = 2000;
|
||||||
|
ret = e->api->control(e->ctx, MPP_SET_OUTPUT_TIMEOUT, &timeout);
|
||||||
|
if (ret) return abort_init("MPP_SET_OUTPUT_TIMEOUT", ret);
|
||||||
|
ret = e->api->control(e->ctx, MPP_SET_INPUT_TIMEOUT, &timeout);
|
||||||
|
if (ret) return abort_init("MPP_SET_INPUT_TIMEOUT", ret);
|
||||||
|
ret = mpp_init(e->ctx, MPP_CTX_ENC, codec ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC);
|
||||||
|
if (ret) return abort_init("mpp_init", ret);
|
||||||
|
ret = mpp_enc_cfg_init(&e->cfg);
|
||||||
|
if (ret) return abort_init("mpp_enc_cfg_init", ret);
|
||||||
|
ret = e->api->control(e->ctx, MPP_ENC_GET_CFG, e->cfg);
|
||||||
|
if (ret) return abort_init("MPP_ENC_GET_CFG", ret);
|
||||||
|
if (!set_cfg(e, "prep:width", width) || !set_cfg(e, "prep:height", height) ||
|
||||||
|
!set_cfg(e, "prep:hor_stride", stride) || !set_cfg(e, "prep:ver_stride", vstride) ||
|
||||||
|
!set_cfg(e, "prep:format", e->format) || !set_cfg(e, "rc:mode", MPP_ENC_RC_MODE_CBR) ||
|
||||||
|
!set_cfg(e, "rc:fps_in_flex", 0) || !set_cfg(e, "rc:fps_in_num", fps) ||
|
||||||
|
!set_cfg(e, "rc:fps_in_denorm", 1) || !set_cfg(e, "rc:fps_out_flex", 0) ||
|
||||||
|
!set_cfg(e, "rc:fps_out_num", fps) || !set_cfg(e, "rc:fps_out_denorm", 1) ||
|
||||||
|
!set_cfg(e, "codec:type", codec ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC)) {
|
||||||
|
delete e; return nullptr;
|
||||||
|
}
|
||||||
|
// Match the browser-friendly baseline profile used by the existing RKMPP
|
||||||
|
// byte encoder, rather than inheriting MPP's High-profile default.
|
||||||
|
const int level = int64_t(width) * height * fps <= int64_t(1920) * 1080 * 60 ? 42 : 52;
|
||||||
|
if (!codec && (!set_cfg(e, "h264:profile", 66) || !set_cfg(e, "h264:level", level) ||
|
||||||
|
!set_cfg(e, "h264:cabac_en", 0) || !set_cfg(e, "h264:trans8x8", 0))) {
|
||||||
|
delete e; return nullptr;
|
||||||
|
}
|
||||||
|
if (rkmpp_dma_reconfigure(e, kbps, gop)) { delete e; return nullptr; }
|
||||||
|
MppEncHeaderMode mode = MPP_ENC_HEADER_MODE_EACH_IDR;
|
||||||
|
ret = e->api->control(e->ctx, MPP_ENC_SET_HEADER_MODE, &mode);
|
||||||
|
if (ret) return abort_init("MPP_ENC_SET_HEADER_MODE", ret);
|
||||||
|
// Reject arithmetic overflow even on 32-bit ARM; stride is supplied by a driver.
|
||||||
|
if (size_t(stride) > std::numeric_limits<size_t>::max() / size_t(height))
|
||||||
|
return abort_init("DMA buffer size overflow", -1);
|
||||||
|
size_t minimum = size_t(stride) * height;
|
||||||
|
if (format == 0) {
|
||||||
|
if (minimum > std::numeric_limits<size_t>::max() / 3)
|
||||||
|
return abort_init("DMA buffer size overflow", -1);
|
||||||
|
minimum = minimum * 3 / 2;
|
||||||
|
}
|
||||||
|
if (e->jpeg) minimum = 68; // SOI/payload plus bounded hardware read headroom.
|
||||||
|
e->minimum = minimum;
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
if (fds[i] < 0 || sizes[i] < minimum) return abort_init("short DMA buffer", -1);
|
||||||
|
MppBufferInfo info{};
|
||||||
|
info.type = MPP_BUFFER_TYPE_EXT_DMA; info.fd = fds[i];
|
||||||
|
info.size = sizes[i]; info.index = static_cast<int>(i);
|
||||||
|
ret = mpp_buffer_import(&e->buffers[i], &info);
|
||||||
|
if (ret) return abort_init("mpp_buffer_import", ret);
|
||||||
|
e->capacities[i] = sizes[i];
|
||||||
|
}
|
||||||
|
if (e->jpeg) {
|
||||||
|
ret = mpp_create(&e->decoder, &e->dec_api);
|
||||||
|
if (ret) return abort_init("mpp_create JPEG decoder", ret);
|
||||||
|
ret = mpp_init(e->decoder, MPP_CTX_DEC, MPP_VIDEO_CodingMJPEG);
|
||||||
|
if (ret) return abort_init("mpp_init JPEG decoder", ret);
|
||||||
|
MppFrameFormat output = MPP_FMT_YUV420SP;
|
||||||
|
ret = e->dec_api->control(e->decoder, MPP_DEC_SET_OUTPUT_FORMAT, &output);
|
||||||
|
if (ret) return abort_init("JPEG NV12 output", ret);
|
||||||
|
ret = mpp_buffer_group_get_internal(&e->decoded_group, MPP_BUFFER_TYPE_DRM);
|
||||||
|
if (ret) return abort_init("JPEG output buffer group", ret);
|
||||||
|
// MPP JPEG requires aligned storage; reserve the conservative size used
|
||||||
|
// by its advanced-task decoder demo. One output reused after encode.
|
||||||
|
ret = mpp_buffer_get(e->decoded_group, &e->decoded_buffer, size_t(stride) * vstride * 4);
|
||||||
|
if (ret) return abort_init("JPEG output buffer", ret);
|
||||||
|
ret = mpp_frame_init(&e->decoded_frame);
|
||||||
|
if (ret) return abort_init("JPEG output frame", ret);
|
||||||
|
mpp_frame_set_buffer(e->decoded_frame, e->decoded_buffer);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int dma_read_sync(MppBuffer buffer, bool start) {
|
||||||
|
dma_buf_sync sync{};
|
||||||
|
sync.flags = DMA_BUF_SYNC_READ | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END);
|
||||||
|
int ret;
|
||||||
|
do { ret = ioctl(mpp_buffer_get_fd(buffer), DMA_BUF_IOCTL_SYNC, &sync); }
|
||||||
|
while (ret < 0 && errno == EINTR);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int decode_jpeg(RkmppDmaEncoder *e, size_t index, size_t bytes_used) {
|
||||||
|
MppBuffer input = e->buffers[index];
|
||||||
|
// The parser reads only header bytes with explicit DMA CPU-read ownership.
|
||||||
|
if (dma_read_sync(input, true)) return fail("JPEG DMA read sync start", errno);
|
||||||
|
const auto *data = static_cast<const uint8_t *>(mpp_buffer_get_ptr(input));
|
||||||
|
const bool valid = rkmpp_dma_jpeg_header(data, bytes_used, e->width, e->height);
|
||||||
|
if (dma_read_sync(input, false)) return fail("JPEG DMA read sync end", errno);
|
||||||
|
if (!valid) return fail("unsupported/mismatched JPEG header", -1);
|
||||||
|
|
||||||
|
int ret = mpp_packet_init_with_buffer(&e->input_packet, input);
|
||||||
|
if (ret) return fail("JPEG input packet", ret);
|
||||||
|
mpp_packet_set_length(e->input_packet, bytes_used);
|
||||||
|
MppTask task = nullptr;
|
||||||
|
ret = e->dec_api->poll(e->decoder, MPP_PORT_INPUT, static_cast<MppPollType>(2000));
|
||||||
|
if (ret) return fail("JPEG input poll", ret);
|
||||||
|
ret = e->dec_api->dequeue(e->decoder, MPP_PORT_INPUT, &task);
|
||||||
|
if (ret || !task) return fail("JPEG input task", ret);
|
||||||
|
ret = mpp_task_meta_set_packet(task, KEY_INPUT_PACKET, e->input_packet);
|
||||||
|
if (ret) return fail("JPEG input metadata", ret);
|
||||||
|
ret = mpp_task_meta_set_frame(task, KEY_OUTPUT_FRAME, e->decoded_frame);
|
||||||
|
if (ret) return fail("JPEG output metadata", ret);
|
||||||
|
ret = e->dec_api->enqueue(e->decoder, MPP_PORT_INPUT, task);
|
||||||
|
if (ret) return fail("JPEG submit", ret);
|
||||||
|
task = nullptr;
|
||||||
|
ret = e->dec_api->poll(e->decoder, MPP_PORT_OUTPUT, static_cast<MppPollType>(2000));
|
||||||
|
if (ret) return fail("JPEG output poll", ret);
|
||||||
|
ret = e->dec_api->dequeue(e->decoder, MPP_PORT_OUTPUT, &task);
|
||||||
|
if (ret || !task) return fail("JPEG output task", ret);
|
||||||
|
MppFrame result = nullptr;
|
||||||
|
ret = mpp_task_meta_get_frame(task, KEY_OUTPUT_FRAME, &result);
|
||||||
|
if (ret || result != e->decoded_frame) return fail("JPEG output frame mismatch", ret);
|
||||||
|
ret = e->dec_api->enqueue(e->decoder, MPP_PORT_OUTPUT, task);
|
||||||
|
if (ret) return fail("JPEG return output task", ret);
|
||||||
|
ret = e->dec_api->poll(e->decoder, MPP_PORT_INPUT, static_cast<MppPollType>(2000));
|
||||||
|
if (ret) return fail("JPEG input completion", ret);
|
||||||
|
mpp_packet_deinit(&e->input_packet);
|
||||||
|
if (mpp_frame_get_errinfo(result) || mpp_frame_get_discard(result) ||
|
||||||
|
mpp_frame_get_info_change(result) ||
|
||||||
|
mpp_frame_get_width(result) != unsigned(e->width) ||
|
||||||
|
mpp_frame_get_height(result) != unsigned(e->height) ||
|
||||||
|
mpp_frame_get_fmt(result) != MPP_FMT_YUV420SP ||
|
||||||
|
mpp_frame_get_buffer(result) != e->decoded_buffer) {
|
||||||
|
std::snprintf(dma_error, sizeof(dma_error),
|
||||||
|
"invalid JPEG decoded frame: err=%u discard=%u info_change=%u size=%ux%u fmt=%x buffer_match=%d",
|
||||||
|
mpp_frame_get_errinfo(result), mpp_frame_get_discard(result), mpp_frame_get_info_change(result),
|
||||||
|
mpp_frame_get_width(result), mpp_frame_get_height(result), unsigned(mpp_frame_get_fmt(result)),
|
||||||
|
int(mpp_frame_get_buffer(result) == e->decoded_buffer));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const int hs = mpp_frame_get_hor_stride(result), vs = mpp_frame_get_ver_stride(result);
|
||||||
|
if (hs < e->width || vs < e->height || hs > 8192 || vs > 8192 || (hs & 15) || (vs & 15) ||
|
||||||
|
size_t(hs) * vs * 3 / 2 > mpp_buffer_get_size(e->decoded_buffer))
|
||||||
|
return fail("invalid JPEG decoded stride", -1);
|
||||||
|
if (!e->decoded_layout_set) {
|
||||||
|
if (!set_cfg(e, "prep:hor_stride", hs) || !set_cfg(e, "prep:ver_stride", vs)) return -1;
|
||||||
|
ret = e->api->control(e->ctx, MPP_ENC_SET_CFG, e->cfg);
|
||||||
|
if (ret) return fail("JPEG encoder layout", ret);
|
||||||
|
e->decoded_stride = hs; e->decoded_vstride = vs; e->decoded_layout_set = true;
|
||||||
|
} else if (hs != e->decoded_stride || vs != e->decoded_vstride) {
|
||||||
|
return fail("JPEG decoded layout changed", -1);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int rkmpp_dma_encode(RkmppDmaEncoder *e, size_t index, size_t bytes_used, int fresh_fd, int64_t pts_us,
|
||||||
|
int force_idr, const uint8_t **data, size_t *size) {
|
||||||
|
if (!e || !e->ctx || index >= e->count || !data || !size)
|
||||||
|
return fail("invalid DMA encode call", -1);
|
||||||
|
*data = nullptr; *size = 0;
|
||||||
|
if (e->packet) mpp_packet_deinit(&e->packet);
|
||||||
|
auto abort_encode = [e](const char *op, int ret) {
|
||||||
|
fail(op, ret); e->close(); return -1;
|
||||||
|
};
|
||||||
|
if (bytes_used > e->capacities[index] ||
|
||||||
|
(e->jpeg ? (bytes_used < 4 || e->capacities[index] - bytes_used < 64) : bytes_used != e->minimum))
|
||||||
|
return abort_encode("invalid DMA payload length", -1);
|
||||||
|
if (fresh_fd >= 0) {
|
||||||
|
MppBuffer replacement = nullptr;
|
||||||
|
MppBufferInfo info{};
|
||||||
|
info.type = MPP_BUFFER_TYPE_EXT_DMA; info.fd = fresh_fd;
|
||||||
|
info.size = e->capacities[index]; info.index = static_cast<int>(index);
|
||||||
|
const int ret = mpp_buffer_import(&replacement, &info);
|
||||||
|
if (ret) return abort_encode("refresh USB DMA import", ret);
|
||||||
|
if (e->buffers[index]) mpp_buffer_put(e->buffers[index]);
|
||||||
|
e->buffers[index] = replacement;
|
||||||
|
}
|
||||||
|
if (e->jpeg && decode_jpeg(e, index, bytes_used)) {
|
||||||
|
// Preserve the detailed decoder failure while ending BOTH engines.
|
||||||
|
e->close(); return -1;
|
||||||
|
}
|
||||||
|
if (force_idr) {
|
||||||
|
int ret = e->api->control(e->ctx, MPP_ENC_SET_IDR_FRAME, nullptr);
|
||||||
|
if (ret) return abort_encode("MPP_ENC_SET_IDR_FRAME", ret);
|
||||||
|
}
|
||||||
|
MppFrame frame = e->jpeg ? e->decoded_frame : nullptr;
|
||||||
|
int ret = 0;
|
||||||
|
if (!e->jpeg) {
|
||||||
|
ret = mpp_frame_init(&frame);
|
||||||
|
if (ret) return abort_encode("mpp_frame_init", ret);
|
||||||
|
mpp_frame_set_width(frame, e->width); mpp_frame_set_height(frame, e->height);
|
||||||
|
mpp_frame_set_hor_stride(frame, e->stride); mpp_frame_set_ver_stride(frame, e->height);
|
||||||
|
mpp_frame_set_fmt(frame, e->format);
|
||||||
|
mpp_frame_set_buffer(frame, e->buffers[index]);
|
||||||
|
}
|
||||||
|
mpp_frame_set_pts(frame, pts_us);
|
||||||
|
ret = e->api->encode_put_frame(e->ctx, frame);
|
||||||
|
if (!e->jpeg) mpp_frame_deinit(&frame);
|
||||||
|
if (ret) return abort_encode("encode_put_frame", ret);
|
||||||
|
ret = e->api->encode_get_packet(e->ctx, &e->packet);
|
||||||
|
if (ret || !e->packet) return abort_encode("encode_get_packet", ret);
|
||||||
|
if (mpp_packet_is_partition(e->packet) || !mpp_packet_get_length(e->packet))
|
||||||
|
return abort_encode("incomplete DMA encoder output", -1);
|
||||||
|
// One synchronous input, no temporal scalability/reordering/split output.
|
||||||
|
// A completed packet is the input-consumption barrier for this mode.
|
||||||
|
*data = static_cast<const uint8_t *>(mpp_packet_get_pos(e->packet));
|
||||||
|
*size = mpp_packet_get_length(e->packet);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
extern "C" void rkmpp_dma_free(RkmppDmaEncoder *e) { delete e; }
|
||||||
|
#else
|
||||||
|
extern "C" RkmppDmaEncoder *rkmpp_dma_new(int,int,int,int,int,int,int,int,const int*,const size_t*,size_t) {
|
||||||
|
fail("RKMPP DMA support not built", -1); return nullptr;
|
||||||
|
}
|
||||||
|
extern "C" int rkmpp_dma_encode(RkmppDmaEncoder*,size_t,size_t,int,int64_t,int,const uint8_t**,size_t*) { return -1; }
|
||||||
|
extern "C" int rkmpp_dma_reconfigure(RkmppDmaEncoder*,int,int) { return -1; }
|
||||||
|
extern "C" void rkmpp_dma_free(RkmppDmaEncoder*) {}
|
||||||
|
#endif
|
||||||
|
extern "C" const char *rkmpp_dma_error(void) { return dma_error; }
|
||||||
31
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dmabuf_ffi.h
Normal file
31
libs/hwcodec/cpp/ffmpeg_hw/rkmpp_dmabuf_ffi.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct RkmppDmaEncoder RkmppDmaEncoder;
|
||||||
|
// format: 0=NV12, 1=BGR24, 2=YUYV, 3=RGB24 (byte stride), 4=MJPEG (stride ignored).
|
||||||
|
// codec: 0=H264, 1=HEVC. MJPEG is decoded to hardware NV12, then encoded.
|
||||||
|
RkmppDmaEncoder *rkmpp_dma_new(int width, int height, int stride, int format,
|
||||||
|
int codec, int fps, int kbps, int gop,
|
||||||
|
const int *fds, const size_t *sizes, size_t count);
|
||||||
|
// Synchronous input-completion boundary. On failure the encoder is destroyed
|
||||||
|
// internally BEFORE returning, so it cannot keep reading the capture buffer.
|
||||||
|
// Output is borrowed until the next call or destruction; copy before reusing it.
|
||||||
|
// bytes_used must be the actual captured payload. MJPEG needs 64 bytes of
|
||||||
|
// readable allocation headroom; never pass sizeimage as the compressed length.
|
||||||
|
// fresh_fd=-1 reuses the original import (native HDMI). UVC supplies a new
|
||||||
|
// export of this dequeued slot. Keep it open until replacement/free; the old
|
||||||
|
// export can be closed AFTER this call, including on failure.
|
||||||
|
int rkmpp_dma_encode(RkmppDmaEncoder *encoder, size_t index, size_t bytes_used, int fresh_fd, int64_t pts_us,
|
||||||
|
int force_idr, const uint8_t **data, size_t *size);
|
||||||
|
int rkmpp_dma_reconfigure(RkmppDmaEncoder *encoder, int kbps, int gop);
|
||||||
|
void rkmpp_dma_free(RkmppDmaEncoder *encoder);
|
||||||
|
const char *rkmpp_dma_error(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Minimal FFmpeg RAM MJPEG decoder (RKMPP only) -> NV12 in CPU memory.
|
// FFmpeg RAM decoder with optional RKMPP hardware-frame support.
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
@@ -54,9 +54,11 @@ public:
|
|||||||
thread_count_ = thread_count > 0 ? thread_count : 1;
|
thread_count_ = thread_count > 0 ? thread_count : 1;
|
||||||
callback_ = callback;
|
callback_ = callback;
|
||||||
|
|
||||||
|
#ifdef ONE_KVM_FFMPEG_RKMPP
|
||||||
if (name_.find("rkmpp") != std::string::npos) {
|
if (name_.find("rkmpp") != std::string::npos) {
|
||||||
hw_device_type_ = AV_HWDEVICE_TYPE_RKMPP;
|
hw_device_type_ = AV_HWDEVICE_TYPE_RKMPP;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
~FFmpegRamDecoder() {}
|
~FFmpegRamDecoder() {}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ extern "C" {
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
@@ -21,6 +22,12 @@ extern "C" {
|
|||||||
#include "win.h"
|
#include "win.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static thread_local std::string g_encoder_last_error;
|
||||||
|
|
||||||
|
static void set_encoder_last_error(const std::string &message) {
|
||||||
|
g_encoder_last_error = message;
|
||||||
|
}
|
||||||
|
|
||||||
static int calculate_offset_length(int pix_fmt, int height, const int *linesize,
|
static int calculate_offset_length(int pix_fmt, int height, const int *linesize,
|
||||||
int *offset, int *length) {
|
int *offset, int *length) {
|
||||||
switch (pix_fmt) {
|
switch (pix_fmt) {
|
||||||
@@ -112,6 +119,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:
|
||||||
@@ -119,7 +129,6 @@ public:
|
|||||||
AVFrame *frame_ = NULL;
|
AVFrame *frame_ = NULL;
|
||||||
AVPacket *pkt_ = NULL;
|
AVPacket *pkt_ = NULL;
|
||||||
std::string name_;
|
std::string name_;
|
||||||
std::string mc_name_; // for mediacodec
|
|
||||||
|
|
||||||
int width_ = 0;
|
int width_ = 0;
|
||||||
int height_ = 0;
|
int height_ = 0;
|
||||||
@@ -134,6 +143,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
|
||||||
|
|
||||||
@@ -142,12 +152,11 @@ public:
|
|||||||
AVBufferRef *hw_device_ctx_ = NULL;
|
AVBufferRef *hw_device_ctx_ = NULL;
|
||||||
AVFrame *hw_frame_ = NULL;
|
AVFrame *hw_frame_ = NULL;
|
||||||
|
|
||||||
FFmpegRamEncoder(const char *name, const char *mc_name, int width, int height,
|
FFmpegRamEncoder(const char *name, int width, int height,
|
||||||
int pixfmt, int align, int fps, int gop, int rc, int quality,
|
int pixfmt, int align, int fps, int gop, int rc, int quality,
|
||||||
int kbs, int q, int thread_count, int gpu,
|
int kbs, int q, int thread_count, int gpu,
|
||||||
RamEncodeCallback callback) {
|
RamEncodeCallback callback) {
|
||||||
name_ = name;
|
name_ = name;
|
||||||
mc_name_ = mc_name ? mc_name : "";
|
|
||||||
width_ = width;
|
width_ = width;
|
||||||
height_ = height;
|
height_ = height;
|
||||||
pixfmt_ = (AVPixelFormat)pixfmt;
|
pixfmt_ = (AVPixelFormat)pixfmt;
|
||||||
@@ -179,12 +188,13 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool init(int *linesize, int *offset, int *length) {
|
bool init(int *linesize, int *offset, int *length) {
|
||||||
|
g_encoder_last_error.clear();
|
||||||
const AVCodec *codec = NULL;
|
const AVCodec *codec = NULL;
|
||||||
|
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
if (!(codec = avcodec_find_encoder_by_name(name_.c_str()))) {
|
if (!(codec = avcodec_find_encoder_by_name(name_.c_str()))) {
|
||||||
LOG_ERROR(std::string("Codec ") + name_ + " not found");
|
set_encoder_last_error(std::string("Codec ") + name_ + " not found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +257,6 @@ public:
|
|||||||
LOG_ERROR(std::string("Could not allocate video packet"));
|
LOG_ERROR(std::string("Could not allocate video packet"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* resolution must be a multiple of two */
|
/* resolution must be a multiple of two */
|
||||||
c_->width = width_;
|
c_->width = width_;
|
||||||
c_->height = height_;
|
c_->height = height_;
|
||||||
@@ -263,23 +272,16 @@ public:
|
|||||||
LOG_ERROR(std::string("set_quality failed, name: ") + name_);
|
LOG_ERROR(std::string("set_quality failed, name: ") + name_);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
util_encode::set_rate_control(c_, name_, rc_, q_);
|
if (!util_encode::set_rate_control(c_, name_, rc_, q_)) {
|
||||||
|
LOG_ERROR(std::string("set_rate_control failed, name: ") + name_);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
util_encode::set_gpu(c_->priv_data, name_, gpu_);
|
util_encode::set_gpu(c_->priv_data, name_, gpu_);
|
||||||
util_encode::force_hw(c_->priv_data, name_);
|
util_encode::force_hw(c_->priv_data, name_);
|
||||||
util_encode::set_others(c_->priv_data, name_);
|
util_encode::set_others(c_->priv_data, name_);
|
||||||
if (name_.find("mediacodec") != std::string::npos) {
|
|
||||||
if (mc_name_.length() > 0) {
|
|
||||||
LOG_INFO(std::string("mediacodec codec_name: ") + mc_name_);
|
|
||||||
if ((ret = av_opt_set(c_->priv_data, "codec_name", mc_name_.c_str(),
|
|
||||||
0)) < 0) {
|
|
||||||
LOG_ERROR(std::string("mediacodec codec_name failed, ret = ") + av_err2str(ret));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((ret = avcodec_open2(c_, codec, NULL)) < 0) {
|
if ((ret = avcodec_open2(c_, codec, NULL)) < 0) {
|
||||||
LOG_ERROR(std::string("avcodec_open2 failed, ret = ") + av_err2str(ret) +
|
set_encoder_last_error(std::string("avcodec_open2 failed, ret = ") +
|
||||||
", name: " + name_);
|
av_err2str(ret) + ", name: " + name_);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +303,7 @@ public:
|
|||||||
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 +319,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_);
|
||||||
@@ -369,108 +379,210 @@ private:
|
|||||||
frame->pts = ms;
|
frame->pts = ms;
|
||||||
|
|
||||||
// Force keyframe if requested
|
// Force keyframe if requested
|
||||||
if (force_keyframe_) {
|
if (force_keyframe_ && util_encode::supports_forced_keyframe(name_)) {
|
||||||
frame->pict_type = AV_PICTURE_TYPE_I;
|
frame->pict_type = AV_PICTURE_TYPE_I;
|
||||||
force_keyframe_ = false;
|
|
||||||
} else {
|
} else {
|
||||||
frame->pict_type = AV_PICTURE_TYPE_NONE;
|
frame->pict_type = AV_PICTURE_TYPE_NONE;
|
||||||
}
|
}
|
||||||
|
force_keyframe_ = false;
|
||||||
|
|
||||||
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,23 +591,64 @@ private:
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
extern "C" FFmpegRamEncoder *
|
extern "C" FFmpegRamEncoder *
|
||||||
ffmpeg_ram_new_encoder(const char *name, const char *mc_name, int width,
|
ffmpeg_ram_new_encoder(const char *name, int width,
|
||||||
int height, int pixfmt, int align, int fps, int gop,
|
int height, int pixfmt, int align, int fps, int gop,
|
||||||
int rc, int quality, int kbs, int q, int thread_count,
|
int rc, int quality, int kbs, int q, int thread_count,
|
||||||
int gpu, int *linesize, int *offset, int *length,
|
int gpu, int *linesize, int *offset, int *length,
|
||||||
RamEncodeCallback callback) {
|
RamEncodeCallback callback) {
|
||||||
FFmpegRamEncoder *encoder = NULL;
|
FFmpegRamEncoder *encoder = NULL;
|
||||||
try {
|
try {
|
||||||
encoder = new FFmpegRamEncoder(name, mc_name, width, height, pixfmt, align,
|
auto try_create = [&](int attempt_rc, int attempt_kbs) {
|
||||||
fps, gop, rc, quality, kbs, q, thread_count,
|
FFmpegRamEncoder *candidate = new FFmpegRamEncoder(
|
||||||
gpu, callback);
|
name, width, height, pixfmt, align, fps, gop, attempt_rc, quality,
|
||||||
|
attempt_kbs, q, thread_count, gpu, callback);
|
||||||
|
if (candidate && candidate->init(linesize, offset, length)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate) {
|
||||||
|
candidate->free_encoder();
|
||||||
|
delete candidate;
|
||||||
|
}
|
||||||
|
return static_cast<FFmpegRamEncoder *>(NULL);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve the existing VAAPI path first. With a target bitrate and no
|
||||||
|
// explicit QP, FFmpeg negotiates a supported bitrate-based mode such as
|
||||||
|
// AVBR, VBR or CBR with the driver.
|
||||||
|
encoder = try_create(rc, kbs);
|
||||||
|
if (encoder) {
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry only VAAPI with a fresh context in explicit-QP mode. This avoids
|
||||||
|
// changing rate control on hardware which already supports CBR/VBR while
|
||||||
|
// allowing CQP-only drivers to pass probing and normal encoder creation.
|
||||||
|
if (name && std::string(name).find("vaapi") != std::string::npos &&
|
||||||
|
rc != RC_CQ) {
|
||||||
|
LOG_DEBUG(std::string("VAAPI bitrate-based rate control failed for ") +
|
||||||
|
name + ", retrying with CQP");
|
||||||
|
encoder = try_create(RC_CQ, 0);
|
||||||
if (encoder) {
|
if (encoder) {
|
||||||
if (encoder->init(linesize, offset, length)) {
|
|
||||||
return encoder;
|
return encoder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -532,6 +685,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);
|
||||||
@@ -550,3 +722,7 @@ extern "C" void ffmpeg_ram_request_keyframe(FFmpegRamEncoder *encoder) {
|
|||||||
LOG_ERROR(std::string("ffmpeg_ram_request_keyframe failed, ") + std::string(e.what()));
|
LOG_ERROR(std::string("ffmpeg_ram_request_keyframe failed, ") + std::string(e.what()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" const char *ffmpeg_ram_encoder_last_error(void) {
|
||||||
|
return g_encoder_last_error.c_str();
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,13 @@
|
|||||||
|
|
||||||
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);
|
||||||
|
|
||||||
void *ffmpeg_ram_new_encoder(const char *name, const char *mc_name, int width,
|
void *ffmpeg_ram_new_encoder(const char *name, int width,
|
||||||
int height, int pixfmt, int align, int fps,
|
int height, int pixfmt, int align, int fps,
|
||||||
int gop, int rc, int quality, int kbs, int q,
|
int gop, int rc, int quality, int kbs, int q,
|
||||||
int thread_count, int gpu, int *linesize,
|
int thread_count, int gpu, int *linesize,
|
||||||
@@ -18,12 +21,17 @@ 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);
|
||||||
int ffmpeg_ram_set_bitrate(void *encoder, int kbs);
|
int ffmpeg_ram_set_bitrate(void *encoder, int kbs);
|
||||||
void ffmpeg_ram_request_keyframe(void *encoder);
|
void ffmpeg_ram_request_keyframe(void *encoder);
|
||||||
|
const char *ffmpeg_ram_encoder_last_error(void);
|
||||||
|
|
||||||
void *ffmpeg_ram_new_decoder(const char *name, int width, int height,
|
void *ffmpeg_ram_new_decoder(const char *name, int width, int height,
|
||||||
int sw_pixfmt, int thread_count,
|
int sw_pixfmt, int thread_count,
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ pub extern "C" fn hwcodec_av_log_callback(level: i32, message: *const std::os::r
|
|||||||
if let Ok(str_slice) = c_str.to_str() {
|
if let Ok(str_slice) = c_str.to_str() {
|
||||||
let string = String::from(str_slice);
|
let string = String::from(str_slice);
|
||||||
if level == AV_LOG_ERROR as i32 {
|
if level == AV_LOG_ERROR as i32 {
|
||||||
log::error!("{}", string);
|
|
||||||
if string.contains(could_not_find_ref_with_poc) {
|
if string.contains(could_not_find_ref_with_poc) {
|
||||||
hwcodec_set_flag_could_not_find_ref_with_poc();
|
hwcodec_set_flag_could_not_find_ref_with_poc();
|
||||||
}
|
}
|
||||||
|
log::debug!("{}", string);
|
||||||
} else if level == AV_LOG_PANIC as i32 || level == AV_LOG_FATAL as i32 {
|
} else if level == AV_LOG_PANIC as i32 || level == AV_LOG_FATAL as i32 {
|
||||||
log::error!("{}", string);
|
log::error!("{}", string);
|
||||||
} else if level == AV_LOG_WARNING as i32 {
|
} else if level == AV_LOG_WARNING as i32 {
|
||||||
log::warn!("{}", string);
|
log::debug!("{}", string);
|
||||||
} else if level == AV_LOG_INFO as i32 {
|
} else if level == AV_LOG_INFO as i32 {
|
||||||
log::info!("{}", string);
|
log::info!("{}", string);
|
||||||
} else if level == AV_LOG_VERBOSE as i32 || level == AV_LOG_DEBUG as i32 {
|
} else if level == AV_LOG_VERBOSE as i32 || level == AV_LOG_DEBUG as i32 {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ impl Drop for Decoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_error_message() -> String {
|
pub fn last_error_message() -> String {
|
||||||
unsafe {
|
unsafe {
|
||||||
let ptr = ffmpeg_ram_last_error();
|
let ptr = ffmpeg_ram_last_error();
|
||||||
if ptr.is_null() {
|
if ptr.is_null() {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
#[cfg(feature = "bytes")]
|
||||||
|
use crate::ffmpeg_ram::{ffmpeg_ram_encode_packet, ffmpeg_ram_free_packet};
|
||||||
use crate::{
|
use crate::{
|
||||||
common::DataFormat::{self, *},
|
common::DataFormat::{self, *},
|
||||||
ffmpeg::{init_av_log, AVPixelFormat},
|
ffmpeg::{init_av_log, AVPixelFormat},
|
||||||
ffmpeg_ram::{
|
ffmpeg_ram::{
|
||||||
ffmpeg_linesize_offset_length, ffmpeg_ram_encode, ffmpeg_ram_free_encoder,
|
ffmpeg_linesize_offset_length, ffmpeg_ram_encode, ffmpeg_ram_encoder_last_error,
|
||||||
ffmpeg_ram_new_encoder, ffmpeg_ram_request_keyframe, ffmpeg_ram_set_bitrate, CodecInfo,
|
ffmpeg_ram_free_encoder, ffmpeg_ram_new_encoder, ffmpeg_ram_request_keyframe,
|
||||||
AV_NUM_DATA_POINTERS,
|
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},
|
||||||
@@ -97,6 +101,7 @@ fn enumerate_candidate_codecs(ctx: &EncodeContext) -> Vec<CodecInfo> {
|
|||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
let mut codecs = Vec::new();
|
let mut codecs = Vec::new();
|
||||||
|
|
||||||
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.
|
||||||
@@ -304,13 +309,14 @@ 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
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
@@ -343,7 +349,6 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo
|
|||||||
|
|
||||||
let test_ctx = EncodeContext {
|
let test_ctx = EncodeContext {
|
||||||
name: codec.name.clone(),
|
name: codec.name.clone(),
|
||||||
mc_name: codec.mc_name.clone(),
|
|
||||||
..ctx.clone()
|
..ctx.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -407,10 +412,7 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo
|
|||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => false,
|
||||||
debug!("Failed to create encoder {}", codec.name);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +433,6 @@ fn add_software_fallback(codecs: &mut Vec<CodecInfo>) {
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct EncodeContext {
|
pub struct EncodeContext {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub mc_name: Option<String>,
|
|
||||||
pub width: i32,
|
pub width: i32,
|
||||||
pub height: i32,
|
pub height: i32,
|
||||||
pub pixfmt: i32,
|
pub pixfmt: i32,
|
||||||
@@ -451,6 +452,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)
|
||||||
@@ -483,10 +517,8 @@ impl Encoder {
|
|||||||
.unwrap_or("-1".to_owned())
|
.unwrap_or("-1".to_owned())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(-1);
|
.unwrap_or(-1);
|
||||||
let mc_name = ctx.mc_name.clone().unwrap_or_default();
|
|
||||||
let codec = ffmpeg_ram_new_encoder(
|
let codec = ffmpeg_ram_new_encoder(
|
||||||
CString::new(ctx.name.as_str()).map_err(|_| ())?.as_ptr(),
|
CString::new(ctx.name.as_str()).map_err(|_| ())?.as_ptr(),
|
||||||
CString::new(mc_name.as_str()).map_err(|_| ())?.as_ptr(),
|
|
||||||
ctx.width,
|
ctx.width,
|
||||||
ctx.height,
|
ctx.height,
|
||||||
ctx.pixfmt,
|
ctx.pixfmt,
|
||||||
@@ -506,6 +538,10 @@ impl Encoder {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if codec.is_null() {
|
if codec.is_null() {
|
||||||
|
let message = encoder_last_error_message();
|
||||||
|
if !message.is_empty() {
|
||||||
|
log::debug!("ffmpeg_ram_new_encoder failed: {}", message);
|
||||||
|
}
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,6 +579,35 @@ 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 {
|
||||||
|
if self.ctx.name.contains("v4l2m2m") {
|
||||||
|
return Ok(frames
|
||||||
|
.into_iter()
|
||||||
|
.map(|frame| EncodeBytesFrame {
|
||||||
|
data: Bytes::copy_from_slice(frame.data.as_ref()),
|
||||||
|
pts: frame.pts,
|
||||||
|
key: frame.key,
|
||||||
|
})
|
||||||
|
.collect());
|
||||||
|
}
|
||||||
|
return Ok(frames);
|
||||||
|
}
|
||||||
|
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>);
|
||||||
@@ -554,6 +619,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 {
|
||||||
@@ -626,6 +715,16 @@ impl Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn encoder_last_error_message() -> String {
|
||||||
|
unsafe {
|
||||||
|
let ptr = ffmpeg_ram_encoder_last_error();
|
||||||
|
if ptr.is_null() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
std::ffi::CStr::from_ptr(ptr).to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for Encoder {
|
impl Drop for Encoder {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
@@ -63,8 +63,6 @@ pub enum Priority {
|
|||||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
pub struct CodecInfo {
|
pub struct CodecInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(skip)]
|
|
||||||
pub mc_name: Option<String>,
|
|
||||||
pub format: DataFormat,
|
pub format: DataFormat,
|
||||||
pub priority: i32,
|
pub priority: i32,
|
||||||
pub hwdevice: AVHWDeviceType,
|
pub hwdevice: AVHWDeviceType,
|
||||||
@@ -74,7 +72,6 @@ impl Default for CodecInfo {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: Default::default(),
|
name: Default::default(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: DataFormat::H264,
|
format: DataFormat::H264,
|
||||||
priority: Default::default(),
|
priority: Default::default(),
|
||||||
hwdevice: AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
|
||||||
@@ -87,28 +84,24 @@ impl CodecInfo {
|
|||||||
match format {
|
match format {
|
||||||
H264 => Some(CodecInfo {
|
H264 => Some(CodecInfo {
|
||||||
name: "libx264".to_owned(),
|
name: "libx264".to_owned(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: H264,
|
format: H264,
|
||||||
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
||||||
priority: Priority::Soft as _,
|
priority: Priority::Soft as _,
|
||||||
}),
|
}),
|
||||||
H265 => Some(CodecInfo {
|
H265 => Some(CodecInfo {
|
||||||
name: "libx265".to_owned(),
|
name: "libx265".to_owned(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: H265,
|
format: H265,
|
||||||
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
||||||
priority: Priority::Soft as _,
|
priority: Priority::Soft as _,
|
||||||
}),
|
}),
|
||||||
VP8 => Some(CodecInfo {
|
VP8 => Some(CodecInfo {
|
||||||
name: "libvpx".to_owned(),
|
name: "libvpx".to_owned(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: VP8,
|
format: VP8,
|
||||||
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
||||||
priority: Priority::Soft as _,
|
priority: Priority::Soft as _,
|
||||||
}),
|
}),
|
||||||
VP9 => Some(CodecInfo {
|
VP9 => Some(CodecInfo {
|
||||||
name: "libvpx-vp9".to_owned(),
|
name: "libvpx-vp9".to_owned(),
|
||||||
mc_name: Default::default(),
|
|
||||||
format: VP9,
|
format: VP9,
|
||||||
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
||||||
priority: Priority::Soft as _,
|
priority: Priority::Soft as _,
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ pub mod ffmpeg;
|
|||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))]
|
#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))]
|
||||||
pub mod ffmpeg_hw;
|
pub mod ffmpeg_hw;
|
||||||
pub mod ffmpeg_ram;
|
pub mod ffmpeg_ram;
|
||||||
|
#[cfg(all(
|
||||||
|
target_os = "linux",
|
||||||
|
any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp")
|
||||||
|
))]
|
||||||
|
pub mod rkmpp_dmabuf;
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn hwcodec_log(level: i32, message: *const std::os::raw::c_char) {
|
pub extern "C" fn hwcodec_log(level: i32, message: *const std::os::raw::c_char) {
|
||||||
|
|||||||
193
libs/hwcodec/src/rkmpp_dmabuf.rs
Normal file
193
libs/hwcodec/src/rkmpp_dmabuf.rs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
//! Synchronous RKMPP encoder for pre-exported V4L2 DMA buffers.
|
||||||
|
//! Unlike the byte-slice encoder this never reads raw pixels on the CPU.
|
||||||
|
|
||||||
|
use std::ffi::{c_char, c_int, c_void, CStr};
|
||||||
|
use std::os::fd::{AsRawFd, OwnedFd};
|
||||||
|
use std::ptr::NonNull;
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn rkmpp_dma_new(
|
||||||
|
width: c_int,
|
||||||
|
height: c_int,
|
||||||
|
stride: c_int,
|
||||||
|
format: c_int,
|
||||||
|
codec: c_int,
|
||||||
|
fps: c_int,
|
||||||
|
kbps: c_int,
|
||||||
|
gop: c_int,
|
||||||
|
fds: *const c_int,
|
||||||
|
sizes: *const usize,
|
||||||
|
count: usize,
|
||||||
|
) -> *mut c_void;
|
||||||
|
fn rkmpp_dma_encode(
|
||||||
|
encoder: *mut c_void,
|
||||||
|
index: usize,
|
||||||
|
bytes_used: usize,
|
||||||
|
fresh_fd: c_int,
|
||||||
|
pts_us: i64,
|
||||||
|
force_idr: c_int,
|
||||||
|
data: *mut *const u8,
|
||||||
|
size: *mut usize,
|
||||||
|
) -> c_int;
|
||||||
|
fn rkmpp_dma_reconfigure(encoder: *mut c_void, kbps: c_int, gop: c_int) -> c_int;
|
||||||
|
fn rkmpp_dma_free(encoder: *mut c_void);
|
||||||
|
fn rkmpp_dma_error() -> *const c_char;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum DmaFormat {
|
||||||
|
Nv12 = 0,
|
||||||
|
Bgr24 = 1,
|
||||||
|
Yuyv = 2,
|
||||||
|
Rgb24 = 3,
|
||||||
|
Mjpeg = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DmaEncoderConfig {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub stride: u32,
|
||||||
|
pub format: DmaFormat,
|
||||||
|
pub hevc: bool,
|
||||||
|
pub fps: u32,
|
||||||
|
pub bitrate_kbps: u32,
|
||||||
|
pub gop: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DmaEncoder {
|
||||||
|
ctx: NonNull<c_void>,
|
||||||
|
// Export FDs remain open until AFTER mpp_destroy and imported buffer release.
|
||||||
|
_buffers: Vec<(OwnedFd, usize)>,
|
||||||
|
// Keep refreshed exports alive until native replacement/release has ended
|
||||||
|
// all references to the previous import. At most one FD per capture slot.
|
||||||
|
fresh_buffers: Vec<Option<OwnedFd>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclusive ownership: the context can move between threads but all calls are sequential.
|
||||||
|
unsafe impl Send for DmaEncoder {}
|
||||||
|
|
||||||
|
fn last_error() -> String {
|
||||||
|
unsafe {
|
||||||
|
CStr::from_ptr(rkmpp_dma_error())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DmaEncoder {
|
||||||
|
pub fn new(config: DmaEncoderConfig, buffers: Vec<(OwnedFd, usize)>) -> Result<Self, String> {
|
||||||
|
let fds: Vec<_> = buffers.iter().map(|(fd, _)| fd.as_raw_fd()).collect();
|
||||||
|
let sizes: Vec<_> = buffers.iter().map(|(_, size)| *size).collect();
|
||||||
|
for value in [
|
||||||
|
config.width,
|
||||||
|
config.height,
|
||||||
|
config.stride,
|
||||||
|
config.fps,
|
||||||
|
config.bitrate_kbps,
|
||||||
|
config.gop,
|
||||||
|
] {
|
||||||
|
if value > c_int::MAX as u32 {
|
||||||
|
return Err("DMA encoder parameter overflow".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ptr = unsafe {
|
||||||
|
rkmpp_dma_new(
|
||||||
|
config.width as _,
|
||||||
|
config.height as _,
|
||||||
|
config.stride as _,
|
||||||
|
config.format as c_int,
|
||||||
|
config.hevc as _,
|
||||||
|
config.fps as _,
|
||||||
|
config.bitrate_kbps as _,
|
||||||
|
config.gop as _,
|
||||||
|
fds.as_ptr(),
|
||||||
|
sizes.as_ptr(),
|
||||||
|
buffers.len(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
ctx: NonNull::new(ptr).ok_or_else(last_error)?,
|
||||||
|
fresh_buffers: (0..buffers.len()).map(|_| None).collect(),
|
||||||
|
_buffers: buffers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// The indexed buffer must be dequeued and exclusively leased to this call.
|
||||||
|
/// Do not requeue/write it until this function returns. On failure native MPP
|
||||||
|
/// is synchronously destroyed before returning, ending all input access.
|
||||||
|
/// `bytes_used` is the actual DQBUF payload length, not the buffer capacity.
|
||||||
|
/// A refreshed FD, if supplied, must refer to the same leased capture slot
|
||||||
|
/// with the capacity registered at construction. Ownership is retained here.
|
||||||
|
pub unsafe fn encode(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
bytes_used: usize,
|
||||||
|
fresh_fd: Option<OwnedFd>,
|
||||||
|
pts_ms: i64,
|
||||||
|
force_idr: bool,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let mut data = std::ptr::null();
|
||||||
|
let mut size = 0;
|
||||||
|
if index >= self.fresh_buffers.len() {
|
||||||
|
return Err("Invalid DMA capture index".into());
|
||||||
|
}
|
||||||
|
let ret = unsafe {
|
||||||
|
rkmpp_dma_encode(
|
||||||
|
self.ctx.as_ptr(),
|
||||||
|
index,
|
||||||
|
bytes_used,
|
||||||
|
fresh_fd.as_ref().map_or(-1, AsRawFd::as_raw_fd),
|
||||||
|
pts_ms.saturating_mul(1000),
|
||||||
|
force_idr as _,
|
||||||
|
&mut data,
|
||||||
|
&mut size,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if fresh_fd.is_some() {
|
||||||
|
// Native has now released the previous import, or destroyed both
|
||||||
|
// hardware contexts on error. Only now may its old FD be closed.
|
||||||
|
self.fresh_buffers[index] = fresh_fd;
|
||||||
|
}
|
||||||
|
if ret != 0 {
|
||||||
|
return Err(last_error());
|
||||||
|
}
|
||||||
|
if data.is_null() || size == 0 {
|
||||||
|
return Err("Empty RKMPP DMA packet".into());
|
||||||
|
}
|
||||||
|
// Copy only the compressed output, releasing the driver's packet promptly
|
||||||
|
// regardless of how long a network subscriber retains its Bytes.
|
||||||
|
Ok(unsafe { std::slice::from_raw_parts(data, size) }.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reconfigure(&mut self, kbps: u32, gop: u32) -> Result<(), String> {
|
||||||
|
if kbps > c_int::MAX as u32 || gop > c_int::MAX as u32 {
|
||||||
|
return Err("DMA encoder parameter overflow".into());
|
||||||
|
}
|
||||||
|
if unsafe { rkmpp_dma_reconfigure(self.ctx.as_ptr(), kbps as _, gop as _) } != 0 {
|
||||||
|
return Err(last_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DmaEncoder {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { rkmpp_dma_free(self.ctx.as_ptr()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_discriminants_match_native_abi() {
|
||||||
|
assert_eq!(DmaFormat::Nv12 as c_int, 0);
|
||||||
|
assert_eq!(DmaFormat::Bgr24 as c_int, 1);
|
||||||
|
assert_eq!(DmaFormat::Yuyv as c_int, 2);
|
||||||
|
assert_eq!(DmaFormat::Rgb24 as c_int, 3);
|
||||||
|
assert_eq!(DmaFormat::Mjpeg as c_int, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
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"
|
||||||
|
}
|
||||||
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.31"
|
||||||
|
features = [
|
||||||
|
"ioctl",
|
||||||
|
"mman",
|
||||||
|
"poll",
|
||||||
|
"fs",
|
||||||
|
"event",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.thiserror]
|
||||||
|
version = "2"
|
||||||
|
|
||||||
|
[build-dependencies.bindgen]
|
||||||
|
version = "0.72"
|
||||||
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.31", features = ["ioctl", "mman", "poll", "fs", "event"] }
|
||||||
|
bitflags = "2.4"
|
||||||
|
thiserror = "2"
|
||||||
|
log = "0.4.14"
|
||||||
|
enumn = "0.1.6"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
bindgen = "0.72"
|
||||||
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.
|
||||||
19
libs/v4l2r/README.md
Normal file
19
libs/v4l2r/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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 the vendored Linux UAPI headers in
|
||||||
|
`include/`.
|
||||||
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 `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, item_info: bindgen::callbacks::ItemInfo<'_>) -> Option<String> {
|
||||||
|
Some(item_info.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)
|
||||||
|
}
|
||||||
43
libs/v4l2r/build.rs
Normal file
43
libs/v4l2r/build.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
include!("bindgen.rs");
|
||||||
|
|
||||||
|
/// Vendored Linux UAPI include root.
|
||||||
|
const VENDORED_INCLUDE_DIR: &str = "include";
|
||||||
|
|
||||||
|
/// 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 include_root =
|
||||||
|
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` is not set"))
|
||||||
|
.join(VENDORED_INCLUDE_DIR);
|
||||||
|
let videodev2_h = include_root.join("linux/videodev2.h");
|
||||||
|
|
||||||
|
println!("cargo::rerun-if-changed={}", videodev2_h.display());
|
||||||
|
println!("cargo::rerun-if-changed={}", FIX753_H);
|
||||||
|
println!("cargo::rerun-if-changed={}", WRAPPER_H);
|
||||||
|
|
||||||
|
let clang_args = vec![
|
||||||
|
format!("-I{}", include_root.display()),
|
||||||
|
#[cfg(all(feature = "arch64", not(feature = "arch32")))]
|
||||||
|
"--target=x86_64-linux-gnu".into(),
|
||||||
|
#[cfg(all(feature = "arch32", not(feature = "arch64")))]
|
||||||
|
"--target=i686-linux-gnu".into(),
|
||||||
|
];
|
||||||
|
|
||||||
|
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!");
|
||||||
|
}
|
||||||
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
|
||||||
31
libs/v4l2r/include/asm-generic/bitsperlong.h
Normal file
31
libs/v4l2r/include/asm-generic/bitsperlong.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
#ifndef __ASM_GENERIC_BITS_PER_LONG
|
||||||
|
#define __ASM_GENERIC_BITS_PER_LONG
|
||||||
|
|
||||||
|
#ifndef __BITS_PER_LONG
|
||||||
|
/*
|
||||||
|
* In order to keep safe and avoid regression, only unify uapi
|
||||||
|
* bitsperlong.h for some archs which are using newer toolchains
|
||||||
|
* that have the definitions of __CHAR_BIT__ and __SIZEOF_LONG__.
|
||||||
|
* See the following link for more info:
|
||||||
|
* https://lore.kernel.org/linux-arch/b9624545-2c80-49a1-ac3c-39264a591f7b@app.fastmail.com/
|
||||||
|
*/
|
||||||
|
#if defined(__CHAR_BIT__) && defined(__SIZEOF_LONG__)
|
||||||
|
#define __BITS_PER_LONG (__CHAR_BIT__ * __SIZEOF_LONG__)
|
||||||
|
#else
|
||||||
|
/*
|
||||||
|
* There seems to be no way of detecting this automatically from user
|
||||||
|
* space, so 64 bit architectures should override this in their
|
||||||
|
* bitsperlong.h. In particular, an architecture that supports
|
||||||
|
* both 32 and 64 bit user space must not rely on CONFIG_64BIT
|
||||||
|
* to decide it, but rather check a compiler provided macro.
|
||||||
|
*/
|
||||||
|
#define __BITS_PER_LONG 32
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __BITS_PER_LONG_LONG
|
||||||
|
#define __BITS_PER_LONG_LONG 64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* __ASM_GENERIC_BITS_PER_LONG */
|
||||||
40
libs/v4l2r/include/asm-generic/int-ll64.h
Normal file
40
libs/v4l2r/include/asm-generic/int-ll64.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
/*
|
||||||
|
* asm-generic/int-ll64.h
|
||||||
|
*
|
||||||
|
* Integer declarations for architectures which use "long long"
|
||||||
|
* for 64-bit types.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _ASM_GENERIC_INT_LL64_H
|
||||||
|
#define _ASM_GENERIC_INT_LL64_H
|
||||||
|
|
||||||
|
#include <asm/bitsperlong.h>
|
||||||
|
|
||||||
|
#ifndef __ASSEMBLY__
|
||||||
|
/*
|
||||||
|
* __xx is ok: it doesn't pollute the POSIX namespace. Use these in the
|
||||||
|
* header files exported to user space
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef __signed__ char __s8;
|
||||||
|
typedef unsigned char __u8;
|
||||||
|
|
||||||
|
typedef __signed__ short __s16;
|
||||||
|
typedef unsigned short __u16;
|
||||||
|
|
||||||
|
typedef __signed__ int __s32;
|
||||||
|
typedef unsigned int __u32;
|
||||||
|
|
||||||
|
#ifdef __GNUC__
|
||||||
|
__extension__ typedef __signed__ long long __s64;
|
||||||
|
__extension__ typedef unsigned long long __u64;
|
||||||
|
#else
|
||||||
|
typedef __signed__ long long __s64;
|
||||||
|
typedef unsigned long long __u64;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* __ASSEMBLY__ */
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* _ASM_GENERIC_INT_LL64_H */
|
||||||
105
libs/v4l2r/include/asm-generic/ioctl.h
Normal file
105
libs/v4l2r/include/asm-generic/ioctl.h
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
#ifndef _ASM_GENERIC_IOCTL_H
|
||||||
|
#define _ASM_GENERIC_IOCTL_H
|
||||||
|
|
||||||
|
/* ioctl command encoding: 32 bits total, command in lower 16 bits,
|
||||||
|
* size of the parameter structure in the lower 14 bits of the
|
||||||
|
* upper 16 bits.
|
||||||
|
* Encoding the size of the parameter structure in the ioctl request
|
||||||
|
* is useful for catching programs compiled with old versions
|
||||||
|
* and to avoid overwriting user space outside the user buffer area.
|
||||||
|
* The highest 2 bits are reserved for indicating the ``access mode''.
|
||||||
|
* NOTE: This limits the max parameter size to 16kB -1 !
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The following is for compatibility across the various Linux
|
||||||
|
* platforms. The generic ioctl numbering scheme doesn't really enforce
|
||||||
|
* a type field. De facto, however, the top 8 bits of the lower 16
|
||||||
|
* bits are indeed used as a type field, so we might just as well make
|
||||||
|
* this explicit here. Please be sure to use the decoding macros
|
||||||
|
* below from now on.
|
||||||
|
*/
|
||||||
|
#define _IOC_NRBITS 8
|
||||||
|
#define _IOC_TYPEBITS 8
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Let any architecture override either of the following before
|
||||||
|
* including this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _IOC_SIZEBITS
|
||||||
|
# define _IOC_SIZEBITS 14
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef _IOC_DIRBITS
|
||||||
|
# define _IOC_DIRBITS 2
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define _IOC_NRMASK ((1 << _IOC_NRBITS)-1)
|
||||||
|
#define _IOC_TYPEMASK ((1 << _IOC_TYPEBITS)-1)
|
||||||
|
#define _IOC_SIZEMASK ((1 << _IOC_SIZEBITS)-1)
|
||||||
|
#define _IOC_DIRMASK ((1 << _IOC_DIRBITS)-1)
|
||||||
|
|
||||||
|
#define _IOC_NRSHIFT 0
|
||||||
|
#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS)
|
||||||
|
#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS)
|
||||||
|
#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Direction bits, which any architecture can choose to override
|
||||||
|
* before including this file.
|
||||||
|
*
|
||||||
|
* NOTE: _IOC_WRITE means userland is writing and kernel is
|
||||||
|
* reading. _IOC_READ means userland is reading and kernel is writing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _IOC_NONE
|
||||||
|
# define _IOC_NONE 0U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef _IOC_WRITE
|
||||||
|
# define _IOC_WRITE 1U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef _IOC_READ
|
||||||
|
# define _IOC_READ 2U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define _IOC(dir,type,nr,size) \
|
||||||
|
(((dir) << _IOC_DIRSHIFT) | \
|
||||||
|
((type) << _IOC_TYPESHIFT) | \
|
||||||
|
((nr) << _IOC_NRSHIFT) | \
|
||||||
|
((size) << _IOC_SIZESHIFT))
|
||||||
|
|
||||||
|
#define _IOC_TYPECHECK(t) (sizeof(t))
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Used to create numbers.
|
||||||
|
*
|
||||||
|
* NOTE: _IOW means userland is writing and kernel is reading. _IOR
|
||||||
|
* means userland is reading and kernel is writing.
|
||||||
|
*/
|
||||||
|
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
|
||||||
|
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
|
||||||
|
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
|
||||||
|
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
|
||||||
|
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
|
||||||
|
#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
|
||||||
|
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
|
||||||
|
|
||||||
|
/* used to decode ioctl numbers.. */
|
||||||
|
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
|
||||||
|
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
|
||||||
|
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
|
||||||
|
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
|
||||||
|
|
||||||
|
/* ...and for the drivers/sound files... */
|
||||||
|
|
||||||
|
#define IOC_IN (_IOC_WRITE << _IOC_DIRSHIFT)
|
||||||
|
#define IOC_OUT (_IOC_READ << _IOC_DIRSHIFT)
|
||||||
|
#define IOC_INOUT ((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT)
|
||||||
|
#define IOCSIZE_MASK (_IOC_SIZEMASK << _IOC_SIZESHIFT)
|
||||||
|
#define IOCSIZE_SHIFT (_IOC_SIZESHIFT)
|
||||||
|
|
||||||
|
#endif /* _ASM_GENERIC_IOCTL_H */
|
||||||
99
libs/v4l2r/include/asm-generic/posix_types.h
Normal file
99
libs/v4l2r/include/asm-generic/posix_types.h
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
#ifndef __ASM_GENERIC_POSIX_TYPES_H
|
||||||
|
#define __ASM_GENERIC_POSIX_TYPES_H
|
||||||
|
|
||||||
|
#include <asm/bitsperlong.h>
|
||||||
|
/*
|
||||||
|
* This file is generally used by user-level software, so you need to
|
||||||
|
* be a little careful about namespace pollution etc.
|
||||||
|
*
|
||||||
|
* First the types that are often defined in different ways across
|
||||||
|
* architectures, so that you can override them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __kernel_long_t
|
||||||
|
typedef long __kernel_long_t;
|
||||||
|
typedef unsigned long __kernel_ulong_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_ino_t
|
||||||
|
typedef __kernel_ulong_t __kernel_ino_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_mode_t
|
||||||
|
typedef unsigned int __kernel_mode_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_pid_t
|
||||||
|
typedef int __kernel_pid_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_ipc_pid_t
|
||||||
|
typedef int __kernel_ipc_pid_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_uid_t
|
||||||
|
typedef unsigned int __kernel_uid_t;
|
||||||
|
typedef unsigned int __kernel_gid_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_suseconds_t
|
||||||
|
typedef __kernel_long_t __kernel_suseconds_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_daddr_t
|
||||||
|
typedef int __kernel_daddr_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_uid32_t
|
||||||
|
typedef unsigned int __kernel_uid32_t;
|
||||||
|
typedef unsigned int __kernel_gid32_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_old_uid_t
|
||||||
|
typedef __kernel_uid_t __kernel_old_uid_t;
|
||||||
|
typedef __kernel_gid_t __kernel_old_gid_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_old_dev_t
|
||||||
|
typedef unsigned int __kernel_old_dev_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Most 32 bit architectures use "unsigned int" size_t,
|
||||||
|
* and all 64 bit architectures use "unsigned long" size_t.
|
||||||
|
*/
|
||||||
|
#ifndef __kernel_size_t
|
||||||
|
#if __BITS_PER_LONG != 64
|
||||||
|
typedef unsigned int __kernel_size_t;
|
||||||
|
typedef int __kernel_ssize_t;
|
||||||
|
typedef int __kernel_ptrdiff_t;
|
||||||
|
#else
|
||||||
|
typedef __kernel_ulong_t __kernel_size_t;
|
||||||
|
typedef __kernel_long_t __kernel_ssize_t;
|
||||||
|
typedef __kernel_long_t __kernel_ptrdiff_t;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __kernel_fsid_t
|
||||||
|
typedef struct {
|
||||||
|
int val[2];
|
||||||
|
} __kernel_fsid_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* anything below here should be completely generic
|
||||||
|
*/
|
||||||
|
typedef __kernel_long_t __kernel_off_t;
|
||||||
|
typedef long long __kernel_loff_t;
|
||||||
|
typedef __kernel_long_t __kernel_old_time_t;
|
||||||
|
typedef __kernel_long_t __kernel_time_t;
|
||||||
|
typedef long long __kernel_time64_t;
|
||||||
|
typedef __kernel_long_t __kernel_clock_t;
|
||||||
|
typedef int __kernel_timer_t;
|
||||||
|
typedef int __kernel_clockid_t;
|
||||||
|
typedef char * __kernel_caddr_t;
|
||||||
|
typedef unsigned short __kernel_uid16_t;
|
||||||
|
typedef unsigned short __kernel_gid16_t;
|
||||||
|
|
||||||
|
#endif /* __ASM_GENERIC_POSIX_TYPES_H */
|
||||||
9
libs/v4l2r/include/asm-generic/types.h
Normal file
9
libs/v4l2r/include/asm-generic/types.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
#ifndef _ASM_GENERIC_TYPES_H
|
||||||
|
#define _ASM_GENERIC_TYPES_H
|
||||||
|
/*
|
||||||
|
* int-ll64 is used everywhere now.
|
||||||
|
*/
|
||||||
|
#include <asm-generic/int-ll64.h>
|
||||||
|
|
||||||
|
#endif /* _ASM_GENERIC_TYPES_H */
|
||||||
53
libs/v4l2r/include/linux/const.h
Normal file
53
libs/v4l2r/include/linux/const.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
/* const.h: Macros for dealing with constants. */
|
||||||
|
|
||||||
|
#ifndef _LINUX_CONST_H
|
||||||
|
#define _LINUX_CONST_H
|
||||||
|
|
||||||
|
/* Some constant macros are used in both assembler and
|
||||||
|
* C code. Therefore we cannot annotate them always with
|
||||||
|
* 'UL' and other type specifiers unilaterally. We
|
||||||
|
* use the following macros to deal with this.
|
||||||
|
*
|
||||||
|
* Similarly, _AT() will cast an expression with a type in C, but
|
||||||
|
* leave it unchanged in asm.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef __ASSEMBLY__
|
||||||
|
#define _AC(X,Y) X
|
||||||
|
#define _AT(T,X) X
|
||||||
|
#else
|
||||||
|
#define __AC(X,Y) (X##Y)
|
||||||
|
#define _AC(X,Y) __AC(X,Y)
|
||||||
|
#define _AT(T,X) ((T)(X))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define _UL(x) (_AC(x, UL))
|
||||||
|
#define _ULL(x) (_AC(x, ULL))
|
||||||
|
|
||||||
|
#define _BITUL(x) (_UL(1) << (x))
|
||||||
|
#define _BITULL(x) (_ULL(1) << (x))
|
||||||
|
|
||||||
|
#if !defined(__ASSEMBLY__)
|
||||||
|
/*
|
||||||
|
* Missing __asm__ support
|
||||||
|
*
|
||||||
|
* __BIT128() would not work in the __asm__ code, as it shifts an
|
||||||
|
* 'unsigned __init128' data type as direct representation of
|
||||||
|
* 128 bit constants is not supported in the gcc compiler, as
|
||||||
|
* they get silently truncated.
|
||||||
|
*
|
||||||
|
* TODO: Please revisit this implementation when gcc compiler
|
||||||
|
* starts representing 128 bit constants directly like long
|
||||||
|
* and unsigned long etc. Subsequently drop the comment for
|
||||||
|
* GENMASK_U128() which would then start supporting __asm__ code.
|
||||||
|
*/
|
||||||
|
#define _BIT128(x) ((unsigned __int128)(1) << (x))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (__typeof__(x))(a) - 1)
|
||||||
|
#define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask))
|
||||||
|
|
||||||
|
#define __KERNEL_DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
|
||||||
|
|
||||||
|
#endif /* _LINUX_CONST_H */
|
||||||
8
libs/v4l2r/include/linux/ioctl.h
Normal file
8
libs/v4l2r/include/linux/ioctl.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
#ifndef _LINUX_IOCTL_H
|
||||||
|
#define _LINUX_IOCTL_H
|
||||||
|
|
||||||
|
#include <asm/ioctl.h>
|
||||||
|
|
||||||
|
#endif /* _LINUX_IOCTL_H */
|
||||||
|
|
||||||
38
libs/v4l2r/include/linux/posix_types.h
Normal file
38
libs/v4l2r/include/linux/posix_types.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
#ifndef _LINUX_POSIX_TYPES_H
|
||||||
|
#define _LINUX_POSIX_TYPES_H
|
||||||
|
|
||||||
|
#include <linux/stddef.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This allows for 1024 file descriptors: if NR_OPEN is ever grown
|
||||||
|
* beyond that you'll have to change this too. But 1024 fd's seem to be
|
||||||
|
* enough even for such "real" unices like OSF/1, so hopefully this is
|
||||||
|
* one limit that doesn't have to be changed [again].
|
||||||
|
*
|
||||||
|
* Note that POSIX wants the FD_CLEAR(fd,fdsetp) defines to be in
|
||||||
|
* <sys/time.h> (and thus <linux/time.h>) - but this is a more logical
|
||||||
|
* place for them. Solved by having dummy defines in <sys/time.h>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This macro may have been defined in <gnu/types.h>. But we always
|
||||||
|
* use the one here.
|
||||||
|
*/
|
||||||
|
#undef __FD_SETSIZE
|
||||||
|
#define __FD_SETSIZE 1024
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
|
||||||
|
} __kernel_fd_set;
|
||||||
|
|
||||||
|
/* Type of a signal handler. */
|
||||||
|
typedef void (*__kernel_sighandler_t)(int);
|
||||||
|
|
||||||
|
/* Type of a SYSV IPC key. */
|
||||||
|
typedef int __kernel_key_t;
|
||||||
|
typedef int __kernel_mqd_t;
|
||||||
|
|
||||||
|
#include <asm/posix_types.h>
|
||||||
|
|
||||||
|
#endif /* _LINUX_POSIX_TYPES_H */
|
||||||
73
libs/v4l2r/include/linux/stddef.h
Normal file
73
libs/v4l2r/include/linux/stddef.h
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
#ifndef _LINUX_STDDEF_H
|
||||||
|
#define _LINUX_STDDEF_H
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef __always_inline
|
||||||
|
#define __always_inline __inline__
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Not all C++ standards support type declarations inside an anonymous union */
|
||||||
|
#ifndef __cplusplus
|
||||||
|
#define __struct_group_tag(TAG) TAG
|
||||||
|
#else
|
||||||
|
#define __struct_group_tag(TAG)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __struct_group() - Create a mirrored named and anonyomous struct
|
||||||
|
*
|
||||||
|
* @TAG: The tag name for the named sub-struct (usually empty)
|
||||||
|
* @NAME: The identifier name of the mirrored sub-struct
|
||||||
|
* @ATTRS: Any struct attributes (usually empty)
|
||||||
|
* @MEMBERS: The member declarations for the mirrored structs
|
||||||
|
*
|
||||||
|
* Used to create an anonymous union of two structs with identical layout
|
||||||
|
* and size: one anonymous and one named. The former's members can be used
|
||||||
|
* normally without sub-struct naming, and the latter can be used to
|
||||||
|
* reason about the start, end, and size of the group of struct members.
|
||||||
|
* The named struct can also be explicitly tagged for layer reuse (C only),
|
||||||
|
* as well as both having struct attributes appended.
|
||||||
|
*/
|
||||||
|
#define __struct_group(TAG, NAME, ATTRS, MEMBERS...) \
|
||||||
|
union { \
|
||||||
|
struct { MEMBERS } ATTRS; \
|
||||||
|
struct __struct_group_tag(TAG) { MEMBERS } ATTRS NAME; \
|
||||||
|
} ATTRS
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
/* sizeof(struct{}) is 1 in C++, not 0, can't use C version of the macro. */
|
||||||
|
#define __DECLARE_FLEX_ARRAY(T, member) \
|
||||||
|
T member[0]
|
||||||
|
#else
|
||||||
|
/**
|
||||||
|
* __DECLARE_FLEX_ARRAY() - Declare a flexible array usable in a union
|
||||||
|
*
|
||||||
|
* @TYPE: The type of each flexible array element
|
||||||
|
* @NAME: The name of the flexible array member
|
||||||
|
*
|
||||||
|
* In order to have a flexible array member in a union or alone in a
|
||||||
|
* struct, it needs to be wrapped in an anonymous struct with at least 1
|
||||||
|
* named member, but that member can be empty.
|
||||||
|
*/
|
||||||
|
#define __DECLARE_FLEX_ARRAY(TYPE, NAME) \
|
||||||
|
struct { \
|
||||||
|
struct { } __empty_ ## NAME; \
|
||||||
|
TYPE NAME[]; \
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __counted_by
|
||||||
|
#define __counted_by(m)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __counted_by_le
|
||||||
|
#define __counted_by_le(m)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __counted_by_be
|
||||||
|
#define __counted_by_be(m)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* _LINUX_STDDEF_H */
|
||||||
58
libs/v4l2r/include/linux/types.h
Normal file
58
libs/v4l2r/include/linux/types.h
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||||
|
#ifndef _LINUX_TYPES_H
|
||||||
|
#define _LINUX_TYPES_H
|
||||||
|
|
||||||
|
#include <asm/types.h>
|
||||||
|
|
||||||
|
#ifndef __ASSEMBLY__
|
||||||
|
|
||||||
|
#include <linux/posix_types.h>
|
||||||
|
|
||||||
|
#ifdef __SIZEOF_INT128__
|
||||||
|
typedef __signed__ __int128 __s128 __attribute__((aligned(16)));
|
||||||
|
typedef unsigned __int128 __u128 __attribute__((aligned(16)));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Below are truly Linux-specific types that should never collide with
|
||||||
|
* any application/library that wants linux/types.h.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* sparse defines __CHECKER__; see Documentation/dev-tools/sparse.rst */
|
||||||
|
#ifdef __CHECKER__
|
||||||
|
#define __bitwise __attribute__((bitwise))
|
||||||
|
#else
|
||||||
|
#define __bitwise
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* The kernel doesn't use this legacy form, but user space does */
|
||||||
|
#define __bitwise__ __bitwise
|
||||||
|
|
||||||
|
typedef __u16 __bitwise __le16;
|
||||||
|
typedef __u16 __bitwise __be16;
|
||||||
|
typedef __u32 __bitwise __le32;
|
||||||
|
typedef __u32 __bitwise __be32;
|
||||||
|
typedef __u64 __bitwise __le64;
|
||||||
|
typedef __u64 __bitwise __be64;
|
||||||
|
|
||||||
|
typedef __u16 __bitwise __sum16;
|
||||||
|
typedef __u32 __bitwise __wsum;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* aligned_u64 should be used in defining kernel<->userspace ABIs to avoid
|
||||||
|
* common 32/64-bit compat problems.
|
||||||
|
* 64-bit values align to 4-byte boundaries on x86_32 (and possibly other
|
||||||
|
* architectures) and to 8-byte boundaries on 64-bit architectures. The new
|
||||||
|
* aligned_64 type enforces 8-byte alignment so that structs containing
|
||||||
|
* aligned_64 values have the same alignment on 32-bit and 64-bit architectures.
|
||||||
|
* No conversions are necessary between 32-bit user-space and a 64-bit kernel.
|
||||||
|
*/
|
||||||
|
#define __aligned_u64 __u64 __attribute__((aligned(8)))
|
||||||
|
#define __aligned_s64 __s64 __attribute__((aligned(8)))
|
||||||
|
#define __aligned_be64 __be64 __attribute__((aligned(8)))
|
||||||
|
#define __aligned_le64 __le64 __attribute__((aligned(8)))
|
||||||
|
|
||||||
|
typedef unsigned __bitwise __poll_t;
|
||||||
|
|
||||||
|
#endif /* __ASSEMBLY__ */
|
||||||
|
#endif /* _LINUX_TYPES_H */
|
||||||
69
libs/v4l2r/include/linux/v4l2-common.h
Normal file
69
libs/v4l2r/include/linux/v4l2-common.h
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/* SPDX-License-Identifier: ((GPL-2.0+ WITH Linux-syscall-note) OR BSD-3-Clause) */
|
||||||
|
/*
|
||||||
|
* include/linux/v4l2-common.h
|
||||||
|
*
|
||||||
|
* Common V4L2 and V4L2 subdev definitions.
|
||||||
|
*
|
||||||
|
* Users are advised to #include this file either through videodev2.h
|
||||||
|
* (V4L2) or through v4l2-subdev.h (V4L2 subdev) rather than to refer
|
||||||
|
* to this file directly.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012 Nokia Corporation
|
||||||
|
* Contact: Sakari Ailus <sakari.ailus@iki.fi>
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __V4L2_COMMON__
|
||||||
|
#define __V4L2_COMMON__
|
||||||
|
|
||||||
|
#include <linux/types.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Selection interface definitions
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Current cropping area */
|
||||||
|
#define V4L2_SEL_TGT_CROP 0x0000
|
||||||
|
/* Default cropping area */
|
||||||
|
#define V4L2_SEL_TGT_CROP_DEFAULT 0x0001
|
||||||
|
/* Cropping bounds */
|
||||||
|
#define V4L2_SEL_TGT_CROP_BOUNDS 0x0002
|
||||||
|
/* Native frame size */
|
||||||
|
#define V4L2_SEL_TGT_NATIVE_SIZE 0x0003
|
||||||
|
/* Current composing area */
|
||||||
|
#define V4L2_SEL_TGT_COMPOSE 0x0100
|
||||||
|
/* Default composing area */
|
||||||
|
#define V4L2_SEL_TGT_COMPOSE_DEFAULT 0x0101
|
||||||
|
/* Composing bounds */
|
||||||
|
#define V4L2_SEL_TGT_COMPOSE_BOUNDS 0x0102
|
||||||
|
/* Current composing area plus all padding pixels */
|
||||||
|
#define V4L2_SEL_TGT_COMPOSE_PADDED 0x0103
|
||||||
|
|
||||||
|
/* Selection flags */
|
||||||
|
#define V4L2_SEL_FLAG_GE (1 << 0)
|
||||||
|
#define V4L2_SEL_FLAG_LE (1 << 1)
|
||||||
|
#define V4L2_SEL_FLAG_KEEP_CONFIG (1 << 2)
|
||||||
|
|
||||||
|
struct v4l2_edid {
|
||||||
|
__u32 pad;
|
||||||
|
__u32 start_block;
|
||||||
|
__u32 blocks;
|
||||||
|
__u32 reserved[5];
|
||||||
|
__u8 *edid;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Backward compatibility target definitions --- to be removed. */
|
||||||
|
#define V4L2_SEL_TGT_CROP_ACTIVE V4L2_SEL_TGT_CROP
|
||||||
|
#define V4L2_SEL_TGT_COMPOSE_ACTIVE V4L2_SEL_TGT_COMPOSE
|
||||||
|
#define V4L2_SUBDEV_SEL_TGT_CROP_ACTUAL V4L2_SEL_TGT_CROP
|
||||||
|
#define V4L2_SUBDEV_SEL_TGT_COMPOSE_ACTUAL V4L2_SEL_TGT_COMPOSE
|
||||||
|
#define V4L2_SUBDEV_SEL_TGT_CROP_BOUNDS V4L2_SEL_TGT_CROP_BOUNDS
|
||||||
|
#define V4L2_SUBDEV_SEL_TGT_COMPOSE_BOUNDS V4L2_SEL_TGT_COMPOSE_BOUNDS
|
||||||
|
|
||||||
|
/* Backward compatibility flag definitions --- to be removed. */
|
||||||
|
#define V4L2_SUBDEV_SEL_FLAG_SIZE_GE V4L2_SEL_FLAG_GE
|
||||||
|
#define V4L2_SUBDEV_SEL_FLAG_SIZE_LE V4L2_SEL_FLAG_LE
|
||||||
|
#define V4L2_SUBDEV_SEL_FLAG_KEEP_CONFIG V4L2_SEL_FLAG_KEEP_CONFIG
|
||||||
|
|
||||||
|
#endif /* __V4L2_COMMON__ */
|
||||||
3499
libs/v4l2r/include/linux/v4l2-controls.h
Normal file
3499
libs/v4l2r/include/linux/v4l2-controls.h
Normal file
File diff suppressed because it is too large
Load Diff
2730
libs/v4l2r/include/linux/videodev2.h
Normal file
2730
libs/v4l2r/include/linux/videodev2.h
Normal file
File diff suppressed because it is too large
Load Diff
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
68
libs/v4l2r/src/ioctl/dqbuf.rs
Normal file
68
libs/v4l2r/src/ioctl/dqbuf.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
use crate::ioctl::ioctl_and_convert;
|
||||||
|
use crate::ioctl::IoctlConvertError;
|
||||||
|
use crate::ioctl::IoctlConvertResult;
|
||||||
|
use crate::ioctl::UncheckedV4l2Buffer;
|
||||||
|
use crate::memory::MemoryType;
|
||||||
|
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, memory: MemoryType) -> DqBufResult<O, O::Error>
|
||||||
|
where
|
||||||
|
O: TryFrom<UncheckedV4l2Buffer>,
|
||||||
|
O::Error: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let mut v4l2_buf = UncheckedV4l2Buffer::new_for_querybuf(queue, None);
|
||||||
|
v4l2_buf.0.memory = memory as u32;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
libs/v4l2r/v4l2r_wrapper.h
Normal file
4
libs/v4l2r/v4l2r_wrapper.h
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#include <linux/videodev2.h>
|
||||||
|
|
||||||
|
#define MARK_FIX_753(name) const unsigned long int Fix753_##name = name;
|
||||||
|
#include "fix753.h"
|
||||||
@@ -10,9 +10,7 @@ license = "GPL-2.0"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
thiserror = "1"
|
thiserror = "2"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -97,14 +97,14 @@ impl ExfatBootSector {
|
|||||||
let heap_sectors = volume_length as u32 - cluster_heap_offset;
|
let heap_sectors = volume_length as u32 - cluster_heap_offset;
|
||||||
let cluster_count = heap_sectors / sectors_per_cluster;
|
let cluster_count = heap_sectors / sectors_per_cluster;
|
||||||
|
|
||||||
// Calculate root directory cluster based on upcase table size
|
// Calculate root directory cluster based on bitmap and upcase table size.
|
||||||
// Cluster 2: Bitmap (1 cluster)
|
|
||||||
// Cluster 3...: Upcase table (128KB, may span multiple clusters)
|
|
||||||
// Next available: Root directory
|
|
||||||
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
|
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
|
||||||
|
let bitmap_size = ((cluster_count + 7) / 8) as u64;
|
||||||
|
let bitmap_clusters =
|
||||||
|
((bitmap_size + cluster_size as u64 - 1) / cluster_size as u64).max(1) as u32;
|
||||||
let upcase_clusters =
|
let upcase_clusters =
|
||||||
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
|
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
|
||||||
let first_cluster_of_root = 3 + upcase_clusters;
|
let first_cluster_of_root = 2 + bitmap_clusters + upcase_clusters;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
jump_boot: [0xEB, 0x76, 0x90],
|
jump_boot: [0xEB, 0x76, 0x90],
|
||||||
@@ -211,6 +211,15 @@ const ENTRY_TYPE_VOLUME_LABEL: u8 = 0x83;
|
|||||||
const ENTRY_TYPE_BITMAP: u8 = 0x81;
|
const ENTRY_TYPE_BITMAP: u8 = 0x81;
|
||||||
const ENTRY_TYPE_UPCASE: u8 = 0x82;
|
const ENTRY_TYPE_UPCASE: u8 = 0x82;
|
||||||
|
|
||||||
|
fn set_cluster_allocated(bitmap: &mut [u8], cluster: u32) {
|
||||||
|
let index = (cluster - 2) as usize;
|
||||||
|
let byte_idx = index / 8;
|
||||||
|
let bit_idx = index % 8;
|
||||||
|
if byte_idx < bitmap.len() {
|
||||||
|
bitmap[byte_idx] |= 1 << bit_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create volume label directory entry
|
/// Create volume label directory entry
|
||||||
fn create_volume_label_entry(label: &str) -> [u8; 32] {
|
fn create_volume_label_entry(label: &str) -> [u8; 32] {
|
||||||
let mut entry = [0u8; 32];
|
let mut entry = [0u8; 32];
|
||||||
@@ -301,27 +310,42 @@ pub fn format_exfat<W: Write + Seek>(
|
|||||||
let fat_offset = partition_offset + boot_sector.fat_offset as u64 * 512;
|
let fat_offset = partition_offset + boot_sector.fat_offset as u64 * 512;
|
||||||
writer.seek(SeekFrom::Start(fat_offset))?;
|
writer.seek(SeekFrom::Start(fat_offset))?;
|
||||||
|
|
||||||
|
let bitmap_size = (boot_sector.cluster_count + 7) / 8;
|
||||||
|
let bitmap_clusters =
|
||||||
|
((bitmap_size as u64 + cluster_size as u64 - 1) / cluster_size as u64).max(1) as u32;
|
||||||
|
|
||||||
// Calculate how many clusters the upcase table needs (128KB)
|
// Calculate how many clusters the upcase table needs (128KB)
|
||||||
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
|
const UPCASE_TABLE_SIZE: u64 = 128 * 1024;
|
||||||
let upcase_clusters =
|
let upcase_clusters =
|
||||||
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
|
((UPCASE_TABLE_SIZE + cluster_size as u64 - 1) / cluster_size as u64) as u32;
|
||||||
let root_cluster = 3 + upcase_clusters; // Root comes after bitmap and upcase
|
let bitmap_start_cluster = 2;
|
||||||
|
let upcase_start_cluster = bitmap_start_cluster + bitmap_clusters;
|
||||||
|
let root_cluster = upcase_start_cluster + upcase_clusters;
|
||||||
|
|
||||||
// FAT entries: cluster 0 and 1 are reserved
|
// FAT entries: cluster 0 and 1 are reserved
|
||||||
// 0: Media type (0xFFFFFFF8)
|
// 0: Media type (0xFFFFFFF8)
|
||||||
// 1: Reserved (0xFFFFFFFF)
|
// 1: Reserved (0xFFFFFFFF)
|
||||||
// 2: Bitmap cluster (single cluster, end of chain)
|
// 2..2+bitmap_clusters-1: Bitmap cluster chain
|
||||||
// 3..3+upcase_clusters-1: Upcase table cluster chain
|
// upcase_start_cluster..upcase_start_cluster+upcase_clusters-1: Upcase table cluster chain
|
||||||
// 3+upcase_clusters: Root directory cluster (end of chain)
|
// root_cluster: Root directory cluster (end of chain)
|
||||||
let mut fat_entries = vec![
|
let mut fat_entries = vec![
|
||||||
0xFFFFFFF8, // Media type
|
0xFFFFFFF8, // Media type
|
||||||
0xFFFFFFFF, // Reserved
|
0xFFFFFFFF, // Reserved
|
||||||
0xFFFFFFFF, // Bitmap (single cluster, end of chain)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Build allocation bitmap cluster chain
|
||||||
|
for i in 0..bitmap_clusters {
|
||||||
|
let cluster_num = bitmap_start_cluster + i;
|
||||||
|
if i == bitmap_clusters - 1 {
|
||||||
|
fat_entries.push(0xFFFFFFFF);
|
||||||
|
} else {
|
||||||
|
fat_entries.push(cluster_num + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build upcase table cluster chain
|
// Build upcase table cluster chain
|
||||||
for i in 0..upcase_clusters {
|
for i in 0..upcase_clusters {
|
||||||
let cluster_num = 3 + i;
|
let cluster_num = upcase_start_cluster + i;
|
||||||
if i == upcase_clusters - 1 {
|
if i == upcase_clusters - 1 {
|
||||||
// Last cluster in chain
|
// Last cluster in chain
|
||||||
fat_entries.push(0xFFFFFFFF);
|
fat_entries.push(0xFFFFFFFF);
|
||||||
@@ -345,38 +369,26 @@ pub fn format_exfat<W: Write + Seek>(
|
|||||||
// Calculate cluster heap offset
|
// Calculate cluster heap offset
|
||||||
let heap_offset = partition_offset + boot_sector.cluster_heap_offset as u64 * 512;
|
let heap_offset = partition_offset + boot_sector.cluster_heap_offset as u64 * 512;
|
||||||
|
|
||||||
// Cluster 2: Allocation Bitmap
|
// Allocation Bitmap
|
||||||
let bitmap_size = (boot_sector.cluster_count + 7) / 8;
|
let mut bitmap = vec![0u8; bitmap_clusters as usize * cluster_size as usize];
|
||||||
let _bitmap_clusters =
|
|
||||||
((bitmap_size as u64 + cluster_size as u64 - 1) / cluster_size as u64).max(1);
|
|
||||||
let mut bitmap = vec![0u8; cluster_size as usize];
|
|
||||||
|
|
||||||
// Mark clusters 2, 3..3+upcase_clusters-1, root_cluster as used
|
// Mark bitmap, upcase, and root directory clusters as used.
|
||||||
// Cluster 2: bitmap
|
// exFAT allocation bitmap bit 0 describes cluster 2.
|
||||||
bitmap[0] |= 0b00000100; // Bit 2
|
for i in 0..bitmap_clusters {
|
||||||
// Clusters 3..3+upcase_clusters-1: upcase table
|
set_cluster_allocated(&mut bitmap, bitmap_start_cluster + i);
|
||||||
|
}
|
||||||
for i in 0..upcase_clusters {
|
for i in 0..upcase_clusters {
|
||||||
let cluster = 3 + i;
|
set_cluster_allocated(&mut bitmap, upcase_start_cluster + i);
|
||||||
let byte_idx = (cluster / 8) as usize;
|
|
||||||
let bit_idx = cluster % 8;
|
|
||||||
if byte_idx < bitmap.len() {
|
|
||||||
bitmap[byte_idx] |= 1 << bit_idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Root directory cluster
|
|
||||||
let byte_idx = (root_cluster / 8) as usize;
|
|
||||||
let bit_idx = root_cluster % 8;
|
|
||||||
if byte_idx < bitmap.len() {
|
|
||||||
bitmap[byte_idx] |= 1 << bit_idx;
|
|
||||||
}
|
}
|
||||||
|
set_cluster_allocated(&mut bitmap, root_cluster);
|
||||||
|
|
||||||
writer.seek(SeekFrom::Start(heap_offset))?;
|
writer.seek(SeekFrom::Start(heap_offset))?;
|
||||||
writer.write_all(&bitmap)?;
|
writer.write_all(&bitmap)?;
|
||||||
|
|
||||||
// Cluster 3..3+upcase_clusters-1: Upcase table
|
// Upcase table
|
||||||
let upcase_data = generate_upcase_table();
|
let upcase_data = generate_upcase_table();
|
||||||
let upcase_checksum = calculate_upcase_checksum(&upcase_data);
|
let upcase_checksum = calculate_upcase_checksum(&upcase_data);
|
||||||
let upcase_offset = heap_offset + cluster_size as u64; // Start at cluster 3
|
let upcase_offset = heap_offset + bitmap_clusters as u64 * cluster_size as u64;
|
||||||
writer.seek(SeekFrom::Start(upcase_offset))?;
|
writer.seek(SeekFrom::Start(upcase_offset))?;
|
||||||
writer.write_all(&upcase_data)?;
|
writer.write_all(&upcase_data)?;
|
||||||
|
|
||||||
@@ -388,13 +400,18 @@ pub fn format_exfat<W: Write + Seek>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Root directory cluster
|
// Root directory cluster
|
||||||
let root_offset = heap_offset + (1 + upcase_clusters as u64) * cluster_size as u64;
|
let root_offset =
|
||||||
|
heap_offset + (bitmap_clusters as u64 + upcase_clusters as u64) * cluster_size as u64;
|
||||||
writer.seek(SeekFrom::Start(root_offset))?;
|
writer.seek(SeekFrom::Start(root_offset))?;
|
||||||
|
|
||||||
// Write directory entries
|
// Write directory entries
|
||||||
let volume_label_entry = create_volume_label_entry(label);
|
let volume_label_entry = create_volume_label_entry(label);
|
||||||
let bitmap_entry = create_bitmap_entry(2, bitmap_size as u64);
|
let bitmap_entry = create_bitmap_entry(bitmap_start_cluster, bitmap_size as u64);
|
||||||
let upcase_entry = create_upcase_entry(3, upcase_data.len() as u64, upcase_checksum);
|
let upcase_entry = create_upcase_entry(
|
||||||
|
upcase_start_cluster,
|
||||||
|
upcase_data.len() as u64,
|
||||||
|
upcase_checksum,
|
||||||
|
);
|
||||||
|
|
||||||
writer.write_all(&volume_label_entry)?;
|
writer.write_all(&volume_label_entry)?;
|
||||||
writer.write_all(&bitmap_entry)?;
|
writer.write_all(&bitmap_entry)?;
|
||||||
@@ -413,6 +430,9 @@ pub fn format_exfat<W: Write + Seek>(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::partition::PartitionLayout;
|
||||||
|
use std::io::Read;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cluster_size() {
|
fn test_cluster_size() {
|
||||||
@@ -428,4 +448,120 @@ mod tests {
|
|||||||
assert_eq!(get_cluster_size(8 * 1024 * 1024 * 1024 / 512), 131072); // 8GB → 128KB
|
assert_eq!(get_cluster_size(8 * 1024 * 1024 * 1024 / 512), 131072); // 8GB → 128KB
|
||||||
assert_eq!(get_cluster_size(16 * 1024 * 1024 * 1024 / 512), 131072); // 16GB → 128KB
|
assert_eq!(get_cluster_size(16 * 1024 * 1024 * 1024 / 512), 131072); // 16GB → 128KB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_bitmap_uses_cluster_heap_bit_indices() {
|
||||||
|
let temp_file = NamedTempFile::new().unwrap();
|
||||||
|
let path = temp_file.path();
|
||||||
|
let size = 64 * 1024 * 1024u64;
|
||||||
|
let layout = PartitionLayout::calculate(size).unwrap();
|
||||||
|
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(path)
|
||||||
|
.unwrap();
|
||||||
|
file.set_len(size).unwrap();
|
||||||
|
format_exfat(&mut file, layout.data_offset(), layout.data_size(), "TEST").unwrap();
|
||||||
|
|
||||||
|
let mut boot_sector = [0u8; 512];
|
||||||
|
file.seek(SeekFrom::Start(layout.data_offset())).unwrap();
|
||||||
|
file.read_exact(&mut boot_sector).unwrap();
|
||||||
|
let cluster_heap_offset = u32::from_le_bytes(boot_sector[88..92].try_into().unwrap());
|
||||||
|
let first_cluster_of_root = u32::from_le_bytes(boot_sector[96..100].try_into().unwrap());
|
||||||
|
let sectors_per_cluster = 1u64 << boot_sector[109];
|
||||||
|
let cluster_size = sectors_per_cluster * 512;
|
||||||
|
|
||||||
|
let bitmap_offset = layout.data_offset() + cluster_heap_offset as u64 * 512;
|
||||||
|
let mut bitmap = vec![0u8; cluster_size as usize];
|
||||||
|
file.seek(SeekFrom::Start(bitmap_offset)).unwrap();
|
||||||
|
file.read_exact(&mut bitmap).unwrap();
|
||||||
|
|
||||||
|
let is_allocated = |cluster: u32| {
|
||||||
|
let index = (cluster - 2) as usize;
|
||||||
|
(bitmap[index / 8] & (1 << (index % 8))) != 0
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
is_allocated(2),
|
||||||
|
"allocation bitmap cluster must be allocated"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
is_allocated(3),
|
||||||
|
"upcase table first cluster must be allocated"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
is_allocated(first_cluster_of_root),
|
||||||
|
"root directory cluster must be allocated"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!is_allocated(first_cluster_of_root + 1),
|
||||||
|
"first data cluster after root should be free after formatting"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_supports_multi_cluster_allocation_bitmap() {
|
||||||
|
let temp_file = NamedTempFile::new().unwrap();
|
||||||
|
let path = temp_file.path();
|
||||||
|
let size = 240 * 1024 * 1024u64;
|
||||||
|
let layout = PartitionLayout::calculate(size).unwrap();
|
||||||
|
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(path)
|
||||||
|
.unwrap();
|
||||||
|
file.set_len(size).unwrap();
|
||||||
|
format_exfat(&mut file, layout.data_offset(), layout.data_size(), "TEST").unwrap();
|
||||||
|
|
||||||
|
let mut boot_sector = [0u8; 512];
|
||||||
|
file.seek(SeekFrom::Start(layout.data_offset())).unwrap();
|
||||||
|
file.read_exact(&mut boot_sector).unwrap();
|
||||||
|
let fat_offset = u32::from_le_bytes(boot_sector[80..84].try_into().unwrap());
|
||||||
|
let cluster_heap_offset = u32::from_le_bytes(boot_sector[88..92].try_into().unwrap());
|
||||||
|
let cluster_count = u32::from_le_bytes(boot_sector[92..96].try_into().unwrap());
|
||||||
|
let first_cluster_of_root = u32::from_le_bytes(boot_sector[96..100].try_into().unwrap());
|
||||||
|
let sectors_per_cluster = 1u64 << boot_sector[109];
|
||||||
|
let cluster_size = sectors_per_cluster * 512;
|
||||||
|
|
||||||
|
let bitmap_size = ((cluster_count + 7) / 8) as u64;
|
||||||
|
let bitmap_clusters = bitmap_size.div_ceil(cluster_size) as u32;
|
||||||
|
assert!(
|
||||||
|
bitmap_clusters > 1,
|
||||||
|
"test volume should require a multi-cluster allocation bitmap"
|
||||||
|
);
|
||||||
|
|
||||||
|
let upcase_clusters = (128 * 1024u64).div_ceil(cluster_size) as u32;
|
||||||
|
assert_eq!(first_cluster_of_root, 2 + bitmap_clusters + upcase_clusters);
|
||||||
|
|
||||||
|
let read_fat = |file: &mut std::fs::File, cluster: u32| -> u32 {
|
||||||
|
let offset = layout.data_offset() + fat_offset as u64 * 512 + cluster as u64 * 4;
|
||||||
|
let mut bytes = [0u8; 4];
|
||||||
|
file.seek(SeekFrom::Start(offset)).unwrap();
|
||||||
|
file.read_exact(&mut bytes).unwrap();
|
||||||
|
u32::from_le_bytes(bytes)
|
||||||
|
};
|
||||||
|
assert_eq!(read_fat(&mut file, 2), 3);
|
||||||
|
assert_eq!(read_fat(&mut file, 2 + bitmap_clusters - 1), 0xFFFFFFFF);
|
||||||
|
|
||||||
|
let root_offset = layout.data_offset()
|
||||||
|
+ cluster_heap_offset as u64 * 512
|
||||||
|
+ (first_cluster_of_root - 2) as u64 * cluster_size;
|
||||||
|
let mut root = vec![0u8; cluster_size as usize];
|
||||||
|
file.seek(SeekFrom::Start(root_offset)).unwrap();
|
||||||
|
file.read_exact(&mut root).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(root[32], ENTRY_TYPE_BITMAP);
|
||||||
|
assert_eq!(u32::from_le_bytes(root[52..56].try_into().unwrap()), 2);
|
||||||
|
assert_eq!(
|
||||||
|
u64::from_le_bytes(root[56..64].try_into().unwrap()),
|
||||||
|
bitmap_size
|
||||||
|
);
|
||||||
|
assert_eq!(root[64], ENTRY_TYPE_UPCASE);
|
||||||
|
assert_eq!(
|
||||||
|
u32::from_le_bytes(root[84..88].try_into().unwrap()),
|
||||||
|
2 + bitmap_clusters
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -28,8 +28,15 @@ impl VentoyImage {
|
|||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create sparse file
|
// Build beside the destination so a failed create cannot leave a partial image.
|
||||||
let mut file = File::create(path)?;
|
let parent = path
|
||||||
|
.parent()
|
||||||
|
.filter(|parent| !parent.as_os_str().is_empty())
|
||||||
|
.unwrap_or_else(|| Path::new("."));
|
||||||
|
let mut temp = tempfile::Builder::new()
|
||||||
|
.prefix(".ventoy-")
|
||||||
|
.tempfile_in(parent)?;
|
||||||
|
let mut file = temp.as_file_mut();
|
||||||
file.set_len(size)?;
|
file.set_len(size)?;
|
||||||
|
|
||||||
// Write boot code
|
// Write boot code
|
||||||
@@ -64,6 +71,8 @@ impl VentoyImage {
|
|||||||
format_exfat(&mut file, layout.data_offset(), layout.data_size(), label)?;
|
format_exfat(&mut file, layout.data_offset(), layout.data_size(), label)?;
|
||||||
|
|
||||||
file.flush()?;
|
file.flush()?;
|
||||||
|
temp.persist(path)
|
||||||
|
.map_err(|error| VentoyError::Io(error.error))?;
|
||||||
|
|
||||||
println!("[INFO] Ventoy IMG created successfully!");
|
println!("[INFO] Ventoy IMG created successfully!");
|
||||||
|
|
||||||
@@ -274,3 +283,20 @@ impl VentoyImage {
|
|||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_create_does_not_publish_partial_image() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("ventoy.img");
|
||||||
|
|
||||||
|
let error = VentoyImage::create(&path, "64M", "TEST").err().unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(error, VentoyError::ResourceNotFound(_)));
|
||||||
|
assert!(!path.exists());
|
||||||
|
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ license = "BSD-3-Clause"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
bindgen = "0.59"
|
bindgen = "0.72"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
// Read the current source path when running, since build artifacts may move.
|
||||||
|
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let cpp_dir = manifest_dir.join("cpp");
|
let cpp_dir = manifest_dir.join("cpp");
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=cpp/yuv_ffi.h");
|
println!("cargo:rerun-if-changed=cpp/yuv_ffi.h");
|
||||||
@@ -19,9 +20,9 @@ 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 builder = bindgen::builder().header(ffi_header.to_string_lossy().to_string());
|
||||||
|
|
||||||
bindgen::builder()
|
builder
|
||||||
.header(ffi_header.to_string_lossy().to_string())
|
|
||||||
// YUYV conversions
|
// YUYV conversions
|
||||||
.allowlist_function("YUY2ToI420")
|
.allowlist_function("YUY2ToI420")
|
||||||
.allowlist_function("YUY2ToNV12")
|
.allowlist_function("YUY2ToNV12")
|
||||||
@@ -38,6 +39,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 +51,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 +65,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,6 +87,21 @@ fn generate_bindings(cpp_dir: &Path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn link_libyuv() {
|
fn link_libyuv() {
|
||||||
|
println!("cargo:rerun-if-env-changed=ONE_KVM_LIBS_PATH");
|
||||||
|
println!("cargo:rerun-if-env-changed=LIBYUV_STATIC");
|
||||||
|
|
||||||
|
// An explicit library root must take precedence over host discovery.
|
||||||
|
if env::var("ONE_KVM_LIBS_PATH")
|
||||||
|
.ok()
|
||||||
|
.is_some_and(|path| !path.trim().is_empty())
|
||||||
|
{
|
||||||
|
if link_system() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("libyuv not found under ONE_KVM_LIBS_PATH");
|
||||||
|
}
|
||||||
|
|
||||||
// Try vcpkg first
|
// Try vcpkg first
|
||||||
if let Some(vcpkg_installed) = vcpkg_installed_root() {
|
if let Some(vcpkg_installed) = vcpkg_installed_root() {
|
||||||
if link_vcpkg(vcpkg_installed) {
|
if link_vcpkg(vcpkg_installed) {
|
||||||
@@ -169,14 +190,17 @@ 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");
|
||||||
#[cfg(target_os = "linux")]
|
link_libjpeg_for_static_libyuv(&[lib_path.clone()], &target_os);
|
||||||
|
if target_os == "linux" {
|
||||||
println!("cargo:rustc-link-lib=stdc++");
|
println!("cargo:rustc-link-lib=stdc++");
|
||||||
|
}
|
||||||
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");
|
||||||
#[cfg(target_os = "linux")]
|
if target_os == "linux" {
|
||||||
println!("cargo:rustc-link-lib=stdc++");
|
println!("cargo:rustc-link-lib=stdc++");
|
||||||
|
}
|
||||||
println!("cargo:info=Using libyuv from vcpkg (dynamic linking)");
|
println!("cargo:info=Using libyuv from vcpkg (dynamic linking)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,9 +248,13 @@ fn link_system() -> bool {
|
|||||||
// Build custom library paths based on target architecture:
|
// Build custom library paths based on target architecture:
|
||||||
// 1. Check ONE_KVM_LIBS_PATH environment variable (explicit override)
|
// 1. Check ONE_KVM_LIBS_PATH environment variable (explicit override)
|
||||||
// 2. Fall back to architecture-based detection
|
// 2. Fall back to architecture-based detection
|
||||||
let custom_lib_path = if let Ok(path) = env::var("ONE_KVM_LIBS_PATH") {
|
let explicit_lib_root = env::var("ONE_KVM_LIBS_PATH")
|
||||||
format!("{}/lib", path)
|
.ok()
|
||||||
} else {
|
.filter(|path| !path.trim().is_empty());
|
||||||
|
let custom_lib_path = explicit_lib_root
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| format!("{}/lib", path))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
match target_arch.as_str() {
|
match target_arch.as_str() {
|
||||||
"x86_64" => "/usr/local/lib",
|
"x86_64" => "/usr/local/lib",
|
||||||
"aarch64" => "/usr/aarch64-linux-gnu/lib",
|
"aarch64" => "/usr/aarch64-linux-gnu/lib",
|
||||||
@@ -234,7 +262,7 @@ fn link_system() -> bool {
|
|||||||
_ => "",
|
_ => "",
|
||||||
}
|
}
|
||||||
.to_string()
|
.to_string()
|
||||||
};
|
});
|
||||||
|
|
||||||
// Try common system library paths (custom paths first)
|
// Try common system library paths (custom paths first)
|
||||||
let mut lib_paths: Vec<String> = Vec::new();
|
let mut lib_paths: Vec<String> = Vec::new();
|
||||||
@@ -244,7 +272,8 @@ fn link_system() -> bool {
|
|||||||
lib_paths.push(custom_lib_path);
|
lib_paths.push(custom_lib_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then standard paths
|
// An explicit root is strict: do not silently fall back to host libraries.
|
||||||
|
if explicit_lib_root.is_none() {
|
||||||
lib_paths.extend(
|
lib_paths.extend(
|
||||||
[
|
[
|
||||||
"/usr/local/lib", // Custom builds
|
"/usr/local/lib", // Custom builds
|
||||||
@@ -258,6 +287,7 @@ fn link_system() -> bool {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.to_string()),
|
.map(|s| s.to_string()),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for path in &lib_paths {
|
for path in &lib_paths {
|
||||||
let lib_path = Path::new(path);
|
let lib_path = Path::new(path);
|
||||||
@@ -268,6 +298,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)",
|
||||||
@@ -294,3 +325,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);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user