diff --git a/Cargo.toml b/Cargo.toml
index a95b5b8f..aa8d66ec 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 2ff2c63f..54ad79dd 100644
--- a/README.md
+++ b/README.md
@@ -1,81 +1,294 @@
-# One-KVM
+
+

+
One-KVM
+
Rust 编写的开放轻量 IP-KVM 解决方案,实现 BIOS 级远程管理
-
- 开放轻量的 IP-KVM 解决方案,实现 BIOS 级远程管理
-
+
简体中文
-
- 功能特性 •
- 快速开始
-
+ [](https://github.com/mofeng-git/One-KVM/stargazers)
+ [](https://github.com/mofeng-git/One-KVM/network/members)
+ [](https://github.com/mofeng-git/One-KVM/issues)
+
+
+ 📖 技术文档 •
+ ⚡ 快速开始 •
+ 📊 功能介绍 •
+ 🔁 迁移说明
+
+
---
-## 介绍
+## 📋 目录
-One-KVM 是一个用 Rust 编写的开放轻量的 IP-KVM(基于 IP 的键盘、视频、鼠标)解决方案,让你可以通过网络远程控制计算机,包括 BIOS 级别的操作。
+- [项目概述](#项目概述)
+- [迁移说明](#迁移说明)
+- [功能介绍](#功能介绍)
+- [快速开始](#快速开始)
+- [贡献与反馈](#贡献与反馈)
+- [致谢](#致谢)
+- [许可证](#许可证)
-**当前软件处于开发早期阶段,各种功能和细节还有待完善,欢迎体验,但请勿应用于生产环境。**
+## 📖 项目概述
-## 功能特性
+**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
+
+项目目标:
+
+- **开放**:不绑定特定硬件配置,尽量适配常见 Linux 设备
+- **轻量**:单二进制分发,部署过程更简单
+- **易用**:网页界面完成设备与参数配置,尽量减少手动改配置文件
+
+> **注意:** One-KVM Rust 目前仍处于开发早期阶段,功能与细节会快速迭代,欢迎体验与反馈。
+
+## 🔁 迁移说明
+
+开发重心正在从 **One-KVM Python** 逐步转向 **One-KVM Rust**。
+
+- 如果你在使用 **One-KVM Python(基于 PiKVM)**,请查看 [One-KVM Python 文档](https://docs.one-kvm.cn/python/)
+- One-KVM Rust 相较于 One-KVM Python:**尚未适配 CSI HDMI 采集卡**、**不支持 VNC 访问**,仍处于开发早期阶段
+
+## 📊 功能介绍
### 核心功能
| 功能 | 说明 |
|------|------|
-| 视频采集 | HDMI USB 采集卡支持,提供 MJPEG/H264/H265/VP8/VP9 视频流 |
-| 键鼠控制 | USB OTG HID 或 CH340 + CH39329 HID,支持绝对/相对鼠标模式 |
-| 虚拟U盘 | USB Mass Storage,支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
+| 视频采集 | HDMI USB 采集卡支持,提供 MJPEG / WebRTC(H.264/H.265/VP8/VP9) |
+| 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID,支持绝对/相对鼠标模式 |
+| 虚拟媒体 | USB Mass Storage,支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
| ATX 电源控制 | GPIO 控制电源/重启按钮 |
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
### 硬件编码
支持自动检测和选择硬件加速:
-- **VAAPI** - Intel/AMD GPU
-- **RKMPP** - Rockchip SoC (**尚未实现**)
-- **V4L2 M2M** - 通用硬件编码器 (**尚未实现**)
-- **软件编码** - CPU 编码
-### 其他特性
+- **VAAPI**:Intel/AMD GPU
+- **RKMPP**:Rockchip SoC
+- **V4L2 M2M**:通用硬件编码器(尚未实现)
+- **软件编码**:CPU 编码
-- 单二进制部署,依赖更轻量
-- Web UI 配置,无需编辑配置文件,多语言支持 (中文/英文)
-- 内置 Web 终端 (ttyd),内网穿透支持 (gostc),P2P 组网支持 (EasyTier)
+### 扩展能力
-## 快速开始
+- Web UI 配置,多语言支持(中文/英文)
+- 内置 Web 终端(ttyd)内网穿透支持(gostc)、P2P 组网支持(EasyTier)、RustDesk 协议集成(用于跨平台远程访问能力扩展)
-### Docker 运行
+## ⚡ 快速开始
+
+安装方式:Docker / DEB 软件包 / 飞牛 NAS(FPK)。
+
+### 方式一:Docker 安装(推荐)
+
+前提条件:
+
+- Linux 主机已安装 Docker
+- 插好 USB HDMI 采集卡
+- 启用 USB OTG 或插好 CH340+CH9329 HID 线(用于 HID 模拟)
+
+启动容器:
```bash
-docker run -d --privileged \
- --name one-kvm \
- -v /dev:/dev \
- -v /sys/kernel/config:/sys/kernel/config \
- --net=host \
- silentwind0/one-kvm
+docker run --name one-kvm -itd --privileged=true \
+ -v /dev:/dev -v /sys/:/sys \
+ --net=host \
+ silentwind0/one-kvm
```
-访问 http://IP:8080
+访问 Web 界面:`http://<设备IP>:8080`(首次访问会引导创建管理员账户)。默认端口:HTTP `8080`;启用 HTTPS 后为 `8443`。
-### 环境变量
+#### 常用环境变量(Docker)
-| 变量 | 说明 | 默认值 |
-|------|------|--------|
-| `ENABLE_HTTPS` | 启用 HTTPS | `false` |
-| `HTTP_PORT` | HTTP 端口 | `8080` |
-| `VERBOSE` | 日志级别 (1/2/3) | - |
+| 变量名 | 默认值 | 说明 |
+|------|------|------|
+| `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 软件包安装
-感谢以下项目:
+前提条件:
-- [PiKVM](https://github.com/pikvm/pikvm) - 原始 Python 版 IP-KVM
-- [RustDesk](https://github.com/rustdesk/rustdesk) - hwcodec 硬件编码库
-- [ttyd](https://github.com/tsl0922/ttyd) - Web 终端
-- [EasyTier](https://github.com/EasyTier/EasyTier) - P2P 组网
+- 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`。
+
+## 报告问题
+
+如果您发现了问题,请:
+1. 使用 [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) 报告
+2. 提供详细的错误信息和复现步骤
+3. 包含您的硬件配置和系统信息
+
+## 赞助支持
+
+本项目基于多个优秀开源项目进行二次开发,作者投入了大量时间进行测试和维护。如果您觉得这个项目有价值,欢迎通过 **[为爱发电](https://afdian.com/a/silentwind)** 支持项目发展。
+
+### 感谢名单
+
+
+点击查看感谢名单
+
+- 浩龙的电子嵌入式之路
+
+- Tsuki
+
+- H_xiaoming
+
+- 0蓝蓝0
+
+- fairybl
+
+- Will
+
+- 浩龙的电子嵌入式之路
+
+- 自.知
+
+- 观棋不语٩ ི۶
+
+- 爱发电用户_a57a4
+
+- 爱发电用户_2c769
+
+- 霜序
+
+- 远方(闲鱼用户名:小远技术店铺)
+
+- 爱发电用户_399fc
+
+- 斐斐の
+
+- 爱发电用户_09451
+
+- 超高校级的錆鱼
+
+- 爱发电用户_08cff
+
+- guoke
+
+- mgt
+
+- 姜沢掵
+
+- ui_beam
+
+- 爱发电用户_c0dd7
+
+- 爱发电用户_dnjK
+
+- 忍者胖猪
+
+- 永遠の願い
+
+- 爱发电用户_GBrF
+
+- 爱发电用户_fd65c
+
+- 爱发电用户_vhNa
+
+- 爱发电用户_Xu6S
+
+- moss
+
+- woshididi
+
+- 爱发电用户_a0fd1
+
+- 爱发电用户_f6bH
+
+- 码农
+
+- 爱发电用户_6639f
+
+- jeron
+
+- 爱发电用户_CN7y
+
+- 爱发电用户_Up6w
+
+- 爱发电用户_e3202
+
+- 一语念白
+
+- 云边
+
+- 爱发电用户_5a711
+
+- 爱发电用户_9a706
+
+- T0m9ir1SUKI
+
+- 爱发电用户_56d52
+
+- 爱发电用户_3N6F
+
+- DUSK
+
+- 飘零
+
+- .
+
+- 饭太稀
+
+- 葱
+
+- ......
+
+
+
+### 赞助商
+
+本项目得到以下赞助商的支持:
+
+**CDN 加速及安全防护:**
+- **[Tencent EdgeOne](https://edgeone.ai/zh?from=github)** - 提供 CDN 加速及安全防护服务
+
+
+
+**文件存储服务:**
+- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
+
+**云服务商**
+
+- **[林枫云](https://www.dkdun.cn)** - 赞助了本项目宁波大带宽服务器
+
+
+
+林枫云主营国内外地域的精品线路业务服务器、高主频游戏服务器和大带宽服务器。
\ No newline at end of file
diff --git a/build/init.sh b/build/init.sh
index 6929f8ae..70089554 100644
--- a/build/init.sh
+++ b/build/init.sh
@@ -4,36 +4,44 @@
set -e
-# Start one-kvm with default options
-# Additional options can be passed via environment variables
-EXTRA_ARGS="-d /etc/one-kvm"
+# Start one-kvm with default options.
+# Additional options can be passed via environment variables.
+
+# Data directory (prefer DATA_DIR, keep ONE_KVM_DATA_DIR for backward compatibility)
+DATA_DIR="${DATA_DIR:-${ONE_KVM_DATA_DIR:-/etc/one-kvm}}"
+ARGS=(-d "$DATA_DIR")
# Enable HTTPS if requested
if [ "${ENABLE_HTTPS:-false}" = "true" ]; then
- EXTRA_ARGS="$EXTRA_ARGS --enable-https"
+ ARGS+=(--enable-https)
fi
# Custom bind address
if [ -n "$BIND_ADDRESS" ]; then
- EXTRA_ARGS="$EXTRA_ARGS -a $BIND_ADDRESS"
+ ARGS+=(-a "$BIND_ADDRESS")
fi
# Custom port
if [ -n "$HTTP_PORT" ]; then
- EXTRA_ARGS="$EXTRA_ARGS -p $HTTP_PORT"
+ ARGS+=(-p "$HTTP_PORT")
+fi
+
+# Custom HTTPS port
+if [ -n "$HTTPS_PORT" ]; then
+ ARGS+=(--https-port "$HTTPS_PORT")
fi
# Verbosity level
if [ -n "$VERBOSE" ]; then
case "$VERBOSE" in
- 1) EXTRA_ARGS="$EXTRA_ARGS -v" ;;
- 2) EXTRA_ARGS="$EXTRA_ARGS -vv" ;;
- 3) EXTRA_ARGS="$EXTRA_ARGS -vvv" ;;
+ 1) ARGS+=(-v) ;;
+ 2) ARGS+=(-vv) ;;
+ 3) ARGS+=(-vvv) ;;
esac
fi
echo "[INFO] Starting one-kvm..."
-echo "[INFO] Extra arguments: $EXTRA_ARGS"
+echo "[INFO] Arguments: ${ARGS[*]}"
# Execute one-kvm
-exec /usr/bin/one-kvm $EXTRA_ARGS
+exec /usr/bin/one-kvm "${ARGS[@]}"
diff --git a/libs/hwcodec/build.rs b/libs/hwcodec/build.rs
index 791316eb..7da2da2f 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
@@ -373,4 +374,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_h264.cpp"));
+ } else {
+ println!(
+ "cargo:info=Skipping ffmpeg_hw_mjpeg_h264.cpp (RKMPP) for arch {}",
+ target_arch
+ );
+ }
+ }
}
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..dc7179a5
--- /dev/null
+++ b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_ffi.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct FfmpegHwMjpegH264 FfmpegHwMjpegH264;
+
+FfmpegHwMjpegH264* ffmpeg_hw_mjpeg_h264_new(const char* dec_name,
+ const char* enc_name,
+ int width,
+ int height,
+ int fps,
+ int bitrate_kbps,
+ int gop,
+ int thread_count);
+
+int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* ctx,
+ const uint8_t* data,
+ int len,
+ int64_t pts_ms,
+ uint8_t** out_data,
+ int* out_len,
+ int* out_keyframe);
+
+int ffmpeg_hw_mjpeg_h264_reconfigure(FfmpegHwMjpegH264* ctx,
+ int bitrate_kbps,
+ int gop);
+
+int ffmpeg_hw_mjpeg_h264_request_keyframe(FfmpegHwMjpegH264* ctx);
+
+void ffmpeg_hw_mjpeg_h264_free(FfmpegHwMjpegH264* ctx);
+
+void ffmpeg_hw_packet_free(uint8_t* data);
+
+const char* ffmpeg_hw_last_error(void);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h264.cpp b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h264.cpp
new file mode 100644
index 00000000..cbdf3736
--- /dev/null
+++ b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h264.cpp
@@ -0,0 +1,444 @@
+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 FfmpegHwMjpegH264Ctx {
+ 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 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(FfmpegHwMjpegH264Ctx *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(FfmpegHwMjpegH264Ctx *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->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->enc_ctx->width = hwfc->width;
+ if (hwfc->height > 0) ctx->enc_ctx->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);
+ av_dict_set(&opts, "profile", "high", 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(FfmpegHwMjpegH264Ctx *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" FfmpegHwMjpegH264* ffmpeg_hw_mjpeg_h264_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_h264_new");
+ return nullptr;
+ }
+
+ auto *ctx = new FfmpegHwMjpegH264Ctx();
+ 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_h264_free(reinterpret_cast(ctx));
+ return nullptr;
+ }
+
+ return reinterpret_cast(ctx);
+}
+
+extern "C" int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* 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;
+ }
+
+ 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_h264_reconfigure(FfmpegHwMjpegH264* 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_h264_request_keyframe(FfmpegHwMjpegH264* 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_h264_free(FfmpegHwMjpegH264* 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..737a7d1b
--- /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 HwMjpegH264Config {
+ 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 HwMjpegH264Pipeline {
+ ctx: *mut FfmpegHwMjpegH264,
+ config: HwMjpegH264Config,
+}
+
+unsafe impl Send for HwMjpegH264Pipeline {}
+
+impl HwMjpegH264Pipeline {
+ pub fn new(config: HwMjpegH264Config) -> 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_h264_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