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) 视频通道: ![CarPlay 主屏幕由 carlinkit-kms 扫描输出至 KMS 硬件图层](https://raw.githubusercontent.com/jwinarske/carlinkit-cxx/main/docs/carplay-home.jpg) ``` 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, 视频解码, 车载系统, 预握手