点击下方👇关注 Android系统攻城狮
第211篇原创文章
持续迭代 | Android+Linux多媒体进阶体系
专栏·课程 知识体系持续更新
上一篇已经知道应用通过pw_stream创建播放流Node,并把Node注册到PipeWire服务端。此时Graph中已经有了Stream/Output/Audio播放流Node,但它还不能真正播放声音。
系统里可能同时存在板载扬声器、HDMI和蓝牙耳机,WirePlumber首先要从这些Audio/Sink设备中选出一个目标,然后再把播放流的输出Port连接到目标Node的输入Port。
本文只回答两个问题:先决定:声音给谁,再决定:两边怎么连。


1


播放流Node和扬声器Node同时存在,只能说明两个对象都已经进入PipeWire对象体系,并不代表它们已经连接。可以把PipeWire Graph想成一个房间:播放流Node是“送声音的人”,Audio/Sink Node是“接声音的人”,两个人都在房间里,不代表已经牵上线。
PipeWire负责维护Node、Port和Link组成的Graph,WirePlumber负责看播放流想去哪里、默认设备是谁、设备当前能不能用,然后做出目标选择。

这里最容易混淆三个概念:default.audio.sink解决默认选谁,si-standard-link解决WirePlumber内部怎么组织这次连接,真正的数据通路则是PipeWire Graph里的Link。


2


播放流Node进入Graph后,并不是所有流都会自动寻找设备。WirePlumber先要判断:这个流是否允许系统帮它自动连接。
WirePlumber会把可以参与连接的Node封装为Linkable Session Item,然后检查播放流的node.autoconnect。如果它为true,意思就是:这个播放流允许WirePlumber帮我自动找一个Sink。

所以,pw_stream_connect()里的自动连接标志并不会直接选中Speaker。它只是给播放流留下一个“我允许自动连接”的意图,真正选择哪个设备的是WirePlumber。
普通播放流进入会话后,WirePlumber可以触发select-target。如果后来发生默认设备变化、蓝牙耳机接入、HDMI变化或者新的Linkable对象进入Graph,WirePlumber还会进行Linking rescan,再重新检查是否需要选择目标。


3


最关键的问题:如果系统里有多个Sink,WirePlumber到底选谁?最容易理解的方法,是把它看成一个有明确顺序的目标查找过程。
第一优先级是播放流自己已经指定目标,例如target.object或node.target。
如果没有指定目标,WirePlumber再检查默认Audio/Sink。默认设备来自Default Nodes策略,其中default.configured.audio.sink更接近用户希望使用哪个设备,default.audio.sink则表示当前条件下实际选中的默认Sink。

如果默认目标也不可用,find-best-target.lua会遍历候选Linkable对象。候选设备至少要满足:媒体类型一致、数据方向能够对接、canLink()检查通过,如果Node关联了设备Profile和Route,还要确认当前Route确实可用。
候选设备都通过检查后,再比较priority.session。数值更高的设备更优先,如果相同,则继续比较item.plugged.usec,较晚接入的设备优先。
不要把priority.session和priority.driver混淆,前者帮助WirePlumber知道:声音送给哪个设备,后者用于Graph Driver相关的优先级选择。

4


设备选好以后,WirePlumber只是回答了“连接谁”,还没有真正创建PipeWire Link,接下来需要把这次连接关系组织起来。
播放场景中,播放流是输出端,目标Audio/Sink是输入端。link-target.lua根据方向确定两边角色,然后创建一个si-standard-linkSession Item。

可以把si-standard-link理解成一张连接任务单。它保存谁输出、谁输入、媒体类型是什么,以及两边Port应该从什么上下文获取。播放流使用outputPort上下文,Audio/Sink使用inputPort上下文。


5


真正的数据连接最终发生在Port和Port之间,比如一个立体声播放流可能有FL、FR两个输出Port,Speaker也有FL、FR两个输入Port,那么WirePlumber需要把左声道接左声道,右声道接右声道。

si-standard-link激活后,会先确认输出项和输入项仍然有效,然后取得两端Port列表。Port匹配优先选择相同声道;如果没有完全一致的声道,还会按内部评分规则寻找可连接的Port,并避免重复使用同一个输入Port。
一旦Port匹配成功,WirePlumber就能得到创建Link所需的四个关键标识:输出Node ID、输出Port ID、输入Node ID和输入Port ID。

随后WirePlumber调用wp_link_new_from_factory(),并把link-factory作为工厂名称传入。这里先得到WirePlumber侧的WpLink对象,再由它请求远端PipeWire服务端的Link Factory创建真正的PipeWire Link。
新建的WpLink还需要激活。只有远端Link成功绑定并可以使用,PipeWire Graph中才真正出现播放流Node到Audio/Sink Node之间的连接。

6


WirePlumber建立播放Link,看起来涉及很多脚本、Session Item和对象,但本质只有两个问题:第一,声音给谁;第二,两边怎么连。
整个过程可以分成四步:
①发现播放流需要连接②按“指定目标→默认目标→最佳候选设备”选择Audio/Sink③创建si-standard-link保存“谁连接谁”④匹配两端Port,通过link-factory创建真实PipeWire Link。
所以:default.audio.sink解决选谁,si-standard-link解决怎么组织连接,PipeWire Link解决真正传输数据。
到这里,上一篇创建出来的播放流Node,才真正接到了Audio/Sink Node。
若读者朋友发现有错误、疑问的地方,或者好的建议,欢迎拍砖!!!