Compare commits

...

132 Commits

Author SHA1 Message Date
SilentWind
d60ddb21dc Merge pull request #279 from mofeng-git/dev
Dev
2026-07-20 08:04:01 +08:00
mofeng-git
219d1ccd08 fix: 前端 UI 下拉框修改为全宽 2026-07-20 08:01:23 +08:00
mofeng-git
6f697e56c8 fix: 修复 windows 构建 2026-07-19 23:44:34 +08:00
SilentWind
05535b9bb2 Merge pull request #278 from mofeng-git/dev
Dev
2026-07-19 21:50:59 +08:00
SilentWind
5ce2cb658c Merge pull request #277 from uranami/fix/otg-led-disconnect-cpu
fix: prevent OTG LED listener busy loop after USB disconnect
2026-07-19 21:49:44 +08:00
mofeng-git
a86df52d9a chore: bump version to v0.2.5 2026-07-19 21:47:02 +08:00
mofeng-git
275c2ca343 feat: 统一和完善 UI 2026-07-19 21:43:27 +08:00
mofeng-git
5d871e8b21 fix: 修复 MSD 启用时 Ventoy 资源初始化问题 #275
- 将 Ventoy 资源初始化纳入 MSD 控制器启用流程
- 支持运行时启用 MSD,无需重启服务
- 调整验收测试并移除启用后的重启逻辑
2026-07-19 20:08:03 +08:00
uranami
694afdd07f fix: stop OTG LED listener spin after USB disconnect 2026-07-19 09:41:54 +08:00
mofeng-git
d31e70515a fix: 修复 npm 依赖版本错误 2026-07-18 11:50:43 +08:00
mofeng-git
701b141347 fix: 完善虚拟U盘创建错误提示 #275 2026-07-18 11:50:26 +08:00
mofeng-git
eaa114f40c fix: 修复 v4l2loopback 的 DQBUF 兼容问题 2026-07-17 23:25:24 +08:00
mofeng-git
563c808836 fix: 完善 libyuv 静态库构建 2026-07-17 23:25:04 +08:00
mofeng-git
79b6683817 feat: 优化 UI 样式 2026-07-17 22:07:25 +08:00
mofeng-git
8bd16df6f8 feat: 添加 TOTP 2FA 功能 2026-07-17 16:34:06 +08:00
mofeng-git
fdea52d59d feat: 优化 CUA 功能体验 2026-07-17 16:32:17 +08:00
mofeng-git
4031abd8d6 fix: 修复移动端浏览器的触屏鼠标映射 2026-07-17 13:16:20 +08:00
mofeng-git
cf2bfaf6a7 fix: 尝试修复虚拟U盘文件系统大文件解析错误 2026-07-17 10:47:04 +08:00
mofeng-git
d35021ee5d fix: 修复扩展服务开启和自启动状态错误 2026-07-17 10:46:34 +08:00
mofeng-git
57d1597264 update: 更新前端依赖;修改前端样式 2026-07-17 01:33:44 +08:00
mofeng-git
aca847e76c update: 升级 rust 依赖 2026-07-16 22:59:22 +08:00
mofeng-git
8036c164b4 fix: 修复 MJPEG/HTTP 视频窗口可被选中拖拽的问题 2026-07-16 21:03:27 +08:00
mofeng-git
0482987bf1 feat: 完善按配置隐藏控制台菜单选项 2026-07-16 21:02:23 +08:00
mofeng-git
50dd419e5a feat: 支持 WatchDog 2026-07-16 20:07:33 +08:00
mofeng-git
213359b6c8 feat: 初步支持 USB 网卡桥接;删除 OTG 端点预算管理 2026-07-16 19:19:24 +08:00
mofeng-git
63c2bb4ca2 Merge branch 'main' into dev
# Conflicts:
#	libs/hwcodec/cpp/common/util.cpp
#	libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp
2026-07-14 23:43:27 +08:00
mofeng-git
6693020e6d feat: 增强 VNC 功能的兼容性 2026-07-14 22:59:33 +08:00
SilentWind
05c5f45995 Merge pull request #273 from cheng-zongkai/fix/vaapi-cqp-rate-control
fix: 为 VAAPI 添加 CQP 码率控制回退
2026-07-14 21:39:04 +08:00
mofeng-git
1ded7a8a66 del: 移除安卓支持 2026-07-14 21:01:47 +08:00
chengzongkai
809d0ca65c fix: 为 VAAPI 添加 CQP 码率控制回退 2026-07-13 16:01:43 +08:00
mofeng-git
139064de35 feta: 更新测试套件中的虚拟媒体部分测试 2026-07-12 17:04:37 +08:00
mofeng-git
54e0cbc84e featt: 增加 redfish 身份验证,完善 redfish 虚拟媒体资源管理能力 2026-07-12 16:13:41 +08:00
mofeng-git
7c9166a8cf feat: 虚拟媒体支持同时挂载多个镜像 2026-07-12 11:40:01 +08:00
mofeng-git
a9a16f2218 docs: 更新说明 2026-07-08 08:22:27 +08:00
mofeng-git
c9ae9dd893 docs: 更新说明 2026-07-08 08:19:09 +08:00
SilentWind
010d50b727 Merge branch 'dev' 2026-07-08 07:07:03 +08:00
mofeng-git
c88fb03243 chore: bump version to v0.2.4 2026-07-08 00:06:33 +08:00
mofeng-git
7bae9b633c del: 删除构建日期设定 2026-07-08 00:04:59 +08:00
mofeng-git
fc3f1d6aac feat: 优化网页终端样式 2026-07-08 00:04:19 +08:00
mofeng-git
b719be3e38 ci: 移除 GitHub Actions 里的 APK 构建 2026-07-07 23:55:55 +08:00
mofeng-git
db2410cc7f feat: 优化 MSD 功能体验和控件样式 2026-07-07 23:52:38 +08:00
mofeng-git
d9c2854911 feat: 优化网页 ATX 控件中的状态显示样式 2026-07-07 22:08:59 +08:00
mofeng-git
99c051a874 del: MJPEG /HTTTP 视频模式删除网页连接统计功能 2026-07-07 22:08:20 +08:00
mofeng-git
a0ad151fbc fix: 修复 typeshare 生成问题 2026-07-07 00:41:25 +08:00
mofeng-git
4806a5e0a9 fix: 修复 LCUS HID 继电器协议错误 #263 2026-07-07 00:29:33 +08:00
mofeng-git
e60152d38b feat: 完善 ATX 功能逻辑与控件样式 2026-07-07 00:21:54 +08:00
mofeng-git
f6e97a06f5 del: 删除 MJPEG 输入格式自动切换功能 2026-07-06 20:51:58 +08:00
mofeng-git
618599266e fix:恢复错误消失的 redfish 入口 2026-07-05 22:01:33 +08:00
mofeng-git
696c1b4bb2 test: 增加测试套件 2026-07-05 21:30:11 +08:00
mofeng-git
b346af35d3 feat: VNC、RTSP 服务支持监听 ipv6 地址 2026-07-05 21:20:23 +08:00
mofeng-git
16a65289f2 feat: v4l2 buffer 由2改为4 2026-07-05 21:15:44 +08:00
mofeng-git
bb2691fe8f fix: 从 gitee 切换到 github,避免 gitee 平台不稳定影响构建 2026-07-05 16:35:44 +08:00
mofeng-git
7753c83e27 Merge branch 'dev' 2026-06-26 23:23:37 +08:00
mofeng-git
1d6fb2e3c5 chore: bump version to v0.2.3 2026-06-26 23:18:08 +08:00
mofeng-git
6c1bff5d0c fix: 修复 mpp 库静态构建错误 2026-06-20 21:28:01 +08:00
mofeng-git
f1e362a820 refactor: 前端界面微调 2026-06-15 23:36:17 +08:00
mofeng-git
e2c19d550c Merge commit '4b7be20fe0cce3e7979cc3bdfdd7b02ec6630c00' into dev 2026-06-15 22:26:22 +08:00
mofeng-git
c101ef1c80 feat: 新增 MJPEG/H.264 VNC 初步支持 2026-06-15 22:25:18 +08:00
mofeng-git
4b7be20fe0 feat: 新增 Computer Use Agent 初步支持 2026-06-15 22:24:40 +08:00
mofeng-git
5c98aea7e3 feat: 新增 Linux 绝对鼠标兼容模式 #266;新增 CH9329 描述符设置 2026-06-14 20:59:23 +08:00
mofeng-git
da61644dbc fix: 修复 CH9329 健康检测错误和切换错误 #251 #255 #265 2026-06-13 16:47:21 +08:00
mofeng-git
5de7ecd4c5 feat: 新增 frp 远程访问扩展 2026-06-13 16:05:34 +08:00
SilentWind
3fc6b055a0 Merge pull request #267 from mofeng-git/dev
ci: 完善构建流程和中国网络环境加速
2026-06-10 09:50:04 +08:00
mofeng-git
4b65eebd5d ci: 完善构建流程和中国网络环境加速 2026-06-10 09:48:42 +08:00
mofeng-git
921c00c472 fix: 修复 libyuv 构建错误 2026-06-04 18:29:17 +08:00
mofeng-git
b6bd76534f fix: 修正 iConfiguration 描述字符串 2026-05-31 13:46:56 +08:00
mofeng-git
780cfb4cca fix:修复 deb 依赖关系,兼容 Dieban 13 系统安装 2026-05-26 14:16:44 +08:00
mofeng-git
8f9272d985 ci: 添加 docker 镜像构建 2026-05-25 15:33:57 +00:00
mofeng-git
f679647ec9 fix: 安卓构建脚本添加可执行权限 2026-05-25 11:04:51 +08:00
mofeng-git
198552014b chore: bump version to v0.2.2 2026-05-24 14:04:03 +00:00
mofeng-git
3e3937605e fix:修复在线升级错误 2026-05-24 14:03:49 +00:00
mofeng-git
4a85fbfab8 ci: 统一 Android APK 产物命名 2026-05-24 13:21:34 +00:00
mofeng-git
87d1110a87 ci: 调整 GitHub Actions 构建与发布流程 2026-05-24 09:58:30 +00:00
mofeng-git
b31aae284d feat: 新增安卓平台支持 2026-05-24 09:44:41 +00:00
mofeng-git
dc6475776e fix: 修复 rtsp 和 RustDesk 扩展启停错误;修改部分参数描述文本 2026-05-23 15:16:39 +00:00
mofeng-git
3de72677e6 fix: 优化 RustDesk 中继服务器推导策略;修复鼠键输入异常 2026-05-23 12:21:13 +00:00
mofeng-git
6a1616c32a chore: vendor trimmed v4l2r capture crate 2026-05-23 02:36:40 +00:00
mofeng-git
032f47a891 fix: 完善晶晨视频设备探测逻辑,避免内核缺陷引起软件崩溃 #262 2026-05-21 14:45:47 +00:00
mofeng-git
2e0ca89943 chore: bump version to v0.2.1 2026-05-20 00:09:31 +08:00
mofeng-git
1f7cfb373c fix: 修复设置页滚动和 HID 继电器识别 #252 2026-05-19 22:17:50 +08:00
mofeng-git
da05656a89 fix(web): 设置页按菜单加载并优化错误提示 2026-05-19 21:38:56 +08:00
mofeng-git
265852b312 fix: 避免 CH9329 配置保存时误触发 OTG reconcile 2026-05-19 20:50:33 +08:00
mofeng-git
02bf04ed7f fix: 修复 MSD 状态卡片 i18n 键名 2026-05-19 20:44:12 +08:00
mofeng-git
8915d36bcf fix: 升级 Vite、Rollup、PostCSS 等依赖清除 Github 安全漏洞提示 2026-05-19 11:45:45 +00:00
mofeng-git
3ea15e37a4 feat: 增加 CHINAMIRRO 构建环境变量 2026-05-19 11:22:04 +00:00
mofeng-git
cb0c66af96 ci: 调整 GitHub Actions 构建与发布流程 2026-05-19 18:01:53 +08:00
SilentWind
a3ebcded34 Merge pull request #261 from fcsha/fix/issue-260-msd-endpoint-budget
fix: 关闭 MSD 后保存 HID 配置时端点预算校验误判超限
2026-05-19 09:58:03 +08:00
mofeng-git
f7c2cd1b90 ci: 支持 GitHub Actions 构建 2026-05-19 09:54:54 +08:00
mofeng-git
e774210ae3 fix: 修复构建错误并清理未使用导入 2026-05-18 15:23:42 +00:00
mofeng-git
935fa823f2 feat: 初步增加 Windows 支持 2026-05-18 22:44:59 +08:00
Fucheng Sha
dd3f73ae54 fix: 关闭 MSD 后保存 HID 配置时先更新 MSD 状态再校验端点预算
saveConfig 中调换 updateMsd 和 updateHid 的调用顺序,确保 HID
校验端点预算时 MSD enabled 状态已是最新值,避免被误判为超限。

Fixes mofeng-git/One-KVM#260
2026-05-16 13:00:49 +08:00
SilentWind
0b9d94f53f docs: Update README with 贝塔网络 sponsorship 2026-05-13 22:38:58 +08:00
SilentWind
e5d6279a54 Merge pull request #257 from btzen/redfish
feat: 实现 Redfish API 标准接口;支持通过前端开关控制 Redfish 服务
2026-05-13 13:47:07 +08:00
Fucheng Sha
57d4091497 fix: 恢复被误删的 MSD Section 注释 2026-05-12 14:53:04 +08:00
Fucheng Sha
4e8c342905 feat: 实现 Redfish API 标准接口;支持通过前端开关控制 Redfish 服务 2026-05-12 10:53:26 +08:00
SilentWind
17cd74f64c Merge pull request #250 from arounyf/pr/audio-fix
fix: 修复 WebRTC 音频/视频接收器重启时破音问题
2026-05-05 12:12:17 +08:00
arounyf
9923670426 fix: 修复 WebRTC 音频/视频接收器重启时破音问题
start_audio_from_opus 和 start_from_video_frames 替换旧 handle 时先
abort 旧任务,防止新旧两个任务同时向同一个 track 写数据导致破音。
2026-05-05 05:11:04 +08:00
mofeng-git
3ee3df77b8 chroe: 不再配置 iceCandidatePoolSize,沿用浏览器默认 2026-05-05 01:19:17 +08:00
mofeng-git
8ec2f25e82 chore: bump version to v0.2.0 2026-05-05 00:59:16 +08:00
mofeng-git
c27d3a6703 fix:改进atx usb 继电器适配;修复 webrtc 无法建立连接问题;网页样式优化 2026-05-05 00:52:16 +08:00
mofeng-git
6723f432a3 feat: 允许通过环境变量手动指定前端资源路径,删除 debug 分支默认资源路径 2026-05-04 17:53:27 +08:00
mofeng-git
12a3f1c947 feat: 增加设备丢失自恢复机制
增加音频设备丢失自恢复机制,完善视频设备丢失自恢复机制

降级部分日志级别,GOSTC key打印脱敏

代码格式化
2026-05-02 10:55:05 +08:00
mofeng-git
52754c862b feat: 优化网页消息提醒样式 2026-05-01 21:46:32 +08:00
mofeng-git
e51d243324 feat: 增加 MSD 虚拟盘文件路径编码 2026-05-01 21:27:03 +08:00
mofeng-git
a1ebd34083 feat: 外部扩展程序输出日志级别修改为 info 级别 2026-05-01 21:21:54 +08:00
mofeng-git
89b19ea7dd refactor: 修改为同步请求 2026-05-01 20:06:22 +08:00
mofeng-git
0d47d8395d refactor: 重构视频采集状态与错误分类公共逻辑 2026-05-01 17:56:56 +08:00
mofeng-git
d82c863f40 refactor: 精简依赖 2026-05-01 17:41:11 +08:00
mofeng-git
d8e7de74a6 refactor: 删除部分多余的代码和注释 2026-05-01 17:31:04 +08:00
SilentWind
74035f8e12 Merge pull request #247 from tedaimengtech/main
Update: Add CQU Mirror Information
2026-04-29 14:13:25 +08:00
tedaimeng
8d45186eba Add Mirror Download Services to README
Added a section for Mirror Download Services and updated sponsors.
2026-04-29 13:12:35 +08:00
tedaimeng
c484580b8f Update README with CQUMirror details
Added CQUMirror information and links to the README.
2026-04-29 13:09:23 +08:00
SilentWind
56bce7937c Add GNU General Public License v3 2026-04-29 10:38:30 +08:00
mofeng-git
07b982d1d2 feat: 完善 USB UVC 设备异常处理,添加 USB 设备复位功能 2026-04-27 16:37:04 +08:00
mofeng-git
9065e01225 feat: 优化控制台页面状态工具栏在不同宽度网页下的自适应能力 2026-04-25 20:32:44 +08:00
mofeng-git
cc3cc15774 refactor: 删除部分多余的 Ventoy 逻辑 2026-04-20 14:07:28 +08:00
mofeng-git
fcb39c73fc refactor: 删除未使用的公共 STUN/TURN 逻辑 2026-04-20 10:15:53 +08:00
mofeng-git
7c703b8b4b feat: 深入适配 RK628D CSI 采集卡的设备识别、参数读取、自恢复和音频采集 2026-04-19 11:26:21 +08:00
mofeng-git
8eac31f69f chore: bump version to v0.1.9
Made-with: Cursor
2026-04-12 20:43:44 +08:00
mofeng-git
9653e16a68 feat: CLI 改密、自定义 TLS、移动端适配与扩展校验
- 新增 one-kvm user set-password(交互式),改密后吊销该用户全部会话
- /api/config/web 支持 PEM 证书/密钥上传与清除,响应含 has_custom_cert
- 移动端:ActionBar 溢出菜单、ATX/粘贴底部 Sheet、BrandMark 与控制台等响应式优化
- GOSTC:校验服务器地址非空,管理器启动条件与 HTTP 热更新一致
- RustDesk:中继密钥 relay_key 校验为标准 Base64 且解码后恰好 32 字节
- StatusCard、InfoBar:合并精简冗余状态信息
2026-04-12 19:28:17 +08:00
mofeng-git
d0c0852fbb fix: 修复设置页 IPv6 URL 拼接问题 #241
统一处理 Settings 页面中的主机地址格式,避免 IPv6 场景下 RTSP 地址展示和重启后跳转 URL 因缺少方括号而失效。

Made-with: Cursor
2026-04-12 09:49:42 +08:00
mofeng-git
c0a0c90cbd fix: 修复 ATX 串口继电器共享设备失效 #233
当 power/reset 配置为同一串口设备时,改为复用同一个串口句柄,避免重复 open 导致 reset 不生效。

Made-with: Cursor
2026-04-11 22:31:17 +08:00
mofeng-git
9e3483b836 style: 统一代码格式
应用 fmt 对已暂存 Rust 文件进行格式化,保持代码风格一致并减少无意义差异。

Made-with: Cursor
2026-04-11 22:25:46 +08:00
mofeng-git
132f445c29 完善音频采集 2026-04-11 22:22:05 +08:00
mofeng-git
4952cbaf19 fix: 修复 RTSP 功能对 VLC 视频播放器的兼容性 #237 2026-04-11 21:55:34 +08:00
mofeng-git
099f0b1ca2 fix: 优化视频切换流畅性;修复 OTG HID 功能无法一次保存成功和页面未即刻生效问题 2026-04-11 21:20:54 +08:00
mofeng-git
eecbc0fc13 CSI 采集适配优化 2026-04-11 20:26:33 +08:00
mofeng-git
2d81a071e5 fix: 修复 MJPEG模式可用但状态显示离线的问题 2026-04-11 15:11:47 +08:00
mofeng-git
3e35181583 fix: 修复镜像列表滚动条问题 #238 2026-04-11 13:13:24 +08:00
mofeng-git
c3a3f41a2c 修改 .gitignore 2026-04-11 13:12:11 +08:00
mofeng-git
2b2b471cfb chore(release): bump version to v0.1.8 2026-04-01 21:30:41 +08:00
mofeng-git
abb319068b feat: 适配 RK 原生 HDMI IN 适配采集 2026-04-01 21:28:15 +08:00
527 changed files with 70529 additions and 27756 deletions

177
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,177 @@
name: Build
on:
pull_request:
workflow_dispatch:
inputs:
publish_release:
description: Publish GitHub Release
required: false
default: false
type: boolean
release_tag:
description: Release tag name when publishing
required: false
default: ""
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
frontend:
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: web/package-lock.json
- name: Build frontend
working-directory: web
run: |
npm ci
npm run build
- name: Upload frontend dist
uses: actions/upload-artifact@v4
with:
name: web-dist
path: web/dist
if-no-files-found: error
retention-days: 7
deb:
runs-on: ubuntu-22.04
needs: frontend
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Download frontend dist
uses: actions/download-artifact@v4
with:
name: web-dist
path: web/dist
- uses: dtolnay/rust-toolchain@stable
- name: Install cross
run: cargo install cross --locked
- name: Build linux binary
run: bash build/build-images.sh
- name: Package deb
run: bash build/package-deb.sh
- name: Upload deb
uses: actions/upload-artifact@v4
with:
name: one-kvm-deb
path: target/debian/*.deb
if-no-files-found: error
retention-days: 7
windows:
runs-on: windows-2022
needs: frontend
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Download frontend dist
uses: actions/download-artifact@v4
with:
name: web-dist
path: web/dist
- uses: dtolnay/rust-toolchain@stable
- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@v1
- name: Prepare vcpkg and dependencies
shell: pwsh
run: |
$env:VCPKG_ROOT = "C:\vcpkg"
$env:VCPKG_DEFAULT_TRIPLET = "x64-windows-static"
$env:VCPKG_INSTALLED_DIR = Join-Path $pwd "vcpkg_installed"
if (-not (Test-Path $env:VCPKG_ROOT)) {
git clone https://github.com/microsoft/vcpkg $env:VCPKG_ROOT
}
& "$env:VCPKG_ROOT\bootstrap-vcpkg.bat" -disableMetrics
& "$env:VCPKG_ROOT\vcpkg.exe" install --triplet $env:VCPKG_DEFAULT_TRIPLET --x-install-root="$env:VCPKG_INSTALLED_DIR"
$tripletRoot = Join-Path $env:VCPKG_INSTALLED_DIR $env:VCPKG_DEFAULT_TRIPLET
$env:TURBOJPEG_SOURCE = "explicit"
$env:TURBOJPEG_LIB_DIR = Join-Path $tripletRoot "lib"
$env:TURBOJPEG_INCLUDE_DIR = Join-Path $tripletRoot "include"
"VCPKG_ROOT=$env:VCPKG_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
"VCPKG_DEFAULT_TRIPLET=$env:VCPKG_DEFAULT_TRIPLET" | Out-File -FilePath $env:GITHUB_ENV -Append
"VCPKG_INSTALLED_DIR=$env:VCPKG_INSTALLED_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
"TURBOJPEG_SOURCE=$env:TURBOJPEG_SOURCE" | Out-File -FilePath $env:GITHUB_ENV -Append
"TURBOJPEG_LIB_DIR=$env:TURBOJPEG_LIB_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
"TURBOJPEG_INCLUDE_DIR=$env:TURBOJPEG_INCLUDE_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Build Windows exe
shell: pwsh
run: .\build\windows\build.ps1 -Configuration release -Package
- name: Upload exe
uses: actions/upload-artifact@v4
with:
name: one-kvm-windows-exe
path: target/x86_64-pc-windows-msvc/release/one-kvm_*.exe
if-no-files-found: error
retention-days: 7
release:
runs-on: ubuntu-22.04
needs: [deb, windows]
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release }}
timeout-minutes: 30
permissions:
contents: write
steps:
- name: Validate release tag
run: |
if [ -z "${{ inputs.release_tag }}" ]; then
echo "release_tag is required when publish_release is true"
exit 1
fi
- name: Download deb artifact
uses: actions/download-artifact@v4
with:
name: one-kvm-deb
path: release-artifacts/deb
- name: Download exe artifact
uses: actions/download-artifact@v4
with:
name: one-kvm-windows-exe
path: release-artifacts/windows
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.release_tag }}
prerelease: true
generate_release_notes: true
files: |
release-artifacts/deb/*.deb
release-artifacts/windows/*.exe

85
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
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
cache: npm
cache-dependency-path: web/package-lock.json
- uses: dtolnay/rust-toolchain@stable
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y unzip xz-utils
cargo install cross --locked
- name: Build frontend
working-directory: web
run: |
npm ci
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

5
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# Rust # Rust
/target/ target/
Cargo.lock Cargo.lock
# IDE # IDE
@@ -30,6 +30,8 @@ Thumbs.db
# Build artifacts # Build artifacts
/dist/ /dist/
/build-staging
/.tmp/
# Frontend (built files) # Frontend (built files)
/web/node_modules/ /web/node_modules/
@@ -41,3 +43,4 @@ CLAUDE.md
secrets.toml secrets.toml
.env .env
/docs/ /docs/
web/package-lock.json

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "one-kvm" name = "one-kvm"
version = "0.1.7" version = "0.2.5"
edition = "2021" edition = "2021"
authors = ["SilentWind"] authors = ["SilentWind"]
description = "A open and lightweight IP-KVM solution written in Rust" description = "A open and lightweight IP-KVM solution written in Rust"
@@ -9,129 +9,199 @@ 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: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: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 = ["typed-header", "cookie"] } axum-extra = { version = "0.12", features = ["cookie"], optional = true }
tower-http = { version = "0.6", features = ["fs", "cors", "trace", "compression-gzip"] } tower-http = { version = "0.6", features = ["cors", "trace", "set-header"], optional = true }
# Database - Use bundled SQLite for static linking # Database - Use bundled SQLite for static linking
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } sqlx = { version = "0.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 }
tracing-log = "0.2"
# 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 }
chrono = { version = "0.4", features = ["serde"] } base64 = { version = "0.22", optional = true }
base64 = "0.22" tempfile = { version = "3", optional = true }
nix = { version = "0.30", features = ["fs", "net", "hostname", "poll"] }
# HTTP client (for URL downloads) # HTTP client (for URL downloads)
# Use rustls by default, but allow native-tls for systems with older GLIBC # Use rustls by default, but allow native-tls for systems with older GLIBC
reqwest = { version = "0.13", features = ["stream", "rustls", "json"], default-features = false } reqwest = { version = "0.13", features = ["stream", "rustls-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"] } 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 # Time (cookie max_age + RFC3339 timestamps)
time = "0.3" time = { version = "0.3", features = ["serde", "formatting", "parsing"], optional = true }
# Video capture (V4L2)
v4l2r = "0.0.7"
# JPEG encoding (libjpeg-turbo, SIMD accelerated)
turbojpeg = "1.3"
# Bytes handling # Bytes handling
bytes = "1" bytes = { version = "1", optional = true }
bytemuck = { version = "1.24", features = ["derive"] } bytemuck = { version = "1.24", features = ["derive"], optional = true }
# Frame deduplication (hash-based comparison) # Frame deduplication (hash-based comparison)
xxhash-rust = { version = "0.8", features = ["xxh64"] } xxhash-rust = { version = "0.8", features = ["xxh64"], optional = true }
# Async channels # Async channels
async-stream = "0.3" async-stream = { version = "0.3", optional = true }
futures = "0.3" futures = { version = "0.3", optional = true }
# WebSocket client (for ttyd proxy) # WebSocket client (for ttyd proxy)
tokio-tungstenite = "0.28" tokio-tungstenite = { version = "0.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 }
# Audio (ALSA capture + Opus encoding)
# Note: audiopus links to libopus.so (unavoidable for audio support)
alsa = "0.11"
audiopus = "0.2"
# HID (serial port for CH9329) # HID (serial port for CH9329)
serialport = "4" serialport = { version = "4", optional = true }
async-trait = "0.1" async-trait = { version = "0.1", optional = true }
libc = "0.2" libc = { version = "0.2", optional = true }
# Ventoy bootable image support # Ventoy bootable image support
ventoy-img = { path = "libs/ventoy-img-rs" } ventoy-img = { path = "libs/ventoy-img-rs", optional = true }
# ATX (GPIO control)
gpio-cdev = "0.6"
# H264 hardware/software encoding (hwcodec from rustdesk)
hwcodec = { path = "libs/hwcodec" }
# RustDesk protocol support # RustDesk protocol support
protobuf = { version = "3.7", features = ["with-bytes"] } protobuf = { version = "3.7", features = ["with-bytes"], optional = true }
sodiumoxide = "0.2" sodiumoxide = { version = "0.2", optional = true }
sha2 = "0.10" des = { version = "0.9", optional = true }
# High-performance pixel format conversion (libyuv) sha2 = { version = "0.11", optional = true }
libyuv = { path = "res/vcpkg/libyuv" }
# TypeScript type generation # TypeScript type generation
typeshare = "1.0" typeshare = { version = "1.0", optional = true }
[dev-dependencies] [target.'cfg(any(target_os = "linux", windows))'.dependencies]
tokio-test = "0.4" # Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux)
tempfile = "3" hwcodec = { path = "libs/hwcodec", features = ["bytes"], optional = true }
libyuv = { path = "res/vcpkg/libyuv", optional = true }
turbojpeg = { version = "1.3", optional = true }
# Note: audiopus links to libopus.so (unavoidable for audio support)
audiopus = { version = "0.2", optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
# Utilities
nix = { version = "0.31", default-features = false, features = ["fs", "socket", "net", "hostname", "poll"], optional = true }
# Video capture (V4L2)
v4l2r = { path = "libs/v4l2r", optional = true }
# Audio (ALSA capture)
alsa = { version = "0.11", optional = true }
# ATX (GPIO control)
gpio-cdev = { version = "0.6", optional = true }
[target.'cfg(windows)'.dependencies]
cpal = { version = "0.18", default-features = false, optional = true }
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_NetworkManagement_IpHelper",
"Win32_NetworkManagement_Ndis",
"Win32_Networking_WinSock",
"Win32_System_SystemInformation",
"Win32_System_Threading",
], optional = true }
[build-dependencies] [build-dependencies]
protobuf-codegen = "3.7" protobuf-codegen = "3.7"
toml = "0.9"
cc = "1"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

251
README.en.md Normal file
View File

@@ -0,0 +1,251 @@
<div align="center">
<h1>One-KVM</h1>
<p><strong>An open, lightweight IP-KVM stack in Rust — remote management down to BIOS level</strong></p>
<p><a href="README.md">简体中文</a> · <a href="README.en.md">English</a></p>
[![GitHub Release](https://img.shields.io/github/v/release/mofeng-git/One-KVM)](https://github.com/mofeng-git/One-KVM/releases)
[![GitHub stars](https://img.shields.io/github/stars/mofeng-git/One-KVM?style=social)](https://github.com/mofeng-git/One-KVM/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/mofeng-git/One-KVM?style=social)](https://github.com/mofeng-git/One-KVM/network/members)
[![GitHub issues](https://img.shields.io/github/issues/mofeng-git/One-KVM)](https://github.com/mofeng-git/One-KVM/issues)
</div>
---
## Overview
**One-KVM (Rust)** is a lightweight IP-KVM solution written in Rust. It lets you manage servers and workstations over the network, including at BIOS level.
Goals: an open, lightweight, easy-to-use IP-KVM stack.
- **Open**: not tied to one hardware recipe; runs across many setups.
- **Lightweight**: shipped as a binary with minimal moving parts for deployment.
- **Easy to use**: no hand-edited config files required; settings are done in the web UI.
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>.
<div align="center">
![One-KVM web console](https://one-kvm.cn/hero-app-effect.png)
</div>
## Features
**Core**
| Area | Capabilities |
|------|----------------|
| Video capture | HDMI USB / MIPI CSI / RK3588 HDMI IN; MJPEG and WebRTC (H.264 / H.265 / VP8 / VP9) |
| Video encoding | VAAPI / QSV / RKMPP / V4L2 M2M hardware paths, with software fallback |
| Keyboard & mouse | USB OTG HID or CH340 + CH9329 HID; absolute / relative mouse |
| Virtual media | USB mass storage; ISO/IMG mount and Ventoy-style virtual USB |
| ATX power | GPIO or USB relay; power control |
| Audio | ALSA capture + Opus (HTTP / WebRTC) |
## Installation
Release artifacts are on [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases). Below are short paths for common setups. For **system requirements, hardware, Docker env vars, USB OTG**, and full troubleshooting, see the [One-KVM documentation](https://docs.one-kvm.cn/) (Chinese; use a translator if needed).
### Debian / Ubuntu (.deb)
Download a `one-kvm_*.deb` matching your CPU architecture from Releases, then from the directory containing the package:
```bash
sudo apt update
sudo apt install ./one-kvm_0.x.x_<arch>.deb
```
Replace the version and architecture in the filename with your actual file name.
### Docker
Images:
- **one-kvm** — main app + ttyd
- **one-kvm-full** — same plus optional extras (e.g. gostc, easytier-core)
Example:
```bash
docker run --name one-kvm -itd \
--privileged=true --restart unless-stopped \
-v /dev:/dev -v /sys:/sys \
--net=host \
silentwind0/one-kvm-full
```
If pulls are slow, use the Aliyun mirror, e.g. `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm-full` (and `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm` for the slim image).
### fnOS NAS (Feiniu / 飞牛)
One-KVM is listed in the fnOS **app store**; search and install on your NAS.
### Web UI and first run
Open `http://<device-ip>:8080` in a browser (**8420** after fnOS install). The first visit runs initial setup.
## Reporting issues
If something breaks:
1. Open [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) or report in the project QQ group.
2. Include **useful** error messages and steps to reproduce.
3. Mention software version, hardware, and OS details.
## Sponsorship
One-KVM builds on many great open-source projects; a lot of time goes into testing and maintenance. If you find it useful, you can support development on **[Afdian (为爱发电)](https://afdian.com/a/silentwind)**.
### Thanks
<details>
<summary><strong>Supporter list</strong></summary>
- 浩龙的电子嵌入式之路
- Tsuki
- H_xiaoming
- 0蓝蓝0
- fairybl
- Will
- 自.知
- 观棋不语٩ ི۶
- 爱发电用户_a57a4
- 爱发电用户_2c769
- 霜序
- 远方(闲鱼用户名:小远技术店铺)
- 爱发电用户_399fc
- 斐斐の
- 爱发电用户_09451
- 超高校级的錆鱼
- 爱发电用户_08cff
- guoke
- mgt
- 姜沢掵
- ui_beam
- 爱发电用户_c0dd7
- 爱发电用户_dnjK
- 忍者胖猪
- 永遠の願い
- 爱发电用户_GBrF
- 爱发电用户_fd65c
- 爱发电用户_vhNa
- 爱发电用户_Xu6S
- moss
- woshididi
- 爱发电用户_a0fd1
- 爱发电用户_f6bH
- 码农
- 爱发电用户_6639f
- jeron
- 爱发电用户_CN7y
- 爱发电用户_Up6w
- 爱发电用户_e3202
- 一语念白
- 云边
- 爱发电用户_5a711
- 爱发电用户_9a706
- T0m9ir1SUKI
- 爱发电用户_56d52
- 爱发电用户_3N6F
- DUSK
- 飘零
- .
- 饭太稀
-
- MaxZ
- 爱发电用户_c5f33
- 爱发电用户_09386
- 爱发电用户_JT6c
- 爱发电用户_d3d9c
- 爱发电用户_97b41
- 偶然
- 爱发电用户_dba45
- 爱发电用户_d4f8b
- 故人。
- ......
</details>
### Sponsors
**File hosting**
- **[Huang1111 public-interest program](https://pan.huang1111.cn/s/mxkx3T1)** — login-free downloads
**Cloud**
- **[林枫云](https://www.dkdun.cn)** — project server sponsorship
![林枫云](https://docs.one-kvm.cn/img/36076FEFF0898A80EBD5756D28F4076C.png)
林枫云 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
![BTBT](https://github.com/user-attachments/assets/c442d5f5-d72f-4a07-b9f4-400a6a0c3f1e)
Remote computers, consumer GPU servers, and dedicated physical machines with fully automated online delivery.

106
README.md
View File

@@ -2,8 +2,9 @@
<h1>One-KVM</h1> <h1>One-KVM</h1>
<p><strong>Rust 编写的开放轻量 IP-KVM 解决方案,实现 BIOS 级远程管理</strong></p> <p><strong>Rust 编写的开放轻量 IP-KVM 解决方案,实现 BIOS 级远程管理</strong></p>
<p><a href="README.md">简体中文</a></p> <p><a href="README.md">简体中文</a> · <a href="README.en.md">English</a></p>
[![GitHub Release](https://img.shields.io/github/v/release/mofeng-git/One-KVM)](https://github.com/mofeng-git/One-KVM/releases)
[![GitHub stars](https://img.shields.io/github/stars/mofeng-git/One-KVM?style=social)](https://github.com/mofeng-git/One-KVM/stargazers) [![GitHub stars](https://img.shields.io/github/stars/mofeng-git/One-KVM?style=social)](https://github.com/mofeng-git/One-KVM/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/mofeng-git/One-KVM?style=social)](https://github.com/mofeng-git/One-KVM/network/members) [![GitHub forks](https://img.shields.io/github/forks/mofeng-git/One-KVM?style=social)](https://github.com/mofeng-git/One-KVM/network/members)
[![GitHub issues](https://img.shields.io/github/issues/mofeng-git/One-KVM)](https://github.com/mofeng-git/One-KVM/issues) [![GitHub issues](https://img.shields.io/github/issues/mofeng-git/One-KVM)](https://github.com/mofeng-git/One-KVM/issues)
@@ -11,61 +12,82 @@
--- ---
## 📖 项目概述 ## 项目概述
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。 **One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
项目目标: 项目目标:提供一个开放、轻量、易用的 IPKVM 解决方案。
- **开放**:不绑定特定硬件配置,尽量适配常见 Linux 设备 - **开放**:不绑定特定硬件配置,可在各类硬件环境中稳定运行。
- **轻量**二进制分发,部署过程简单 - **轻量**二进制文件形式分发,无繁杂的依赖项,部署过程简单
- **易用**网页界面完成设备与参数配置,无需手动配置文件 - **易用**:无需手动编辑配置文件,参数设置均可通过网页界面完成。
> **注意:** One-KVM Rust 目前仍处于开发早期阶段,功能与细节会快速迭代,欢迎体验与反馈 更多内容可访问 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)
## 🔁 迁移说明 > **One-KVM Python** 已停止开发,如有需要可访问 <https://github.com/mofeng-git/One-KVM/tree/python>。
开发重心正在从 **One-KVM Python** 逐步转向 **One-KVM Rust** <div align="center">
- 如果你在使用 **One-KVM Python基于 PiKVM**,请查看 [One-KVM Python 文档](https://docs.one-kvm.cn/python/) ![One-KVM Web 控制台界面](https://one-kvm.cn/hero-app-effect.png)
- One-KVM Rust 相较于 One-KVM Python**尚未完全适配 CSI HDMI 采集卡**、**不支持 VNC 访问**,仍处于开发早期阶段
## 📊 功能介绍 </div>
### 核心功能 ## 功能介绍
| 功能 | 说明 | **核心功能**
| 功能 | 能力说明 |
|------|------| |------|------|
| 视频采集 | HDMI USB 采集支持,提供 MJPEG / WebRTCH.264/H.265/VP8/VP9 | | 视频采集 | HDMI USB /MIPI CSI/RK3588 HDMI IN 采集支持,提供 MJPEG / WebRTCH.264/H.265/VP8/VP9 视频流|
| 视频编码 | VAAPI/QSV/RKMPP/V4L2M2M 硬件编码支持,以及软件编码兜底 |
| 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID支持绝对/相对鼠标模式 | | 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID支持绝对/相对鼠标模式 |
| 虚拟媒体 | USB Mass Storage支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 | | 虚拟媒体 | USB Mass Storage支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
| ATX 电源控制 | GPIO 控制电源/重启按钮 | | ATX 电源控制 | GPIO/USB 继电器,支持电源控制 |
| 音频传输 | ALSA 采集 + Opus 编码HTTP/WebRTC | | 音频传输 | ALSA 采集 + Opus 编码HTTP/WebRTC |
### 硬件编码 ## 安装使用
支持自动检测和选择硬件加速: 构建产物见 [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases)。以下为常见安装方式的简要步骤;**系统要求、硬件准备、Docker 环境变量与 USB OTG 等完整说明**请查阅 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
- **VAAPI**Intel/AMD GPU ### 使用 deb 安装Debian / Ubuntu
- **RKMPP**Rockchip SoC
- **V4L2 M2M**:通用硬件编码器
- **软件编码**CPU 编码
### 扩展能力 从 Releases 下载与本机架构匹配的 `one-kvm_*.deb`,在包所在目录执行:
- Web UI 配置,多语言支持(中文/英文) ```bash
- 内置 Web 终端ttyd、内网穿透支持gostc、P2P 组网支持EasyTier、RustDesk 协议集成(用于跨平台远程访问能力扩展)和 RTSP 视频流(用于视频推流) sudo apt update
sudo apt install ./one-kvm_0.x.x_<arch>.deb
```
## ⚡ 安装使用 将文件名中的版本号与架构替换为实际下载的包名。
可以访问 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/) 获取详细信息。 ### 使用 Docker
镜像分为 **one-kvm**One-KVM 主程序 + ttyd**one-kvm-full**(另含 gostc、easytier-core 等可选扩展),按需选用。
```bash
docker run --name one-kvm -itd \
--privileged=true --restart unless-stopped \
-v /dev:/dev -v /sys:/sys \
--net=host \
silentwind0/one-kvm-full
```
拉取较慢时,可将镜像名替换为阿里云加速,例如 `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm-full``one-kvm` 镜像同理,将 `silentwind0/one-kvm` 换为 `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm`)。
### 飞牛 NAS
One-KVM 已上架飞牛 **应用市场**,在 NAS 上直接搜索安装即可。
### 访问 Web 与首次配置
浏览器访问 `http://<设备 IP>:8080`(飞牛 NAS 安装后为 8420 端口)。首次访问将引导完成初始配置。
## 报告问题 ## 报告问题
如果您发现了问题,请: 如果您发现了问题,请:
1. 使用 [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) 报告,或加入 QQ 群聊反馈。 1. 使用 [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) 报告,或加入 QQ 群聊反馈。
2. 提供详细的错误信息和复现步骤 2. 提供有帮助的错误信息和复现步骤
3. 包含您硬件配置和系统信息 3. 包含您使用的软件版本、硬件配置和系统信息
## 赞助支持 ## 赞助支持
@@ -88,8 +110,6 @@
- Will - Will
- 浩龙的电子嵌入式之路
- 自.知 - 自.知
- 观棋不语٩ ི۶ - 观棋不语٩ ི۶
@@ -188,10 +208,18 @@
- 爱发电用户_JT6c - 爱发电用户_JT6c
- MaxZ
- 爱发电用户_d3d9c - 爱发电用户_d3d9c
- 爱发电用户_97b41
- 偶然
- 爱发电用户_dba45
- 爱发电用户_d4f8b
- 故人。
- ...... - ......
</details> </details>
@@ -205,8 +233,14 @@
**云服务商** **云服务商**
- **[林枫云](https://www.dkdun.cn)** - 赞助了本项目宁波大带宽服务器 - **[林枫云](https://www.dkdun.cn)** - 赞助了本项目服务器
![林枫云](https://docs.one-kvm.cn/img/36076FEFF0898A80EBD5756D28F4076C.png) <img height="128" alt="林枫云" src="https://docs.one-kvm.cn/img/36076FEFF0898A80EBD5756D28F4076C.png" />
林枫云主营国内外地域的精品线路业务服务器、高主频游戏服务器和大带宽服务器。 林枫云主营国内外地域的精品线路业务服务器、高主频游戏服务器和大带宽服务器。
- **[贝塔网络](https://my.beita.cc/?ref=github_onekvm)** - 赞助了本项目服务器
<img height="128" alt="BTBT" src="https://github.com/user-attachments/assets/c442d5f5-d72f-4a07-b9f4-400a6a0c3f1e" />
远程电脑、消费级GPU服务器、独服物理机全自动在线交付。

View File

@@ -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();
@@ -64,25 +49,6 @@ fn generate_secrets() {
pub mod ice { pub mod ice {
/// Google public STUN server URL (hardcoded) /// Google public STUN server URL (hardcoded)
pub const STUN_SERVER: &str = "stun:stun.l.google.com:19302"; pub const STUN_SERVER: &str = "stun:stun.l.google.com:19302";
/// TURN server URLs - not provided, users must configure their own
pub const TURN_URLS: &str = "";
/// TURN authentication username
pub const TURN_USERNAME: &str = "";
/// TURN authentication password
pub const TURN_PASSWORD: &str = "";
/// Always returns true since we have STUN
pub const fn is_configured() -> bool {
true
}
/// Always returns false since TURN is not provided
pub const fn has_turn() -> bool {
false
}
} }
/// RustDesk public server configuration - NOT PROVIDED /// RustDesk public server configuration - NOT PROVIDED
@@ -105,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)
}

View File

@@ -19,6 +19,28 @@ ARCH_MAP=(
build_arch() { build_arch() {
local rust_target="$1" local rust_target="$1"
case "${CHINAMIRRO:-}" in
1|true|TRUE|yes|YES|on|ON)
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--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:11}"
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) ==="
env \
CROSS_BUILD_OPTS="$cross_build_opts" \
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=rsproxy-sparse \
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_NET_GIT_FETCH_WITH_CLI=true \
RUSTUP_DIST_SERVER=https://rsproxy.cn \
RUSTUP_UPDATE_ROOT=https://rsproxy.cn/rustup \
cross build --release --target "$rust_target"
return
;;
esac
echo "=== Building: $rust_target (via cross with custom Dockerfile) ===" echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
cross build --release --target "$rust_target" cross build --release --target "$rust_target"
} }
@@ -49,6 +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 China mirrors"
exit 0 exit 0
;; ;;
*) *)
@@ -65,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"

View File

@@ -1,21 +1,55 @@
# Cross-compilation image for ARM64 based on Debian 11 # Cross-compilation image for ARM64 based on Debian 11
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility # Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
FROM debian:11 ARG DEBIAN_IMAGE=debian:11
FROM ${DEBIAN_IMAGE}
# Linux headers used by v4l2r bindgen ARG CHINAMIRRO=0
ARG LINUX_HEADERS_VERSION=6.6 ARG GH_PROXY=https://gh-proxy.com/
ARG LINUX_HEADERS_SHA256= 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
# Set Rustup mirrors (Aliyun) # Optionally use China mirrors for builds in China.
#ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \ RUN if [ "$CHINAMIRRO" = "1" ]; then \
# RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup sed -i -E \
-e 's|http://deb.debian.org/debian([[:space:]])|http://mirrors.tuna.tsinghua.edu.cn/debian\1|g' \
/etc/apt/sources.list; \
fi
# Install Rust toolchain # Install Rust toolchain
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
ca-certificates \ ca-certificates \
&& if [ "$CHINAMIRRO" = "1" ]; then \
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
fi \
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
&& if [ "$CHINAMIRRO" = "1" ]; then \
mkdir -p /root/.cargo; \
printf '%s\n' \
'[source.crates-io]' \
"replace-with = 'rsproxy-sparse'" \
'[source.rsproxy]' \
"registry = '${CARGO_INDEX_CN}'" \
'[source.rsproxy-sparse]' \
"registry = '${CARGO_REGISTRY_CN}'" \
'[registries.rsproxy]' \
"index = '${CARGO_INDEX_CN}'" \
'[net]' \
'git-fetch-with-cli = true' \
> /root/.cargo/config.toml; \
fi \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ENV PATH="/root/.cargo/bin:${PATH}" ENV PATH="/root/.cargo/bin:${PATH}"
@@ -49,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 \
libssl1.1 \
libssl1.1:arm64 \
libasound2-dev:arm64 \ libasound2-dev:arm64 \
libv4l-dev:arm64 \ libv4l-dev:arm64 \
libudev-dev:arm64 \ libudev-dev:arm64 \
@@ -58,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 \
@@ -84,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 \
@@ -93,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++ \
@@ -123,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 \
@@ -139,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 \
@@ -178,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 \
@@ -191,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 .. \
@@ -212,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 \
@@ -239,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 \
@@ -327,7 +393,11 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
&& rm -rf /tmp/ffmpeg-build /tmp/aarch64-cross.txt /tmp/aarch64-pkg-config && rm -rf /tmp/ffmpeg-build /tmp/aarch64-cross.txt /tmp/aarch64-pkg-config
# Add Rust target # Add Rust target
RUN rustup target add aarch64-unknown-linux-gnu RUN if [ "$CHINAMIRRO" = "1" ]; then \
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
fi \
&& rustup target add aarch64-unknown-linux-gnu
# Configure environment for cross-compilation # Configure environment for cross-compilation
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
@@ -341,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"

View File

@@ -1,21 +1,55 @@
# Cross-compilation image for ARMv7 based on Debian 11 # Cross-compilation image for ARMv7 based on Debian 11
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility # Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
FROM debian:11 ARG DEBIAN_IMAGE=debian:11
FROM ${DEBIAN_IMAGE}
# Linux headers used by v4l2r bindgen ARG CHINAMIRRO=0
ARG LINUX_HEADERS_VERSION=6.6 ARG GH_PROXY=https://gh-proxy.com/
ARG LINUX_HEADERS_SHA256= 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
# Set Rustup mirrors (Aliyun) # Optionally use China mirrors for builds in China.
#ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \ RUN if [ "$CHINAMIRRO" = "1" ]; then \
# RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup sed -i -E \
-e 's|http://deb.debian.org/debian([[:space:]])|http://mirrors.tuna.tsinghua.edu.cn/debian\1|g' \
/etc/apt/sources.list; \
fi
# Install Rust toolchain # Install Rust toolchain
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
ca-certificates \ ca-certificates \
&& if [ "$CHINAMIRRO" = "1" ]; then \
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
fi \
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
&& if [ "$CHINAMIRRO" = "1" ]; then \
mkdir -p /root/.cargo; \
printf '%s\n' \
'[source.crates-io]' \
"replace-with = 'rsproxy-sparse'" \
'[source.rsproxy]' \
"registry = '${CARGO_INDEX_CN}'" \
'[source.rsproxy-sparse]' \
"registry = '${CARGO_REGISTRY_CN}'" \
'[registries.rsproxy]' \
"index = '${CARGO_INDEX_CN}'" \
'[net]' \
'git-fetch-with-cli = true' \
> /root/.cargo/config.toml; \
fi \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ENV PATH="/root/.cargo/bin:${PATH}" ENV PATH="/root/.cargo/bin:${PATH}"
@@ -49,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 \
libssl1.1 \
libssl1.1:armhf \
libasound2-dev:armhf \ libasound2-dev:armhf \
libv4l-dev:armhf \ libv4l-dev:armhf \
libudev-dev:armhf \ libudev-dev:armhf \
@@ -57,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 \
@@ -83,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 \
@@ -92,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 \
@@ -115,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 \
@@ -132,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 \
@@ -167,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 \
@@ -180,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 .. \
@@ -201,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 \
@@ -228,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 \
@@ -316,7 +382,11 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
&& rm -rf /tmp/ffmpeg-build /tmp/armhf-cross.txt /tmp/armhf-pkg-config && rm -rf /tmp/ffmpeg-build /tmp/armhf-cross.txt /tmp/armhf-pkg-config
# Add Rust target # Add Rust target
RUN rustup target add armv7-unknown-linux-gnueabihf RUN if [ "$CHINAMIRRO" = "1" ]; then \
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
fi \
&& rustup target add armv7-unknown-linux-gnueabihf
# Configure environment for cross-compilation # Configure environment for cross-compilation
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \ ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
@@ -330,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"]

View File

@@ -1,21 +1,53 @@
# Cross-compilation image for x86_64 based on Debian 11 # Cross-compilation image for x86_64 based on Debian 11
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility # Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
FROM debian:11 ARG DEBIAN_IMAGE=debian:11
FROM ${DEBIAN_IMAGE}
# Linux headers used by v4l2r bindgen ARG CHINAMIRRO=0
ARG LINUX_HEADERS_VERSION=6.6 ARG GH_PROXY=https://gh-proxy.com/
ARG LINUX_HEADERS_SHA256= 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
# Set Rustup mirrors (Aliyun) # Optionally use China mirrors for builds in China.
#ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \ RUN if [ "$CHINAMIRRO" = "1" ]; then \
# RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup sed -i -E \
-e 's|http://deb.debian.org/debian([[:space:]])|http://mirrors.tuna.tsinghua.edu.cn/debian\1|g' \
/etc/apt/sources.list; \
fi
# Install Rust toolchain # Install Rust toolchain
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
ca-certificates \ ca-certificates \
&& if [ "$CHINAMIRRO" = "1" ]; then \
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
fi \
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
&& if [ "$CHINAMIRRO" = "1" ]; then \
mkdir -p /root/.cargo; \
printf '%s\n' \
'[source.crates-io]' \
"replace-with = 'rsproxy-sparse'" \
'[source.rsproxy]' \
"registry = '${CARGO_INDEX_CN}'" \
'[source.rsproxy-sparse]' \
"registry = '${CARGO_REGISTRY_CN}'" \
'[registries.rsproxy]' \
"index = '${CARGO_INDEX_CN}'" \
'[net]' \
'git-fetch-with-cli = true' \
> /root/.cargo/config.toml; \
fi \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ENV PATH="/root/.cargo/bin:${PATH}" ENV PATH="/root/.cargo/bin:${PATH}"
@@ -56,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) \
@@ -105,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 \
@@ -135,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 \
@@ -221,7 +280,11 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
&& rm -rf /tmp/ffmpeg-build && rm -rf /tmp/ffmpeg-build
# Add Rust target # Add Rust target
RUN rustup target add x86_64-unknown-linux-gnu RUN if [ "$CHINAMIRRO" = "1" ]; then \
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
fi \
&& rustup target add x86_64-unknown-linux-gnu
# Configure environment for static linking # Configure environment for static linking
ENV PKG_CONFIG_ALLOW_CROSS=1\ ENV PKG_CONFIG_ALLOW_CROSS=1\

View File

@@ -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
Wants=network-online.target Wants=network-online.target
[Service] [Service]

View File

@@ -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.31), 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"

87
build/windows/build.ps1 Normal file
View File

@@ -0,0 +1,87 @@
param(
[string]$Configuration = "debug",
[string]$Target = "x86_64-pc-windows-msvc",
[string]$Triplet = "x64-windows-static",
[string]$VcpkgRoot = $env:VCPKG_ROOT,
[string]$VcpkgInstalledRoot = $env:VCPKG_INSTALLED_DIR,
[switch]$NoDefaultFeatures,
[string[]]$Features = @(),
[switch]$Package,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$CargoArgs = @()
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
Set-Location $repoRoot
if ([string]::IsNullOrWhiteSpace($VcpkgRoot)) {
$VcpkgRoot = Join-Path (Split-Path $repoRoot -Parent) "vcpkg"
}
$VcpkgRoot = [System.IO.Path]::GetFullPath($VcpkgRoot)
if ([string]::IsNullOrWhiteSpace($VcpkgInstalledRoot)) {
$VcpkgInstalledRoot = Join-Path $VcpkgRoot "installed"
}
$VcpkgInstalledRoot = [System.IO.Path]::GetFullPath($VcpkgInstalledRoot)
$vcpkgTripletRoot = Join-Path $VcpkgInstalledRoot $Triplet
$turbojpegLibDir = Join-Path $vcpkgTripletRoot "lib"
$turbojpegIncludeDir = Join-Path $vcpkgTripletRoot "include"
if (-not (Test-Path $VcpkgRoot)) {
throw "VCPKG_ROOT does not exist: $VcpkgRoot. Run build/windows/bootstrap-vcpkg.ps1 first."
}
if (-not (Test-Path $turbojpegLibDir) -or -not (Test-Path $turbojpegIncludeDir)) {
throw "vcpkg triplet is not installed at $vcpkgTripletRoot. Run build/windows/bootstrap-vcpkg.ps1 first."
}
$env:VCPKG_ROOT = $VcpkgRoot
$env:VCPKG_DEFAULT_TRIPLET = $Triplet
$env:VCPKG_INSTALLED_DIR = $VcpkgInstalledRoot
$env:TURBOJPEG_SOURCE = "explicit"
$env:TURBOJPEG_LIB_DIR = $turbojpegLibDir
$env:TURBOJPEG_INCLUDE_DIR = $turbojpegIncludeDir
$cargoCommand = @("build", "--target", $Target)
if ($Configuration -eq "release") {
$cargoCommand += "--release"
} elseif ($Configuration -ne "debug") {
throw "Unsupported configuration '$Configuration'. Use 'debug' or 'release'."
}
if ($NoDefaultFeatures) {
$cargoCommand += "--no-default-features"
}
if ($Features.Count -gt 0) {
$cargoCommand += "--features"
$cargoCommand += ($Features -join ",")
}
$cargoCommand += $CargoArgs
cargo @cargoCommand
if ($Package) {
$metadata = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json
$packageInfo = $metadata.packages | Where-Object { $_.name -eq "one-kvm" } | Select-Object -First 1
if ($null -eq $packageInfo -or [string]::IsNullOrWhiteSpace($packageInfo.version)) {
throw "Failed to resolve version from Cargo metadata"
}
$sourcePath = Join-Path $repoRoot "target/$Target/release/one-kvm.exe"
$targetName = "one-kvm_{0}_amd64.exe" -f $packageInfo.version
$targetPath = Join-Path $repoRoot "target/$Target/release/$targetName"
if (-not (Test-Path $sourcePath)) {
throw "Windows binary not found: $sourcePath"
}
Copy-Item $sourcePath $targetPath
Write-Host $targetPath
}

View File

@@ -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

View File

@@ -4,19 +4,21 @@ version = "0.8.0"
edition = "2021" edition = "2021"
description = "Hardware video codec for IP-KVM (Windows/Linux)" description = "Hardware video codec for IP-KVM (Windows/Linux)"
[package.metadata.cargo-machete]
ignored = ["serde"]
[features] [features]
default = [] default = []
bytes = ["dep:bytes"]
rkmpp = [] rkmpp = []
[dependencies] [dependencies]
log = "0.4" log = "0.4"
bytes = { version = "1", optional = true }
serde_derive = "1.0" serde_derive = "1.0"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
[build-dependencies] [build-dependencies]
cc = "1.0" cc = "1.0"
bindgen = "0.59" bindgen = "0.72"
[dev-dependencies]
env_logger = "0.10"

View File

@@ -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

View File

@@ -21,11 +21,12 @@ fn build_common(builder: &mut Build) {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let common_dir = manifest_dir.join("cpp").join("common"); let common_dir = manifest_dir.join("cpp").join("common");
bindgen::builder() let 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"))
@@ -34,7 +35,9 @@ fn build_common(builder: &mut Build) {
// system // system
#[cfg(windows)] #[cfg(windows)]
{ {
["d3d11", "dxgi"].map(|lib| println!("cargo:rustc-link-lib={}", lib)); for lib in ["d3d11", "dxgi"] {
println!("cargo:rustc-link-lib={}", lib);
}
} }
builder.include(&common_dir); builder.include(&common_dir);
@@ -69,9 +72,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())
@@ -89,8 +92,8 @@ mod ffmpeg {
ffmpeg_ffi(); ffmpeg_ffi();
// Try VCPKG first, fallback to system FFmpeg via pkg-config // Try VCPKG first, fallback to system FFmpeg via pkg-config
if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") { if let Some(vcpkg_installed) = vcpkg_installed_root() {
link_vcpkg(builder, vcpkg_root.into()); link_vcpkg(builder, vcpkg_installed);
} else { } else {
// Use system FFmpeg via pkg-config // Use system FFmpeg via pkg-config
link_system_ffmpeg(builder); link_system_ffmpeg(builder);
@@ -99,6 +102,23 @@ mod ffmpeg {
link_os(); link_os();
build_ffmpeg_ram(builder); build_ffmpeg_ram(builder);
build_ffmpeg_hw(builder); build_ffmpeg_hw(builder);
build_ffmpeg_capture(builder);
}
fn vcpkg_installed_root() -> Option<PathBuf> {
println!("cargo:rerun-if-env-changed=VCPKG_INSTALLED_DIR");
println!("cargo:rerun-if-env-changed=VCPKG_ROOT");
if let Ok(path) = std::env::var("VCPKG_INSTALLED_DIR") {
if !path.trim().is_empty() {
return Some(PathBuf::from(path));
}
}
std::env::var("VCPKG_ROOT")
.ok()
.filter(|path| !path.trim().is_empty())
.map(|path| PathBuf::from(path).join("installed"))
} }
/// Link system FFmpeg using pkg-config or custom path /// Link system FFmpeg using pkg-config or custom path
@@ -142,7 +162,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
@@ -151,13 +171,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");
} }
} }
@@ -216,12 +234,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);
@@ -271,7 +283,6 @@ mod ffmpeg {
target = target.replace("x64", "x86"); target = target.replace("x64", "x86");
} }
println!("cargo:info={}", target); println!("cargo:info={}", target);
path.push("installed");
path.push(target); path.push(target);
println!( println!(
@@ -282,15 +293,26 @@ mod ffmpeg {
) )
); );
{ {
// Only need avcodec and avutil for encoding // avdevice/avformat are needed by the Windows DirectShow capture bridge.
let mut static_libs = vec!["avcodec", "avutil"]; let mut static_libs = vec!["avcodec", "avutil"];
if target_os == "windows" { if target_os == "windows" {
static_libs.push("libmfx"); static_libs.extend([
"avformat",
"avdevice",
"avfilter",
"swresample",
"swscale",
"vpx",
"libx264",
"x265-static",
]);
}
for lib in static_libs {
println!("cargo:rustc-link-lib=static={}", lib);
}
if target_os == "windows" {
link_windows_qsv_lib(&path.join("lib"));
} }
static_libs
.iter()
.map(|lib| println!("cargo:rustc-link-lib=static={}", lib))
.count();
} }
let include = path.join("include"); let include = path.join("include");
@@ -299,12 +321,28 @@ mod ffmpeg {
include include
} }
fn link_windows_qsv_lib(lib_dir: &Path) {
if lib_dir.join("libmfx.lib").exists() {
println!("cargo:rustc-link-lib=static=libmfx");
println!("cargo:info=Using Windows QSV support library libmfx.lib");
return;
}
println!(
"cargo:warning=Windows QSV support library not found in {}",
lib_dir.display()
);
}
fn link_os() { fn link_os() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let dyn_libs: Vec<&str> = if target_os == "windows" { let dyn_libs: Vec<&str> = if target_os == "windows" {
["User32", "bcrypt", "ole32", "advapi32"].to_vec() [
"User32", "bcrypt", "ole32", "advapi32", "mfuuid", "strmiids",
]
.to_vec()
} else if target_os == "linux" { } else if target_os == "linux" {
// Base libraries for all Linux platforms // Base libraries for all Linux platforms
let mut v = vec!["drm", "stdc++"]; let mut v = vec!["drm", "stdc++"];
@@ -334,9 +372,8 @@ mod ffmpeg {
let ffi_header_path = ffmpeg_ram_dir.join("ffmpeg_ffi.h"); let ffi_header_path = ffmpeg_ram_dir.join("ffmpeg_ffi.h");
println!("cargo:rerun-if-changed={}", ffi_header_path.display()); println!("cargo:rerun-if-changed={}", ffi_header_path.display());
let ffi_header = ffi_header_path.to_string_lossy().to_string(); let ffi_header = ffi_header_path.to_string_lossy().to_string();
bindgen::builder() let 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"))
@@ -350,9 +387,8 @@ mod ffmpeg {
.join("ffmpeg_ram_ffi.h") .join("ffmpeg_ram_ffi.h")
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
bindgen::builder() let 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"))
@@ -367,6 +403,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 {}",
@@ -375,6 +414,34 @@ mod ffmpeg {
} }
} }
fn build_ffmpeg_capture(builder: &mut Build) {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "windows" {
return;
}
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let capture_header = manifest_dir
.join("cpp")
.join("ffmpeg_capture_ffi.h")
.to_string_lossy()
.to_string();
bindgen::builder()
.header(capture_header)
.rustified_enum(".*")
.generate()
.unwrap()
.write_to_file(
Path::new(&env::var_os("OUT_DIR").unwrap()).join("ffmpeg_capture_ffi.rs"),
)
.unwrap();
builder.file(manifest_dir.join("cpp").join("ffmpeg_capture.cpp"));
println!("cargo:rustc-link-lib=strmiids");
println!("cargo:rustc-link-lib=oleaut32");
println!("cargo:rustc-link-lib=quartz");
}
fn build_ffmpeg_hw(builder: &mut Build) { fn build_ffmpeg_hw(builder: &mut Build) {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw"); let ffmpeg_hw_dir = manifest_dir.join("cpp").join("ffmpeg_hw");
@@ -384,7 +451,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"))

View File

@@ -25,6 +25,7 @@ enum AVPixelFormat {
AV_PIX_FMT_NV24 = 188, AV_PIX_FMT_NV24 = 188,
}; };
int av_get_pix_fmt(const char *name);
int av_log_get_level(void); int av_log_get_level(void);
void av_log_set_level(int level); void av_log_set_level(int level);
void hwcodec_set_av_log_callback(); void hwcodec_set_av_log_callback();

View File

@@ -122,12 +122,12 @@ int linux_support_v4l2m2m() {
if (!file.is_open()) { if (!file.is_open()) {
return false; return false;
} }
std::getline(file, *out); std::getline(file, *out, '\0');
return !out->empty(); return !out->empty();
}; };
auto allow_video0_probe = []() -> bool { auto v4l2m2m_allowed = []() -> bool {
const char *env = std::getenv("ONE_KVM_V4L2M2M_ALLOW_VIDEO0"); const char *env = std::getenv("ONE_KVM_V4L2M2M_ALLOW");
if (env == nullptr) { if (env == nullptr) {
return false; return false;
} }
@@ -137,30 +137,90 @@ int linux_support_v4l2m2m() {
return std::strcmp(env, "0") != 0; return std::strcmp(env, "0") != 0;
}; };
auto is_amlogic_vdec = [&]() -> bool { auto contains_any = [](const std::string &value, const char *const *needles, size_t len) -> bool {
std::string name; for (size_t i = 0; i < len; i++) {
std::string modalias; if (value.find(needles[i]) != std::string::npos) {
if (read_text_file("/sys/class/video4linux/video0/name", &name)) {
const std::string lowered = to_lower(name);
if (lowered.find("meson") != std::string::npos ||
lowered.find("vdec") != std::string::npos ||
lowered.find("decoder") != std::string::npos ||
lowered.find("video-decoder") != std::string::npos) {
return true;
}
}
if (read_text_file("/sys/class/video4linux/video0/device/modalias", &modalias)) {
const std::string lowered = to_lower(modalias);
if (lowered.find("amlogic") != std::string::npos ||
lowered.find("meson") != std::string::npos ||
lowered.find("gxl-vdec") != std::string::npos ||
lowered.find("gx-vdec") != std::string::npos) {
return true; return true;
} }
} }
return false; return false;
}; };
auto is_amlogic_platform = [&]() -> bool {
const char *platform_hints[] = {
"amlogic",
"meson",
"gxl",
"gxbb",
"gxm",
"g12a",
"g12b",
"sm1",
};
const char *platform_files[] = {
"/proc/device-tree/compatible",
"/proc/device-tree/model",
"/sys/firmware/devicetree/base/compatible",
"/sys/firmware/devicetree/base/model",
};
for (size_t i = 0; i < sizeof(platform_files) / sizeof(platform_files[0]); i++) {
std::string value;
if (read_text_file(platform_files[i], &value) &&
contains_any(to_lower(value), platform_hints,
sizeof(platform_hints) / sizeof(platform_hints[0]))) {
return true;
}
}
const char *video_nodes[] = {
"video0",
"video1",
"video2",
"video10",
"video11",
"video32",
};
const char *vdec_hints[] = {
"meson",
"amlogic",
"vdec",
"decoder",
"video-decoder",
"gxl-vdec",
"gx-vdec",
};
for (size_t i = 0; i < sizeof(video_nodes) / sizeof(video_nodes[0]); i++) {
std::string name;
std::string modalias;
const std::string base = std::string("/sys/class/video4linux/") + video_nodes[i];
if (read_text_file((base + "/name").c_str(), &name) &&
contains_any(to_lower(name), vdec_hints, sizeof(vdec_hints) / sizeof(vdec_hints[0]))) {
return true;
}
if (read_text_file((base + "/device/modalias").c_str(), &modalias) &&
contains_any(to_lower(modalias), vdec_hints,
sizeof(vdec_hints) / sizeof(vdec_hints[0]))) {
return true;
}
}
return false;
};
const bool amlogic_platform = is_amlogic_platform();
if (amlogic_platform && !v4l2m2m_allowed()) {
LOG_WARN(std::string(
"V4L2 M2M: skipped probe on Amlogic platform; set ONE_KVM_V4L2M2M_ALLOW=1 to enable"));
return -1;
}
if (amlogic_platform) {
LOG_WARN(std::string("V4L2 M2M: ONE_KVM_V4L2M2M_ALLOW is set; probing Amlogic video nodes"));
}
// Check common V4L2 M2M device paths used by various ARM SoCs // Check common V4L2 M2M device paths used by various ARM SoCs
// /dev/video10 - Standard on many SoCs // /dev/video10 - Standard on many SoCs
// /dev/video11 - Standard on many SoCs (often decoder) // /dev/video11 - Standard on many SoCs (often decoder)
@@ -179,13 +239,6 @@ int linux_support_v4l2m2m() {
for (size_t i = 0; i < sizeof(m2m_devices) / sizeof(m2m_devices[0]); i++) { for (size_t i = 0; i < sizeof(m2m_devices) / sizeof(m2m_devices[0]); i++) {
if (access(m2m_devices[i], F_OK) == 0) { if (access(m2m_devices[i], F_OK) == 0) {
if (std::strcmp(m2m_devices[i], "/dev/video0") == 0) {
if (!allow_video0_probe() && is_amlogic_vdec()) {
LOG_TRACE(std::string("V4L2 M2M: Skipping /dev/video0 (Amlogic vdec)"));
continue;
}
}
// Device exists, check if it's an M2M device by trying to open it // Device exists, check if it's an M2M device by trying to open it
int fd = open(m2m_devices[i], O_RDWR | O_NONBLOCK); int fd = open(m2m_devices[i], O_RDWR | O_NONBLOCK);
if (fd >= 0) { if (fd >= 0) {

View File

@@ -1,4 +1,5 @@
extern "C" { extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h> #include <libavutil/opt.h>
} }
@@ -23,7 +24,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;
@@ -36,7 +37,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;
@@ -99,13 +100,12 @@ void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs,
c->color_primaries = AVCOL_PRI_SMPTE170M; c->color_primaries = AVCOL_PRI_SMPTE170M;
c->color_trc = AVCOL_TRC_SMPTE170M; c->color_trc = AVCOL_TRC_SMPTE170M;
// Profile selection: use BASELINE for software H264 (faster, simpler) // WebRTC SDP advertises constrained baseline. Keep hardware and software
if (is_software_h264(name)) { // encoders on the same browser-friendly H264 profile.
c->profile = FF_PROFILE_H264_BASELINE; // Simpler profile for real-time if (name.find("h264") != std::string::npos) {
} else if (name.find("h264") != std::string::npos) { c->profile = AV_PROFILE_H264_CONSTRAINED_BASELINE;
c->profile = FF_PROFILE_H264_HIGH;
} else if (name.find("hevc") != std::string::npos) { } else if (name.find("hevc") != std::string::npos) {
c->profile = FF_PROFILE_HEVC_MAIN; c->profile = AV_PROFILE_HEVC_MAIN;
} }
} }
@@ -120,8 +120,7 @@ bool set_lantency_free(void *priv_data, const std::string &name) {
} }
if (name.find("amf") != std::string::npos) { if (name.find("amf") != std::string::npos) {
if ((ret = av_opt_set(priv_data, "query_timeout", "1000", 0)) < 0) { if ((ret = av_opt_set(priv_data, "query_timeout", "1000", 0)) < 0) {
LOG_ERROR(std::string("amf set_lantency_free failed, ret = ") + av_err2str(ret)); LOG_WARN(std::string("amf query_timeout option is unavailable, ret = ") + av_err2str(ret));
return false;
} }
} }
if (name.find("qsv") != std::string::npos) { if (name.find("qsv") != std::string::npos) {
@@ -306,23 +305,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;
@@ -378,6 +360,25 @@ 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) {
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;
@@ -385,9 +386,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"}}},
}; };
@@ -402,13 +400,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;
} }

View File

@@ -0,0 +1,879 @@
#define NOMINMAX
#include "ffmpeg_capture_ffi.h"
#include <Windows.h>
#include <dshow.h>
#include <dvdmedia.h>
extern "C" {
#include <libavcodec/codec_id.h>
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/error.h>
#include <libavutil/pixfmt.h>
}
#include <atomic>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#pragma comment(lib, "strmiids")
thread_local std::string g_last_error;
struct HwcodecDshowCaptureContext {
AVFormatContext* format_ctx = nullptr;
int stream_index = -1;
int width = 0;
int height = 0;
int pixel_format = HWCODEC_CAPTURE_FMT_UNKNOWN;
int stride = 0;
int timeout_ms = 2000;
std::atomic<long long> deadline_ms{0};
std::atomic<int> timed_out{0};
uint64_t sequence = 0;
};
namespace {
struct DshowCapabilityEntry {
std::string format;
int width = 0;
int height = 0;
std::vector<int> fps;
};
const char* requested_pixel_format_name(int requested_format);
void set_last_error(const std::string& message) {
g_last_error = message;
}
std::string ffmpeg_error(int errnum) {
char buffer[AV_ERROR_MAX_STRING_SIZE] = {0};
av_make_error_string(buffer, sizeof(buffer), errnum);
return std::string(buffer);
}
long long now_ms() {
return static_cast<long long>(GetTickCount64());
}
std::string wide_to_utf8(const wchar_t* value) {
if (!value) {
return std::string();
}
int size = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr);
if (size <= 1) {
return std::string();
}
std::string result(static_cast<size_t>(size - 1), '\0');
WideCharToMultiByte(
CP_UTF8,
0,
value,
-1,
result.empty() ? nullptr : &result[0],
size,
nullptr,
nullptr);
return result;
}
void add_fps_candidate(std::vector<int>* fps, LONGLONG interval_100ns) {
if (!fps || interval_100ns <= 0) {
return;
}
double fps_value = 10000000.0 / static_cast<double>(interval_100ns);
int rounded = static_cast<int>(fps_value + 0.5);
if (rounded <= 0) {
return;
}
if (std::find(fps->begin(), fps->end(), rounded) == fps->end()) {
fps->push_back(rounded);
}
}
void normalize_fps(std::vector<int>* fps) {
if (!fps) {
return;
}
std::sort(fps->begin(), fps->end(), std::greater<int>());
fps->erase(std::unique(fps->begin(), fps->end()), fps->end());
}
const char* media_subtype_to_format(const GUID& subtype) {
if (subtype == MEDIASUBTYPE_MJPG) {
return "MJPEG";
}
if (subtype == MEDIASUBTYPE_YUY2) {
return "YUYV";
}
if (subtype == MEDIASUBTYPE_UYVY) {
return "UYVY";
}
if (subtype == MEDIASUBTYPE_YVYU) {
return "YVYU";
}
if (subtype == MEDIASUBTYPE_NV12) {
return "NV12";
}
if (subtype == MEDIASUBTYPE_RGB24) {
return "RGB24";
}
if (subtype == MEDIASUBTYPE_RGB32) {
return "BGR24";
}
if (subtype == MEDIASUBTYPE_IYUV) {
return "YUV420";
}
if (subtype == MEDIASUBTYPE_YV12) {
return "YVU420";
}
return nullptr;
}
void free_media_type(AM_MEDIA_TYPE* media_type) {
if (!media_type) {
return;
}
if (media_type->cbFormat != 0) {
CoTaskMemFree(media_type->pbFormat);
media_type->cbFormat = 0;
media_type->pbFormat = nullptr;
}
if (media_type->pUnk != nullptr) {
media_type->pUnk->Release();
media_type->pUnk = nullptr;
}
CoTaskMemFree(media_type);
}
bool fill_capability_entry(
const AM_MEDIA_TYPE* media_type,
const VIDEO_STREAM_CONFIG_CAPS* caps,
DshowCapabilityEntry* out_entry) {
if (!media_type || !out_entry) {
return false;
}
const char* format = media_subtype_to_format(media_type->subtype);
if (!format) {
return false;
}
LONG width = 0;
LONG height = 0;
REFERENCE_TIME avg_time_per_frame = 0;
if (media_type->formattype == FORMAT_VideoInfo && media_type->pbFormat &&
media_type->cbFormat >= sizeof(VIDEOINFOHEADER)) {
const auto* info = reinterpret_cast<const VIDEOINFOHEADER*>(media_type->pbFormat);
width = info->bmiHeader.biWidth;
height = std::abs(info->bmiHeader.biHeight);
avg_time_per_frame = info->AvgTimePerFrame;
} else if (media_type->formattype == FORMAT_VideoInfo2 && media_type->pbFormat &&
media_type->cbFormat >= sizeof(VIDEOINFOHEADER2)) {
const auto* info = reinterpret_cast<const VIDEOINFOHEADER2*>(media_type->pbFormat);
width = info->bmiHeader.biWidth;
height = std::abs(info->bmiHeader.biHeight);
avg_time_per_frame = info->AvgTimePerFrame;
}
if ((width <= 0 || height <= 0) && caps) {
width = std::max<LONG>(caps->InputSize.cx, caps->MinOutputSize.cx);
height = std::max<LONG>(caps->InputSize.cy, caps->MinOutputSize.cy);
if (width <= 0 || height <= 0) {
width = caps->MaxOutputSize.cx;
height = caps->MaxOutputSize.cy;
}
}
if (width <= 0 || height <= 0) {
return false;
}
out_entry->format = format;
out_entry->width = static_cast<int>(width);
out_entry->height = static_cast<int>(height);
out_entry->fps.clear();
add_fps_candidate(&out_entry->fps, avg_time_per_frame);
if (caps) {
add_fps_candidate(&out_entry->fps, caps->MinFrameInterval);
add_fps_candidate(&out_entry->fps, caps->MaxFrameInterval);
}
normalize_fps(&out_entry->fps);
return true;
}
void append_stream_capabilities(IAMStreamConfig* stream_config, std::vector<DshowCapabilityEntry>* entries) {
if (!stream_config || !entries) {
return;
}
int cap_count = 0;
int cap_size = 0;
HRESULT hr = stream_config->GetNumberOfCapabilities(&cap_count, &cap_size);
if (FAILED(hr) || cap_count <= 0 || cap_size < static_cast<int>(sizeof(VIDEO_STREAM_CONFIG_CAPS))) {
return;
}
std::vector<BYTE> caps_buffer(static_cast<size_t>(cap_size));
for (int index = 0; index < cap_count; ++index) {
AM_MEDIA_TYPE* media_type = nullptr;
hr = stream_config->GetStreamCaps(index, &media_type, caps_buffer.data());
if (FAILED(hr) || !media_type) {
continue;
}
DshowCapabilityEntry entry;
const auto* caps = reinterpret_cast<const VIDEO_STREAM_CONFIG_CAPS*>(caps_buffer.data());
if (fill_capability_entry(media_type, caps, &entry)) {
entries->push_back(std::move(entry));
}
free_media_type(media_type);
}
}
bool find_device_filter(const std::string& device_name, IBaseFilter** out_filter) {
if (!out_filter) {
return false;
}
*out_filter = nullptr;
ICreateDevEnum* dev_enum = nullptr;
IEnumMoniker* enum_moniker = nullptr;
HRESULT hr = CoCreateInstance(
CLSID_SystemDeviceEnum,
nullptr,
CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum,
reinterpret_cast<void**>(&dev_enum));
if (FAILED(hr) || !dev_enum) {
return false;
}
hr = dev_enum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enum_moniker, 0);
dev_enum->Release();
if (hr != S_OK || !enum_moniker) {
return false;
}
bool found = false;
IMoniker* moniker = nullptr;
ULONG fetched = 0;
while (!found && enum_moniker->Next(1, &moniker, &fetched) == S_OK) {
IPropertyBag* bag = nullptr;
hr = moniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, reinterpret_cast<void**>(&bag));
if (SUCCEEDED(hr) && bag) {
VARIANT name;
VariantInit(&name);
if (SUCCEEDED(bag->Read(L"FriendlyName", &name, nullptr)) && name.vt == VT_BSTR) {
auto utf8_name = wide_to_utf8(name.bstrVal);
if (utf8_name == device_name) {
hr = moniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, reinterpret_cast<void**>(out_filter));
found = SUCCEEDED(hr) && *out_filter != nullptr;
}
}
VariantClear(&name);
bag->Release();
}
moniker->Release();
}
enum_moniker->Release();
return found;
}
std::string build_capabilities_payload(const std::vector<DshowCapabilityEntry>& entries) {
std::string payload;
for (size_t i = 0; i < entries.size(); ++i) {
const auto& entry = entries[i];
payload += entry.format;
payload.push_back('|');
payload += std::to_string(entry.width);
payload.push_back('|');
payload += std::to_string(entry.height);
payload.push_back('|');
for (size_t fps_index = 0; fps_index < entry.fps.size(); ++fps_index) {
payload += std::to_string(entry.fps[fps_index]);
if (fps_index + 1 < entry.fps.size()) {
payload.push_back(',');
}
}
if (i + 1 < entries.size()) {
payload.push_back('\n');
}
}
return payload;
}
char* copy_payload(const std::string& payload) {
char* out = reinterpret_cast<char*>(std::malloc(payload.size() + 1));
if (!out) {
set_last_error("Failed to allocate capture payload buffer");
return nullptr;
}
std::memcpy(out, payload.c_str(), payload.size() + 1);
return out;
}
int open_dshow_input_with_options(
AVFormatContext** format_ctx,
const AVInputFormat* input,
const std::string& device_name,
int width,
int height,
int fps,
int requested_format,
bool use_video_size,
bool use_framerate,
bool use_pixel_format,
std::string* attempt_desc) {
if (!format_ctx || !input) {
return AVERROR(EINVAL);
}
AVDictionary* options = nullptr;
std::vector<std::string> parts;
if (use_video_size && width > 0 && height > 0) {
std::string video_size = std::to_string(width) + "x" + std::to_string(height);
av_dict_set(&options, "video_size", video_size.c_str(), 0);
parts.push_back("video_size=" + video_size);
}
if (use_framerate && fps > 0) {
std::string framerate = std::to_string(fps);
av_dict_set(&options, "framerate", framerate.c_str(), 0);
parts.push_back("framerate=" + framerate);
}
av_dict_set(&options, "rtbufsize", "64M", 0);
parts.push_back("rtbufsize=64M");
const char* pixel_format_name = requested_pixel_format_name(requested_format);
if (use_pixel_format && pixel_format_name) {
av_dict_set(&options, "pixel_format", pixel_format_name, 0);
parts.push_back(std::string("pixel_format=") + pixel_format_name);
}
if (attempt_desc) {
*attempt_desc = parts.empty() ? "default options" : "options{";
if (!parts.empty()) {
for (size_t i = 0; i < parts.size(); ++i) {
if (i > 0) {
attempt_desc->append(", ");
}
attempt_desc->append(parts[i]);
}
attempt_desc->append("}");
}
}
std::string input_name = "video=" + device_name;
int ret = avformat_open_input(format_ctx, input_name.c_str(), input, &options);
av_dict_free(&options);
return ret;
}
class ScopedComInit {
public:
ScopedComInit() {
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
initialized_ = hr == S_OK || hr == S_FALSE;
}
~ScopedComInit() {
if (initialized_) {
CoUninitialize();
}
}
private:
bool initialized_ = false;
};
int capture_stride(int pixel_format, int width) {
switch (pixel_format) {
case HWCODEC_CAPTURE_FMT_YUYV:
case HWCODEC_CAPTURE_FMT_YVYU:
case HWCODEC_CAPTURE_FMT_UYVY:
return width * 2;
case HWCODEC_CAPTURE_FMT_RGB24:
case HWCODEC_CAPTURE_FMT_BGR24:
return width * 3;
case HWCODEC_CAPTURE_FMT_NV24:
return width * 2;
case HWCODEC_CAPTURE_FMT_NV12:
case HWCODEC_CAPTURE_FMT_NV21:
case HWCODEC_CAPTURE_FMT_NV16:
case HWCODEC_CAPTURE_FMT_YUV420:
case HWCODEC_CAPTURE_FMT_YVU420:
case HWCODEC_CAPTURE_FMT_GREY:
case HWCODEC_CAPTURE_FMT_MJPEG:
case HWCODEC_CAPTURE_FMT_JPEG:
default:
return width;
}
}
int map_raw_pixfmt(int format) {
switch (format) {
case AV_PIX_FMT_YUYV422:
return HWCODEC_CAPTURE_FMT_YUYV;
case AV_PIX_FMT_UYVY422:
return HWCODEC_CAPTURE_FMT_UYVY;
#ifdef AV_PIX_FMT_YVYU422
case AV_PIX_FMT_YVYU422:
return HWCODEC_CAPTURE_FMT_YVYU;
#endif
case AV_PIX_FMT_NV12:
return HWCODEC_CAPTURE_FMT_NV12;
case AV_PIX_FMT_NV21:
return HWCODEC_CAPTURE_FMT_NV21;
#ifdef AV_PIX_FMT_NV16
case AV_PIX_FMT_NV16:
return HWCODEC_CAPTURE_FMT_NV16;
#endif
#ifdef AV_PIX_FMT_NV24
case AV_PIX_FMT_NV24:
return HWCODEC_CAPTURE_FMT_NV24;
#endif
case AV_PIX_FMT_YUV420P:
return HWCODEC_CAPTURE_FMT_YUV420;
#ifdef AV_PIX_FMT_YVU420P
case AV_PIX_FMT_YVU420P:
return HWCODEC_CAPTURE_FMT_YVU420;
#endif
case AV_PIX_FMT_RGB24:
return HWCODEC_CAPTURE_FMT_RGB24;
case AV_PIX_FMT_BGR24:
return HWCODEC_CAPTURE_FMT_BGR24;
case AV_PIX_FMT_GRAY8:
return HWCODEC_CAPTURE_FMT_GREY;
default:
return HWCODEC_CAPTURE_FMT_UNKNOWN;
}
}
int map_codec_to_capture_format(const AVCodecParameters* codecpar) {
if (!codecpar) {
return HWCODEC_CAPTURE_FMT_UNKNOWN;
}
switch (codecpar->codec_id) {
case AV_CODEC_ID_MJPEG:
return HWCODEC_CAPTURE_FMT_MJPEG;
case AV_CODEC_ID_JPEG2000:
return HWCODEC_CAPTURE_FMT_JPEG;
case AV_CODEC_ID_RAWVIDEO:
return map_raw_pixfmt(codecpar->format);
default:
return HWCODEC_CAPTURE_FMT_UNKNOWN;
}
}
int interrupt_callback(void* opaque) {
auto* ctx = reinterpret_cast<HwcodecDshowCaptureContext*>(opaque);
if (!ctx) {
return 0;
}
auto deadline = ctx->deadline_ms.load();
if (deadline <= 0) {
return 0;
}
if (now_ms() > deadline) {
ctx->timed_out.store(1);
return 1;
}
return 0;
}
const char* requested_pixel_format_name(int requested_format) {
switch (requested_format) {
case HWCODEC_CAPTURE_FMT_YUYV:
return "yuyv422";
case HWCODEC_CAPTURE_FMT_UYVY:
return "uyvy422";
case HWCODEC_CAPTURE_FMT_NV12:
return "nv12";
case HWCODEC_CAPTURE_FMT_NV21:
return "nv21";
case HWCODEC_CAPTURE_FMT_RGB24:
return "rgb24";
case HWCODEC_CAPTURE_FMT_BGR24:
return "bgr24";
case HWCODEC_CAPTURE_FMT_GREY:
return "gray";
default:
return nullptr;
}
}
} // namespace
extern "C" const char* hwcodec_capture_last_error(void) {
return g_last_error.c_str();
}
extern "C" char* hwcodec_dshow_list_video_devices(void) {
ScopedComInit com;
ICreateDevEnum* dev_enum = nullptr;
IEnumMoniker* enum_moniker = nullptr;
HRESULT hr = CoCreateInstance(
CLSID_SystemDeviceEnum,
nullptr,
CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum,
reinterpret_cast<void**>(&dev_enum));
if (FAILED(hr)) {
set_last_error("Failed to create DirectShow device enumerator");
return nullptr;
}
hr = dev_enum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enum_moniker, 0);
dev_enum->Release();
if (hr != S_OK || !enum_moniker) {
char* out = reinterpret_cast<char*>(std::malloc(1));
if (out) {
out[0] = '\0';
}
return out;
}
std::vector<std::string> devices;
IMoniker* moniker = nullptr;
ULONG fetched = 0;
while (enum_moniker->Next(1, &moniker, &fetched) == S_OK) {
IPropertyBag* bag = nullptr;
hr = moniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, reinterpret_cast<void**>(&bag));
if (SUCCEEDED(hr) && bag) {
VARIANT name;
VariantInit(&name);
if (SUCCEEDED(bag->Read(L"FriendlyName", &name, nullptr)) && name.vt == VT_BSTR) {
auto utf8_name = wide_to_utf8(name.bstrVal);
if (!utf8_name.empty()) {
devices.push_back(utf8_name);
}
}
VariantClear(&name);
bag->Release();
}
moniker->Release();
}
enum_moniker->Release();
std::string payload;
for (size_t i = 0; i < devices.size(); ++i) {
payload += devices[i];
if (i + 1 < devices.size()) {
payload.push_back('\n');
}
}
return copy_payload(payload);
}
extern "C" char* hwcodec_dshow_list_device_capabilities(const char* device_name) {
if (!device_name || device_name[0] == '\0') {
set_last_error("DirectShow device name is empty");
return nullptr;
}
ScopedComInit com;
IBaseFilter* filter = nullptr;
if (!find_device_filter(device_name, &filter) || !filter) {
set_last_error("Failed to find DirectShow device filter");
return nullptr;
}
std::vector<DshowCapabilityEntry> entries;
IEnumPins* enum_pins = nullptr;
HRESULT hr = filter->EnumPins(&enum_pins);
if (SUCCEEDED(hr) && enum_pins) {
IPin* pin = nullptr;
ULONG fetched = 0;
while (enum_pins->Next(1, &pin, &fetched) == S_OK) {
PIN_DIRECTION direction = PINDIR_INPUT;
if (SUCCEEDED(pin->QueryDirection(&direction)) && direction == PINDIR_OUTPUT) {
IAMStreamConfig* stream_config = nullptr;
if (SUCCEEDED(pin->QueryInterface(IID_IAMStreamConfig, reinterpret_cast<void**>(&stream_config))) &&
stream_config) {
append_stream_capabilities(stream_config, &entries);
stream_config->Release();
}
}
pin->Release();
}
enum_pins->Release();
}
filter->Release();
std::sort(entries.begin(), entries.end(), [](const DshowCapabilityEntry& left, const DshowCapabilityEntry& right) {
if (left.format != right.format) {
return left.format < right.format;
}
if (left.width != right.width) {
return left.width < right.width;
}
if (left.height != right.height) {
return left.height < right.height;
}
return left.fps > right.fps;
});
entries.erase(
std::unique(entries.begin(), entries.end(), [](const DshowCapabilityEntry& left, const DshowCapabilityEntry& right) {
return left.format == right.format && left.width == right.width && left.height == right.height && left.fps == right.fps;
}),
entries.end());
return copy_payload(build_capabilities_payload(entries));
}
extern "C" void hwcodec_capture_string_free(char* ptr) {
if (ptr) {
std::free(ptr);
}
}
extern "C" HwcodecDshowCaptureContext* hwcodec_dshow_capture_open(
const char* device_name,
int width,
int height,
int fps,
int requested_format,
int timeout_ms) {
if (!device_name || device_name[0] == '\0') {
set_last_error("Device name is empty");
return nullptr;
}
avdevice_register_all();
const AVInputFormat* input = av_find_input_format("dshow");
if (!input) {
set_last_error("FFmpeg dshow input format is unavailable");
return nullptr;
}
auto* ctx = new HwcodecDshowCaptureContext();
ctx->timeout_ms = timeout_ms > 0 ? timeout_ms : 2000;
ctx->format_ctx = avformat_alloc_context();
if (!ctx->format_ctx) {
delete ctx;
set_last_error("Failed to allocate FFmpeg format context");
return nullptr;
}
ctx->format_ctx->interrupt_callback.callback = interrupt_callback;
ctx->format_ctx->interrupt_callback.opaque = ctx;
std::string open_attempt;
int ret = open_dshow_input_with_options(
&ctx->format_ctx,
input,
device_name,
width,
height,
fps,
requested_format,
true,
true,
true,
&open_attempt);
if (ret < 0) {
avformat_free_context(ctx->format_ctx);
ctx->format_ctx = avformat_alloc_context();
if (!ctx->format_ctx) {
delete ctx;
set_last_error("Failed to allocate FFmpeg format context for fallback open");
return nullptr;
}
ctx->format_ctx->interrupt_callback.callback = interrupt_callback;
ctx->format_ctx->interrupt_callback.opaque = ctx;
std::string fallback_attempt;
ret = open_dshow_input_with_options(
&ctx->format_ctx,
input,
device_name,
width,
height,
fps,
requested_format,
true,
false,
true,
&fallback_attempt);
if (ret >= 0) {
open_attempt = fallback_attempt;
}
}
if (ret < 0) {
avformat_free_context(ctx->format_ctx);
ctx->format_ctx = avformat_alloc_context();
if (!ctx->format_ctx) {
delete ctx;
set_last_error("Failed to allocate FFmpeg format context for final fallback open");
return nullptr;
}
ctx->format_ctx->interrupt_callback.callback = interrupt_callback;
ctx->format_ctx->interrupt_callback.opaque = ctx;
std::string fallback_attempt;
ret = open_dshow_input_with_options(
&ctx->format_ctx,
input,
device_name,
width,
height,
fps,
requested_format,
false,
false,
false,
&fallback_attempt);
if (ret >= 0) {
open_attempt = fallback_attempt;
}
}
if (ret < 0) {
set_last_error("Failed to open dshow input (" + open_attempt + "): " + ffmpeg_error(ret));
avformat_free_context(ctx->format_ctx);
delete ctx;
return nullptr;
}
ret = avformat_find_stream_info(ctx->format_ctx, nullptr);
if (ret < 0) {
set_last_error("Failed to read stream info: " + ffmpeg_error(ret));
avformat_close_input(&ctx->format_ctx);
delete ctx;
return nullptr;
}
for (unsigned int i = 0; i < ctx->format_ctx->nb_streams; ++i) {
AVStream* stream = ctx->format_ctx->streams[i];
if (stream && stream->codecpar && stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
ctx->stream_index = static_cast<int>(i);
ctx->width = stream->codecpar->width > 0 ? stream->codecpar->width : width;
ctx->height = stream->codecpar->height > 0 ? stream->codecpar->height : height;
ctx->pixel_format = map_codec_to_capture_format(stream->codecpar);
ctx->stride = capture_stride(ctx->pixel_format, ctx->width);
break;
}
}
if (ctx->stream_index < 0) {
set_last_error("No video stream found on DirectShow device");
avformat_close_input(&ctx->format_ctx);
delete ctx;
return nullptr;
}
if (ctx->pixel_format == HWCODEC_CAPTURE_FMT_UNKNOWN) {
set_last_error("DirectShow stream format is unsupported in current Windows backend");
avformat_close_input(&ctx->format_ctx);
delete ctx;
return nullptr;
}
return ctx;
}
extern "C" int hwcodec_dshow_capture_info(
HwcodecDshowCaptureContext* ctx,
HwcodecCaptureStreamInfo* out_info) {
if (!ctx || !out_info) {
set_last_error("Invalid capture context");
return -1;
}
out_info->width = ctx->width;
out_info->height = ctx->height;
out_info->pixel_format = ctx->pixel_format;
out_info->stride = ctx->stride;
return 0;
}
extern "C" int hwcodec_dshow_capture_read(
HwcodecDshowCaptureContext* ctx,
uint8_t** out_data,
int* out_len,
uint64_t* out_sequence) {
if (!ctx || !out_data || !out_len || !out_sequence) {
set_last_error("Invalid capture read arguments");
return -1;
}
*out_data = nullptr;
*out_len = 0;
*out_sequence = 0;
AVPacket packet;
av_init_packet(&packet);
packet.data = nullptr;
packet.size = 0;
while (true) {
ctx->timed_out.store(0);
ctx->deadline_ms.store(now_ms() + ctx->timeout_ms);
int ret = av_read_frame(ctx->format_ctx, &packet);
ctx->deadline_ms.store(0);
if (ret < 0) {
if (ctx->timed_out.load() != 0) {
set_last_error("Timed out waiting for frame");
return -110;
}
set_last_error("Failed to read frame: " + ffmpeg_error(ret));
return ret;
}
if (packet.stream_index != ctx->stream_index) {
av_packet_unref(&packet);
continue;
}
if (packet.size <= 0 || !packet.data) {
av_packet_unref(&packet);
continue;
}
auto* buffer = reinterpret_cast<uint8_t*>(std::malloc(static_cast<size_t>(packet.size)));
if (!buffer) {
av_packet_unref(&packet);
set_last_error("Failed to allocate packet buffer");
return -12;
}
std::memcpy(buffer, packet.data, static_cast<size_t>(packet.size));
*out_data = buffer;
*out_len = packet.size;
*out_sequence = ctx->sequence++;
av_packet_unref(&packet);
return 0;
}
}
extern "C" void hwcodec_dshow_capture_packet_free(uint8_t* data) {
if (data) {
std::free(data);
}
}
extern "C" void hwcodec_dshow_capture_close(HwcodecDshowCaptureContext* ctx) {
if (!ctx) {
return;
}
if (ctx->format_ctx) {
avformat_close_input(&ctx->format_ctx);
}
delete ctx;
}

View File

@@ -0,0 +1,64 @@
#ifndef HWCODEC_FFMPEG_CAPTURE_FFI_H
#define HWCODEC_FFMPEG_CAPTURE_FFI_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct HwcodecDshowCaptureContext HwcodecDshowCaptureContext;
enum HwcodecCapturePixelFormat {
HWCODEC_CAPTURE_FMT_UNKNOWN = 0,
HWCODEC_CAPTURE_FMT_MJPEG = 1,
HWCODEC_CAPTURE_FMT_JPEG = 2,
HWCODEC_CAPTURE_FMT_YUYV = 3,
HWCODEC_CAPTURE_FMT_YVYU = 4,
HWCODEC_CAPTURE_FMT_UYVY = 5,
HWCODEC_CAPTURE_FMT_NV12 = 6,
HWCODEC_CAPTURE_FMT_NV21 = 7,
HWCODEC_CAPTURE_FMT_NV16 = 8,
HWCODEC_CAPTURE_FMT_NV24 = 9,
HWCODEC_CAPTURE_FMT_YUV420 = 10,
HWCODEC_CAPTURE_FMT_YVU420 = 11,
HWCODEC_CAPTURE_FMT_RGB24 = 12,
HWCODEC_CAPTURE_FMT_BGR24 = 13,
HWCODEC_CAPTURE_FMT_GREY = 14,
};
typedef struct HwcodecCaptureStreamInfo {
int width;
int height;
int pixel_format;
int stride;
} HwcodecCaptureStreamInfo;
const char* hwcodec_capture_last_error(void);
char* hwcodec_dshow_list_video_devices(void);
char* hwcodec_dshow_list_device_capabilities(const char* device_name);
void hwcodec_capture_string_free(char* ptr);
HwcodecDshowCaptureContext* hwcodec_dshow_capture_open(
const char* device_name,
int width,
int height,
int fps,
int requested_format,
int timeout_ms);
int hwcodec_dshow_capture_info(
HwcodecDshowCaptureContext* ctx,
HwcodecCaptureStreamInfo* out_info);
int hwcodec_dshow_capture_read(
HwcodecDshowCaptureContext* ctx,
uint8_t** out_data,
int* out_len,
uint64_t* out_sequence);
void hwcodec_dshow_capture_packet_free(uint8_t* data);
void hwcodec_dshow_capture_close(HwcodecDshowCaptureContext* ctx);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -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() {}

View File

@@ -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,13 @@ 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;
LOG_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) {
@@ -30,9 +38,15 @@ static int calculate_offset_length(int pix_fmt, int height, const int *linesize,
*length = offset[1] + linesize[2] * height / 2; *length = offset[1] + linesize[2] * height / 2;
break; break;
case AV_PIX_FMT_NV12: case AV_PIX_FMT_NV12:
case AV_PIX_FMT_NV21:
offset[0] = linesize[0] * height; offset[0] = linesize[0] * height;
*length = offset[0] + linesize[1] * height / 2; *length = offset[0] + linesize[1] * height / 2;
break; break;
case AV_PIX_FMT_NV16:
case AV_PIX_FMT_NV24:
offset[0] = linesize[0] * height;
*length = offset[0] + linesize[1] * height;
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:
@@ -41,6 +55,11 @@ static int calculate_offset_length(int pix_fmt, int height, const int *linesize,
offset[0] = 0; // Only one plane offset[0] = 0; // Only one plane
*length = linesize[0] * height; *length = linesize[0] * height;
break; break;
case AV_PIX_FMT_RGB24:
case AV_PIX_FMT_BGR24:
offset[0] = 0; // Only one plane
*length = linesize[0] * height;
break;
default: default:
LOG_ERROR(std::string("unsupported pixfmt") + std::to_string(pix_fmt)); LOG_ERROR(std::string("unsupported pixfmt") + std::to_string(pix_fmt));
return -1; return -1;
@@ -101,6 +120,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:
@@ -108,7 +130,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;
@@ -123,6 +144,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
@@ -131,12 +153,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;
@@ -168,12 +189,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;
} }
@@ -236,7 +258,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_;
@@ -252,23 +273,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;
} }
@@ -290,7 +304,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) {
@@ -306,6 +320,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_);
@@ -365,76 +387,203 @@ private:
frame->pict_type = AV_PICTURE_TYPE_NONE; frame->pict_type = AV_PICTURE_TYPE_NONE;
} }
if ((ret = avcodec_send_frame(c_, frame)) < 0) { ret = avcodec_send_frame(c_, frame);
if (ret == AVERROR(EAGAIN)) {
int drain_ret = receive_available_packets(obj, encoded);
if (drain_ret < 0) {
return drain_ret;
}
ret = avcodec_send_frame(c_, frame);
}
if (ret == AVERROR(EAGAIN)) {
return encoded ? 0 : AVERROR(EAGAIN);
}
if (ret < 0) {
LOG_ERROR(std::string("avcodec_send_frame failed, ret = ") + av_err2str(ret)); LOG_ERROR(std::string("avcodec_send_frame failed, ret = ") + av_err2str(ret));
return ret; return ret;
} }
auto start = util::now(); ret = receive_available_packets(obj, encoded);
while (ret >= 0 && util::elapsed_ms(start) < DECODE_TIMEOUT_MS) { if (ret < 0) {
if ((ret = avcodec_receive_packet(c_, pkt_)) < 0) { return ret;
if (ret != AVERROR(EAGAIN)) {
LOG_ERROR(std::string("avcodec_receive_packet failed, ret = ") + av_err2str(ret));
} }
goto _exit;
}
if (!pkt_->data || !pkt_->size) {
LOG_ERROR(std::string("avcodec_receive_packet failed, pkt size is 0"));
goto _exit;
}
encoded = true;
callback_(pkt_->data, pkt_->size, pkt_->pts,
pkt_->flags & AV_PKT_FLAG_KEY, obj);
}
_exit:
av_packet_unref(pkt_);
// If no packet is produced for this input frame, treat it as EAGAIN. // If no packet is produced for this input frame, treat it as EAGAIN.
// This is not a fatal error: encoders may buffer internally (e.g., startup delay). // This is not a fatal error: encoders may buffer internally (e.g., startup delay).
return encoded ? 0 : AVERROR(EAGAIN); return encoded ? 0 : AVERROR(EAGAIN);
} }
int fill_frame(AVFrame *frame, uint8_t *data, int data_length, int receive_available_packets(const void *obj, bool &encoded) {
const int *const offset) { int ret = 0;
auto start = util::now();
while (util::elapsed_ms(start) < DECODE_TIMEOUT_MS) {
ret = avcodec_receive_packet(c_, pkt_);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
}
if (ret < 0) {
LOG_ERROR(std::string("avcodec_receive_packet failed, ret = ") + av_err2str(ret));
av_packet_unref(pkt_);
return ret;
}
if (!pkt_->data || !pkt_->size) {
LOG_WARN(std::string("avcodec_receive_packet returned empty packet"));
av_packet_unref(pkt_);
continue;
}
encoded = true;
if (packet_callback_) {
AVPacket *owned_pkt = av_packet_clone(pkt_);
if (!owned_pkt) {
LOG_ERROR("av_packet_clone failed");
av_packet_unref(pkt_);
return AVERROR(ENOMEM);
}
packet_callback_(owned_pkt, owned_pkt->data, owned_pkt->size,
owned_pkt->pts, owned_pkt->flags & AV_PKT_FLAG_KEY,
obj);
} else {
callback_(pkt_->data, pkt_->size, pkt_->pts,
pkt_->flags & AV_PKT_FLAG_KEY, obj);
}
av_packet_unref(pkt_);
}
return 0;
}
int copy_plane(uint8_t *dst, int dst_stride, const uint8_t *src,
int src_stride, int row_bytes, int rows) {
if (!dst || !src || dst_stride < row_bytes || src_stride < row_bytes) {
return -1;
}
if (rows <= 0 || row_bytes <= 0) {
return 0;
}
if (dst_stride == row_bytes && src_stride == row_bytes) {
memcpy(dst, src, static_cast<size_t>(row_bytes) * rows);
return 0;
}
for (int y = 0; y < rows; y++) {
memcpy(dst + y * dst_stride, src + y * src_stride, row_bytes);
}
return 0;
}
int fill_frame(AVFrame *frame, const uint8_t *data, int data_length,
const int *const) {
const int src_y_stride = width_;
const int src_packed_stride = width_ * bytes_per_pixel(frame->format);
const int src_uv_stride = width_;
const int src_y_size = width_ * frame->height;
const int src_420_chroma_size = (width_ / 2) * (frame->height / 2);
switch (frame->format) { switch (frame->format) {
case AV_PIX_FMT_NV12: case AV_PIX_FMT_NV12:
case AV_PIX_FMT_NV21:
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 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:
if (data_length <
frame->height * src_y_stride + frame->height * src_uv_stride) {
LOG_ERROR(std::string("fill_frame: NV16 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_uv_stride, width_, frame->height) != 0) {
LOG_ERROR("fill_frame: NV16 copy failed");
return -1;
}
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;
case AV_PIX_FMT_RGB24:
case AV_PIX_FMT_BGR24:
if (data_length < frame->height * src_packed_stride) {
LOG_ERROR(std::string("fill_frame: RGB24/BGR24 data length error. data_length:") +
std::to_string(data_length) +
", stride:" + std::to_string(src_packed_stride) +
", height:" + std::to_string(frame->height));
return -1;
}
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, ") +
@@ -443,23 +592,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_WARN(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;
} }
} }
@@ -496,6 +686,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);
@@ -514,3 +723,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();
}

View File

@@ -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,

297
libs/hwcodec/src/capture.rs Normal file
View File

@@ -0,0 +1,297 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use std::ffi::{CStr, CString};
use std::os::raw::c_int;
include!(concat!(env!("OUT_DIR"), "/ffmpeg_capture_ffi.rs"));
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapturePixelFormat {
Unknown,
Mjpeg,
Jpeg,
Yuyv,
Yvyu,
Uyvy,
Nv12,
Nv21,
Nv16,
Nv24,
Yuv420,
Yvu420,
Rgb24,
Bgr24,
Grey,
}
impl CapturePixelFormat {
pub fn to_ffi(self) -> c_int {
match self {
Self::Unknown => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UNKNOWN as c_int,
Self::Mjpeg => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_MJPEG as c_int,
Self::Jpeg => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_JPEG as c_int,
Self::Yuyv => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUYV as c_int,
Self::Yvyu => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVYU as c_int,
Self::Uyvy => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UYVY as c_int,
Self::Nv12 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV12 as c_int,
Self::Nv21 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV21 as c_int,
Self::Nv16 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV16 as c_int,
Self::Nv24 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV24 as c_int,
Self::Yuv420 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUV420 as c_int,
Self::Yvu420 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVU420 as c_int,
Self::Rgb24 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_RGB24 as c_int,
Self::Bgr24 => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_BGR24 as c_int,
Self::Grey => HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_GREY as c_int,
}
}
pub fn from_ffi(value: c_int) -> Self {
match value {
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_MJPEG as c_int => Self::Mjpeg,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_JPEG as c_int => Self::Jpeg,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUYV as c_int => Self::Yuyv,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVYU as c_int => Self::Yvyu,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UYVY as c_int => Self::Uyvy,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV12 as c_int => Self::Nv12,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV21 as c_int => Self::Nv21,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV16 as c_int => Self::Nv16,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_NV24 as c_int => Self::Nv24,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YUV420 as c_int => {
Self::Yuv420
}
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_YVU420 as c_int => {
Self::Yvu420
}
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_RGB24 as c_int => Self::Rgb24,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_BGR24 as c_int => Self::Bgr24,
x if x == HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_GREY as c_int => Self::Grey,
_ => Self::Unknown,
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name.trim().to_ascii_uppercase().as_str() {
"MJPEG" | "MJPG" => Some(Self::Mjpeg),
"JPEG" => Some(Self::Jpeg),
"YUYV" => Some(Self::Yuyv),
"YVYU" => Some(Self::Yvyu),
"UYVY" => Some(Self::Uyvy),
"NV12" => Some(Self::Nv12),
"NV21" => Some(Self::Nv21),
"NV16" => Some(Self::Nv16),
"NV24" => Some(Self::Nv24),
"YUV420" | "I420" | "IYUV" => Some(Self::Yuv420),
"YVU420" | "YV12" => Some(Self::Yvu420),
"RGB24" => Some(Self::Rgb24),
"BGR24" => Some(Self::Bgr24),
"GREY" | "GRAY" | "Y800" => Some(Self::Grey),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct DshowCapability {
pub format: CapturePixelFormat,
pub width: u32,
pub height: u32,
pub fps: Vec<u32>,
}
#[derive(Debug, Clone, Copy)]
pub struct CaptureStreamInfo {
pub width: i32,
pub height: i32,
pub pixel_format: CapturePixelFormat,
pub stride: i32,
}
#[derive(Debug)]
pub struct CaptureError {
pub code: i32,
pub message: String,
}
impl std::fmt::Display for CaptureError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for CaptureError {}
fn last_error_message() -> String {
unsafe {
let ptr = hwcodec_capture_last_error();
if ptr.is_null() {
return String::new();
}
CStr::from_ptr(ptr).to_string_lossy().to_string()
}
}
pub fn list_dshow_video_devices() -> Result<Vec<String>, CaptureError> {
unsafe {
let ptr = hwcodec_dshow_list_video_devices();
if ptr.is_null() {
return Err(CaptureError {
code: -1,
message: last_error_message(),
});
}
let payload = CStr::from_ptr(ptr).to_string_lossy().to_string();
hwcodec_capture_string_free(ptr as *mut _);
Ok(payload
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect())
}
}
pub fn list_dshow_device_capabilities(device_name: &str) -> Result<Vec<DshowCapability>, CaptureError> {
let device_name = CString::new(device_name).map_err(|_| CaptureError {
code: -1,
message: "device name contains NUL byte".to_string(),
})?;
unsafe {
let ptr = hwcodec_dshow_list_device_capabilities(device_name.as_ptr());
if ptr.is_null() {
return Err(CaptureError {
code: -1,
message: last_error_message(),
});
}
let payload = CStr::from_ptr(ptr).to_string_lossy().to_string();
hwcodec_capture_string_free(ptr as *mut _);
let capabilities = payload
.lines()
.filter_map(parse_dshow_capability_line)
.collect();
Ok(capabilities)
}
}
fn parse_dshow_capability_line(line: &str) -> Option<DshowCapability> {
let mut parts = line.split('|');
let format = CapturePixelFormat::from_name(parts.next()?.trim())?;
let width = parts.next()?.trim().parse::<u32>().ok()?;
let height = parts.next()?.trim().parse::<u32>().ok()?;
let fps = parts
.next()
.unwrap_or_default()
.split(',')
.filter_map(|value| value.trim().parse::<u32>().ok())
.filter(|value| *value > 0)
.collect::<Vec<_>>();
Some(DshowCapability {
format,
width,
height,
fps,
})
}
pub struct DshowCapture {
ctx: *mut HwcodecDshowCaptureContext,
}
unsafe impl Send for DshowCapture {}
impl DshowCapture {
pub fn open(
device_name: &str,
width: i32,
height: i32,
fps: i32,
requested_format: CapturePixelFormat,
timeout_ms: i32,
) -> Result<Self, CaptureError> {
let device_name = CString::new(device_name).map_err(|_| CaptureError {
code: -1,
message: "device name contains NUL byte".to_string(),
})?;
unsafe {
let ctx = hwcodec_dshow_capture_open(
device_name.as_ptr(),
width,
height,
fps,
requested_format.to_ffi(),
timeout_ms,
);
if ctx.is_null() {
return Err(CaptureError {
code: -1,
message: last_error_message(),
});
}
Ok(Self { ctx })
}
}
pub fn info(&self) -> Result<CaptureStreamInfo, CaptureError> {
unsafe {
let mut info = HwcodecCaptureStreamInfo {
width: 0,
height: 0,
pixel_format: HwcodecCapturePixelFormat::HWCODEC_CAPTURE_FMT_UNKNOWN as c_int,
stride: 0,
};
let ret = hwcodec_dshow_capture_info(self.ctx, &mut info);
if ret != 0 {
return Err(CaptureError {
code: ret,
message: last_error_message(),
});
}
Ok(CaptureStreamInfo {
width: info.width,
height: info.height,
pixel_format: CapturePixelFormat::from_ffi(info.pixel_format),
stride: info.stride,
})
}
}
pub fn read_packet(&mut self) -> Result<(Vec<u8>, u64), CaptureError> {
unsafe {
let mut data = std::ptr::null_mut();
let mut len = 0;
let mut sequence = 0u64;
let ret = hwcodec_dshow_capture_read(self.ctx, &mut data, &mut len, &mut sequence);
if ret != 0 {
return Err(CaptureError {
code: ret,
message: last_error_message(),
});
}
if data.is_null() || len <= 0 {
return Err(CaptureError {
code: -1,
message: "empty packet returned by capture backend".to_string(),
});
}
let slice = std::slice::from_raw_parts(data, len as usize);
let vec = slice.to_vec();
hwcodec_dshow_capture_packet_free(data);
Ok((vec, sequence))
}
}
}
impl Drop for DshowCapture {
fn drop(&mut self) {
unsafe {
hwcodec_dshow_capture_close(self.ctx);
}
self.ctx = std::ptr::null_mut();
}
}

View File

@@ -6,7 +6,7 @@
include!(concat!(env!("OUT_DIR"), "/ffmpeg_ffi.rs")); include!(concat!(env!("OUT_DIR"), "/ffmpeg_ffi.rs"));
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::env; use std::{env, ffi::CString};
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum AVHWDeviceType { pub enum AVHWDeviceType {
@@ -59,6 +59,22 @@ pub(crate) fn init_av_log() {
}); });
} }
pub fn resolve_pixel_format(name: &str, fallback: AVPixelFormat) -> i32 {
let c_name = match CString::new(name) {
Ok(name) => name,
Err(_) => return fallback as i32,
};
unsafe {
let resolved = av_get_pix_fmt(c_name.as_ptr());
if resolved >= 0 {
resolved
} else {
fallback as i32
}
}
}
fn parse_ffmpeg_log_level() -> i32 { fn parse_ffmpeg_log_level() -> i32 {
let raw = match env::var("ONE_KVM_FFMPEG_LOG") { let raw = match env::var("ONE_KVM_FFMPEG_LOG") {
Ok(value) => value, Ok(value) => value,

View File

@@ -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() {

View File

@@ -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.
@@ -243,7 +248,7 @@ fn enumerate_candidate_codecs(ctx: &EncodeContext) -> Vec<CodecInfo> {
} }
codecs.retain(|codec| { codecs.retain(|codec| {
!(ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_YUV420P && codec.name.contains("qsv")) !(ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_YUV420P as i32 && codec.name.contains("qsv"))
}); });
codecs codecs
} }
@@ -257,7 +262,13 @@ struct ProbePolicy {
impl ProbePolicy { impl ProbePolicy {
fn for_codec(codec_name: &str) -> Self { fn for_codec(codec_name: &str) -> Self {
if codec_name.contains("v4l2m2m") { if codec_name.contains("amf") {
Self {
max_attempts: 5,
request_keyframe: true,
accept_any_output: true,
}
} else if codec_name.contains("v4l2m2m") {
Self { Self {
max_attempts: 5, max_attempts: 5,
request_keyframe: true, request_keyframe: true,
@@ -298,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!(
@@ -331,13 +343,12 @@ fn log_failed_probe_attempt(
} }
fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> bool { fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> bool {
use log::debug; use log::{debug, warn};
debug!("Testing encoder: {}", codec.name); debug!("Testing encoder: {}", codec.name);
let test_ctx = EncodeContext { let test_ctx = EncodeContext {
name: codec.name.clone(), name: codec.name.clone(),
mc_name: codec.mc_name.clone(),
..ctx.clone() ..ctx.clone()
}; };
@@ -384,7 +395,7 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo
} }
Err(err) => { Err(err) => {
last_err = Some(err); last_err = Some(err);
debug!( warn!(
"Encoder {} test attempt {} returned error: {}", "Encoder {} test attempt {} returned error: {}",
codec.name, attempt_no, err codec.name, attempt_no, err
); );
@@ -402,7 +413,7 @@ fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> boo
false false
} }
Err(_) => { Err(_) => {
debug!("Failed to create encoder {}", codec.name); warn!("Failed to create encoder {}", codec.name);
false false
} }
} }
@@ -425,10 +436,9 @@ 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: AVPixelFormat, pub pixfmt: i32,
pub align: i32, pub align: i32,
pub fps: i32, pub fps: i32,
pub gop: i32, pub gop: i32,
@@ -445,6 +455,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)
@@ -477,13 +520,11 @@ 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 as c_int, ctx.pixfmt,
ctx.align, ctx.align,
ctx.fps, ctx.fps,
ctx.gop, ctx.gop,
@@ -500,6 +541,10 @@ impl Encoder {
); );
if codec.is_null() { if codec.is_null() {
let message = encoder_last_error_message();
if !message.is_empty() {
log::error!("ffmpeg_ram_new_encoder failed: {}", message);
}
return Err(()); return Err(());
} }
@@ -537,6 +582,25 @@ impl Encoder {
} }
} }
#[cfg(feature = "bytes")]
pub fn encode_bytes(&mut self, data: &[u8], ms: i64) -> Result<Vec<EncodeBytesFrame>, i32> {
unsafe {
let mut frames = Vec::<EncodeBytesFrame>::new();
let result = ffmpeg_ram_encode_packet(
self.codec,
data.as_ptr(),
data.len() as _,
&mut frames as *mut _ as *const c_void,
ms,
Some(Encoder::packet_callback),
);
if result == -11 || result == 0 {
return Ok(frames);
}
Err(result)
}
}
extern "C" fn callback(data: *const u8, size: c_int, pts: i64, key: i32, obj: *const c_void) { extern "C" fn callback(data: *const u8, size: c_int, pts: i64, key: i32, obj: *const c_void) {
unsafe { unsafe {
let frames = &mut *(obj as *mut Vec<EncodeFrame>); let frames = &mut *(obj as *mut Vec<EncodeFrame>);
@@ -548,6 +612,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 {
@@ -620,6 +708,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 {

View File

@@ -3,10 +3,7 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use crate::common::DataFormat::{self, *}; use crate::common::DataFormat::{self, *};
use crate::ffmpeg::{ use crate::ffmpeg::AVHWDeviceType::{self, *};
AVHWDeviceType::{self, *},
AVPixelFormat,
};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::ffi::c_int; use std::ffi::c_int;
@@ -66,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,
@@ -77,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,
@@ -90,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 _,
@@ -234,7 +224,7 @@ impl CodecInfos {
} }
pub fn ffmpeg_linesize_offset_length( pub fn ffmpeg_linesize_offset_length(
pixfmt: AVPixelFormat, pixfmt: i32,
width: usize, width: usize,
height: usize, height: usize,
align: usize, align: usize,
@@ -247,7 +237,7 @@ pub fn ffmpeg_linesize_offset_length(
length.resize(1, 0); length.resize(1, 0);
unsafe { unsafe {
if ffmpeg_ram_get_linesize_offset_length( if ffmpeg_ram_get_linesize_offset_length(
pixfmt as _, pixfmt,
width as _, width as _,
height as _, height as _,
align as _, align as _,

View File

@@ -1,3 +1,5 @@
#[cfg(windows)]
pub mod capture;
pub mod common; pub mod common;
pub mod ffmpeg; pub mod ffmpeg;
#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))] #[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))]

1
libs/v4l2r/.cargo-ok Normal file
View File

@@ -0,0 +1 @@
{"v":1}

View File

@@ -0,0 +1,6 @@
{
"git": {
"sha1": "7b441383125ae583017a1c18b3fc9ec6c88ddbe8"
},
"path_in_vcs": "lib"
}

65
libs/v4l2r/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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__ */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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),
)
}

View 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
}
}
}
}

View 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) })
}

View 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)),
}
}

View 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)),
}
}

View 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)),
}
}

View 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))
);
}
}

View 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)),
}
}

View 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)),
}
}

View 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,
})
}

View 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),
)
}

View 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),
)
}

View 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)),
}
}

View 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)),
}
}

View 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)),
}
}

View 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
View 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
View 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
);
}
}

View 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)
}
}

View 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()
}
}

View 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;
}
}

View File

@@ -0,0 +1,4 @@
#include <linux/videodev2.h>
#define MARK_FIX_753(name) const unsigned long int Fix753_##name = name;
#include "fix753.h"

View File

@@ -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]

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -45,4 +45,4 @@ pub use error::{Result, VentoyError};
pub use exfat::FileInfo; pub use exfat::FileInfo;
pub use image::VentoyImage; pub use image::VentoyImage;
pub use partition::{parse_size, PartitionLayout}; pub use partition::{parse_size, PartitionLayout};
pub use resources::{get_resource_dir, init_resources, is_initialized, required_files}; pub use resources::{init_resources, is_initialized, required_files};

View File

@@ -5,7 +5,7 @@
use crate::error::{Result, VentoyError}; use crate::error::{Result, VentoyError};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::sync::OnceLock; use std::sync::OnceLock;
/// Resource file names /// Resource file names
@@ -151,13 +151,6 @@ pub fn get_ventoy_disk_img() -> Result<&'static [u8]> {
}) })
} }
/// Get the resource directory path for a given data directory
///
/// Returns `{data_dir}/ventoy`
pub fn get_resource_dir(data_dir: &Path) -> PathBuf {
data_dir.join("ventoy")
}
/// List required resource files /// List required resource files
pub fn required_files() -> &'static [&'static str] { pub fn required_files() -> &'static [&'static str] {
&[BOOT_IMG_NAME, CORE_IMG_NAME, VENTOY_DISK_IMG_NAME] &[BOOT_IMG_NAME, CORE_IMG_NAME, VENTOY_DISK_IMG_NAME]
@@ -166,22 +159,6 @@ pub fn required_files() -> &'static [&'static str] {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::io::Write;
use tempfile::TempDir;
fn create_test_resources(dir: &Path) {
// Create boot.img (512 bytes)
let mut boot = std::fs::File::create(dir.join(BOOT_IMG_NAME)).unwrap();
boot.write_all(&[0u8; 512]).unwrap();
// Create core.img (fake, 1KB)
let mut core = std::fs::File::create(dir.join(CORE_IMG_NAME)).unwrap();
core.write_all(&[0u8; 1024]).unwrap();
// Create ventoy.disk.img (fake, 1KB)
let mut ventoy = std::fs::File::create(dir.join(VENTOY_DISK_IMG_NAME)).unwrap();
ventoy.write_all(&[0u8; 1024]).unwrap();
}
#[test] #[test]
fn test_required_files() { fn test_required_files() {
@@ -191,11 +168,4 @@ mod tests {
assert!(files.contains(&"core.img")); assert!(files.contains(&"core.img"));
assert!(files.contains(&"ventoy.disk.img")); assert!(files.contains(&"ventoy.disk.img"));
} }
#[test]
fn test_get_resource_dir() {
let data_dir = Path::new("/var/lib/one-kvm");
let resource_dir = get_resource_dir(data_dir);
assert_eq!(resource_dir, PathBuf::from("/var/lib/one-kvm/ventoy"));
}
} }

View File

@@ -8,5 +8,4 @@ license = "BSD-3-Clause"
[dependencies] [dependencies]
[build-dependencies] [build-dependencies]
cc = "1.0" bindgen = "0.72"
bindgen = "0.59"

View File

@@ -19,9 +19,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")
@@ -34,10 +34,13 @@ fn generate_bindings(cpp_dir: &Path) {
.allowlist_function("I420Copy") .allowlist_function("I420Copy")
// I422 conversions // I422 conversions
.allowlist_function("I422ToI420") .allowlist_function("I422ToI420")
.allowlist_function("I444ToI420")
// 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")
// ARGB/BGRA conversions // ARGB/BGRA conversions
.allowlist_function("ARGBToI420") .allowlist_function("ARGBToI420")
.allowlist_function("ARGBToNV12") .allowlist_function("ARGBToNV12")
@@ -47,18 +50,23 @@ 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")
// YUV to RGB conversions // YUV to RGB conversions
.allowlist_function("I420ToRGB24") .allowlist_function("I420ToRGB24")
.allowlist_function("I420ToARGB") .allowlist_function("I420ToARGB")
.allowlist_function("H444ToARGB")
.allowlist_function("NV12ToRGB24") .allowlist_function("NV12ToRGB24")
.allowlist_function("NV12ToARGB") .allowlist_function("NV12ToARGB")
.allowlist_function("YUY2ToARGB") .allowlist_function("YUY2ToARGB")
.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")
@@ -78,9 +86,24 @@ 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 Ok(vcpkg_root) = env::var("VCPKG_ROOT") { if let Some(vcpkg_installed) = vcpkg_installed_root() {
if link_vcpkg(vcpkg_root.into()) { if link_vcpkg(vcpkg_installed) {
return; return;
} }
} }
@@ -106,6 +129,22 @@ fn link_libyuv() {
); );
} }
fn vcpkg_installed_root() -> Option<PathBuf> {
println!("cargo:rerun-if-env-changed=VCPKG_INSTALLED_DIR");
println!("cargo:rerun-if-env-changed=VCPKG_ROOT");
if let Ok(path) = env::var("VCPKG_INSTALLED_DIR") {
if !path.trim().is_empty() {
return Some(PathBuf::from(path));
}
}
env::var("VCPKG_ROOT")
.ok()
.filter(|path| !path.trim().is_empty())
.map(|path| PathBuf::from(path).join("installed"))
}
fn link_vcpkg(mut path: PathBuf) -> bool { fn link_vcpkg(mut path: PathBuf) -> bool {
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
@@ -127,7 +166,6 @@ fn link_vcpkg(mut path: PathBuf) -> bool {
} }
}; };
path.push("installed");
path.push(triplet); path.push(triplet);
let include_path = path.join("include"); let include_path = path.join("include");
@@ -151,12 +189,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");
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");
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)");
} }
@@ -204,9 +247,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",
@@ -214,7 +261,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();
@@ -224,7 +271,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
@@ -238,6 +286,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);
@@ -248,6 +297,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)",
@@ -274,3 +324,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
}

View File

@@ -58,6 +58,15 @@ int I422ToI420(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);
// I444 (YUV444P) -> I420 (YUV420P) with horizontal and vertical chroma downsampling
int I444ToI420(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_u, int src_stride_u,
const uint8_t* src_v, int src_stride_v,
uint8_t* dst_y, int dst_stride_y,
uint8_t* dst_u, int dst_stride_u,
uint8_t* dst_v, int dst_stride_v,
int width, int height);
// I420 -> NV12 // I420 -> NV12
int I420ToNV12(const uint8_t* src_y, int src_stride_y, int I420ToNV12(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_u, int src_stride_u, const uint8_t* src_u, int src_stride_u,
@@ -94,6 +103,19 @@ 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
void SplitUVPlane(const uint8_t* src_uv, int src_stride_uv,
uint8_t* dst_u, int dst_stride_u,
uint8_t* dst_v, int dst_stride_v,
int width, int height);
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// ARGB/BGRA conversions (32-bit RGB) // ARGB/BGRA conversions (32-bit RGB)
// Note: libyuv uses ARGB to mean BGRA in memory (little-endian) // Note: libyuv uses ARGB to mean BGRA in memory (little-endian)
@@ -152,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,
@@ -180,6 +208,13 @@ int I420ToARGB(const uint8_t* src_y, int src_stride_y,
uint8_t* dst_argb, int dst_stride_argb, uint8_t* dst_argb, int dst_stride_argb,
int width, int height); int width, int height);
// H444 (BT.709 limited-range YUV444P) -> ARGB (BGRA)
int H444ToARGB(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_u, int src_stride_u,
const uint8_t* src_v, int src_stride_v,
uint8_t* dst_argb, int dst_stride_argb,
int width, int height);
// NV12 -> RGB24 // NV12 -> RGB24
int NV12ToRGB24(const uint8_t* src_y, int src_stride_y, int NV12ToRGB24(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_uv, int src_stride_uv, const uint8_t* src_uv, int src_stride_uv,
@@ -231,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);

View File

@@ -32,17 +32,9 @@ use std::fmt;
// Include auto-generated FFI bindings // Include auto-generated FFI bindings
include!(concat!(env!("OUT_DIR"), "/yuv_ffi.rs")); include!(concat!(env!("OUT_DIR"), "/yuv_ffi.rs"));
// Type alias for C's size_t - adapts to platform pointer width
#[cfg(target_pointer_width = "32")]
type SizeT = u32;
#[cfg(target_pointer_width = "64")]
type SizeT = u64;
// Helper function to convert usize to C's size_t type
#[inline] #[inline]
fn usize_to_size_t(val: usize) -> SizeT { fn usize_to_size_t(val: usize) -> usize {
val as SizeT val
} }
// ============================================================================ // ============================================================================
@@ -297,6 +289,93 @@ pub fn i422_to_i420_planar(
)) ))
} }
/// Convert I444 (YUV444P) to I420 (YUV420P) with separate planes and explicit strides
/// This performs horizontal and vertical chroma downsampling using SIMD
pub fn i444_to_i420_planar(
src_y: &[u8],
src_y_stride: i32,
src_u: &[u8],
src_u_stride: i32,
src_v: &[u8],
src_v_stride: i32,
dst: &mut [u8],
width: i32,
height: i32,
) -> Result<()> {
if width % 2 != 0 || height % 2 != 0 {
return Err(YuvError::InvalidDimensions);
}
let w = width as usize;
let h = height as usize;
let y_size = w * h;
let uv_size = (w / 2) * (h / 2);
if dst.len() < i420_size(w, h) {
return Err(YuvError::BufferTooSmall);
}
call_yuv!(I444ToI420(
src_y.as_ptr(),
src_y_stride,
src_u.as_ptr(),
src_u_stride,
src_v.as_ptr(),
src_v_stride,
dst.as_mut_ptr(),
width,
dst[y_size..].as_mut_ptr(),
width / 2,
dst[y_size + uv_size..].as_mut_ptr(),
width / 2,
width,
height,
))
}
/// Split an interleaved UV plane into separate U and V planes using libyuv SIMD helpers.
///
/// `width` is the number of chroma samples per row, not the number of source bytes.
pub fn split_uv_plane(
src_uv: &[u8],
src_stride_uv: i32,
dst_u: &mut [u8],
dst_stride_u: i32,
dst_v: &mut [u8],
dst_stride_v: i32,
width: i32,
height: i32,
) -> Result<()> {
if width <= 0 || height <= 0 {
return Err(YuvError::InvalidDimensions);
}
let width = width as usize;
let height = height as usize;
let src_required = (src_stride_uv as usize).saturating_mul(height);
let dst_u_required = (dst_stride_u as usize).saturating_mul(height);
let dst_v_required = (dst_stride_v as usize).saturating_mul(height);
if src_uv.len() < src_required || dst_u.len() < dst_u_required || dst_v.len() < dst_v_required {
return Err(YuvError::BufferTooSmall);
}
unsafe {
SplitUVPlane(
src_uv.as_ptr(),
src_stride_uv,
dst_u.as_mut_ptr(),
dst_stride_u,
dst_v.as_mut_ptr(),
dst_stride_v,
width as i32,
height as i32,
);
}
Ok(())
}
// ============================================================================ // ============================================================================
// I420 <-> NV12 conversions // I420 <-> NV12 conversions
// ============================================================================ // ============================================================================
@@ -435,6 +514,34 @@ pub fn nv21_to_i420(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Resu
)) ))
} }
/// Convert NV21 to NV12 by swapping interleaved chroma bytes.
pub fn nv21_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> {
if width % 2 != 0 || height % 2 != 0 {
return Err(YuvError::InvalidDimensions);
}
let w = width as usize;
let h = height as usize;
let y_size = w * h;
if src.len() < nv12_size(w, h) || dst.len() < nv12_size(w, h) {
return Err(YuvError::BufferTooSmall);
}
call_yuv!(NV21ToNV12(
src.as_ptr(),
width,
src[y_size..].as_ptr(),
width,
dst.as_mut_ptr(),
width,
dst[y_size..].as_mut_ptr(),
width,
width,
height,
))
}
// ============================================================================ // ============================================================================
// ARGB/BGRA conversions (32-bit) // ARGB/BGRA conversions (32-bit)
// Note: libyuv ARGB = BGRA in memory on little-endian systems // Note: libyuv ARGB = BGRA in memory on little-endian systems
@@ -761,6 +868,41 @@ pub fn i420_to_bgra(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Resu
)) ))
} }
/// Convert H444 (BT.709 limited-range YUV444P) to BGRA.
pub fn h444_to_bgra(
src_y: &[u8],
src_u: &[u8],
src_v: &[u8],
dst: &mut [u8],
width: i32,
height: i32,
) -> Result<()> {
let w = width as usize;
let h = height as usize;
let plane_size = w * h;
if src_y.len() < plane_size || src_u.len() < plane_size || src_v.len() < plane_size {
return Err(YuvError::BufferTooSmall);
}
if dst.len() < argb_size(w, h) {
return Err(YuvError::BufferTooSmall);
}
call_yuv!(H444ToARGB(
src_y.as_ptr(),
width,
src_u.as_ptr(),
width,
src_v.as_ptr(),
width,
dst.as_mut_ptr(),
width * 4,
width,
height,
))
}
/// Convert NV12 to RGB24 /// Convert NV12 to RGB24
pub fn nv12_to_rgb24(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> { pub fn nv12_to_rgb24(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> {
if width % 2 != 0 || height % 2 != 0 { if width % 2 != 0 || height % 2 != 0 {
@@ -924,7 +1066,7 @@ pub fn rgb24_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Res
i420_to_nv12(&i420_buffer, dst, width, height) i420_to_nv12(&i420_buffer, dst, width, height)
} }
/// Convert BGR24 to NV12 (via two-step conversion: BGR24 → I420 → NV12) /// Convert BGR24 to NV12.
pub fn bgr24_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> { pub fn bgr24_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> {
if width % 2 != 0 || height % 2 != 0 { if width % 2 != 0 || height % 2 != 0 {
return Err(YuvError::InvalidDimensions); return Err(YuvError::InvalidDimensions);
@@ -937,10 +1079,71 @@ pub fn bgr24_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Res
return Err(YuvError::BufferTooSmall); return Err(YuvError::BufferTooSmall);
} }
// Two-step conversion: BGR24 → I420 → NV12 #[cfg(windows)]
{
let mut i420_buffer = vec![0u8; i420_size(w, h)]; let mut i420_buffer = vec![0u8; i420_size(w, h)];
bgr24_to_i420(src, &mut i420_buffer, width, height)?; bgr24_to_i420(src, &mut i420_buffer, width, height)?;
i420_to_nv12(&i420_buffer, dst, width, height) return i420_to_nv12(&i420_buffer, dst, width, height);
}
#[cfg(not(windows))]
{
let y_size = w * h;
call_yuv!(RGB24ToNV12(
src.as_ptr(),
width * 3,
dst.as_mut_ptr(),
width,
dst[y_size..].as_mut_ptr(),
width,
width,
height,
))
}
}
/// Read MJPEG dimensions without decoding the frame.
pub fn mjpg_size(src: &[u8]) -> Result<(i32, i32)> {
let mut width = 0;
let mut height = 0;
call_yuv!(MJPGSize(
src.as_ptr(),
usize_to_size_t(src.len()),
&mut width,
&mut height,
))?;
Ok((width, height))
}
/// Decode MJPEG directly to NV12.
pub fn mjpg_to_nv12(src: &[u8], dst: &mut [u8], width: i32, height: i32) -> Result<()> {
if width % 2 != 0 || height % 2 != 0 {
return Err(YuvError::InvalidDimensions);
}
let w = width as usize;
let h = height as usize;
if dst.len() < nv12_size(w, h) {
return Err(YuvError::BufferTooSmall);
}
let y_size = w * h;
let (dst_y, dst_uv) = dst.split_at_mut(y_size);
call_yuv!(MJPGToNV12(
src.as_ptr(),
usize_to_size_t(src.len()),
dst_y.as_mut_ptr(),
width,
dst_uv.as_mut_ptr(),
width,
width,
height,
width,
height,
))
} }
// ============================================================================ // ============================================================================
@@ -1188,4 +1391,15 @@ mod tests {
assert_eq!(converter.dimensions(), (8, 8)); assert_eq!(converter.dimensions(), (8, 8));
assert_eq!(converter.nv12_buffer().len(), nv12_size(8, 8)); assert_eq!(converter.nv12_buffer().len(), nv12_size(8, 8));
} }
#[test]
fn test_bgr24_to_nv12() {
let src = [0u8; 2 * 2 * 3];
let mut dst = [0xffu8; 2 * 2 * 3 / 2];
bgr24_to_nv12(&src, &mut dst, 2, 2).unwrap();
assert_eq!(&dst[..4], &[16, 16, 16, 16]);
assert_eq!(&dst[4..], &[128, 128]);
}
} }

341
scripts/build-update-site.sh Executable file
View File

@@ -0,0 +1,341 @@
#!/usr/bin/env bash
#
# 生成 One-KVM 在线升级静态站点并打包为可部署 tar.gz。
# 输出目录结构:
# <site_name>/v1/channels.json
# <site_name>/v1/releases.json
# <site_name>/v1/bin/<version>/one-kvm-<triple>
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
VERSION=""
RELEASE_CHANNEL="stable"
STABLE_VERSION=""
BETA_VERSION=""
PUBLISHED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
ARTIFACTS_DIR=""
X86_64_BIN=""
AARCH64_BIN=""
ARMV7_BIN=""
X86_64_SET=0
AARCH64_SET=0
ARMV7_SET=0
SITE_NAME="one-kvm-update"
OUTPUT_FILE=""
OUTPUT_DIR="${PROJECT_ROOT}/dist"
declare -a NOTES=()
usage() {
cat <<'EOF'
Usage:
./scripts/build-update-site.sh --version <x.x.x> [options]
Required:
--version <x.x.x> Release 版本号(如 0.1.10
Artifact input (二选一,可混用):
--artifacts-dir <dir> 自动扫描目录中的标准文件名:
one-kvm-x86_64-unknown-linux-gnu
one-kvm-aarch64-unknown-linux-gnu
one-kvm-armv7-unknown-linux-gnueabihf
--x86_64 <file> 指定 x86_64 二进制路径
--aarch64 <file> 指定 aarch64 二进制路径
--armv7 <file> 指定 armv7 二进制路径
Manifest options:
--release-channel <stable|beta> releases.json 里该版本所属渠道,默认 stable
--stable <x.x.x> channels.json 的 stable 指针,默认等于 --version
--beta <x.x.x> channels.json 的 beta 指针,默认等于 --version
--published-at <RFC3339> 发布时间,默认当前 UTC 时间
--note <text> 发布说明,可重复传入多次
Output options:
--site-name <name> 打包根目录名,默认 one-kvm-update
--output-dir <dir> 输出目录(默认 <repo>/dist
--output <file.tar.gz> 输出包完整路径(优先级高于 --output-dir
Other:
-h, --help 显示帮助
Example:
./scripts/build-update-site.sh \
--version 0.1.10 \
--artifacts-dir ./target/release \
--release-channel stable \
--stable 0.1.10 \
--beta 0.1.11 \
--note "修复 WebRTC 断流问题" \
--note "优化 HID 输入延迟"
EOF
}
fail() {
echo "Error: $*" >&2
exit 1
}
require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || fail "Missing required command: ${cmd}"
}
json_escape() {
local s="$1"
s=${s//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\n'/\\n}
s=${s//$'\r'/\\r}
s=${s//$'\t'/\\t}
printf '%s' "$s"
}
is_valid_version() {
local v="$1"
[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
}
is_valid_channel() {
local c="$1"
[[ "$c" == "stable" || "$c" == "beta" ]]
}
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
VERSION="${2:-}"
shift 2
;;
--release-channel)
RELEASE_CHANNEL="${2:-}"
shift 2
;;
--stable)
STABLE_VERSION="${2:-}"
shift 2
;;
--beta)
BETA_VERSION="${2:-}"
shift 2
;;
--published-at)
PUBLISHED_AT="${2:-}"
shift 2
;;
--note)
NOTES+=("${2:-}")
shift 2
;;
--artifacts-dir)
ARTIFACTS_DIR="${2:-}"
shift 2
;;
--x86_64)
X86_64_BIN="${2:-}"
X86_64_SET=1
shift 2
;;
--aarch64)
AARCH64_BIN="${2:-}"
AARCH64_SET=1
shift 2
;;
--armv7)
ARMV7_BIN="${2:-}"
ARMV7_SET=1
shift 2
;;
--site-name)
SITE_NAME="${2:-}"
shift 2
;;
--output-dir)
OUTPUT_DIR="${2:-}"
shift 2
;;
--output)
OUTPUT_FILE="${2:-}"
shift 2
;;
-h | --help)
usage
exit 0
;;
*)
fail "Unknown argument: $1 (use --help)"
;;
esac
done
require_cmd sha256sum
require_cmd stat
require_cmd tar
require_cmd mktemp
[[ -n "$VERSION" ]] || fail "--version is required"
is_valid_version "$VERSION" || fail "Invalid --version: ${VERSION} (expected x.x.x)"
is_valid_channel "$RELEASE_CHANNEL" || fail "Invalid --release-channel: ${RELEASE_CHANNEL}"
if [[ -z "$STABLE_VERSION" ]]; then
STABLE_VERSION="$VERSION"
fi
if [[ -z "$BETA_VERSION" ]]; then
BETA_VERSION="$VERSION"
fi
is_valid_version "$STABLE_VERSION" || fail "Invalid --stable: ${STABLE_VERSION}"
is_valid_version "$BETA_VERSION" || fail "Invalid --beta: ${BETA_VERSION}"
if [[ -n "$ARTIFACTS_DIR" ]]; then
[[ -d "$ARTIFACTS_DIR" ]] || fail "--artifacts-dir not found: ${ARTIFACTS_DIR}"
[[ -n "$X86_64_BIN" ]] || X86_64_BIN="${ARTIFACTS_DIR}/one-kvm-x86_64-unknown-linux-gnu"
[[ -n "$AARCH64_BIN" ]] || AARCH64_BIN="${ARTIFACTS_DIR}/one-kvm-aarch64-unknown-linux-gnu"
[[ -n "$ARMV7_BIN" ]] || ARMV7_BIN="${ARTIFACTS_DIR}/one-kvm-armv7-unknown-linux-gnueabihf"
fi
if [[ "$X86_64_SET" -eq 1 && ! -f "$X86_64_BIN" ]]; then
fail "--x86_64 file not found: ${X86_64_BIN}"
fi
if [[ "$AARCH64_SET" -eq 1 && ! -f "$AARCH64_BIN" ]]; then
fail "--aarch64 file not found: ${AARCH64_BIN}"
fi
if [[ "$ARMV7_SET" -eq 1 && ! -f "$ARMV7_BIN" ]]; then
fail "--armv7 file not found: ${ARMV7_BIN}"
fi
declare -A SRC_BY_TRIPLE=()
if [[ -n "$X86_64_BIN" && -f "$X86_64_BIN" ]]; then
SRC_BY_TRIPLE["x86_64-unknown-linux-gnu"]="$X86_64_BIN"
fi
if [[ -n "$AARCH64_BIN" && -f "$AARCH64_BIN" ]]; then
SRC_BY_TRIPLE["aarch64-unknown-linux-gnu"]="$AARCH64_BIN"
fi
if [[ -n "$ARMV7_BIN" && -f "$ARMV7_BIN" ]]; then
SRC_BY_TRIPLE["armv7-unknown-linux-gnueabihf"]="$ARMV7_BIN"
fi
if [[ ${#SRC_BY_TRIPLE[@]} -eq 0 ]]; then
fail "No artifact found. Provide --artifacts-dir or at least one of --x86_64/--aarch64/--armv7."
fi
BUILD_DIR="$(mktemp -d)"
trap 'rm -rf "$BUILD_DIR"' EXIT
SITE_DIR="${BUILD_DIR}/${SITE_NAME}"
V1_DIR="${SITE_DIR}/v1"
BIN_DIR="${V1_DIR}/bin/${VERSION}"
mkdir -p "$BIN_DIR"
declare -A SHA_BY_TRIPLE=()
declare -A SIZE_BY_TRIPLE=()
TRIPLES=(
"x86_64-unknown-linux-gnu"
"aarch64-unknown-linux-gnu"
"armv7-unknown-linux-gnueabihf"
)
for triple in "${TRIPLES[@]}"; do
src="${SRC_BY_TRIPLE[$triple]:-}"
if [[ -z "$src" ]]; then
continue
fi
[[ -f "$src" ]] || fail "Artifact not found for ${triple}: ${src}"
dest_name="one-kvm-${triple}"
dest_path="${BIN_DIR}/${dest_name}"
cp "$src" "$dest_path"
sha="$(sha256sum "$dest_path" | awk '{print $1}')"
size="$(stat -c%s "$dest_path")"
SHA_BY_TRIPLE["$triple"]="$sha"
SIZE_BY_TRIPLE["$triple"]="$size"
done
cat >"${V1_DIR}/channels.json" <<EOF
{
"stable": "${STABLE_VERSION}",
"beta": "${BETA_VERSION}"
}
EOF
RELEASES_FILE="${V1_DIR}/releases.json"
{
echo '{'
echo ' "releases": ['
echo ' {'
echo " \"version\": \"${VERSION}\","
echo " \"channel\": \"${RELEASE_CHANNEL}\","
echo " \"published_at\": \"${PUBLISHED_AT}\","
if [[ ${#NOTES[@]} -eq 0 ]]; then
echo ' "notes": [],'
else
echo ' "notes": ['
for i in "${!NOTES[@]}"; do
esc_note="$(json_escape "${NOTES[$i]}")"
if [[ "$i" -lt $((${#NOTES[@]} - 1)) ]]; then
echo " \"${esc_note}\","
else
echo " \"${esc_note}\""
fi
done
echo ' ],'
fi
echo ' "artifacts": {'
written=0
for triple in "${TRIPLES[@]}"; do
if [[ -z "${SHA_BY_TRIPLE[$triple]:-}" ]]; then
continue
fi
url="/v1/bin/${VERSION}/one-kvm-${triple}"
if [[ $written -eq 1 ]]; then
echo ','
fi
cat <<EOF
"${triple}": {
"url": "${url}",
"sha256": "${SHA_BY_TRIPLE[$triple]}",
"size": ${SIZE_BY_TRIPLE[$triple]}
}
EOF
written=1
done
echo
echo ' }'
echo ' }'
echo ' ]'
echo '}'
} >"$RELEASES_FILE"
if [[ -n "$OUTPUT_FILE" ]]; then
if [[ "$OUTPUT_FILE" != /* ]]; then
OUTPUT_FILE="${PROJECT_ROOT}/${OUTPUT_FILE}"
fi
else
mkdir -p "$OUTPUT_DIR"
OUTPUT_FILE="${OUTPUT_DIR}/${SITE_NAME}-${VERSION}.tar.gz"
fi
mkdir -p "$(dirname "$OUTPUT_FILE")"
tar -C "$BUILD_DIR" -czf "$OUTPUT_FILE" "$SITE_NAME"
echo "Build complete:"
echo " package: ${OUTPUT_FILE}"
echo " site root in tar: ${SITE_NAME}/"
echo " release version: ${VERSION}"
echo " release channel: ${RELEASE_CHANNEL}"
echo " channels: stable=${STABLE_VERSION}, beta=${BETA_VERSION}"
echo " artifacts:"
for triple in "${TRIPLES[@]}"; do
if [[ -n "${SHA_BY_TRIPLE[$triple]:-}" ]]; then
echo " - ${triple}: size=${SIZE_BY_TRIPLE[$triple]} sha256=${SHA_BY_TRIPLE[$triple]}"
fi
done
echo
echo "Deploy example:"
echo " tar -xzf \"${OUTPUT_FILE}\" -C /var/www/"
echo " # then ensure nginx root points to /var/www/${SITE_NAME}"

View File

@@ -20,6 +20,9 @@ typeshare "$PROJECT_ROOT/src" \
--lang=typescript \ --lang=typescript \
--output-file="$OUTPUT_FILE" --output-file="$OUTPUT_FILE"
# Keep generated output stable for git diff --check.
perl -0pi -e 's/\n+\z/\n/' "$OUTPUT_FILE"
echo "" echo ""
echo "TypeScript types generated successfully!" echo "TypeScript types generated successfully!"
echo "Output: $OUTPUT_FILE" echo "Output: $OUTPUT_FILE"

View File

@@ -8,99 +8,187 @@ use tracing::{debug, info, warn};
use super::executor::{timing, AtxKeyExecutor}; use super::executor::{timing, AtxKeyExecutor};
use super::led::LedSensor; use super::led::LedSensor;
use super::types::{AtxAction, AtxKeyConfig, AtxLedConfig, AtxState, PowerStatus}; use super::types::{
AtxAction, AtxDriverType, AtxInputBinding, AtxKeyConfig, AtxOutputBinding, AtxState, HddStatus,
PowerStatus,
};
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
/// ATX power control configuration
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct AtxControllerConfig { pub struct AtxControllerConfig {
/// Whether ATX is enabled
pub enabled: bool, pub enabled: bool,
/// Power button configuration (used for both short and long press) pub driver: AtxDriverType,
pub power: AtxKeyConfig, pub device: String,
/// Reset button configuration pub baud_rate: u32,
pub reset: AtxKeyConfig, pub power: AtxOutputBinding,
/// LED sensing configuration pub reset: AtxOutputBinding,
pub led: AtxLedConfig, pub led: AtxInputBinding,
pub hdd: AtxInputBinding,
} }
/// Internal state holding all ATX components
/// Grouped together to reduce lock acquisitions /// Grouped together to reduce lock acquisitions
struct AtxInner { struct AtxInner {
config: AtxControllerConfig, config: AtxControllerConfig,
power_executor: Option<AtxKeyExecutor>, power_executor: Option<AtxKeyExecutor>,
reset_executor: Option<AtxKeyExecutor>, reset_executor: Option<AtxKeyExecutor>,
led_sensor: Option<LedSensor>, led_sensor: Option<LedSensor>,
hdd_sensor: Option<LedSensor>,
} }
/// ATX Controller
///
/// Manages ATX power control through independent executors for each action. /// Manages ATX power control through independent executors for each action.
/// Supports hot-reload of configuration. /// Supports hot-reload of configuration.
pub struct AtxController { pub struct AtxController {
/// Single lock for all internal state to reduce lock contention
inner: RwLock<AtxInner>, inner: RwLock<AtxInner>,
} }
impl AtxController { impl AtxController {
fn build_key_config(
config: &AtxControllerConfig,
binding: &AtxOutputBinding,
) -> Option<AtxKeyConfig> {
if !binding.is_configured_for(config.driver, &config.device) {
return None;
}
let device = match config.driver {
AtxDriverType::Gpio => binding.device.clone(),
AtxDriverType::UsbRelay | AtxDriverType::Serial => config.device.clone(),
AtxDriverType::None => return None,
};
Some(AtxKeyConfig {
driver: config.driver,
device,
pin: binding.pin,
active_level: binding.active_level,
baud_rate: config.baud_rate,
})
}
fn runtime_key_configs(
config: &AtxControllerConfig,
) -> (Option<AtxKeyConfig>, Option<AtxKeyConfig>) {
(
Self::build_key_config(config, &config.power),
Self::build_key_config(config, &config.reset),
)
}
fn should_share_serial_device(config: &AtxControllerConfig) -> bool {
if config.driver != AtxDriverType::Serial || config.device.trim().is_empty() {
return false;
}
config.power.enabled && config.reset.enabled
}
async fn init_key_executor(
warn_label: &str,
info_label: &str,
config: AtxKeyConfig,
mut executor: AtxKeyExecutor,
) -> Option<AtxKeyExecutor> {
if let Err(e) = executor.init().await {
warn!("Failed to initialize {} executor: {}", warn_label, e);
return None;
}
info!(
"{} executor initialized: {:?} on {} pin {}",
info_label, config.driver, config.device, config.pin
);
Some(executor)
}
async fn init_components(inner: &mut AtxInner) { async fn init_components(inner: &mut AtxInner) {
// Initialize power executor let (power_config, reset_config) = Self::runtime_key_configs(&inner.config);
if inner.config.power.is_configured() {
let mut executor = AtxKeyExecutor::new(inner.config.power.clone()); if Self::should_share_serial_device(&inner.config) {
if let Err(e) = executor.init().await { match AtxKeyExecutor::open_shared_serial(&inner.config.device, inner.config.baud_rate) {
warn!("Failed to initialize power executor: {}", e); Ok(shared_serial) => {
} else { for (slot, warn_label, info_label, config, serial) in [
info!( (
"Power executor initialized: {:?} on {} pin {}", &mut inner.power_executor,
inner.config.power.driver, inner.config.power.device, inner.config.power.pin "power",
"Power",
power_config.clone(),
shared_serial.clone(),
),
(
&mut inner.reset_executor,
"reset",
"Reset",
reset_config.clone(),
shared_serial,
),
] {
if let Some(config) = config {
let executor =
AtxKeyExecutor::new_with_shared_serial(config.clone(), serial);
*slot =
Self::init_key_executor(warn_label, info_label, config, executor)
.await;
}
}
}
Err(e) => {
warn!(
"Failed to open shared serial device {} for ATX power/reset: {}",
inner.config.device, e
); );
inner.power_executor = Some(executor); }
}
} else {
for (slot, warn_label, info_label, config) in [
(&mut inner.power_executor, "power", "Power", power_config),
(&mut inner.reset_executor, "reset", "Reset", reset_config),
] {
if let Some(config) = config {
let executor = AtxKeyExecutor::new(config.clone());
*slot = Self::init_key_executor(warn_label, info_label, config, executor).await;
}
} }
} }
// Initialize reset executor if inner.config.driver == AtxDriverType::Gpio && inner.config.led.is_configured() {
if inner.config.reset.is_configured() {
let mut executor = AtxKeyExecutor::new(inner.config.reset.clone());
if let Err(e) = executor.init().await {
warn!("Failed to initialize reset executor: {}", e);
} else {
info!(
"Reset executor initialized: {:?} on {} pin {}",
inner.config.reset.driver, inner.config.reset.device, inner.config.reset.pin
);
inner.reset_executor = Some(executor);
}
}
// Initialize LED sensor
if inner.config.led.is_configured() {
let mut sensor = LedSensor::new(inner.config.led.clone()); let mut sensor = LedSensor::new(inner.config.led.clone());
if let Err(e) = sensor.init().await { if let Err(e) = sensor.init().await {
warn!("Failed to initialize LED sensor: {}", e); warn!("Failed to initialize LED sensor: {}", e);
} else { } else {
info!( info!(
"LED sensor initialized on {} pin {}", "LED sensor initialized on {} pin {}",
inner.config.led.gpio_chip, inner.config.led.gpio_pin inner.config.led.device, inner.config.led.pin
); );
inner.led_sensor = Some(sensor); inner.led_sensor = Some(sensor);
} }
} }
if inner.config.driver == AtxDriverType::Gpio && inner.config.hdd.is_configured() {
let mut sensor = LedSensor::new(inner.config.hdd.clone());
if let Err(e) = sensor.init().await {
warn!("Failed to initialize HDD sensor: {}", e);
} else {
info!(
"HDD sensor initialized on {} pin {}",
inner.config.hdd.device, inner.config.hdd.pin
);
inner.hdd_sensor = Some(sensor);
}
}
} }
async fn shutdown_components(inner: &mut AtxInner) { async fn shutdown_components(inner: &mut AtxInner) {
if let Some(executor) = inner.power_executor.as_mut() { for (slot, label) in [
(&mut inner.power_executor, "power"),
(&mut inner.reset_executor, "reset"),
] {
if let Some(executor) = slot.as_mut() {
if let Err(e) = executor.shutdown().await { if let Err(e) = executor.shutdown().await {
warn!("Failed to shutdown power executor: {}", e); warn!("Failed to shutdown {} executor: {}", label, e);
} }
} }
inner.power_executor = None; *slot = None;
if let Some(executor) = inner.reset_executor.as_mut() {
if let Err(e) = executor.shutdown().await {
warn!("Failed to shutdown reset executor: {}", e);
} }
}
inner.reset_executor = None;
if let Some(sensor) = inner.led_sensor.as_mut() { if let Some(sensor) = inner.led_sensor.as_mut() {
if let Err(e) = sensor.shutdown().await { if let Err(e) = sensor.shutdown().await {
@@ -108,9 +196,44 @@ impl AtxController {
} }
} }
inner.led_sensor = None; inner.led_sensor = None;
if let Some(sensor) = inner.hdd_sensor.as_mut() {
if let Err(e) = sensor.shutdown().await {
warn!("Failed to shutdown HDD sensor: {}", e);
}
}
inner.hdd_sensor = None;
}
async fn read_power_status(sensor: Option<&LedSensor>) -> PowerStatus {
let Some(sensor) = sensor else {
return PowerStatus::Unknown;
};
match sensor.read().await {
Ok(status) => status,
Err(e) => {
debug!("Failed to read ATX LED sensor: {}", e);
PowerStatus::Unknown
}
}
}
async fn read_hdd_status(sensor: Option<&LedSensor>) -> HddStatus {
let Some(sensor) = sensor else {
return HddStatus::Unknown;
};
match sensor.read_active().await {
Ok(true) => HddStatus::Active,
Ok(false) => HddStatus::Inactive,
Err(e) => {
debug!("Failed to read ATX HDD sensor: {}", e);
HddStatus::Unknown
}
}
} }
/// Create a new ATX controller with the specified configuration
pub fn new(config: AtxControllerConfig) -> Self { pub fn new(config: AtxControllerConfig) -> Self {
Self { Self {
inner: RwLock::new(AtxInner { inner: RwLock::new(AtxInner {
@@ -118,16 +241,15 @@ impl AtxController {
power_executor: None, power_executor: None,
reset_executor: None, reset_executor: None,
led_sensor: None, led_sensor: None,
hdd_sensor: None,
}), }),
} }
} }
/// Create a disabled ATX controller
pub fn disabled() -> Self { pub fn disabled() -> Self {
Self::new(AtxControllerConfig::default()) Self::new(AtxControllerConfig::default())
} }
/// Initialize the ATX controller and its executors
pub async fn init(&self) -> Result<()> { pub async fn init(&self) -> Result<()> {
let mut inner = self.inner.write().await; let mut inner = self.inner.write().await;
@@ -143,7 +265,6 @@ impl AtxController {
Ok(()) Ok(())
} }
/// Reload ATX controller configuration
pub async fn reload(&self, config: AtxControllerConfig) -> Result<()> { pub async fn reload(&self, config: AtxControllerConfig) -> Result<()> {
let mut inner = self.inner.write().await; let mut inner = self.inner.write().await;
@@ -164,7 +285,6 @@ impl AtxController {
Ok(()) Ok(())
} }
/// Shutdown ATX controller and release all resources
pub async fn shutdown(&self) -> Result<()> { pub async fn shutdown(&self) -> Result<()> {
let mut inner = self.inner.write().await; let mut inner = self.inner.write().await;
Self::shutdown_components(&mut inner).await; Self::shutdown_components(&mut inner).await;
@@ -172,93 +292,162 @@ impl AtxController {
Ok(()) Ok(())
} }
/// Trigger a power action (short/long/reset)
pub async fn trigger_power_action(&self, action: AtxAction) -> Result<()> { pub async fn trigger_power_action(&self, action: AtxAction) -> Result<()> {
let inner = self.inner.read().await; let inner = self.inner.read().await;
match action { let (executor, duration) = match action {
AtxAction::Short | AtxAction::Long => { AtxAction::Short => (inner.power_executor.as_ref(), timing::SHORT_PRESS),
if let Some(executor) = &inner.power_executor { AtxAction::Long => (inner.power_executor.as_ref(), timing::LONG_PRESS),
let duration = match action { AtxAction::Reset => (inner.reset_executor.as_ref(), timing::RESET_PRESS),
AtxAction::Short => timing::SHORT_PRESS,
AtxAction::Long => timing::LONG_PRESS,
_ => unreachable!(),
}; };
executor.pulse(duration).await?;
} else {
return Err(AppError::Config(
"Power button not configured for ATX controller".to_string(),
));
}
}
AtxAction::Reset => {
if let Some(executor) = &inner.reset_executor {
executor.pulse(timing::RESET_PRESS).await?;
} else {
return Err(AppError::Config(
"Reset button not configured for ATX controller".to_string(),
));
}
}
}
let Some(executor) = executor else {
return Err(AppError::Config(
match action {
AtxAction::Reset => "Reset button not configured for ATX controller",
_ => "Power button not configured for ATX controller",
}
.to_string(),
));
};
executor.pulse(duration).await?;
Ok(()) Ok(())
} }
/// Trigger a short power button press
pub async fn power_short(&self) -> Result<()> { pub async fn power_short(&self) -> Result<()> {
self.trigger_power_action(AtxAction::Short).await self.trigger_power_action(AtxAction::Short).await
} }
/// Trigger a long power button press
pub async fn power_long(&self) -> Result<()> { pub async fn power_long(&self) -> Result<()> {
self.trigger_power_action(AtxAction::Long).await self.trigger_power_action(AtxAction::Long).await
} }
/// Trigger a reset button press
pub async fn reset(&self) -> Result<()> { pub async fn reset(&self) -> Result<()> {
self.trigger_power_action(AtxAction::Reset).await self.trigger_power_action(AtxAction::Reset).await
} }
/// Get the current power status using the LED sensor (if configured)
pub async fn power_status(&self) -> PowerStatus { pub async fn power_status(&self) -> PowerStatus {
let inner = self.inner.read().await; let inner = self.inner.read().await;
Self::read_power_status(inner.led_sensor.as_ref()).await
if let Some(sensor) = &inner.led_sensor {
match sensor.read().await {
Ok(status) => status,
Err(e) => {
debug!("Failed to read ATX LED sensor: {}", e);
PowerStatus::Unknown
}
}
} else {
PowerStatus::Unknown
}
} }
/// Get a snapshot of the ATX state for API responses
pub async fn state(&self) -> AtxState { pub async fn state(&self) -> AtxState {
let inner = self.inner.read().await; let inner = self.inner.read().await;
let power_status = if let Some(sensor) = &inner.led_sensor { let power_status = Self::read_power_status(inner.led_sensor.as_ref()).await;
match sensor.read().await { let hdd_status = Self::read_hdd_status(inner.hdd_sensor.as_ref()).await;
Ok(status) => status,
Err(e) => {
debug!("Failed to read ATX LED sensor: {}", e);
PowerStatus::Unknown
}
}
} else {
PowerStatus::Unknown
};
AtxState { AtxState {
available: inner.config.enabled, available: inner.config.enabled,
driver: if inner.config.enabled {
inner.config.driver
} else {
AtxDriverType::None
},
power_configured: inner.power_executor.is_some(), power_configured: inner.power_executor.is_some(),
reset_configured: inner.reset_executor.is_some(), reset_configured: inner.reset_executor.is_some(),
power_status, power_status,
led_supported: inner.led_sensor.is_some(), led_supported: inner.led_sensor.is_some(),
hdd_status,
hdd_supported: inner.hdd_sensor.is_some(),
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::atx::{AtxDriverType, AtxOutputBinding};
#[test]
fn test_should_share_serial_device_true() {
let config = AtxControllerConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
baud_rate: 9600,
power: AtxOutputBinding {
enabled: true,
pin: 1,
..Default::default()
},
reset: AtxOutputBinding {
enabled: true,
pin: 2,
..Default::default()
},
..Default::default()
};
assert!(AtxController::should_share_serial_device(&config));
}
#[test]
fn test_should_share_serial_device_false_when_reset_disabled() {
let config = AtxControllerConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
baud_rate: 9600,
power: AtxOutputBinding {
enabled: true,
pin: 1,
..Default::default()
},
reset: AtxOutputBinding {
enabled: false,
pin: 2,
..Default::default()
},
..Default::default()
};
assert!(!AtxController::should_share_serial_device(&config));
}
#[test]
fn test_gpio_runtime_key_uses_binding_device() {
let config = AtxControllerConfig {
driver: AtxDriverType::Gpio,
device: "/dev/ignored".to_string(),
baud_rate: 9600,
power: AtxOutputBinding {
enabled: true,
device: "/dev/gpiochip1".to_string(),
pin: 4,
active_level: super::super::types::ActiveLevel::Low,
},
..Default::default()
};
let (power, reset) = AtxController::runtime_key_configs(&config);
let power = power.unwrap();
assert!(reset.is_none());
assert_eq!(power.driver, AtxDriverType::Gpio);
assert_eq!(power.device, "/dev/gpiochip1");
assert_eq!(power.pin, 4);
assert_eq!(power.active_level, super::super::types::ActiveLevel::Low);
}
#[test]
fn test_serial_runtime_key_uses_top_level_device() {
let config = AtxControllerConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
baud_rate: 115200,
power: AtxOutputBinding {
enabled: true,
device: "/dev/ignored".to_string(),
pin: 3,
..Default::default()
},
..Default::default()
};
let (power, _) = AtxController::runtime_key_configs(&config);
let power = power.unwrap();
assert_eq!(power.driver, AtxDriverType::Serial);
assert_eq!(power.device, "/dev/ttyUSB0");
assert_eq!(power.pin, 3);
assert_eq!(power.baud_rate, 115200);
}
}

34
src/atx/disabled_key.rs Normal file
View File

@@ -0,0 +1,34 @@
use async_trait::async_trait;
use std::time::Duration;
use super::traits::AtxKeyBackend;
use crate::error::{AppError, Result};
pub struct DisabledAtxKeyBackend {
reason: &'static str,
}
impl DisabledAtxKeyBackend {
pub fn new(reason: &'static str) -> Self {
Self { reason }
}
}
#[async_trait]
impl AtxKeyBackend for DisabledAtxKeyBackend {
async fn init(&mut self) -> Result<()> {
Err(AppError::Internal(self.reason.to_string()))
}
async fn pulse(&self, _duration: Duration) -> Result<()> {
Err(AppError::Internal(self.reason.to_string()))
}
async fn shutdown(&mut self) -> Result<()> {
Ok(())
}
fn is_initialized(&self) -> bool {
false
}
}

38
src/atx/disabled_led.rs Normal file
View File

@@ -0,0 +1,38 @@
#![allow(dead_code)]
use super::types::{AtxInputBinding, PowerStatus};
use crate::error::Result;
pub struct LedSensor {
config: AtxInputBinding,
}
impl LedSensor {
pub fn new(config: AtxInputBinding) -> Self {
Self { config }
}
pub fn is_configured(&self) -> bool {
self.config.is_configured()
}
pub fn is_initialized(&self) -> bool {
false
}
pub async fn init(&mut self) -> Result<()> {
Ok(())
}
pub async fn read(&self) -> Result<PowerStatus> {
Ok(PowerStatus::Unknown)
}
pub async fn read_active(&self) -> Result<bool> {
Ok(false)
}
pub async fn shutdown(&mut self) -> Result<()> {
Ok(())
}
}

View File

@@ -1,418 +1,150 @@
//! ATX Key Executor //! ATX key executor backend selector.
//!
//! Lightweight executor for a single ATX key operation.
//! Each executor handles one button (power or reset) with its own hardware binding.
use gpio_cdev::{Chip, LineHandle, LineRequestFlags};
use serialport::SerialPort;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::Duration; use std::time::Duration;
use tokio::time::sleep; use tracing::debug;
use tracing::{debug, info};
use super::types::{ActiveLevel, AtxDriverType, AtxKeyConfig}; use super::serial_relay::SerialRelayBackend;
use super::traits::{AtxKeyBackend, AtxKeyBackendContext, SharedSerialHandle};
use super::types::{AtxDriverType, AtxKeyConfig};
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
/// Timing constants for ATX operations
pub mod timing { pub mod timing {
use std::time::Duration; use std::time::Duration;
/// Short press duration (power on/graceful shutdown)
pub const SHORT_PRESS: Duration = Duration::from_millis(500); pub const SHORT_PRESS: Duration = Duration::from_millis(500);
/// Long press duration (force power off)
pub const LONG_PRESS: Duration = Duration::from_millis(5000); pub const LONG_PRESS: Duration = Duration::from_millis(5000);
/// Reset press duration
pub const RESET_PRESS: Duration = Duration::from_millis(500); pub const RESET_PRESS: Duration = Duration::from_millis(500);
} }
/// Executor for a single ATX key operation
///
/// Each executor manages one hardware button (power or reset).
/// It handles both GPIO and USB relay backends.
pub struct AtxKeyExecutor { pub struct AtxKeyExecutor {
config: AtxKeyConfig, config: AtxKeyConfig,
gpio_handle: Mutex<Option<LineHandle>>, backend: Option<Box<dyn AtxKeyBackend>>,
/// Cached USB relay file handle to avoid repeated open/close syscalls
usb_relay_handle: Mutex<Option<File>>,
/// Cached Serial port handle
serial_handle: Mutex<Option<Box<dyn SerialPort>>>,
initialized: AtomicBool,
} }
impl AtxKeyExecutor { impl AtxKeyExecutor {
/// Create a new executor with the given configuration
pub fn new(config: AtxKeyConfig) -> Self { pub fn new(config: AtxKeyConfig) -> Self {
Self { Self::with_context(config, AtxKeyBackendContext::Standalone)
config, }
gpio_handle: Mutex::new(None),
usb_relay_handle: Mutex::new(None), pub fn new_with_shared_serial(config: AtxKeyConfig, serial_handle: SharedSerialHandle) -> Self {
serial_handle: Mutex::new(None), Self::with_context(config, AtxKeyBackendContext::SharedSerial(serial_handle))
initialized: AtomicBool::new(false), }
}
pub fn open_shared_serial(device: &str, baud_rate: u32) -> Result<SharedSerialHandle> {
SerialRelayBackend::open_shared_serial(device, baud_rate)
}
fn with_context(config: AtxKeyConfig, context: AtxKeyBackendContext) -> Self {
let backend = build_backend(&config, context);
Self { config, backend }
} }
/// Check if this executor is configured
pub fn is_configured(&self) -> bool { pub fn is_configured(&self) -> bool {
self.config.is_configured() self.config.is_configured()
} }
/// Check if this executor is initialized
pub fn is_initialized(&self) -> bool {
self.initialized.load(Ordering::Relaxed)
}
/// Initialize the executor
pub async fn init(&mut self) -> Result<()> { pub async fn init(&mut self) -> Result<()> {
if !self.config.is_configured() { if !self.config.is_configured() {
debug!("ATX key executor not configured, skipping init"); debug!("ATX key executor not configured, skipping init");
return Ok(()); return Ok(());
} }
self.validate_runtime_config()?; let backend = self.backend.as_mut().ok_or_else(|| {
AppError::Internal(format!(
match self.config.driver { "ATX backend {:?} is unsupported on this platform",
AtxDriverType::Gpio => self.init_gpio().await?, self.config.driver
AtxDriverType::UsbRelay => self.init_usb_relay().await?, ))
AtxDriverType::Serial => self.init_serial().await?,
AtxDriverType::None => {}
}
self.initialized.store(true, Ordering::Relaxed);
Ok(())
}
fn validate_runtime_config(&self) -> Result<()> {
match self.config.driver {
AtxDriverType::Serial => {
if self.config.pin == 0 {
return Err(AppError::Config(
"Serial ATX channel must be 1-based (>= 1)".to_string(),
));
}
if self.config.pin > u8::MAX as u32 {
return Err(AppError::Config(format!(
"Serial ATX channel must be <= {}",
u8::MAX
)));
}
if self.config.baud_rate == 0 {
return Err(AppError::Config(
"Serial ATX baud_rate must be greater than 0".to_string(),
));
}
}
AtxDriverType::UsbRelay => {
if self.config.pin > u8::MAX as u32 {
return Err(AppError::Config(format!(
"USB relay channel must be <= {}",
u8::MAX
)));
}
}
AtxDriverType::Gpio | AtxDriverType::None => {}
}
Ok(())
}
/// Initialize GPIO backend
async fn init_gpio(&mut self) -> Result<()> {
info!(
"Initializing GPIO ATX executor on {} pin {}",
self.config.device, self.config.pin
);
let mut chip = Chip::new(&self.config.device)
.map_err(|e| AppError::Internal(format!("GPIO chip open failed: {}", e)))?;
let line = chip.get_line(self.config.pin).map_err(|e| {
AppError::Internal(format!("GPIO line {} failed: {}", self.config.pin, e))
})?; })?;
// Initial value depends on active level (start in inactive state) backend.init().await
let initial_value = match self.config.active_level {
ActiveLevel::High => 0, // Inactive = low
ActiveLevel::Low => 1, // Inactive = high
};
let handle = line
.request(LineRequestFlags::OUTPUT, initial_value, "one-kvm-atx")
.map_err(|e| AppError::Internal(format!("GPIO request failed: {}", e)))?;
*self.gpio_handle.lock().unwrap() = Some(handle);
debug!("GPIO pin {} configured successfully", self.config.pin);
Ok(())
} }
/// Initialize USB relay backend
async fn init_usb_relay(&self) -> Result<()> {
info!(
"Initializing USB relay ATX executor on {} channel {}",
self.config.device, self.config.pin
);
// Open and cache the device handle
let device = OpenOptions::new()
.read(true)
.write(true)
.open(&self.config.device)
.map_err(|e| AppError::Internal(format!("USB relay device open failed: {}", e)))?;
*self.usb_relay_handle.lock().unwrap() = Some(device);
// Ensure relay is off initially
self.send_usb_relay_command(false)?;
debug!(
"USB relay channel {} configured successfully",
self.config.pin
);
Ok(())
}
/// Initialize Serial relay backend
async fn init_serial(&self) -> Result<()> {
info!(
"Initializing Serial relay ATX executor on {} channel {}",
self.config.device, self.config.pin
);
let baud_rate = self.config.baud_rate;
let port = serialport::new(&self.config.device, baud_rate)
.timeout(Duration::from_millis(100))
.open()
.map_err(|e| AppError::Internal(format!("Serial port open failed: {}", e)))?;
*self.serial_handle.lock().unwrap() = Some(port);
// Ensure relay is off initially
self.send_serial_relay_command(false)?;
debug!(
"Serial relay channel {} configured successfully",
self.config.pin
);
Ok(())
}
/// Pulse the button for the specified duration
pub async fn pulse(&self, duration: Duration) -> Result<()> { pub async fn pulse(&self, duration: Duration) -> Result<()> {
if !self.is_configured() { if !self.is_configured() {
return Err(AppError::Internal("ATX key not configured".to_string())); return Err(AppError::Internal("ATX key not configured".to_string()));
} }
if !self.is_initialized() { let backend = self.backend.as_ref().ok_or_else(|| {
AppError::Internal(format!(
"ATX backend {:?} is unsupported on this platform",
self.config.driver
))
})?;
if !backend.is_initialized() {
return Err(AppError::Internal("ATX key not initialized".to_string())); return Err(AppError::Internal("ATX key not initialized".to_string()));
} }
match self.config.driver { backend.pulse(duration).await
AtxDriverType::Gpio => self.pulse_gpio(duration).await,
AtxDriverType::UsbRelay => self.pulse_usb_relay(duration).await,
AtxDriverType::Serial => self.pulse_serial(duration).await,
AtxDriverType::None => Ok(()),
}
} }
/// Pulse GPIO pin
async fn pulse_gpio(&self, duration: Duration) -> Result<()> {
let (active, inactive) = match self.config.active_level {
ActiveLevel::High => (1u8, 0u8),
ActiveLevel::Low => (0u8, 1u8),
};
// Set to active state
{
let guard = self.gpio_handle.lock().unwrap();
let handle = guard
.as_ref()
.ok_or_else(|| AppError::Internal("GPIO not initialized".to_string()))?;
handle
.set_value(active)
.map_err(|e| AppError::Internal(format!("GPIO set failed: {}", e)))?;
}
// Wait for duration (no lock held)
sleep(duration).await;
// Set to inactive state
{
let guard = self.gpio_handle.lock().unwrap();
if let Some(handle) = guard.as_ref() {
handle.set_value(inactive).ok();
}
}
Ok(())
}
/// Pulse USB relay
async fn pulse_usb_relay(&self, duration: Duration) -> Result<()> {
// Turn relay on
self.send_usb_relay_command(true)?;
// Wait for duration
sleep(duration).await;
// Turn relay off
self.send_usb_relay_command(false)?;
Ok(())
}
/// Send USB relay command using cached handle
fn send_usb_relay_command(&self, on: bool) -> Result<()> {
let channel = u8::try_from(self.config.pin).map_err(|_| {
AppError::Config(format!(
"USB relay channel {} exceeds max {}",
self.config.pin,
u8::MAX
))
})?;
// Standard HID relay command format
let cmd = if on {
[0x00, channel + 1, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00]
} else {
[0x00, channel + 1, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00]
};
let mut guard = self.usb_relay_handle.lock().unwrap();
let device = guard
.as_mut()
.ok_or_else(|| AppError::Internal("USB relay not initialized".to_string()))?;
device
.write_all(&cmd)
.map_err(|e| AppError::Internal(format!("USB relay write failed: {}", e)))?;
Ok(())
}
/// Pulse Serial relay
async fn pulse_serial(&self, duration: Duration) -> Result<()> {
info!(
"Pulse serial relay on {} pin {}",
self.config.device, self.config.pin
);
// Turn relay on
self.send_serial_relay_command(true)?;
// Wait for duration
sleep(duration).await;
// Turn relay off
self.send_serial_relay_command(false)?;
Ok(())
}
/// Send Serial relay command using cached handle
fn send_serial_relay_command(&self, on: bool) -> Result<()> {
let channel = u8::try_from(self.config.pin).map_err(|_| {
AppError::Config(format!(
"Serial relay channel {} exceeds max {}",
self.config.pin,
u8::MAX
))
})?;
if channel == 0 {
return Err(AppError::Config(
"Serial relay channel must be 1-based (>= 1)".to_string(),
));
}
// LCUS-Type Protocol
// Frame: [StopByte(A0), Channel, State, Checksum]
// Checksum = A0 + channel + state
let state = if on { 1 } else { 0 };
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
// Example for Channel 1:
// ON: A0 01 01 A2
// OFF: A0 01 00 A1
let cmd = [0xA0, channel, state, checksum];
let mut guard = self.serial_handle.lock().unwrap();
let port = guard
.as_mut()
.ok_or_else(|| AppError::Internal("Serial relay not initialized".to_string()))?;
port.write_all(&cmd)
.map_err(|e| AppError::Internal(format!("Serial relay write failed: {}", e)))?;
Ok(())
}
/// Shutdown the executor
pub async fn shutdown(&mut self) -> Result<()> { pub async fn shutdown(&mut self) -> Result<()> {
if !self.is_initialized() { if let Some(backend) = self.backend.as_mut() {
return Ok(()); backend.shutdown().await?;
} }
match self.config.driver {
AtxDriverType::Gpio => {
// Release GPIO handle
*self.gpio_handle.lock().unwrap() = None;
}
AtxDriverType::UsbRelay => {
// Ensure relay is off before closing handle
let _ = self.send_usb_relay_command(false);
// Release USB relay handle
*self.usb_relay_handle.lock().unwrap() = None;
}
AtxDriverType::Serial => {
// Ensure relay is off before closing handle
let _ = self.send_serial_relay_command(false);
// Release Serial relay handle
*self.serial_handle.lock().unwrap() = None;
}
AtxDriverType::None => {}
}
self.initialized.store(false, Ordering::Relaxed);
debug!("ATX key executor shutdown complete");
Ok(()) Ok(())
} }
} }
impl Drop for AtxKeyExecutor { fn build_backend(
fn drop(&mut self) { config: &AtxKeyConfig,
// Ensure GPIO lines are released context: AtxKeyBackendContext,
*self.gpio_handle.lock().unwrap() = None; ) -> Option<Box<dyn AtxKeyBackend>> {
match config.driver {
AtxDriverType::Serial => Some(match context {
AtxKeyBackendContext::Standalone => Box::new(SerialRelayBackend::new(config.clone())),
AtxKeyBackendContext::SharedSerial(handle) => Box::new(
SerialRelayBackend::new_with_shared_serial(config.clone(), handle),
),
}),
AtxDriverType::Gpio => build_gpio_backend(config),
AtxDriverType::UsbRelay => build_hidraw_backend(config),
AtxDriverType::None => None,
}
}
// Ensure USB relay is off and handle released #[cfg(unix)]
if self.config.driver == AtxDriverType::UsbRelay && self.is_initialized() { fn build_gpio_backend(config: &AtxKeyConfig) -> Option<Box<dyn AtxKeyBackend>> {
let _ = self.send_usb_relay_command(false); Some(Box::new(super::gpio_linux::GpioLinuxBackend::new(
} config.clone(),
*self.usb_relay_handle.lock().unwrap() = None; )))
}
// Ensure Serial relay is off and handle released #[cfg(not(unix))]
if self.config.driver == AtxDriverType::Serial && self.is_initialized() { fn build_gpio_backend(_config: &AtxKeyConfig) -> Option<Box<dyn AtxKeyBackend>> {
let _ = self.send_serial_relay_command(false); Some(Box::new(super::disabled_key::DisabledAtxKeyBackend::new(
} "GPIO ATX backend is only available on Linux",
*self.serial_handle.lock().unwrap() = None; )))
} }
#[cfg(unix)]
fn build_hidraw_backend(config: &AtxKeyConfig) -> Option<Box<dyn AtxKeyBackend>> {
Some(Box::new(super::hidraw_linux::HidrawLinuxRelayBackend::new(
config.clone(),
)))
}
#[cfg(not(unix))]
fn build_hidraw_backend(_config: &AtxKeyConfig) -> Option<Box<dyn AtxKeyBackend>> {
Some(Box::new(super::disabled_key::DisabledAtxKeyBackend::new(
"USB hidraw relay backend is only available on Linux",
)))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::atx::ActiveLevel;
#[test] #[test]
fn test_executor_creation() { fn executor_creation() {
let config = AtxKeyConfig::default(); let config = AtxKeyConfig::default();
let executor = AtxKeyExecutor::new(config); let executor = AtxKeyExecutor::new(config);
assert!(!executor.is_configured()); assert!(!executor.is_configured());
assert!(!executor.is_initialized());
} }
#[test] #[test]
fn test_executor_with_gpio_config() { fn executor_with_gpio_config() {
let config = AtxKeyConfig { let config = AtxKeyConfig {
driver: AtxDriverType::Gpio, driver: AtxDriverType::Gpio,
device: "/dev/gpiochip0".to_string(), device: "/dev/gpiochip0".to_string(),
@@ -422,16 +154,15 @@ mod tests {
}; };
let executor = AtxKeyExecutor::new(config); let executor = AtxKeyExecutor::new(config);
assert!(executor.is_configured()); assert!(executor.is_configured());
assert!(!executor.is_initialized());
} }
#[test] #[test]
fn test_executor_with_usb_relay_config() { fn executor_with_usb_relay_config() {
let config = AtxKeyConfig { let config = AtxKeyConfig {
driver: AtxDriverType::UsbRelay, driver: AtxDriverType::UsbRelay,
device: "/dev/hidraw0".to_string(), device: "/dev/hidraw0".to_string(),
pin: 0, pin: 1,
active_level: ActiveLevel::High, // Ignored for USB relay active_level: ActiveLevel::High,
baud_rate: 9600, baud_rate: 9600,
}; };
let executor = AtxKeyExecutor::new(config); let executor = AtxKeyExecutor::new(config);
@@ -439,12 +170,12 @@ mod tests {
} }
#[test] #[test]
fn test_executor_with_serial_config() { fn executor_with_serial_config() {
let config = AtxKeyConfig { let config = AtxKeyConfig {
driver: AtxDriverType::Serial, driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(), device: "/dev/ttyUSB0".to_string(),
pin: 1, pin: 1,
active_level: ActiveLevel::High, // Ignored active_level: ActiveLevel::High,
baud_rate: 9600, baud_rate: 9600,
}; };
let executor = AtxKeyExecutor::new(config); let executor = AtxKeyExecutor::new(config);
@@ -452,51 +183,9 @@ mod tests {
} }
#[test] #[test]
fn test_timing_constants() { fn timing_constants() {
assert_eq!(timing::SHORT_PRESS.as_millis(), 500); assert_eq!(timing::SHORT_PRESS.as_millis(), 500);
assert_eq!(timing::LONG_PRESS.as_millis(), 5000); assert_eq!(timing::LONG_PRESS.as_millis(), 5000);
assert_eq!(timing::RESET_PRESS.as_millis(), 500); assert_eq!(timing::RESET_PRESS.as_millis(), 500);
} }
#[tokio::test]
async fn test_executor_init_rejects_serial_channel_zero() {
let config = AtxKeyConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
pin: 0,
active_level: ActiveLevel::High,
baud_rate: 9600,
};
let mut executor = AtxKeyExecutor::new(config);
let err = executor.init().await.unwrap_err();
assert!(matches!(err, AppError::Config(_)));
}
#[tokio::test]
async fn test_executor_init_rejects_serial_channel_overflow() {
let config = AtxKeyConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
pin: 256,
active_level: ActiveLevel::High,
baud_rate: 9600,
};
let mut executor = AtxKeyExecutor::new(config);
let err = executor.init().await.unwrap_err();
assert!(matches!(err, AppError::Config(_)));
}
#[tokio::test]
async fn test_executor_init_rejects_zero_serial_baud_rate() {
let config = AtxKeyConfig {
driver: AtxDriverType::Serial,
device: "/dev/ttyUSB0".to_string(),
pin: 1,
active_level: ActiveLevel::High,
baud_rate: 0,
};
let mut executor = AtxKeyExecutor::new(config);
let err = executor.init().await.unwrap_err();
assert!(matches!(err, AppError::Config(_)));
}
} }

106
src/atx/gpio_linux.rs Normal file
View File

@@ -0,0 +1,106 @@
use async_trait::async_trait;
use gpio_cdev::{Chip, LineHandle, LineRequestFlags};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::Duration;
use tokio::time::sleep;
use tracing::{debug, info};
use super::traits::AtxKeyBackend;
use super::types::{ActiveLevel, AtxKeyConfig};
use crate::error::{AppError, Result};
pub struct GpioLinuxBackend {
config: AtxKeyConfig,
handle: Mutex<Option<LineHandle>>,
initialized: AtomicBool,
}
impl GpioLinuxBackend {
pub fn new(config: AtxKeyConfig) -> Self {
Self {
config,
handle: Mutex::new(None),
initialized: AtomicBool::new(false),
}
}
}
#[async_trait]
impl AtxKeyBackend for GpioLinuxBackend {
async fn init(&mut self) -> Result<()> {
info!(
"Initializing GPIO ATX backend on {} pin {}",
self.config.device, self.config.pin
);
let mut chip = Chip::new(&self.config.device)
.map_err(|e| AppError::Internal(format!("GPIO chip open failed: {}", e)))?;
let line = chip.get_line(self.config.pin).map_err(|e| {
AppError::Internal(format!("GPIO line {} failed: {}", self.config.pin, e))
})?;
let initial_value = match self.config.active_level {
ActiveLevel::High => 0,
ActiveLevel::Low => 1,
};
let handle = line
.request(LineRequestFlags::OUTPUT, initial_value, "one-kvm-atx")
.map_err(|e| AppError::Internal(format!("GPIO request failed: {}", e)))?;
*self.handle.lock().unwrap() = Some(handle);
self.initialized.store(true, Ordering::Relaxed);
debug!("GPIO pin {} configured successfully", self.config.pin);
Ok(())
}
async fn pulse(&self, duration: Duration) -> Result<()> {
if !self.is_initialized() {
return Err(AppError::Internal("GPIO not initialized".to_string()));
}
let (active, inactive) = match self.config.active_level {
ActiveLevel::High => (1u8, 0u8),
ActiveLevel::Low => (0u8, 1u8),
};
{
let guard = self.handle.lock().unwrap();
let handle = guard
.as_ref()
.ok_or_else(|| AppError::Internal("GPIO not initialized".to_string()))?;
handle
.set_value(active)
.map_err(|e| AppError::Internal(format!("GPIO set failed: {}", e)))?;
}
sleep(duration).await;
{
let guard = self.handle.lock().unwrap();
if let Some(handle) = guard.as_ref() {
handle.set_value(inactive).ok();
}
}
Ok(())
}
async fn shutdown(&mut self) -> Result<()> {
*self.handle.lock().unwrap() = None;
self.initialized.store(false, Ordering::Relaxed);
Ok(())
}
fn is_initialized(&self) -> bool {
self.initialized.load(Ordering::Relaxed)
}
}
impl Drop for GpioLinuxBackend {
fn drop(&mut self) {
*self.handle.lock().unwrap() = None;
}
}

166
src/atx/hidraw_linux.rs Normal file
View File

@@ -0,0 +1,166 @@
use async_trait::async_trait;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::Duration;
use tokio::time::sleep;
use tracing::{debug, info};
use super::traits::AtxKeyBackend;
use super::types::{AtxKeyConfig, LCUS_RELAY_MAX_CHANNEL};
use crate::error::{AppError, Result};
const LCUS_RELAY_COMMAND_LEN: usize = 4;
pub struct HidrawLinuxRelayBackend {
config: AtxKeyConfig,
handle: Mutex<Option<File>>,
initialized: AtomicBool,
}
impl HidrawLinuxRelayBackend {
pub fn new(config: AtxKeyConfig) -> Self {
Self {
config,
handle: Mutex::new(None),
initialized: AtomicBool::new(false),
}
}
fn validate_config(&self) -> Result<()> {
if self.config.pin == 0 {
return Err(AppError::Config(
"LCUS HID relay channel must be 1-based (>= 1)".to_string(),
));
}
if self.config.pin > LCUS_RELAY_MAX_CHANNEL as u32 {
return Err(AppError::Config(format!(
"LCUS HID relay channel must be <= {}",
LCUS_RELAY_MAX_CHANNEL
)));
}
Ok(())
}
fn send_command(&self, on: bool) -> Result<()> {
let channel = u8::try_from(self.config.pin).map_err(|_| {
AppError::Config(format!(
"LCUS HID relay channel {} exceeds max {}",
self.config.pin, LCUS_RELAY_MAX_CHANNEL
))
})?;
if channel == 0 {
return Err(AppError::Config(
"LCUS HID relay channel must be 1-based (>= 1)".to_string(),
));
}
if channel > LCUS_RELAY_MAX_CHANNEL {
return Err(AppError::Config(format!(
"LCUS HID relay channel must be <= {}",
LCUS_RELAY_MAX_CHANNEL
)));
}
let cmd = Self::build_command(channel, on);
let mut guard = self.handle.lock().unwrap();
let device = guard
.as_mut()
.ok_or_else(|| AppError::Internal("LCUS HID relay not initialized".to_string()))?;
device
.write_all(&cmd)
.map_err(|e| AppError::Internal(format!("LCUS HID relay write failed: {}", e)))?;
device
.flush()
.map_err(|e| AppError::Internal(format!("LCUS HID relay flush failed: {}", e)))?;
Ok(())
}
pub fn build_command(channel: u8, on: bool) -> [u8; LCUS_RELAY_COMMAND_LEN] {
let state = if on { 1 } else { 0 };
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
[0xA0, channel, state, checksum]
}
}
#[async_trait]
impl AtxKeyBackend for HidrawLinuxRelayBackend {
async fn init(&mut self) -> Result<()> {
self.validate_config()?;
info!(
"Initializing LCUS HID relay ATX backend on {} channel {}",
self.config.device, self.config.pin
);
let device = OpenOptions::new()
.read(true)
.write(true)
.open(&self.config.device)
.map_err(|e| AppError::Internal(format!("LCUS HID relay device open failed: {}", e)))?;
*self.handle.lock().unwrap() = Some(device);
self.send_command(false)?;
self.initialized.store(true, Ordering::Relaxed);
debug!(
"LCUS HID relay channel {} configured successfully",
self.config.pin
);
Ok(())
}
async fn pulse(&self, duration: Duration) -> Result<()> {
if !self.is_initialized() {
return Err(AppError::Internal(
"LCUS HID relay not initialized".to_string(),
));
}
self.send_command(true)?;
sleep(duration).await;
self.send_command(false)?;
Ok(())
}
async fn shutdown(&mut self) -> Result<()> {
if self.is_initialized() {
let _ = self.send_command(false);
}
*self.handle.lock().unwrap() = None;
self.initialized.store(false, Ordering::Relaxed);
Ok(())
}
fn is_initialized(&self) -> bool {
self.initialized.load(Ordering::Relaxed)
}
}
impl Drop for HidrawLinuxRelayBackend {
fn drop(&mut self) {
if self.is_initialized() {
let _ = self.send_command(false);
}
*self.handle.lock().unwrap() = None;
}
}
#[cfg(test)]
mod tests {
use super::HidrawLinuxRelayBackend;
#[test]
fn lcus_hid_relay_command_format() {
assert_eq!(
HidrawLinuxRelayBackend::build_command(1, true),
[0xA0, 0x01, 0x01, 0xA2]
);
assert_eq!(
HidrawLinuxRelayBackend::build_command(1, false),
[0xA0, 0x01, 0x00, 0xA1]
);
}
}

View File

@@ -7,21 +7,17 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex; use std::sync::Mutex;
use tracing::{debug, info}; use tracing::{debug, info};
use super::types::{AtxLedConfig, PowerStatus}; use super::types::{ActiveLevel, AtxInputBinding, PowerStatus};
use crate::error::{AppError, Result}; use crate::error::{AppError, Result};
/// LED sensor for reading power status
///
/// Uses GPIO to read the power LED state and determine if the system is on or off.
pub struct LedSensor { pub struct LedSensor {
config: AtxLedConfig, config: AtxInputBinding,
handle: Mutex<Option<LineHandle>>, handle: Mutex<Option<LineHandle>>,
initialized: AtomicBool, initialized: AtomicBool,
} }
impl LedSensor { impl LedSensor {
/// Create a new LED sensor with the given configuration pub fn new(config: AtxInputBinding) -> Self {
pub fn new(config: AtxLedConfig) -> Self {
Self { Self {
config, config,
handle: Mutex::new(None), handle: Mutex::new(None),
@@ -29,17 +25,6 @@ impl LedSensor {
} }
} }
/// Check if the sensor is configured
pub fn is_configured(&self) -> bool {
self.config.is_configured()
}
/// Check if the sensor is initialized
pub fn is_initialized(&self) -> bool {
self.initialized.load(Ordering::Relaxed)
}
/// Initialize the LED sensor
pub async fn init(&mut self) -> Result<()> { pub async fn init(&mut self) -> Result<()> {
if !self.config.is_configured() { if !self.config.is_configured() {
debug!("LED sensor not configured, skipping init"); debug!("LED sensor not configured, skipping init");
@@ -48,17 +33,14 @@ impl LedSensor {
info!( info!(
"Initializing LED sensor on {} pin {}", "Initializing LED sensor on {} pin {}",
self.config.gpio_chip, self.config.gpio_pin self.config.device, self.config.pin
); );
let mut chip = Chip::new(&self.config.gpio_chip) let mut chip = Chip::new(&self.config.device)
.map_err(|e| AppError::Internal(format!("LED GPIO chip failed: {}", e)))?; .map_err(|e| AppError::Internal(format!("LED GPIO chip failed: {}", e)))?;
let line = chip.get_line(self.config.gpio_pin).map_err(|e| { let line = chip.get_line(self.config.pin).map_err(|e| {
AppError::Internal(format!( AppError::Internal(format!("LED GPIO line {} failed: {}", self.config.pin, e))
"LED GPIO line {} failed: {}",
self.config.gpio_pin, e
))
})?; })?;
let handle = line let handle = line
@@ -72,10 +54,11 @@ impl LedSensor {
Ok(()) Ok(())
} }
/// Read the current power status pub async fn read_active(&self) -> Result<bool> {
pub async fn read(&self) -> Result<PowerStatus> { if !self.config.is_configured() || !self.initialized.load(Ordering::Relaxed) {
if !self.is_configured() || !self.is_initialized() { return Err(AppError::Internal(
return Ok(PowerStatus::Unknown); "GPIO input sensor not initialized".to_string(),
));
} }
let guard = self.handle.lock().unwrap(); let guard = self.handle.lock().unwrap();
@@ -85,24 +68,31 @@ impl LedSensor {
.get_value() .get_value()
.map_err(|e| AppError::Internal(format!("LED read failed: {}", e)))?; .map_err(|e| AppError::Internal(format!("LED read failed: {}", e)))?;
// Apply inversion if configured let active = match self.config.active_level {
let is_on = if self.config.inverted { ActiveLevel::High => value == 1,
value == 0 // Active low: 0 means on ActiveLevel::Low => value == 0,
} else {
value == 1 // Active high: 1 means on
}; };
Ok(if is_on { Ok(active)
}
None => Err(AppError::Internal(
"GPIO input sensor not initialized".to_string(),
)),
}
}
pub async fn read(&self) -> Result<PowerStatus> {
if !self.config.is_configured() || !self.initialized.load(Ordering::Relaxed) {
return Ok(PowerStatus::Unknown);
}
Ok(if self.read_active().await? {
PowerStatus::On PowerStatus::On
} else { } else {
PowerStatus::Off PowerStatus::Off
}) })
} }
None => Ok(PowerStatus::Unknown),
}
}
/// Shutdown the LED sensor
pub async fn shutdown(&mut self) -> Result<()> { pub async fn shutdown(&mut self) -> Result<()> {
*self.handle.lock().unwrap() = None; *self.handle.lock().unwrap() = None;
self.initialized.store(false, Ordering::Relaxed); self.initialized.store(false, Ordering::Relaxed);
@@ -123,35 +113,34 @@ mod tests {
#[test] #[test]
fn test_led_sensor_creation() { fn test_led_sensor_creation() {
let config = AtxLedConfig::default(); let config = AtxInputBinding::default();
let sensor = LedSensor::new(config); let sensor = LedSensor::new(config);
assert!(!sensor.is_configured()); assert!(!sensor.config.is_configured());
assert!(!sensor.is_initialized()); assert!(!sensor.initialized.load(Ordering::Relaxed));
} }
#[test] #[test]
fn test_led_sensor_with_config() { fn test_led_sensor_with_config() {
let config = AtxLedConfig { let config = AtxInputBinding {
enabled: true, enabled: true,
gpio_chip: "/dev/gpiochip0".to_string(), device: "/dev/gpiochip0".to_string(),
gpio_pin: 7, pin: 7,
inverted: false, active_level: ActiveLevel::High,
}; };
let sensor = LedSensor::new(config); let sensor = LedSensor::new(config);
assert!(sensor.is_configured()); assert!(sensor.config.is_configured());
assert!(!sensor.is_initialized()); assert!(!sensor.initialized.load(Ordering::Relaxed));
} }
#[test] #[test]
fn test_led_sensor_inverted_config() { fn test_led_sensor_inverted_config() {
let config = AtxLedConfig { let config = AtxInputBinding {
enabled: true, enabled: true,
gpio_chip: "/dev/gpiochip0".to_string(), device: "/dev/gpiochip0".to_string(),
gpio_pin: 7, pin: 7,
inverted: true, active_level: ActiveLevel::Low,
}; };
let sensor = LedSensor::new(config); let sensor = LedSensor::new(config);
assert!(sensor.is_configured()); assert_eq!(sensor.config.active_level, ActiveLevel::Low);
assert!(sensor.config.inverted);
} }
} }

View File

@@ -2,79 +2,70 @@
//! //!
//! Provides ATX power management functionality for IP-KVM. //! Provides ATX power management functionality for IP-KVM.
//! Supports flexible hardware binding with independent configuration for each action. //! Supports flexible hardware binding with independent configuration for each action.
//!
//! # Features
//!
//! - Power button control (short press for on/graceful shutdown, long press for force off)
//! - Reset button control
//! - Power status monitoring via LED sensing (GPIO only)
//! - Independent hardware binding for each action (GPIO or USB relay)
//! - Hot-reload configuration support
//!
//! # Hardware Support
//!
//! - **GPIO**: Uses Linux GPIO character device (/dev/gpiochipX) for direct hardware control
//! - **USB Relay**: Uses HID USB relay modules for isolated switching
//!
//! # Example
//!
//! ```ignore
//! use one_kvm::atx::{AtxController, AtxControllerConfig, AtxKeyConfig, AtxDriverType, ActiveLevel};
//!
//! let config = AtxControllerConfig {
//! enabled: true,
//! power: AtxKeyConfig {
//! driver: AtxDriverType::Gpio,
//! device: "/dev/gpiochip0".to_string(),
//! pin: 5,
//! active_level: ActiveLevel::High,
//! baud_rate: 9600,
//! },
//! reset: AtxKeyConfig {
//! driver: AtxDriverType::UsbRelay,
//! device: "/dev/hidraw0".to_string(),
//! pin: 0,
//! active_level: ActiveLevel::High,
//! baud_rate: 9600,
//! },
//! led: Default::default(),
//! };
//!
//! let controller = AtxController::new(config);
//! controller.init().await?;
//! controller.power_short().await?; // Turn on or graceful shutdown
//! ```
mod controller; mod controller;
#[cfg(not(unix))]
mod disabled_key;
mod executor; mod executor;
#[cfg(unix)]
mod gpio_linux;
#[cfg(unix)]
mod hidraw_linux;
#[cfg(unix)]
mod led; mod led;
#[cfg(not(unix))]
#[path = "disabled_led.rs"]
mod led;
mod serial_relay;
mod traits;
mod types; mod types;
mod wol; mod wol;
pub use controller::{AtxController, AtxControllerConfig}; pub use controller::{AtxController, AtxControllerConfig};
pub use executor::timing; pub use executor::timing;
pub use types::{ pub use types::{
ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxKeyConfig, AtxLedConfig, AtxPowerRequest, ActiveLevel, AtxAction, AtxDevices, AtxDriverType, AtxInputBinding, AtxKeyConfig,
AtxState, PowerStatus, AtxOutputBinding, AtxPowerRequest, AtxState, HddStatus, PowerStatus, LCUS_RELAY_MAX_CHANNEL,
}; };
pub use wol::send_wol; pub use wol::{list_wol_history, record_wol_history, send_wol};
#[cfg(any(unix, test))]
fn hidraw_uevent_is_usb_relay(uevent: &str) -> bool {
let upper = uevent.to_ascii_uppercase();
upper.contains("00005131:00002007")
|| upper.contains("5131:2007")
|| upper.contains("PRODUCT=5131/2007")
}
#[cfg(unix)]
fn is_usb_relay_hidraw(name: &str) -> bool {
let uevent_path = format!("/sys/class/hidraw/{}/device/uevent", name);
std::fs::read_to_string(uevent_path)
.map(|uevent| hidraw_uevent_is_usb_relay(&uevent))
.unwrap_or(false)
}
/// Discover available ATX devices on the system /// Discover available ATX devices on the system
/// ///
/// Scans for GPIO chips and USB HID relay devices in a single pass. /// Scans for GPIO chips, LCUS USB HID relay devices, and serial relay ports.
pub fn discover_devices() -> AtxDevices { pub fn discover_devices() -> AtxDevices {
let mut devices = AtxDevices::default(); let mut devices = AtxDevices::default();
// Single pass through /dev directory devices.serial_ports = crate::utils::list_serial_ports();
#[cfg(unix)]
if let Ok(entries) = std::fs::read_dir("/dev") { if let Ok(entries) = std::fs::read_dir("/dev") {
for entry in entries.flatten() { for entry in entries.flatten() {
let name = entry.file_name(); let name = entry.file_name();
let name_str = name.to_string_lossy(); let name_str = name.to_string_lossy();
if name_str.starts_with("gpiochip") { if name_str.starts_with("gpiochip") {
devices.gpio_chips.push(format!("/dev/{}", name_str)); devices.gpio_chips.push(format!("/dev/{}", name_str));
} else if name_str.starts_with("hidraw") { }
#[cfg(unix)]
if name_str.starts_with("hidraw") && is_usb_relay_hidraw(&name_str) {
devices.usb_relays.push(format!("/dev/{}", name_str)); devices.usb_relays.push(format!("/dev/{}", name_str));
} else if name_str.starts_with("ttyUSB") || name_str.starts_with("ttyACM") { }
if name_str.starts_with("ttyUSB") || name_str.starts_with("ttyACM") {
devices.serial_ports.push(format!("/dev/{}", name_str)); devices.serial_ports.push(format!("/dev/{}", name_str));
} }
} }
@@ -83,6 +74,7 @@ pub fn discover_devices() -> AtxDevices {
devices.gpio_chips.sort(); devices.gpio_chips.sort();
devices.usb_relays.sort(); devices.usb_relays.sort();
devices.serial_ports.sort(); devices.serial_ports.sort();
devices.serial_ports.dedup();
devices devices
} }
@@ -96,13 +88,35 @@ mod tests {
let _devices = discover_devices(); let _devices = discover_devices();
} }
#[test]
fn test_hidraw_uevent_rejects_non_lcus_usb_relay_id() {
assert!(!hidraw_uevent_is_usb_relay(
"HID_ID=0003:000016C0:000005DF\nHID_NAME=www.dcttech.com USBRelay2\n"
));
}
#[test]
fn test_hidraw_uevent_detects_lcus_hid_relay_id() {
assert!(hidraw_uevent_is_usb_relay(
"HID_ID=0003:00005131:00002007\n"
));
assert!(hidraw_uevent_is_usb_relay("PRODUCT=5131/2007/100"));
}
#[test]
fn test_hidraw_uevent_rejects_unrelated_hid() {
assert!(!hidraw_uevent_is_usb_relay(
"HID_ID=0003:0000046D:0000C534\nHID_NAME=Logitech USB Receiver\n"
));
}
#[test] #[test]
fn test_module_exports() { fn test_module_exports() {
// Verify all public exports are accessible
let _: AtxDriverType = AtxDriverType::None; let _: AtxDriverType = AtxDriverType::None;
let _: ActiveLevel = ActiveLevel::High; let _: ActiveLevel = ActiveLevel::High;
let _: AtxKeyConfig = AtxKeyConfig::default(); let _: AtxKeyConfig = AtxKeyConfig::default();
let _: AtxLedConfig = AtxLedConfig::default(); let _: AtxInputBinding = AtxInputBinding::default();
let _: AtxOutputBinding = AtxOutputBinding::default();
let _: AtxState = AtxState::default(); let _: AtxState = AtxState::default();
let _: AtxDevices = AtxDevices::default(); let _: AtxDevices = AtxDevices::default();
} }

172
src/atx/serial_relay.rs Normal file
View File

@@ -0,0 +1,172 @@
use async_trait::async_trait;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::sleep;
use tracing::{debug, info};
use super::traits::{validate_serial_config, AtxKeyBackend, SharedSerialHandle};
use super::types::{AtxKeyConfig, LCUS_RELAY_MAX_CHANNEL};
use crate::error::{AppError, Result};
pub struct SerialRelayBackend {
config: AtxKeyConfig,
serial_handle: Mutex<Option<SharedSerialHandle>>,
initialized: AtomicBool,
}
impl SerialRelayBackend {
pub fn new(config: AtxKeyConfig) -> Self {
Self {
config,
serial_handle: Mutex::new(None),
initialized: AtomicBool::new(false),
}
}
pub fn new_with_shared_serial(config: AtxKeyConfig, serial_handle: SharedSerialHandle) -> Self {
Self {
config,
serial_handle: Mutex::new(Some(serial_handle)),
initialized: AtomicBool::new(false),
}
}
pub fn open_shared_serial(device: &str, baud_rate: u32) -> Result<SharedSerialHandle> {
let port = serialport::new(device, baud_rate)
.timeout(Duration::from_millis(100))
.open()
.map_err(|e| AppError::Internal(format!("Serial port open failed: {}", e)))?;
Ok(Arc::new(Mutex::new(port)))
}
fn send_command(&self, on: bool) -> Result<()> {
let channel = u8::try_from(self.config.pin).map_err(|_| {
AppError::Config(format!(
"LCUS serial relay channel {} exceeds max {}",
self.config.pin, LCUS_RELAY_MAX_CHANNEL
))
})?;
if channel == 0 {
return Err(AppError::Config(
"LCUS serial relay channel must be 1-based (>= 1)".to_string(),
));
}
if channel > LCUS_RELAY_MAX_CHANNEL {
return Err(AppError::Config(format!(
"LCUS serial relay channel must be <= {}",
LCUS_RELAY_MAX_CHANNEL
)));
}
let cmd = Self::build_command(channel, on);
let serial_handle = self
.serial_handle
.lock()
.unwrap()
.as_ref()
.cloned()
.ok_or_else(|| AppError::Internal("LCUS serial relay not initialized".to_string()))?;
let mut port = serial_handle.lock().unwrap();
port.write_all(&cmd)
.map_err(|e| AppError::Internal(format!("LCUS serial relay write failed: {}", e)))?;
port.flush()
.map_err(|e| AppError::Internal(format!("LCUS serial relay flush failed: {}", e)))?;
Ok(())
}
pub fn build_command(channel: u8, on: bool) -> [u8; 4] {
let state = if on { 1 } else { 0 };
let checksum = 0xA0u8.wrapping_add(channel).wrapping_add(state);
[0xA0, channel, state, checksum]
}
}
#[async_trait]
impl AtxKeyBackend for SerialRelayBackend {
async fn init(&mut self) -> Result<()> {
validate_serial_config(&self.config)?;
info!(
"Initializing LCUS serial relay ATX backend on {} channel {}",
self.config.device, self.config.pin
);
let existing_handle = self.serial_handle.lock().unwrap().as_ref().cloned();
if existing_handle.is_none() {
let shared = Self::open_shared_serial(&self.config.device, self.config.baud_rate)?;
*self.serial_handle.lock().unwrap() = Some(shared);
}
self.send_command(false)?;
self.initialized.store(true, Ordering::Relaxed);
debug!(
"LCUS serial relay channel {} configured successfully",
self.config.pin
);
Ok(())
}
async fn pulse(&self, duration: Duration) -> Result<()> {
if !self.is_initialized() {
return Err(AppError::Internal(
"LCUS serial relay not initialized".to_string(),
));
}
info!(
"Pulse LCUS serial relay on {} channel {}",
self.config.device, self.config.pin
);
self.send_command(true)?;
sleep(duration).await;
self.send_command(false)?;
Ok(())
}
async fn shutdown(&mut self) -> Result<()> {
if !self.is_initialized() {
return Ok(());
}
let _ = self.send_command(false);
*self.serial_handle.lock().unwrap() = None;
self.initialized.store(false, Ordering::Relaxed);
Ok(())
}
fn is_initialized(&self) -> bool {
self.initialized.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::SerialRelayBackend;
#[test]
fn lcus_serial_relay_command_format() {
assert_eq!(
SerialRelayBackend::build_command(1, true),
[0xA0, 0x01, 0x01, 0xA2]
);
assert_eq!(
SerialRelayBackend::build_command(1, false),
[0xA0, 0x01, 0x00, 0xA1]
);
}
}
impl Drop for SerialRelayBackend {
fn drop(&mut self) {
if self.is_initialized() {
let _ = self.send_command(false);
}
*self.serial_handle.lock().unwrap() = None;
}
}

Some files were not shown because too many files have changed in this diff Show More