简介: CSDN博客专家、《Android系统多媒体进阶实战》作者
博主新书推荐:《Android系统多媒体进阶实战》🚀
Android Audio工程师专栏地址: Audio工程师进阶系列【原创干货持续更新中……】🚀
Android多媒体专栏地址: 多媒体系统工程师系列【原创干货持续更新中……】🚀
专题一 二:AAOS车载系统+AOSP14系统攻城狮入门视频实战课 🚀
专题三:Android14 Binder之HIDL与AIDL通信实战课 🚀
专题四:Android15快速自定义与集成音效实战课 🚀
专题五:Android15音频策略实战课 🚀
专题六:Android15音频性能实战课(无声/杂音/断音/爆音实战案例) 🚀
人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药.
🍉🍉🍉文章目录🍉🍉🍉
🌻1.前言
本篇目的:理解
pw_stream_connect()如何让一个刚创建的pw_stream进入PipeWire Graph,并为后续格式协商、路由连接和Buffer传输建立基础。
应用调用:
pw_stream_new()
创建pw_stream以后,此时得到的只是一个:
Stream对象
它还没有真正进入PipeWire的数据处理链路。
如果是音乐播放应用,还需要告诉PipeWire:
这是一个输出流
支持什么音频格式
是否需要自动选择播放设备
Buffer如何映射
这些工作由:
pw_stream_connect()
启动。
PipeWire官方将Stream定义为用于客户端与PipeWire交换数据的高级封装,并指出Stream初始处于UNCONNECTED状态,需要调用pw_stream_connect()连接;对于普通播放流,方向使用PW_DIRECTION_OUTPUT。
整体调用流程:
简单理解:
pw_stream_new()
=
造出一辆汽车
pw_stream_connect()
=
把汽车开上PipeWire这条公路
🌻2.应用场景和用法
🌻2.1 应用场景
pw_stream主要用于应用和PipeWire之间传输媒体数据。
例如播放器:
音乐播放器
↓
pw_stream
↓
PipeWire
↓
Audio Sink
录音应用则相反:
Audio Source
↓
PipeWire
↓
pw_stream
↓
录音应用
因此pw_stream_connect()最常见的两个应用场景是:
音频播放
↓
PW_DIRECTION_OUTPUT
音频录音
↓
PW_DIRECTION_INPUT
这里的方向是站在Stream自身的数据方向看:
PipeWire官方对此有一个非常重要的定义:
PW_DIRECTION_OUTPUT
=
Stream生产数据
PW_DIRECTION_INPUT
=
Stream消费数据
所以普通播放器使用PW_DIRECTION_OUTPUT,普通录音程序使用PW_DIRECTION_INPUT。
另外,PW_STREAM_FLAG_AUTOCONNECT表示希望Session Manager自动把Stream连接到合适的目标Node;真正决定连接哪个设备以及创建Graph Link,属于Session Manager的策略职责,而不是pw_stream_connect()单独完成。
可以简单理解为:
pw_stream_connect()
=
“我要进入PipeWire”
WirePlumber
=
“我决定把你接到哪个设备”
🌻2.2 函数用法
pw_stream_connect()函数原型:
int pw_stream_connect(struct pw_stream *stream,
enum pw_direction direction,
uint32_t target_id,
enum pw_stream_flags flags,
const struct spa_pod **params,
uint32_t n_params);
参数作用:
| 参数 | 作用 |
|---|---|
stream |
已创建的pw_stream对象 |
direction |
Stream的数据方向 |
target_id |
目标对象ID,现代用法通常使用PW_ID_ANY |
flags |
Stream连接行为 |
params |
Stream支持的SPA参数,例如音频格式 |
n_params |
参数数量 |
官方当前文档建议target_id使用:
PW_ID_ANY
如果需要指定目标设备,推荐在Stream属性中使用:
PW_KEY_TARGET_OBJECT
并设置目标Node的object.serial或node.name;直接通过target_id指定Node ID已经属于废弃用法。
播放器常见调用:
pw_stream_connect(stream,
PW_DIRECTION_OUTPUT,
PW_ID_ANY,
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS,
params,
n_params);
几个常用Flag:
| Flag | 作用 |
|---|---|
PW_STREAM_FLAG_AUTOCONNECT |
请求自动连接合适的目标 |
PW_STREAM_FLAG_MAP_BUFFERS |
将可映射Buffer映射到客户端地址空间 |
PW_STREAM_FLAG_RT_PROCESS |
在Realtime线程中调用process回调 |
PW_STREAM_FLAG_INACTIVE |
创建后先保持Inactive |
PW_STREAM_FLAG_NO_CONVERT |
不使用Stream内部格式转换 |
PipeWire官方播放示例同样采用PW_DIRECTION_OUTPUT + PW_ID_ANY + AUTOCONNECT + MAP_BUFFERS + RT_PROCESS的组合。
params通常不是PCM数据,而是告诉PipeWire:
“我的Stream支持什么媒体格式?”
例如:
params[n_params++] =
spa_format_audio_raw_build(
&b,
SPA_PARAM_EnumFormat,
&SPA_AUDIO_INFO_RAW_INIT(
.format = SPA_AUDIO_FORMAT_F32,
.channels = 2,
.rate = 48000));
也就是告诉PipeWire:
Format = F32
Channels = 2
Rate = 48000
🌻3.调用流程剖析
🌻3.1 客户端Stream连接流程
调用pw_stream_new()以后,Stream最开始处于:
PW_STREAM_STATE_UNCONNECTED
调用:
pw_stream_connect()
以后,Stream开始进入连接过程。
PipeWire的Stream并不是一个孤立的应用对象。官方架构说明中,Stream实际上封装了一个带Adapter的pw_client_node Proxy,用这种方式把客户端中的媒体处理能力表示成PipeWire服务端能够看到的Node。
因此可以把客户端过程理解为:
这里最核心的变化是:
调用前:
应用
└── pw_stream
调用后:
应用
└── pw_stream
↓
ClientNode Proxy
↕ IPC
PipeWire服务端Node
Client Node机制本身通过Native Protocol在客户端Proxy和服务端Resource之间同步Node状态。官方Native Protocol文档说明,pw-stream和pw-filter正是利用Client Node机制实现客户端媒体处理Node;底层通过client-nodeFactory创建ClientNode Proxy和服务端Resource。
需要特别注意:
pw_stream_connect()成功返回,只代表连接请求成功启动,并不表示此刻已经完成格式协商、建立设备Link并开始播放。
Stream状态后面还可能经历:
UNCONNECTED
↓
CONNECTING
↓
PAUSED
↓
STREAMING
PipeWire官方Stream API定义了这几个状态,用于描述连接和运行过程。
🌻3.2 服务端协商与Graph连接流程
Stream Node进入PipeWire以后,服务端开始处理Node和Port。
客户端在pw_stream_connect()中传入:
direction
flags
params
其中params中的:
SPA_PARAM_EnumFormat
表示Stream支持哪些格式。
PipeWire随后通过参数协商确定真正使用的格式,并通过:
pw_stream_events.param_changed
通知客户端。
官方Stream文档明确说明,连接之后服务端会配置Stream参数;发生格式参数变化时会产生param_changed事件,随后客户端可以通过pw_stream_update_params()继续完成Buffer数量、大小、Meta等协商。
整体过程:
对于一个普通音乐播放器:
应用pw_stream
↓
Stream Node
↓
输出Port
↓
Link
↓
Audio Sink Node
这里需要区分两件事情:
pw_stream_connect()
=
把Stream接入PipeWire连接流程
Link
=
把Stream Node的Port连接到设备Node的Port
如果设置:
PW_STREAM_FLAG_AUTOCONNECT
含义是:
“请Session Manager帮我自动寻找合适目标”
并不是pw_stream_connect()内部自己直接创建与ALSA Sink之间的Link。官方教程也明确把AUTOCONNECT描述为请求Session Manager将Stream连接到合适的Consumer。
🌻4.实战案例
实战目标:创建一个48kHz、双声道、F32格式的播放Stream,通过
pw_stream_connect()接入PipeWire,并在process回调中持续写入音频数据。
整体流程:
🌻4.1 创建pw_stream
首先定义Stream事件:
static void on_process(void *userdata);
static void on_state_changed(
void *userdata,
enum pw_stream_state old,
enum pw_stream_state state,
const char *error)
{
printf("stream state: %s\n",
pw_stream_state_as_string(state));
}
static const struct pw_stream_events stream_events = {
PW_VERSION_STREAM_EVENTS,
.state_changed = on_state_changed,
.process = on_process,
};
然后创建播放Stream:
struct pw_properties *props;
props = pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
PW_KEY_MEDIA_ROLE, "Music",
NULL);
stream = pw_stream_new_simple(
pw_main_loop_get_loop(loop),
"audio-playback",
props,
&stream_events,
NULL);
这里创建出来的是:
pw_stream
此时它还没有进入PipeWire Graph。
🌻4.2 调用pw_stream_connect
首先构造Stream支持的音频格式:
uint8_t buffer[1024];
struct spa_pod_builder b =
SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const struct spa_pod *params[1];
params[0] = spa_format_audio_raw_build(
&b,
SPA_PARAM_EnumFormat,
&SPA_AUDIO_INFO_RAW_INIT(
.format = SPA_AUDIO_FORMAT_F32,
.channels = 2,
.rate = 48000));
然后执行:
int res;
res = pw_stream_connect(
stream,
PW_DIRECTION_OUTPUT,
PW_ID_ANY,
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS,
params,
1);
if (res < 0) {
fprintf(stderr,
"pw_stream_connect failed: %s\n",
spa_strerror(res));
}
这里相当于告诉PipeWire:
我是播放流
↓
PW_DIRECTION_OUTPUT
请自动选择目标
↓
PW_STREAM_FLAG_AUTOCONNECT
请映射Buffer
↓
PW_STREAM_FLAG_MAP_BUFFERS
支持格式
↓
F32 / 2ch / 48000Hz
核心变化:
官方音频播放示例采用的就是这种基本结构:创建pw_stream、构造SPA_PARAM_EnumFormat,然后以PW_DIRECTION_OUTPUT和AUTOCONNECT等Flag执行pw_stream_connect()。
🌻4.3 通过process回调验证数据传输
Stream真正开始处理数据以后,PipeWire通过:
process()
回调通知应用。
应用调用:
pw_stream_dequeue_buffer()
取出Buffer。
例如:
static void on_process(void *userdata)
{
struct pw_buffer *b;
struct spa_buffer *buf;
float *dst;
uint32_t n_frames;
uint32_t i;
b = pw_stream_dequeue_buffer(stream);
if (b == NULL)
return;
buf = b->buffer;
dst = buf->datas[0].data;
if (dst == NULL)
goto done;
n_frames =
buf->datas[0].maxsize /
(sizeof(float) * 2);
for (i = 0; i < n_frames; i++) {
dst[i * 2] = 0.0f;
dst[i * 2 + 1] = 0.0f;
}
buf->datas[0].chunk->offset = 0;
buf->datas[0].chunk->stride =
sizeof(float) * 2;
buf->datas[0].chunk->size =
n_frames * sizeof(float) * 2;
done:
pw_stream_queue_buffer(stream, b);
}
这里写入的是静音PCM。
真正的数据循环是:
当状态变化能够看到:
CONNECTING
↓
PAUSED
↓
STREAMING
并且开始持续收到:
process()
就说明:
pw_stream
↓
pw_stream_connect()
↓
Stream Node
↓
格式/Buffer协商
↓
Graph
↓
process()数据传输
这条播放链路已经建立。PipeWire官方也要求应用监听process事件,并通过pw_stream_dequeue_buffer()和pw_stream_queue_buffer()完成播放或采集Buffer循环。
🌻5.总结
pw_stream_connect()就是把已经创建的pw_stream按照指定方向、连接Flag和媒体参数接入PipeWire连接流程,为Stream Node导出、格式与Buffer协商以及后续Graph连接建立基础。