diff --git a/source/_posts/pipewire-architecture-00-roadmap.md b/source/_posts/pipewire-architecture-00-roadmap.md new file mode 100644 index 0000000..417157f --- /dev/null +++ b/source/_posts/pipewire-architecture-00-roadmap.md @@ -0,0 +1,174 @@ +--- +title: PipeWire 架构学习路线:从 Linux 声音栈到源码模块地图(系列导读) +date: 2026-10-23 13:30:00 +tags: [PipeWire, Linux音频, 架构, 源码阅读, 教程] +categories: [教程] +--- + +很多人第一次接触 PipeWire,是在终端里看到一句 `Server Name: PulseAudio (on PipeWire 0.3.x)`。 +于是问题马上变成:它到底是 PulseAudio 的替代品、ALSA 的上层、JACK 的竞品,还是一个通用的多媒体图框架? + +这些说法都只对了一部分。PipeWire 同时承担了**用户态媒体服务器、图调度器、进程间传输层和兼容层后端**四个角色。 +如果只从 `pactl` 或 `pw-play` 的使用角度切入,很容易把“会话策略”和“数据搬运”混在一起,后面读源码就会一直打结。 + +这个系列按源码结构拆成 10 篇,目标是让一个会 C、了解 Linux 基础概念的读者,能够建立完整的 PipeWire 心智模型, +并且知道遇到具体问题时该去看哪个目录、哪个对象、哪条协议消息。 + +--- + +## 一、先固定事实边界 + +本系列基于 [PipeWire/pipewire](https://github.com/PipeWire/pipewire) 的一个源码快照: + +| 项目 | 值 | +| --- | --- | +| 提交 | `110e1a42c` | +| `git describe` | `1.6.0-1315-g110e1a42c` | +| `meson.build` 中的项目版本 | `1.7.0` | +| 顶层模块源文件 | 50 个 `src/modules/module-*.c` | +| SPA 插件目录 | 19 个 `spa/plugins/*` | + +这不等于某个发行版安装的版本。Linux 桌面上的 PipeWire、WirePlumber、ALSA、PulseAudio 兼容层和内核驱动版本可能各不相同, +排障时第一步永远是确认**实际运行的二进制和配置**,而不是只看源码树。 + +本文不编造性能数字。涉及调度、延迟和吞吐的结论来自仓库内的设计文档与代码; +所有命令都应以 Linux 环境为准。Windows 上可以读源码,但不能直接运行 PipeWire 守护进程。 + +## 二、PipeWire 在系统中的位置 + +先用一张图把它放回 Linux 多媒体栈: + +```text +应用 / 浏览器 / 游戏 / DAW / 会议软件 + │ + ├── 原生 PipeWire API ───────────────┐ + ├── PulseAudio 客户端库 ── pipewire-pulse + ├── JACK 客户端库 ──────── pipewire-jack + ├── ALSA 客户端库 ──────── pipewire-alsa + └── V4L2 客户端库 ──────── pipewire-v4l2 + │ + libpipewire / native protocol + │ + PipeWire daemon(pipewire 进程) + ├── modules(协议、节点、虚拟设备) + ├── SPA plugins(ALSA/V4L2/BlueZ/转换/混音) + └── graph scheduler(driver + nodes) + │ + Linux 内核与硬件 + ALSA / V4L2 / Bluetooth / DRM / 网络 +``` + +这里最容易混淆的是三层: + +1. **PipeWire daemon**:真正持有全局对象图、打开设备、调度节点、在客户端之间搬数据。 +2. **WirePlumber**:独立进程,通常作为 session manager,负责“发现设备、选默认设备、决定谁连谁、保存策略”。 +3. **兼容层**:让 PulseAudio、JACK、ALSA、V4L2 应用不用改代码,仍然连接到 PipeWire。 + +一句话记忆:**PipeWire 提供机制,WirePlumber 提供策略。** + +PipeWire 本身可以创建节点、端口和链路,但它默认不会因为插入一副耳机就自动决定“音乐应该走耳机”。 +这个决策由 session manager 完成。源码文档也明确把 PipeWire daemon 描述为框架,把策略交给 session manager。 + +## 三、必须建立的五个二分法 + +### 1. 控制面与数据面 + +控制面走 Unix socket 和原生协议,传输的是对象创建、参数协商、状态变化等消息。 +数据面通常走共享内存或 `memfd`,视频场景也可以使用 DMA-BUF;音频不复制整段 PCM 到 socket 里。 + +所以“`pw-dump` 很快”不代表音频处理也走 JSON;“socket 很忙”也不一定意味着音频卡顿。 +排障时要先分清是控制面问题还是数据面问题。 + +### 2. 本地对象与远端代理 + +`pw_context` 是进程内的资源管理器;`pw_core` 是到另一个 PipeWire 实例的代理。 +客户端看到的 `pw_node`、`pw_port`、`pw_link` 多数是远处对象的代理,不是内核对象,也不是 daemon 内部结构体本身。 + +### 3. 模块与插件 + +- **PipeWire module**:加载进 PipeWire 进程的 `.so`,操作 `pw_context`、创建全局对象、扩展协议。 +- **SPA plugin**:更底层的组件,提供 factory 和 interface,例如 ALSA PCM、音频转换、日志、循环。 +- **Extension**:协议和对象模型的扩展,通常同时有客户端侧与服务端侧实现。 + +### 4. 静态配置与运行时状态 + +配置写在 `pipewire.conf`、`client.conf`、`pipewire-pulse.conf` 等文件中; +运行时状态则通过 registry、metadata、对象参数和 session manager 的数据库维护。 +修改配置不等于修改当前图,很多变化需要重载或重启对应进程。 + +### 5. 同步节点与异步节点 + +大多数音频节点按 driver 的周期同步运行。异步节点可以脱离当前周期的严格依赖,代价是至少多一个周期的缓冲和延迟。 +这个区别会直接影响延迟、卡顿和音画同步。 + +## 四、系列文章地图 + +| 篇目 | 主题 | 读完应能回答的问题 | +| --- | --- | --- | +| [00](/2026/10/23/pipewire-architecture-00-roadmap/) | 导读与全局图景 | PipeWire、WirePlumber、ALSA 各自负责什么? | +| [01](/2026/10/23/pipewire-architecture-01-core-model/) | 核心对象模型 | Context、Core、Registry、Proxy、Resource 怎么对应? | +| [02](/2026/10/23/pipewire-architecture-02-spa/) | SPA 插件架构 | 插件如何加载?Node、POD、Buffer、IO 是什么? | +| [03](/2026/10/23/pipewire-architecture-03-graph-scheduling/) | 图调度与实时数据流 | driver、runnable、activation、xrun 怎么工作? | +| [04](/2026/10/23/pipewire-architecture-04-daemon-protocol-security/) | 守护进程、配置与协议 | 启动时加载什么?客户端如何连上并创建对象? | +| [05](/2026/10/23/pipewire-architecture-05-module-catalog/) | 模块全目录 | 50 个 `src/modules` 模块分别做什么? | +| [06](/2026/10/23/pipewire-architecture-06-spa-plugin-catalog/) | SPA 插件全目录 | 19 个插件目录提供哪些 factory? | +| [07](/2026/10/23/pipewire-architecture-07-session-integrations/) | 会话管理与兼容层 | WirePlumber、Pulse、JACK、ALSA、V4L2 如何接进来? | +| [08](/2026/10/23/pipewire-architecture-08-tools-debugging/) | 工具与排障 | 用 `pw-dump`、`pw-top`、`pw-cli` 怎么定位问题? | +| [09](/2026/10/23/pipewire-architecture-09-code-reading-contribution/) | 源码阅读与贡献 | 从哪个入口开始读?写客户端或模块要看哪里? | + +如果你是第一次系统学习,建议按 00 → 01 → 02 → 03 顺序读。 +如果你只想排障,可以先读 01、03、08,再按需查 05 和 06。 +如果你准备写客户端或修改模块,01、02、04、09 是主线。 + +## 五、源码目录怎么读 + +```text +pipewire/ +├── src/pipewire/ libpipewire 公共 API 与对象实现 +├── src/daemon/ pipewire、pipewire-pulse 等配置模板与入口 +├── src/modules/ PipeWire 运行时模块 +├── src/tools/ pw-cli、pw-dump、pw-top 等工具 +├── src/examples/ 客户端示例 +├── spa/include/spa/ SPA 头文件 API:node、pod、buffer、support +├── spa/plugins/ SPA 插件实现:ALSA、V4L2、BlueZ、audioconvert +├── pipewire-alsa/ ALSA 兼容插件 +├── pipewire-jack/ JACK 兼容库 +├── pipewire-v4l2/ V4L2 兼容库 +├── doc/dox/ 设计文档、教程、模块文档 +└── test/ 测试与辅助程序 +``` + +读源码时不要一上来从 `pipewire.c` 的 `main()` 顺着读。 +更有效的路线是: + +1. 从 `src/pipewire/context.c` 和 `src/pipewire/core.c` 理解进程内资源与远端代理。 +2. 从 `spa/include/spa/node/node.h` 理解节点处理接口。 +3. 从 `doc/dox/internals/scheduling.dox` 理解数据线程如何被驱动。 +4. 再回到 `src/modules` 看一个具体模块如何把对象挂进图里。 + +## 六、几个常见误解 + +**误解一:PipeWire 就是 PulseAudio 的重写。** +不是。PipeWire 的图模型、协议和调度层独立于 PulseAudio; +`pipewire-pulse` 只是在其上实现 PulseAudio 服务端协议。 + +**误解二:WirePlumber 是 PipeWire 的插件。** +它是独立进程,通过 PipeWire 客户端 API 控制系统。它可以退出,PipeWire 仍然存在,只是设备发现和自动路由会失去策略来源。 + +**误解三:有 Link 就一定有数据流。** +链路存在只说明图结构存在。节点是否 runnable、是否有 driver、链路是否 passive、对象是否 active,都会影响实际调度。 + +**误解四:延迟只由 buffer size 决定。** +调度周期、驱动时钟、异步链路、重采样、图深度、客户端唤醒时间都会叠加。后文会把 latency 的传播路径单独展开。 + +## 七、小结 + +先记住三句话: + +1. **PipeWire 是用户态媒体图,不是单一音频 API。** +2. **控制面负责“图长什么样”,数据面负责“这一周期谁先跑、谁读谁写”。** +3. **PipeWire 是机制,WirePlumber 是策略,兼容层是入口。** + +下一篇从最核心的对象模型开始:一个客户端连上 daemon 之后,到底拿到了哪些对象,它们又如何跟服务端资源一一对应。 + + diff --git a/source/_posts/pipewire-architecture-01-core-model.md b/source/_posts/pipewire-architecture-01-core-model.md new file mode 100644 index 0000000..90492fd --- /dev/null +++ b/source/_posts/pipewire-architecture-01-core-model.md @@ -0,0 +1,335 @@ +--- +title: PipeWire 核心对象模型:Context、Core、Registry、Proxy 与资源生命周期 +date: 2026-10-23 13:45:00 +tags: [PipeWire, 对象模型, 异步IPC, 源码阅读] +categories: [教程] +--- + +读懂 PipeWire 的第一道门槛不是调度,也不是音频格式,而是“对象到底在哪一边”。 + +同一次 API 调用里,你可能会同时看到 `pw_context`、`pw_core`、`pw_registry`、`pw_proxy`、`pw_global`、`pw_resource` 和 `pw_impl_node`。 +它们不是同一种东西的不同名字,而是分别属于**本地进程、远端连接、服务端全局命名空间、服务端实现层**的不同视图。 +如果把这些概念压成一个“Node 对象”,后面的绑定、销毁、权限和异步消息就会完全对不上。 + +这篇文章从一次客户端连接开始,把 PipeWire 的对象模型拆开。 + +--- + +## 一、PipeWire 同时是库和服务器 + +`libpipewire` 包含两套能力: + +1. **进程内图引擎**:创建 context、加载模块、实现 node/device、维护 loop 和 mempool。 +2. **异步 IPC 客户端**:连接另一个 PipeWire 实例,通过代理对象操作远端图。 + +守护进程和普通客户端用的都是同一套库,只是使用方式不同: + +```text +┌──────────────────────────── 客户端进程 ────────────────────────────┐ +│ pw_main_loop │ +│ └── pw_context │ +│ ├── 本地模块 / SPA 库 / 导出类型 │ +│ ├── pw_core ───────────── Unix socket ──────────────┐ │ +│ └── pw_registry / pw_node / pw_port ...(代理) │ │ +└──────────────────────────────────────────────────────────────┼──────┘ + │ +┌──────────────────────────── 服务端进程 ──────────────────────┼──────┐ +│ pw_main_loop │ │ +│ └── pw_context │ │ +│ ├── pw_global 对象表 │ │ +│ ├── pw_resource(对应每个客户端代理) │ │ +│ └── pw_impl_*(node/link/device 等服务端实现) │ │ +└───────────────────────────────────────────────────────────────┴──────┘ +``` + +客户端侧的 `pw_node` 是代理;服务端侧的 `pw_impl_node` 是实现。 +代理只保存远端 ID、接口版本、属性和事件回调;真正的处理逻辑和调度状态在服务端实现对象里。 + +## 二、Loop:所有异步行为的底座 + +PipeWire 的 API 是异步的,状态变化通过事件推送,而不是同步查询。 +这一切建立在 `pw_loop` 上。 + +源码提供三种常用包装: + +| 类型 | 用途 | +| --- | --- | +| `pw_main_loop` | 在调用线程里运行一个 poll loop,适合工具和普通客户端 | +| `pw_thread_loop` | 在独立线程运行 loop,并提供锁和同步辅助 | +| `pw_data_loop` | 面向实时数据线程的 loop,按优先级和线程属性运行 | + +一个 context 必须绑定一个 main loop。daemon 通常还有一到多个 data loop,数据线程在上面执行 node 的 `process`。 +控制消息、对象状态变化、超时、eventfd 回调都依赖 loop。 + +理解这一点很重要:**PipeWire API 函数大多只是发请求,结果稍后通过 listener 回来。** +看到函数返回 0,只代表请求已经入队,不代表远端对象已经创建完成。 + +## 三、Context:进程内的资源总管 + +`struct pw_context` 是 libpipewire 的本地根对象。它管理: + +- 加载模块和 SPA 库; +- 创建 `pw_impl_*` 对象; +- 维护服务端的 global 对象表; +- 连接远端 PipeWire 实例; +- 注册导出类型,把本地对象导出到另一个实例; +- 获取 data loop、work queue、timer queue、mempool; +- 保存支持接口数组(`spa_support`)。 + +可以把 context 看成“这个进程里的 PipeWire 运行时”。 +它既可能扮演服务端,也可能扮演客户端,甚至同时做两者——例如 `pipewire-pulse` 就是一个连接主 daemon 的客户端,同时自己也能导出对象。 + +### 1. 模块加载与 SPA 库映射 + +模块是加载进 context 的共享库。配置中的: + +```text +context.modules = [ + { name = libpipewire-module-protocol-native } + { name = libpipewire-module-profiler } +] +``` + +最终会进入 `pw_context` 的模块加载流程。模块初始化函数拿到 `pw_impl_module` 和参数,然后可以向 context 注册对象、工厂、协议扩展和监听器。 + +SPA 插件则通过 `context.spa-libs` 把 factory 名称映射到具体库,例如把某个 `api.alsa.*` factory 映射到 `alsa/libspa-alsa.so`。 +这样上层代码只依赖稳定的 factory 名称,不需要硬编码安装路径。 + +### 2. 本地对象与服务端全局对象 + +服务端创建 node 时,流程通常是: + +```text +pw_context_create_node() + │ + ├── 建立 pw_impl_node(本地实现) + ├── 分配 pw_global(可被客户端发现的全局对象) + └── global 获得一个进程内唯一 ID +``` + +`pw_global` 是对象在服务端 registry 中的身份。客户端通过 registry 看到它,再通过 bind 获得一个代理。 + +### 3. 导出类型 + +`pw_context_register_export_type()` 允许模块注册“如何把一个本地对象导出到远端”的函数。 +`client-node`、`client-device`、`metadata` 等模块都利用这个机制,把客户端进程里的实现映射成服务端可调度的对象。 + +这也是 PipeWire 能让 node 跑在客户端进程里的关键:服务端并不复制一份处理逻辑,而是创建远端节点代理,再把调度信号、共享内存和 IO 区域交给客户端。 + +## 四、Core:连接远端实例的根代理 + +`pw_core` 是一个远端连接上的特殊对象,固定 ID 为 `PW_ID_CORE = 0`。 +每次 `pw_context_connect()` 都会得到一个 core 代理。 + +它负责: + +- 接收服务端信息、错误、ping/pong; +- 执行 `sync`,等待此前请求全部处理完; +- 管理对象 ID 的生命周期事件; +- 传递共享内存 ID(`add_mem` / `remove_mem`); +- 创建和销毁对象; +- 作为其他代理的父连接。 + +`pw_core` 不是“整个 daemon”的万能句柄。它更像一个连接会话的根,具体对象要通过 registry 或工厂创建。 + +### Sync 与 Roundtrip + +异步协议里,“我已经发送了请求”和“服务端已经处理完请求”是两件事。 +`pw_core_sync()` 会发送一个同步请求,服务端处理到此位置后返回 `done` 事件。 +客户端常把它当作 roundtrip:等待 `done`,就能确定之前的消息已经被处理。 + +很多工具在枚举 registry 后立刻 sync,因为 registry 没有“枚举结束”事件。 +收到 `done` 后,才知道当前批次的 global 事件已经到齐。 + +## 五、Registry:全局对象目录 + +`pw_registry` 是服务端对象目录的代理。它持续推送: + +- `global`:新增一个全局对象; +- `global_remove`:移除一个全局对象。 + +每个 global 包含: + +| 字段 | 含义 | +| --- | --- | +| `id` | 服务端分配的对象 ID | +| `type` | 接口类型,如 `PipeWire:Interface:Node` | +| `version` | 服务端支持的接口版本 | +| `permissions` | 当前客户端对该对象的权限位 | +| `props` | 对象属性 | + +客户端看到感兴趣的 global 后,调用 bind,把某个本地代理 ID 绑定到该 global。 +绑定成功后,本地代理就可以调用接口方法并接收事件。 + +```text +server global(id=42, type=Node) + │ + │ Registry::Bind(new_id=7) + ▼ +client proxy(id=7) ───────────────► server resource(id=7) + └── 指向 global 42 +``` + +注意两个 ID: + +- **global ID**:对象在服务端 registry 中的身份; +- **proxy ID**:客户端连接内的本地句柄编号。 + +同一个 global 可以被多个客户端分别 bind,每个客户端都有不同的 proxy ID。 +代理 ID 只在当前连接内有效。 + +## 六、Proxy 与 Resource 的一一对应 + +客户端调用方法时,消息头里的目标 ID 是 proxy ID。 +服务端收到后,根据这个 ID 找到对应客户端的 resource,再分派到真正的对象实现。 +反过来,服务端向 resource 发事件,客户端按同一 ID 找到 proxy。 + +```text +client server +pw_node (proxy id 7) ── method ──► pw_resource (id 7) + ▲ │ + │ ├── 指向 pw_global 42 + └────────── event ──────────────────┘ +``` + +### 生命周期 + +删除对象时,客户端发送 destroy,服务端释放 resource 并可能移除 global。 +服务端通过 `remove_id` 告诉客户端“这个代理 ID 可以安全复用”。 +在收到这个事件之前,客户端不能随意把同一个 ID 分配给新对象,否则在途消息可能落到错误对象上。 + +这也是为什么 PipeWire 有一个 `object.serial` 概念: +global ID 可能被复用,而 serial 在对象生命周期内唯一,更适合做持久标识。 + +### Registry generation + +协议还维护 registry generation。服务端每次分配新 global 时递增 generation,并通过消息 footer 同步给客户端。 +客户端在下一条消息里带回自己已处理的 generation。 +服务端据此判断:客户端引用的是当前对象,还是已经被销毁并复用 ID 的旧对象。若已过期,可以返回 `ESTALE`。 + +这是异步对象系统里非常典型的“ID 复用 + 在途消息”问题。读源码时看到 generation 字段,不要把它当成普通序号。 + +## 七、对象类型一览 + +PipeWire 的核心接口包括: + +| 接口 | 作用 | +| --- | --- | +| Core | 连接根对象,ID 固定为 0 | +| Registry | 枚举和绑定 global | +| Client | 一个已连接客户端 | +| Module | 已加载模块 | +| Factory | 创建其他对象的工厂 | +| Device | 设备句柄,可枚举 profile、route、params | +| Node | 媒体处理节点 | +| Port | 节点的输入或输出端口 | +| Link | 两个端口的连接 | +| Metadata | 键值元数据存储 | +| Profiler | 调度与性能信息 | +| Session / Endpoint | 会话管理扩展对象 | + +会话管理扩展不是所有部署都启用。WirePlumber 主要使用 core、device、node、metadata 和权限接口完成策略,不要求这些对象一定存在。 + +## 八、Properties、Params、Methods、Events 的区别 + +这四个词在 PipeWire API 里很常见,但语义不同: + +| 概念 | 用途 | 例子 | +| --- | --- | --- | +| Property | 附加的键值信息,服务端通常不理解其业务含义 | `node.name`、`media.class`、`application.name` | +| Param | 有明确类型和语义的运行时配置 | 格式、缓冲区大小、延迟、端口配置 | +| Method | 客户端发给对象的方法 | 设置参数、订阅参数、销毁对象 | +| Event | 服务端推给客户端的状态变化 | Info、Param、State、Global | + +Property 更像标签和策略输入;Param 才是协议层定义的结构化配置。 +例如 `node.name` 是属性,而 `SPA_PARAM_Format` 是参数。 + +对象第一次 bind 后,一般会收到一个 Info 事件,里面有属性、参数信息、状态等。 +之后只推送发生变化的部分,因此客户端必须维护缓存,不能假设每次事件都是完整快照。 + +## 九、权限也属于对象模型 + +每个客户端对每个 global 都有一组权限位: + +| 位 | 含义 | +| --- | --- | +| `R` | 能看到对象并接收事件 | +| `W` | 能调用修改对象状态的方法 | +| `X` | 能调用对象方法 | +| `M` | 能以该对象作为 subject 设置 metadata | +| `L` | 允许建立到不可见对象的 link | + +客户端连上后并不是立刻拥有全部权限。 +`access` 模块会做第一层判断;如果不能直接授予 unrestricted,就挂起客户端,等待 session manager 配置权限。 + +这解释了为什么某些 Flatpak 应用能看到设备但不能改别人的音量,也解释了为什么 portal 连接需要单独标记。 + +## 十、一次典型客户端启动流程 + +伪代码如下,重点看对象之间的关系,不纠结具体参数: + +```c +pw_init(NULL, NULL); + +loop = pw_main_loop_new(NULL); +context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 0); +core = pw_context_connect(context, NULL, 0); + +pw_core_add_listener(core, &core_listener, &core_events, data); +registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0); +pw_registry_add_listener(registry, ®istry_listener, ®istry_events, data); + +/* 等待 Registry::Global 事件;收到感兴趣的 global 后 bind */ +node = pw_registry_bind(registry, global_id, + PW_TYPE_INTERFACE_Node, + PW_VERSION_NODE, 0); +pw_node_add_listener(node, &node_listener, &node_events, data); + +/* 确保此前请求都已处理 */ +pw_core_sync(core, PW_ID_CORE, 0); + +pw_main_loop_run(loop); +``` + +实际代码还要处理: + +- 对象创建失败和协议错误; +- listener 的注销顺序; +- proxy 在销毁前先移除 listener; +- 连接断开; +- 多线程下把事件投递到正确的 loop; +- registry 中被移除对象对应的本地代理清理。 + +## 十一、高层封装简化了什么 + +手写 registry + proxy 很啰嗦,所以 libpipewire 提供两类高层对象: + +| API | 面向的场景 | 内部大致行为 | +| --- | --- | --- | +| `pw_stream` | 播放、采集、屏幕共享等单向或双向流 | 创建 client-node、协商格式、管理缓冲区和 process 回调 | +| `pw_filter` | 多输入/多输出处理节点,类似 JACK client | 创建并导出处理节点,暴露端口和 process 回调 | + +它们没有绕过对象模型,只是替你完成连接、创建节点、绑定端口、协商参数、处理缓冲区这些模板代码。 +读 `stream.c` 或 `filter.c` 仍然需要理解 proxy/resource、SPA node 和调度,否则只能停留在“能跑”的层面。 + +## 十二、小结 + +对象模型可以压缩成一条链路: + +```text +pw_context + └── pw_core(远端连接根) + └── pw_registry + └── global + └── bind → pw_proxy ↔ pw_resource → pw_impl_* +``` + +需要牢记: + +1. **Context 是本地的,Core 是远端的,Global 是服务端的命名空间条目。** +2. **Proxy 和 Resource 通过 ID 成对出现,但属于不同进程。** +3. **Registry 是持续事件流,不是一次性快照;sync 用来划线。** +4. **Property 是策略信息,Param 是结构化运行时配置。** +5. **权限是对象模型的一部分,不是外层包装。** + +下一篇进入更底层:SPA 如何用 factory、interface、POD、buffer 和 IO 区域组成 PipeWire 的“零件箱”。 diff --git a/source/_posts/pipewire-architecture-02-spa.md b/source/_posts/pipewire-architecture-02-spa.md new file mode 100644 index 0000000..2cc9b6f --- /dev/null +++ b/source/_posts/pipewire-architecture-02-spa.md @@ -0,0 +1,390 @@ +--- +title: SPA 插件架构详解:PipeWire 如何加载、实例化并驱动底层组件 +date: 2026-10-23 14:00:00 +tags: [PipeWire, SPA, 插件架构, SPA POD, 源码阅读] +categories: [教程] +--- + +PipeWire 的模块负责“把对象挂进图里”,但真正碰 ALSA PCM、V4L2、蓝牙编解码、重采样和混音的,往往是 SPA 插件。 + +SPA 全称 Simple Plugin API。它既是一套**只有头文件的 C API**,也是一组运行时加载的 `.so` 插件。 +PipeWire 用它来描述节点、参数、缓冲区、IO 区域和日志/事件循环等宿主能力。 +不理解 SPA,就很难解释 `spa_node_process()` 为什么能被客户端进程执行,也很难看懂格式协商到底在协商什么。 + +这篇文章按“加载一个插件需要哪几步”展开。 + +--- + +## 一、SPA 的设计目标 + +仓库中的 SPA 文档把目标写得很明确: + +- 除了标准 C 库外不引入依赖; +- 时间和空间开销尽量小; +- 宿主可配置,不能把日志、poll loop、系统调用写死; +- API 一致且可版本化; +- 新接口可以低成本的扩展。 + +因此 SPA 不是“PipeWire 的私有对象系统”,而是一个可独立使用的底层插件框架。 +PipeWire 只是它最重要的使用者。 + +SPA 分成两层: + +```text +spa/include/spa/ 头文件 API,调用者和插件共享 ABI 约定 +spa/plugins/ 具体实现,如 alsa、v4l2、audioconvert、support +``` + +常见安装路径是 `spa-0.2` 目录,例如 `/usr/lib64/spa-0.2/`。 +目录位置可以通过 `pkg-config --variable plugindir libspa-0.2` 或环境变量 `SPA_PLUGIN_DIR` 找到。 + +## 二、Factory、Interface、Handle 三个核心概念 + +一个 SPA 插件通常包含一个或多个 factory,每个 factory 可以创建对象,对象再暴露一个或多个 interface。 + +| 概念 | 作用 | +| --- | --- | +| `spa_handle` | 插件实例的内存载体和生命周期根 | +| `spa_handle_factory` | 描述如何计算大小、初始化、枚举接口 | +| `spa_interface` | 带类型和版本的方法表,调用者通过它使用功能 | +| support | 宿主提供给插件的基础能力数组 | + +插件导出固定符号: + +```c +spa_handle_factory_enum +``` + +加载者通过 `dlsym()` 找到它,然后反复调用,枚举所有 factory。 + +每个 factory 有: + +- 名称,例如 `support.cpu`、`api.alsa.pcm.sink`; +- 版本; +- 属性; +- 创建对象所需大小; +- 初始化函数; +- 接口信息枚举函数。 + +Factory 名称是稳定的功能标识,具体由哪个 `.so` 实现可以变化。 +这让 PipeWire 可以在 `context.spa-libs` 里做名称到库的映射。 + +## 三、加载流程 + +完整的加载顺序如下: + +```text +dlopen(".../libspa-xxx.so") + │ + ▼ +dlsym(SPA_HANDLE_FACTORY_ENUM_FUNC_NAME) + │ + ▼ +for each factory: + if factory->name 匹配目标: + if interface 列表包含需要的类型: + size = spa_handle_factory_get_size(factory, info) + mem = calloc(1, size) + spa_handle_factory_init(factory, mem, info, support, n_support) + spa_handle_get_interface(handle, interface_type, &iface) + cast iface 到具体接口结构 +``` + +几个容易忽略的点: + +1. **对象内存通常由调用者分配**,插件不负责 `malloc`。 +2. `get_size()` 可能依赖 `info` 里的参数,所以初始化和计算大小要使用同一组信息。 +3. `support` 是宿主注入的能力数组,插件需要日志、loop、system 等接口时会从这里取。 +4. 接口类型用字符串标识,具体方法表则按头文件里的结构布局调用。 +5. 使用完后要 `spa_handle_clear()`,再 `dlclose()`。 + +SPA 插件没有统一的“构造函数参数对象”,初始化和接口获取是两步。 +这种设计让插件可以只暴露所需接口,也方便宿主进行能力探测。 + +## 四、事件与结果 + +SPA interface 既包含方法,也可以注册事件 listener。 + +例如 `spa_node` 的调用者会传入一个 `spa_node_events` 结构,里面包含: + +- `info`:节点信息变化; +- `port_info`:端口信息变化; +- `result`:参数枚举等操作的结果; +- `event`:节点事件; +- `process` 完成后的状态通知等。 + +参数枚举不是返回一个数组,而是通过 `result` 回调逐项推送。 +枚举函数的返回值可能是同步完成,也可能是异步结果: + +```c +res = spa_node_enum_params(node, seq, id, start, num, filter); +if (SPA_RESULT_IS_ASYNC(res)) + expected_seq = SPA_RESULT_ASYNC_SEQ(res); +``` + +异步结果用高位的 `ASYNC_BIT` 编码,低 bits 是序列号。 +调用者用序列号把后续结果和请求对应起来。 + +这种“事件推送结果”的模式避免了插件分配和返回复杂对象,也减少了所有权问题。 +回调里的对象通常只在回调期间有效,调用者要保存就必须自己复制。 + +## 五、Node 接口:处理单元的最小契约 + +`spa_node` 是 PipeWire 图处理的核心接口。它大致包含: + +| 能力 | 说明 | +| --- | --- | +| 端口信息 | 描述输入/输出端口数量、方向、可用性 | +| 参数枚举 | 查询支持的格式、缓冲区、延迟、端口配置等 | +| 参数设置 | 设置协商后的格式和运行参数 | +| IO 绑定 | 把共享内存中的 IO 区域绑定到节点 | +| 状态切换 | 在 suspended、idle、running 等状态间切换 | +| process | 消费输入、处理并产生输出 | +| 调度信号 | 由 driver 触发节点处理 | + +一个节点可能完全在 daemon 进程里,也可能通过 `client-node` 协议跑在客户端进程里。 +无论在哪边,调度端最终都是对同一个 `spa_node` 接口发命令。 + +### process 里不能做什么 + +数据线程上的 `process` 必须保持实时安全: + +- 不做阻塞 I/O; +- 不等待互斥锁,除非能保证不会阻塞; +- 不进行不可预测的内存分配; +- 不打印高成本日志; +- 不假设每个周期都有完整数据; +- 正确处理空缓冲、部分缓冲和 xrun。 + +调试日志应使用 SPA/PipeWire 的日志设施,并把高噪声的 trace 日志留到明确排障时再开。 + +## 六、Port 与格式协商 + +端口是节点的方法入口,也是一个带方向的对象。 +音频设备通常把每个声道暴露成独立端口;视频设备可能按帧或 plane 组织。 + +格式协商不是一次性“设置格式”,而是一个约束求解过程: + +```text +应用希望:44.1kHz / 立体声 / S16 +设备支持:48kHz 或 44.1kHz / 2ch / S32 或 S16 +转换器支持:多种输入输出格式和重采样 + │ + ├── 枚举 EnumFormat + ├── 选择交集 + ├── 设置 Format + ├── 协商 Buffers + └── 绑定 IO / 开始运行 +``` + +常见的 SPA 参数包括: + +- `SPA_PARAM_EnumFormat`:枚举可用格式; +- `SPA_PARAM_Format`:设置当前格式; +- `SPA_PARAM_Buffers`:缓冲区数量、大小、stride、数据类型; +- `SPA_PARAM_Meta`:需要哪些元数据; +- `SPA_PARAM_IO`:需要哪些 IO 区域; +- `SPA_PARAM_Latency`:延迟信息; +- `SPA_PARAM_PortConfig`:DSP/passthrough 等端口布局; +- `SPA_PARAM_Props`:音量、静音等通用属性。 + +格式协商经常由 adapter 完成。设备节点保持原生格式,adapter 在外部暴露统一格式,必要时插入转换或重采样。 +这就是为什么一个只支持 48kHz 的声卡仍然可以被 44.1kHz 的应用使用,也解释了 DSP 模式为什么通常使用 float32 单声道端口。 + +## 七、SPA POD:可自描述的消息格式 + +PipeWire 协议、参数和元数据都使用 SPA POD 编码。 +POD 不是 JSON,也不是 protobuf,而是一种带类型信息的二进制对象树。 + +核心思想是:**长度 + 类型 + 内容**。 +这样接收方无需协议文件,也能知道下一个字段是 Int、String、Struct、Array 还是 Choice。 + +常见 POD 类型包括: + +| 类型 | 用途 | +| --- | --- | +| `SPA_TYPE_Int` / `Long` / `Float` / `Bool` | 基础值 | +| `SPA_TYPE_Id` | 带命名空间的整数 ID,如参数类型 | +| `SPA_TYPE_String` | 字符串 | +| `SPA_TYPE_Struct` | 固定字段序列 | +| `SPA_TYPE_Object` | 带对象类型和属性键的结构 | +| `SPA_TYPE_Array` | 同类型元素数组 | +| `SPA_TYPE_Choice` | 在多个值、范围或步进值中选择 | +| `SPA_TYPE_Range` | 最小/最大值 | +| `SPA_TYPE_Fraction` | 分数,常用于采样率和延迟 | + +POD 可以用 builder 构造,也可以用 parser 和 iterator 解析。 +最典型的对象是格式描述: + +```text +Object(SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat) + Prop(mediaType, Id, audio) + Prop(mediaSubtype, Id, raw) + Prop(format, Id, S16LE) + Prop(rate, Int, 48000) + Prop(channels, Int, 2) + Prop(position, Array, [FL, FR]) +``` + +POD 的另一个优势是**可以表达约束**。 +例如采样率不是只写一个 48000,而是写一个 Choice,包含多个离散值或一个范围。协商器据此寻找双方都能接受的配置。 + +POD 是 PipeWire 协议的通用交换格式。理解 POD 后,很多 `pw-dump` 输出、`pw-cli` 参数和日志里的结构就不再神秘。 + +## 八、Buffer:数据、元数据和 chunk + +一个 `spa_buffer` 不直接等同于“一块 PCM 内存”。它包含: + +```text +struct spa_buffer +├── n_metas / metas[] 头部、颜色信息、光标、损坏标记等 +├── n_datas / datas[] 音频通道、视频 plane 等数据块 +│ ├── type 内存类型(memfd、DMA-BUF、...) +│ ├── fd 文件描述符 +│ ├── mapoffset 映射偏移 +│ ├── maxsize 最大长度 +│ ├── data 映射后的指针 +│ └── chunk offset / size / stride / flags +└── 内联或外部存储的元数据、chunk 和实际数据 +``` + +几个关键点: + +- **buffer 结构可以单独协商**,实际数据放在共享内存里; +- 多个 data 可以表示视频 plane 或分离的音频通道; +- chunk 描述当前有效区间,不一定等于整块 `maxsize`; +- 元数据可以描述时间戳、损坏、光标、画面方向等; +- 文件描述符可以被协议层传递,接收端再映射。 + +音频常见的是 memfd 共享环形缓冲;视频常用 memfd 或 DMA-BUF。 +因此 PipeWire 可以在不复制大块媒体数据的情况下,把同一缓冲区交给多个消费者。 + +## 九、IO 区域:调度状态和缓冲区指针 + +SPA 用 `spa_io_*` 结构把共享状态放进节点可访问的内存。 +常见 IO 区域包括: + +| IO 类型 | 内容 | +| --- | --- | +| `spa_io_buffers` | 每个端口的 buffer 数组和状态 | +| `spa_io_clock` | 当前周期的时间、位置、采样率、持续时间 | +| `spa_io_position` | 图位置和时钟状态 | +| `spa_io_activation` | 调度依赖、pending/required 计数、状态 | +| `spa_io_async_buffers` | 异步链路的双缓冲槽位 | +| `spa_io_control` | 控制端口数据 | +| `spa_io_profiler` | 性能采样信息 | + +这些区域由主线程或服务端在运行前配置好,数据线程只做指针访问和原子操作。 +调度文档反复强调:节点运行时不应该再做资源分配和复杂配置。 + +`spa_io_clock` 是 driver 的契约。driver 每个周期要更新: + +- `nsec`:本周期开始的单调时钟时间; +- `rate`:采样率或时间换算率; +- `position`:理想的样本位置; +- `duration`:本周期样本数; +- `rate_diff`:内部时钟相对单调时钟的校正因子; +- `next_nsec`:下一周期预计开始时间。 + +消费者依赖这些字段做时间戳、音视频同步和延迟计算。 +如果驱动实现不遵守这套契约,图表面能跑,时间信息却会漂。 + +## 十、Support:宿主注入的基础能力 + +SPA 插件不应该直接假设自己在一个完整桌面环境里。 +它需要的日志、主循环、系统调用封装、CPU 特性、线程和 DBus 能力,都通过 `spa_support` 数组获取。 + +常见 support interface: + +| 接口 | 用途 | +| --- | --- | +| Log | 日志输出,可接 stderr、journal 或自定义后端 | +| Loop | poll/event loop,支持 fd、timer、eventfd | +| System | 文件、内存、线程、时钟等系统封装 | +| CPU | 检测 SIMD 和 CPU 特性,选择优化实现 | +| Thread | 线程创建和实时属性设置 | +| DBus | 访问 DBus,用于 BlueZ、portal 等 | +| Node Driver | 提供节点驱动相关支持 | + +`spa/plugins/support` 目录就实现了这些基础插件,例如 `support.cpu`、`support.loop`、`support.system`、`support.log`。 +它们本身也是 SPA 插件,因此 PipeWire 不需要为宿主能力写死一套全局函数。 + +## 十一、用 SPA 视角看 ALSA 与 audioconvert + +以 ALSA 为例,SPA 层的典型对象链路是: + +```text +spa_device(ALSA card / PCM 抽象) + └── spa_node(PCM source 或 sink) + ├── ports(每声道一个端口) + ├── Format / Buffers 参数 + └── process(读写 ALSA ring buffer) +``` + +`audioconvert` 则提供另一类节点: + +```text +spa_audioadapter + ├── 内部节点:设备原生格式 + ├── 外部端口:DSP 单声道 float32 或 passthrough + ├── 格式转换 / 重采样 / 声道映射 + └── 可选的音量、峰值、静音处理 +``` + +设备的原生格式和应用的期望格式不必一致,adapter 负责把它们接起来。 +这也是 PipeWire 能同时支持 pro-audio 多声道、桌面立体声和蓝牙编码的原因之一。 + +## 十二、读 SPA 源码的顺序 + +建议按这个顺序看: + +1. `spa/include/spa/support/plugin.h`:factory 和 handle 的加载约定。 +2. `spa/include/spa/node/node.h`:node、port、process、params 的接口。 +3. `spa/include/spa/buffer/buffer.h`:buffer、data、chunk 和 meta。 +4. `spa/include/spa/pod/*.h`:POD builder、parser、iterator。 +5. `spa/plugins/support/plugin.c`:一个最小的多 interface 插件。 +6. `spa/plugins/audioconvert/plugin.c`:复杂处理插件如何暴露多个 factory。 +7. `spa/plugins/alsa/alsa.c`:设备类插件如何创建 source/sink/device。 + +读的时候不要只看 factory 名称,要顺着 `spa_handle_factory_init()` 找到对象结构,再找 `spa_node_methods` 里的 `process`、`set_io`、`enum_params` 实现。 +这三个函数通常能解释一个节点 80% 的行为。 + +## 十三、常见坑 + +**把 SPA 当普通同步库调用。** +很多接口返回异步序列号,结果在之后的事件里回来。不要假设函数返回后状态已经稳定。 + +**在 process 中分配内存或加锁。** +实时线程的优先级越高,这类错误造成的抖动越明显。 + +**忽略版本字段。** +接口和事件结构都有版本号。用旧版本编译的调用者可能收不到新字段,也可能传入更小的结构。 + +**把回调指针长期保存。** +参数枚举和事件回调里的对象通常只在回调期间有效。需要保留就复制到自己的内存。 + +**混淆 POD 的类型和值。** +`SPA_TYPE_Id` 不是普通整数,`SPA_TYPE_Object` 也不是任意字典。调试时先看 POD 类型,再看内容。 + +## 十四、小结 + +SPA 的核心可以压缩成一条加载链: + +```text +dlopen + → factory enum + → factory init + → get interface + → enum/set params + → bind IO + → process + → clear +``` + +记住三组对象: + +- **Factory / Handle / Interface**:插件如何被发现和实例化; +- **POD / Param / Buffer**:配置和数据如何表达; +- **Support / IO / Activation**:宿主如何提供能力、时间和调度状态。 + +下一篇回到 PipeWire 图本身:node、port、link 如何变成依赖关系,driver 如何触发整个图,xrun、异步和延迟又是怎么产生的。 diff --git a/source/_posts/pipewire-architecture-03-graph-scheduling.md b/source/_posts/pipewire-architecture-03-graph-scheduling.md new file mode 100644 index 0000000..f6f437e --- /dev/null +++ b/source/_posts/pipewire-architecture-03-graph-scheduling.md @@ -0,0 +1,309 @@ +--- +title: PipeWire 图调度与实时数据流:driver、activation、async 与 latency +date: 2026-10-23 14:15:00 +tags: [PipeWire, 实时音频, 图调度, latency, xrun] +categories: [教程] +--- + +PipeWire 的图不是“每个节点各自开线程读数据”的松散集合。 +它把一个 driver 和一组 runnable 节点组织成调度组,用共享内存里的激活记录和 eventfd 串起依赖关系。 +只有理解了这套机制,`pw-top` 里的 quantum、wait、busy、xrun 才有意义。 + +这篇文章按一个周期的执行顺序讲:谁决定开始,谁先跑,谁通知谁,异步节点为什么多一拍延迟。 + +--- + +## 一、图的三个基本对象 + +```text ++------------+ +------------+ +| | | | +| Node A | Port ── Link ── Port | Node B | +| | | | ++------------+ +------------+ +``` + +- **Node**:媒体处理单元,有 0 个或多个输入/输出端口。 +- **Port**:节点的入口或出口,方向固定为 input 或 output。 +- **Link**:从 output 连到 input,表示数据方向。 + +节点可以在 daemon 内,也可以在客户端进程内。 +远程节点的调度信息通过共享内存和 fd 传递,数据本身也走共享缓冲,不经过 socket 复制。 + +## 二、控制线程与数据线程 + +PipeWire 至少有两类线程: + +| 线程 | 工作 | +| --- | --- | +| main thread | IPC、对象创建/销毁、参数协商、图配置 | +| data thread | 实时处理、调用 node process、更新周期状态 | + +主线程先把资源准备好: + +- 分配共享内存; +- 把 buffer 指针写进 IO 区域; +- 配置节点格式和参数; +- 建立 activation 记录; +- 决定哪些节点 runnable、哪个节点是 driver。 + +之后数据线程只做轻量操作。 +这是 PipeWire 能保持低延迟的基础:**配置不在实时路径上反复发生。** + +## 三、Activation:调度的核心状态 + +每个节点有一个 eventfd 和一份 activation 记录。记录里有: + +```text +status NEED_DATA / HAVE_DATA / OK 等状态位 +pending 还有多少依赖未满足 +required 一共有多少依赖 +``` + +依赖关系来自 link: + +```text +A 的 output ──link──► B 的 input +``` + +这意味着 A 必须先完成,B 才能开始。于是 B 的 `required` 加一,A 处理完后递减 B 的 `pending`。 +当 pending 变成 0,A 通过 eventfd 唤醒 B。 + +多个 link 如果连接的是同一对节点,只会形成一个节点级依赖,不会因为端口多就重复计数。 + +## 四、Driver:周期的发令枪 + +一个调度组必须有一个 driver。driver 可以是: + +- ALSA PCM 节点; +- 蓝牙设备节点; +- 虚拟驱动; +- 网络设备节点; +- 其他声明 `node.driver = true` 的节点。 + +driver 通过硬件中断或 timer 开始周期,并更新 `spa_io_clock`。 +PipeWire 会在候选 driver 中选择 `priority.driver` 最高者。 + +driver 维护它需要调度的 follower 列表。一个周期大致是: + +1. 检查上一周期是否完成;未完成的节点可能被记为 xrun。 +2. 处理 reposition、timebase 等控制请求。 +3. 把每个 follower 的 pending 设置为 required。 +4. 更新周期计数。 +5. 递减目标节点的 required/pending,pending 为 0 的节点被 eventfd 唤醒。 +6. follower 处理完后继续向下游递减。 +7. driver 自己也作为依赖末端被唤醒,周期结束。 +8. 统计 CPU 时间和性能数据。 + +图中 A、B、driver 的依赖可以画成这样: + +```text +driver ──依赖──► A ──依赖──► B + ▲ │ + └────────依赖──────────────┘ +``` + +driver 并不是简单调用一个“全局 process”,而是驱动一个依赖图。 + +## 五、Runnable:哪些节点应该跑 + +一个节点即使存在,也不一定参与处理。未连接的设备节点、暂时无用的 filter 不应该持续空转。 + +PipeWire 用 port passive mode、node group、link group、sync group 等属性决定 runnable 状态。 + +### 1. Port passive mode + +端口可以设置: + +| 值 | 行为 | +| --- | --- | +| `false` | 主动激活 peer,也会被 active peer 激活 | +| `true` | 不激活 peer,也不被 peer 激活 | +| `follow` | 不主动激活 peer,但会被 active peer 激活 | +| `follow-suspend` | 特殊情况可互相激活,通常用于设备端口 | + +节点的 `node.passive` 可以把这些模式批量应用到输入/输出端口。 +设备节点通常默认 `follow-suspend`,这样它们不会单独空转,但被应用链路激活时能正常启动。 + +### 2. Node group 与 link group + +- `node.group`:同一组的节点一起 runnable,并共享调度; +- `node.link-group`:表示多个节点内部已经相连,避免 session manager 再连成环; +- `node.sync-group`:在 JACK transport 等场景下把时间线同步到一起。 + +当组内有多个 driver 候选时,仍然按优先级选一个,其余成为 follower。 + +### 3. 没有 driver 时 + +一个 playback 节点直接连到 capture 节点,如果双方都不声明 driver,组里就没有驱动源,图不会运行。 +可以设置 `node.want-driver = true` 让它加入一个可用 driver,或者设置 `node.always-process = true` 强制参与处理。 + +`always-process` 隐含 `want-driver`,适合需要持续产生数据的节点,但也会带来不必要的 CPU 开销。 + +## 六、远程节点如何被调度 + +如果 node 在客户端进程里,服务端不能直接调用它的函数。 +解决方法是把 eventfd 和 activation 记录通过协议传给客户端。 + +```text +server 共享内存 client 进程 +┌───────────────┐ ┌────────────────┐ +│ activation fd ├─────────►│ eventfd │ +│ buffers ├─────────►│ 映射后的 IO │ +└───────────────┘ └───────┬────────┘ + │ + spa_node_process() +``` + +客户端需要的时候可以直接被 peer 或 driver 唤醒,不必每次先经过服务端中转。 +远程 driver 也可以直接启动周期;只有在 profiler 启用时,才额外通知服务端汇总性能信息。 + +这也是 PipeWire 支持“处理在客户端、调度在服务端”的关键。 + +## 七、Async 节点:多一拍换并发 + +异步节点的 `node.async = true`。它不参与同步依赖链: + +- 不增加 peer 的 pending; +- peer 也不增加它的 pending; +- driver 只负责启动它; +- 它的完成不影响整个图的完成。 + +为了防止读写同一缓冲区,异步链路使用 `spa_io_async_buffers` 的两个槽位: + +```text +cycle N : 写 slot 1,读 slot 0 +cycle N+1 : 写 slot 0,读 slot 1 +``` + +因此异步链路至少增加一个量子的延迟。 +driver 输出端口有一个例外:driver 启动时会把新数据复制到上一周期槽位,让异步节点立刻拿到当前数据。 + +异步适合无法保证实时优先级的处理,例如某些客户端侧计算或网络处理。 +代价是延迟和缓冲管理更复杂,不能把它当成“免费的并行”。 + +## 八、Lazy scheduling:让数据可用性决定何时跑 + +普通 driver 按自己的节拍周期触发。但有些场景不适合固定频率: + +- 屏幕共享只在有新帧时才有数据; +- 采集设备的数据速率与播放设备时钟不完全一致; +- 消费者处理速度比 producer 慢,producer 不应空转。 + +PipeWire 用 `RequestProcess` 事件实现 lazy scheduling: + +- follower 设置 `node.supports-request`,在需要时请求处理; +- driver 设置 `node.supports-lazy`,监听请求并按策略启动图; +- 两边都支持时,调度可以在“driver 定时唤醒”和“follower 请求唤醒”之间切换。 + +典型组合: + +| 场景 | driver | follower | +| --- | --- | --- | +| 屏幕共享 | producer 声明 driver,有新帧才触发 | consumer 跟随 | +| 无头服务器 | consumer 作为 lazy driver | producer 发出请求 | +| 帧编码 | producer 作为 lazy driver | consumer 请求下一帧 | + +lazy 不等于“没有延迟”,它只是把唤醒时机交给数据可用性。 +消费者仍要处理缓冲策略,否则可能引入丢帧或额外排队。 + +## 九、Latency:两股方向相反的传播流 + +PipeWire 的 latency 不是节点上一个静态数字,而是沿链路传播的对象。 + +有两个方向: + +```text +output latency ──────────────► 从上游累积到下游 +input latency ◄────────────── 从下游累积到上游 +``` + +- 输出端口上的 output latency 表示“信号到达这里之前已经延迟了多少”; +- 输入端口上的 input latency 表示“信号从这里离开后还要延迟多少”。 + +当端口有多个链路时,PipeWire 对 latency 取范围合并: + +- 最小值取所有分支的最小值; +- 最大值取所有分支的最大值。 + +节点自身的 `ProcessLatency` 会加到端口 latency 上。 +adapter、重采样、异步链路、设备内部缓冲都会贡献延迟。 + +### 异步链路的额外一拍 + +普通链路只是传播 latency;异步链路会在传播时额外加一个 quantum。 +如果 sink 的输入 latency 是 1 quantum,经过异步链路后在 stream 侧可能变成 2 quantum。 +这不是估算,而是调度结构决定的事实。 + +### 延迟不匹配 + +如果一条信号路径经过 512 样本延迟的处理器,另一条路径没有,它们汇合时就会错位。 +latency 信息可以用来插入 delay/补偿节点,或者由 session manager 调整路由。 + +排障时不要只看 `PIPEWIRE_LATENCY`。 +它主要影响请求的图和缓冲配置,实际端到端延迟还取决于驱动、图深度和设备。 + +## 十、Xrun 与性能观察 + +xrun 通常表示上一周期没有在下一个周期开始前完成。 +可能原因包括: + +- 数据线程被抢占; +- 客户端 process 阻塞或做重活; +- 设备时钟与图时钟漂移; +- 缓冲区太小; +- CPU 频率/功耗策略导致调度不稳定; +- 某个节点异步或远程唤醒延迟过大。 + +工具上可以用 `pw-top` 看: + +- driver 和节点的 quantum; +- wait 时间; +- busy 时间; +- xrun 计数; +- 采样率和格式。 + +但不要只盯单个数字。一次 xrun 可能是偶发,也可能是整条链路已经长期超时。 + +## 十一、一个播放周期串起来 + +假设 ALSA sink 是 driver,应用播放节点是 follower: + +```text +1. ALSA 中断 / timer 到期 +2. sink 更新 spa_io_clock:nsec、position、duration、next_nsec +3. 服务端把应用节点的 pending 设为 required +4. 应用节点被唤醒,写入播放缓冲区 +5. 应用节点递减 sink 的 pending +6. sink 的 pending 归零,被唤醒 +7. sink 从共享缓冲区读取并写入 ALSA ring buffer +8. 周期结束,profiler 记录时间 +``` + +如果应用节点是远程 client-node,第 4 步的唤醒直接通过 eventfd 到客户端进程。 +如果链路是异步的,第 3 到第 5 步之间会经过双缓冲槽位,数据对应到上一个周期。 + +## 十二、设计取舍 + +PipeWire 的调度设计有几组明确取舍: + +| 选择 | 好处 | 代价 | +| --- | --- | --- | +| 共享内存传数据 | 避免大块复制 | 生命周期和同步更复杂 | +| 中心 driver 调度 | 时间线统一,延迟可计算 | 依赖 driver 质量 | +| 客户端侧 node | 应用可直接处理数据 | 远程唤醒和 RT 属性更关键 | +| async 节点 | 避免同步阻塞 | 至少多一拍延迟 | +| lazy scheduling | 空闲时不空转 | 需要双方声明能力 | +| 图级 latency 传播 | 可做端到端补偿 | 每个节点必须正确实现 | + +理解这些取舍后,再看 `pw-top`、日志和链路属性,就不会只停留在“这个数大不大”,而能判断是结构问题还是实现问题。 + +## 十三、小结 + +调度层记三件事: + +1. **driver 决定周期,activation 决定依赖,eventfd 决定唤醒。** +2. **runnable 是图属性,不是节点创建后就自动成立。** +3. **latency 是双向传播的链式信息,异步链路会额外增加一拍。** + +下一篇看 daemon 如何加载这些模块、如何用原生协议连接客户端,以及 socket、fd 传递、权限和 portal 如何共同保证安全边界。 diff --git a/source/_posts/pipewire-architecture-04-daemon-protocol-security.md b/source/_posts/pipewire-architecture-04-daemon-protocol-security.md new file mode 100644 index 0000000..6d72100 --- /dev/null +++ b/source/_posts/pipewire-architecture-04-daemon-protocol-security.md @@ -0,0 +1,329 @@ +--- +title: PipeWire 守护进程、配置与原生协议:控制面如何建立连接 +date: 2026-10-23 14:30:00 +tags: [PipeWire, daemon, 原生协议, 安全, Flatpak] +categories: [教程] +--- + +当你在终端输入 `pw-dump`,到 JSON 出现在屏幕上,中间发生了几件完全不同的事: + +1. 客户端找到并连接 PipeWire 的 Unix socket; +2. 双方用原生协议交换版本和对象信息; +3. 客户端绑定 registry,收齐 global 事件; +4. 对每个对象 bind,再枚举属性和参数; +5. 服务端通过共享内存和 fd 传递必要的资源。 + +这条链路同时涉及配置、模块、协议、对象模型和权限。本文把这些部分串起来。 + +--- + +## 一、daemon 的职责边界 + +PipeWire daemon 是用户态中心进程,负责: + +- 维护全局对象图和 global 表; +- 加载模块; +- 打开受控设备; +- 在客户端之间转发控制消息; +- 调度媒体图; +- 通过共享内存传递媒体数据; +- 执行基础访问控制。 + +它不负责决定“默认输出设备应该是哪个”,也不主动发现所有硬件。 +这些属于 session manager 的策略层。 + +一个典型桌面会话中有: + +```text +systemd --user +├── pipewire.service / pipewire.socket +├── wireplumber.service ← session manager +├── pipewire-pulse.service ← PulseAudio 兼容服务 +└── 应用进程 +``` + +`pipewire-pulse` 自己也是一个客户端,它把 PulseAudio 协议翻译成 PipeWire API。 +WirePlumber 也是客户端,它监听设备、创建节点、配置 profile、建立或移动链路。 + +## 二、启动与配置文件 + +daemon 启动时会解析配置。基础配置文件查找顺序是: + +1. `$XDG_CONFIG_HOME/pipewire/pipewire.conf`(通常是 `~/.config/pipewire/pipewire.conf`) +2. `$sysconfdir/pipewire/pipewire.conf`(通常是 `/etc/pipewire/pipewire.conf`) +3. `$datadir/pipewire/pipewire.conf`(通常是 `/usr/share/pipewire/pipewire.conf`) + +找到第一个就作为基础配置。 +然后按顺序合并 `pipewire.conf.d/` 中的片段: + +1. `$datadir/pipewire/pipewire.conf.d/` +2. `$sysconfdir/pipewire/pipewire.conf.d/` +3. `$XDG_CONFIG_HOME/pipewire/pipewire.conf.d/` + +属性会被覆盖,数组元素会追加。于是用户可以在不改发行版文件的情况下追加模块或覆盖属性。 + +可用环境变量改变查找路径: + +- `PIPEWIRE_CONFIG_DIR` +- `PIPEWIRE_CONFIG_PREFIX` +- `PIPEWIRE_CONFIG_NAME` + +### 配置不是严格 JSON + +PipeWire 配置使用一种更宽松的 JSON 表示: + +- 字符串的引号可以省略; +- 对象键值可以用 `=` 或 `:`; +- 可以有行尾逗号; +- `#` 开始注释。 + +常见 section: + +| section | 作用 | +| --- | --- | +| `context.properties` | context 全局属性 | +| `context.spa-libs` | factory 名称到 SPA 库的映射 | +| `context.modules` | 启动时加载的模块 | +| `context.objects` | 由 factory 创建的对象 | +| `context.exec` | 启动子进程,通常用于 session manager | + +在当前源码树的默认 `pipewire.conf` 中,可以看到 protocol-native、profiler、metadata、client-node、adapter、access、scheduler-v1 等模块。 +不同发行版会修改这些配置,排障时应该直接检查实际文件。 + +## 三、模块如何进入 daemon + +PipeWire 模块是一个 `.so`,入口符号是: + +```c +pipewire__module_init(struct pw_impl_module *module, const char *args); +``` + +模块加载后可以: + +- 注册 factory; +- 创建 global 对象; +- 添加 context 事件监听器; +- 扩展协议; +- 注册导出类型; +- 启动虚拟设备或网络监听。 + +模块不是“客户端插件”。它在加载它的进程里运行,并共享该进程的 `pw_context`。 +客户端不能要求服务端加载任意模块;加载由 daemon 配置决定。 + +当前默认配置会加载的基础模块包括: + +```text +libpipewire-module-rt +libpipewire-module-scheduler-v1 +libpipewire-module-protocol-native +libpipewire-module-profiler +libpipewire-module-metadata +libpipewire-module-spa-device-factory +libpipewire-module-spa-node-factory +libpipewire-module-client-node +libpipewire-module-client-device +libpipewire-module-portal +libpipewire-module-access +libpipewire-module-adapter +libpipewire-module-link-factory +libpipewire-module-session-manager +libpipewire-module-x11-bell +libpipewire-module-jackdbus-detect +``` + +其中有些是条件加载,具体仍以实际配置为准。 + +## 四、原生协议的消息格式 + +默认的 native protocol 使用 Unix domain socket。 +每条消息有 16 字节头: + +```text +0 1 2 3 +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Id | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| opcode | size | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| seq | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| n_fds | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| payload POD | +| ... | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| optional footer POD | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +字段含义: + +| 字段 | 含义 | +| --- | --- | +| `Id` | 目标 proxy/resource ID | +| `opcode` | 接口方法或事件编号 | +| `size` | payload 加 footer 的长度 | +| `seq` | 消息序号,用于请求关联 | +| `n_fds` | 随消息发送的 fd 数量 | +| `payload` | 单个 SPA POD | +| `footer` | 额外控制信息,如 registry generation | + +消息使用本机字节序。fd 通过 `SCM_RIGHTS` 随 Unix socket 传递。 +payload 是 POD,因此复杂参数不需要另写序列化代码。 + +### 连接握手 + +首次连接大致是: + +```text +client server + │──────── open socket ─────────────────►│ + │──────── Core::Hello(version) ────────►│ + │◄─────── Core::Info ───────────────────│ + │──────── Client::UpdateProperties ────►│ + │◄─────── Registry::Global ... ─────────│ +``` + +客户端申请一个 Core 代理,ID 固定为 0;Client 代理通常使用 ID 1。 +服务端根据消息里的 client 属性完成注册,然后触发访问检查。 +如果客户端没有被授予 core 的读权限,它会被挂起,直到 session manager 更新权限。 + +### Sync 与 Done + +Registry 是持续事件流,没有“列表结束”标记。 +客户端通常发送: + +```text +Core::GetRegistry +Core::Sync +``` + +服务端把此前的 global 事件都发送完后返回 `Core::Done`。 +客户端收到 Done 后,才认为当前 registry 快照稳定。 + +## 五、对象创建与 fd 传递 + +创建 node、link、device 等对象时,客户端通常调用 `Core::CreateObject`: + +```text +factory_name +type +version +properties +new_id +``` + +服务端找到 factory,创建对象,并把 `new_id` 对应该客户端的 resource。 +之后客户端在这个 ID 上调用接口方法。 + +媒体数据不走同一路径。服务端通过 `Core::AddMem` 把共享内存 fd 发给客户端: + +```text +AddMem(id, type, fd, flags) +``` + +后续 buffer 参数只引用 memid、offset 和 size。 +客户端映射内存后,把指针写进 IO 区域,process 时直接读写。 + +视频可以通过 DMA-BUF 传递。音频通常使用 memfd 共享环形缓冲。 +这让 socket 只负责控制面,避免每个周期复制整块 PCM/视频帧。 + +## 六、client-node:把处理节点放到客户端 + +`module-client-node` 提供两个导出类型: + +- `PW_TYPE_INTERFACE_Node` +- `SPA_TYPE_INTERFACE_Node` + +客户端可以用 `pw_stream` 或 `pw_filter` 创建节点,再导出给服务端。 +服务端看到的是一个 `pw_impl_node`,但它的处理函数和共享缓冲区指向客户端。 + +协议扩展会传递: + +- node 和 port 信息; +- 参数枚举/设置; +- buffer 和 IO 区域; +- activation 记录; +- eventfd; +- 调度命令和状态。 + +这就是为什么 `spa_node_process()` 可以在客户端进程执行,而调度仍由服务端统一决定。 + +## 七、访问控制模型 + +PipeWire 对每个客户端的每个 global 维护权限位: + +| 位 | 含义 | +| --- | --- | +| R | 能看见对象,能接收事件 | +| W | 能修改对象状态 | +| X | 能调用对象方法 | +| M | 能设置该对象的 metadata | +| L | 能建立到不可见对象的 link | + +`module-access` 在客户端注册时触发检查: + +- 如果解析结果是 `unrestricted`,授予全部权限,客户端继续; +- 否则不授予 core 的 R,客户端挂起; +- session manager 根据策略,为客户端逐对象配置权限; +- 至少给 core 的 R,客户端才能恢复通信。 + +这套设计允许桌面环境限制应用能看到哪些设备、能否改别人的音量、能否移动别人的流。 + +### Flatpak 与 portal + +Flatpak 应用通常不能直接访问宿主机的 PipeWire socket。 +桌面 portal 在沙箱外建立连接,再把连接 fd 交给应用。 + +portal 进程会被标记为 `pipewire.access.portal.is_portal`,它代表的沙箱客户端会带上: + +- `pipewire.access.portal.app_id` +- `pipewire.access.portal.media_roles` + +`module-portal` 通过 DBus 识别 portal 进程,`module-access` 标记连接,session manager 再根据 app id 和 media role 授予有限对象权限。 +例如 Camera portal 只应看到被允许的摄像头节点,而不是整个音频图。 + +## 八、安全边界在哪里 + +PipeWire 的安全不是“socket 权限一刀切”。它在多个层次上工作: + +```text +Unix socket 文件权限 + └── 连接级访问检查 + └── 每客户端、每 global 的权限位 + └── session manager / portal 策略 + └── metadata 与 link 权限 +``` + +需要特别小心的是: + +- 有 W/X 权限的客户端可以操作对象; +- 有 L 权限的客户端可以建立到不可见对象的 link; +- metadata 可以改变 target、默认设备等策略输入; +- session manager 自身通常需要更高权限,因此它的身份验证很关键。 + +PipeWire 默认 socket 在 `$XDG_RUNTIME_DIR` 下,并依赖 Unix 用户边界。 +如果手工把 socket 暴露到网络,安全模型会完全不同。 + +## 九、配置修改的实践建议 + +1. **优先用 `*.conf.d` 片段**,不要直接改 `/usr/share` 下的发行版文件。 +2. **先确认实际加载路径**:`pw-config` 可以查看配置和搜索路径。 +3. **一次只改一个变量**,重启对应 user service,再看 `pw-dump` 或 `pw-mon`。 +4. **区分 daemon、client、pulse、jack 配置**:它们不是同一个文件。 +5. **不要把服务端模块写进客户端配置**:模块只能加载到自己的进程。 +6. **权限问题先看 `pw-dump` 里客户端和对象的权限字段**,不要只猜配置。 + +## 十、小结 + +守护进程和协议层可以概括为: + +```text +配置 → 加载模块 → 创建 global +socket → Hello → Registry → Bind +method/event → POD + fd +access → 权限位 → session manager/portal +``` + +下一篇开始做“全目录索引”:把这棵树里的 50 个 `src/modules` 模块逐一归类,说明它们在真实系统里扮演什么角色。 diff --git a/source/_posts/pipewire-architecture-05-module-catalog.md b/source/_posts/pipewire-architecture-05-module-catalog.md new file mode 100644 index 0000000..550e1a5 --- /dev/null +++ b/source/_posts/pipewire-architecture-05-module-catalog.md @@ -0,0 +1,228 @@ +--- +title: PipeWire 模块全目录:50 个 src/modules 模块分别做什么 +date: 2026-10-23 14:45:00 +tags: [PipeWire, 模块, 源码地图, Linux音频] +categories: [教程] +--- + +PipeWire 的 `src/modules` 目录里,当前快照有 50 个 `module-*.c`。 +这些模块不是“可选功能列表”那么简单,它们决定了 PipeWire 能不能提供协议、能不能创建虚拟设备、能不能连接其他服务器。 + +这篇文章按职责把 50 个模块全部过一遍。 +先说明:**存在源文件不等于当前系统已经构建或加载**。很多模块依赖 ALSA、BlueZ、Avahi、FFmpeg、ROC、X11 等外部库,配置中也可能只在特定场景加载。 + +--- + +## 一、怎么理解 PipeWire 模块 + +模块是加载进某个 PipeWire 进程的共享库,入口是: + +```c +pipewire__module_init(struct pw_impl_module *module, const char *args) +``` + +模块可以: + +- 创建 factory 或 global 对象; +- 给 context 添加 listener; +- 注册协议扩展; +- 注册导出类型; +- 创建虚拟 sink/source; +- 监听 DBus、mDNS、网络端口等外部事件。 + +模块和 SPA 插件不是一回事: + +| 层级 | 例子 | 运行位置 | +| --- | --- | --- | +| PipeWire module | `module-protocol-native` | 加载它的进程 | +| SPA plugin | `spa-alsa`、`spa-videoconvert` | 由模块或应用按需加载 | +| Application API | `pw_stream`、`pw_filter` | 客户端进程 | + +下面的目录按“基础管道、虚拟处理、设备、网络”分组。 + +--- + +## 二、基础管道与协议模块 + +| 模块 | 作用 | +| --- | --- | +| `module-access` | 客户端首次连接时做访问检查,决定授予 unrestricted 还是挂起等待 session manager | +| `module-adapter` | 把 SPA node 包装成可对接统一格式的 PipeWire 节点,负责端口、格式转换和 DSP/passthrough | +| `module-client-device` | 允许客户端把本地 `spa_device` 导出到服务端,服务端以 device 资源代理它 | +| `module-client-node` | 允许客户端把处理节点导出到服务端,是 `pw_stream`、`pw_filter` 和 JACK 客户端的底层通道 | +| `module-link-factory` | 提供 `link-factory`,让客户端按 node/port 属性创建 Link | +| `module-metadata` | 创建 metadata factory,并支持客户端导出 metadata 存储 | +| `module-profiler` | 提供 Profiler 接口,为 `pw-top`、`pw-profiler` 收集图状态和性能数据 | +| `module-protocol-native` | 实现默认的 Unix socket 原生协议,包括消息、fd 传递和协议扩展 | +| `module-protocol-pulse` | 在 PipeWire 上实现 PulseAudio 服务端协议,兼容 `pactl`、`pavucontrol` 等工具 | +| `module-protocol-simple` | 提供面向 simple protocol player 和 Snapcast 等场景的简单音频 socket 协议 | +| `module-rt` | 给数据线程设置实时优先级、nice、rlimit、RTKit/portal 回退和 uclamp 等调度属性 | +| `module-scheduler-v1` | 当前 1.7 开发树的调度实现模块,负责 runnable、节点组、driver 选择与图重算;文档标注 Since 1.7.0 | +| `module-session-manager` | 提供 session、endpoint、endpoint-stream、endpoint-link 等会话管理扩展对象;文档说明目前并未被主流 session manager 主动使用 | +| `module-spa-device` | 加载并管理一个 SPA device,把它映射成 PipeWire device | +| `module-spa-device-factory` | 提供创建 SPA device 的 factory | +| `module-spa-node` | 加载并管理一个 SPA node,把它映射成 PipeWire node | +| `module-spa-node-factory` | 提供加载 SPA node 的 factory | +| `module-portal` | 通过 DBus 识别 XDG portal 进程,给 portal 管理的客户端打 access 标记 | + + +### protocol-pulse 为什么是 module + +`pipewire-pulse` 可执行文件启动一个 PipeWire context,再加载 `module-protocol-pulse`。 +模块负责 PulseAudio 协议解析、命令分派、stream/sink/source 映射、volume 和 sample cache 等。 +对内核或 PipeWire 核心来说,它只是一个模块;对 PulseAudio 客户端来说,它看起来像一个完整的服务端。 + +### scheduler-v1 的定位 + +老版本调度逻辑内嵌在 core 中,1.7.0 引入 `module-scheduler-v1` 把调度行为模块化。 +它维护 node runnable、组、driver 选择和 graph 重算。源码中的 passive mode 判断与调度的设计文档一致。 +如果你读的是更老的 PipeWire,模块列表和调度入口可能不同。 + +--- + +## 三、虚拟设备与处理模块 + +| 模块 | 作用 | +| --- | --- | +| `module-combine-stream` | 创建一个虚拟 sink/source,把多个 sink、source 或 monitor 流合并;支持延迟补偿和按需创建 stream | +| `module-echo-cancel` | 创建 echo-cancel capture/playback 节点,用参考信号抵消扬声器回授 | +| `module-example-filter` | 自定义 filter 的示例模块,展示 stream 输入输出和处理回调 | +| `module-example-sink` | 自定义 sink 的示例模块,通常不安装,适合学习模块结构 | +| `module-example-source` | 自定义 source 的示例模块,通常不安装,适合学习模块结构 | +| `module-fallback-sink` | 没有其他 sink 时动态出现一个兜底 sink,主要用于 PulseAudio 兼容场景 | +| `module-filter-chain` | 用 LADSPA、LV2、builtin、SOFA、EBUR128、FFmpeg 等构建任意音频滤波图,可作为虚拟设备或插入链路 | +| `module-loopback` | 把 capture stream 的输出原样送给 playback stream,用于虚拟设备、桥接和声道重映射 | +| `module-parametric-equalizer` | 读取 AutoEQ/Squiglink 生成的参数均衡配置,转换为可用的 filter chain | +| `module-pipe-tunnel` | 把 PipeWire 音频与 Unix pipe 双向桥接 | +| `module-pulse-tunnel` | 把 PipeWire 音频隧道到远端 PulseAudio 或 pipewire-pulse 连接,通常与 zeroconf 发现模块配合 | + +这些模块的共同点是:**它们用 PipeWire 自己的 stream/node 能力构造新对象**,通常不直接打开硬件。 + +### filter-chain 的关键结构 + +```text +capture stream → filter graph nodes → playback stream + │ + ├── LADSPA / LV2 + ├── builtin convolver / delay / mixer / copy + ├── SOFA / EBUR128 + └── FFmpeg filtergraph +``` + +filter-chain 的配置本身是一个小图:节点、节点之间的 links、对外 inputs/outputs、控制参数。 +它适合做耳机校正、房间校正、动态处理和实验性 DSP。 + +### loopback 与 combine-stream 的区别 + +- loopback 是一条流量通道:capture → playback。 +- combine-stream 是多对一/一对多的聚合:多个 sink/source 合并,或一个虚拟 sink 分发到多个设备。 + +两者都可能被 session manager 当作虚拟设备管理,但图结构和用途不同。 + +--- + +## 四、硬件与外部服务模块 + +| 模块 | 作用 | +| --- | --- | +| `module-avb` | AVB/AVDECC/Milan 音频网络支持,包含 ADP、ACMP、AECP、MSRP、MVRP、gPTP、stream 等实现 | +| `module-ffado-driver` | 通过 libffado 提供 FireWire 音频 source/sink/duplex 驱动 | +| `module-jack-tunnel` | 把 PipeWire 音频隧道到外部 JACK server;可配置 sink/source/duplex 和 MIDI 端口 | +| `module-jackdbus-detect` | 监听 jackdbus 启动,自动加载并配置 jack-tunnel | +| `module-x11-bell` | 监听 X11 bell 事件,通过 libcanberra 播放系统提示音 | + +这些模块直接和外部设备或桌面服务打交道: + +- `avb` 面向专业音视频桥接网络; +- `ffado` 面向 FireWire 音频接口; +- `jack-tunnel` / `jackdbus-detect` 用于 PipeWire 与真实 JACK 并存; +- `x11-bell` 是桌面集成功能,不属于核心音频路径。 + +`module-avb` 的构建还要求 Linux 平台;其协议栈比普通虚拟设备复杂得多,阅读时应先看 `module-avb` 子目录的 ADP/ACMP/AECP/MVRP 等文件。 + +--- + +## 五、网络流与发现模块 + +| 模块 | 作用 | +| --- | --- | +| `module-netjack2-driver` | 作为 netjack2 driver,与 netjack2 manager 建立低延迟网络音频通道 | +| `module-netjack2-manager` | 监听 netjack2 driver,协商通道数量并管理会话 | +| `module-raop-discover` | 通过 zeroconf 发现 AirPlay/RAOP 接收器,并按规则加载 raop-sink | +| `module-raop-sink` | 创建 AirPlay/RAOP sink,向远端设备发送音频 | +| `module-roc-sink` | 把音频通过 ROC 发送到预配置接收端 | +| `module-roc-source` | 从 ROC sender 接收音频并作为 PipeWire source | +| `module-rtp-sap` | 通过 SAP 公告 RTP 流,并按规则创建 RTP source | +| `module-rtp-session` | 创建通过 mDNS/Avahi 公告的 RTP 媒体会话,兼容 Apple MIDI 的 MIDI 场景 | +| `module-rtp-sink` | 创建 PipeWire sink,把音频打成 RTP 包发出 | +| `module-rtp-source` | 创建 PipeWire source,接收 RTP 音频/MIDI | +| `module-sendspin-recv` | 通过 sendspin 协议接收音频/控制数据,默认监听 8928 | +| `module-sendspin-send` | 通过 sendspin 协议发送音频,默认监听 8927 | +| `module-snapcast-discover` | 通过 zeroconf 发现 Snapcast server,并加载 simple protocol sink | +| `module-vban-recv` | 接收 VBAN 音频/MIDI,默认端口 6980 | +| `module-vban-send` | 发送 VBAN 音频/MIDI | +| `module-zeroconf-discover` | 用 zeroconf 发现远端 PulseAudio/PipeWire,并自动加载 pulse-tunnel | + +这张表里最容易混淆的是“发现”和“传输”: + +- `*-discover` 负责找服务、匹配规则、加载 tunnel/sink; +- `*-sink` / `*-source` 负责真正的媒体传输; +- `netjack2-manager` 和 `netjack2-driver` 是两端角色,不是同一个进程里的简单客户端/服务端。 + +### RTP 系列为什么有 4 个模块 + +- `rtp-sink`、`rtp-source`:最直接的发送和接收端; +- `rtp-sap`:负责 SAP 公告和动态建立接收端; +- `rtp-session`:负责 mDNS 公告和会话级发现,偏 Apple MIDI 兼容。 + +RTP 模块共享 `module-rtp/stream.c` 的 ring buffer、buffer mode、时间戳和线程模型。 +阅读顺序建议先看 sink/source,再看 sap/session。 + +### AirPlay、ROC、VBAN、Sendspin、Snapcast 的取舍 + +| 方案 | 适用场景 | +| --- | --- | +| RAOP | AirPlay 接收器,兼容 Apple 生态 | +| ROC | 低延迟、带纠错的实时音频网络 | +| VBAN | VB-Audio 生态和简单 UDP 音频 | +| RTP/SAP | 标准组播和局域网广播 | +| Sendspin | 基于 WebSocket 的发送/接收模型 | +| Snapcast | 多房间同步播放生态 | + +它们各有自己的发现、编码和时钟处理,不能简单替换。 + +--- + +## 六、如何确认某个模块是否可用 + +不要只看源码文件。按顺序检查: + +```bash +# 1. 查二进制模块是否存在 +find /usr/lib* -name 'libpipewire-module-*.so' | sort + +# 2. 查实际配置加载了哪些模块 +pw-config -n pipewire.conf + +# 3. 查运行时对象和 factory +pw-cli ls Module +pw-cli ls Factory + +# 4. 查依赖是否满足 +ldd /path/to/libpipewire-module-raop-sink.so +``` + +如果模块存在但没有加载,问题通常在配置或依赖; +如果加载了但没有对象,可能是 session manager 没创建、规则没匹配或设备未出现。 + +## 七、小结 + +50 个模块可以记为五类: + +1. **基础管道**:access、adapter、client-node/device、factory、metadata、profiler、协议、scheduler。 +2. **虚拟处理**:combine、echo-cancel、filter-chain、loopback、例程。 +3. **硬件/桌面**:AVB、FFADO、JACK tunnel、X11 bell。 +4. **网络传输**:RTP、RAOP、ROC、VBAN、Sendspin、NetJack2。 +5. **发现与兼容**:zeroconf、snapcast、PulseAudio、simple protocol。 + +下一篇进入更底层的 19 个 SPA 插件目录,看这些模块和节点最终调用了哪些 factory 和硬件实现。 diff --git a/source/_posts/pipewire-architecture-06-spa-plugin-catalog.md b/source/_posts/pipewire-architecture-06-spa-plugin-catalog.md new file mode 100644 index 0000000..77fa89d --- /dev/null +++ b/source/_posts/pipewire-architecture-06-spa-plugin-catalog.md @@ -0,0 +1,350 @@ +--- +title: PipeWire SPA 插件全目录:19 个底层插件目录与 factory 速查 +date: 2026-10-23 15:00:00 +tags: [PipeWire, SPA, 插件, ALSA, BlueZ, 源码地图] +categories: [教程] +--- + +PipeWire module 决定“图里有哪些对象”,SPA plugin 决定“这些对象怎么处理媒体、怎么访问设备”。 +`src/modules` 里有 50 个模块,`spa/plugins` 下则有 19 个插件目录,覆盖 ALSA、V4L2、Bluetooth、音频格式转换、混音、测试源、Vulkan 和宿主支持。 + +这篇文章按目录逐个说明它们提供的 factory 和适用场景。 +和模块目录一样,**源码目录存在不代表本机已经构建**;很多插件受编译选项和系统依赖控制。 + +--- + +## 一、先看 SPA 插件的共同结构 + +每个插件至少导出: + +```c +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index); +``` + +典型目录结构: + +```text +spa/plugins// +├── meson.build +├── plugin.c # factory 枚举入口 +├── .c # 具体 factory / interface 实现 +└── test-*.c # 单元测试或基准 +``` + +安装位置通常是 `spa-0.2//libspa-.so`。 +可以用下面的命令检查: + +```bash +export SPA_PLUGIN_DIR=$(pkg-config --variable plugindir libspa-0.2) +find "$SPA_PLUGIN_DIR" -name 'libspa-*.so' | sort +spa-inspect "$SPA_PLUGIN_DIR/support/libspa-support.so" +``` + +`spa-inspect` 会列出 factory、接口和版本,是读插件前最快的入口工具。 + +--- + +## 二、音频设备与编码插件 + +### 1. `alsa` + +ALSA 后端插件,输出 `spa-alsa`。它包含多个 factory: + +| Factory / 组件 | 作用 | +| --- | --- | +| ACP device | 基于 ALSA Card Profile / UCM 的设备抽象,管理 profile、route 和端口 | +| PCM device | 一个 ALSA PCM 设备的 device 抽象 | +| PCM sink | 播放方向 SPA node,把数据写入 PCM | +| PCM source | 采集方向 SPA node,从 PCM 读取数据 | +| ALSA sequencer bridge | 把 ALSA sequencer MIDI 端口映射为 control 端口 | +| ALSA udev | 枚举和监听 ALSA 设备变化 | +| Compress offload | 对支持压缩卸载的 ALSA 设备提供 sink/device | + +ACP 是理解现代声卡 profile 的关键。它把“硬件支持哪些 PCM、route、UCM verb”抽象成 device profile 和 port。 +session manager 选择 profile,PipeWire 再创建对应 node。 + +### 2. `bluez5` + +Bluetooth 音频与 MIDI 插件,输出 `spa-bluez5` 和一组编解码插件。 +主工厂包括: + +- BlueZ DBus 设备; +- BlueZ device; +- media sink / media source; +- SCO sink / source; +- A2DP sink / source; +- MIDI enum / MIDI node。 + +编解码器单独构建为 `spa-codec-bluez5-*`,例如 SBC、FastStream、AAC、aptX、LDAC、LHDC、LC3、LC3plus、Opus、G722、CVSD、mSBC 等。 +具体哪些可用取决于编译依赖和耳机支持。 + +BlueZ 插件负责蓝牙协议侧的 profile、codec、SCO/A2DP 传输和 MIDI,但设备发现与路由仍由 session manager 协调。 + +### 3. `v4l2` + +V4L2 摄像头/视频设备插件,输出 `spa-v4l2`: + +| 组件 | 作用 | +| --- | --- | +| device | 打开 `/dev/video*`,管理格式和缓冲区 | +| source | 视频采集 node | +| udev | 监听 V4L2 设备变化 | + +PipeWire 的 V4L2 后端和 `pipewire-v4l2` 兼容库不是一回事: +前者让 PipeWire 使用摄像头;后者让传统 V4L2 应用把 PipeWire 当作 V4L2 设备。 + +### 4. `libcamera` + +libcamera 后端插件,源码是 C++ 文件,输出 `spa-libcamera`。 +它通过 libcamera 管理摄像头设备和 source,适合需要 libcamera 管线控制的平台。 +目录中的文件包括 device、manager、source 等实现。 + +### 5. `avb` + +AVB/AVDECC 的 SPA 设备插件,提供 AVB PCM source/sink 和 device 抽象。 +它和 `module-avb` 的协议栈配合,面向专业音视频桥接网络。 +启用条件包含 Linux 平台和 AVB 编译选项。 + +### 6. `jack` + +JACK 后端插件,输出 `spa-jack`: + +- JACK device; +- JACK source; +- JACK sink。 + +它的用途是让 PipeWire 作为一个 JACK 客户端连接真实 JACK server。 +这和 `pipewire-jack` 的方向相反:后者是让 JACK 应用连接到 PipeWire。 + +--- + +## 三、处理与转换插件 + +### 7. `audioconvert` + +输出 `spa-audioconvert`,核心工厂: + +- `audioadapter`:设备原生格式与 DSP/passthrough 端口之间的适配器; +- `audioconvert`:采样格式转换、声道映射、重采样、音量/峰值处理等。 + +该目录包含大量 SIMD 变体: + +```text +fmt-ops-c / sse2 / ssse3 / sse41 / avx2 / neon / rvv +channelmix-ops-c / sse / avx +resample-native-c / sse / ssse3 / avx2 / neon +volume-ops-c / sse +``` + +运行时会根据 CPU 能力选择实现。 +这也是为什么同一个 `audioconvert` 在不同机器上的性能路径可能不同。 + +### 8. `audiomixer` + +输出 `spa-audiomixer`: + +- `audiomixer`:把多个输入混成一个输出; +- `mixer-dsp`:DSP 混音节点。 + +同样包含 C、SSE、SSE2、AVX2 等混音实现。 +音频图中的多路应用混音通常会落到这里。 + +### 9. `aec` + +回声消除插件: + +- `spa-aec-null`:空实现路径; +- `spa-aec-webrtc`:WebRTC 回声消除实现,受 `echo-cancel-webrtc` 构建选项控制。 + +它服务于 `module-echo-cancel`,把麦克风信号和参考播放信号做自适应抵消。 + +### 10. `filter-graph` + +滤波器图插件,输出 `spa-filter-graph`。 +核心是通用的音频 DSP 图,支持内置节点和外部插件: + +| 插件库 | 能力 | +| --- | --- | +| `spa-filter-graph-plugin-builtin` | copy、mixer、delay、convolver 等内置节点 | +| `spa-filter-graph-plugin-ladspa` | LADSPA 插件 | +| `spa-filter-graph-plugin-lv2` | LV2 插件 | +| `spa-filter-graph-plugin-sofa` | SOFA/HRTF 空间音频 | +| `spa-filter-graph-plugin-ebur128` | EBU R128 响度测量 | +| `spa-filter-graph-plugin-ffmpeg` | FFmpeg filtergraph | +| `spa-filter-graph-plugin-onnx` | ONNX 推理节点 | + +`module-filter-chain` 是它最常见的上层使用者。 + +### 11. `control` + +输出 `spa-control`,提供 control mixer。 +它处理 `application/control` 媒体类型的控制流,MIDI 也是其中一种控制消息。 +与音频 PCM 不同,control 端口传输的是带时间偏移的事件序列。 + +### 12. `ffmpeg` + +输出 `spa-ffmpeg`,提供编码/解码节点,源码分为 `ffmpeg-dec.c` 和 `ffmpeg-enc.c`。 +它可用于压缩视频或音频格式转换,默认构建选项通常关闭,需要显式启用。 + +### 13. `videoconvert` + +输出 `spa-videoconvert`: + +- `videoadapter`:视频设备与 DSP/passthrough 端口适配; +- `videoconvert_dummy`:测试用转换实现; +- `videoconvert_ffmpeg`:基于 FFmpeg 的像素格式转换。 + +视频格式、色彩空间、stride 和 DMA-BUF 布局都在这一层处理。 + +### 14. `volume` + +输出 `spa-volume`,提供 legacy volume 插件。 +现代音频链路更多通过 `audioconvert`/`audiomixer` 里的 volume 处理完成; +这个插件主要保留给兼容或特定配置。 + +--- + +## 四、测试、源与 GPU 插件 + +### 15. `audiotestsrc` + +输出 `spa-audiotestsrc`,可以生成测试音。 +在排障时,它比真实应用更容易控制,适合验证图、格式和延迟。 + +### 16. `videotestsrc` + +输出 `spa-videotestsrc`,生成测试图样。 +它和 audiotestsrc 一样,是验证视频链路和格式协商的基线工具。 + +### 17. `test` + +输出 `spa-test`,包含 `fakesrc` 和 `fakesink`。 +这些不是给普通用户的功能,而是单元测试和开发调试用的可控节点。 + +### 18. `vulkan` + +输出 `spa-vulkan`,包含: + +- Vulkan compute source; +- Vulkan compute filter; +- Vulkan blit filter; +- Vulkan DSP blit filter。 + +它把 GPU 计算和图像拷贝接入视频图。源码中同时包含 DMA-BUF 的 Linux 与非 Linux 回退路径。 +默认通常不启用,需要 Vulkan 依赖和构建选项。 + +--- + +## 五、宿主支持插件 + +### 19. `support` + +输出 `spa-support`,是 SPA 插件运行所需的基础能力集合: + +| Factory | 用途 | +| --- | --- | +| `support.log` | 日志输出 | +| `support.system` | 系统调用、内存、文件等封装 | +| `support.cpu` | CPU 特性检测 | +| `support.loop` | poll/event loop | +| `support.node-driver` | 节点驱动辅助 | +| `support.null-audio-sink` | 空音频 sink 支持 | + +此外还构建: + +- `spa-dbus`:DBus 支持; +- `spa-journal`:systemd journal 日志; +- `spa-evl`:EVL 实时系统支持,受构建选项控制。 + +`support` 是最适合初学者读的插件:factory 结构清晰,接口不复杂,也没有大型硬件协议栈。 + +--- + +## 六、插件与模块的对应关系 + +把两层放在一起看,常见组合是: + +```text +ALSA 声卡 + module-spa-device / adapter + → spa-alsa(ACP/PCM) + → spa-audioconvert(格式/DSP) + → graph links + +蓝牙耳机 + WirePlumber / module-spa-device + → spa-bluez5 + codec + → graph links + +摄像头 + WirePlumber / module-spa-device + → spa-v4l2 或 spa-libcamera + → graph links + +滤波链 + module-filter-chain + → spa-filter-graph + → LADSPA / LV2 / builtin / FFmpeg / ONNX +``` + +注意:模块名、SPA factory 名和实际安装库名不同。 +排障时要分层确认: + +1. 配置有没有加载模块; +2. 模块能不能找到 SPA 库; +3. SPA 库能不能枚举到目标 factory; +4. factory 初始化是否因设备/依赖失败; +5. 对象是否被 session manager 选中并连接。 + +## 七、如何自己检查一个插件 + +以 audioadapter 为例: + +```bash +export SPA_PLUGIN_DIR=$(pkg-config --variable plugindir libspa-0.2) + +spa-inspect "$SPA_PLUGIN_DIR/audioconvert/libspa-audioconvert.so" +spa-inspect "$SPA_PLUGIN_DIR/alsa/libspa-alsa.so" +``` + +看输出时重点关注: + +- factory 名称; +- factory 版本; +- 提供的 interface 类型; +- 初始化需要的 support; +- 是否有可实例化对象。 + +如果 `spa-inspect` 能看到 factory,但 PipeWire 运行时创建失败,问题通常在设备权限、格式、参数或外部服务,而不在插件加载本身。 + +## 八、读插件源码的推荐顺序 + +1. `spa/plugins/support/plugin.c`:最小 factory 枚举。 +2. `spa/plugins/audiotestsrc/audiotestsrc.c`:生成数据的 node。 +3. `spa/plugins/audiomixer/audiomixer.c`:多输入处理。 +4. `spa/plugins/audioconvert/audioadapter.c`:适配器和格式协商。 +5. `spa/plugins/alsa/alsa-pcm.c`:真实设备的读写和 poll。 +6. `spa/plugins/v4l2/v4l2-source.c`:视频 buffer 和 mmap。 +7. `spa/plugins/bluez5/bluez5-device.c`:外部协议集成。 + +每读一个插件,都先找三件事: + +```text +factory 定义 +spa_node_methods(或对应 interface methods) +set_io / process / enum_params +``` + +这三处通常足够解释插件的核心行为。 + +## 九、小结 + +19 个 SPA 插件目录大致分为四类: + +1. **硬件与协议后端**:alsa、bluez5、v4l2、libcamera、avb、jack。 +2. **处理与转换**:audioconvert、audiomixer、aec、filter-graph、control、ffmpeg、videoconvert、volume。 +3. **测试与 GPU**:audiotestsrc、videotestsrc、test、vulkan。 +4. **宿主支持**:support。 + +下一篇把 PipeWire daemon 与桌面环境之间的“策略层”补齐:WirePlumber 如何管理设备,Pulse/JACK/ALSA/V4L2 兼容层如何接入,以及音频、视频、MIDI、网络流如何汇合。 + diff --git a/source/_posts/pipewire-architecture-07-session-integrations.md b/source/_posts/pipewire-architecture-07-session-integrations.md new file mode 100644 index 0000000..672a649 --- /dev/null +++ b/source/_posts/pipewire-architecture-07-session-integrations.md @@ -0,0 +1,327 @@ +--- +title: PipeWire 会话管理与兼容层:WirePlumber、Pulse、JACK、ALSA、V4L2 如何协同 +date: 2026-10-23 15:15:00 +tags: [PipeWire, WirePlumber, PulseAudio, JACK, ALSA, V4L2] +categories: [教程] +--- + +PipeWire 单独运行时,更像一个“媒体图内核”:它能把 node、port、link 调度起来,但不会自动把所有桌面设备安排得井井有条。 + +真正让系统“开机就能用”的,是外面那圈组件: + +- WirePlumber 负责设备发现、默认设备、策略和恢复; +- `pipewire-pulse` 负责 PulseAudio 协议兼容; +- `pipewire-jack` 负责 JACK API 兼容; +- `pipewire-alsa` 负责 ALSA PCM 兼容; +- `pipewire-v4l2` 负责 V4L2 兼容; +- GStreamer 元素和桌面 portal 负责应用侧接入。 + +这篇文章把机制层和策略层拼起来。 + +--- + +## 一、机制与策略的分工 + +| 层 | 组件 | 负责什么 | +| --- | --- | --- | +| 机制 | PipeWire daemon | 对象图、协议、设备节点、调度、共享内存 | +| 策略 | WirePlumber | 发现设备、选 profile、创建节点、决定连谁、保存状态 | +| 兼容 | pipewire-pulse/jack/alsa/v4l2 | 让旧 API 应用接入 PipeWire | +| 应用 | 播放器、浏览器、DAW、会议软件 | 创建 stream 或使用兼容 API | + +PipeWire 自己的文档把 session manager 的职责写得很清楚:客户端权限、设备管理、endpoint 分组和策略都不属于核心 daemon。 +所以“PipeWire 没声音”并不等于 PipeWire 进程本身有问题,可能是 WirePlumber 没起、配置没匹配、设备被占用或 profile 选错。 + +## 二、WirePlumber 的工作 + +WirePlumber 是一个独立进程,通过 PipeWire 客户端 API 观察和操作图。 +它通常负责: + +1. 监听 udev、DBus、BlueZ、libcamera 等事件; +2. 为新设备创建 SPA device 和 adapter; +3. 选择 device profile; +4. 创建 sink/source node 和端口; +5. 设置默认设备; +6. 把应用 stream 链接到目标设备; +7. 保存音量、静音、profile、默认路由; +8. 执行权限和策略规则; +9. 处理设备热插拔和错误恢复。 + +它并不“代替 PipeWire 播放音频”。它只是创建和连接节点,真正的数据搬运仍由 PipeWire 的图调度完成。 + +常见的用户可见对象: + +| WirePlumber 概念 | PipeWire 表现 | +| --- | --- | +| Default sink/source | metadata 中的默认目标 | +| Device | `pw_device` 及 profile/route 参数 | +| Node | `pw_node` | +| Stream | 应用导出的 client-node | +| Link | `pw_link` | +| Route | 设备端口/连接选择 | +| Profile | 设备当前工作模式 | + +`wpctl` 是 WirePlumber 的命令行工具,`pavucontrol` 通过 Pulse 兼容层操作同一套对象。 + +## 三、ALSA 设备的完整路径 + +一块声卡从硬件到应用的典型路径: + +```text +内核 ALSA driver + │ /dev/snd/* + ▼ +udev / WirePlumber monitor + │ 发现 card + ▼ +SPA ALSA ACP/PCM device + │ 枚举 profiles / routes / nodes + ▼ +module-spa-device + module-adapter + │ 创建 PipeWire device/node/ports + ▼ +WirePlumber policy + │ 选 profile、设置默认设备、连接 stream + ▼ +应用 stream +``` + +### ACP 与 PCM 的区别 + +- **ACP**:基于 ALSA Card Profile / UCM,能表达声卡 profile、route、端口组合,现代桌面优先使用。 +- **PCM**:更直接的 ALSA PCM 抽象,通常退化为 on/off 两种 profile 或简单设备。 + +UCM 能描述“耳机插入时切到哪个 route”“扬声器和耳机是否互斥”等硬件关系。 +没有 UCM 时,fallback 会简单很多,profile 选择也更粗糙。 + +### adapter 在中间做什么 + +设备原生格式可能是 S32、多声道、特殊 rate。 +adapter 对外提供: + +- DSP 模式:每声道一个 float32 端口; +- passthrough 模式:保留设备原生格式; +- 格式转换、重采样、声道映射; +- 音量、静音、峰值等处理。 + +因此应用的 44.1kHz 立体声和设备的 48kHz 多声道可以被桥接,而不用应用自己处理硬件格式。 + +## 四、PulseAudio 兼容层 + +`pipewire-pulse` 运行 PulseAudio 服务端协议。 +Pulse 客户端仍然链接 `libpulse`,只是连接到的服务端换成了 PipeWire。 + +验证方式: + +```bash +pactl info +``` + +输出中应能看到类似: + +```text +Server Name: PulseAudio (on PipeWire ...) +``` + +常见工具: + +| 工具 | 功能 | +| --- | --- | +| `pactl` | 查询/修改 sink、source、stream、module | +| `pavucontrol` | GUI 控制音量、设备、流 | +| `paplay` / `parecord` | Pulse 播放/录音 | +| `pamon` | 监听 PCM | + +`module-protocol-pulse` 会维护 Pulse 对象到 PipeWire 对象的映射: + +- Pulse sink/source ↔ PipeWire node/endpoint; +- Pulse sink-input/source-output ↔ 应用 stream; +- Pulse card ↔ device; +- Pulse port ↔ route/target; +- Pulse module ↔ 内部命令或 PipeWire 模块。 + +`pipewire-pulse` 不是第二个音频服务器。它只是另一个 PipeWire 客户端,不能和真正的 PulseAudio 服务端同时占用同一 socket。 + +## 五、JACK 兼容层 + +JACK 有两个不同方向: + +```text +方向 A:JACK 应用 → PipeWire + 应用链接 libjack + → pipewire-jack 替代库 + → PipeWire client-node + +方向 B:PipeWire → 真实 JACK server + module-jack-tunnel / spa-jack + → 连接 jackd +``` + +`pw-jack` 通过修改 `LD_LIBRARY_PATH`,让应用加载 PipeWire 提供的 JACK 库: + +```bash +pw-jack ardour +``` + +如果发行版已经完全用 PipeWire 替代 JACK 库,可以不加 `pw-jack`。 + +JACK 的映射关系: + +| JACK | PipeWire | +| --- | --- | +| client | client + node | +| port | port | +| connection | link | +| buffer size | quantum | +| transport | sync group / timebase | + +JACK 应用通常要求固定 buffer 和低延迟。 +PipeWire 会尽量按 `PIPEWIRE_LATENCY` 或 JACK 请求配置图,但实际值还受设备 driver 和 session manager 影响。 + +## 六、ALSA 兼容层 + +`pipewire-alsa` 安装一个 ALSA PCM 插件,让传统 ALSA 应用使用 `pipewire:` 设备。 +这和 PipeWire 的 SPA ALSA 后端是两个方向: + +| 名称 | 方向 | 作用 | +| --- | --- | --- | +| `spa-alsa` | PipeWire → ALSA | 让 PipeWire 打开真实声卡 | +| `pipewire-alsa` | ALSA 应用 → PipeWire | 让 ALSA 应用把 PipeWire 当声卡 | + +安装后可以检查: + +```bash +aplay -L | grep pipewire +``` + +如果 `pipewire-alsa` 没装,PipeWire 仍然能管理声卡,只是 ALSA 应用不会自动走 PipeWire。 + +## 七、V4L2 与摄像头 portal + +V4L2 也有两个方向: + +- `spa-v4l2`:PipeWire 采集真实摄像头; +- `pipewire-v4l2`:把 PipeWire 节点伪装成 V4L2 设备,供传统应用使用。 + +摄像头权限通常通过 XDG Desktop Portal 处理。 +沙箱应用不直接看到 `/dev/video*`,而是向 portal 请求 Camera 会话;portal 建立一个受限 PipeWire 连接,并把 fd 交给应用。 +session manager 根据 `media_roles` 和 app id 授予相应节点权限。 + +这条链路把“设备权限”“PipeWire 对象权限”“应用沙箱”分开了: +portal 负责跨沙箱授权,PipeWire 负责对象级访问,session manager 负责策略落地。 + +## 八、GStreamer 接入 + +PipeWire 提供 GStreamer 元素: + +```bash +gst-launch-1.0 pipewiresrc ! videoconvert ! autovideosink +gst-launch-1.0 audiotestsrc ! pipewiresink +``` + +以及设备 provider,让 `gst-device-monitor-1.0` 能看到 PipeWire 设备。 +对 GStreamer 应用来说,PipeWire 是一个 source/sink 后端;底层仍然是 client-node 和共享缓冲区。 + +## 九、MIDI 如何走 + +MIDI 在 PipeWire 中不是 PCM,而是 control 端口上的事件序列: + +- 媒体类型通常是 `application/control`; +- 每个事件带样本偏移,能与音频对齐; +- 常见控制类型包括 MIDI、OSC、UMP; +- 长 SysEx 可以分片传输。 + +ALSA sequencer 通过 `spa-alsa` 的 seq bridge 映射。 +JACK MIDI 通过 pipewire-jack 做 1:1 事件转换。 +PipeWire daemon 本身对 MIDI 没有特殊“MIDI 对象”,它只看 control 端口的格式和事件。 + +源码文档中提到,1.7 开发树重新把 MIDI1 作为默认偏好,同时仍支持 UMP producer;消费者未显式开启 UMP 时会做转换。 +这类版本行为要以你实际运行的版本为准。 + +## 十、网络与远端设备 + +PipeWire 的网络能力不是单一协议,而是一组模块: + +| 场景 | 模块 | +| --- | --- | +| RTP 组播/单播 | `module-rtp-sink/source/sap/session` | +| AirPlay | `module-raop-discover/raop-sink` | +| ROC | `module-roc-sink/source` | +| VBAN | `module-vban-send/recv` | +| Sendspin | `module-sendspin-send/recv` | +| Snapcast | `module-snapcast-discover` + simple protocol | +| NetJack2 | `module-netjack2-driver/manager` | +| 远端 Pulse | `module-pulse-tunnel` + `module-zeroconf-discover` | + +网络路径和本地路径的本质区别是: + +- 时钟不同步,需要 latency 和重采样/漂移补偿; +- 编码可能是有损的; +- 发现依赖 mDNS/SAP; +- 丢包和抖动会直接变成缓冲或 xrun。 + +因此网络 sink/source 通常配置更大的 latency,并通过 `sess.*`、`rtp.*` 参数控制包时长、缓冲和会话行为。 + +## 十一、一条典型桌面链路 + +以浏览器播放音乐为例: + +```text +浏览器 + → Pulse 或 native stream + → client-node + → session manager 选择默认 sink + → adapter / audioconvert / mixer + → ALSA sink node + → 内核 ALSA + → 声卡 +``` + +如果浏览器使用 Pulse,多一层 `pipewire-pulse` 协议翻译; +如果使用 JACK,多一层 pipewire-jack; +如果使用 PipeWire native,直接进入 client-node。 + +无论入口是什么,最终都汇聚到同一个 PipeWire 图和同一组 SPA 后端。 + +## 十二、排障时的分层方法 + +建议按下面顺序排查: + +1. **进程层**:`pipewire`、`wireplumber`、`pipewire-pulse` 是否运行? +2. **对象层**:`pw-dump` 里有没有 device/node/port? +3. **策略层**:默认 sink 是谁?profile/route 是否正确? +4. **链路层**:应用 stream 和目标 node 之间有没有 link? +5. **格式层**:采样率、声道、格式是否协商成功? +6. **调度层**:有没有 xrun、wait 高、节点不 runnable? +7. **兼容层**:Pulse/JACK/ALSA 应用是否真的连接到了 PipeWire? +8. **权限层**:Flatpak/portal 客户端是否被授予目标对象? + +这比一上来改 `pipewire.conf` 更有效,因为大多数“没声音”问题并不是 daemon 配置错误,而是对象不存在、默认路由错误或兼容层没接上。 + +## 十三、小结 + +这张关系图值得记住: + +```text +应用 API(native/Pulse/JACK/ALSA/V4L2/GStreamer) + │ + ▼ +PipeWire 对象图 ← WirePlumber 策略 + │ + ▼ +adapter / converter / mixer + │ + ▼ +SPA 后端(ALSA/V4L2/BlueZ/网络) + │ + ▼ +内核与硬件 +``` + +- PipeWire 负责机制; +- WirePlumber 负责策略; +- 兼容层负责旧 API; +- SPA 负责硬件与处理; +- session manager 负责把“设备存在”变成“用户期望的路由”。 + +下一篇进入实践:如何用 `pw-dump`、`pw-mon`、`pw-top`、`pw-cli`、`pw-link`、`pw-cat` 等工具,把这套结构一步步观察出来。 diff --git a/source/_posts/pipewire-architecture-08-tools-debugging.md b/source/_posts/pipewire-architecture-08-tools-debugging.md new file mode 100644 index 0000000..3f88d46 --- /dev/null +++ b/source/_posts/pipewire-architecture-08-tools-debugging.md @@ -0,0 +1,424 @@ +--- +title: PipeWire 工具与排障手册:从 pw-dump 到 pw-top 的实战路径 +date: 2026-10-23 15:30:00 +tags: [PipeWire, 排障, pw-cli, pw-dump, pw-top] +categories: [教程] +--- + +PipeWire 的问题通常不会以“PipeWire 报错”这种形式出现。 +更常见的是:应用没声音、默认设备不对、麦克风被占用、延迟变大、视频没有画面、JACK 应用找不到端口。 + +此时最有效的做法不是改配置,而是按控制面、图结构、数据面和兼容层逐层观察。 +PipeWire 自带了一套工具,正好对应这些层次。 + +本文列出常用工具、典型命令和排障顺序。以下命令面向 Linux;当前源码是在 Windows 上阅读的,没有在本机运行 PipeWire 做性能测试。 + +--- + +## 一、工具总览 + +| 工具 | 用途 | +| --- | --- | +| `pipewire` | daemon 主程序 | +| `pipewire-pulse` | PulseAudio 兼容服务 | +| `pw-cli` | 交互式或单命令对象操作 | +| `pw-dump` | 导出整个图或指定类型的 JSON | +| `pw-mon` | 实时监控对象和状态变化 | +| `pw-top` | 实时查看 driver、节点、quantum、xrun | +| `pw-link` | 列出、创建、删除链路 | +| `pw-cat` / `pw-play` / `pw-record` | 播放、录音、MIDI 收发 | +| `pw-config` | 查看配置内容和搜索路径 | +| `pw-metadata` | 读写 metadata | +| `pw-dot` | 生成 Graphviz 图 | +| `pw-profiler` | 采集 profiler 数据 | +| `pw-loopback` | 快速创建 loopback 流 | +| `pw-mididump` | 打印 MIDI 事件 | +| `pw-container` | 在受限环境运行客户端,调试权限/沙箱 | +| `pw-reserve` | 设备抢占/保留辅助 | +| `pw-audioconvert` | 音频格式转换工具 | +| `spa-inspect` | 检查 SPA 插件的 factory 和 interface | +| `spa-monitor` | 监控 SPA device | +| `spa-json-dump` | 导出 SPA 对象 JSON | +| `spa-resample` | 重采样工具 | +| `spa-acp-tool` | 调试 ALSA ACP | + +不需要一次记住全部。先掌握 `pw-dump`、`pw-cli`、`pw-top`、`pw-link` 四个就能解决大部分问题。 + +## 二、第一步:确认进程和连接 + +```bash +systemctl --user status pipewire.service wireplumber.service +systemctl --user status pipewire-pulse.service +``` + +如果使用 socket activation,可能只看到 socket 和对应 service。 +再确认客户端能够连接: + +```bash +pw-cli info 0 +``` + +`0` 是 core 对象 ID。能返回 server 信息,说明 native protocol 至少可用。 +如果这里失败,问题在 daemon、socket 路径、权限或 `XDG_RUNTIME_DIR`,还轮不到音频路由。 + +检查 socket: + +```bash +echo "$XDG_RUNTIME_DIR" +ls -l "$XDG_RUNTIME_DIR"/pipewire-* +``` + +默认 socket 名是 `pipewire-0`。 +`PIPEWIRE_RUNTIME_DIR` 可以覆盖运行时目录。 + +## 三、pw-dump:先看对象图 + +最常用命令: + +```bash +pw-dump > /tmp/pw-dump.json +pw-dump Node +pw-dump Device +pw-dump Link +``` + +JSON 里能看到对象的: + +- `id` +- `type` +- `version` +- `permissions` +- `properties` +- `info` +- `params` + +### 找默认 sink + +先看 Node 的 `media.class` 和 `node.name`: + +```bash +pw-dump Node | jq -r '.[] | [.id, .info.props["node.name"], .info.props["media.class"]] | @tsv' +``` + +如果装了 `jq`,可以快速筛选: + +```bash +pw-dump Node | jq '.[] | select(.info.props["media.class"] == "Audio/Sink")' +``` + +再看默认设备 metadata: + +```bash +pw-metadata -n default +``` + +默认 sink/source 通常由 session manager 写入 metadata。 +如果没有默认对象,WirePlumber 的配置或设备状态可能有问题。 + +### 找应用 stream + +应用播放时,通常会出现一个 `Stream/Output/Audio` 节点。 +用下面命令找: + +```bash +pw-dump Node | jq '.[] | select(.info.props["media.class"] | startswith("Stream/"))' +``` + +重点看: + +- `application.name` +- `media.name` +- `node.target` +- `target.object` +- `state` +- 是否已有 Link + +### 看设备 profile 和 route + +```bash +pw-dump Device | jq '.[] | {id, name: .info.props["device.name"], params}' +``` + +具体 profile/route 的枚举结果通常很大。 +可以结合 `pw-cli` 对指定 device 枚举参数,或者用 `wpctl status` 看 session manager 的整理结果。 + +## 四、pw-cli:单命令和交互式操作 + +`pw-cli` 适合做精确查询: + +```bash +pw-cli info 0 +pw-cli ls Node +pw-cli ls Port +pw-cli ls Link +pw-cli ls Device +``` + +进入交互模式后可以: + +```text +pw-cli +> help +> ls Node +> info 42 +> enum-params 42 Format +> quit +``` + +也可以直接执行: + +```bash +pw-cli enum-params 42 Format +pw-cli enum-params 42 Props +``` + +`enum-params` 的对象 ID 和参数名要匹配实际对象。 +如果参数 ID 不确定,可以先看对象 Info 里的 param 列表。 + +### 设置参数 + +`pw-cli s` 可以设置参数,但不同对象参数结构不同。 +例如设置 device profile 通常需要 JSON 对象: + +```bash +pw-cli s 42 Profile '{ index: 1, save: true }' +``` + +不同版本的参数格式可能有差异,操作前先用 `enum-params` 看清结构。 +不要在不了解字段含义时执行写操作。 + +## 五、pw-mon:观察事件流 + +`pw-dump` 是一次快照,`pw-mon` 是连续事件流: + +```bash +pw-mon +``` + +典型输出会显示 global 新增、删除、对象属性变化。 +它适合: + +- 插拔 USB 设备时看对象是否出现; +- 打开应用时看 stream 是否创建; +- 修改 profile 时看 node/port 如何变化; +- 调试 session manager 规则是否生效。 + +当 `pw-dump` 看起来正常但问题仍存在时,`pw-mon` 可以告诉你“对象是后来才消失/重建”还是“根本没有创建”。 + +## 六、pw-top:看实时调度 + +```bash +pw-top +``` + +重点列: + +| 列 | 含义 | +| --- | --- | +| `S` | 状态 | +| `ID` | 对象 ID | +| `QUANT` | 当前周期大小 | +| `RATE` | 采样率 | +| `WAIT` | 等待时间 | +| `BUSY` | 处理耗时 | +| `W/Q` | wait/quantum 等比 | +| `B/Q` | busy/quantum 等比 | +| `ERR` | xrun/错误计数 | + +driver 通常显示在最上方,其他节点按层次排列。 +如果 `BUSY` 接近 quantum,说明处理线程接近超时; +如果 `ERR` 增长,说明已经发生 xrun。 + +但不要把 `BUSY` 当成唯一指标。一个偶发尖峰可能只是一次调度抖动; +持续增长才说明图或进程有系统性问题。 + +## 七、pw-link:检查链路的最后一公里 + +```bash +pw-link -l +pw-link -o +pw-link -i +``` + +- `-l` 列出链路; +- `-o` 列出输出端口; +- `-i` 列出输入端口。 + +如果端口名包含空格或特殊字符,创建链路时使用准确名称: + +```bash +pw-link "output-port-name" "input-port-name" +``` + +删除: + +```bash +pw-link -d "output-port-name" "input-port-name" +``` + +注意端口名不是 node 名。一个 stereo sink 至少有两个端口,必须逐个建立链路,或者由 session manager 按规则建立。 + +如果应用节点没有 Link,先看 session manager 是否识别了它的 `media.class` 和 `target.object`。 +手工连一条链路可以验证“数据路径是否通”,但不应该替代策略配置。 + +## 八、pw-cat 与最小复现 + +`pw-cat` 支持播放、录音和 MIDI: + +```bash +pw-play test.wav +pw-record /tmp/record.wav +pw-cat --playback --target 42 test.wav +pw-cat --record --target 42 /tmp/record.wav +pw-mididump +``` + +用 `pw-play` 指向指定 node 可以做最小复现: + +```bash +pw-play --target 42 test.wav +``` + +如果指定 node 能出声、默认路由不出声,问题在 session manager 的默认目标或应用 target。 +如果两者都不出声,问题可能在设备、adapter、格式或 daemon。 + +`pw-loopback` 可以快速建立源到宿的回路: + +```bash +pw-loopback +``` + +它适合验证流创建、端口和链路,不适合直接替代生产配置。 + +## 九、日志:从级别到主题 + +PipeWire 支持 `PIPEWIRE_DEBUG`: + +```bash +PIPEWIRE_DEBUG=D pipewire +PIPEWIRE_DEBUG=E,mod.*:D,mod.foo:X pipewire +``` + +级别: + +| 值 | 含义 | +| --- | --- | +| `X` / `0` | 关闭 | +| `E` / `1` | 错误 | +| `W` / `2` | 警告 | +| `I` / `3` | 信息 | +| `D` / `4` | 调试 | +| `T` / `5` | trace | + +主题使用 `category.topic` 命名: + +- `pw.*`:核心; +- `mod.*`:模块; +- `ms.*`:session manager 相关; +- `conn.*`:连接级原始消息,通常非常详细。 + +常用环境变量: + +```bash +PIPEWIRE_LOG=/tmp/pipewire.log +PIPEWIRE_LOG_SYSTEMD=false +PIPEWIRE_LOG_LINE=false +PIPEWIRE_LOG_COLOR=false +``` + +排查连接问题时,`conn.*` 可以帮助确认消息是否发出,但数据量很大,应该限定时间和主题。 + +## 十、配置与生成图 + +查看配置加载情况: + +```bash +pw-config -n pipewire.conf +pw-config -n client.conf +``` + +生成 Graphviz 图: + +```bash +pw-dot -o /tmp/pipewire.dot +dot -Tsvg /tmp/pipewire.dot -o /tmp/pipewire.svg +``` + +`pw-dot` 适合看整体拓扑,不适合看每个周期的实时状态。 +如果图太大,可以先用 `pw-dump` 筛选节点,再手工画出关键路径。 + +Profiler: + +```bash +pw-profiler -o /tmp/profile +``` + +它记录 driver、节点、周期、CPU 时间等数据,适合定位持续抖动或耗时节点。 +和 `pw-top` 相比,profiler 更适合离线分析;和普通日志相比,它更接近调度事实。 + +## 十一、SPA 层排障 + +```bash +export SPA_PLUGIN_DIR=$(pkg-config --variable plugindir libspa-0.2) +spa-inspect "$SPA_PLUGIN_DIR/alsa/libspa-alsa.so" +spa-monitor +``` + +- `spa-inspect`:检查 factory/interface; +- `spa-monitor`:观察 SPA device 和 node; +- `spa-json-dump`:导出结构化状态; +- `spa-acp-tool`:调试 ALSA ACP; +- `spa-resample`:验证重采样行为。 + +如果 PipeWire 里没有某个设备,但 `spa-inspect` 能看到对应 factory,说明问题大概率在设备枚举、权限、参数或 session manager,而不是插件文件缺失。 + +## 十二、典型问题定位表 + +| 现象 | 先看什么 | 常见原因 | +| --- | --- | --- | +| 所有应用没声音 | `pw-cli info 0`、`pw-dump Node` | daemon 未起、没有 sink、默认路由缺失 | +| 只有一个应用没声音 | 应用 stream 节点、Link | target 错误、stream 未创建、格式协商失败 | +| 默认设备不对 | `pw-metadata -n default`、`wpctl status` | session manager 策略或优先级 | +| 麦克风无输入 | source node、Link、权限 | profile/route 错、portal 未授权、静音 | +| 延迟变大 | `pw-top`、链路 latency | quantum 大、异步节点、重采样、网络缓冲 | +| xrun 增长 | `pw-top`、profiler、日志 | RT 优先级、阻塞回调、CPU 抢占、buffer 太小 | +| Pulse 工具看不到设备 | `pactl info`、`pipewire-pulse` 状态 | Pulse 服务未起、socket 冲突 | +| JACK 应用找不到端口 | `pw-jack`、JACK 节点 | 兼容库未加载、transport/端口映射 | +| Flatpak 摄像头失败 | portal 日志、`pw-dump` 权限 | media role、permission store、portal 未授权 | +| 配置没生效 | `pw-config`、`pw-mon` | 找错配置文件、未重启、section 拼写错误 | + +## 十三、推荐的排障顺序 + +```text +1. 进程是否活着 +2. socket 是否可连 +3. 对象是否存在 +4. 默认目标是否正确 +5. 链路是否建立 +6. 格式是否协商 +7. 调度是否正常 +8. 兼容层是否命中 +9. 权限是否足够 +10. 最后才改配置 +``` + +这套顺序的价值在于:每一层都有可验证的证据。 +不要因为看到一条日志里有 `error` 就直接改全局配置,也不要因为 `pw-top` 里某个数字高就断定设备有问题。 + +## 十四、小结 + +工具和层次的对应关系: + +| 层次 | 工具 | +| --- | --- | +| 进程与连接 | `systemctl`、`pw-cli info 0` | +| 对象快照 | `pw-dump` | +| 对象变化 | `pw-mon` | +| 链路 | `pw-link` | +| 调度 | `pw-top`、`pw-profiler` | +| 配置 | `pw-config` | +| 最小复现 | `pw-play`、`pw-record`、`pw-loopback` | +| SPA | `spa-inspect`、`spa-monitor` | +| 权限/沙箱 | portal 日志、`pw-container`、`pw-dump` permissions | + +下一篇是系列的收尾:如何从源码入口开始读 PipeWire,怎样写第一个客户端或模块,以及哪些实时和协议约束最容易踩坑。 diff --git a/source/_posts/pipewire-architecture-09-code-reading-contribution.md b/source/_posts/pipewire-architecture-09-code-reading-contribution.md new file mode 100644 index 0000000..b1cd342 --- /dev/null +++ b/source/_posts/pipewire-architecture-09-code-reading-contribution.md @@ -0,0 +1,411 @@ +--- +title: PipeWire 源码阅读与上手:构建、入口、第一个客户端与贡献路径 +date: 2026-10-23 15:40:00 +tags: [PipeWire, 源码阅读, Meson, C语言, 开发] +categories: [教程] +--- + +到这里,前面的文章已经分别讲了对象模型、SPA、调度、协议、模块、插件、会话管理和排障工具。 +最后一篇解决“怎么动手”:源码从哪读、怎么构建、怎么跑 uninstalled 环境、怎么写一个最小客户端,以及哪些约束最容易在开发阶段踩坑。 + +需要先说明:PipeWire 主要面向 Linux。Windows 可以克隆和阅读代码,但不能直接构建并运行完整 daemon。 +本文的构建命令来自仓库 README/INSTALL 和 Meson 配置,运行结果应以你的 Linux 发行版和依赖为准。 + +--- + +## 一、源码目录再回顾 + +```text +pipewire/ +├── src/pipewire/ 公共 API 与对象实现 +├── src/daemon/ daemon 入口与配置模板 +├── src/modules/ 运行时模块 +├── src/tools/ 命令行工具 +├── src/examples/ 客户端示例 +├── spa/include/spa/ SPA 头文件 +├── spa/plugins/ SPA 插件实现 +├── pipewire-alsa/ ALSA 兼容插件 +├── pipewire-jack/ JACK 兼容库 +├── pipewire-v4l2/ V4L2 兼容库 +├── doc/dox/ 设计文档与 man page 源 +├── test/ 测试 +└── meson.build 构建入口 +``` + +最重要的阅读顺序不是“按目录字母顺序”,而是沿着一次调用链: + +```text +应用调用 pw_stream + → stream.c 创建 client-node + → core.c 发送协议消息 + → protocol-native 解析 + → 服务端 impl-node + → scheduler 触发 process + → SPA node 处理 buffer +``` + +## 二、构建环境 + +PipeWire 使用 Meson + Ninja。最低 Meson 版本在顶层 `meson.build` 中声明。 +基础工具: + +```bash +meson --version +ninja --version +pkg-config --version +``` + +依赖项按发行版不同而变化,很多是可选的。 +仓库 README 建议参考 CI 中的 `FDO_DISTRIBUTION_PACKAGES`,因为那里面列出了官方构建实际安装的包。 +不要只装 CMake 时代的“大而全依赖”,按你要启用的选项来装更实际。 + +### 基本构建 + +```bash +git clone https://github.com/PipeWire/pipewire.git +cd pipewire + +meson setup builddir +meson configure builddir +meson compile -C builddir +``` + +安装: + +```bash +sudo meson install -C builddir +``` + +默认前缀是 `/usr/local`,可以用: + +```bash +meson setup builddir -Dprefix=/usr +``` + +### 不安装运行 + +README 提供 uninstalled 环境脚本: + +```bash +./pw-uninstalled.sh -b builddir +``` + +它会设置库搜索路径和运行时目录,让 ALSA、PulseAudio、JACK 客户端优先使用构建目录中的 PipeWire 组件。 +这适合开发,但**不要和系统 daemon 同时抢同一套 socket/服务**。 +切换前先停掉系统 user service: + +```bash +systemctl --user stop pipewire.service pipewire.socket +systemctl --user stop wireplumber.service +systemctl --user stop pipewire-pulse.service pipewire-pulse.socket +``` + +具体 service 名可能因发行版不同,先 `systemctl --user list-units | grep -E 'pipewire|wireplumber'` 确认。 + +## 三、常用构建选项 + +`meson_options.txt` 定义了文档、测试、兼容层、SPA 插件和外部库选项。常用项: + +| 选项 | 默认 | 说明 | +| --- | --- | --- | +| `-Dtests=enabled` | enabled | 构建测试 | +| `-Ddocs=disabled` | disabled | 生成 API 文档 | +| `-Dman=disabled` | disabled | 生成 man page | +| `-Dgstreamer=auto` | auto | GStreamer 元素与 device provider | +| `-Dpipewire-alsa=auto` | auto | ALSA 兼容层 | +| `-Dpipewire-jack=enabled` | enabled | JACK 兼容库 | +| `-Dpipewire-v4l2=enabled` | enabled | V4L2 兼容库 | +| `-Dbluez5=auto` | auto | Bluetooth 后端 | +| `-Dv4l2=auto` | auto | V4L2 后端 | +| `-Dffmpeg=disabled` | disabled | FFmpeg SPA 编解码 | +| `-Dvulkan=disabled` | disabled | Vulkan SPA 插件 | +| `-Dsession-managers=['wireplumber']` | wireplumber | 要构建的 session manager 列表 | + +调试构建: + +```bash +meson setup builddebug -Dbuildtype=debug -Db_sanitize=address,undefined +meson compile -C builddebug +``` + +ASAN/UBSAN 对协议、buffer 生命周期和对象释放问题很有用,但实时线程里的性能会明显变化,不能拿来做延迟基准。 + +## 四、测试与验证 + +```bash +meson test -C builddir +meson test -C builddir --print-errorlogs +``` + +测试覆盖协议、SPA 格式转换、混音、RAOP 认证、连接等模块。 +读测试比读完整实现更快,尤其是: + +- `spa/plugins/audioconvert/test-*.c` +- `spa/plugins/audiomixer/test-mix-ops.c` +- `spa/plugins/alsa/test-*.c` +- `src/modules/module-protocol-native/test-connection.c` +- `src/modules/module-raop/test-*.c` + +测试通常直接构造最小对象,不依赖完整桌面环境,适合理解 API 的前置条件。 + +## 五、从哪个源文件开始读 + +### 1. daemon 入口 + +`src/daemon/pipewire.c` 是 daemon 主程序。 +它负责解析参数、创建 main loop、创建 context、连接配置、加载模块、运行 loop。 + +阅读时不要被配置解析细节淹没,先抓主线: + +```text +main() + → pw_init() + → pw_main_loop_new() + → pw_context_new() + → 加载配置/模块 + → pw_main_loop_run() +``` + +### 2. context 与 core + +`src/pipewire/context.c` 管理本地资源和 global 表。 +`src/pipewire/core.c` 实现远端连接和协议消息。 + +重点看: + +- context 如何加载 SPA handle; +- 如何创建 `pw_impl_*` 和 `pw_global`; +- core proxy 如何分配 object ID; +- sync/done 如何实现; +- add_mem 如何处理 fd。 + +### 3. stream 与 filter + +`src/pipewire/stream.c` 是应用最常用的高层对象: + +- 连接 core; +- 创建 client-node; +- 协商格式; +- 分配 buffer; +- 调用用户 process 回调; +- 处理状态变化。 + +`src/pipewire/filter.c` 更接近 JACK client,适合多端口处理。 + +### 4. SPA node + +`spa/include/spa/node/node.h` 定义节点接口。 +先看 `spa_node_methods`,再看 `spa_node_events`,最后看 `spa_node_call_*` 辅助函数。 +实现文件可以从 `spa/plugins/audiotestsrc/audiotestsrc.c` 开始。 + +### 5. 调度 + +`doc/dox/internals/scheduling.dox`、`running.dox`、`driver.dox` 是调度设计文档。 +代码上可以看 `module-scheduler-v1.c`、`impl-node.c`、`impl-link.c` 和 `context.c` 中的 recalc 逻辑。 + +## 六、官方示例的阅读路径 + +仓库里有 7 个教程和对应 C 文件: + +```text +doc/examples/tutorial1.c ... tutorial7.c +doc/dox/tutorial/tutorial1.dox ... tutorial7.dox +``` + +推荐顺序: + +| 教程 | 主题 | +| --- | --- | +| 1 | 连接 core、获取 registry、监听全局对象 | +| 2 | 绑定对象、监听对象事件 | +| 3 | 创建 stream 并处理 process | +| 4 | 参数和格式协商 | +| 5 | 多端口 filter | +| 6 | 事件循环与异步操作 | +| 7 | 更完整的客户端模式 | + +不要一次读完。先编译 tutorial1,确认能连上 daemon,再逐个增加功能。 +每加一个 listener,都检查它的 `version` 字段和回调生命周期。 + +## 七、写第一个最小客户端 + +从概念上,一个播放客户端需要: + +```text +1. pw_init +2. main loop + context +3. connect core +4. pw_stream_new +5. 设置 stream properties +6. 连接方向、格式、buffer 参数 +7. 注册 process 回调 +8. 在回调中写音频 +9. 处理 state_changed / param_changed +10. 清理 listener、stream、context、loop +``` + +`process` 回调里通常要: + +- 获取当前 buffer; +- 只在有空间时写入; +- 更新 chunk 的 offset/size/stride; +- 不阻塞; +- 不用不可控的锁和分配。 + +流状态变化会通过 `state_changed` 回调通知,例如 paused、connecting、error。 +不要在 `process` 里做文件读取、网络请求或 UI 操作,这些工作应提前完成或交给其他线程。 + +## 八、写一个最小模块 + +模块的核心是导出 `pipewire__module_init`: + +```c +int pipewire__module_init(struct pw_impl_module *module, const char *args) +{ + struct pw_context *context = pw_impl_module_get_context(module); + /* 解析 args,创建对象,注册 listener/factory */ + return 0; +} +``` + +最小模块通常做几件事: + +1. 从 `args` 解析属性; +2. 从 context 获取 support、main loop; +3. 创建需要的 factory 或 node; +4. 添加 destroy listener 做清理; +5. 返回 0 表示初始化成功。 + +模块失败要返回负 errno,让配置加载流程能报告错误。 +不要在模块初始化阶段做长期阻塞操作,否则 daemon 启动会被拖住。 + +## 九、协议扩展与对象导出 + +如果要新增对象类型,一般需要: + +1. 定义接口类型字符串和版本; +2. 定义 method/event 编号和 POD 编解码; +3. 客户端侧 proxy 实现; +4. 服务端侧 resource 实现; +5. 模块注册 factory 或导出类型; +6. 处理权限、版本兼容和销毁。 + +这部分可以参考: + +- `module-client-node`:导出 node; +- `module-client-device`:导出 device; +- `module-metadata`:导出 metadata; +- `module-profiler`:协议扩展和 profiler 数据; +- `module-session-manager`:一组 session 对象。 + +新增协议时要考虑旧版本客户端:接口只能加方法/事件,不应随意改旧编号和结构。 +POD 里新增字段也要保留旧版本可忽略的布局。 + +## 十、实时与并发约束 + +PipeWire 开发最容易踩的坑不是 C 语法,而是实时语义: + +| 约束 | 原因 | +| --- | --- | +| process 不阻塞 | 阻塞直接导致 xrun | +| process 不分配内存 | 分配器可能加锁、触发 page fault | +| 谨慎使用互斥锁 | 优先级反转和无界等待 | +| 不写普通文件 | I/O 不可预测 | +| 不调用 DBus/网络 | 延迟不可控 | +| 不假设 buffer 总是满 | 音频/视频帧边界不同 | +| 不使用未初始化 fd/映射 | 远程节点和共享内存要求精确生命周期 | +| 管理好 listener 生命周期 | 回调可能在对象销毁后仍排队 | + +如果需要在实时线程外处理数据,使用 `pw_work_queue`、`pw_data_loop` 或自己的无锁队列,并确保跨线程所有权清晰。 + +## 十一、调试构建与日志 + +开发时建议: + +```bash +PIPEWIRE_DEBUG=D ./pw-uninstalled.sh -b builddir +PIPEWIRE_LOG=/tmp/pw.log ./pw-uninstalled.sh -b builddir +``` + +针对模块: + +```bash +PIPEWIRE_DEBUG=E,mod.*:D,mod.protocol-native:T +``` + +针对连接: + +```bash +PIPEWIRE_DEBUG=E,conn.*:T +``` + +先用较低级别定位模块,再临时提高某个 topic 的级别。 +不要把 trace 日志当成常态,它会改变实时行为并快速填满磁盘。 + +## 十二、贡献前检查清单 + +1. 在目标 Linux 环境完成构建。 +2. 运行相关测试,而不是只跑 demo。 +3. 新增/修改 API 时检查版本号和 ABI 兼容。 +4. 实时路径改动要有 xrun、延迟和并发分析。 +5. 协议改动要验证旧客户端连接。 +6. 模块和 SPA 插件要处理失败路径和清理。 +7. 配置变更同步更新 man page 或 doc/dox。 +8. 不提交构建目录、临时 socket、日志和个人路径。 +9. 遵循仓库 README、CODE_OF_CONDUCT 和上游 issue/MR 流程。 +10. 提交信息说明“为什么改”,而不是只描述“改了什么”。 + +当前仓库 README 指向 GitLab 作为上游开发与 issue 入口。 +GitHub 镜像可以用于阅读和克隆,但提交贡献前应确认上游的实际流程。 + +## 十三、术语表 + +| 术语 | 含义 | +| --- | --- | +| PipeWire daemon | 用户态媒体图服务器 | +| Session manager | 负责策略、设备发现和自动路由的独立进程,常见实现是 WirePlumber | +| Node | 媒体处理单元 | +| Port | 节点输入/输出接口 | +| Link | 两个端口的连接 | +| Driver | 启动图周期的节点 | +| Quantum | 一次调度的样本/时间单位 | +| Activation | 节点依赖和唤醒状态 | +| SPA | Simple Plugin API,底层插件和节点接口 | +| POD | SPA 的自描述二进制对象格式 | +| Proxy / Resource | 客户端代理与服务端资源的成对对象 | +| Global | 服务端 registry 中的全局对象 | +| Adapter | 设备格式与 DSP/passthrough 端口之间的适配层 | +| Client-node | 运行在客户端进程中的 PipeWire 节点 | +| Metadata | 运行时键值状态,常被 session manager 用作策略输入 | +| Xrun | 周期未按时完成或数据未及时供给 | + +## 十四、系列回顾 + +这 10 篇的主线可以压缩成四层: + +```text +应用 / 兼容 API + │ + ▼ +PipeWire 对象与协议(控制面) + │ + ▼ +图调度与共享内存(数据面) + │ + ▼ +SPA 插件与硬件/网络后端 +``` + +再配一条横向的策略线: + +```text +WirePlumber / portal / session manager +``` + +学习时建议反复问三个问题: + +1. 这个对象属于哪个进程、哪个层? +2. 这条消息走控制面还是数据面? +3. 这个行为是 PipeWire 的机制,还是 session manager 的策略? + +能稳定回答这三个问题,就已经越过了 PipeWire 架构最难的部分。