diff --git a/.gitignore b/.gitignore
index 7158bec6..725f1869 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,4 @@ CLAUDE.md
# Secrets (compile-time configuration)
secrets.toml
+.env
diff --git a/Cargo.toml b/Cargo.toml
index a95b5b8f..c4f7dc1a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "one-kvm"
-version = "0.1.1"
+version = "0.1.4"
edition = "2021"
authors = ["SilentWind"]
description = "A open and lightweight IP-KVM solution written in Rust"
@@ -129,6 +129,7 @@ tempfile = "3"
[build-dependencies]
protobuf-codegen = "3.7"
toml = "0.9"
+cc = "1"
[profile.release]
opt-level = 3
diff --git a/README.md b/README.md
index 8ead0e9c..4f1f91e6 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-
One-KVM
Rust 编写的开放轻量 IP-KVM 解决方案,实现 BIOS 级远程管理
@@ -19,16 +18,6 @@
---
-## 📋 目录
-
-- [项目概述](#项目概述)
-- [迁移说明](#迁移说明)
-- [功能介绍](#功能介绍)
-- [快速开始](#快速开始)
-- [贡献与反馈](#贡献与反馈)
-- [致谢](#致谢)
-- [许可证](#许可证)
-
## 📖 项目概述
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
@@ -66,7 +55,7 @@
- **VAAPI**:Intel/AMD GPU
- **RKMPP**:Rockchip SoC
-- **V4L2 M2M**:RaspberryPi
+- **V4L2 M2M**:通用硬件编码器(尚未实现)
- **软件编码**:CPU 编码
### 扩展能力
@@ -74,85 +63,14 @@
- Web UI 配置,多语言支持(中文/英文)
- 内置 Web 终端(ttyd)内网穿透支持(gostc)、P2P 组网支持(EasyTier)、RustDesk 协议集成(用于跨平台远程访问能力扩展)
-## ⚡ 快速开始
+## ⚡ 安装使用
-安装方式:Docker / DEB 软件包 / 飞牛 NAS(FPK)。
-
-### 方式一:Docker 安装(推荐)
-
-前提条件:
-
-- Linux 主机已安装 Docker
-- 插好 USB HDMI 采集卡
-- 启用 USB OTG 或插好 CH340+CH9329 HID 线(用于 HID 模拟)
-
-启动容器:
-
-```bash
-docker run --name one-kvm -itd --privileged=true \
- -v /dev:/dev -v /sys/:/sys \
- --net=host \
- silentwind0/one-kvm
-```
-
-访问 Web 界面:`http://<设备IP>:8080`(首次访问会引导创建管理员账户)。默认端口:HTTP `8080`;启用 HTTPS 后为 `8443`。
-
-#### 常用环境变量(Docker)
-
-| 变量名 | 默认值 | 说明 |
-|------|------|------|
-| `ENABLE_HTTPS` | `false` | 是否启用 HTTPS(`true/false`) |
-| `HTTP_PORT` | `8080` | HTTP 端口(`ENABLE_HTTPS=false` 时生效) |
-| `HTTPS_PORT` | `8443` | HTTPS 端口(`ENABLE_HTTPS=true` 时生效) |
-| `BIND_ADDRESS` | - | 监听地址(如 `0.0.0.0`) |
-| `VERBOSE` | `0` | 日志详细程度:`1`(-v)、`2`(-vv)、`3`(-vvv) |
-| `DATA_DIR` | `/etc/one-kvm` | 数据目录(等价于 `one-kvm -d
`,优先级高于 `ONE_KVM_DATA_DIR`) |
-
-> 说明:`--privileged=true` 和挂载 `/dev`、`/sys` 是硬件访问所需配置,当前版本不可省略。
->
-> 兼容性:同时支持旧变量名 `ONE_KVM_DATA_DIR`。
->
-> HTTPS:未提供证书时会自动生成默认自签名证书。
->
-> Ventoy:若修改 `DATA_DIR`,请确保 Ventoy 资源文件位于 `${DATA_DIR}/ventoy`(`boot.img`、`core.img`、`ventoy.disk.img`)。
-
-### 方式二:DEB 软件包安装
-
-前提条件:
-
-- Debian 11+ / Ubuntu 22+
-- 插好 USB HDMI 采集卡、HID 线(OTG 或 CH340+CH9329)
-
-安装步骤:
-
-1. 从 GitHub Releases 下载适合架构的 `one-kvm_*.deb`:[Releases](https://github.com/mofeng-git/One-KVM/releases)
-2. 安装:
-
-```bash
-sudo apt update
-sudo apt install ./one-kvm_*_*.deb
-```
-
-访问 Web 界面:`http://<设备IP>:8080`。
-
-### 方式三:飞牛 NAS(FPK)安装
-
-前提条件:
-
-- 飞牛 NAS 系统(目前仅支持 x86_64 架构)
-- 插好 USB HDMI 采集卡、CH340+CH9329 HID 线
-
-安装步骤:
-
-1. 从 GitHub Releases 下载 `*.fpk` 软件包:[Releases](https://github.com/mofeng-git/One-KVM/releases)
-2. 在飞牛应用商店选择“手动安装”,导入 `*.fpk`
-
-访问 Web 界面:`http://<设备IP>:8420`。
+可以访问 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/) 获取详细信息。
## 报告问题
如果您发现了问题,请:
-1. 使用 [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) 报告
+1. 使用 [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) 报告,或加入 QQ 群聊反馈。
2. 提供详细的错误信息和复现步骤
3. 包含您的硬件配置和系统信息
@@ -269,6 +187,14 @@ sudo apt install ./one-kvm_*_*.deb
- 葱
+- MaxZ
+
+- 爱发电用户_c5f33
+
+- 爱发电用户_09386
+
+- 爱发电用户_JT6c
+
- ......
@@ -277,11 +203,6 @@ sudo apt install ./one-kvm_*_*.deb
本项目得到以下赞助商的支持:
-**CDN 加速及安全防护:**
-- **[Tencent EdgeOne](https://edgeone.ai/zh?from=github)** - 提供 CDN 加速及安全防护服务
-
-
-
**文件存储服务:**
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
diff --git a/build/Dockerfile.runtime b/build/Dockerfile.runtime
index 44617adf..ccdc1658 100644
--- a/build/Dockerfile.runtime
+++ b/build/Dockerfile.runtime
@@ -39,7 +39,7 @@ RUN apt-get update && \
COPY --chmod=755 init.sh /init.sh
# Copy binaries (these are placed by the build script)
-COPY --chmod=755 one-kvm ttyd gostc easytier-core /usr/bin/
+COPY --chmod=755 one-kvm ttyd /usr/bin/
# Copy ventoy resources if they exist
COPY ventoy/ /etc/one-kvm/ventoy/
diff --git a/build/Dockerfile.runtime-full b/build/Dockerfile.runtime-full
new file mode 100644
index 00000000..32428305
--- /dev/null
+++ b/build/Dockerfile.runtime-full
@@ -0,0 +1,48 @@
+# One-KVM Runtime Image (full)
+# This Dockerfile only packages pre-compiled binaries (no compilation)
+# Used after cross-compiling with `cross build`
+# Using Debian 11 for maximum compatibility (GLIBC 2.31)
+
+ARG TARGETPLATFORM=linux/amd64
+
+FROM debian:11-slim
+
+ARG TARGETPLATFORM
+
+# Install runtime dependencies in a single layer
+# All codec libraries (libx264, libx265, libopus) are now statically linked
+# Only hardware acceleration drivers and core system libraries remain dynamic
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ # Core runtime (all platforms) - no codec libs needed
+ ca-certificates \
+ libudev1 \
+ libasound2 \
+ # v4l2 is handled by kernel, minimal userspace needed
+ libv4l-0 \
+ && \
+ # Platform-specific hardware acceleration
+ if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
+ apt-get install -y --no-install-recommends \
+ libva2 libva-drm2 libva-x11-2 libx11-6 libxcb1 libxau6 libxdmcp6 libmfx1; \
+ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
+ apt-get install -y --no-install-recommends \
+ libdrm2 libva2; \
+ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
+ apt-get install -y --no-install-recommends \
+ libdrm2 libva2; \
+ fi && \
+ rm -rf /var/lib/apt/lists/* && \
+ mkdir -p /etc/one-kvm/ventoy
+
+# Copy init script
+COPY --chmod=755 init.sh /init.sh
+
+# Copy binaries (these are placed by the build script)
+COPY --chmod=755 one-kvm ttyd gostc easytier-core /usr/bin/
+
+# Copy ventoy resources if they exist
+COPY ventoy/ /etc/one-kvm/ventoy/
+
+# Entrypoint
+CMD ["/init.sh"]
diff --git a/build/package-docker.sh b/build/package-docker.sh
index ceb1217d..94c9ba82 100755
--- a/build/package-docker.sh
+++ b/build/package-docker.sh
@@ -25,11 +25,13 @@ echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Configuration
REGISTRY="${REGISTRY:-}" # e.g., docker.io/username or ghcr.io/username
-IMAGE_NAME="${IMAGE_NAME:-one-kvm}"
+IMAGE_NAME="${IMAGE_NAME:-}"
TAG="${TAG:-latest}"
+VARIANT="${VARIANT:-minimal}"
+INCLUDE_THIRD_PARTY=false
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
-STAGING_DIR="$PROJECT_ROOT/build-staging"
+BASE_STAGING_DIR="$PROJECT_ROOT/build-staging"
# Full image name with registry
get_full_image_name() {
@@ -77,6 +79,18 @@ while [[ $# -gt 0 ]]; do
REGISTRY="$2"
shift 2
;;
+ --image-name)
+ IMAGE_NAME="$2"
+ shift 2
+ ;;
+ --variant)
+ VARIANT="$2"
+ shift 2
+ ;;
+ --full)
+ VARIANT="full"
+ shift
+ ;;
--build)
BUILD_BINARY=true
shift
@@ -91,9 +105,12 @@ while [[ $# -gt 0 ]]; do
echo " Use comma to specify multiple: linux/amd64,linux/arm64"
echo " Default: $DEFAULT_PLATFORM"
echo " --registry REGISTRY Container registry (e.g., docker.io/user, ghcr.io/user)"
+ echo " --image-name NAME Override image name (default: one-kvm or one-kvm-full)"
echo " --push Push image to registry"
echo " --load Load image to local Docker (single platform only)"
echo " --tag TAG Image tag (default: latest)"
+ echo " --variant VARIANT Image variant: minimal or full (default: minimal)"
+ echo " --full Shortcut for --variant full"
echo " --build Also build the binary with cross (optional)"
echo " --help Show this help"
echo ""
@@ -101,6 +118,9 @@ while [[ $# -gt 0 ]]; do
echo " # Build for current platform and load locally"
echo " $0 --platform linux/arm64 --load"
echo ""
+ echo " # Build full image (includes gostc + easytier)"
+ echo " $0 --variant full --platform linux/arm64 --load"
+ echo ""
echo " # Build and push single platform"
echo " $0 --platform linux/arm64 --registry docker.io/user --push"
echo ""
@@ -115,6 +135,28 @@ while [[ $# -gt 0 ]]; do
esac
done
+# Normalize variant and image name
+case "$VARIANT" in
+ minimal)
+ INCLUDE_THIRD_PARTY=false
+ ;;
+ full)
+ INCLUDE_THIRD_PARTY=true
+ ;;
+ *)
+ echo_error "Unknown variant: $VARIANT (expected: minimal or full)"
+ exit 1
+ ;;
+esac
+
+if [ -z "$IMAGE_NAME" ]; then
+ if [ "$VARIANT" = "full" ]; then
+ IMAGE_NAME="one-kvm-full"
+ else
+ IMAGE_NAME="one-kvm"
+ fi
+fi
+
# Default platform
if [ -z "$PLATFORMS" ]; then
PLATFORMS="$DEFAULT_PLATFORM"
@@ -176,21 +218,23 @@ download_tools() {
chmod +x "$staging/ttyd"
fi
- # gostc
- if [ ! -f "$staging/gostc" ]; then
- curl -fsSL "$GOSTC_URL" -o /tmp/gostc.tar.gz
- tar -xzf /tmp/gostc.tar.gz -C "$staging"
- chmod +x "$staging/gostc"
- rm /tmp/gostc.tar.gz
- fi
+ if [ "$INCLUDE_THIRD_PARTY" = true ]; then
+ # gostc
+ if [ ! -f "$staging/gostc" ]; then
+ curl -fsSL "$GOSTC_URL" -o /tmp/gostc.tar.gz
+ tar -xzf /tmp/gostc.tar.gz -C "$staging"
+ chmod +x "$staging/gostc"
+ rm /tmp/gostc.tar.gz
+ fi
- # easytier
- if [ ! -f "$staging/easytier-core" ]; then
- curl -fsSL "$EASYTIER_URL" -o /tmp/easytier.zip
- unzip -o /tmp/easytier.zip -d /tmp/easytier
- cp "/tmp/easytier/$EASYTIER_DIR/easytier-core" "$staging/easytier-core"
- chmod +x "$staging/easytier-core"
- rm -rf /tmp/easytier.zip /tmp/easytier
+ # easytier
+ if [ ! -f "$staging/easytier-core" ]; then
+ curl -fsSL "$EASYTIER_URL" -o /tmp/easytier.zip
+ unzip -o /tmp/easytier.zip -d /tmp/easytier
+ cp "/tmp/easytier/$EASYTIER_DIR/easytier-core" "$staging/easytier-core"
+ chmod +x "$staging/easytier-core"
+ rm -rf /tmp/easytier.zip /tmp/easytier
+ fi
fi
}
@@ -198,13 +242,14 @@ download_tools() {
build_for_platform() {
local platform="$1"
local target=$(platform_to_target "$platform")
- local staging="$STAGING_DIR/$target"
+ local staging="$BASE_STAGING_DIR/$VARIANT/$target"
echo_info "=========================================="
echo_info "Processing: $platform ($target)"
echo_info "=========================================="
# Create staging directory
+ rm -rf "$staging"
mkdir -p "$staging/ventoy"
# Build binary if requested
@@ -252,7 +297,11 @@ build_for_platform() {
fi
# Copy Dockerfile
- cp "$PROJECT_ROOT/build/Dockerfile.runtime" "$staging/Dockerfile"
+ local dockerfile="$PROJECT_ROOT/build/Dockerfile.runtime"
+ if [ "$INCLUDE_THIRD_PARTY" = true ]; then
+ dockerfile="$PROJECT_ROOT/build/Dockerfile.runtime-full"
+ fi
+ cp "$dockerfile" "$staging/Dockerfile"
# Build Docker image
echo_info "Building Docker image..."
@@ -292,6 +341,7 @@ main() {
echo_info "One-KVM Docker Image Builder"
echo_info "Image: $full_image:$TAG"
+ echo_info "Variant: $VARIANT"
echo_info "Platforms: $PLATFORMS"
if [ -n "$REGISTRY" ]; then
echo_info "Registry: $REGISTRY"
diff --git a/libs/hwcodec/build.rs b/libs/hwcodec/build.rs
index bdae4450..f2c89db2 100644
--- a/libs/hwcodec/build.rs
+++ b/libs/hwcodec/build.rs
@@ -98,6 +98,7 @@ mod ffmpeg {
link_os();
build_ffmpeg_ram(builder);
+ build_ffmpeg_hw(builder);
}
/// Link system FFmpeg using pkg-config or custom path
@@ -374,4 +375,57 @@ mod ffmpeg {
);
}
}
+
+ fn build_ffmpeg_hw(builder: &mut Build) {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
+ let ffi_header = ffmpeg_hw_dir
+ .join("ffmpeg_hw_ffi.h")
+ .to_string_lossy()
+ .to_string();
+ bindgen::builder()
+ .header(ffi_header)
+ .rustified_enum("*")
+ .generate()
+ .unwrap()
+ .write_to_file(Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_hw_ffi.rs"))
+ .unwrap();
+
+ let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
+ let enable_rkmpp = matches!(target_arch.as_str(), "aarch64" | "arm")
+ || std::env::var_os("CARGO_FEATURE_RKMPP").is_some();
+ if enable_rkmpp {
+ // Include RGA headers for NV16->NV12 conversion (RGA im2d API)
+ let rga_sys_dirs = [
+ Path::new("/usr/aarch64-linux-gnu/include/rga"),
+ Path::new("/usr/include/rga"),
+ ];
+ let mut added = false;
+ for dir in rga_sys_dirs.iter() {
+ if dir.exists() {
+ builder.include(dir);
+ added = true;
+ }
+ }
+ if !added {
+ // Fallback to repo-local rkrga headers if present
+ let repo_root = manifest_dir
+ .parent()
+ .and_then(|p| p.parent())
+ .map(|p| p.to_path_buf())
+ .unwrap_or_else(|| manifest_dir.clone());
+ let rkrga_dir = repo_root.join("ffmpeg").join("rkrga");
+ if rkrga_dir.exists() {
+ builder.include(rkrga_dir.join("include"));
+ builder.include(rkrga_dir.join("im2d_api"));
+ }
+ }
+ builder.file(ffmpeg_hw_dir.join("ffmpeg_hw_mjpeg_h26x.cpp"));
+ } else {
+ println!(
+ "cargo:info=Skipping ffmpeg_hw_mjpeg_h26x.cpp (RKMPP) for arch {}",
+ target_arch
+ );
+ }
+ }
}
diff --git a/libs/hwcodec/cpp/common/platform/linux/linux.cpp b/libs/hwcodec/cpp/common/platform/linux/linux.cpp
index c9036d20..e9f1e6c4 100644
--- a/libs/hwcodec/cpp/common/platform/linux/linux.cpp
+++ b/libs/hwcodec/cpp/common/platform/linux/linux.cpp
@@ -1,12 +1,16 @@
#include "linux.h"
#include "../../log.h"
+#include
+#include
#include
#include
#include
+#include
#include
#include
#include
#include
+#include
// Check for NVIDIA driver support by loading CUDA libraries
int linux_support_nv()
@@ -106,6 +110,57 @@ int linux_support_rkmpp() {
// Check for V4L2 Memory-to-Memory (M2M) codec support
// Returns 0 if a M2M capable device is found, -1 otherwise
int linux_support_v4l2m2m() {
+ auto to_lower = [](std::string value) {
+ std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
+ return static_cast(std::tolower(c));
+ });
+ return value;
+ };
+
+ auto read_text_file = [](const char *path, std::string *out) -> bool {
+ std::ifstream file(path);
+ if (!file.is_open()) {
+ return false;
+ }
+ std::getline(file, *out);
+ return !out->empty();
+ };
+
+ auto allow_video0_probe = []() -> bool {
+ const char *env = std::getenv("ONE_KVM_V4L2M2M_ALLOW_VIDEO0");
+ if (env == nullptr) {
+ return false;
+ }
+ if (env[0] == '\0') {
+ return false;
+ }
+ return std::strcmp(env, "0") != 0;
+ };
+
+ auto is_amlogic_vdec = [&]() -> bool {
+ std::string name;
+ std::string modalias;
+ 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 false;
+ };
+
// Check common V4L2 M2M device paths used by various ARM SoCs
// /dev/video10 - Standard on many SoCs
// /dev/video11 - Standard on many SoCs (often decoder)
@@ -124,6 +179,13 @@ int linux_support_v4l2m2m() {
for (size_t i = 0; i < sizeof(m2m_devices) / sizeof(m2m_devices[0]); i++) {
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
int fd = open(m2m_devices[i], O_RDWR | O_NONBLOCK);
if (fd >= 0) {
diff --git a/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_ffi.h b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_ffi.h
new file mode 100644
index 00000000..ac4cba21
--- /dev/null
+++ b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_ffi.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// MJPEG -> H26x (H.264 / H.265) hardware pipeline
+typedef struct FfmpegHwMjpegH26x FfmpegHwMjpegH26x;
+
+// Create a new MJPEG -> H26x pipeline.
+FfmpegHwMjpegH26x* ffmpeg_hw_mjpeg_h26x_new(const char* dec_name,
+ const char* enc_name,
+ int width,
+ int height,
+ int fps,
+ int bitrate_kbps,
+ int gop,
+ int thread_count);
+
+// Encode one MJPEG frame. Returns 1 if output produced, 0 if no output, <0 on error.
+int ffmpeg_hw_mjpeg_h26x_encode(FfmpegHwMjpegH26x* ctx,
+ const uint8_t* data,
+ int len,
+ int64_t pts_ms,
+ uint8_t** out_data,
+ int* out_len,
+ int* out_keyframe);
+
+// Reconfigure bitrate/gop (best-effort, may recreate encoder internally).
+int ffmpeg_hw_mjpeg_h26x_reconfigure(FfmpegHwMjpegH26x* ctx,
+ int bitrate_kbps,
+ int gop);
+
+// Request next frame to be a keyframe.
+int ffmpeg_hw_mjpeg_h26x_request_keyframe(FfmpegHwMjpegH26x* ctx);
+
+// Free pipeline resources.
+void ffmpeg_hw_mjpeg_h26x_free(FfmpegHwMjpegH26x* ctx);
+
+// Free packet buffer allocated by ffmpeg_hw_mjpeg_h26x_encode.
+void ffmpeg_hw_packet_free(uint8_t* data);
+
+// Get last error message (thread-local).
+const char* ffmpeg_hw_last_error(void);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h26x.cpp b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h26x.cpp
new file mode 100644
index 00000000..d19aafca
--- /dev/null
+++ b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h26x.cpp
@@ -0,0 +1,468 @@
+extern "C" {
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+}
+
+#include
+#include
+#include
+#include
+
+#define LOG_MODULE "FFMPEG_HW"
+#include "../common/log.h"
+
+#include "ffmpeg_hw_ffi.h"
+
+namespace {
+thread_local std::string g_last_error;
+
+static void set_last_error(const std::string &msg) {
+ g_last_error = msg;
+ LOG_ERROR(msg);
+}
+
+static std::string make_err(const std::string &ctx, int err) {
+ return ctx + " (ret=" + std::to_string(err) + "): " + av_err2str(err);
+}
+
+static const char* pix_fmt_name(AVPixelFormat fmt) {
+ const char *name = av_get_pix_fmt_name(fmt);
+ return name ? name : "unknown";
+}
+
+struct FfmpegHwMjpegH26xCtx {
+ AVCodecContext *dec_ctx = nullptr;
+ AVCodecContext *enc_ctx = nullptr;
+ AVPacket *dec_pkt = nullptr;
+ AVFrame *dec_frame = nullptr;
+ AVPacket *enc_pkt = nullptr;
+ AVBufferRef *hw_device_ctx = nullptr;
+ AVBufferRef *hw_frames_ctx = nullptr;
+ AVPixelFormat hw_pixfmt = AV_PIX_FMT_NONE;
+ std::string dec_name;
+ std::string enc_name;
+ int width = 0;
+ int height = 0;
+ int aligned_width = 0;
+ int aligned_height = 0;
+ int fps = 30;
+ int bitrate_kbps = 2000;
+ int gop = 60;
+ int thread_count = 1;
+ bool force_keyframe = false;
+};
+
+static enum AVPixelFormat get_hw_format(AVCodecContext *ctx,
+ const enum AVPixelFormat *pix_fmts) {
+ auto *self = reinterpret_cast(ctx->opaque);
+ if (self && self->hw_pixfmt != AV_PIX_FMT_NONE) {
+ const enum AVPixelFormat *p;
+ for (p = pix_fmts; *p != AV_PIX_FMT_NONE; p++) {
+ if (*p == self->hw_pixfmt) {
+ return *p;
+ }
+ }
+ }
+ return pix_fmts[0];
+}
+
+static int init_decoder(FfmpegHwMjpegH26xCtx *ctx) {
+ const AVCodec *dec = avcodec_find_decoder_by_name(ctx->dec_name.c_str());
+ if (!dec) {
+ set_last_error("Decoder not found: " + ctx->dec_name);
+ return -1;
+ }
+
+ ctx->dec_ctx = avcodec_alloc_context3(dec);
+ if (!ctx->dec_ctx) {
+ set_last_error("Failed to allocate decoder context");
+ return -1;
+ }
+
+ ctx->dec_ctx->width = ctx->width;
+ ctx->dec_ctx->height = ctx->height;
+ ctx->dec_ctx->thread_count = ctx->thread_count > 0 ? ctx->thread_count : 1;
+ ctx->dec_ctx->opaque = ctx;
+
+ // Pick HW pixfmt for RKMPP
+ const AVCodecHWConfig *cfg = nullptr;
+ for (int i = 0; (cfg = avcodec_get_hw_config(dec, i)); i++) {
+ if (cfg->device_type == AV_HWDEVICE_TYPE_RKMPP) {
+ ctx->hw_pixfmt = cfg->pix_fmt;
+ break;
+ }
+ }
+ if (ctx->hw_pixfmt == AV_PIX_FMT_NONE) {
+ set_last_error("No RKMPP hw pixfmt for decoder");
+ return -1;
+ }
+
+ int ret = av_hwdevice_ctx_create(&ctx->hw_device_ctx,
+ AV_HWDEVICE_TYPE_RKMPP, NULL, NULL, 0);
+ if (ret < 0) {
+ set_last_error(make_err("av_hwdevice_ctx_create failed", ret));
+ return -1;
+ }
+
+ ctx->dec_ctx->hw_device_ctx = av_buffer_ref(ctx->hw_device_ctx);
+ ctx->dec_ctx->get_format = get_hw_format;
+
+ ret = avcodec_open2(ctx->dec_ctx, dec, NULL);
+ if (ret < 0) {
+ set_last_error(make_err("avcodec_open2 decoder failed", ret));
+ return -1;
+ }
+
+ ctx->dec_pkt = av_packet_alloc();
+ ctx->dec_frame = av_frame_alloc();
+ ctx->enc_pkt = av_packet_alloc();
+ if (!ctx->dec_pkt || !ctx->dec_frame || !ctx->enc_pkt) {
+ set_last_error("Failed to allocate packet/frame");
+ return -1;
+ }
+
+ return 0;
+}
+
+static int init_encoder(FfmpegHwMjpegH26xCtx *ctx, AVBufferRef *frames_ctx) {
+ const AVCodec *enc = avcodec_find_encoder_by_name(ctx->enc_name.c_str());
+ if (!enc) {
+ set_last_error("Encoder not found: " + ctx->enc_name);
+ return -1;
+ }
+
+ ctx->enc_ctx = avcodec_alloc_context3(enc);
+ if (!ctx->enc_ctx) {
+ set_last_error("Failed to allocate encoder context");
+ return -1;
+ }
+
+ ctx->enc_ctx->width = ctx->width;
+ ctx->enc_ctx->height = ctx->height;
+ ctx->enc_ctx->coded_width = ctx->width;
+ ctx->enc_ctx->coded_height = ctx->height;
+ ctx->aligned_width = ctx->width;
+ ctx->aligned_height = ctx->height;
+ ctx->enc_ctx->time_base = AVRational{1, 1000};
+ ctx->enc_ctx->framerate = AVRational{ctx->fps, 1};
+ ctx->enc_ctx->bit_rate = (int64_t)ctx->bitrate_kbps * 1000;
+ ctx->enc_ctx->gop_size = ctx->gop > 0 ? ctx->gop : ctx->fps;
+ ctx->enc_ctx->max_b_frames = 0;
+ ctx->enc_ctx->pix_fmt = AV_PIX_FMT_DRM_PRIME;
+ ctx->enc_ctx->sw_pix_fmt = AV_PIX_FMT_NV12;
+
+ if (frames_ctx) {
+ AVHWFramesContext *hwfc = reinterpret_cast(frames_ctx->data);
+ if (hwfc) {
+ ctx->enc_ctx->pix_fmt = static_cast(hwfc->format);
+ ctx->enc_ctx->sw_pix_fmt = static_cast(hwfc->sw_format);
+ if (hwfc->width > 0) {
+ ctx->aligned_width = hwfc->width;
+ ctx->enc_ctx->coded_width = hwfc->width;
+ }
+ if (hwfc->height > 0) {
+ ctx->aligned_height = hwfc->height;
+ ctx->enc_ctx->coded_height = hwfc->height;
+ }
+ }
+ ctx->hw_frames_ctx = av_buffer_ref(frames_ctx);
+ ctx->enc_ctx->hw_frames_ctx = av_buffer_ref(frames_ctx);
+ }
+ if (ctx->hw_device_ctx) {
+ ctx->enc_ctx->hw_device_ctx = av_buffer_ref(ctx->hw_device_ctx);
+ }
+
+ AVDictionary *opts = nullptr;
+ av_dict_set(&opts, "rc_mode", "CBR", 0);
+ if (enc->id == AV_CODEC_ID_H264) {
+ av_dict_set(&opts, "profile", "high", 0);
+ } else if (enc->id == AV_CODEC_ID_HEVC) {
+ av_dict_set(&opts, "profile", "main", 0);
+ }
+ av_dict_set_int(&opts, "qp_init", 23, 0);
+ av_dict_set_int(&opts, "qp_max", 48, 0);
+ av_dict_set_int(&opts, "qp_min", 0, 0);
+ av_dict_set_int(&opts, "qp_max_i", 48, 0);
+ av_dict_set_int(&opts, "qp_min_i", 0, 0);
+ int ret = avcodec_open2(ctx->enc_ctx, enc, &opts);
+ av_dict_free(&opts);
+ if (ret < 0) {
+ std::string detail = "avcodec_open2 encoder failed: ";
+ detail += ctx->enc_name;
+ detail += " fmt=" + std::string(pix_fmt_name(ctx->enc_ctx->pix_fmt));
+ detail += " sw=" + std::string(pix_fmt_name(ctx->enc_ctx->sw_pix_fmt));
+ detail += " size=" + std::to_string(ctx->enc_ctx->width) + "x" + std::to_string(ctx->enc_ctx->height);
+ detail += " fps=" + std::to_string(ctx->fps);
+ set_last_error(make_err(detail, ret));
+ avcodec_free_context(&ctx->enc_ctx);
+ ctx->enc_ctx = nullptr;
+ if (ctx->hw_frames_ctx) {
+ av_buffer_unref(&ctx->hw_frames_ctx);
+ ctx->hw_frames_ctx = nullptr;
+ }
+ return -1;
+ }
+
+ return 0;
+}
+
+static void free_encoder(FfmpegHwMjpegH26xCtx *ctx) {
+ if (ctx->enc_ctx) {
+ avcodec_free_context(&ctx->enc_ctx);
+ ctx->enc_ctx = nullptr;
+ }
+ if (ctx->hw_frames_ctx) {
+ av_buffer_unref(&ctx->hw_frames_ctx);
+ ctx->hw_frames_ctx = nullptr;
+ }
+}
+
+} // namespace
+
+extern "C" FfmpegHwMjpegH26x* ffmpeg_hw_mjpeg_h26x_new(const char* dec_name,
+ const char* enc_name,
+ int width,
+ int height,
+ int fps,
+ int bitrate_kbps,
+ int gop,
+ int thread_count) {
+ if (!dec_name || !enc_name || width <= 0 || height <= 0) {
+ set_last_error("Invalid parameters for ffmpeg_hw_mjpeg_h26x_new");
+ return nullptr;
+ }
+
+ auto *ctx = new FfmpegHwMjpegH26xCtx();
+ ctx->dec_name = dec_name;
+ ctx->enc_name = enc_name;
+ ctx->width = width;
+ ctx->height = height;
+ ctx->fps = fps > 0 ? fps : 30;
+ ctx->bitrate_kbps = bitrate_kbps > 0 ? bitrate_kbps : 2000;
+ ctx->gop = gop > 0 ? gop : ctx->fps;
+ ctx->thread_count = thread_count > 0 ? thread_count : 1;
+
+ if (init_decoder(ctx) != 0) {
+ ffmpeg_hw_mjpeg_h26x_free(reinterpret_cast(ctx));
+ return nullptr;
+ }
+
+ return reinterpret_cast(ctx);
+}
+
+extern "C" int ffmpeg_hw_mjpeg_h26x_encode(FfmpegHwMjpegH26x* handle,
+ const uint8_t* data,
+ int len,
+ int64_t pts_ms,
+ uint8_t** out_data,
+ int* out_len,
+ int* out_keyframe) {
+ if (!handle || !data || len <= 0 || !out_data || !out_len || !out_keyframe) {
+ set_last_error("Invalid parameters for encode");
+ return -1;
+ }
+
+ auto *ctx = reinterpret_cast(handle);
+ *out_data = nullptr;
+ *out_len = 0;
+ *out_keyframe = 0;
+
+ av_packet_unref(ctx->dec_pkt);
+ int ret = av_new_packet(ctx->dec_pkt, len);
+ if (ret < 0) {
+ set_last_error(make_err("av_new_packet failed", ret));
+ return -1;
+ }
+ memcpy(ctx->dec_pkt->data, data, len);
+ ctx->dec_pkt->size = len;
+
+ ret = avcodec_send_packet(ctx->dec_ctx, ctx->dec_pkt);
+ if (ret < 0) {
+ set_last_error(make_err("avcodec_send_packet failed", ret));
+ return -1;
+ }
+
+ while (true) {
+ ret = avcodec_receive_frame(ctx->dec_ctx, ctx->dec_frame);
+ if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
+ return 0;
+ }
+ if (ret < 0) {
+ set_last_error(make_err("avcodec_receive_frame failed", ret));
+ return -1;
+ }
+
+ if (ctx->dec_frame->format != AV_PIX_FMT_DRM_PRIME) {
+ set_last_error("Decoder output is not DRM_PRIME");
+ av_frame_unref(ctx->dec_frame);
+ return -1;
+ }
+
+ if (!ctx->enc_ctx) {
+ if (!ctx->dec_frame->hw_frames_ctx) {
+ set_last_error("Decoder returned frame without hw_frames_ctx");
+ av_frame_unref(ctx->dec_frame);
+ return -1;
+ }
+ if (init_encoder(ctx, ctx->dec_frame->hw_frames_ctx) != 0) {
+ av_frame_unref(ctx->dec_frame);
+ return -1;
+ }
+ }
+
+ AVFrame *send_frame = ctx->dec_frame;
+ AVFrame *tmp = nullptr;
+ if (ctx->force_keyframe) {
+ tmp = av_frame_clone(send_frame);
+ if (tmp) {
+ tmp->pict_type = AV_PICTURE_TYPE_I;
+ send_frame = tmp;
+ }
+ ctx->force_keyframe = false;
+ }
+
+ // Apply visible size crop if aligned buffer is larger than display size
+ if (ctx->aligned_width > 0 && ctx->width > 0 && ctx->aligned_width > ctx->width) {
+ send_frame->crop_right = ctx->aligned_width - ctx->width;
+ }
+ if (ctx->aligned_height > 0 && ctx->height > 0 && ctx->aligned_height > ctx->height) {
+ send_frame->crop_bottom = ctx->aligned_height - ctx->height;
+ }
+
+ send_frame->pts = pts_ms; // time_base is ms
+
+ ret = avcodec_send_frame(ctx->enc_ctx, send_frame);
+ if (tmp) {
+ av_frame_free(&tmp);
+ }
+ if (ret < 0) {
+ std::string detail = "avcodec_send_frame failed";
+ if (send_frame) {
+ detail += " frame_fmt=";
+ detail += pix_fmt_name(static_cast(send_frame->format));
+ detail += " w=" + std::to_string(send_frame->width);
+ detail += " h=" + std::to_string(send_frame->height);
+ if (send_frame->format == AV_PIX_FMT_DRM_PRIME && send_frame->data[0]) {
+ const AVDRMFrameDescriptor *drm =
+ reinterpret_cast(send_frame->data[0]);
+ if (drm && drm->layers[0].format) {
+ detail += " drm_fmt=0x";
+ char buf[9];
+ snprintf(buf, sizeof(buf), "%08x", drm->layers[0].format);
+ detail += buf;
+ }
+ if (drm && drm->objects[0].format_modifier) {
+ detail += " drm_mod=0x";
+ char buf[17];
+ snprintf(buf, sizeof(buf), "%016llx",
+ (unsigned long long)drm->objects[0].format_modifier);
+ detail += buf;
+ }
+ }
+ }
+ set_last_error(make_err(detail, ret));
+ av_frame_unref(ctx->dec_frame);
+ return -1;
+ }
+
+ av_packet_unref(ctx->enc_pkt);
+ ret = avcodec_receive_packet(ctx->enc_ctx, ctx->enc_pkt);
+ if (ret == AVERROR(EAGAIN)) {
+ av_frame_unref(ctx->dec_frame);
+ return 0;
+ }
+ if (ret < 0) {
+ set_last_error(make_err("avcodec_receive_packet failed", ret));
+ av_frame_unref(ctx->dec_frame);
+ return -1;
+ }
+
+ if (ctx->enc_pkt->size > 0) {
+ uint8_t *buf = (uint8_t*)malloc(ctx->enc_pkt->size);
+ if (!buf) {
+ set_last_error("malloc for output packet failed");
+ av_packet_unref(ctx->enc_pkt);
+ av_frame_unref(ctx->dec_frame);
+ return -1;
+ }
+ memcpy(buf, ctx->enc_pkt->data, ctx->enc_pkt->size);
+ *out_data = buf;
+ *out_len = ctx->enc_pkt->size;
+ *out_keyframe = (ctx->enc_pkt->flags & AV_PKT_FLAG_KEY) ? 1 : 0;
+ av_packet_unref(ctx->enc_pkt);
+ av_frame_unref(ctx->dec_frame);
+ return 1;
+ }
+
+ av_frame_unref(ctx->dec_frame);
+ }
+}
+
+extern "C" int ffmpeg_hw_mjpeg_h26x_reconfigure(FfmpegHwMjpegH26x* handle,
+ int bitrate_kbps,
+ int gop) {
+ if (!handle) {
+ set_last_error("Invalid handle for reconfigure");
+ return -1;
+ }
+ auto *ctx = reinterpret_cast(handle);
+ if (!ctx->enc_ctx || !ctx->hw_frames_ctx) {
+ set_last_error("Encoder not initialized for reconfigure");
+ return -1;
+ }
+
+ ctx->bitrate_kbps = bitrate_kbps > 0 ? bitrate_kbps : ctx->bitrate_kbps;
+ ctx->gop = gop > 0 ? gop : ctx->gop;
+
+ AVBufferRef *frames_ref = ctx->hw_frames_ctx ? av_buffer_ref(ctx->hw_frames_ctx) : nullptr;
+ free_encoder(ctx);
+
+ if (init_encoder(ctx, frames_ref) != 0) {
+ if (frames_ref) av_buffer_unref(&frames_ref);
+ return -1;
+ }
+ if (frames_ref) av_buffer_unref(&frames_ref);
+
+ return 0;
+}
+
+extern "C" int ffmpeg_hw_mjpeg_h26x_request_keyframe(FfmpegHwMjpegH26x* handle) {
+ if (!handle) {
+ set_last_error("Invalid handle for request_keyframe");
+ return -1;
+ }
+ auto *ctx = reinterpret_cast(handle);
+ ctx->force_keyframe = true;
+ return 0;
+}
+
+extern "C" void ffmpeg_hw_mjpeg_h26x_free(FfmpegHwMjpegH26x* handle) {
+ auto *ctx = reinterpret_cast(handle);
+ if (!ctx) return;
+
+ if (ctx->dec_pkt) av_packet_free(&ctx->dec_pkt);
+ if (ctx->dec_frame) av_frame_free(&ctx->dec_frame);
+ if (ctx->enc_pkt) av_packet_free(&ctx->enc_pkt);
+
+ if (ctx->dec_ctx) avcodec_free_context(&ctx->dec_ctx);
+ free_encoder(ctx);
+
+ if (ctx->hw_device_ctx) av_buffer_unref(&ctx->hw_device_ctx);
+
+ delete ctx;
+}
+
+extern "C" void ffmpeg_hw_packet_free(uint8_t* data) {
+ if (data) {
+ free(data);
+ }
+}
+
+extern "C" const char* ffmpeg_hw_last_error(void) {
+ return g_last_error.c_str();
+}
diff --git a/libs/hwcodec/src/ffmpeg_hw/mod.rs b/libs/hwcodec/src/ffmpeg_hw/mod.rs
new file mode 100644
index 00000000..222c9d14
--- /dev/null
+++ b/libs/hwcodec/src/ffmpeg_hw/mod.rs
@@ -0,0 +1,118 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+use std::{
+ ffi::{CStr, CString},
+ os::raw::c_int,
+};
+
+include!(concat!(env!("OUT_DIR"), "/ffmpeg_hw_ffi.rs"));
+
+#[derive(Debug, Clone)]
+pub struct HwMjpegH26xConfig {
+ pub decoder: String,
+ pub encoder: String,
+ pub width: i32,
+ pub height: i32,
+ pub fps: i32,
+ pub bitrate_kbps: i32,
+ pub gop: i32,
+ pub thread_count: i32,
+}
+
+pub struct HwMjpegH26xPipeline {
+ ctx: *mut FfmpegHwMjpegH26x,
+ config: HwMjpegH26xConfig,
+}
+
+unsafe impl Send for HwMjpegH26xPipeline {}
+
+impl HwMjpegH26xPipeline {
+ pub fn new(config: HwMjpegH26xConfig) -> Result {
+ unsafe {
+ let dec = CString::new(config.decoder.as_str()).map_err(|_| "decoder name invalid".to_string())?;
+ let enc = CString::new(config.encoder.as_str()).map_err(|_| "encoder name invalid".to_string())?;
+ let ctx = ffmpeg_hw_mjpeg_h26x_new(
+ dec.as_ptr(),
+ enc.as_ptr(),
+ config.width,
+ config.height,
+ config.fps,
+ config.bitrate_kbps,
+ config.gop,
+ config.thread_count,
+ );
+ if ctx.is_null() {
+ return Err(last_error_message());
+ }
+ Ok(Self { ctx, config })
+ }
+ }
+
+ pub fn encode(&mut self, data: &[u8], pts_ms: i64) -> Result