jwinarske/carlinkit-cxx
GitHub: jwinarske/carlinkit-cxx
为 Carlinkit USB dongle 设计的 Linux 车载 CarPlay/Android Auto 接收器,通过零拷贝硬件解码管线在 DRM/KMS 图层上实时渲染视频与音频。
Stars: 1 | Forks: 1
# carlinkit-cxx
一个专为 Carlinkit **“Auto Box” CarPlay/Android Auto dongle** 设计的 C++/libusb 接收器
(USB `1314:1520` / `1314:1521`),具备零拷贝的硬件图层 (hardware-plane) 视频通道:

```
dongle ──USB bulk──► libusb async DMA ring ──► H.264 ──► decode (VAAPI ▸ V4L2 M2M ▸ CPU)
│ NV12 (DMA-BUF; CPU copy on the software path)
▼
drm-cxx scene ──► HW overlay plane (KMS)
```
手机**无线**连接到 dongle(通过 Wi-Fi + 蓝牙);dongle 上不需要插入任何
设备。完整的设计说明请参阅 `docs/ARCHITECTURE.md`。
## 解码路径
来自 dongle 的 H.264 视频流由三个可互换的后端之一进行解码(每个
后端都是一个 `ck::DecoderSource`,为同一个 KMS 图层生成 NV12 格式):
| 后端 | 适用场景 | 实现方式 |
|---------|------|-----|
| **`vaapi`** | 桌面 / 开发环境 | libavcodec + VAAPI hwaccel → 厂商平铺 NV12 **DMA-BUF**,零拷贝扫描输出。 |
| **`v4l2`** | 嵌入式 SoC (Rockchip, …) | drm-cxx 的有状态 V4L2 M2M 解码器 → NV12 DMA-BUF,无需 GStreamer。 |
| **`software`** | 任何环境(最后手段) | libavcodec CPU 解码 + libswscale → DRM **dumb buffer** 中的 NV12。总是能用,但速度慢——会打印显眼的警告。 |
`CARLINKIT_DECODER` 用于选择后端(`vaapi` / `v4l2` / `software` / `auto`,
默认为 `auto`)。**`auto` 是一个回退链:它会依次尝试 VAAPI、V4L2,然后是
software,并使用第一个成功打开的后端。** 指定具体的后端会将其固定——如果
该后端打开失败,将会报错而不是静默回退,因此你始终明确自己正在运行的是哪个后端。显示旋转(`CARLINKIT_ROTATE`)在每条
路径上均受支持:VAAPI/V4L2 在硬件图层上旋转,software 则在 CPU 上进行旋转。
使用 `-DCARLINKIT_SOFTWARE_ONLY=ON` 进行构建,将**仅**编译 software
后端(移除 VAAPI/V4L2 源代码以及对 libva 的链接,以生成更小的纯 CPU 构建版本)。在包含完整 ffmpeg 的发行版(SteamOS/Arch 等)上,VAAPI 开箱即用;
**Fedora 需要进行两项软件包替换才能实现硬件解码——请参阅下文。**
在 Raspberry Pi 上,后端的选择取决于芯片:**Pi 5** 没有硬件
H.264 解码器,因此使用 `software`;**Pi 4** 的 VideoCore VI 拥有 V4L2 有状态
解码器,因此使用 `v4l2`。`.emb/` 清单针对不同主板进行了相应设置——请参阅
[为 Raspberry Pi 交叉编译](#cross-compiling-for-raspberry-pi-emb)。
## ⚠️ Fedora 环境配置(重要)
Fedora 附带的 Mesa **和** ffmpeg **移除了**受专利限制的 H.264/H.265 编解码器。
如果不进行下面的替换,VAAPI 后端将无法打开(Mesa 不提供 VA 解码器;libavcodec 不提供 VAAPI hwaccel),因此 `auto` 会回退到
**software** 后端:你仍然能看到画面,但这是由 CPU 解码的,速度很慢,
并伴有显眼的 `SOFTWARE DECODE WARNING` 警告。请恢复这两者以实现零拷贝硬件解码。
```
# 1. 启用 RPM Fusion (免费版本足以解决这两个问题)
sudo dnf install -y \
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# 2. 完整的 Mesa VA drivers (为 radeonsi driver 添加 H.264/H.265 *VA decode*)
sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld
# 3. 完整的 libavcodec (为 libavcodec 添加 H.264/H.265 *VAAPI hwaccel*)
sudo dnf install -y libavcodec-freeworld
# 4. 验证
vainfo | grep VAProfileH264 # expect: VAProfileH264High : VAEntrypointVLD
./build/carlinkit-decode-probe out.h264 # expect: "... zero-copy DRM-PRIME export OK"
```
`libavcodec-freeworld` 会覆盖被精简的 `libavcodec-free`。`vainfo` 来自于
`libva-utils`。包含完整 ffmpeg/Mesa 的非 Fedora 发行版可以跳过这些步骤。
## 构建依赖
- `cmake` 和 `meson`(meson 用于配置 drm-cxx 的内置依赖项),一个 **C++17**
编译器(使用 C++17 以匹配 drm-cxx 的 ABI——如果使用 C++20,则会选择 `std::span`,
从而导致与 drm-cxx 的 `tcb::span` 符号不匹配)
- `libusb-1.0` (`libusb1-devel`)
- `libavcodec` + `libavutil` + `libswscale` (`libavcodec-free-devel`,
`libswscale-free-devel` —— 头文件位于 `/usr/include/ffmpeg`),用于 H.264
解码和 NV12 转换;`libva` + `libva-drm` (`libva-devel`) 用于 VAAPI
路径
- KMS 应用:位于 `third_party/drm-cxx` 的 **`drm-cxx`** git 子模块(已锁定版本以保证
可复现构建;可以使用 `-DDRM_CXX_DIR=/path` 覆盖,以便基于
本地工作树进行构建),`libdrm`,`libva`(除非设置了 `-DCARLINKIT_SOFTWARE_ONLY=ON`),
以及 ALSA (`alsa-lib-devel`)
```
git submodule update --init --recursive # fetch the drm-cxx submodule
cmake -S . -B build && cmake --build build # configure provisions drm-cxx's meson subprojects
# 仅 CPU build (省略 VAAPI/V4L2 以及 libva 链接):
cmake -S . -B build -DCARLINKIT_SOFTWARE_ONLY=ON && cmake --build build
```
构建目标:`carlinkit-probe`(无界面捕获)、`carlinkit-decode-probe`(解码
检查)、`carlinkit-audio`(无界面音频)、`carlinkit-drm-dump`(DRM 图层/模式
拓扑),以及 `carlinkit-kms`(完整的主机端体验)。KMS 应用程序仅在
找到 `drm-cxx` + libswscale + libdrm(以及 libva,除非设置了
`CARLINKIT_SOFTWARE_ONLY`)时才会构建。
## 为 Raspberry Pi 交叉编译
`carlinkit-kms` 可以使用
[emb](https://github.com/jwinarske/emb_cli) 针对 64 位 Raspberry Pi OS 进行交叉编译,所需的配置文件位于 `.emb/` 目录中。
解码路径根据各主板芯片进行设置:**Pi 5** 仅限软件解码(没有
硬件 H.264 解码器),**Pi 4** 使用 V4L2 硬件解码器。目标平台:
`rpi5-trixie`, `rpi5-bookworm`, `rpi4-trixie`, `rpi4-bookworm`。
```
# 一次性操作: 安装 emb CLI 并将其 (及其 Dart SDK) 加入 PATH
eval "$(../emb_cli/bootstrap.sh --shellenv)"
git submodule update --init --recursive # drm-cxx + its Vulkan-Headers
emb cross . --list-targets # show the Pi targets
# build carlinkit-kms 并为 Pi 5 (trixie) 打包 .deb:
emb cross . --target rpi5-trixie --build --deb
# -> .config/.../dist/carlinkit-cxx_0.1.0_arm64.deb (依赖自动派生)
```
emb 会下载 ARM GNU 工具链和 PiOS 镜像,从
清单的 `dev_packages` 中准备 sysroot,并基于此构建 drm-cxx + carlinkit(庞大的
工具链/sysroot/构建输出会存放在被 git 忽略的 `.config/` 目录下)。
在 Pi 上部署并安装(emb 自带的 `--deploy` 是为 Flutter bundle 设计的,因此需要通过 scp
传输这个普通的二进制文件):
```
scp .config/.../dist/carlinkit-cxx_0.1.0_arm64.deb pi@raspberrypi.local:
ssh pi@raspberrypi.local 'sudo apt install -y ./carlinkit-cxx_0.1.0_arm64.deb'
```
然后安装 dongle 的 [udev 规则](#usb-access-run-without-root) 并在
控制台 VT 上运行(Pi OS Lite 没有占用 DRM master 的合成器):
```
CARLINKIT_AUDIO_DEV=plughw:0,0 carlinkit-kms /dev/dri/card0 --no-seat
```
**Pi 5 注意事项。** 对 dongle 原生分辨率(例如 1280x1440)进行软件解码可能会
耗尽 CPU 资源并导致音频出现爆音;使用
`CARLINKIT_RESOLUTION=1280x800`(或 `1280x720`)降低 CarPlay 尺寸——在 800p 分辨率下,Pi 5 有充足的
余量(CPU 占用约 18%,温度 59 °C,无降频)。Pi 上没有麦克风,因此
Siri / 通话输入需要 USB 麦克风(`CARLINKIT_MIC_DEV`);HDMI 音频输出
可直接使用。`CARLINKIT_FPS_LOG=1` 会记录解码和显示帧率以及
呈现时序。在播放平缓内容时,显示器能保持 60 fps,但在
解码突发高峰期会出现掉帧——这种残留问题是由于 vc4 KMS 的呈现路径
(翻转缩放后的 NV12 图层)所致,而不是渲染循环的问题,后者已经实现了非阻塞提交。
## USB 访问权限(免 root 运行)
dongle 的 USB 节点默认仅限 root 访问。请安装 udev 规则以实现
非 root 访问(这样即使 dongle 重新枚举,访问权限也能保留):
```
sudo cp scripts/99-carlinkit.rules /etc/udev/rules.d/
sudo udevadm control --reload && sudo udevadm trigger --attr-match=idVendor=1314
```
dongle 还会暴露一个小的 USB 大容量存储分区,该分区会自动挂载到
`/run/media/$USER/APK`;运行前请先卸载它(`umount /run/media/$USER/APK`)。
## 运行
```
# Headless capture: 运行初始 handshake, dump CarPlay stream。
# 将开启了 Wi-Fi + Bluetooth 的 iPhone 靠近设备并接受 CarPlay。
./build/carlinkit-probe # → out.h264 (H.264) + out.pcm (PCM audio)
# 以 headless 方式验证 zero-copy HW decode 路径 (无需 DRM master):
./build/carlinkit-decode-probe out.h264
# Headless 音频 (无显示器): 播放 dongle 的 PCM + 捕获麦克风。
./build/carlinkit-audio [playback-dev] [capture-dev] # e.g. plughw:0,0
# Dump DRM plane/modifier/CRTC topology (只读, 无需 DRM master):
./build/carlinkit-drm-dump /dev/dri/card1
```
### 主机端程序 (`carlinkit-kms`)
完整体验——硬件图层上的实时视频 + 音频 + 触摸/光标。**必须在
空闲的 VT (Ctrl+Alt+F3) 上运行并获取 DRM master 权限**,而不是在桌面环境中或通过 SSH 运行(如有需要,
请先停止你的显示管理器:`sudo systemctl stop gdm`)。
```
./build/carlinkit-kms /dev/dri/card1 --no-seat
# 配对你的 iPhone (开启 Wi-Fi + Bluetooth)。按 Esc / Q / Ctrl-C 退出。
# 检查 / 选择 display mode (同时设置 resolution、fps 和 DPI):
./build/carlinkit-kms /dev/dri/card1 --no-seat --drm-list-modes
./build/carlinkit-kms /dev/dri/card1 --no-seat --drm-mode 9 # by index
./build/carlinkit-kms /dev/dri/card1 --no-seat --drm-mode 1280x720 # or WxH[@R]
```
- **视频**通过硬件图层缩放器填满屏幕并保持长宽比(零
CPU 消耗);当屏幕比例不是 16:9 时,会显示黑边。屏幕的**原生模式**会被保留
(强制使用非原生模式会导致显示器重新缩放并使画面变形)。
- **音频**通过 ALSA 播放(存在时 `"default"` → PipeWire);在
Siri/通话期间会捕获麦克风音频。
- **触摸 / 鼠标**驱动 CarPlay UI;硬件光标会跟随鼠标移动。
- **热插拔**:dongle 会自动(重新)连接;能够妥善处理意外断开
(在视频恢复之前,最后一帧会一直保留在屏幕上)。
环境变量覆盖:
| 变量 | 作用 |
|-----|--------|
| `CARLINKIT_CONNECTOR=DP-1` | 指定目标显示器(例如 `HDMI-A-1`, `DP-1`) |
| `DRM_FORCE_MODE=2560x1440` | 强制在接口上使用特定模式 |
| `CARLINKIT_CRTC=` | 强制使用特定的 CRTC(用于调试;参见 `carlinkit-drm-dump`) |
| `CARLINKIT_DECODER=auto` | 解码后端:`vaapi`/`v4l2`/`software`/`auto`(默认 `auto` = VAAPI ▸ V4L2 ▸ software)。如果指定的后端无法打开,则为致命错误,不会回退 |
| `CARLINKIT_V4L2_DEV=/dev/video0` | V4L2 解码器节点;默认 = 扫描 `/dev/video*` 以查找 H.264 M2M 解码器 |
| `CARLINKIT_ROTATE=90` | 将视频旋转 `90`/`180`/`270°`(VAAPI/V4L2 在硬件图层旋转,software 在 CPU 转换) |
| `CARLINKIT_RESOLUTION=1280x720` | CarPlay/dongle 分辨率;默认 = DRM 模式 (1:1,无缩放) |
| `CARLINKIT_AA_RESOLUTION=1280x720` | Android Auto 画布;默认 = dongle 分辨率 |
| `CARLINKIT_FPS=60` | 帧率;默认 = 模式的刷新率,上限为 60 |
| `CARLINKIT_DPI=160` | UI 密度;默认根据屏幕物理尺寸计算,否则为 160 |
| `CARLINKIT_DRIVE_POSITION=left` | `left`/`right` (LHD/RHD);默认 `left` |
| `CARLINKIT_MEDIA_DELAY=300` | 音视频同步补偿,单位为毫秒(默认 300) |
| `CARLINKIT_GNSS=0` | 向手机广播的 `GNSSCapability` 位掩码(默认 0 = 无;仅在拥有真实 GNSS 数据源时设置) |
| `CARLINKIT_DASHBOARD=1` | `DashboardInfo`:提供 CarPlay Dashboard 功能(默认 1) |
| `CARLINKIT_BT_PHONE=0` | `UseBTPhone`:1 表示通过车载蓝牙路由通话,而不是通过此链路(默认 0) |
| `CARLINKIT_HICAR=0` | 用于华为 HiCar 的 `HiCarConnectMode`(默认 0) |
| `CARLINKIT_POINTER_GAIN=4` | 自由鼠标光标速度倍数(默认 2.5) |
| `CARLINKIT_DRAG_GAIN=5` | 按住按钮时的光标速度——用于滑动/快速滑动(默认 5.0) |
| `CARLINKIT_TOUCH_HZ=90` | 拖动时的触摸上报率;将鼠标数据流合并为类似触摸屏的速率,以便 CarPlay 注册滑动操作(默认 90) |
| `CARLINKIT_AUDIO_DEV=plughw:1,3` | ALSA 播设备(默认 `default`)——参见音频设置 |
| `CARLINKIT_AUDIO_LATENCY_MS=120` | 播放缓冲区大小(毫秒);越大延迟越高,但可减少欠载引起的咔嗒声(默认 120) |
| `CARLINKIT_MIC_DEV=plughw:2,0` | 用于 Siri / 通话的 ALSA 捕获设备(默认 `default`) |
| `CARLINKIT_OEM_ICON=assets/logo.png` | 设为 dongle 自身启动器图块的 PNG 图像(连接时发送);未设置 = 保留 dongle 的默认图标 |
| `CARLINKIT_OEM_LABEL="carlinkit-cxx"` | 显示在 OEM 图标下方的可选说明文字 |
| `CARLINKIT_CAPTURE_DIR=.` | `SIGUSR1` 写入当前帧原始 NV12 屏幕截图的目录(默认 `.`) |
| `CARLINKIT_AUDIO_TRACE=1` | 调试:记录音频混音器的每个流的路由/闪避决策 |
### 音频设置
音频通过 ALSA 播放。应用程序会打开 `"default"` PCM,在正常的桌面
**PipeWire** 会话中,音频会被路由到你的系统音频输出设备——这
通常可以直接使用。但是,在纯粹的 VT 环境(PipeWire 通常未运行)或者当你的扬声器
连接在非默认声卡上时,你必须使用
`CARLINKIT_AUDIO_DEV` 为应用程序指定正确的设备。
1. **列出你的播放设备**并找到你的扬声器所在的声卡:
aplay -l
# card 0: Audio [USB Audio] ... ← USB DAC / 耳机
# card 1: Generic [HD-Audio Generic], device 3: HDMI 0 [LG ...] ← 通过 HDMI 连接的显示器
ALSA 设备名称为 `plughw:,`(`plug` 前缀允许 ALSA 转换
采样率,这是应用程序的数据流所需要的)。示例:`plughw:0,0` (USB),
`plughw:1,3`(上方的 HDMI/显示器输出)。
2. **不确定哪一个接口连接了你的扬声器?** 逐个播放测试音:
speaker-test -D plughw:1,3 -c 2 -t sine -f 440 -l 1 # 聆听,按 Ctrl-C 停止
3. **使用该设备运行:**
CARLINKIT_AUDIO_DEV=plughw:1,3 ./build/carlinkit-kms /dev/dri/card1 --no-seat
或者单独测试音频(无显示,在手机串流时播放):
./build/carlinkit-audio plughw:1,3
**静音:** `AudioOutput` 在打开
设备时会清除声卡的播放静音开关,因此被静音的混音器(常见于 USB DAC)不会
阻止播放。如果你仍然听不到声音,请使用 `alsamixer -c ` 检查音量级别(按 M 切换静音,按上箭头调高
音量)。要在重启后保留混音器状态:`sudo alsactl store`。
**HDMI / DisplayPort 音频**没有软件音量控制(它是固定的;请在
显示器上控制),并且无法捕获音频,因此当你需要使用 Siri / 通话时,请将
麦克风(`CARLINKIT_MIC_DEV`)指向真实的
输入设备——网络摄像头或 USB 麦克风。
## 状态
- ✅ USB 协议 + 异步 DMA 环接收器 (`carlinkit-probe`)
- ✅ 通过 `CARLINKIT_DECODER` 实现的解码后端链 (VAAPI ▸ V4L2 ▸ software)
- ✅ libavcodec+VAAPI H.264 → 零拷贝 NV12 DMA-BUF (`carlinkit-decode-probe`)
- ✅ 软件 (CPU) H.264 → NV12 dumb-buffer 回退方案 (`CARLINKIT_DECODER=software`)
- ✅ 硬件图层上的 drm-cxx KMS 视频接收端 (`carlinkit-kms`) —— 适应长宽比,保持原生模式
- ✅ 所有路径均支持显示旋转(VAAPI/V4L2 在硬件图层,software 在 CPU)
- ✅ ALSA 音频输出 + 麦克风;触摸 + 鼠标光标输入
- ✅ 热插拔 / 意外断开(自动重连)
- ✅ 非阻塞呈现 (`DRM_MODE_ATOMIC_NONBLOCK`) + `CARLINKIT_FPS_LOG` 性能分析
- ✅ 通过 emb (`.emb/`) 交叉编译适用于 Raspberry Pi 5 —— 已在硬件上完成端到端验证(软件解码,HDMI 音频,CarPlay)
- 🚧 Raspberry Pi 4 V4L2 硬件解码路径(可编译;需要 Pi 4 硬件进行验证) + DP-1 图层分配修复
标签:Android Auto, Bash脚本, CarPlay, DMA-BUF, libusb, Linux DRM/KMS, 视频解码, 车载系统, 预握手