mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-05-06 11:48:02 +02:00
Compare commits
533 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b42a5a3ae | ||
|
|
5a2f52dab4 | ||
|
|
7fa44ef8d0 | ||
|
|
c579d1d839 | ||
|
|
26f5fc11a6 | ||
|
|
1f0c590f49 | ||
|
|
1f8475b15f | ||
|
|
bd4beadb43 | ||
|
|
85a7201409 | ||
|
|
5c0712322f | ||
|
|
2fa1414fbe | ||
|
|
45a2786c1b | ||
|
|
409446046c | ||
|
|
767a83a5f0 | ||
|
|
f4f1a33446 | ||
|
|
e1874f8b31 | ||
|
|
478c9402fc | ||
|
|
210467c5ce | ||
|
|
dcd59bc31d | ||
|
|
c03f4fd4d7 | ||
|
|
484e1f0fb7 | ||
|
|
78bd42cad8 | ||
|
|
529aaa66cb | ||
|
|
374c48b339 | ||
|
|
07e730b279 | ||
|
|
c2b96ebb39 | ||
|
|
1cfeab9a86 | ||
|
|
b453464320 | ||
|
|
7023ad0c2c | ||
|
|
20238072e2 | ||
|
|
f38f1a2af8 | ||
|
|
9fb963d4e5 | ||
|
|
8dcf3ce6ed | ||
|
|
2954e0d5e8 | ||
|
|
76b26deabf | ||
|
|
eef9baee61 | ||
|
|
46638971c3 | ||
|
|
215c9efd02 | ||
|
|
f535befda4 | ||
|
|
1762d91e75 | ||
|
|
355bb0fb8f | ||
|
|
b262ac43be | ||
|
|
38c07393e5 | ||
|
|
83d08dfa43 | ||
|
|
48ed27d11b | ||
|
|
de0bca5902 | ||
|
|
27337ed268 | ||
|
|
72b680fc4c | ||
|
|
11af177902 | ||
|
|
2b78d9c20d | ||
|
|
4cebb63d76 | ||
|
|
d81b170bbf | ||
|
|
024f88322c | ||
|
|
80842cbb96 | ||
|
|
a5a079ec1d | ||
|
|
9040ec1e51 | ||
|
|
f088a6f63d | ||
|
|
b60b2f4ece | ||
|
|
84429b4794 | ||
|
|
58b48c0a8a | ||
|
|
af7a951bd9 | ||
|
|
ded213093d | ||
|
|
3a6f2c1e90 | ||
|
|
1846d75717 | ||
|
|
444bfc04d8 | ||
|
|
3887e1ca82 | ||
|
|
00c272670c | ||
|
|
da831fdc65 | ||
|
|
6a9e977d26 | ||
|
|
80478e7548 | ||
|
|
3fb5b775ee | ||
|
|
a5538f4167 | ||
|
|
bec20fc054 | ||
|
|
beded0214d | ||
|
|
2286152c07 | ||
|
|
94fe1cbfbd | ||
|
|
6ebf81453c | ||
|
|
ceed5dca7c | ||
|
|
84e4752f1a | ||
|
|
9e390f1121 | ||
|
|
133b82e61a | ||
|
|
9045d2439a | ||
|
|
278541f637 | ||
|
|
27b6027649 | ||
|
|
6398bf1bce | ||
|
|
f196d10e87 | ||
|
|
5071a85997 | ||
|
|
15d98f59e5 | ||
|
|
be6f2b2926 | ||
|
|
01eb206460 | ||
|
|
5a4ecceee6 | ||
|
|
a35e40c1d2 | ||
|
|
2712cbb5a9 | ||
|
|
c68eb59017 | ||
|
|
c0e047c241 | ||
|
|
238fd3c067 | ||
|
|
b80a0975c7 | ||
|
|
7ca21699a9 | ||
|
|
551353482a | ||
|
|
f0b224b210 | ||
|
|
0dad52f774 | ||
|
|
27f97f6c45 | ||
|
|
285230af67 | ||
|
|
c095ae5254 | ||
|
|
d2a49e8bc5 | ||
|
|
b2c4993ab5 | ||
|
|
ae30b4f022 | ||
|
|
962be34a2b | ||
|
|
6f5ca5a79d | ||
|
|
5ecfe9f555 | ||
|
|
93377a8b4f | ||
|
|
ee72196500 | ||
|
|
e30c2a7cd9 | ||
|
|
4239055454 | ||
|
|
f188ddfb34 | ||
|
|
fb1738932b | ||
|
|
f8be5a76e6 | ||
|
|
5c6a72e3cf | ||
|
|
7b78078ed2 | ||
|
|
6cfaf3f70d | ||
|
|
2942903d0e | ||
|
|
38a21ea191 | ||
|
|
41b310c2d5 | ||
|
|
499916b996 | ||
|
|
627b003a05 | ||
|
|
385fc83f46 | ||
|
|
f82247c42c | ||
|
|
084b3aab89 | ||
|
|
71f98c40f0 | ||
|
|
2a4aa9281c | ||
|
|
35d63a7847 | ||
|
|
d21ff24ea1 | ||
|
|
68bd93e1ed | ||
|
|
a461d9e738 | ||
|
|
ebd9d2a7d5 | ||
|
|
7eacea9da9 | ||
|
|
dcf083ef3b | ||
|
|
a6c0bb202d | ||
|
|
97d8761914 | ||
|
|
df0136ce0b | ||
|
|
a5e58536dd | ||
|
|
1bde4f2cdf | ||
|
|
3a785e5026 | ||
|
|
865cdcb89c | ||
|
|
5ded5cbf1e | ||
|
|
e1807231ce | ||
|
|
8fdf726a66 | ||
|
|
2287c49139 | ||
|
|
9a89596ad2 | ||
|
|
0fd1b05b01 | ||
|
|
2794764d5a | ||
|
|
bc026593d6 | ||
|
|
ed58f65184 | ||
|
|
5060b27a9e | ||
|
|
ea6f24e861 | ||
|
|
ebd6d49a81 | ||
|
|
8ab6ae5897 | ||
|
|
30e8440b25 | ||
|
|
15f5f96693 | ||
|
|
05c3f31362 | ||
|
|
83e992b238 | ||
|
|
96848883fe | ||
|
|
f3625bee61 | ||
|
|
0cba7b9525 | ||
|
|
2ea068de1b | ||
|
|
db755a6a19 | ||
|
|
36f809fb50 | ||
|
|
9e47393643 | ||
|
|
a32e31ffa1 | ||
|
|
8d26e9f73c | ||
|
|
3b1acc5474 | ||
|
|
a8283001d9 | ||
|
|
e070260c5c | ||
|
|
f6912ec23c | ||
|
|
fb218fe016 | ||
|
|
bc713acafd | ||
|
|
6430e747f9 | ||
|
|
faf042a82b | ||
|
|
e9928b4beb | ||
|
|
754c805061 | ||
|
|
f1c96843ee | ||
|
|
a433a49e28 | ||
|
|
e97551818a | ||
|
|
84e2fcc050 | ||
|
|
73f52cfb94 | ||
|
|
f198f39f37 | ||
|
|
7a4d317755 | ||
|
|
ee42211bc6 | ||
|
|
76b9e509d1 | ||
|
|
404a634b92 | ||
|
|
0251d644a8 | ||
|
|
b8506d0d56 | ||
|
|
af3c520d3e | ||
|
|
050cd772be | ||
|
|
2a5606e437 | ||
|
|
08d7e51efb | ||
|
|
2d48caa74b | ||
|
|
9f440d0b50 | ||
|
|
e51e1b6080 | ||
|
|
a2605a2cdf | ||
|
|
0d356f90ed | ||
|
|
1eed9669f1 | ||
|
|
ce4f9b08a9 | ||
|
|
0f3e005a92 | ||
|
|
ff692952c4 | ||
|
|
0b716118c7 | ||
|
|
86cdfaccc4 | ||
|
|
78f1e34029 | ||
|
|
d222b957af | ||
|
|
eec702e4a1 | ||
|
|
07e8248928 | ||
|
|
d91c366c2f | ||
|
|
5846d12ea1 | ||
|
|
f3bc7168ed | ||
|
|
32d2abdf34 | ||
|
|
ac69acb3c2 | ||
|
|
b031d3fcd1 | ||
|
|
b697546476 | ||
|
|
b8f0cf3644 | ||
|
|
14cbddd007 | ||
|
|
48a415bc8f | ||
|
|
cb770c1d7e | ||
|
|
899943bfcf | ||
|
|
a1bc3d9285 | ||
|
|
4d02d0275f | ||
|
|
83e93876d5 | ||
|
|
027aba7f3b | ||
|
|
ada687a4cc | ||
|
|
65989e7e38 | ||
|
|
2df5d94697 | ||
|
|
22ab3c938f | ||
|
|
2bfc464444 | ||
|
|
4c1a6f5840 | ||
|
|
3e7c87a84c | ||
|
|
4c07fd1da1 | ||
|
|
e76ebde6d8 | ||
|
|
f79d4b1b3b | ||
|
|
77d2dcd97f | ||
|
|
1ddb473deb | ||
|
|
f4f495ee21 | ||
|
|
19526e128f | ||
|
|
b65b53b200 | ||
|
|
71f8682337 | ||
|
|
76985fff5b | ||
|
|
881c7ce2d7 | ||
|
|
e3fbc91abf | ||
|
|
51b68a7c20 | ||
|
|
f5ed10d857 | ||
|
|
3e101f6941 | ||
|
|
b2d2f656fd | ||
|
|
848b6326a9 | ||
|
|
ed80938b8c | ||
|
|
255b65d182 | ||
|
|
b68a6794cd | ||
|
|
141b2d5d3f | ||
|
|
f2013d8cd0 | ||
|
|
e7dc79859d | ||
|
|
ec8975ac6a | ||
|
|
40e5dbff3d | ||
|
|
43ea3db02c | ||
|
|
a061018150 | ||
|
|
fae966558c | ||
|
|
32be79ee56 | ||
|
|
89ab5616c0 | ||
|
|
a245d5fa46 | ||
|
|
479523abf9 | ||
|
|
219711129e | ||
|
|
895c1c7286 | ||
|
|
41523b68ba | ||
|
|
8c25ee2e19 | ||
|
|
0a86653085 | ||
|
|
773fee315a | ||
|
|
afcb91f59b | ||
|
|
ff33f38bea | ||
|
|
43e939c0e3 | ||
|
|
c60475b293 | ||
|
|
11f10b4f45 | ||
|
|
01a7339625 | ||
|
|
abc299c1d3 | ||
|
|
96dc045382 | ||
|
|
fe42d931da | ||
|
|
dc6694fb84 | ||
|
|
d31615c58f | ||
|
|
317b5e8013 | ||
|
|
52590ac850 | ||
|
|
d8fbf887b8 | ||
|
|
54d0f6fc7b | ||
|
|
9436dc7afe | ||
|
|
248b489b1b | ||
|
|
39a1ce4eee | ||
|
|
ebe158d8cb | ||
|
|
fd4607b4f5 | ||
|
|
fea6b11ad1 | ||
|
|
42727fcbc6 | ||
|
|
8012fbd5cf | ||
|
|
b029e5a5b5 | ||
|
|
0d995c56a8 | ||
|
|
5948539551 | ||
|
|
f6b77c7456 | ||
|
|
fcaece85e9 | ||
|
|
375094151d | ||
|
|
4dd831424a | ||
|
|
4868b3c336 | ||
|
|
fa67121665 | ||
|
|
78dd8b1d8f | ||
|
|
47ec81408e | ||
|
|
31806862b0 | ||
|
|
2353a0991b | ||
|
|
f57a46308d | ||
|
|
eb707096f7 | ||
|
|
789b526c61 | ||
|
|
be9259d952 | ||
|
|
5235c025fe | ||
|
|
65e4ae83b9 | ||
|
|
53e3cc7c7e | ||
|
|
b6595fb586 | ||
|
|
7e21e27ca9 | ||
|
|
0b51b6b570 | ||
|
|
ba0de72a9d | ||
|
|
5ec1d2c2e1 | ||
|
|
a3d5c8088b | ||
|
|
9847ca129a | ||
|
|
e6eed2dfb0 | ||
|
|
e3e8c9cdcb | ||
|
|
9b4628455c | ||
|
|
df8bc12464 | ||
|
|
b40ba825c2 | ||
|
|
226be2e2b2 | ||
|
|
2811d46a38 | ||
|
|
4ed51791e0 | ||
|
|
89b6766cd6 | ||
|
|
1ddfbc532c | ||
|
|
88d1dd86e5 | ||
|
|
a6328cf2c1 | ||
|
|
06e11dc4be | ||
|
|
ae983e6fd7 | ||
|
|
3e643aad85 | ||
|
|
2fb055f43d | ||
|
|
8016ad1cec | ||
|
|
4eb9454ceb | ||
|
|
e6a70db254 | ||
|
|
0bd64f17af | ||
|
|
709eecb21f | ||
|
|
fd7a1af7ff | ||
|
|
42370f0547 | ||
|
|
b302ebd6ab | ||
|
|
7f30adeb42 | ||
|
|
7997fd490b | ||
|
|
e4f9fb824e | ||
|
|
473e463c56 | ||
|
|
1844fd6d61 | ||
|
|
4c57647203 | ||
|
|
d89293b606 | ||
|
|
d77edf70e9 | ||
|
|
f769ea8f30 | ||
|
|
59d29f37ac | ||
|
|
34040d8e44 | ||
|
|
42666e2054 | ||
|
|
105d53025e | ||
|
|
8892204f24 | ||
|
|
9d7c6b85d0 | ||
|
|
8ee351838d | ||
|
|
7856124df0 | ||
|
|
961450b2ac | ||
|
|
1fddefa072 | ||
|
|
ab18cb1848 | ||
|
|
3b0c0fcd7e | ||
|
|
2a45842169 | ||
|
|
b45eafa53c | ||
|
|
692e6e4b5b | ||
|
|
82b0ec8c30 | ||
|
|
9ab7c024f8 | ||
|
|
d5217fd4b6 | ||
|
|
62dd6effa8 | ||
|
|
3ffd0956d4 | ||
|
|
94031f8ef9 | ||
|
|
f69d25631d | ||
|
|
053d2ae69c | ||
|
|
c89316e52c | ||
|
|
7612068675 | ||
|
|
3043d258b3 | ||
|
|
04a248f4d1 | ||
|
|
e6dc547624 | ||
|
|
ccfc501e82 | ||
|
|
c4d4cd48f6 | ||
|
|
61d1398f5b | ||
|
|
a73c931723 | ||
|
|
a562b22f60 | ||
|
|
291d3cd9a2 | ||
|
|
b16763f8d4 | ||
|
|
50497cea03 | ||
|
|
428462ddf3 | ||
|
|
59d190a2bd | ||
|
|
0508561686 | ||
|
|
a79d002402 | ||
|
|
d0b7dde4e9 | ||
|
|
8a893bdaf0 | ||
|
|
59183e938a | ||
|
|
0649ba9aa6 | ||
|
|
7b918060c4 | ||
|
|
4683f1fa44 | ||
|
|
6e4dff6960 | ||
|
|
e0192789e9 | ||
|
|
95cfa9e453 | ||
|
|
857cee10cf | ||
|
|
1ebed7804c | ||
|
|
7d217e37ce | ||
|
|
ad743a2143 | ||
|
|
8caf6a6271 | ||
|
|
f6fede9ee4 | ||
|
|
5f9b3a9659 | ||
|
|
9caa44cfab | ||
|
|
5e19722491 | ||
|
|
bed3b62e0d | ||
|
|
88f893e2ce | ||
|
|
08ae195cdb | ||
|
|
dee7403f69 | ||
|
|
6ed05b3f00 | ||
|
|
b106b774f8 | ||
|
|
f4d8fa94d7 | ||
|
|
e9d8eeedef | ||
|
|
d8b1efcba7 | ||
|
|
3fa5228d22 | ||
|
|
d0f16e4757 | ||
|
|
f24edf67fa | ||
|
|
22de7513c1 | ||
|
|
e496222a03 | ||
|
|
a141ec0c68 | ||
|
|
2249d8d9df | ||
|
|
9da732d88c | ||
|
|
9c3aa5409e | ||
|
|
6321ff9f62 | ||
|
|
655a24acf0 | ||
|
|
3fbf1286e6 | ||
|
|
91b5ba5e92 | ||
|
|
acb446d26e | ||
|
|
00e5c0d7f8 | ||
|
|
ee366446b6 | ||
|
|
4b0024ed27 | ||
|
|
8040967e47 | ||
|
|
a873f47d2e | ||
|
|
ad4c6999e8 | ||
|
|
e7484c16a3 | ||
|
|
14daab576b | ||
|
|
c841ec97a8 | ||
|
|
4596b7162e | ||
|
|
08df33d7a3 | ||
|
|
3d5cee55d8 | ||
|
|
60382df63f | ||
|
|
64c233f3f4 | ||
|
|
ccdca1ffb4 | ||
|
|
d07b6188e5 | ||
|
|
9e77240b64 | ||
|
|
f76f45124e | ||
|
|
c8feaad7a9 | ||
|
|
a95cbda846 | ||
|
|
6ae8c254a0 | ||
|
|
3dc837c370 | ||
|
|
770028aad5 | ||
|
|
28f9716eff | ||
|
|
db21eb5dec | ||
|
|
c0f65f6dc3 | ||
|
|
578b85584c | ||
|
|
8935837cda | ||
|
|
f18a8c5a35 | ||
|
|
f2e7a41175 | ||
|
|
138591c449 | ||
|
|
424a8e5263 | ||
|
|
a492d23019 | ||
|
|
a23248847a | ||
|
|
bebfc07d84 | ||
|
|
5826a21456 | ||
|
|
4dc7317010 | ||
|
|
89b9218031 | ||
|
|
112a45a230 | ||
|
|
ad8d7aaf75 | ||
|
|
475ec4944d | ||
|
|
d61bf89969 | ||
|
|
4d33aff107 | ||
|
|
99ee41c490 | ||
|
|
b703c01d4c | ||
|
|
22f51336aa | ||
|
|
9bf646aed0 | ||
|
|
ac508aef57 | ||
|
|
1d2fe9b62d | ||
|
|
b3a71e3f1c | ||
|
|
1c46115433 | ||
|
|
7a13189ce4 | ||
|
|
d01441ca0a | ||
|
|
32f86e38ad | ||
|
|
6d9205cfe0 | ||
|
|
c6e3dbf887 | ||
|
|
5581a9c2b7 | ||
|
|
2fcd24b2d3 | ||
|
|
052ca9b4a7 | ||
|
|
9cdb8f3110 | ||
|
|
cc4549134a | ||
|
|
74bfe4baa3 | ||
|
|
58b58b4b48 | ||
|
|
ef24f35ff8 | ||
|
|
e19839dfce | ||
|
|
d5926b08e0 | ||
|
|
8b4885d21d | ||
|
|
bdc7839ff2 | ||
|
|
4c94cee54c | ||
|
|
a511c54c5c | ||
|
|
95ae88d3e7 | ||
|
|
2e9e3a7564 | ||
|
|
874a432c69 | ||
|
|
68c6fc2a38 | ||
|
|
caded6070d | ||
|
|
6f3eb32937 | ||
|
|
b932e22849 | ||
|
|
d17c99f63f | ||
|
|
d8a345a30c | ||
|
|
82df32b0b0 | ||
|
|
35c10181d7 | ||
|
|
c746e18350 | ||
|
|
7c28c226b8 | ||
|
|
2ec202dfa1 | ||
|
|
598b3c83ce | ||
|
|
cdeac07814 | ||
|
|
c37f95169d | ||
|
|
42f4fa92b3 | ||
|
|
c5c5317599 | ||
|
|
7bd4032a28 | ||
|
|
4a3557d8e4 | ||
|
|
03098c88bb | ||
|
|
0b0595a156 | ||
|
|
a7d0dacd12 | ||
|
|
d60747f40f | ||
|
|
4ac7dc831e | ||
|
|
cdd472713d |
315 changed files with 25749 additions and 8400 deletions
117
.gitlab-ci.yml
117
.gitlab-ci.yml
|
|
@ -1,6 +1,15 @@
|
|||
# Create merge request pipelines for open merge requests, branch pipelines
|
||||
# otherwise. This allows MRs for new users to run CI, and prevents duplicate
|
||||
# pipelines for branches with open MRs.
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
stages:
|
||||
- container
|
||||
- container_coverity
|
||||
- build
|
||||
- analysis
|
||||
- pages
|
||||
|
|
@ -10,8 +19,8 @@ variables:
|
|||
# change to build against a different tag or branch of pipewire
|
||||
PIPEWIRE_HEAD: 'master'
|
||||
|
||||
# ci-templates as of Mar 24th 2023
|
||||
.templates_sha: &templates_sha dd90ac0d7a03b574eb4f18d7358083f0c97825f3
|
||||
# ci-templates as of Feb 14th 2025
|
||||
.templates_sha: &templates_sha ef5e4669b7500834a17ffe9277e15fbb6d977fff
|
||||
|
||||
include:
|
||||
- project: 'freedesktop/ci-templates'
|
||||
|
|
@ -27,8 +36,8 @@ include:
|
|||
.fedora:
|
||||
variables:
|
||||
# Update this tag when you want to trigger a rebuild
|
||||
FDO_DISTRIBUTION_TAG: '2023-03-24.1'
|
||||
FDO_DISTRIBUTION_VERSION: '37'
|
||||
FDO_DISTRIBUTION_TAG: '2025-03-05.1'
|
||||
FDO_DISTRIBUTION_VERSION: '41'
|
||||
# findutils: used by the .build script below
|
||||
# dbus-devel: required by pipewire
|
||||
# dbus-daemon: required by GDBus unit tests
|
||||
|
|
@ -80,8 +89,8 @@ include:
|
|||
.alpine:
|
||||
variables:
|
||||
# Update this tag when you want to trigger a rebuild
|
||||
FDO_DISTRIBUTION_TAG: '2023-03-24.1'
|
||||
FDO_DISTRIBUTION_VERSION: '3.15'
|
||||
FDO_DISTRIBUTION_TAG: '2025-03-05.1'
|
||||
FDO_DISTRIBUTION_VERSION: '3.21'
|
||||
FDO_DISTRIBUTION_PACKAGES: >-
|
||||
dbus
|
||||
dbus-dev
|
||||
|
|
@ -112,14 +121,23 @@ include:
|
|||
tar xf /tmp/cov-analysis-linux64.tgz ;
|
||||
mv cov-analysis-linux64-* coverity ;
|
||||
rm /tmp/cov-analysis-linux64.tgz
|
||||
only:
|
||||
variables:
|
||||
- $COVERITY
|
||||
|
||||
.not_coverity:
|
||||
except:
|
||||
variables:
|
||||
- $COVERITY
|
||||
.rules_on_success_except_coverity:
|
||||
rules:
|
||||
- if: $COVERITY
|
||||
when: never
|
||||
- when: on_success
|
||||
|
||||
.rules_only_on_coverity:
|
||||
rules:
|
||||
- if: $COVERITY
|
||||
|
||||
.rules_only_on_mr_and_branch:
|
||||
rules:
|
||||
- if: $COVERITY
|
||||
when: never
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH != "master"
|
||||
|
||||
.build:
|
||||
before_script:
|
||||
|
|
@ -136,13 +154,22 @@ include:
|
|||
# Fedora also ships that, but without the test plugins that we need...
|
||||
- git clone --depth=1 --branch="$PIPEWIRE_HEAD"
|
||||
https://gitlab.freedesktop.org/pipewire/pipewire.git
|
||||
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX"
|
||||
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled
|
||||
-Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled
|
||||
-Dvulkan=disabled -Dgstreamer=disabled -Dsystemd=disabled
|
||||
-Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled
|
||||
-Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled
|
||||
-Decho-cancel-webrtc=disabled -Dsession-managers=[]
|
||||
# Set build options based on PipeWire version
|
||||
- |
|
||||
case "$PIPEWIRE_HEAD" in
|
||||
1.0|1.2|1.4)
|
||||
export PIPEWIRE_BUILD_OPTIONS="-Dsystemd=disabled"
|
||||
;;
|
||||
*)
|
||||
export PIPEWIRE_BUILD_OPTIONS="-Dlibsystemd=disabled"
|
||||
;;
|
||||
esac
|
||||
- meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX" $PIPEWIRE_BUILD_OPTIONS
|
||||
-Dpipewire-alsa=disabled -Dpipewire-jack=disabled -Dalsa=disabled
|
||||
-Dv4l2=disabled -Djack=disabled -Dbluez5=disabled -Dvulkan=disabled
|
||||
-Dgstreamer=disabled -Ddocs=disabled -Dman=disabled -Dexamples=disabled
|
||||
-Dpw-cat=disabled -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled
|
||||
-Davahi=disabled -Decho-cancel-webrtc=disabled -Dsession-managers=[]
|
||||
-Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
|
||||
- ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
|
||||
# misc environment only for wireplumber
|
||||
|
|
@ -164,6 +191,7 @@ include:
|
|||
|
||||
container_fedora:
|
||||
extends:
|
||||
- .rules_on_success_except_coverity
|
||||
- .fedora
|
||||
- .fdo.container-build@fedora
|
||||
stage: container
|
||||
|
|
@ -172,6 +200,7 @@ container_fedora:
|
|||
|
||||
container_ubuntu:
|
||||
extends:
|
||||
- .rules_only_on_mr_and_branch
|
||||
- .ubuntu
|
||||
- .fdo.container-build@ubuntu
|
||||
stage: container
|
||||
|
|
@ -180,6 +209,7 @@ container_ubuntu:
|
|||
|
||||
container_alpine:
|
||||
extends:
|
||||
- .rules_only_on_mr_and_branch
|
||||
- .alpine
|
||||
- .fdo.container-build@alpine
|
||||
stage: container
|
||||
|
|
@ -188,17 +218,18 @@ container_alpine:
|
|||
|
||||
container_coverity:
|
||||
extends:
|
||||
- .rules_only_on_coverity
|
||||
- .fedora
|
||||
- .coverity
|
||||
- .fdo.container-build@fedora
|
||||
stage: container_coverity
|
||||
stage: container
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
|
||||
build_on_fedora_with_docs:
|
||||
extends:
|
||||
- .rules_on_success_except_coverity
|
||||
- .fedora
|
||||
- .not_coverity
|
||||
- .fdo.distribution-image@fedora
|
||||
- .build
|
||||
stage: build
|
||||
|
|
@ -207,18 +238,21 @@ build_on_fedora_with_docs:
|
|||
|
||||
build_on_fedora_no_docs:
|
||||
extends:
|
||||
- .rules_only_on_mr_and_branch
|
||||
- .fedora
|
||||
- .not_coverity
|
||||
- .fdo.distribution-image@fedora
|
||||
- .build
|
||||
stage: build
|
||||
variables:
|
||||
BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false
|
||||
parallel:
|
||||
matrix:
|
||||
- PIPEWIRE_HEAD: ['master', '1.4', '1.2', '1.0']
|
||||
|
||||
build_on_ubuntu_with_gir:
|
||||
extends:
|
||||
- .rules_only_on_mr_and_branch
|
||||
- .ubuntu
|
||||
- .not_coverity
|
||||
- .fdo.distribution-image@ubuntu
|
||||
- .build
|
||||
stage: build
|
||||
|
|
@ -227,8 +261,8 @@ build_on_ubuntu_with_gir:
|
|||
|
||||
build_on_ubuntu_no_gir:
|
||||
extends:
|
||||
- .rules_only_on_mr_and_branch
|
||||
- .ubuntu
|
||||
- .not_coverity
|
||||
- .fdo.distribution-image@ubuntu
|
||||
- .build
|
||||
stage: build
|
||||
|
|
@ -237,8 +271,8 @@ build_on_ubuntu_no_gir:
|
|||
|
||||
build_on_alpine:
|
||||
extends:
|
||||
- .rules_only_on_mr_and_branch
|
||||
- .alpine
|
||||
- .not_coverity
|
||||
- .fdo.distribution-image@alpine
|
||||
- .build
|
||||
stage: build
|
||||
|
|
@ -247,6 +281,7 @@ build_on_alpine:
|
|||
|
||||
build_with_coverity:
|
||||
extends:
|
||||
- .rules_only_on_coverity
|
||||
- .fedora
|
||||
- .coverity
|
||||
- .fdo.suffixed-image@fedora
|
||||
|
|
@ -276,16 +311,23 @@ build_with_coverity:
|
|||
shellcheck:
|
||||
extends:
|
||||
- .fedora
|
||||
- .not_coverity
|
||||
- .fdo.distribution-image@fedora
|
||||
stage: analysis
|
||||
script:
|
||||
- shellcheck $(git grep -l "#\!/.*bin/.*sh")
|
||||
rules:
|
||||
- if: $COVERITY
|
||||
when: never
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
changes:
|
||||
- "**/*.sh"
|
||||
- if: $CI_COMMIT_BRANCH != "master"
|
||||
changes:
|
||||
- "**/*.sh"
|
||||
|
||||
linguas_check:
|
||||
extends:
|
||||
- .fedora
|
||||
- .not_coverity
|
||||
- .fdo.distribution-image@fedora
|
||||
stage: analysis
|
||||
script:
|
||||
|
|
@ -294,10 +336,17 @@ linguas_check:
|
|||
- ls *.po | sed s/.po//g | sort > LINGUAS.new
|
||||
- diff -u LINGUAS.sorted LINGUAS.new
|
||||
- rm -f LINGUAS.*
|
||||
rules:
|
||||
- if: $COVERITY
|
||||
when: never
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
changes:
|
||||
- po/*
|
||||
- if: $CI_COMMIT_BRANCH != "master"
|
||||
changes:
|
||||
- po/*
|
||||
|
||||
pages:
|
||||
extends:
|
||||
- .not_coverity
|
||||
stage: pages
|
||||
dependencies:
|
||||
- build_on_fedora_with_docs
|
||||
|
|
@ -307,5 +356,7 @@ pages:
|
|||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
only:
|
||||
- master
|
||||
rules:
|
||||
- if: $COVERITY
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == "master"
|
||||
|
|
|
|||
37
AGENTS.md
Normal file
37
AGENTS.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
## Building and Testing
|
||||
|
||||
- To compile the project: `meson compile -C build` (compiles everything, no target needed)
|
||||
- To run tests: `meson test -C build`
|
||||
- The build artifacts always live in a directory called `build` or `builddir`.
|
||||
If `build` doesn't exist, use `-C builddir` in the meson commands.
|
||||
|
||||
## Git Workflow
|
||||
|
||||
- Main branch: `master`
|
||||
- Always create feature branches for new work
|
||||
- Use descriptive commit messages following project conventions
|
||||
- Reference GitLab MR/issue numbers in commits where applicable
|
||||
- Never commit build artifacts or temporary files
|
||||
- Use `glab` CLI tool for GitLab interactions (MRs, issues, etc.)
|
||||
|
||||
## Making a release
|
||||
|
||||
- Each release always consists of an entry in NEWS.rst, at the top of the file, which describes
|
||||
the changes between the previous release and the current one. In addition, each release is given
|
||||
a unique version number, which is present:
|
||||
1. on the section header of that NEWS.rst entry
|
||||
2. in the project() command in meson.build
|
||||
3. on the commit message of the commit that introduces the above 2 changes
|
||||
4. on the git tag that marks the above commit
|
||||
- In order to make a release:
|
||||
- Begin by analyzing the git history and the merged MRs from GitLab between the previous release
|
||||
and today. GitLab MRs that are relevant always have the new release's version number set as a
|
||||
"milestone"
|
||||
- Create a new entry in NEWS.rst describing the changes, in a similar style and format as the
|
||||
previous entries. Consolidate the changes to larger work items and also reference the relevant
|
||||
gitlab MR that corresponds to each change and/or the gitlab issues that were addressed by each
|
||||
change.
|
||||
- Make sure to move the "Past releases" section header up, so that the only 2 top-level sections
|
||||
are the new release section and the "Past releases" section.
|
||||
- Edit meson.build to change the project version to the new release number
|
||||
- Do not commit anything to git. Let the user review the changes and commit manually.
|
||||
654
NEWS.rst
654
NEWS.rst
|
|
@ -1,5 +1,654 @@
|
|||
WirePlumber 0.5.14
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Additions & Enhancements:
|
||||
|
||||
- Added per-device default volume configuration via the
|
||||
``device.routes.default-{source,sink}-volume`` property, allowing device-specific volume
|
||||
defaults (e.g. a comfortable default for internal speakers or no attenuation for HDMI) (!772)
|
||||
|
||||
- Added Lua 5.5 support; the bundled Lua subproject wrap has also been updated to 5.5.0
|
||||
(!775, !788)
|
||||
|
||||
- Enhanced libcamera monitor to load camera nodes locally within the WirePlumber
|
||||
process instead of the PipeWire daemon, eliminating race conditions that could occur
|
||||
during initial enumeration and hotplug events (!790)
|
||||
|
||||
- Enhanced Bluetooth loopback nodes to always be created when a device supports both
|
||||
A2DP and HSP/HFP profiles, simplifying the logic and making the BT profile autoswitch
|
||||
setting take effect immediately without requiring device reconnection (!782)
|
||||
|
||||
- Enhanced Bluetooth loopback nodes to use ``target.object`` property instead of smart
|
||||
filters, fixing issues that prevented users from setting them as default nodes and
|
||||
also allowing smart filters to be used with them (#898; !792)
|
||||
|
||||
- Enhanced Bluetooth profile autoswitch logic with further robustness improvements,
|
||||
including better headset profile detection using profile name patterns and resolving
|
||||
race conditions by running profile switching after ``device/apply-profile`` in a
|
||||
dedicated event hook (#926, #923; !776, !777, !808)
|
||||
|
||||
- Enhanced wpctl ``set-default`` command to accept virtual nodes (e.g.
|
||||
``Audio/Source/Virtual``) in addition to regular device nodes (#896; !787)
|
||||
|
||||
- Improved stream linking to make the full graph rescan optional when linkable items
|
||||
change, saving CPU on low-end systems and reducing audio startup latency when
|
||||
connecting multiple streams in quick succession (!800)
|
||||
|
||||
- Allowed installation of systemd service units without libsystemd being present,
|
||||
useful for distributions like Alpine Linux that allow systemd service subpackages
|
||||
(!793)
|
||||
|
||||
- Allowed the ``mincore`` syscall in the WirePlumber systemd sandbox, required for
|
||||
Mesa/EGL (e.g. for the libcamera GPUISP pipeline)
|
||||
|
||||
- Allowed passing ``WIREPLUMBER_CONFIG_DIR`` via the ``wp-uninstalled`` script,
|
||||
useful for passing additional configuration paths in an uninstalled environment (!801)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Removed Bluetooth sink loopback node, which was causing issues with KDE and GNOME (!794)
|
||||
|
||||
- Fixed default audio source selection to never automatically use ``Audio/Sink`` nodes
|
||||
as the default source unless explicitly selected by the user (#886; !781)
|
||||
|
||||
- Fixed crash in ``state-stream`` when the Format parameter has a Choice for the
|
||||
number of channels (#903; !795)
|
||||
|
||||
- Fixed BAP Bluetooth device set channel properties, where ``audio.position`` was
|
||||
incorrectly serialized as a pointer address instead of the channel array (!786)
|
||||
|
||||
- Fixed memory leaks in ``wp_interest_event_hook_get_matching_event_types`` and in
|
||||
the Lua ``LocalModule()`` implementation (!784, !810)
|
||||
|
||||
- Fixed HFP HF stream media class being incorrectly assigned due to
|
||||
``api.bluez5.internal=true`` being set on HFP HF streams (!809)
|
||||
|
||||
- Fixed Lua 5.4 compatibility in ``state-stream`` script
|
||||
|
||||
- Updated translations: Bulgarian, Georgian, Kazakh, Swedish
|
||||
|
||||
Past releases
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
WirePlumber 0.5.13
|
||||
..................
|
||||
|
||||
Additions & Enhancements:
|
||||
|
||||
- Added internal filter graph support for audio nodes, allowing users to
|
||||
create audio preprocessing and postprocessing chains without exposing
|
||||
filters to applications, useful for software DSP (!743)
|
||||
|
||||
- Added new Lua Properties API that significantly improves performance by
|
||||
avoiding constant serialization between WpProperties and Lua tables,
|
||||
resulting in approximately 40% faster node linking (!757)
|
||||
|
||||
- Added WpIterator Lua API for more efficient parameter enumeration (!746)
|
||||
|
||||
- Added bash completions for wpctl command (!762)
|
||||
|
||||
- Added script to find suitable volume control when using role-based policy,
|
||||
allowing volume sliders to automatically adjust the volume of the currently
|
||||
active role (e.g., ringing, call, media) (!711)
|
||||
|
||||
- Added experimental HDMI channel detection setting to use HDMI ELD
|
||||
information for channel configuration (!749)
|
||||
|
||||
- Enhanced role-based policy to allow setting preferred target sinks for
|
||||
media role loopbacks via ``policy.role-based.preferred-target`` (!754)
|
||||
|
||||
- Enhanced Bluetooth profile autoswitch logic to be more robust and handle
|
||||
saved profiles correctly, including support for loopback sink nodes (!739)
|
||||
|
||||
- Enhanced ALSA monitor to include ``alsa.*`` device properties on nodes for
|
||||
rule matching (!761)
|
||||
|
||||
- Optimized stream node linking for common cases to reduce latency when new
|
||||
audio/video streams are added (!760)
|
||||
|
||||
- Improved event dispatcher performance by using hash table registration for
|
||||
event hooks, eliminating performance degradation as more hooks are
|
||||
registered (!765)
|
||||
|
||||
- Increased audio headroom for VMware and VirtualBox virtual machines (!756)
|
||||
|
||||
- Added setting to prevent restoring "Off" profiles via
|
||||
``session.dont-restore-off-profile`` property (!753)
|
||||
|
||||
- Added support for 128 audio channels when compiled with a recent version of
|
||||
PipeWire (pipewire#4995; CI checks in !768)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed memory leaks and issues in the modem manager module (!770, !764)
|
||||
|
||||
- Fixed MPRIS module incorrectly treating GHashTable as GObject (!759)
|
||||
|
||||
- Fixed warning messages when process files in ``/proc/<pid>/*`` don't exist,
|
||||
particularly when processes are removed quickly (#816, !717)
|
||||
|
||||
- Fixed MONO audio configuration to only apply to device sink nodes, allowing
|
||||
multi-channel mixing in the graph (!769)
|
||||
|
||||
- Fixed event dispatcher hook registration and removal to avoid spurious
|
||||
errors (!747)
|
||||
|
||||
- Improved logging for standard-link activation failures (!744)
|
||||
|
||||
- Simplified event-hook interest matching for better performance (!758)
|
||||
|
||||
WirePlumber 0.5.12
|
||||
..................
|
||||
|
||||
Additions & Enhancements:
|
||||
|
||||
- Added mono audio configuration support via ``node.features.audio.mono``
|
||||
setting that can be changed at runtime with wpctl (!721)
|
||||
|
||||
- Added automatic muting of ALSA devices when a running node is removed,
|
||||
helping prevent loud audio on speakers when headsets are unplugged (!734)
|
||||
|
||||
- Added notifications API module for sending system notifications (!734)
|
||||
|
||||
- Added comprehensive wpctl man page and documentation (!735, #825)
|
||||
|
||||
- Enhanced object interest handling for PipeWire properties on session items (!738)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed race condition during shutdown in the permissions portal module that
|
||||
could cause crashes in GDBus signal handling (!748)
|
||||
|
||||
- Added device validity check in state-routes handling to prevent issues
|
||||
when devices are removed during async operations (!737, #844)
|
||||
|
||||
- Fixed Log.critical undefined function error in device-info-cache (!733)
|
||||
|
||||
- Improved device hook documentation and configuration (!736)
|
||||
|
||||
WirePlumber 0.5.11
|
||||
..................
|
||||
|
||||
Additions & Enhancements:
|
||||
|
||||
- Added modem manager module for tracking voice call status and voice call
|
||||
device profile selection hooks to improve phone call audio routing on
|
||||
mobile devices (!722, !729, #819)
|
||||
|
||||
- Added MPRIS media player pause functionality that automatically pauses
|
||||
media playback when the audio target (e.g. headphones) is removed (!699, #764)
|
||||
|
||||
- Added support for human-readable names and localization of settings in
|
||||
``wireplumber.conf`` with ``wpctl`` displaying localized setting descriptions (!712)
|
||||
|
||||
- Improved default node selection logic to use both session and route
|
||||
priorities when nodes have equal session priorities (!720)
|
||||
|
||||
- Increased USB device priority in the ALSA monitor (!719)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed multiple Lua runtime issues including type confusion bugs, stack
|
||||
overflow prevention, and SPA POD array/choice builders (!723, !728)
|
||||
|
||||
- Fixed proxy object lifecycle management by properly clearing the
|
||||
OWNED_BY_PROXY flag when proxies are destroyed to prevent dangling
|
||||
pointers (!732)
|
||||
|
||||
- Fixed state-routes handling to prevent saving unavailable routes and
|
||||
eliminate race conditions during profile switching (!730, #762)
|
||||
|
||||
- Fixed some memory leaks in the script tester and the settings iterator (!727, !726)
|
||||
|
||||
- Fixed a potential crash caused by module-loopback destroying itself when the
|
||||
pipewire connection is closed (#812)
|
||||
|
||||
- Fixed profile saving behavior in ``wpctl set-profile`` command (#808)
|
||||
|
||||
- Fixed GObject introspection closure annotation
|
||||
|
||||
WirePlumber 0.5.10
|
||||
..................
|
||||
|
||||
Fixed a critical crash in ``linking-utils.haveAvailableRoutes`` that was
|
||||
introduced accidentally in 0.5.9 and caused loss of audio output on affected
|
||||
systems (#797, #799, #800, !713)
|
||||
|
||||
WirePlumber 0.5.9
|
||||
.................
|
||||
|
||||
Additions & Enhancements:
|
||||
|
||||
- Added a new audio node grouping functionality using an external command line
|
||||
tool (!646)
|
||||
|
||||
- The libcamera monitor now supports devices that are not associated with
|
||||
device ids (!701)
|
||||
|
||||
- The wireplumber user systemd service is now associated with dbus.service to
|
||||
avoid strange warnings when dbus exits (!702)
|
||||
|
||||
- Added "SYSLOG_IDENTIFIER", "SYSLOG_FACILITY", "SYSLOG_PID" and "TID" to log
|
||||
messages that are sent to the journal (!709)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed a crash of ``wpctl set-default`` on 32-bit architectures (#773)
|
||||
|
||||
- Fixed a crash when a configuration component had no 'provides' field (#771)
|
||||
|
||||
- Reduced the log level of some messages that didn't need to be as high (!695)
|
||||
|
||||
- Fixed another nil reference issue in the alsa.lua monitor script (!704)
|
||||
|
||||
- Fixed name deduplication of v4l2 and libcamera devices (!705)
|
||||
|
||||
- Fixed an issue with wpctl not being able to save settings sometimes (!708, #749)
|
||||
|
||||
WirePlumber 0.5.8
|
||||
.................
|
||||
|
||||
Additions & Enhancements:
|
||||
|
||||
- Added support for handling UCM SplitPCM nodes in the ALSA monitor, which
|
||||
allows native PipeWire channel remapping using loopbacks for devices that
|
||||
use this feature (!685)
|
||||
|
||||
- Introduced new functions to mark WpSpaDevice child objects as pending.
|
||||
This allows properly associating asynchronously created loopback nodes with
|
||||
their parent WpSpaDevice without losing ObjectConfig events (!687, !689)
|
||||
|
||||
- Improved the node name deduplication logic in the ALSA monitor to prevent
|
||||
node names with .2, .3, etc appended to them in some more cases (!688)
|
||||
|
||||
- Added a new script to populate ``session.services``. This is a step towards
|
||||
implementing detection of features that PipeWire can service (!686)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed an issue that was causing duplicate Bluetooth SCO (HSP/HFP) source
|
||||
nodes to be shown in UIs (#701, !683)
|
||||
|
||||
- In the BlueZ monitor, marked the source loopback node as non-virtual,
|
||||
addressing how it appears on UIs (#729)
|
||||
|
||||
- Disabled stream-restore for device loopback nodes to prevent unwanted
|
||||
property changes (!691)
|
||||
|
||||
- Fixed ``wp_lua_log_topic_copy()`` to correctly copy topic names (#757)
|
||||
|
||||
- Updated script tests to handle differences in object identifiers
|
||||
(``object.serial`` vs ``node.id``), ensuring proper test behavior (#761)
|
||||
|
||||
WirePlumber 0.5.7
|
||||
.................
|
||||
|
||||
Highlights:
|
||||
|
||||
- Fixed an issue that would cause random profile switching when an application
|
||||
was trying to capture from non-Bluetooth devices (#715, #634, !669)
|
||||
|
||||
- Fixed an issue that would cause strange profile selection issues [choices
|
||||
not being remembered or unavailable routes being selected] (#734)
|
||||
|
||||
- Added a timer that delays switching Bluetooth headsets to the HSP/HFP
|
||||
profile, avoiding needless rapid switching when an application is trying to
|
||||
probe device capabilities instead of actually capturing audio (!664)
|
||||
|
||||
- Improved libcamera/v4l2 device deduplication logic to work with more complex
|
||||
devices (!674, !675, #689, #708)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed two memory leaks in module-mixer-api and module-dbus-connection
|
||||
(!672, !673)
|
||||
|
||||
- Fixed a crash that could occur in module-reserve-device (!680, #742)
|
||||
|
||||
- Fixed an issue that would cause the warning "[string "alsa.lua"]:182:
|
||||
attempt to concatenate a nil value (local 'node_name')" to appear in the
|
||||
logs when an ALSA device was busy, breaking node name deduplication (!681)
|
||||
|
||||
- Fixed an issue that could make find-preferred-profile.lua crash instead of
|
||||
properly applying profile priority rules (#751)
|
||||
|
||||
WirePlumber 0.5.6
|
||||
.................
|
||||
|
||||
Additions:
|
||||
|
||||
- Implemented before/after dependencies for components, to ensure correct
|
||||
load order in custom configurations (#600)
|
||||
|
||||
- Implemented profile inheritance in the configuration file. This allows
|
||||
profiles to inherit all the feature specifications of other profiles, which
|
||||
is useful to avoid copying long lists of features just to make small changes
|
||||
|
||||
- Added multi-instance configuration profiles, tested and documented them
|
||||
|
||||
- Added a ``main-systemwide`` profile, which is now the default for instances
|
||||
started via the system-wide systemd service and disables features that
|
||||
depend on the user session (#608)
|
||||
|
||||
- Added a ``wp_core_connect_fd`` method, which allows making a connection to
|
||||
PipeWire via an existing open socket (useful for portal-based connections)
|
||||
|
||||
Fixes:
|
||||
|
||||
- The Bluetooth auto-switch script now uses the common event source object
|
||||
managers, which should improve its stability (!663)
|
||||
|
||||
- Fix an issue where switching between Bluetooth profiles would temporarily
|
||||
link active audio streams to the internal speakers (!655)
|
||||
|
||||
WirePlumber 0.5.5
|
||||
.................
|
||||
|
||||
Highlights:
|
||||
|
||||
- Hotfix release to address crashes in the Bluetooth HSP/HFP autoswitch
|
||||
functionality that were side-effects of some changes that were part
|
||||
of the role-based linking policy (#682)
|
||||
|
||||
Improvements:
|
||||
|
||||
- wpctl will now properly show a '*' in front of sink filters when they are
|
||||
selected as the default sink (!660)
|
||||
|
||||
WirePlumber 0.5.4
|
||||
.................
|
||||
|
||||
Highlights:
|
||||
|
||||
- Refactored the role-based linking policy (previously known also as
|
||||
"endpoints" or "virtual items" policy) to blend in with the standard desktop
|
||||
policy. It is now possible use role-based sinks alongside standard desktop
|
||||
audio operations and they will only be used for streams that have a
|
||||
"media.role" defined. It is also possible to force streams to have a
|
||||
media.role, using a setting. Other features include: blending with smart
|
||||
filters in the graph and allowing hardware DSP nodes to be also used easily
|
||||
instead of requiring software loopbacks for all roles. (#610, !649)
|
||||
|
||||
Improvements:
|
||||
|
||||
- Filters that are not declared as smart will now behave again as normal
|
||||
application streams, instead of being treated sometimes differently (!657)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed an issue that would cause WirePlumber to crash at startup if an
|
||||
empty configuration file was present in one of the search paths (#671)
|
||||
|
||||
- Fixed Bluetooth profile auto-switching when a filter is permanently linked
|
||||
to the Bluetooth source (!650)
|
||||
|
||||
- Fixed an issue in the software-dsp script that would cause DSP filters to
|
||||
stay around and cause issues after their device node was destroyed (!651)
|
||||
|
||||
- Fixed an issue in the autoswitch-bluetooth-profile script that could cause
|
||||
an infinite loop of switching between profiles (!652, #617)
|
||||
|
||||
- Fixed a rare issue that could cause WirePlumber to crash when dealing with
|
||||
a device object that didn't have the "device.name" property set (#674)
|
||||
|
||||
WirePlumber 0.5.3
|
||||
.................
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed a long standing issue that would cause many device nodes to have
|
||||
inconsistent naming, with a '.N' suffix (where N is a number >= 2) being
|
||||
appended at seemingly random times (#500)
|
||||
|
||||
- Fixed an issue that would cause unavailable device profiles to be selected
|
||||
if they were previously stored in the state file, sometimes requiring users
|
||||
to manually remove the state file to get things working again (#613)
|
||||
|
||||
- Fixed an occasional crash that could sometimes be triggered by hovering
|
||||
the volume icon on the KDE taskbar, and possibly other similar actions
|
||||
(#628, !644)
|
||||
|
||||
- Fixed camera device deduplication logic when the same device is available
|
||||
through both V4L2 and libcamera, and the libcamera one groups multiple V4L2
|
||||
devices together (#623, !636)
|
||||
|
||||
- Fixed applying the default volume on streams that have no volume previously
|
||||
stored in the state file (#655)
|
||||
|
||||
- Fixed an issue that would prevent some camera nodes - in some cases -
|
||||
from being destroyed when the camera device is removed (#640)
|
||||
|
||||
- Fixed an issue that would cause video stream nodes to be linked with audio
|
||||
smart filters, if smart audio filters were configured (!647)
|
||||
|
||||
- Fixed an issue that would cause WP to re-activate device profiles even
|
||||
though they were already active (!639)
|
||||
|
||||
- Configuration files in standard JSON format (starting with a '{', among
|
||||
other things) are now correctly parsed (#633)
|
||||
|
||||
- Fixed overriding non-container values when merging JSON objects (#653)
|
||||
|
||||
- Functions marked with WP_PRIVATE_API are now also marked as
|
||||
non-introspectable in the gobject-introspection metadata (#599)
|
||||
|
||||
Improvements:
|
||||
|
||||
- Logging on the systemd journal now includes the log topic and also the log
|
||||
level and location directly on the message string when the log level is
|
||||
high enough, which is useful for gathering additional context in logs
|
||||
submitted by users (!640)
|
||||
|
||||
- Added a video-only profile in wireplumber.conf, for systems where only
|
||||
camera & screensharing are to be used (#652)
|
||||
|
||||
- Improved seat state monitoring so that Bluetooth devices are only enabled
|
||||
when the user is active on a local seat, instead of allowing remote users
|
||||
as well (!641)
|
||||
|
||||
- Improved how main filter nodes are detected for the smart filters (!642)
|
||||
|
||||
- Added Lua method to merge JSON containers (!637)
|
||||
|
||||
WirePlumber 0.5.2
|
||||
.................
|
||||
|
||||
Highlights:
|
||||
|
||||
- Added support for loading configuration files other than the default
|
||||
wireplumber.conf within Lua scripts (!629)
|
||||
|
||||
- Added support for loading single-section configuration files, without
|
||||
fragments (!629)
|
||||
|
||||
- Updated the node.software-dsp script to be able to load filter-chain graphs
|
||||
from external configuration files, which is needed for Asahi Linux audio
|
||||
DSP configuration (!629)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed destroying camera nodes when the camera device is removed (#627, !631)
|
||||
|
||||
- Fixed an issue with Bluetooth BAP device set naming (!632)
|
||||
|
||||
- Fixed an issue caused by the pipewire event loop not being "entered" as
|
||||
expected (!634, #638)
|
||||
|
||||
- A false positive warning about no modules being loaded is now suppressed
|
||||
when using libpipewire >= 1.0.5 (#620)
|
||||
|
||||
- Default nodes can now be selected using priority.driver when
|
||||
priority.session is not set (#642)
|
||||
|
||||
Changes:
|
||||
|
||||
- The library version is now generated following pipewire's versioning scheme:
|
||||
libwireplumber-0.5.so.0.5.2 becomes libwireplumber-0.5.so.0.0502.0 (!633)
|
||||
|
||||
WirePlumber 0.5.1
|
||||
.................
|
||||
|
||||
Highlights:
|
||||
|
||||
- Added a guide documenting how to migrate configuration from 0.4 to 0.5,
|
||||
also available online at:
|
||||
https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/migration.html
|
||||
If you are packaging WirePlumber for a distribution, please consider
|
||||
informing users about this.
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed an odd issue where microphones would stop being usable when a
|
||||
Bluetooth headset was connected in the HSP/HFP profile (#598, !620)
|
||||
|
||||
- Fixed an issue where it was not possible to store the volume/mute state of
|
||||
system notifications (#604)
|
||||
|
||||
- Fixed a rare crash that could occur when a node was destroyed while the
|
||||
'select-target' event was still being processed (!621)
|
||||
|
||||
- Fixed deleting all the persistent settings via ``wpctl --delete`` (!622)
|
||||
|
||||
- Fixed using Bluetooth autoswitch with A2DP profiles that have an input route
|
||||
(!624)
|
||||
|
||||
- Fixed sending an error to clients when linking fails due to a format
|
||||
mismatch (!625)
|
||||
|
||||
Additions:
|
||||
|
||||
- Added a check that prints a verbose warning when old-style 0.4.x Lua
|
||||
configuration files are found in the system. (#611)
|
||||
|
||||
- The "policy-dsp" script, used in Asahi Linux to provide a software DSP
|
||||
for Apple Sillicon devices, has now been ported to 0.5 properly and
|
||||
documented (#619, !627)
|
||||
|
||||
WirePlumber 0.5.0
|
||||
.................
|
||||
|
||||
Changes:
|
||||
|
||||
- Bumped the minimum required version of PipeWire to 1.0.2, because we
|
||||
make use of the 'api.bluez5.internal' property of the BlueZ monitor (!613)
|
||||
|
||||
- Improved the naming of Bluetooth nodes when the auto-switching loopback
|
||||
node is present (!614)
|
||||
|
||||
- Updated the documentation on "settings", the Bluetooth monitor, the Access
|
||||
configuration, the file search locations and added a document on how to
|
||||
modify the configuration file (#595, !616)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fixed checking for available routes when selecting the default node (!609)
|
||||
|
||||
- Fixed an issue that was causing an infinite loop storing routes in the
|
||||
state file (!610)
|
||||
|
||||
- Fixed the interpretation of boolean values in the alsa monitor rules (#586, !611)
|
||||
|
||||
- Fixes a Lua crash when we have 2 smart filters, one with a target and one
|
||||
without (!612)
|
||||
|
||||
- Fixed an issue where the default nodes would not be updated when the
|
||||
currently selected default node became unavailable (#588, !615)
|
||||
|
||||
- Fixed an issue that would cause the Props (volume, mute, etc) of loopbacks
|
||||
and other filter nodes to not be restored at startup (#577, !617)
|
||||
|
||||
- Fixed how some constants were represented in the gobject-introspection file,
|
||||
mostly by converting them from defines to enums (#540, #591)
|
||||
|
||||
- Fixed an issue using WirePlumber headers in other projects due to
|
||||
redefinition of G_LOG_DOMAIN (#571)
|
||||
|
||||
WirePlumber 0.4.90
|
||||
..................
|
||||
|
||||
This is the first release candidate (RC1) of WirePlumber 0.5.0.
|
||||
|
||||
Highlights:
|
||||
|
||||
- The configuration system has been changed back to load files from the
|
||||
WirePlumber configuration directories, such as ``/etc/wireplumber`` and
|
||||
``$XDG_CONFIG_HOME/wireplumber``, unlike in the pre-releases. This was done
|
||||
because issues were observed with installations that use a different prefix
|
||||
for pipewire and wireplumber. If you had a ``wireplumber.conf`` file in
|
||||
``/etc/pipewire`` or ``$XDG_CONFIG_HOME/pipewire``, you should move it to
|
||||
``/etc/wireplumber`` or ``$XDG_CONFIG_HOME/wireplumber`` respectively (!601)
|
||||
|
||||
- The internal base directories lookup system now also respects the
|
||||
``XDG_CONFIG_DIRS`` and ``XDG_DATA_DIRS`` environment variables, and their
|
||||
default values as per the XDG spec, so it is possible to install
|
||||
configuration files also in places like ``/etc/xdg/wireplumber`` and
|
||||
override system-wide data paths (!601)
|
||||
|
||||
- ``wpctl`` now has a ``settings`` subcommand to show, change and delete
|
||||
settings at runtime. This comes with changes in the ``WpSettings`` system to
|
||||
validate settings using a schema that is defined in the configuration file.
|
||||
The schema is also exported on a metadata object, so it is available to any
|
||||
client that wants to expose WirePlumber settings (!599, !600)
|
||||
|
||||
- The ``WpConf`` API has changed to not be a singleton and support opening
|
||||
arbitrary config files. The main config file now needs to be opened prior to
|
||||
creating a ``WpCore`` and passed to the core using a property. The core uses
|
||||
that without letting the underlying ``pw_context`` open and read the default
|
||||
``client.conf``. The core also closes the ``WpConf`` after all components
|
||||
are loaded, which means all the config loading is done early at startup.
|
||||
Finally, ``WpConf`` loads all sections lazily, keeping the underlying files
|
||||
memory mapped until it is closed and merging them on demand (!601, !606)
|
||||
|
||||
WirePlumber 0.4.82
|
||||
..................
|
||||
|
||||
This is a second pre-release of WirePlumber 0.5.0, made available for testing
|
||||
purposes. This is not API/ABI stable yet and there is still pending work to do
|
||||
before the final 0.5.0 release, both in the codebase and the documentation.
|
||||
|
||||
Highlights:
|
||||
|
||||
- Bluetooth auto-switching is now implemented with a virtual source node. When
|
||||
an application links to it, the actual device switches to the HSP/HFP
|
||||
profile to provide the real audio stream. This is a more robust solution
|
||||
that works with more applications and is more user-friendly than the
|
||||
previous application whitelist approach
|
||||
|
||||
- Added support for dynamic log level changes via the PipeWire ``settings``
|
||||
metadata. Also added support for log level patterns in the configuration
|
||||
file
|
||||
|
||||
- The "persistent" (i.e. stored) settings approach has changed to use two
|
||||
different metadata objects: ``sm-settings`` and ``persistent-sm-settings``.
|
||||
Changes in the former are applied in the current session but not stored,
|
||||
while changes in the latter are stored and restored at startup. Some work
|
||||
was also done to expose a ``wpctl`` interface to read and change these
|
||||
settings, but more is underway
|
||||
|
||||
- Several WirePlumber-specific node properties that used to be called
|
||||
``target.*`` have been renamed to ``node.*`` to match the PipeWire
|
||||
convention of ``node.dont-reconnect``. These are also now fully documented
|
||||
|
||||
Other changes:
|
||||
|
||||
- Many documentation updates
|
||||
|
||||
- Added support for SNAP container permissions
|
||||
|
||||
- Fixed multiple issues related to restoring the Route parameter of devices,
|
||||
which includes volume state (#551)
|
||||
|
||||
- Smart filters can now be targetted by specific streams directly when
|
||||
the ``filter.smart.targetable`` property is set (#554)
|
||||
|
||||
- Ported the mechanism to override device profile priorities in the
|
||||
configuration, which is used to re-prioritize Bluetooth codecs
|
||||
|
||||
- WpSettings is no longer a singleton class and there is a built-in component
|
||||
to preload an instance of it
|
||||
|
||||
WirePlumber 0.4.81
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
..................
|
||||
|
||||
This is a preliminary release of WirePlumber 0.5.0, which is made available
|
||||
for testing purposes. Please test it and report feedback (merge requests are
|
||||
|
|
@ -55,9 +704,6 @@ Highlights:
|
|||
the same camera device, which can cause confusion when looking at the list
|
||||
of available cameras in applications.
|
||||
|
||||
Past releases
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
WirePlumber 0.4.17
|
||||
..................
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ WirePlumber
|
|||
.. image:: https://scan.coverity.com/projects/21488/badge.svg
|
||||
:alt: Coverity Scan Build Status
|
||||
|
||||
.. image:: https://img.shields.io/tokei/lines/gitlab.freedesktop.org/pipewire/wireplumber
|
||||
:alt: Lines of code
|
||||
|
||||
.. image:: https://img.shields.io/badge/license-MIT-green
|
||||
:alt: License
|
||||
|
||||
|
|
|
|||
|
|
@ -1240,15 +1240,6 @@ HTML_COLORSTYLE_SAT = 100
|
|||
|
||||
HTML_COLORSTYLE_GAMMA = 80
|
||||
|
||||
# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
|
||||
# page will contain the date and time when the page was generated. Setting this
|
||||
# to YES can help to show when doxygen was last run and thus if the
|
||||
# documentation is up to date.
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||
|
||||
HTML_TIMESTAMP = NO
|
||||
|
||||
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
|
||||
# documentation will contain a main index with vertical navigation menus that
|
||||
# are dynamically created via JavaScript. If disabled, the navigation index will
|
||||
|
|
@ -1543,17 +1534,6 @@ HTML_FORMULA_FORMAT = png
|
|||
|
||||
FORMULA_FONTSIZE = 10
|
||||
|
||||
# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
|
||||
# generated for formulas are transparent PNGs. Transparent PNGs are not
|
||||
# supported properly for IE 6.0, but are supported on all modern browsers.
|
||||
#
|
||||
# Note that when changing this option you need to delete any form_*.png files in
|
||||
# the HTML output directory before the changes have effect.
|
||||
# The default value is: YES.
|
||||
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||
|
||||
FORMULA_TRANSPARENT = YES
|
||||
|
||||
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
|
||||
# to create new LaTeX commands to be used in formulas as building blocks. See
|
||||
# the section "Including formulas" for details.
|
||||
|
|
@ -1865,14 +1845,6 @@ LATEX_HIDE_INDICES = NO
|
|||
|
||||
LATEX_BIB_STYLE = plain
|
||||
|
||||
# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
|
||||
# page will contain the date and time when the page was generated. Setting this
|
||||
# to NO can help when comparing the output of multiple runs.
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
||||
|
||||
LATEX_TIMESTAMP = NO
|
||||
|
||||
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
|
||||
# path from which the emoji images will be read. If a relative path is entered,
|
||||
# it will be relative to the LATEX_OUTPUT directory. If left blank the
|
||||
|
|
@ -2265,23 +2237,6 @@ HAVE_DOT = YES
|
|||
|
||||
DOT_NUM_THREADS = 0
|
||||
|
||||
# When you want a differently looking font in the dot files that doxygen
|
||||
# generates you can specify the font name using DOT_FONTNAME. You need to make
|
||||
# sure dot is able to find the font, which can be done by putting it in a
|
||||
# standard location or by setting the DOTFONTPATH environment variable or by
|
||||
# setting DOT_FONTPATH to the directory containing the font.
|
||||
# The default value is: Helvetica.
|
||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||
|
||||
DOT_FONTNAME = Helvetica
|
||||
|
||||
# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
|
||||
# dot graphs.
|
||||
# Minimum value: 4, maximum value: 24, default value: 10.
|
||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||
|
||||
DOT_FONTSIZE = 10
|
||||
|
||||
# By default doxygen will tell dot to use the default font as specified with
|
||||
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
|
||||
# the path where dot can find it using this tag.
|
||||
|
|
@ -2518,18 +2473,6 @@ DOT_GRAPH_MAX_NODES = 50
|
|||
|
||||
MAX_DOT_GRAPH_DEPTH = 0
|
||||
|
||||
# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
|
||||
# background. This is disabled by default, because dot on Windows does not seem
|
||||
# to support this out of the box.
|
||||
#
|
||||
# Warning: Depending on the platform used, enabling this option may lead to
|
||||
# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
|
||||
# read).
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||
|
||||
DOT_TRANSPARENT = NO
|
||||
|
||||
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
|
||||
# files in one run (i.e. multiple -o and -T options on the command line). This
|
||||
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'WirePlumber'
|
||||
copyright = '2021-2024, Collabora'
|
||||
author = 'Collabora'
|
||||
copyright = '2020-2025, Collabora & contributors'
|
||||
author = 'The WirePlumber Developers'
|
||||
release = '@VERSION@'
|
||||
version = '@VERSION@'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
smartquotes = False
|
||||
|
||||
# -- Breathe configuration ---------------------------------------------------
|
||||
|
||||
extensions = [
|
||||
|
|
@ -43,3 +47,9 @@ html_css_files = ['custom.css']
|
|||
graphviz_output_format = "svg"
|
||||
|
||||
pygments_style = "friendly"
|
||||
|
||||
# -- Options for manual page output -----------------------------------------
|
||||
|
||||
man_pages = [
|
||||
('tools/wpctl', 'wpctl', 'WirePlumber Control CLI', ['The WirePlumber Developers'], 1)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ class DoxygenProcess(object):
|
|||
def __process_element(self, xml):
|
||||
s = ""
|
||||
|
||||
if xml.text and re.search('\S', xml.text):
|
||||
if xml.text and re.search(r'\S', xml.text):
|
||||
s += xml.text
|
||||
for n in xml.getchildren():
|
||||
if n.tag == "emphasis":
|
||||
|
|
@ -143,7 +143,7 @@ class DoxygenProcess(object):
|
|||
s += " - " + self.__process_element(n)
|
||||
if n.tag == "para":
|
||||
p = self.__process_element(n)
|
||||
if re.search('\S', p):
|
||||
if re.search(r'\S', p):
|
||||
s += p + "\n"
|
||||
if n.tag == "ref":
|
||||
s += n.text if n.text else ""
|
||||
|
|
@ -168,7 +168,7 @@ class DoxygenProcess(object):
|
|||
if n.tag == "htmlonly":
|
||||
s += ""
|
||||
if n.tail:
|
||||
if re.search('\S', n.tail):
|
||||
if re.search(r'\S', n.tail):
|
||||
s += n.tail
|
||||
if n.tag.startswith("param"):
|
||||
pass # parameters are handled separately in DoxyFunction::from_memberdef()
|
||||
|
|
@ -319,6 +319,8 @@ class DoxyFunction(DoxyElement):
|
|||
d = normalize_text(d)
|
||||
|
||||
e = DoxyFunction(name, d)
|
||||
if (xml.get("prot") == "private"):
|
||||
e.extra = "(skip)"
|
||||
e.add_brief(xml.find("briefdescription"))
|
||||
e.add_detail(xml.find("detaileddescription"))
|
||||
for p in xml.xpath(".//detaileddescription/*/parameterlist[@kind='param']/parameteritem"):
|
||||
|
|
|
|||
|
|
@ -102,6 +102,26 @@ if build_doc
|
|||
install_dir: wireplumber_doc_dir,
|
||||
build_by_default: true,
|
||||
)
|
||||
|
||||
# Generate man pages directory with sphinx
|
||||
custom_target('manpages',
|
||||
command: [sphinx_p,
|
||||
'-q', # quiet
|
||||
'-E', # rebuild from scratch
|
||||
'-b', 'man', # man page builder
|
||||
'-d', '@PRIVATE_DIR@', # doctrees dir
|
||||
'-c', '@OUTDIR@', # conf.py dir
|
||||
'@CURRENT_SOURCE_DIR@/rst', # source dir
|
||||
'@OUTDIR@', # output directory
|
||||
],
|
||||
depend_files: [
|
||||
sphinx_conf, sphinx_files,
|
||||
],
|
||||
output: ['wpctl.1'],
|
||||
install: true,
|
||||
install_dir: get_option('mandir') / 'man1',
|
||||
build_by_default: true,
|
||||
)
|
||||
endif
|
||||
|
||||
# Build GObject introspection
|
||||
|
|
|
|||
|
|
@ -20,12 +20,11 @@ the various options available.
|
|||
|
||||
configuration/conf_file.rst
|
||||
configuration/components_and_profiles.rst
|
||||
configuration/configuration_option_types.rst
|
||||
configuration/modifying_configuration.rst
|
||||
configuration/migration.rst
|
||||
configuration/features.rst
|
||||
configuration/settings.rst
|
||||
configuration/locations.rst
|
||||
configuration/main.rst
|
||||
configuration/multi_instance.rst
|
||||
configuration/alsa.rst
|
||||
configuration/bluetooth.rst
|
||||
configuration/policy.rst
|
||||
configuration/access.rst
|
||||
|
|
|
|||
|
|
@ -3,56 +3,123 @@
|
|||
Access configuration
|
||||
====================
|
||||
|
||||
wireplumber.conf.d/access.conf
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
WirePlumber includes a "client access" policy which defines access control
|
||||
rules for PipeWire clients.
|
||||
|
||||
Using a similar format as the :ref:`ALSA monitor <config_alsa>`, this
|
||||
configuration file is charged to configure the client objects created by
|
||||
PipeWire.
|
||||
Rules
|
||||
-----
|
||||
|
||||
* *Settings*
|
||||
This policy can be configured with rules that can be used to match clients and
|
||||
apply default permissions to them.
|
||||
|
||||
Example:
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
.. code-block::
|
||||
|
||||
wireplumber.settings = {
|
||||
access-enable-flatpak-portal = true
|
||||
}
|
||||
access.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
access = "flatpak"
|
||||
media.category = "Manager"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
access = "flatpak-manager"
|
||||
default_permissions = "all",
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
access = "flatpak"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
default_permissions = "rx"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
The above example sets to ``true`` the ``access-enable-flatpak-portal``
|
||||
property.
|
||||
Possible permissions are any combination of:
|
||||
|
||||
The list of valid properties are:
|
||||
* ``r``: client is allowed to **read** objects, i.e. "see" them on the registry
|
||||
and list their properties
|
||||
* ``w``: client is allowed to **write** objects, i.e. call methods that modify
|
||||
their state
|
||||
* ``x``: client is allowed to **execute** methods on objects; the ``w`` flag
|
||||
must also be present to call methods that modify the object
|
||||
* ``m``: client is allowed to set **metadata** on objects
|
||||
* ``l``: nodes of this client are allowed to **link** to other nodes that the
|
||||
client can't "see" (i.e. the client doesn't have ``r`` permission on them)
|
||||
|
||||
.. code-block::
|
||||
The special value ``all`` is also supported and it is synonym for ``rwxm``
|
||||
|
||||
access-enable-flatpak-portal = true,
|
||||
Permission Managers
|
||||
-------------------
|
||||
|
||||
Whether to enable the flatpak portal or not.
|
||||
For more advanced use cases, WirePlumber supports *permission managers* that can
|
||||
apply per-object permissions dynamically based on rules and object interests.
|
||||
Permission managers are defined in the ``access.permission-managers`` section
|
||||
and then referenced by name in ``access.rules``.
|
||||
|
||||
* *rules*
|
||||
Example:
|
||||
|
||||
Example::
|
||||
.. code-block::
|
||||
|
||||
access = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
pipewire.access = "flatpak"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
default_permissions = "rx"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
access.permission-managers = [
|
||||
{
|
||||
name = "custom"
|
||||
default_permissions = "all"
|
||||
core_permissions = "rx"
|
||||
rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
media.class = "Audio/Source"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
set-permissions = "-"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
This grants read and execute permissions to all clients that have the
|
||||
``pipewire.access`` property set to ``flatpak``.
|
||||
access.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
application.name = "paplay"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
permission_manager_name = "custom"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Possible permissions are any combination of ``r``, ``w`` and ``x`` for read,
|
||||
write and execute; or ``all`` for all kind of permissions.
|
||||
Each permission manager supports the following properties:
|
||||
|
||||
* ``name``: (required) a unique name used to reference the manager from
|
||||
``access.rules``
|
||||
* ``default_permissions``: the fallback permissions applied to all objects
|
||||
that don't match any rule (applied as ``PW_ID_ANY``)
|
||||
* ``core_permissions``: permissions applied specifically to the PipeWire core
|
||||
object (``PW_ID_CORE``, ID 0). This is useful when you want to allow a
|
||||
client to interact with the core (e.g. enumerate objects, subscribe to
|
||||
events) while restricting access to individual objects. If not set, the
|
||||
``default_permissions`` value is used for the core as well.
|
||||
* ``rules``: a list of match rules with ``set-permissions`` actions that
|
||||
grant specific permissions to objects matching the given constraints
|
||||
|
||||
When both ``default_permissions`` and ``permission_manager_name`` are set in
|
||||
a rule's ``update-props`` action, ``default_permissions`` takes precedence and
|
||||
the permission manager is ignored.
|
||||
|
|
|
|||
|
|
@ -3,384 +3,423 @@
|
|||
ALSA configuration
|
||||
==================
|
||||
|
||||
Modifying the default configuration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
One of the components of WirePlumber is the ALSA monitor. This monitor is
|
||||
responsible for creating PipeWire devices and nodes for all the ALSA cards that
|
||||
are available on the system. It also manages the configuration of these devices.
|
||||
|
||||
ALSA devices are created and managed by the session manager with the *alsa.lua*
|
||||
monitor script. In the default configuration, this script is loaded by
|
||||
``wireplumber.conf.d/alsa.conf``, which also specifies its settings and
|
||||
rules.
|
||||
The ALSA monitor is enabled by default and can be disabled using the
|
||||
``monitor.alsa`` :ref:`feature <config_features>` in the configuration file.
|
||||
|
||||
* *Settings*
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.settings = {
|
||||
alsa_monitor.alsa.jack-device = true
|
||||
alsa_monitor.alsa.reserve = true
|
||||
}
|
||||
|
||||
The above example will configure the ALSA monitor to not enable the JACK
|
||||
device, and do ALSA device reservation using the mentioned DBus interface.
|
||||
|
||||
A list of valid settings are:
|
||||
|
||||
.. code-block::
|
||||
|
||||
alsa_monitor.alsa.jack-device = true
|
||||
|
||||
Creates a JACK device if set to ``true``. This is not enabled by default
|
||||
because it requires that the PipeWire JACK replacement libraries are not used
|
||||
by the session manager, in order to be able to connect to the real JACK
|
||||
server.
|
||||
|
||||
.. code-block::
|
||||
|
||||
alsa_monitor.alsa.reserve = true
|
||||
|
||||
Reserve ALSA devices via *org.freedesktop.ReserveDevice1* on D-Bus.
|
||||
|
||||
.. code-block::
|
||||
|
||||
alsa_monitor.alsa.reserve-priority = -20
|
||||
|
||||
The used ALSA device reservation priority.
|
||||
|
||||
.. code-block::
|
||||
|
||||
alsa_monitor.alsa.reserve-application-name = WirePlumber
|
||||
|
||||
The used ALSA device reservation application name.
|
||||
|
||||
|
||||
* *rules*
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.settings = {
|
||||
alsa_monitor = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
# This matches the needed sound card.
|
||||
device.name = "<sound_card_name>"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
# Apply all the desired device settings here.
|
||||
api.alsa.use-acp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
matches = [
|
||||
# This matches the needed node.
|
||||
{
|
||||
node.name = "<node_name>"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
# Apply all the desired node specific settings here.
|
||||
update-props = {
|
||||
node.nick = "My Node"
|
||||
priority.driver = 100
|
||||
session.suspend-timeout-seconds = 5
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Device settings
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
All the possible settings that you can apply to devices and nodes of the
|
||||
ALSA monitor are described here.
|
||||
|
||||
PipeWire devices correspond to the ALSA cards.
|
||||
The following settings can be configured on devices created by the monitor:
|
||||
|
||||
.. code-block::
|
||||
|
||||
api.alsa.use-acp = true
|
||||
|
||||
Use the ACP (alsa card profile) code to manage the device. This will probe the
|
||||
device and configure the available profiles, ports and mixer settings. The
|
||||
code to do this is taken directly from PulseAudio and provides devices that
|
||||
look and feel exactly like the PulseAudio devices.
|
||||
|
||||
.. code-block::
|
||||
|
||||
api.alsa.use-ucm = true
|
||||
|
||||
By default, the UCM configuration is used when it is available for your device.
|
||||
With this option you can disable this and use the ACP profiles instead.
|
||||
|
||||
.. code-block::
|
||||
|
||||
api.alsa.soft-mixer = false
|
||||
|
||||
Setting this option to true will disable the hardware mixer for volume control
|
||||
and mute. All volume handling will then use software volume and mute, leaving
|
||||
the hardware mixer untouched. The hardware mixer will still be used to mute
|
||||
unused audio paths in the device.
|
||||
|
||||
.. code-block::
|
||||
|
||||
api.alsa.ignore-dB = false
|
||||
|
||||
Setting this option to true will ignore the decibel setting configured by the
|
||||
driver. Use this when the driver reports wrong settings.
|
||||
|
||||
.. code-block::
|
||||
|
||||
device.profile-set = "profileset-name"
|
||||
|
||||
This option can be used to select a custom profile set name for the device.
|
||||
Usually this is configured in Udev rules but it can also be specified here.
|
||||
|
||||
.. code-block::
|
||||
|
||||
device.profile = "default profile name"
|
||||
|
||||
The default active profile name.
|
||||
|
||||
.. code-block::
|
||||
|
||||
api.acp.auto-profile = false
|
||||
|
||||
Automatically select the best profile for the device. Normally this option is
|
||||
disabled because the session manager will manage the profile of the device.
|
||||
The session manager can save and load previously selected profiles. Enable
|
||||
this if your session manager does not handle this feature.
|
||||
|
||||
.. code-block::
|
||||
|
||||
api.acp.auto-port = false
|
||||
|
||||
Automatically select the highest priority port that is available. This is by
|
||||
default disabled because the session manager handles the task of selecting and
|
||||
restoring ports. It can, for example, restore previously saved volumes. Enable
|
||||
this here when the session manager does not handle port restore.
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
["api.acp.probe-rate"] = 48000
|
||||
|
||||
Sets the samplerate used for probing the ALSA devices and collecting the profiles
|
||||
and ports.
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
["api.acp.pro-channels"] = 64
|
||||
|
||||
Sets the number of channels to use when probing the Pro Audio profile. Normally,
|
||||
the maximum amount of channels will be used but with this setting this can be
|
||||
reduced, which can make it possible to use other samplerates on some devices.
|
||||
|
||||
Some of the other settings that might be configured on devices:
|
||||
|
||||
.. code-block::
|
||||
|
||||
device.nick = "My Device",
|
||||
device.description = "My Device"
|
||||
|
||||
``device.description`` will show up in most apps when a device name is shown.
|
||||
|
||||
Node Settings
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Nodes are sinks or sources in the PipeWire graph. They correspond to the ALSA
|
||||
devices. In addition to the generic stream node configuration options, there are
|
||||
some alsa specific options as well:
|
||||
|
||||
.. code-block::
|
||||
|
||||
priority.driver = 2000
|
||||
|
||||
This configures the node driver priority. Nodes with higher priority will be
|
||||
used as a driver in the graph. Other nodes with lower priority will have to
|
||||
resample to the driver node when they are joined in the same graph. The default
|
||||
value is set based on some heuristics.
|
||||
|
||||
.. code-block::
|
||||
|
||||
priority.session = 1200
|
||||
|
||||
This configures the priority of the node when selecting a default node.
|
||||
Higher priority nodes will be more likely candidates as a default node.
|
||||
The monitor, as with all device monitors, is implemented as a SPA plugin and is
|
||||
part of PipeWire. WirePlumber merely loads the plugin and lets it do its work.
|
||||
The plugin then monitors UDev and creates device and node objects for all the
|
||||
ALSA cards that are available on the system.
|
||||
|
||||
.. note::
|
||||
|
||||
By default, sources have a ``priority.session`` value around 1600-2000 and
|
||||
sinks have a value around 600-1000. If you are increasing the priority of a
|
||||
sink, it is **not advised** to use a value higher than 1500, as it may cause
|
||||
a sink's monitor to be selected as a default source.
|
||||
One thing worth remembering here is that in ALSA, a "card" represents a
|
||||
physical sound controller device, and a "device" is a logical access point
|
||||
that represents a set of inputs and/or outputs that are part of the card. In
|
||||
PipeWire, a "device" is the direct equivalent of an ALSA "card" and a "node"
|
||||
is almost equivalent (close, but not quite) of an ALSA "device".
|
||||
|
||||
Properties
|
||||
----------
|
||||
|
||||
The ALSA monitor SPA plugin (``api.alsa.enum.udev``) supports properties that
|
||||
can be used to configure it when it is loaded. These properties can be set in
|
||||
the ``monitor.alsa.properties`` section of the WirePlumber configuration file.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
node.pause-on-idle = false
|
||||
monitor.alsa.properties = {
|
||||
alsa.use-acp = true
|
||||
}
|
||||
|
||||
Pause-on-idle will stop the node when nothing is linked to it anymore.
|
||||
This is by default false because some devices cause a pop when they are
|
||||
opened/closed. The node will, normally, pause and suspend after a timeout
|
||||
(see suspend-node.lua).
|
||||
.. describe:: alsa.use-acp
|
||||
|
||||
A boolean that controls whether the ACP (alsa card profile) code is to be
|
||||
the default manager of the device. This will probe the device and configure
|
||||
the available profiles, ports and mixer settings. The code to do this is
|
||||
taken directly from PulseAudio and provides devices that look and feel
|
||||
exactly like the PulseAudio devices.
|
||||
|
||||
Rules
|
||||
-----
|
||||
|
||||
When device and node objects are created by the ALSA monitor, they can be
|
||||
configured using rules. These rules allow matching the existing properties of
|
||||
these objects and updating them with new values. This is the main way of
|
||||
configuring ALSA device settings.
|
||||
|
||||
These rules can be set in the ``monitor.alsa.rules`` section of the WirePlumber
|
||||
configuration file.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
session.suspend-timeout-seconds = 5 -- 0 disables suspend
|
||||
monitor.alsa.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
# This matches the value of the 'device.name' property of the device.
|
||||
device.name = "~alsa_card.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
# Apply all the desired device settings here.
|
||||
api.alsa.use-acp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
matches = [
|
||||
# This matches the value of the 'node.name' property of the node.
|
||||
{
|
||||
node.name = "~alsa_output.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
# Apply all the desired node specific settings here.
|
||||
update-props = {
|
||||
node.nick = "My Node"
|
||||
priority.driver = 100
|
||||
session.suspend-timeout-seconds = 5
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
This option configures a different suspend timeout on the node.
|
||||
By default this is 5 seconds. For some devices (HiFi amplifiers, for example)
|
||||
it might make sense to set a higher timeout because they might require some
|
||||
time to restart after being idle.
|
||||
Device properties
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
A value of 0 disables suspend for a node and will leave the ALSA device busy.
|
||||
The device can then manually be suspended with ``pactl suspend-sink|source``.
|
||||
The following properties can be configured on devices created by the monitor:
|
||||
|
||||
**The following properties can be used to configure the format used by the
|
||||
ALSA device:**
|
||||
.. describe:: api.alsa.use-acp
|
||||
|
||||
.. code-block::
|
||||
Use the ACP (alsa card profile) code to manage this device. This will probe
|
||||
the device and configure the available profiles, ports and mixer settings.
|
||||
The code to do this is taken directly from PulseAudio and provides devices
|
||||
that look and feel exactly like the PulseAudio devices.
|
||||
|
||||
audio.format = "S16LE"
|
||||
:Default value: ``true``
|
||||
:Type: boolean
|
||||
|
||||
By default, PipeWire will use a 32 bits sample format but a different format
|
||||
can be set here.
|
||||
.. describe:: api.alsa.use-ucm
|
||||
|
||||
The Audio rate of a device can be set here:
|
||||
When ACP is enabled and a UCM configuration is available for a device, by
|
||||
default it is used instead of the ACP profiles. This option allows you to
|
||||
disable this and use the ACP profiles instead.
|
||||
|
||||
.. code-block::
|
||||
This option does nothing if ``api.alsa.use-acp`` is set to ``false``.
|
||||
|
||||
audio.rate = 44100
|
||||
:Default value: ``true``
|
||||
:Type: boolean
|
||||
|
||||
By default, the ALSA device will be configured with the same samplerate as the
|
||||
global graph. If this is not supported, or a custom values is set here,
|
||||
resampling will be used to match the graph rate.
|
||||
.. describe:: api.alsa.soft-mixer
|
||||
|
||||
.. code-block::
|
||||
Setting this option to ``true`` will disable the hardware mixer for volume
|
||||
control and mute. All volume handling will then use software volume and mute,
|
||||
leaving the hardware mixer untouched. The hardware mixer will still be used
|
||||
to mute unused audio paths in the device.
|
||||
|
||||
audio.channels = 2
|
||||
audio.position = "FL,FR"
|
||||
:Type: boolean
|
||||
|
||||
By default the channels and their position are determined by the selected
|
||||
Device profile. You can override this setting here and optionally swap or
|
||||
reconfigure the channel positions.
|
||||
.. describe:: api.alsa.ignore-dB
|
||||
|
||||
.. code-block::
|
||||
Setting this option to ``true`` will ignore the decibel setting configured by
|
||||
the driver. Use this when the driver reports wrong settings.
|
||||
|
||||
api.alsa.use-chmap = false
|
||||
:Type: boolean
|
||||
|
||||
Use the channel map as reported by the driver. This is disabled by default
|
||||
because it is often wrong and the ACP code handles this better.
|
||||
.. describe:: device.profile-set
|
||||
|
||||
.. code-block::
|
||||
This option can be used to select a custom ACP profile-set name for the
|
||||
device. This can be configured in UDev rules, but it can also be specified
|
||||
here. The default is to use "default.conf".
|
||||
|
||||
api.alsa.disable-mmap = true
|
||||
:Type: string
|
||||
|
||||
PipeWire will by default access the memory of the device using mmap.
|
||||
This can be disabled and force the usage of the slower read and write access
|
||||
modes in case the mmap support of the device is not working properly.
|
||||
.. describe:: device.profile
|
||||
|
||||
.. code-block::
|
||||
The initial active profile name. The default is to start from the "Off"
|
||||
profile and then let WirePlumber select the best profile based on its
|
||||
policy.
|
||||
|
||||
channelmix.normalize = true
|
||||
:Type: string
|
||||
|
||||
Makes sure that during such mixing & resampling original 0 dB level is
|
||||
preserved, so nothing sounds wildly quieter/louder.
|
||||
.. describe:: api.acp.auto-profile
|
||||
|
||||
.. code-block::
|
||||
Automatically select the best profile for the device. Normally this option is
|
||||
disabled because WirePlumber will manage the profile of the device.
|
||||
WirePlumber can save and load previously selected profiles. Enable this in
|
||||
custom configurations where the relevant WirePlumber components are disabled.
|
||||
|
||||
channelmix.mix-lfe = true
|
||||
:Type: boolean
|
||||
|
||||
Creates "center" channel for X.0 recordings from front stereo on X.1 setups and
|
||||
pushes some low-frequency/bass from "center" from X.1 recordings into front
|
||||
stereo on X.0 setups.
|
||||
.. describe:: api.acp.auto-port
|
||||
|
||||
.. code-block::
|
||||
Automatically select the highest priority port that is available ("port" is a
|
||||
PulseAudio/ACP term, the equivalent of a "Route" in PipeWire). This is by
|
||||
default disabled because WirePlumber handles the task of selecting and
|
||||
restoring Routes. Enable this in custom configurations where the relevant
|
||||
WirePlumber components are disabled.
|
||||
|
||||
monitor.channel-volumes = false
|
||||
:Type: boolean
|
||||
|
||||
By default, the volume of the sink/source does not influence the volume on the
|
||||
monitor ports. Set this option to true to change this. PulseAudio has
|
||||
inconsistent behaviour regarding this option, it applies channel-volumes only
|
||||
when the sink/source is using software volumes.
|
||||
.. describe:: api.acp.probe-rate
|
||||
|
||||
Sets the samplerate used for probing the ALSA devices and collecting the
|
||||
profiles and ports.
|
||||
|
||||
:Type: integer
|
||||
|
||||
.. describe:: api.acp.pro-channels
|
||||
|
||||
Sets the number of channels to use when probing the "Pro Audio" profile.
|
||||
Normally, the maximum amount of channels will be used but with this setting
|
||||
this can be reduced, which can make it possible to use other samplerates on
|
||||
some devices.
|
||||
|
||||
:Type: integer
|
||||
|
||||
Some of the other properties that can be configured on devices:
|
||||
|
||||
.. describe:: device.nick
|
||||
|
||||
A short name for the device.
|
||||
|
||||
.. describe:: device.description
|
||||
|
||||
A longer, user-friendly name of the device. This will show up in most
|
||||
user interfaces as the device's name.
|
||||
|
||||
.. describe:: device.disabled
|
||||
|
||||
Disables the device. PipeWire will remove it from the list of cards or
|
||||
devices.
|
||||
|
||||
:Type: boolean
|
||||
|
||||
Node properties
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
The following properties can be configured on nodes created by the monitor:
|
||||
|
||||
.. describe:: priority.driver
|
||||
|
||||
This configures the node driver priority. Nodes with higher priority will be
|
||||
used as a driver in the graph. Other nodes with lower priority will have to
|
||||
resample to the driver node when they are joined in the same graph. The
|
||||
default value is set based on some heuristics.
|
||||
|
||||
:Type: integer
|
||||
|
||||
.. describe:: priority.session
|
||||
|
||||
This configures the priority of the node when selecting a default node
|
||||
(default sink/source as a link target for streams). Higher priority nodes
|
||||
will be more likely candidates for becoming the default node.
|
||||
|
||||
:Type: integer
|
||||
|
||||
.. note::
|
||||
|
||||
By default, sources have a ``priority.session`` value around 1600-2000 and
|
||||
sinks have a value around 600-1000. If you are increasing the priority of
|
||||
a sink, it is **not advised** to use a value higher than 1500, as it may
|
||||
cause a sink's monitor to be selected as the default source.
|
||||
|
||||
.. describe:: node.pause-on-idle
|
||||
|
||||
Pause the node when nothing is linked to it anymore. This is by default false
|
||||
because some devices make a "pop" sound when they are opened/closed.
|
||||
The node will normally pause and suspend after a timeout (see below).
|
||||
|
||||
:Type: boolean
|
||||
|
||||
.. describe:: session.suspend-timeout-seconds
|
||||
|
||||
This option configures a different suspend timeout on the node. By default
|
||||
this is ``5`` seconds. For some devices (HiFi amplifiers, for example) it
|
||||
might make sense to set a higher timeout because they might require some time
|
||||
to restart after being idle.
|
||||
|
||||
A value of ``0`` disables suspend for a node and will leave the ALSA device
|
||||
busy. The device can then be manually suspended with
|
||||
``pactl suspend-sink|source``.
|
||||
|
||||
:Type: integer
|
||||
|
||||
.. describe:: audio.format
|
||||
|
||||
The sample format of the device. By default, PipeWire will use a 32 bits
|
||||
sample format but a different format can be set here.
|
||||
|
||||
:Type: string (``"S16LE"``, ``"S32LE"``, ``"F32LE"``, ...)
|
||||
|
||||
.. describe:: audio.rate
|
||||
|
||||
The sample rate of the device. By default, the ALSA device will be configured
|
||||
with the same samplerate as the global graph. If this is not supported, or a
|
||||
custom value is set here, resampling will be used to match the graph rate.
|
||||
|
||||
:Type: integer
|
||||
|
||||
.. describe:: audio.channels
|
||||
|
||||
The number of channels of the device. By default the channels and their
|
||||
position are determined by the selected device profile. You can override
|
||||
this setting here.
|
||||
|
||||
:Type: integer
|
||||
|
||||
.. describe:: audio.position
|
||||
|
||||
The position of the channels. By default the number of channels and their
|
||||
position are determined by the selected device profile. You can override
|
||||
this setting here and optionally swap or reconfigure the channel positions.
|
||||
|
||||
:Type: array of strings (example: ``["FL", "FR", "LFE", "FC", "RL", "RR"]``)
|
||||
|
||||
.. describe:: api.alsa.use-chmap
|
||||
|
||||
Use the channel map as reported by the driver. This is disabled by default
|
||||
because it is often wrong and the ACP code handles this better.
|
||||
|
||||
:Type: boolean
|
||||
|
||||
.. describe:: api.alsa.disable-mmap
|
||||
|
||||
Disable the use of mmap for the ALSA device. By default, PipeWire will access
|
||||
the memory of the device using mmap. This can be disabled and force the usage
|
||||
of the slower read and write access modes, in case the mmap support of the
|
||||
device is not working properly.
|
||||
|
||||
:Type: boolean
|
||||
|
||||
.. describe:: channelmix.normalize
|
||||
|
||||
Normalize the channel volumes when mixing & resampling, making sure that the
|
||||
original 0 dB level is preserved so that nothing sounds wildly
|
||||
quieter/louder. This is disabled by default.
|
||||
|
||||
:Type: boolean
|
||||
|
||||
.. describe:: channelmix.mix-lfe
|
||||
|
||||
Creates a "center" channel for X.0 recordings from the front stereo on X.1
|
||||
setups and pushes some low-frequency/bass from the "center" of X.1 recordings
|
||||
into the front stereo on X.0 setups. This is disabled by default.
|
||||
|
||||
:Type: boolean
|
||||
|
||||
.. describe:: monitor.channel-volumes
|
||||
|
||||
By default, the volume of the sink/source does not influence the volume on
|
||||
the monitor ports. Set this option to true to change this. PulseAudio has
|
||||
inconsistent behaviour regarding this option, it applies channel-volumes only
|
||||
when the sink/source is using software volumes.
|
||||
|
||||
:Type: boolean
|
||||
|
||||
.. describe:: node.disabled
|
||||
|
||||
Disables the node. Pipewire will remove it from the list of the nodes.
|
||||
|
||||
:Type: boolean
|
||||
|
||||
ALSA buffer properties
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
......................
|
||||
|
||||
PipeWire uses a timer to consume and produce samples to/from ALSA devices.
|
||||
After every timeout, it queries the device hardware pointers of the device and
|
||||
uses this information to set a new timeout. See also this example program.
|
||||
PipeWire by default uses a timer to consume and produce samples to/from ALSA
|
||||
devices. After every timeout, it queries the hardware pointers of the device and
|
||||
uses this information to set a new timeout. This works well for most devices,
|
||||
but there is a class of devices, so called "batch" devices, that need extra
|
||||
buffering and timing tweaks to work properly. This is because batch devices only
|
||||
get their hardware pointers updated after each hardware interrupt. When the
|
||||
hardware interrupt frequency and the timer frequency are aligned, it is possible
|
||||
for the hardware pointers to be updated just after the timer has expired,
|
||||
resulting in sometimes wrong timing information being returned by the query. In
|
||||
contrast, non-batch devices get pointer updates independent of the interrupt.
|
||||
|
||||
By default, PipeWire handles ALSA batch devices differently from non-batch
|
||||
devices. Batch devices only get their hardware pointers updated after each
|
||||
hardware interrupt. Non-batch devices get updates independent of the interrupt.
|
||||
This means that for batch devices we need to set the interrupt at a sufficiently
|
||||
high frequency (at the cost of CPU usage) while for non-batch devices we want to
|
||||
set the interrupt frequency as low as possible (to save CPU).
|
||||
high frequency, at the cost of CPU usage, while for non-batch devices we want to
|
||||
set the interrupt frequency as low as possible to save CPU. For batch devices
|
||||
we also need to take the extra buffering into account caused by the delayed
|
||||
updates of the hardware pointers.
|
||||
|
||||
For batch devices we also need to take the extra buffering into account caused
|
||||
by the delayed updates of the hardware pointers.
|
||||
.. note::
|
||||
|
||||
Most USB devices are batch devices and will be handled as such by PipeWire by
|
||||
default.
|
||||
Most USB devices are batch devices and will be handled as such by PipeWire by
|
||||
default.
|
||||
|
||||
There are 2 tunable parameters to control the buffering and timeouts in a
|
||||
device
|
||||
device:
|
||||
|
||||
.. code-block::
|
||||
.. describe:: api.alsa.period-size
|
||||
|
||||
api.alsa.period-size = 1024
|
||||
This sets the device interrupt to every period-size samples for non-batch
|
||||
devices and to half of this for batch devices. For batch devices, the other
|
||||
half of the period-size is used as extra buffering to compensate for the
|
||||
delayed update. So, for batch devices, there is an additional period-size/2
|
||||
delay. It makes sense to lower the period-size for batch devices to reduce
|
||||
this delay.
|
||||
|
||||
This sets the device interrupt to every period-size samples for non-batch
|
||||
devices and to half of this for batch devices. For batch devices, the other
|
||||
half of the period-size is used as extra buffering to compensate for the delayed
|
||||
update. So, for batch devices, there is an additional period-size/2 delay.
|
||||
It makes sense to lower the period-size for batch devices to reduce this delay.
|
||||
:Type: integer (samples)
|
||||
|
||||
.. code-block::
|
||||
.. describe:: api.alsa.headroom
|
||||
|
||||
api.alsa.headroom = 0
|
||||
This adds extra delay between the hardware pointers and software pointers.
|
||||
In most cases this can be set to 0. For very bad devices or emulated devices
|
||||
(like in a VM) it might be necessary to increase the headroom value.
|
||||
|
||||
:Type: integer (samples)
|
||||
|
||||
.. describe:: api.alsa.period-num
|
||||
|
||||
This configures the number of periods in the hardware buffer, which controls
|
||||
its size. Note that this is multiplied by the period of the device to
|
||||
determine the size, so for batch devices, the total buffer size is
|
||||
effectively period-num * period-size/2.
|
||||
|
||||
:Type: integer
|
||||
|
||||
This adds extra delay between the hardware pointers and software pointers.
|
||||
In most cases this can be set to 0. For very bad devices or emulated devices
|
||||
(like in a VM) it might be necessary to increase the headroom value.
|
||||
In summary, this is the overview of buffering and timings:
|
||||
|
||||
============== ============================================ ==========================================
|
||||
Property Batch Non-Batch
|
||||
============== ============================================ ==========================================
|
||||
IRQ Frequency api.alsa.period-size/2 api.alsa.period-size
|
||||
Extra Delay api.alsa.headroom + api.alsa.period-size/2 api.alsa.headroom
|
||||
Buffer Size api.alsa.period-num * api.alsa.period-size/2 api.alsa.period-num * api.alsa.period-size
|
||||
============== ============================================ ==========================================
|
||||
|
||||
============== ========================================== =========
|
||||
Property Batch Non-Batch
|
||||
============== ========================================== =========
|
||||
IRQ Frequency api.alsa.period-size/2 api.alsa.period-size
|
||||
Extra Delay api.alsa.headroom + api.alsa.period-size/2 api.alsa.headroom
|
||||
============== ========================================== =========
|
||||
Finally, it is possible to disable the batch device tweaks with:
|
||||
|
||||
It is possible to disable the batch device tweaks with:
|
||||
.. describe:: api.alsa.disable-batch
|
||||
|
||||
.. code-block::
|
||||
This disables the batch device tweaks. It removes the extra delay added of
|
||||
period-size/2 if the device can support this. For batch devices it is also a
|
||||
good idea to lower the period-size (and increase the IRQ frequency) to get
|
||||
smaller batch updates and lower latency.
|
||||
|
||||
api.alsa.disable-batch"] = true
|
||||
|
||||
It removes the extra delay added of period-size/2 if the device can support this.
|
||||
For batch devices it is also a good idea to lower the period-size
|
||||
(and increase the IRQ frequency) to get smaller batch updates and lower latency.
|
||||
:Type: boolean
|
||||
|
||||
ALSA extra latency properties
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
.............................
|
||||
|
||||
Extra internal delay in the DAC and ADC converters of the device itself can be
|
||||
set with the ``latency.internal.*`` properties:
|
||||
|
||||
.. code-block::
|
||||
|
||||
latency.internal.rate"] = 256
|
||||
latency.internal.ns"] = 0
|
||||
latency.internal.rate = 256
|
||||
latency.internal.ns = 0
|
||||
|
||||
You can configure a latency in samples (relative to rate with
|
||||
``latency.internal.rate``) or in nanoseconds (``latency.internal.ns``).
|
||||
|
|
@ -419,44 +458,28 @@ Set the internal latency to 256 samples:
|
|||
remote 0 port 76 changed
|
||||
|
||||
Startup tweaks
|
||||
^^^^^^^^^^^^^^
|
||||
..............
|
||||
|
||||
Some devices need some time before they can report accurate hardware pointer
|
||||
positions. In those cases, an extra start delay can be added that is used to
|
||||
compensate for this startup delay:
|
||||
.. describe:: api.alsa.start-delay
|
||||
|
||||
.. code-block::
|
||||
Some devices need some time before they can report accurate hardware pointer
|
||||
positions. In those cases, an extra start delay can be added to compensate
|
||||
for this startup delay. This sets the startup delay in samples. The default
|
||||
is 0.
|
||||
|
||||
["api.alsa.start-delay"] = 0
|
||||
|
||||
It is unsure when this tunable should be used.
|
||||
:Type: integer (samples)
|
||||
|
||||
IEC958 (S/PDIF) passthrough
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
...........................
|
||||
|
||||
S/PDIF passthrough will only be enabled when the accepted codecs are configured
|
||||
on the ALSA device.
|
||||
.. describe:: iec958.codecs
|
||||
|
||||
This can be done in 3 different ways:
|
||||
S/PDIF passthrough will only be enabled when the accepted codecs are configured
|
||||
on the ALSA device. This can be done by setting the list of supported codecs
|
||||
on this property.
|
||||
|
||||
1. Use pavucontrol and toggle the codecs in the output advanced section.
|
||||
Note that it is possible to also configure this property at runtime, either
|
||||
with tools like pavucontrol or with the ``pw-cli`` tool, like this:
|
||||
``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'``
|
||||
|
||||
2. Modify the ``["iec958.codecs"]`` node property to contain supported codecs.
|
||||
|
||||
Example ``~/.config/wireplumber/main.lua.d/51-alsa-spdif.lua``:
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
table.insert (alsa_monitor.rules, {
|
||||
matches = {
|
||||
{
|
||||
{ "node.name", "matches", "alsa_output.*" },
|
||||
},
|
||||
},
|
||||
apply_properties = {
|
||||
["iec958.codecs"] = "[ PCM DTS AC3 EAC3 TrueHD DTS-HD ]",
|
||||
}
|
||||
})
|
||||
|
||||
3. Use ``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'`` to modify
|
||||
the codecs at runtime.
|
||||
:Type: array of strings (example: ``[ "PCM", "DTS", "AC3", "EAC3", "TrueHD", "DTS-HD" ]``)
|
||||
|
|
|
|||
|
|
@ -3,174 +3,438 @@
|
|||
Bluetooth configuration
|
||||
=======================
|
||||
|
||||
Using the same format as the :ref:`ALSA monitor <config_alsa>`, the
|
||||
configuration file ``wireplumber.conf.d/bluetooth.conf`` is charged
|
||||
to configure the Bluetooth devices and nodes created by WirePlumber.
|
||||
Bluetooth audio and MIDI devices are managed by the BlueZ and BlueZ-MIDI
|
||||
monitors, respectively.
|
||||
|
||||
* *Settings*
|
||||
Both monitors are enabled by default and can be disabled using the
|
||||
``monitor.bluez`` and ``monitor.bluez-midi`` :ref:`features <config_features>`
|
||||
in the configuration file.
|
||||
|
||||
Example:
|
||||
As with all device monitors, both of these monitors are implemented as SPA
|
||||
plugins and are part of PipeWire. WirePlumber merely loads the plugins and lets
|
||||
them do their work. These plugins then monitor the BlueZ system-wide D-Bus
|
||||
service and create device and node objects for all the connected Bluetooth audio
|
||||
and MIDI devices.
|
||||
|
||||
.. code-block::
|
||||
Logind integration
|
||||
------------------
|
||||
|
||||
wireplumber.properties = {
|
||||
bluez5.enable-msbc = true,
|
||||
}
|
||||
The BlueZ monitors are integrated with logind to ensure that only one user at a
|
||||
time can use the Bluetooth audio devices. This is because on most Linux desktop
|
||||
systems, the graphical login manager (GDM, SDDM, etc.) is running as a separate
|
||||
user and runs its own instance of PipeWire and Wireplumber. This means that if a
|
||||
user logs in graphically, the Bluetooth audio devices will be automatically
|
||||
grabbed by the PipeWire/WirePlumber instance of the graphical login manager,
|
||||
and the user that logs in will not get access to them.
|
||||
|
||||
This example will enable the MSBC codec in connected Bluetooth devices that
|
||||
support it.
|
||||
To overcome this, the BlueZ monitors are integrated with logind and are only
|
||||
allowed to create device and node objects for Bluetooth audio devices if the
|
||||
user is currently on the "active" logind session.
|
||||
|
||||
The list of all valid properties are:
|
||||
In some cases, however, this behavior is not desired. For example, if you
|
||||
manually switch to a TTY and log in there, you may want to keep the Bluetooth
|
||||
audio devices connected to the now inactive graphical session. Or you may want
|
||||
to have a dedicated user that is always allowed to use the Bluetooth audio
|
||||
devices, regardless of the active logind session, for example for a (possibly
|
||||
headless) music player daemon.
|
||||
|
||||
.. code-block::
|
||||
To disable this behavior, you can set the ``monitor.bluez.seat-monitoring``
|
||||
:ref:`feature <config_features>` to ``disabled``.
|
||||
|
||||
Example configuration :ref:`fragment <config_conf_file_fragments>` file:
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.profiles = {
|
||||
main = {
|
||||
monitor.bluez.seat-monitoring = disabled
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
If logind is not installed on the system, this functionality is disabled
|
||||
automatically.
|
||||
|
||||
Monitor Properties
|
||||
------------------
|
||||
|
||||
The BlueZ monitor SPA plugin (``api.bluez5.enum.dbus``) supports properties that
|
||||
can be used to configure it when it is loaded. These properties can be set in
|
||||
the ``monitor.bluez.properties`` section of the WirePlumber configuration file.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
monitor.bluez.properties = {
|
||||
bluez5.roles = [ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]
|
||||
bluez5.codecs = [ sbc sbc_xq aac ]
|
||||
bluez5.enable-sbc-xq = true
|
||||
|
||||
Enables the SBC-XQ codec in connected Blueooth devices that support it
|
||||
|
||||
.. code-block::
|
||||
|
||||
bluez5.enable-msbc = true
|
||||
|
||||
Enables the MSBC codec in connected Blueooth devices that support it
|
||||
|
||||
.. code-block::
|
||||
|
||||
bluez5.enable-hw-volume = true
|
||||
|
||||
Enables hardware volume controls in Bluetooth devices that support it
|
||||
|
||||
.. code-block::
|
||||
|
||||
bluez5.headset-roles = "[ hsp_hs hsp_ag hfp_hf hfp_ag ]"
|
||||
|
||||
Enabled headset roles (default: [ hsp_hs hfp_ag ]), this property only applies
|
||||
to native backend. Currently some headsets (Sony WH-1000XM3) are not working
|
||||
with both hsp_ag and hfp_ag enabled, disable either hsp_ag or hfp_ag to work
|
||||
around it.
|
||||
|
||||
Supported headset roles: ``hsp_hs`` (HSP Headset), ``hsp_ag`` (HSP Audio
|
||||
Gateway), ``hfp_hf`` (HFP Hands-Free) and ``hfp_ag`` (HFP Audio Gateway)
|
||||
|
||||
.. code-block::
|
||||
|
||||
bluez5.codecs = "[ sbc sbc_xq aac ]"
|
||||
|
||||
Enables ``sbc``, ``sbc_zq`` and ``aac`` A2DP codecs.
|
||||
|
||||
Supported codecs: ``sbc``, ``sbc_xq``, ``aac``, ``ldac``, ``aptx``,
|
||||
``aptx_hd``, ``aptx_ll``, ``aptx_ll_duplex``, ``faststream``,
|
||||
``faststream_duplex``.
|
||||
|
||||
All codecs are supported by default.
|
||||
|
||||
.. code-block::
|
||||
|
||||
bluez5.hfphsp-backend = "native"
|
||||
}
|
||||
|
||||
HFP/HSP backend (default: native). Available values: ``any``, ``none``,
|
||||
``hsphfpd``, ``ofono`` or ``native``.
|
||||
.. describe:: bluez5.roles
|
||||
|
||||
.. code-block::
|
||||
Enabled roles.
|
||||
|
||||
bluez5.default.rate = 48000
|
||||
Currently some headsets (e.g. Sony WH-1000XM3) do not work with both
|
||||
``hsp_ag`` and ``hfp_ag`` enabled, so by default we enable only HFP.
|
||||
|
||||
The bluetooth default audio rate.
|
||||
Supported roles:
|
||||
|
||||
.. code-block::
|
||||
- ``hsp_hs`` (HSP Headset)
|
||||
- ``hsp_ag`` (HSP Audio Gateway),
|
||||
- ``hfp_hf`` (HFP Hands-Free),
|
||||
- ``hfp_ag`` (HFP Audio Gateway)
|
||||
- ``a2dp_sink`` (A2DP Audio Sink)
|
||||
- ``a2dp_source`` (A2DP Audio Source)
|
||||
- ``bap_sink`` (LE Audio Basic Audio Profile Sink)
|
||||
- ``bap_source`` (LE Audio Basic Audio Profile Source)
|
||||
|
||||
bluez5.default.channels = 2
|
||||
:Default value: ``[ a2dp_sink a2dp_source bap_sink bap_source hfp_hf hfp_ag ]``
|
||||
:Type: array of strings
|
||||
|
||||
The bluetooth default number of channels.
|
||||
.. describe:: bluez5.codecs
|
||||
|
||||
* *Rules*
|
||||
Enabled A2DP codecs.
|
||||
|
||||
Example:
|
||||
Supported codecs: ``sbc``, ``sbc_xq``, ``aac``, ``ldac``, ``aptx``,
|
||||
``aptx_hd``, ``aptx_ll``, ``aptx_ll_duplex``, ``faststream``,
|
||||
``faststream_duplex``, ``lc3plus_h3``, ``opus_05``, ``opus_05_51``,
|
||||
``opus_05_71``, ``opus_05_duplex``, ``opus_05_pro``, ``lc3``.
|
||||
|
||||
.. code-block::
|
||||
:Default value: all available codecs
|
||||
:Type: array of strings
|
||||
|
||||
wireplumber.settings = {
|
||||
bluez_monitor = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
# This matches the needed sound card.
|
||||
device.name = "<bluez_sound_card_name>"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
# Apply all the desired device settings here.
|
||||
bluez5.auto-connect = "[ hfp_hf hsp_hs a2dp_sink ]"
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
matches = [
|
||||
# This matches the needed node.
|
||||
{
|
||||
node.name = "<node_name>"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
# Apply all the desired node specific settings here.
|
||||
update-props = {
|
||||
node.nick = "My Node"
|
||||
priority.driver = 100
|
||||
session.suspend-timeout-seconds = 5
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
.. describe:: bluez5.enable-msbc
|
||||
|
||||
This will set the auto-connect property to ``hfp_hf``, ``hsp_hs`` and
|
||||
``a2dp_sink`` on bluetooth devices whose name matches the ``bluez_card.*``
|
||||
pattern.
|
||||
Enable mSBC codec (wideband speech codec for HFP/HSP).
|
||||
|
||||
A list of valid properties are:
|
||||
This does not work on all headsets, so it is enabled based on the hardware
|
||||
quirks database. By explicitly setting this option you can force it to be
|
||||
enabled or disabled regardless.
|
||||
|
||||
.. code-block::
|
||||
:Default value: ``true``
|
||||
:Type: boolean
|
||||
|
||||
bluez5.auto-connect = "[ hfp_hf hsp_hs a2dp_sink ]"
|
||||
.. describe:: bluez5.enable-sbc-xq
|
||||
|
||||
Auto-connect device profiles on start up or when only partial profiles have
|
||||
connected. Disabled by default if the property is not specified.
|
||||
Enable SBC-XQ codec (high quality SBC codec for A2DP).
|
||||
|
||||
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
|
||||
``hsp_ag`` and ``a2dp_source``.
|
||||
This does not work on all headsets, so it is enabled based on the hardware
|
||||
quirks database. By explicitly setting this option you can force it to be
|
||||
enabled or disabled regardless.
|
||||
|
||||
.. code-block::
|
||||
:Default value: ``true``
|
||||
:Type: boolean
|
||||
|
||||
bluez5.hw-volume = "[ hfp_ag hsp_ag a2dp_source ]"
|
||||
.. describe:: bluez5.enable-hw-volume
|
||||
|
||||
Hardware volume controls (default: ``hfp_ag``, ``hsp_ag``, and ``a2dp_source``)
|
||||
Enable hardware volume controls.
|
||||
|
||||
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
|
||||
``hsp_ag`` and ``a2dp_source``.
|
||||
This does not work on all headsets, so it is enabled based on the hardware
|
||||
quirks database. By explicitly setting this option you can force it to be
|
||||
enabled or disabled regardless.
|
||||
|
||||
.. code-block::
|
||||
:Default value: ``true``
|
||||
:Type: boolean
|
||||
|
||||
bluez5.a2dp.ldac.quality = "auto"
|
||||
.. describe:: bluez5.hfphsp-backend
|
||||
|
||||
LDAC encoding quality.
|
||||
HFP/HSP backend.
|
||||
|
||||
Available values: ``auto`` (Adaptive Bitrate, default),
|
||||
``hq`` (High Quality, 990/909kbps), ``sq`` (Standard Quality, 660/606kbps) and
|
||||
``mq`` (Mobile use Quality, 330/303kbps).
|
||||
Available values: ``any``, ``none``, ``hsphfpd``, ``ofono`` or ``native``.
|
||||
|
||||
.. code-block::
|
||||
:Default value: ``native``
|
||||
:Type: string
|
||||
|
||||
bluez5.a2dp.aac.bitratemode = 0
|
||||
.. describe:: bluez5.hfphsp-backend-native-modem
|
||||
|
||||
AAC variable bitrate mode.
|
||||
Modem to use for native HFP/HSP backend ModemManager support. When enabled,
|
||||
PipeWire will forward HFP commands to the specified ModemManager device.
|
||||
This corresponds to the 'Device' property of the
|
||||
``org.freedesktop.ModemManager1.Modem`` interface. May also be ``any`` to
|
||||
use any available modem device.
|
||||
|
||||
Available values: 0 (cbr, default), 1-5 (quality level).
|
||||
:Default value: ``none``
|
||||
:Type: string
|
||||
|
||||
.. code-block::
|
||||
.. describe:: bluez5.hw-offload-sco
|
||||
|
||||
device.profile = "a2dp-sink"
|
||||
HFP/HSP hardware offload SCO support.
|
||||
|
||||
Profile connected first.
|
||||
Using this feature requires a custom WirePlumber script that handles audio
|
||||
routing in a platform-specific way. See ``tests/examples/bt-pinephone.lua``
|
||||
for an example.
|
||||
|
||||
Available values: ``a2dp-sink`` (default) or ``headset-head-unit``.
|
||||
:Default value: ``false``
|
||||
:Type: boolean
|
||||
|
||||
.. describe:: bluez5.default.rate
|
||||
|
||||
The default audio rate for the A2DP codec configuration.
|
||||
|
||||
:Default value: ``48000``
|
||||
:Type: integer
|
||||
|
||||
.. describe:: bluez5.default.channels
|
||||
|
||||
The default number of channels for the A2DP codec configuration.
|
||||
|
||||
:Default value: ``2``
|
||||
:Type: integer
|
||||
|
||||
.. describe:: bluez5.dummy-avrcp-player
|
||||
|
||||
Register dummy AVRCP player. Some devices have wrongly functioning volume or
|
||||
playback controls if this is not enabled. Disabled by default.
|
||||
|
||||
:Default value: ``false``
|
||||
:Type: boolean
|
||||
|
||||
.. describe:: Opus Pro Audio mode settings
|
||||
|
||||
.. code-block::
|
||||
|
||||
bluez5.a2dp.opus.pro.channels = 3
|
||||
bluez5.a2dp.opus.pro.coupled-streams = 1
|
||||
bluez5.a2dp.opus.pro.locations = [ FL,FR,LFE ]
|
||||
bluez5.a2dp.opus.pro.max-bitrate = 600000
|
||||
bluez5.a2dp.opus.pro.frame-dms = 50
|
||||
bluez5.a2dp.opus.pro.bidi.channels = 1
|
||||
bluez5.a2dp.opus.pro.bidi.coupled-streams = 0
|
||||
bluez5.a2dp.opus.pro.bidi.locations = [ FC ]
|
||||
bluez5.a2dp.opus.pro.bidi.max-bitrate = 160000
|
||||
bluez5.a2dp.opus.pro.bidi.frame-dms = 400
|
||||
|
||||
Options for the PipeWire-specific multichannel Opus codec, which can be used
|
||||
to transport audio over Bluetooth between devices running PipeWire.
|
||||
|
||||
MIDI Monitor Properties
|
||||
-----------------------
|
||||
|
||||
The BlueZ MIDI monitor SPA plugin (``api.bluez5.midi.enum``) may, in the future,
|
||||
support properties that can be used to configure it when it is loaded. These
|
||||
properties can be set in the ``monitor.bluez-midi.properties`` section of the
|
||||
WirePlumber configuration file. At the moment of writing, there are no
|
||||
properties that can be set there.
|
||||
|
||||
In addition, the BlueZ MIDI monitor supports a list of MIDI server node names
|
||||
that can be used to create Bluetooth LE MIDI service instances. These
|
||||
server node names can be set in the ``monitor.bluez-midi.servers`` section of
|
||||
the WirePlumber configuration file.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
monitor.bluez-midi.servers = [ "bluez_midi.server" ]
|
||||
|
||||
.. note::
|
||||
|
||||
Typical BLE MIDI instruments have one service instance, so adding more than
|
||||
one here may confuse some clients.
|
||||
|
||||
Rules
|
||||
-----
|
||||
|
||||
When device and node objects are created by the BlueZ monitor, they can be
|
||||
configured using rules. These rules allow matching the existing properties of
|
||||
these objects and updating them with new values. This is the main way of
|
||||
configuring Bluetooth device settings.
|
||||
|
||||
These rules can be set in the ``monitor.bluez.rules`` section of the WirePlumber
|
||||
configuration file.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
monitor.bluez.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
## This matches all bluetooth devices.
|
||||
device.name = "~bluez_card.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
bluez5.auto-connect = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
|
||||
bluez5.hw-volume = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
|
||||
bluez5.a2dp.ldac.quality = "auto"
|
||||
bluez5.a2dp.aac.bitratemode = 0
|
||||
bluez5.a2dp.opus.pro.application = "audio"
|
||||
bluez5.a2dp.opus.pro.bidi.application = "audio"
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
## Matches all sources.
|
||||
node.name = "~bluez_input.*"
|
||||
}
|
||||
{
|
||||
## Matches all sinks.
|
||||
node.name = "~bluez_output.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
bluez5.media-source-role = "input"
|
||||
|
||||
# Common node & audio adapter properties may also be set here
|
||||
node.nick = "My Node"
|
||||
priority.driver = 100
|
||||
priority.session = 100
|
||||
node.pause-on-idle = false
|
||||
resample.quality = 4
|
||||
channelmix.normalize = false
|
||||
channelmix.mix-lfe = false
|
||||
session.suspend-timeout-seconds = 5
|
||||
monitor.channel-volumes = false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Device properties
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
The following properties can be set on device objects:
|
||||
|
||||
.. describe:: bluez5.auto-connect
|
||||
|
||||
Auto-connect device profiles on start up or when only partial profiles have
|
||||
connected. Disabled by default if the property is not specified.
|
||||
|
||||
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
|
||||
``hsp_ag`` and ``a2dp_source``.
|
||||
|
||||
:Default value: ``[]``
|
||||
:Type: array of strings
|
||||
|
||||
.. describe:: bluez5.hw-volume
|
||||
|
||||
Enable hardware volume controls on these profiles.
|
||||
|
||||
Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
|
||||
``hsp_ag`` and ``a2dp_source``.
|
||||
|
||||
:Default value: ``[ hfp_ag hsp_ag a2dp_source ]``
|
||||
:Type: array of strings
|
||||
|
||||
.. describe:: bluez5.a2dp.ldac.quality
|
||||
|
||||
LDAC encoding quality.
|
||||
|
||||
Available values: ``auto`` (Adaptive Bitrate, default), ``hq`` (High
|
||||
Quality, 990/909kbps), ``sq`` (Standard Quality, 660/606kbps) and ``mq``
|
||||
(Mobile use Quality, 330/303kbps).
|
||||
|
||||
:Default value: ``auto``
|
||||
:Type: string
|
||||
|
||||
.. describe:: bluez5.a2dp.aac.bitratemode
|
||||
|
||||
AAC variable bitrate mode.
|
||||
|
||||
Available values: 0 (cbr, default), 1-5 (quality level).
|
||||
|
||||
:Default value: ``0``
|
||||
:Type: integer
|
||||
|
||||
.. describe:: bluez5.a2dp.opus.pro.application
|
||||
|
||||
Opus Pro Audio encoding mode.
|
||||
|
||||
Available values: ``audio``, ``voip``, ``lowdelay``.
|
||||
|
||||
:Default value: ``audio``
|
||||
:Type: string
|
||||
|
||||
.. describe:: bluez5.a2dp.opus.pro.bidi.application
|
||||
|
||||
Opus Pro Audio encoding mode for bidirectional audio.
|
||||
|
||||
Available values: ``audio``, ``voip``, ``lowdelay``.
|
||||
|
||||
:Default value: ``audio``
|
||||
:Type: string
|
||||
|
||||
.. describe:: device.profile
|
||||
|
||||
The profile that is activated initially when the device is connected.
|
||||
|
||||
Available values: ``a2dp-sink`` (default) or ``headset-head-unit``.
|
||||
|
||||
:Default value: ``a2dp-sink``
|
||||
:Type: string
|
||||
|
||||
Node properties
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
The following properties can be set on node objects:
|
||||
|
||||
.. describe:: bluez5.media-source-role
|
||||
|
||||
Media source role, ``input`` or ``playback``. This controls how a media
|
||||
source device, such as a smartphone, is used by the system. Defaults to
|
||||
``playback``, playing the incoming stream out to speakers. Set to ``input``
|
||||
to use the smartphone as an input for apps (like a microphone).
|
||||
|
||||
:Default value: ``playback``
|
||||
:Type: string
|
||||
|
||||
MIDI Rules
|
||||
----------
|
||||
|
||||
Similarly to the above rules, the BlueZ MIDI monitor also supports rules that
|
||||
can be used to configure MIDI nodes when they are created.
|
||||
|
||||
These rules can be set in the ``monitor.bluez-midi.rules`` section of the
|
||||
WirePlumber configuration file.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
monitor.bluez-midi.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
node.name = "~bluez_midi.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
node.nick = "My Node"
|
||||
priority.driver = 100
|
||||
priority.session = 100
|
||||
node.pause-on-idle = false
|
||||
session.suspend-timeout-seconds = 5
|
||||
node.latency-offset-msec = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
.. note::
|
||||
|
||||
It is possible to also match MIDI server nodes by testing the ``node.name``
|
||||
property against the server node names that were set in the
|
||||
``monitor.bluez-midi.servers`` section of the WirePlumber configuration file.
|
||||
|
||||
MIDI-specific properties
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. describe:: node.latency-offset-msec
|
||||
|
||||
Latency adjustment to apply on the node. Larger values add a
|
||||
constant latency, but reduces timing jitter caused by Bluetooth
|
||||
transport.
|
||||
|
||||
:Default value: ``0``
|
||||
:Type: integer (milliseconds)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ The main types of components are:
|
|||
A PipeWire module, which is also a shared library that can be loaded
|
||||
dynamically, but extends the functionality of the underlying *libpipewire*
|
||||
library. Loading PipeWire modules in the WirePlumber context can be useful
|
||||
to load custom protocol extensions or to offload some funcitonality from
|
||||
to load custom protocol extensions or to offload some functionality from
|
||||
the PipeWire daemon.
|
||||
|
||||
* **virtual**
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
.. _config_conf_file:
|
||||
|
||||
Configuration file
|
||||
==================
|
||||
The configuration file
|
||||
======================
|
||||
|
||||
WirePlumber's configuration file is by default ``wireplumber.conf`` and resides
|
||||
in the ``pipewire`` configuration directory (see :ref:`config_locations` for
|
||||
more details on that).
|
||||
in one of the WirePlumber specific
|
||||
:ref:`configuration file search locations <config_locations>`.
|
||||
|
||||
The default configuration file can be changed on the command line by passing
|
||||
the ``--config-file`` or ``-c`` option:
|
||||
|
|
@ -14,19 +14,20 @@ the ``--config-file`` or ``-c`` option:
|
|||
|
||||
$ wireplumber --config-file=custom.conf
|
||||
|
||||
.. note::
|
||||
.. important::
|
||||
|
||||
Starting with WirePlumber 0.5, this is the only file that WirePlumber reads
|
||||
to load configuration (together with its fragments - see below). In the past,
|
||||
WirePlumber also used to read Lua configuration files that were referenced
|
||||
from ``wireplumber.conf`` and all the heavy lifting was done in Lua. This is
|
||||
no longer the case, and the Lua configuration files are no longer supported.
|
||||
no longer the case, and the **Lua configuration files are no longer supported.**
|
||||
See :ref:`config_migration`.
|
||||
|
||||
Note that Lua is still the scripting language for WirePlumber, but it is only
|
||||
used for actual scripting and not for configuration.
|
||||
|
||||
Format
|
||||
------
|
||||
The SPA-JSON Format
|
||||
-------------------
|
||||
|
||||
The format of this configuration file is a variant of JSON that is also
|
||||
used in PipeWire configuration files (also known as SPA-JSON). The file consists
|
||||
|
|
@ -89,6 +90,8 @@ Examples of valid SPA-JSON files:
|
|||
"val1", "val2", "val3"
|
||||
]
|
||||
|
||||
.. _config_conf_file_fragments:
|
||||
|
||||
Fragments
|
||||
---------
|
||||
|
||||
|
|
@ -101,17 +104,18 @@ When loading the configuration file, WirePlumber will also look for
|
|||
additional files in the directory that has the same name as the configuration
|
||||
file suffixed with ``.d`` and will load all of them as well. For example,
|
||||
loading ``wireplumber.conf`` will also load any ``.conf`` files under
|
||||
``wireplumber.conf.d/``. This directory is searched in all the search paths
|
||||
for configuration files (see :ref:`config_locations`) and the fragments are
|
||||
loaded from *all* of them.
|
||||
``wireplumber.conf.d/``. This directory is searched in all the configuration
|
||||
search locations and the fragments are loaded from *all* of them, starting
|
||||
from the most system-wide locations and moving towards the most user-specific
|
||||
locations, in alphanumerical order within each location (see also
|
||||
:ref:`config_locations_fragments`).
|
||||
|
||||
The fragments are loaded in alphabetical order, after the main configuration
|
||||
file. When a JSON object appears in multiple files, the properties of the
|
||||
objects are merged together. When a JSON array appears in multiple files, the
|
||||
arrays are concatenated together. When merging objects, if specific properties
|
||||
appear in many of those objects, the last one to be parsed always overwrites
|
||||
previous ones, unless the value is also an object or array; if it is, then the
|
||||
value is recursively merged using the same rules.
|
||||
When a JSON object appears in multiple files, the properties of the objects are
|
||||
merged together. When a JSON array appears in multiple files, the arrays are
|
||||
concatenated together. When merging objects, if specific properties appear in
|
||||
many of those objects, the last one to be parsed always overwrites previous
|
||||
ones, unless the value is also an object or array; if it is, then the value is
|
||||
recursively merged using the same rules.
|
||||
|
||||
Sections
|
||||
--------
|
||||
|
|
@ -124,6 +128,12 @@ file:
|
|||
This section is an array that lists components that can be loaded by
|
||||
WirePlumber. For more information, see :ref:`config_components_and_profiles`.
|
||||
|
||||
* *wireplumber.components.rules*
|
||||
|
||||
This section is an array containing rules that can be used to modify entries
|
||||
of the *wireplumber.components* array. This is useful to inject changes
|
||||
to the components list without having to modify the main configuration file.
|
||||
|
||||
* *wireplumber.profiles*
|
||||
|
||||
This section is an object that defines profiles that can be loaded by
|
||||
|
|
@ -134,6 +144,13 @@ file:
|
|||
This section is an object that defines settings that can be used to
|
||||
alter WirePlumber's behavior. For more information, see :ref:`config_settings`.
|
||||
|
||||
* *wireplumber.settings.schema*
|
||||
|
||||
This section is an object that defines the schema for the settings that
|
||||
can be listed in *wireplumber.settings*. This is used to validate the
|
||||
settings when they are modified at runtime. For more information, see
|
||||
:ref:`config_configuration_option_types`.
|
||||
|
||||
In addition, there are many sections that are specific to certain components,
|
||||
mostly hardware monitors, such as *monitor.alsa.properties*,
|
||||
*monitor.alsa.rules*, etc. These are documented further on, in the respective
|
||||
|
|
@ -187,9 +204,18 @@ by libpipewire to configure the PipeWire context:
|
|||
.. note::
|
||||
|
||||
PipeWire modules can also be loaded as :ref:`components <config_components_and_profiles>`,
|
||||
which may be preferrable since it allows you to load them conditionally
|
||||
which may be preferable since it allows you to load them conditionally
|
||||
based on the profile and component dependencies.
|
||||
|
||||
.. admonition:: Remember
|
||||
|
||||
Modules listed in *context.modules* are always loaded before attempting a
|
||||
connection to the PipeWire daemon, while modules listed in
|
||||
*wireplumber.components* are always loaded after the connection is
|
||||
established. It is important to load the PipeWire protocol-native module
|
||||
and any extensions (such as module-metadata) in the *context.modules*
|
||||
section, so that the connection can be done properly.
|
||||
|
||||
Each module is described by a JSON object containing the module's *name*,
|
||||
its arguments (*args*) and a combination of *flags*, which can be ``ifexists``
|
||||
and ``nofail``.
|
||||
|
|
|
|||
125
docs/rst/daemon/configuration/configuration_option_types.rst
Normal file
125
docs/rst/daemon/configuration/configuration_option_types.rst
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
.. _config_configuration_option_types:
|
||||
|
||||
Configuration option types
|
||||
==========================
|
||||
|
||||
As seen in the previous sections, WirePlumber can be partly configured by
|
||||
enabling or disabling features, which affect which components are getting
|
||||
loaded. These components, however, can be further configured to fine-tune their
|
||||
behavior. This section describes the different types of configuration options
|
||||
that can be used to configure WirePlumber components.
|
||||
|
||||
Dynamic options ("Settings")
|
||||
----------------------------
|
||||
|
||||
Dynamic options (also simply referred to as "settings") are configuration
|
||||
options that can be changed at runtime. They are typically simple values like
|
||||
booleans, integers, strings, etc. and are all located under the
|
||||
``wireplumber.settings`` section in the configuration file. Their purpose is to
|
||||
allow the user to change simple behavioral aspects of WirePlumber.
|
||||
|
||||
As the name suggests, these options are dynamic and can be changed at runtime
|
||||
using ``wpctl`` or the :ref:`settings_api` API. For example, setting the
|
||||
``device.routes.default-sink-volume`` setting to ``0.5`` can be done like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ wpctl settings device.routes.default-sink-volume 0.5
|
||||
|
||||
Under the hood, when WirePlumber starts, the ``metadata.sm-settings`` component
|
||||
(provided by ``libwireplumber-module-settings``) reads this section from the
|
||||
configuration file and populates the ``sm-settings`` metadata object, which is
|
||||
exported to PipeWire. In addition, it reads the ``wireplumber.settings.schema``
|
||||
section and populates the ``schema-sm-settings`` metadata object, which is used
|
||||
by the API to validate the settings. Any options that are missing from
|
||||
``wireplumber.settings`` are also populated in ``sm-settings`` from their
|
||||
default values in the schema. Then the rest of the components read their
|
||||
configuration options from this metadata object via the :ref:`settings_api` API.
|
||||
|
||||
Most of the components that use such dynamic options make sure to listen
|
||||
to changes in the metadata object so that they can immediately adapt their
|
||||
behavior. Other components, however, do not react immediately and the changes
|
||||
only take effect the next time the option is needed. For instance, some options
|
||||
affect created objects in a way that cannot be changed after the object has been
|
||||
created, so when the option is changed it applies only to new objects and not
|
||||
existing ones.
|
||||
|
||||
Changing the settings at runtime in the ``sm-settings`` metadata object is
|
||||
a non-persistent change. The changes will be lost when WirePlumber is
|
||||
restarted. However, the :ref:`settings_api` API also supports saving settings
|
||||
to a state file, which will be loaded again when WirePlumber starts and
|
||||
override the settings from the configuration file. This is done by using yet
|
||||
another metadata object called ``persistent-sm-settings``. When a setting is
|
||||
changed in the ``persistent-sm-settings`` metadata object, WirePlumber
|
||||
automatically saves the change to the state file and also changes the value in
|
||||
the ``sm-settings`` metadata object immediately.
|
||||
|
||||
To make such a persistent change using ``wpctl``, the ``--save`` option can be
|
||||
used. For example, to set the ``device.routes.default-sink-volume`` setting to
|
||||
``0.5`` and save it to the state file:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ wpctl settings --save device.routes.default-sink-volume 0.5
|
||||
|
||||
With ``wpctl``, it is also possible to restore a setting to its default value
|
||||
(taken from the schema), by using the ``--reset`` option. For example, to reset
|
||||
the ``device.routes.default-sink-volume`` setting, the following command can be
|
||||
used:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ wpctl settings --reset device.routes.default-sink-volume
|
||||
|
||||
In addition, the ``--delete`` option can be used to delete a setting from the
|
||||
``persistent-sm-settings`` metadata object, which will also remove it from the
|
||||
state file. After deleting, the value from the ``wireplumber.settings`` section
|
||||
of the configuration file will be used again. For example, to delete the
|
||||
``device.routes.default-sink-volume`` setting, the following command can be
|
||||
used:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ wpctl settings --delete device.routes.default-sink-volume
|
||||
|
||||
A list of all the available settings can be found in the :ref:`config_settings`
|
||||
section.
|
||||
|
||||
Static options
|
||||
--------------
|
||||
|
||||
Static options are more complex configuration structures that reside only in the
|
||||
configuration file and cannot be changed at runtime. They are typically used to
|
||||
configure device monitors and provide rules that match objects and perform
|
||||
actions such as update their properties.
|
||||
|
||||
While these options could also in theory be stored in the metadata object and
|
||||
be made dynamic, this is not supported because these options are both complex
|
||||
and therefore hard to change on the command line, but also because they are
|
||||
typically used to configure objects that are created at startup and cannot be
|
||||
changed later.
|
||||
|
||||
Static options are located in their own top-level sections. Examples of such
|
||||
sections are ``monitor.alsa.properties`` and ``monitor.alsa.rules`` that are
|
||||
used to configure the ``monitor.alsa`` component. The next sections of this
|
||||
documentation describe in detail all the available static options.
|
||||
|
||||
Component arguments
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Components can also be configured statically by passing arguments to them when
|
||||
they are loaded. This is done by adding an ``arguments`` key to the component
|
||||
description in the ``wireplumber.components`` section (see
|
||||
:ref:`config_components_and_profiles`).
|
||||
|
||||
The arguments are mostly meant as a way to instantiate multiple instances of the
|
||||
same module or script with slightly different configuration to create a new
|
||||
unique component. For example, the ``metadata.lua`` script can be instantiated
|
||||
multiple times to create multiple metadata objects, each with a different name.
|
||||
The name of the metadata object is passed as an argument to the script.
|
||||
|
||||
While many more static options could be passed as arguments, this is not
|
||||
recommended because it is not possible to override the arguments by adding
|
||||
:ref:`fragment<config_conf_file_fragments>` configuration files. Therefore, it
|
||||
is recommended to use component-specific top-level sections, unless the option
|
||||
is not meant to be changed by the user.
|
||||
|
|
@ -11,6 +11,9 @@ can be confusing to go through them. This list here is meant to be a quick
|
|||
reference for the most common ones that actually make sense to be toggled in
|
||||
a configuration file in order to customize WirePlumber's behavior.
|
||||
|
||||
For more information on what features are and how they work, refer to the
|
||||
previous section: :ref:`config_components_and_profiles`.
|
||||
|
||||
Hardware monitors
|
||||
-----------------
|
||||
|
||||
|
|
@ -39,6 +42,12 @@ Audio
|
|||
|
||||
Enables the ALSA MIDI device monitor.
|
||||
|
||||
.. describe:: node.software-dsp
|
||||
|
||||
Enables software DSP based on pre-configured hardware rules.
|
||||
|
||||
See :ref:`policies_software_dsp` for more information.
|
||||
|
||||
Bluetooth
|
||||
~~~~~~~~~
|
||||
|
||||
|
|
@ -131,9 +140,9 @@ Policies
|
|||
for enabling devices, linking streams, granting permissions to clients,
|
||||
etc, as appropriate for a desktop system.
|
||||
|
||||
.. describe:: policy.role-priority-system
|
||||
.. describe:: policy.role-based
|
||||
|
||||
Enables the role priority system policy. This system creates virtual sinks
|
||||
Enables the role based priority system policy. This system creates virtual sinks
|
||||
that group streams based on their ``media.role`` property, and assigns a
|
||||
priority to each role. Depending on the priority configuration, lower
|
||||
priority roles may be corked or ducked when a higher priority role stream
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
.. _config_locations:
|
||||
|
||||
Locations of files
|
||||
==================
|
||||
|
||||
Location of configuration files
|
||||
-------------------------------
|
||||
|
||||
WirePlumber's default locations of its configuration files are the same as
|
||||
pipewire. Typically, those end up being
|
||||
``$XDG_CONFIG_DIR/pipewire``, ``/etc/pipewire``, and
|
||||
``/usr/share/pipewire``, in that order of priority.
|
||||
|
||||
The three locations are intended for custom user configuration,
|
||||
host-specific configuration and distribution-provided configuration,
|
||||
respectively. At runtime, WirePlumber will search the directories
|
||||
for the highest-priority directory to contain the needed configuration file.
|
||||
This allows a user or system administrator to easily override the distribution
|
||||
provided configuration files by placing an equally named file in the respective
|
||||
directory.
|
||||
|
||||
It is also possible to override the configuration directory by setting the
|
||||
``WIREPLUMBER_CONFIG_DIR`` environment variable::
|
||||
|
||||
WIREPLUMBER_CONFIG_DIR=src/config wireplumber
|
||||
|
||||
For convenience, the behaviour of the ``WIREPLUMBER_CONFIG_DIR`` environment
|
||||
variable is the same as the ``PIPEWIRE_CONFIG_DIR`` environment variable.
|
||||
If ``WIREPLUMBER_CONFIG_DIR`` is set, the default locations are ignored and
|
||||
configuration files are *only* looked up in this directory.
|
||||
|
||||
|
||||
Location of scripts
|
||||
-------------------
|
||||
|
||||
WirePlumber's default locations of its scripts are the same ones as for the
|
||||
configuration files, but with the ``scripts`` directory appended.
|
||||
Typically, these end up being ``$XDG_CONFIG_DIR/wireplumber/scripts``,
|
||||
``/etc/wireplumber/scripts``, and ``/usr/share/wireplumber/scripts``,
|
||||
in that order of priority.
|
||||
|
||||
The three locations are intended for custom user scripts,
|
||||
host-specific scripts and distribution-provided scripts, respectively.
|
||||
At runtime, WirePlumber will search the directories for the highest-priority
|
||||
directory to contain the needed script.
|
||||
|
||||
It is also possible to override the scripts directory by setting the
|
||||
``WIREPLUMBER_DATA_DIR`` environment variable::
|
||||
|
||||
WIREPLUMBER_DATA_DIR=src wireplumber
|
||||
|
||||
The "data" directory is a somewhat more generic path that may be used for
|
||||
other kinds of data files in the future. For scripts, WirePlumber still expects
|
||||
to find a ``scripts`` subdirectory in this "data" directory, so in the above
|
||||
example the scripts would be in ``src/scripts``.
|
||||
|
||||
If ``WIREPLUMBER_DATA_DIR`` is set, the default locations are ignored and
|
||||
scripts are *only* looked up in this directory.
|
||||
|
||||
Location of modules
|
||||
-------------------
|
||||
|
||||
WirePlumber modules
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Like with configuration files, WirePlumber's default location of its modules is
|
||||
determined at compile time by the build system. Typically, it ends up being
|
||||
``/usr/lib/wireplumber-0.4`` (or ``/usr/lib/<arch-triplet>/wireplumber-0.4`` on
|
||||
multiarch systems)
|
||||
|
||||
In more detail, this is controlled by the ``--libdir`` meson option. When
|
||||
this is set to an absolute path, such as ``/lib``, the location of the
|
||||
modules is set to be ``$libdir/wireplumber-$abi_version``. When this is set
|
||||
to a relative path, such as ``lib``, then the installation prefix (``--prefix``)
|
||||
is prepended to the path: ``$prefix/$libdir/wireplumber-$abi_version``.
|
||||
|
||||
It is possible to override this directory at runtime by setting the
|
||||
``WIREPLUMBER_MODULE_DIR`` environment variable::
|
||||
|
||||
WIREPLUMBER_MODULE_DIR=build/modules wireplumber
|
||||
|
||||
PipeWire and SPA modules
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
PipeWire and SPA modules are not loaded from the same location as WirePlumber's
|
||||
modules. They are loaded from the location that PipeWire loads them.
|
||||
|
||||
It is also possible to override these locations by using environment variables:
|
||||
``SPA_PLUGIN_DIR`` and ``PIPEWIRE_MODULE_DIR``. For more details, refer to
|
||||
PipeWire's documentation.
|
||||
|
|
@ -1,478 +0,0 @@
|
|||
.. _config_main:
|
||||
|
||||
Main configuration file
|
||||
=======================
|
||||
|
||||
The main configuration file is by default called ``wireplumber.conf``. This can
|
||||
be changed on the command line by passing the ``--config-file`` or ``-c`` option::
|
||||
|
||||
wireplumber --config-file=bluetooth.conf
|
||||
|
||||
The ``--config-file`` option is useful to run multiple instances of wireplumber
|
||||
that do separate tasks each. For more information on this subject, see the
|
||||
:ref:`Multiple Instances <config_multi_instance>` section.
|
||||
|
||||
The format of this configuration file is the variant of JSON that is also
|
||||
used in PipeWire configuration files. Note that this is subject to change
|
||||
in the future.
|
||||
|
||||
All sections are essentially JSON objects. Lines starting with *#* are treated
|
||||
as comments and ignored. The list of all possible section JSON objects are:
|
||||
|
||||
Common configs are present in the main configuration file(wireplumber.conf),
|
||||
rest of the configs that can be grouped logically are grouped into separate
|
||||
files and are placed under ``wireplumber.conf.d/``. More on this below.
|
||||
|
||||
* *context.properties*
|
||||
|
||||
Used to define properties to configure the PipeWire context and some modules.
|
||||
|
||||
Example::
|
||||
|
||||
context.properties = {
|
||||
application.name = WirePlumber
|
||||
log.level = 2
|
||||
}
|
||||
|
||||
This sets the daemon's name to *WirePlumber* and the log level to *2*, which
|
||||
only displays errors and warnings. See the
|
||||
:ref:`Debug Logging <daemon_logging>` section for more details.
|
||||
|
||||
* *context.spa-libs*
|
||||
|
||||
Used to find spa factory names. It maps a spa factory name regular expression
|
||||
to a library name that should contain that factory. The object property names
|
||||
are the regular expression, and the object property values are the actual
|
||||
library name::
|
||||
|
||||
<factory-name regex> = <library-name>
|
||||
|
||||
Example::
|
||||
|
||||
context.spa-libs = {
|
||||
api.alsa.* = alsa/libspa-alsa
|
||||
audio.convert.* = audioconvert/libspa-audioconvert
|
||||
}
|
||||
|
||||
In this example, we instruct wireplumber to only any *api.alsa.** factory name
|
||||
from the *libspa-alsa* library, and also any *audio.convert.** factory name
|
||||
from the *libspa-audioconvert* library.
|
||||
|
||||
* *context.modules*
|
||||
|
||||
Used to load PipeWire modules. This does not affect the PipeWire daemon by any
|
||||
means. It exists simply to allow loading *libpipewire* modules in the PipeWire
|
||||
core that runs inside WirePlumber. This is usually useful to load PipeWire
|
||||
protocol extensions, so that you can export custom objects to PipeWire and
|
||||
other clients.
|
||||
|
||||
Users can also pass key-value pairs if the specific module has arguments, and
|
||||
a combination of 2 flags: ``ifexists`` flag is given, the module is ignored when
|
||||
not found; if ``nofail`` is given, module initialization failures are ignored::
|
||||
|
||||
{
|
||||
name = <module-name>
|
||||
[ args = { <key> = <value> ... } ]
|
||||
[ flags = [ [ ifexists ] [ nofail ] ]
|
||||
}
|
||||
|
||||
Example::
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-adapter }
|
||||
{
|
||||
name = libpipewire-module-metadata,
|
||||
flags = [ ifexists ]
|
||||
}
|
||||
]
|
||||
|
||||
The above example loads both PipeWire adapter and metadata modules. The
|
||||
metadata module will be ignored if not found because of its ``ifexists`` flag.
|
||||
|
||||
* *wireplumber.components*
|
||||
|
||||
Used to load WirePlumber components. Components can be either WirePlumber
|
||||
modules written in C or WirePlumber scripts written in Lua.
|
||||
|
||||
Syntax::
|
||||
|
||||
{ name = <component-name>, type = <component-type>, deps = <dependent-setting>, flags = <flags> }
|
||||
|
||||
* type:
|
||||
|
||||
Valid component types include:
|
||||
|
||||
* ``module``: A WirePlumber shared object module
|
||||
* ``script/lua``: A WirePlumber Lua script
|
||||
(all Lua Scripts implicitly requires libwireplumber-module-lua-scripting module)
|
||||
|
||||
Example::
|
||||
|
||||
wireplumber.components = [
|
||||
{ name = libwireplumber-module-lua-scripting, type = module }
|
||||
{ name = monitors/alsa.lua, type = script/lua }
|
||||
]
|
||||
|
||||
* deps: components can be loaded with a dependency on a wireplumber setting.
|
||||
* flags: ifexists & nofail flags are supported in this section as well.
|
||||
|
||||
|
||||
* `ifexists` - signals wireplumber to ignore if the module is not found.
|
||||
* `nofail` - signals wireplumber to ignore module initialization failures.
|
||||
|
||||
More Examples::
|
||||
|
||||
wireplumber.components = [
|
||||
# Load `libwireplumber-module-si-node` which is of type `module`.
|
||||
{ name = libwireplumber-module-si-node , type = module }
|
||||
|
||||
# Load `libwireplumber-module-reserve-device` module, only if the setting `alsa_monitor.alsa.reserve` is defined as true.
|
||||
{ name = libwireplumber-module-reserve-device , type = module, deps = alsa_monitor.alsa.reserve }
|
||||
|
||||
# Load `alsa.lua` which is of type `script/lua`.
|
||||
{ name = monitors/alsa.lua, type = script/lua }
|
||||
|
||||
# Load `alsa-midi.lua` Lua Script only if `alsa_monitor.alsa.midi` setting is defined as true.
|
||||
{ name = monitors/alsa-midi.lua, type = script/lua, deps = alsa_monitor.alsa.midi }
|
||||
|
||||
# Load `libwireplumber-module-logind` module if the setting `bluez-enable-logind` is true.
|
||||
{ name = libwireplumber-module-logind , type = module, deps = bluez-enable-logind, flags = [ ifexists ] }
|
||||
]
|
||||
|
||||
.. note::
|
||||
|
||||
- `name` & `type` keys are mandatory, while `deps` and `flags` keys are optional
|
||||
- All the components are loaded during the bootup and failure in finding them or any error during the loading process is a fatal error and WirePlumber will exit.
|
||||
|
||||
|
||||
* *wireplumber.settings*
|
||||
|
||||
All the Wireplumber configuration settings are now grouped under this
|
||||
section. They are moved away from Lua.
|
||||
|
||||
All the default settings are distributed into different
|
||||
files(\*settings.conf) under ``wireplumber.conf.d\``
|
||||
|
||||
All the settings are loaded into ``sm-settings`` metadata. Apart from the
|
||||
settings JSON files, Metadata interface can be used to change them.
|
||||
|
||||
:ref:`WpSettings <settings_api>` provides APIs to its clients
|
||||
(modules, lua scripts etc) to access and track them.
|
||||
|
||||
Settings can be persistent, more on this below.
|
||||
|
||||
There can be two types of settings namely plain settings(called just settings
|
||||
for reasons of simplicity) and rules.
|
||||
|
||||
* `Settings`
|
||||
|
||||
Syntax::
|
||||
|
||||
wireplumber.settings = {
|
||||
<setting1> = <value>
|
||||
<setting2> = <value>
|
||||
..
|
||||
}
|
||||
|
||||
Examples::
|
||||
|
||||
wireplumber.settings = {
|
||||
alsa_monitor.alsa.reserve = true
|
||||
alsa_monitor.alsa.midi = "true"
|
||||
default-policy-duck.level = 0.3
|
||||
bt-policy-media-role.applications = ["Firefox", "Chromium input"]
|
||||
}
|
||||
|
||||
Value can be string, int, float, boolean and can even be a JSON array.
|
||||
|
||||
WpSettings exposes the `wp_settings_get_{string|int|float|boolean}()` APIs
|
||||
to access the values.
|
||||
|
||||
Lua scripts, modules use these APIs to access settings.
|
||||
The client accessing the setting should know which API to use to access
|
||||
the setting accurately.
|
||||
|
||||
If the Setting is a JSON array like `bt-policy-media-role.applications`
|
||||
_get_string() API need to be used and the obtained JSON element will have
|
||||
to be parsed using the :ref:`JSON APIs. <spa_json_api>`
|
||||
|
||||
Persistent Behavior::
|
||||
|
||||
wireplumber.settings = {
|
||||
persistent.settings = true
|
||||
}
|
||||
|
||||
Persistent behavior can be enabled with the above syntax.
|
||||
|
||||
When enabled, the settings will be read from conf file only once and for
|
||||
subsequent reboots they will be read from the state(cache) files, till the
|
||||
time the setting is set back to false in the .conf file.
|
||||
|
||||
Settings can be changed through metadata, so when they are updated through
|
||||
metadata and if the user desires those settings to be persistent between
|
||||
reboots this persistent option can be used.
|
||||
|
||||
wp_settings_register_{callback|closure} () API can be used by clients to
|
||||
keep track of the changes to settings.
|
||||
|
||||
The persistent behavior is disabled by default.
|
||||
|
||||
* `Rules`
|
||||
|
||||
Rules are dynamic logic based settings.
|
||||
|
||||
Syntax
|
||||
|
||||
Simple Syntax::
|
||||
|
||||
wireplumber.settings = {
|
||||
<rule-name> = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
<pipewire property1> = <value>
|
||||
<pipewire property2> = <value>
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
<pipewire property> = <value>,
|
||||
<wireplumber setting> = <value>,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Simple Example::
|
||||
|
||||
wireplumber.settings = {
|
||||
stream_default = [
|
||||
{
|
||||
matches = [
|
||||
# Matches all devices
|
||||
{ application.name = "pw-play" }
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
state.restore-props = false
|
||||
state.restore-target = false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Stream_default rule scans for pw-play app and if found it applies the two
|
||||
properties listed above.
|
||||
|
||||
Advanced Syntax::
|
||||
|
||||
# Nested behavior
|
||||
wireplumber.settings = {
|
||||
<rule-name> = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
# Logical AND behavior with the JSON object
|
||||
<pipewire property1> = <value>
|
||||
<pipewire property2> = <value>
|
||||
}
|
||||
|
||||
# Logical OR behavior across the JSON objects.
|
||||
{
|
||||
<pipewire property3> = <value>
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
<pipewire property> = <value>,
|
||||
<wireplumber setting> = <value>,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Use of regular expressions
|
||||
wireplumber.settings = {
|
||||
<rule-name> = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
# if a value starts with ``~`` it triggers regular expression evaluation
|
||||
<pipewire property1> = <~value*>
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
<pipewire property> = <value>,
|
||||
<wireplumber setting> = <value>,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Multiple Matches with in a single rule is possible.
|
||||
wireplumber.settings = {
|
||||
<rule-name> = [
|
||||
{
|
||||
# Match 1
|
||||
matches = [
|
||||
{
|
||||
<pipewire property1> = <~value*>
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
<pipewire property1> = <value>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Match 2
|
||||
matches = [
|
||||
{
|
||||
<pipewire property2> = <~value*>
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
<pipewire property2> = <value>,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Advanced Example::
|
||||
|
||||
wireplumber.settings = {
|
||||
|
||||
alsa_monitor = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
# This matches all sound cards.
|
||||
device.name = "~alsa_card.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
# and applies these properties.
|
||||
api.alsa.use-acp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
matches = [
|
||||
# Matches either input nodes or output nodes
|
||||
{
|
||||
node.name = "~alsa_input.*"
|
||||
}
|
||||
{
|
||||
node.name = "~alsa_output.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
node.nick = "My Node"
|
||||
priority.driver = 100
|
||||
session.suspend-timeout-seconds = 5
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
* wp_settings_apply_rule () is WpSettings API for rules.
|
||||
|
||||
|
||||
* *wireplumber.virtuals*
|
||||
|
||||
Virtual session items are a way of grouping different kinds of clients or
|
||||
applications(for example Music, Voice, Navigation, Gaming etc).
|
||||
The actual grouping is done based on the `media.role` of the client
|
||||
stream node.
|
||||
|
||||
Virtual session items allows for that actions to be taken up at group level
|
||||
rather than at individual stream level, which can be cumbersome.
|
||||
|
||||
For example imagine the following scenarios.
|
||||
* Incoming Navigation message needs to duck the volume of
|
||||
Audio playback(all the apps playing audio).
|
||||
* Incoming voice/voip call needs to stop(cork) the Audio playback.
|
||||
|
||||
Virtual session items realize this functionality with ease.
|
||||
|
||||
* *Defining Virtual session items*
|
||||
|
||||
Example::
|
||||
|
||||
virtual-items = {
|
||||
virtual-item.capture = {
|
||||
media.class = "Audio/Source"
|
||||
role = "Capture"
|
||||
}
|
||||
virtual-item.multimedia = {
|
||||
media.class = "Audio/Sink"
|
||||
role = "Multimedia"
|
||||
}
|
||||
virtual-item.navigation = {
|
||||
media.class = "Audio/Sink"
|
||||
role = "Navigation"
|
||||
}
|
||||
|
||||
This example creates 3 virtual session items, with names
|
||||
``virtual-item.capture``, ``virtual-item.multimedia`` and
|
||||
``virtual-item.navigation`` and assigned roles ``Capture``, ``Multimedia``
|
||||
and ``Navigation`` respectively.
|
||||
|
||||
First virtual item has a media class of ``Audio/Source`` used for capture
|
||||
and rest of the virtual items have ``Audio/Sink`` media class, and so are
|
||||
only used for playback.
|
||||
|
||||
* *Virtual session items config*
|
||||
|
||||
Example::
|
||||
|
||||
Capture = {
|
||||
alias = [ "Multimedia", "Music", "Voice", "Capture" ]
|
||||
priority = 25
|
||||
action.default = "cork"
|
||||
action.capture = "mix"
|
||||
media.class = "Audio/Source"
|
||||
}
|
||||
Multimedia = {
|
||||
alias = [ "Movie" "Music" "Game" ]
|
||||
priority = 25
|
||||
action.default = "cork"
|
||||
}
|
||||
Navigation = {
|
||||
priority = 50
|
||||
action.default = "duck"
|
||||
action.Navigation = "mix"
|
||||
}
|
||||
|
||||
|
||||
The above example defines actions for both ``Multimedia`` and ``Navigation``
|
||||
roles. Since the Navigation role has more priority than the Multimedia
|
||||
role, when a client connects to the Navigation virtual session item, it
|
||||
will ``duck`` the volume of all Multimedia clients. If Multiple Navigation
|
||||
clients want to play audio, their audio will be mixed.
|
||||
|
||||
Possible values of actions are: ``mix`` (Mixes audio),
|
||||
``duck`` (Mixes and lowers the audio volume) or ``cork`` (Pauses audio).
|
||||
|
||||
Virtual session items are not used for desktop use cases, it is more suitable
|
||||
for embedded use cases.
|
||||
|
||||
* *Split Configuration files*
|
||||
|
||||
The Main configuration file is split into multiple files. When loading the main
|
||||
JSON configuration file, WirePlumber will also look for additional files in the
|
||||
same directory suffixed with ``.d`` and will load all of them as well. For
|
||||
example, loading ``wireplumber.conf`` will also load any files under
|
||||
``wireplumber.conf.d/``. It will load all the JSON config files there. All the
|
||||
configurations are logically split into files and placed in this directory.
|
||||
|
|
@ -2,13 +2,12 @@
|
|||
sphinx_files += files(
|
||||
'conf_file.rst',
|
||||
'components_and_profiles.rst',
|
||||
'configuration_option_types.rst',
|
||||
'modifying_configuration.rst',
|
||||
'migration.rst',
|
||||
'features.rst',
|
||||
'settings.rst',
|
||||
'locations.rst',
|
||||
'main.rst',
|
||||
'multi_instance.rst',
|
||||
'alsa.rst',
|
||||
'bluetooth.rst',
|
||||
'policy.rst',
|
||||
'access.rst',
|
||||
)
|
||||
|
|
|
|||
305
docs/rst/daemon/configuration/migration.rst
Normal file
305
docs/rst/daemon/configuration/migration.rst
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
.. _config_migration:
|
||||
|
||||
Migrating configuration from 0.4
|
||||
================================
|
||||
|
||||
The configuration file format has changed in version 0.5. No automatic migration
|
||||
of old configuration files is performed, so you will have to manually update
|
||||
them. This document describes the changes and how to update your configuration.
|
||||
|
||||
wireplumber.conf
|
||||
----------------
|
||||
|
||||
In WirePlumber 0.4, there used to be a ``.conf`` file, typically
|
||||
``wireplumber.conf``, using the SPA-JSON format, that would list some Lua
|
||||
scripts in the ``wireplumber.components`` section. These scripts were of type
|
||||
``config/lua`` and they were called by default ``main.lua``, ``policy.lua`` and
|
||||
``bluetooth.lua``.
|
||||
|
||||
Typical ``wireplumber.components`` section of a ``wireplumber.conf`` file in 0.4
|
||||
would look like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.components = [
|
||||
#{ name = <component-name>, type = <component-type> }
|
||||
#
|
||||
# WirePlumber components to load
|
||||
#
|
||||
|
||||
# The lua scripting engine
|
||||
{ name = libwireplumber-module-lua-scripting, type = module }
|
||||
|
||||
# The lua configuration file(s)
|
||||
# Other components are loaded from there
|
||||
{ name = main.lua, type = config/lua }
|
||||
{ name = policy.lua, type = config/lua }
|
||||
{ name = bluetooth.lua, type = config/lua }
|
||||
]
|
||||
|
||||
These Lua "configuration" scripts were then looked up in the standard
|
||||
configuration directories (``/usr/share/wireplumber``, ``/etc/wireplumber`` and
|
||||
``~/.config/wireplumber``). The system also supported fragments of these scripts
|
||||
to be placed in directories called ``main.lua.d``, ``policy.lua.d`` and
|
||||
``bluetooth.lua.d`` respectively, in the same locations.
|
||||
|
||||
.. attention::
|
||||
|
||||
Starting with WirePlumber 0.5, Lua "configuration" files are **no longer
|
||||
supported**.
|
||||
|
||||
If you attempt to start it with a ``wireplumber.conf`` that still
|
||||
lists ``config/lua`` components in its ``wireplumber.components`` section, you
|
||||
will see the following error message on the output:
|
||||
|
||||
Failed to load configuration: The configuration file at '...' is likely an
|
||||
old WirePlumber 0.4 config and is not supported anymore. Try removing it.
|
||||
|
||||
As the message says, to resolve this you should remove the old
|
||||
``wireplumber.conf`` file from the designated location. This should allow the
|
||||
new WirePlumber to start using the default configuration that it ships with.
|
||||
|
||||
Lua configuration scripts
|
||||
-------------------------
|
||||
|
||||
If you had custom Lua configuration scripts in the standard configuration
|
||||
directories, such as *"main.lua.d"*, *"policy.lua.d"* or *"bluetooth.lua.d"*,
|
||||
**you need to port them**.
|
||||
|
||||
Locations of files
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The first thing you need to know is that the new files should be placed in the
|
||||
``~/.config/wireplumber/wireplumber.conf.d/`` directory instead of
|
||||
``~/.config/wireplumber/main.lua.d/`` and such ...
|
||||
|
||||
In addition, since the new files are in the SPA-JSON format, they should have
|
||||
the ``.conf`` extension instead of ``.lua``.
|
||||
|
||||
See also :ref:`config_locations`.
|
||||
|
||||
Porting device/node rules
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
One of the most common use-cases for these scripts was to set up properties
|
||||
for devices and nodes using rules. Here is an example of an old rules script:
|
||||
|
||||
.. code-block:: lua
|
||||
:caption: ~/.config/wireplumber/main.lua.d/51-alsa-pro-audio.lua
|
||||
|
||||
local rule = {
|
||||
matches = {
|
||||
{
|
||||
{ "device.name", "matches", "alsa_card.*" },
|
||||
},
|
||||
},
|
||||
apply_properties = {
|
||||
["api.alsa.use-acp"] = false,
|
||||
["device.profile"] = "pro-audio",
|
||||
["api.acp.auto-profile"] = false,
|
||||
["api.acp.auto-port"] = false,
|
||||
},
|
||||
}
|
||||
|
||||
table.insert(alsa_monitor.rules, rule)
|
||||
|
||||
This equivalent of this script in the new configuration format would look like
|
||||
this:
|
||||
|
||||
.. code-block::
|
||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/51-alsa-pro-audio.conf
|
||||
|
||||
monitor.alsa.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
device.name = "~alsa_card.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
api.alsa.use-acp = false,
|
||||
device.profile = "pro-audio"
|
||||
api.acp.auto-profile = false
|
||||
api.acp.auto-port = false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Another example of Bluetooth node rules:
|
||||
|
||||
.. code-block:: lua
|
||||
:caption: ~/.config/wireplumber/bluetooth.lua.d/51-headphones.lua
|
||||
|
||||
local rule = {
|
||||
matches = {
|
||||
{
|
||||
{ "node.name", "equals", "bluez_output.02_11_45_A0_B3_27.a2dp-sink" },
|
||||
},
|
||||
},
|
||||
apply_properties = {
|
||||
["node.nick"] = "Headphones",
|
||||
},
|
||||
}
|
||||
|
||||
table.insert(bluez_monitor.rules, rule)
|
||||
|
||||
This equivalent of this script in the new configuration format would look like:
|
||||
|
||||
.. code-block::
|
||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/51-headphones.conf
|
||||
|
||||
monitor.bluez.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
node.name = "bluez_output.02_11_45_A0_B3_27.a2dp-sink"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
node.nick = "Headphones"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
See also :ref:`config_modifying_configuration_rules`.
|
||||
|
||||
Porting properties configuration
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you had configuration scripts that were setting properties in tables such
|
||||
as ``alsa_monitor.properties`` or ``bluez_monitor.properties``, then in many
|
||||
cases porting to the new format can be done as follows:
|
||||
|
||||
.. code-block:: lua
|
||||
:caption: ~/.config/wireplumber/bluetooth.lua.d/80-bluez-properties.lua
|
||||
|
||||
bluez_monitor.properties["bluez5.roles"] = "[ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]"
|
||||
bluez_monitor.properties["bluez5.hfphsp-backend"] = "native"
|
||||
|
||||
.. code-block::
|
||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-bluez-properties.conf
|
||||
|
||||
monitor.bluez.properties = {
|
||||
bluez5.roles = [ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]
|
||||
bluez5.hfphsp-backend = "native"
|
||||
}
|
||||
|
||||
See also :ref:`config_modifying_configuration_static`.
|
||||
|
||||
In a lot of cases, however, these properties have been promoted to become either
|
||||
:ref:`Settings <config_modifying_configuration_settings>` or
|
||||
:ref:`Features <config_modifying_configuration_features>`.
|
||||
Here are some common examples:
|
||||
|
||||
Disabling the D-Bus device reservation API in the ALSA monitor:
|
||||
|
||||
* Old format:
|
||||
|
||||
.. code-block:: lua
|
||||
:caption: ~/.config/wireplumber/main.lua.d/80-disable-alsa-reserve.lua
|
||||
|
||||
alsa_monitor.properties["alsa.reserve"] = false
|
||||
|
||||
* New format:
|
||||
|
||||
.. code-block::
|
||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-disable-alsa-reserve.conf
|
||||
|
||||
wireplumber.profiles = {
|
||||
main = {
|
||||
monitor.alsa.reserve-device = disabled
|
||||
}
|
||||
}
|
||||
|
||||
Disabling seat monitoring via logind in the BlueZ monitor:
|
||||
|
||||
* Old format:
|
||||
|
||||
.. code-block:: lua
|
||||
:caption: ~/.config/wireplumber/bluetooth.lua.d/80-disable-logind.lua
|
||||
|
||||
bluez_monitor.properties["with-logind"] = false
|
||||
|
||||
* New format:
|
||||
|
||||
.. code-block::
|
||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-disable-logind.conf
|
||||
|
||||
wireplumber.profiles = {
|
||||
main = {
|
||||
monitor.bluez.seat-monitoring = disabled
|
||||
}
|
||||
}
|
||||
|
||||
See also :ref:`config_modifying_configuration_features`.
|
||||
|
||||
Linking policy configuration (moved to settings and renamed):
|
||||
|
||||
* Old format:
|
||||
|
||||
.. code-block:: lua
|
||||
:caption: ~/.config/wireplumber/policy.lua.d/80-policy.lua
|
||||
|
||||
default_policy.policy = {
|
||||
["move"] = false,
|
||||
["follow"] = false,
|
||||
}
|
||||
|
||||
* New format:
|
||||
|
||||
.. code-block::
|
||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/80-policy.conf
|
||||
|
||||
wireplumber.settings = {
|
||||
linking.allow-moving-streams = false
|
||||
linking.follow-default-target = false
|
||||
}
|
||||
|
||||
See also :ref:`config_modifying_configuration_settings` and remember that
|
||||
settings can also be changed at runtime via :command:`wpctl`.
|
||||
|
||||
Loading custom scripts
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you had custom Lua scripts that were loaded by the old configuration file,
|
||||
you need to port the old ``load_script()`` commands into component descriptions.
|
||||
|
||||
For example, if you had a script that was loaded like this:
|
||||
|
||||
.. code-block:: lua
|
||||
:caption: ~/.config/wireplumber/main.lua.d/99-my-script.lua
|
||||
|
||||
load_script("my-script.lua")
|
||||
|
||||
You should now create a new component description in the configuration file
|
||||
and also make sure to require it in the profile:
|
||||
|
||||
.. code-block::
|
||||
:caption: ~/.config/wireplumber/wireplumber.conf.d/99-my-script.conf
|
||||
|
||||
wireplumber.components = [
|
||||
{
|
||||
name = my-script.lua, type = script/lua
|
||||
provides = custom.my-script
|
||||
}
|
||||
]
|
||||
|
||||
wireplumber.profiles = {
|
||||
main = {
|
||||
custom.my-script = required
|
||||
}
|
||||
}
|
||||
|
||||
.. attention::
|
||||
|
||||
Another important thing to mention here is the location of custom scripts. In
|
||||
0.4, scripts could be loaded in configuration locations such as
|
||||
``~/.config/wireplumber/scripts/`` and ``/etc/wireplumber/scripts/``. In 0.5,
|
||||
the XDG base directory specification for data files is honored, so the new
|
||||
location for custom scripts is ``~/.local/share/wireplumber/scripts/`` and
|
||||
anything else specified in ``$XDG_DATA_HOME`` and ``$XDG_DATA_DIRS``. See
|
||||
:ref:`daemon_file_locations` for more information.
|
||||
365
docs/rst/daemon/configuration/modifying_configuration.rst
Normal file
365
docs/rst/daemon/configuration/modifying_configuration.rst
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
.. _config_modifying_configuration:
|
||||
|
||||
Modifying configuration
|
||||
=======================
|
||||
|
||||
WirePlumber is a heavily modular daemon that depends on its configuration
|
||||
file to operate. If you were to start WirePlumber with an empty configuration
|
||||
file, it would fail to start. This is why the default configuration file is
|
||||
installed in the system-wide application data directory, which prevents it from
|
||||
being modified by the user.
|
||||
|
||||
It is technically possible, if you wish, to copy the default configuration
|
||||
file in one of the other :ref:`configuration search locations <config_locations>`
|
||||
and modify it. However, this is **not recommended**, as it may lead to issues
|
||||
when upgrading WirePlumber.
|
||||
|
||||
In the :ref:`Configuration file <config_conf_file>` section, we saw that
|
||||
configuration files support fragments, which allow you to override or extend the
|
||||
default configuration. This is the recommended way to modify the configuration.
|
||||
|
||||
Working with fragments
|
||||
----------------------
|
||||
|
||||
The easiest way to add :ref:`fragments <config_conf_file_fragments>` to
|
||||
modify the default configuration is to create a directory called
|
||||
``~/.config/wireplumber/wireplumber.conf.d`` and place your fragments there.
|
||||
|
||||
All fragment files need to have the ``.conf`` extension and must be valid
|
||||
SPA-JSON files. The fragments are loaded in alphanumerical order, so you can
|
||||
control the order in which they are loaded by naming them accordingly. It is
|
||||
recommended to use a numeric prefix for the file names, e.g.
|
||||
``10-my-fragment.conf``, ``20-my-other-fragment.conf``, etc., so that you can
|
||||
easily control the order in which they are loaded.
|
||||
|
||||
.. _config_modifying_configuration_features:
|
||||
|
||||
Customizing the loaded features
|
||||
-------------------------------
|
||||
|
||||
As seen in the :ref:`Components & Profiles <config_components_and_profiles>`
|
||||
section, the list of components that are loaded can be customized by enabling or
|
||||
disabling :ref:`well-known features <config_features>` in the profile that is
|
||||
in use by WirePlumber.
|
||||
|
||||
The default profile of WirePlumber is called ``main``, so a fragment that
|
||||
enables or disables a specific feature in the default configuration should look
|
||||
like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.profiles = {
|
||||
main = {
|
||||
some.feature.name = disabled
|
||||
some.other.feature.name = required
|
||||
}
|
||||
}
|
||||
|
||||
Remember that features can be ``required``, ``optional`` or ``disabled``. See
|
||||
the :ref:`Components & Profiles <config_components_and_profiles>` for details.
|
||||
|
||||
.. _config_modifying_configuration_settings:
|
||||
|
||||
Modifying dynamic options ("settings")
|
||||
--------------------------------------
|
||||
|
||||
As seen in the :ref:`Configuration option types <config_configuration_option_types>`
|
||||
section, WirePlumber components can be partly configured with dynamic options
|
||||
(referred to as "settings"). These settings can either be modified permanently
|
||||
in the configuration file, or they can be modified at runtime using the
|
||||
``wpctl`` command-line tool.
|
||||
|
||||
To modify a setting in the configuration file, you can use a fragment like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.settings = {
|
||||
some.setting.name = value
|
||||
}
|
||||
|
||||
For example, setting the ``device.routes.default-sink-volume`` setting to
|
||||
``0.5`` can be done like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.settings = {
|
||||
device.routes.default-sink-volume = 0.5
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
Since the configuration file is only read at startup, this will only take
|
||||
effect after restarting WirePlumber.
|
||||
|
||||
If you would prefer to change the setting at runtime, you can use ``wpctl`` as
|
||||
follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ wpctl settings device.routes.default-sink-volume 0.5
|
||||
Updated setting 'device.routes.default-sink-volume' to: 0.5
|
||||
|
||||
The above command changes the setting immediately, but for the current
|
||||
WirePlumber instance only. If you want the setting to be applied every time
|
||||
WirePlumber is started, you may also use the ``--save`` option:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ wpctl settings --save device.routes.default-sink-volume 0.5
|
||||
Updated and saved setting 'device.routes.default-sink-volume' to: 0.5
|
||||
|
||||
This will save the setting persistently in WirePlumber's state storage.
|
||||
Even though it is not in the configuration file, this saved value will be
|
||||
applied automatically when WirePlumber is started.
|
||||
|
||||
.. attention::
|
||||
|
||||
When a setting's value is saved, it will override the value from the
|
||||
configuration file. Changing the value in the configuration file will
|
||||
have no effect until the saved value is removed. Use the ``--delete``
|
||||
switch in ``wpctl`` to remove a saved value (see below).
|
||||
|
||||
With ``wpctl``, it is also possible to restore a setting to its default value
|
||||
(taken from the schema), by using the ``--reset`` option. For example, to reset
|
||||
the ``device.routes.default-sink-volume`` setting, the following command can be
|
||||
used:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ wpctl settings --reset device.routes.default-sink-volume
|
||||
Reset setting 'device.routes.default-sink-volume' successfully
|
||||
$ wpctl settings device.routes.default-sink-volume
|
||||
Value: 0.064 (Saved: 0.5)
|
||||
|
||||
Note that the ``--reset`` option will only reset the setting to its default
|
||||
value, but it will not remove the saved value from the state file. If you want
|
||||
to remove the saved value, you can use the ``--delete`` option:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ wpctl settings --delete device.routes.default-sink-volume
|
||||
Deleted setting 'device.routes.default-sink-volume' successfully
|
||||
$ wpctl settings device.routes.default-sink-volume
|
||||
Value: 0.064
|
||||
|
||||
A list of all the available settings can be found in the :ref:`config_settings`
|
||||
section.
|
||||
|
||||
.. _config_modifying_configuration_static:
|
||||
|
||||
Modifying static options
|
||||
------------------------
|
||||
|
||||
Static options always live in their own section of the configuration file.
|
||||
Sections can be of two types: either a JSON object or a JSON array.
|
||||
|
||||
When dealing with a **JSON object**, you can add or modify a key-value pair by
|
||||
creating a fragment like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.some-section = {
|
||||
some.option = new_value
|
||||
}
|
||||
|
||||
This is similar to what we have seen also above for modifying profile features
|
||||
and settings (because both are JSON objects).
|
||||
|
||||
When dealing with a **JSON array**, any values that you define in a fragment
|
||||
will be appended to the array. For example, to add a new rule to the
|
||||
``monitor.alsa.rules`` array, you can create a fragment like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
monitor.alsa.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
device.name = "~alsa_card.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
api.alsa.use-ucm = false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
This will add a new rule to the ``monitor.alsa.rules`` array, which will
|
||||
be evaluated **after** all other rules that were parsed before. This is where
|
||||
the order in which fragments are loaded actually matters.
|
||||
|
||||
If you don't want to append a new rule, but rather override the entire array
|
||||
with a new one, you can do so by using the ``override.`` prefix on the array
|
||||
name:
|
||||
|
||||
.. code-block::
|
||||
|
||||
override.monitor.alsa.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
device.name = "~alsa_card.*"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
api.alsa.use-ucm = false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
This will now replace the entire ``monitor.alsa.rules`` array with this new one.
|
||||
|
||||
.. attention::
|
||||
|
||||
If you want to remove a rule from the array, you will need to override the
|
||||
whole array with a new one that does not contain the rule you want to remove.
|
||||
There is no way to remove a specific element from an array using fragments.
|
||||
|
||||
Another thing worth remembering here is that this behavior of appending values
|
||||
to arrays also works in arrays that are nested inside other arrays or objects.
|
||||
For example, consider this fragment:
|
||||
|
||||
.. code-block::
|
||||
|
||||
monitor.bluez.properties = {
|
||||
bluez5.codecs = [ sbc_xq aac ldac ]
|
||||
}
|
||||
|
||||
If this is the first time that the ``bluez5.codecs`` array is being defined, it
|
||||
will be created with the given values. If it already exists, the given values
|
||||
will be appended to the existing array. If you want to make sure that this
|
||||
fragment will override the existing array, you need to use the ``override.``
|
||||
prefix on the array name:
|
||||
|
||||
.. code-block::
|
||||
|
||||
monitor.bluez.properties = {
|
||||
override.bluez5.codecs = [ sbc_xq aac ldac ]
|
||||
}
|
||||
|
||||
The ``override.`` prefix may also be used in JSON object keys, to override the
|
||||
entire object with a new one. For example, to override the entire
|
||||
``monitor.bluez.properties`` object, you can use a fragment like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
override.monitor.bluez.properties = {
|
||||
bluez5.codecs = [ sbc_xq aac ldac ]
|
||||
}
|
||||
|
||||
Here, the entire ``monitor.bluez.properties`` object will be replaced with the
|
||||
new one, and all previous key-value pairs configured will be discarded. This
|
||||
also means that the ``bluez5.codecs`` array will be replaced with the new one
|
||||
and does not require the ``override.`` prefix.
|
||||
|
||||
.. note::
|
||||
|
||||
Even though WirePlumber uses PipeWire's syntax for configuration files, the
|
||||
``override.`` prefix is a WirePlumber extension and does not work in
|
||||
PipeWire.
|
||||
|
||||
.. _config_modifying_configuration_rules:
|
||||
|
||||
Working with rules
|
||||
------------------
|
||||
|
||||
Some of the static option sections in the configuration file are used to define
|
||||
rules that are evaluated by WirePlumber at runtime. These rules are typically
|
||||
used to match objects and perform actions on them. For example, the
|
||||
``monitor.alsa.rules`` section is used to define rules that are evaluated by
|
||||
the ALSA monitor to match ALSA devices and update their properties.
|
||||
|
||||
The syntax of these rules is the same as the syntax of
|
||||
`PipeWire's rules <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PipeWire#rules>`_.
|
||||
|
||||
A rule is always a JSON object with two keys: ``matches`` and ``actions``. The
|
||||
``matches`` key is used to define the conditions that need to be met for the
|
||||
rule to be evaluated as true, and the ``actions`` key is used to define the
|
||||
actions that are performed when the rule is evaluated as true.
|
||||
|
||||
The ``matches`` key is always a JSON array of objects, where each object
|
||||
defines a condition that needs to be met. Each condition is a list of key-value
|
||||
pairs, where the key is the name of the property that is being matched, and the
|
||||
value is the value that the property needs to have. Within a condition, all
|
||||
the key-value pairs are combined with a logical AND, and all the conditions in
|
||||
the ``matches`` array are combined with a logical OR.
|
||||
|
||||
The ``actions`` key is always a JSON object, where each key-value pair defines
|
||||
an action that is performed when the rule is evaluated as true. The action
|
||||
name is specific to the rule and is defined by the rule's documentation, but
|
||||
most frequently you will see the ``update-props`` action, which is used to
|
||||
update the properties of the matched object.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
some.theoretical.rules = [
|
||||
{
|
||||
matches = [
|
||||
{
|
||||
object.name = "my_object"
|
||||
object.profile.name = "my_profile"
|
||||
}
|
||||
{
|
||||
object.name = "other_object"
|
||||
}
|
||||
]
|
||||
actions = {
|
||||
update-props = {
|
||||
object.tag = "matched_by_my_rule"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
This rule is equivalent to the following expression:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
if (properties["object.name"] == "my_object" and properties["object.profile.name"] == "my_profile") or (properties["object.name"] == "other_object"):
|
||||
properties["object.tag"] = "matched_by_my_rule"
|
||||
|
||||
In the ``matches`` array, it is also possible to use regular expressions to match
|
||||
property values. For example, to match all nodes with a name that starts with
|
||||
``my_``, you can use the following condition:
|
||||
|
||||
.. code-block::
|
||||
|
||||
matches = [
|
||||
{
|
||||
node.name = "~my_.*"
|
||||
}
|
||||
]
|
||||
|
||||
The ``~`` character signifies that the value is a regular expression. The exact
|
||||
syntax of the regular expressions is the POSIX extended regex syntax, as
|
||||
described in the `regex (7)` man page.
|
||||
|
||||
In addition to regular expressions, you may also use the ``!`` character to
|
||||
negate a condition. For example, to match all nodes with a name that does not
|
||||
start with ``my_``, you can use the following condition:
|
||||
|
||||
.. code-block::
|
||||
|
||||
matches = [
|
||||
{
|
||||
node.name = "!~my_.*"
|
||||
}
|
||||
]
|
||||
|
||||
The ``!`` character can be used with or without a regular expression. For
|
||||
example, to match all nodes with a name that is not equal to ``my_node``,
|
||||
you can use the following condition:
|
||||
|
||||
.. code-block::
|
||||
|
||||
matches = [
|
||||
{
|
||||
node.name = "!my_node"
|
||||
}
|
||||
]
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
.. _config_multi_instance:
|
||||
|
||||
Running multiple instances
|
||||
==========================
|
||||
|
||||
WirePlumber has the ability to run either as a single instance daemon or as
|
||||
multiple instances, meaning that there can be multiple processes, each one
|
||||
doing a different task.
|
||||
|
||||
In the default configuration, both setups are supported. The default is to run
|
||||
in single-instance mode.
|
||||
|
||||
In single-instance mode, WirePlumber reads ``wireplumber.conf``, which is the
|
||||
default configuration file, and from there it loads ``main.lua``, ``policy.lua``
|
||||
and ``bluetooth.lua``, which are lua configuration files (deployed as directories)
|
||||
that enable all the relevant functionality.
|
||||
|
||||
In multi-instance mode, WirePlumber is meant to be started with the
|
||||
``--config-file`` command line option 3 times:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ wireplumber --config-file=main.conf
|
||||
$ wireplumber --config-file=policy.conf
|
||||
$ wireplumber --config-file=bluetooth.conf
|
||||
|
||||
That loads one process which reads ``main.conf``, which then loads ``main.lua``
|
||||
and enables core functionality. Then another process that reads ``policy.conf``,
|
||||
which then loads ``policy.lua`` and enables policy functionality... and so on.
|
||||
|
||||
To make this easier to work with, a template systemd unit is provided, which is
|
||||
meant to be started with the name of the main configuration file as a
|
||||
template argument:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ systemctl --user disable wireplumber # disable the single instance
|
||||
|
||||
$ systemctl --user enable wireplumber@main
|
||||
$ systemctl --user enable wireplumber@policy
|
||||
$ systemctl --user enable wireplumber@bluetooth
|
||||
|
||||
It is obviously possible to start as many instances as desired, with manually
|
||||
crafted configuration files, as long as it is ensured that these instances
|
||||
serve a different purpose and they do not conflict with each other.
|
||||
|
|
@ -1,347 +0,0 @@
|
|||
.. _config_policy:
|
||||
|
||||
Policy Configuration
|
||||
====================
|
||||
|
||||
wireplumber.conf.d/policy.conf
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This file contains generic default policy properties that can be configured.
|
||||
|
||||
* *Settings*
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.properties = {
|
||||
default-policy-move = true
|
||||
}
|
||||
|
||||
The above example will set the ``move`` policy property to ``true``.
|
||||
|
||||
The list of supported properties are:
|
||||
|
||||
.. code-block::
|
||||
|
||||
default-policy-move = true
|
||||
|
||||
Moves session items when metadata ``target.node`` changes.
|
||||
|
||||
.. code-block::
|
||||
|
||||
default-policy-follow = true
|
||||
|
||||
Moves session items to the default device when it has changed.
|
||||
|
||||
.. code-block::
|
||||
|
||||
default-policy-audio.no-dsp = false
|
||||
|
||||
Set to ``true`` to disable channel splitting & merging on nodes and enable
|
||||
passthrough of audio in the same format as the format of the device. Note that
|
||||
this breaks JACK support; it is generally not recommended.
|
||||
|
||||
.. code-block::
|
||||
|
||||
default-policy-duck.level = 0.3
|
||||
|
||||
How much to lower the volume of lower priority streams when ducking. Note that
|
||||
this is a linear volume modifier (not cubic as in PulseAudio).
|
||||
|
||||
|
||||
Filters
|
||||
^^^^^^^
|
||||
|
||||
* *Introduction*
|
||||
|
||||
A pair of nodes will be considered filter nodes by wireplumber if they have the
|
||||
"node.link-group" property set to a common value. This propery is always set by
|
||||
PipeWire when creating filter nodes if they are defined in the PipeWire's
|
||||
configuration file. The pair of nodes always consist of a stream node, and a
|
||||
main node. When using the filter nodes, the main node acts as a virtual device,
|
||||
where the audio is sent or captured to/from; and the stream node acts as a
|
||||
virtual stream, where the audio is sent or received to/from the next node in the
|
||||
graph.
|
||||
|
||||
For example, the media class of the nodes for a input filter would be:
|
||||
|
||||
- main node: Audio/Sink
|
||||
- stream node: Stream/Output/Audio
|
||||
|
||||
And, if this filter is used between an application stream, and the default audio
|
||||
device, the graph would look like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
application stream node -> filter main node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
.. code-block::
|
||||
|
||||
filter stream node -> default device node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
|
||||
On the other hand, the media class of the nodes for an output filter would be:
|
||||
|
||||
- main node: Audio/Source
|
||||
- stream node: Stream/Input/Audio
|
||||
|
||||
And the same logic is applied if they are used, but in the opposite direction.
|
||||
This is how the graph would look like if an application wants to capture audio
|
||||
from a device that uses an input filter.
|
||||
|
||||
.. code-block::
|
||||
|
||||
application stream node <- filter main node
|
||||
(Stream/Input/Audio) (Audio/Source)
|
||||
|
||||
.. code-block::
|
||||
|
||||
filter stream node <- default device node
|
||||
(Stream/Input/Audio) (Audio/Source)
|
||||
|
||||
Finally, if multiple filters have the same direction, they can also be chained
|
||||
together so that the audio of a filter is sent to the input of the next filter.
|
||||
|
||||
Example of existing filters in PipeWire are echo-cancel, filter-chain and
|
||||
loopback nodes.
|
||||
|
||||
The next section will describe how we can define filter properties so that they
|
||||
are automatically linked by the wirepluber policy in any way we want.
|
||||
|
||||
|
||||
* *Filter properties*
|
||||
|
||||
Currently, if a filter node is created, wireplumber will check the following
|
||||
optional node properties on the main node:
|
||||
|
||||
- filter.smart:
|
||||
Boolean indicating whether smart policy will be used in the filter nodes or
|
||||
not. This is disabled by default, therefore filter nodes will be treated as
|
||||
regular nodes, without applying any kid of extra logic. On the other hand, if
|
||||
this property is set to true, automatic (smart) filter policy will be used
|
||||
when linking filters. The properties below will instruct the smart policy how
|
||||
to link the filters automatically.
|
||||
|
||||
- filter.smart.name:
|
||||
The unique name of the filter. WirePlumber will use the "node.link-group"
|
||||
property as filter name if this property is not set.
|
||||
|
||||
- filter.smart.disabled:
|
||||
Boolean indicating whether the filter should be disabled at all or not. A
|
||||
disabled filter will never be used in any circumstances. If the property is
|
||||
not set, wireplumber will consider the filter not disabled by default.
|
||||
|
||||
- filter.smart.target:
|
||||
A JSON object that defines the matching properties of the filter's target node.
|
||||
A filter target can never be another filter node (wireplumber will ignore it),
|
||||
and must always be a device node. If this property is not set, WirePlumber will
|
||||
use the default node as target.
|
||||
|
||||
- filter.smart.before:
|
||||
A JSON array with the filters names that are supposed to be used before this
|
||||
filter. If not set, wireplumber will link the filters by order of creation.
|
||||
|
||||
- filter.smart.after:
|
||||
A JSON array with the filters names that are supposed to be used after this
|
||||
filter. If not set, wireplumber will link the filters by order of creation.
|
||||
|
||||
Note that these properties must be set in the filter's main node, not the
|
||||
filter's stream node.
|
||||
|
||||
As an example, we will describe here how to create 2 loopback filters in the
|
||||
PipeWire's configuration, with names loopback-1 and loopback-2, that will be
|
||||
linked with the default audio device, and use loopback-2 filter as the last
|
||||
filter in the chain.
|
||||
|
||||
The PipeWire configuration files for the 2 filters should be like this:
|
||||
|
||||
- /usr/share/pipewire/pipewire.conf.d/loopback-1.conf:
|
||||
|
||||
.. code-block::
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-loopback
|
||||
args = {
|
||||
node.name = loopback-1-sink
|
||||
node.description = "Loopback 1 Sink"
|
||||
capture.props = {
|
||||
audio.position = [ FL FR ]
|
||||
media.class = Audio/Sink
|
||||
filter.smart.name = loopback-1
|
||||
filter.smart.disabled = false
|
||||
filter.smart.before = [ loopback-2 ]
|
||||
}
|
||||
playback.props = {
|
||||
audio.position = [ FL FR ]
|
||||
node.passive = true
|
||||
node.dont-remix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
- /usr/share/pipewire/pipewire.conf.d/loopback-2.conf:
|
||||
|
||||
.. code-block::
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-loopback
|
||||
args = {
|
||||
node.name = loopback-2-sink
|
||||
node.description = "Loopback 2 Sink"
|
||||
capture.props = {
|
||||
audio.position = [ FL FR ]
|
||||
media.class = Audio/Sink
|
||||
filter.smart.name = loopback-2
|
||||
filter.smart.disabled = false
|
||||
}
|
||||
playback.props = {
|
||||
audio.position = [ FL FR ]
|
||||
node.passive = true
|
||||
node.dont-remix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Finally, if we restart PipeWire and WirePlumber to apply the configuration
|
||||
changes, and play a test.wave audio file with paplay to see if wireplumber links
|
||||
the filter nodes properly, the graph should look like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
paplay node -> loopback-1 main node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
.. code-block::
|
||||
|
||||
loopback-1 stream node -> loopback-1 main node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
.. code-block::
|
||||
|
||||
loopback-2 stream node -> default device node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
|
||||
If we remove `filter.smart.before = [ loopback-2 ]` property from the loopback-1
|
||||
filter, and add a `filter.smart.before = [ loopback-1 ]` property in the loopback-2
|
||||
filter configuration file. WirePlumber should link the loopback-1 filter as the last
|
||||
filter in the chain, like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
paplay node -> loopback-2 main node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
.. code-block::
|
||||
|
||||
loopback-2 stream node -> loopback-1 main node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
.. code-block::
|
||||
|
||||
loopback-1 stream node -> default device node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
|
||||
On the other hand, the filters can have different targets. For example, we can
|
||||
define the filters like this:
|
||||
|
||||
- `/usr/share/pipewire/pipewire.conf.d/loopback-1.conf`:
|
||||
|
||||
.. code-block::
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-loopback
|
||||
args = {
|
||||
node.name = loopback-1-sink
|
||||
node.description = "Loopback 1 Sink"
|
||||
capture.props = {
|
||||
audio.position = [ FL FR ]
|
||||
media.class = Audio/Sink
|
||||
filter.smart.name = loopback-1
|
||||
filter.smart.disabled = false
|
||||
filter.smart.before = [ loopback-2 ]
|
||||
filter.smart.target = { node.name = "not-default-audio-device-name" }
|
||||
}
|
||||
playback.props = {
|
||||
audio.position = [ FL FR ]
|
||||
node.passive = true
|
||||
node.dont-remix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
- `/usr/share/pipewire/pipewire.conf.d/loopback-2.conf`:
|
||||
|
||||
.. code-block::
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-loopback
|
||||
args = {
|
||||
node.name = loopback-2-sink
|
||||
node.description = "Loopback 2 Sink"
|
||||
capture.props = {
|
||||
audio.position = [ FL FR ]
|
||||
media.class = Audio/Sink
|
||||
filter.smart.name = loopback-2
|
||||
filter.smart.disabled = false
|
||||
}
|
||||
playback.props = {
|
||||
audio.position = [ FL FR ]
|
||||
node.passive = true
|
||||
node.dont-remix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
If this is the case, WirePlumber will link the filters like this when using
|
||||
paplay:
|
||||
|
||||
.. code-block::
|
||||
|
||||
paplay node -> loopback-2 main node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
.. code-block::
|
||||
|
||||
loopback-2 stream node -> default device node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
.. code-block::
|
||||
|
||||
loopback-1 stream node -> not-default-audio-device-name device node
|
||||
(Stream/Output/Audio) (Audio/Sink)
|
||||
|
||||
The loopback-1 main node will only be used if an application wants to play audio
|
||||
on the device node with node name "not-default-audio-device-name".
|
||||
|
||||
|
||||
* *Filters metadata*
|
||||
|
||||
Similar to the default metadata, it is also possible to override the filter
|
||||
properties by using the "filters" metadata. This allow users to change the filters
|
||||
policy at runtime.
|
||||
|
||||
For example, if loopback-1 main node Id is `40`, we can disable the filter by
|
||||
setting its "filter.smart.disabled" metadata key to true using the `pw-metadata`
|
||||
tool:
|
||||
|
||||
.. code-block::
|
||||
|
||||
$ pw-metadata -n filters 40 "filter.smart.disabled" true Spa:String:JSON
|
||||
|
||||
We can also change the target of a filter at runtime:
|
||||
|
||||
.. code-block::
|
||||
|
||||
$ pw-metadata -n filters 40 "filter.smart.target" { node.name = "new-target-node-name" } Spa:String:JSON
|
||||
|
||||
Every time a key in the filters metadata changes, all filters are unlinked and
|
||||
re-linked properly by the policy.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
.. _config_settings:
|
||||
|
||||
WirePlumber Settings
|
||||
====================
|
||||
Well-known settings
|
||||
===================
|
||||
|
||||
This section describes the settings that can be configured on WirePlumber.
|
||||
|
||||
|
|
@ -9,4 +9,220 @@ Settings can be either configured statically in the configuration file
|
|||
by setting them under the ``wireplumber.settings`` section, or they can be
|
||||
configured dynamically at runtime by using metadata.
|
||||
|
||||
.. include:: ../../../../src/scripts/lib/SETTINGS.rst
|
||||
For more information on what "settings" are and how they work, refer to the
|
||||
previous section: :ref:`config_configuration_option_types`.
|
||||
|
||||
.. describe:: device.restore-profile
|
||||
|
||||
When a device profile is changed manually (e.g. via pavucontrol), WirePlumber
|
||||
stores the selected profile and restores it when the device appears again
|
||||
(e.g. after a reboot). If this setting is disabled, WirePlumber will always
|
||||
pick the best profile for the device based on profile priorities and
|
||||
availability (or custom rules, if any).
|
||||
|
||||
:Default value: ``true``
|
||||
|
||||
.. describe:: device.restore-routes
|
||||
|
||||
When a device route is changed manually (e.g. via pavucontrol), WirePlumber
|
||||
stores the selected route and restores it when the same profile is
|
||||
selected for this device. If this setting is disabled, WirePlumber will
|
||||
always pick the best route for this device profile based on route priorities
|
||||
and availability (or custom rules, if any).
|
||||
|
||||
This setting also enables WirePlumber to restore properties of the device
|
||||
route when the route is restored. This includes the volume levels of sources
|
||||
and sinks, as well as the IEC958 codecs selected (for routes that support
|
||||
encoded streams, such as HDMI).
|
||||
|
||||
:Default value: ``true``
|
||||
|
||||
.. describe:: device.routes.default-sink-volume
|
||||
|
||||
This option allows to set the default volume for sinks that are part of a
|
||||
device route (e.g. ALSA PCM sinks). This is used when the route is restored
|
||||
and the sink does not have a previously stored volume.
|
||||
|
||||
It is possible to override the value on a per-device basis with a property
|
||||
(*not* a setting, so this would go into a configuration file) on the device
|
||||
named ``device.routes.default-sink-volume``.
|
||||
|
||||
:Default value: ``0.4 ^ 3`` (40% on the cubic scale)
|
||||
|
||||
.. describe:: device.routes.default-source-volume
|
||||
|
||||
This option allows to set the default volume for sources that are part of a
|
||||
device route (e.g. ALSA PCM sources). This is used when the route is restored
|
||||
and the source does not have a previously stored volume.
|
||||
|
||||
It is possible to override the value on a per-device basis with a property
|
||||
(*not* a setting, so this would go into a configuration file) on the device
|
||||
named ``device.routes.default-source-volume``.
|
||||
|
||||
:Default value: ``1.0`` (100%)
|
||||
|
||||
.. describe:: linking.allow-moving-streams
|
||||
|
||||
This option allows moving streams by overriding their target via metadata.
|
||||
When enabled, WirePlumber monitors the "default" metadata for changes in the
|
||||
``target.object`` key of streams and if this key is set to a valid node name
|
||||
(``node.name``) or serial (``object.serial``), the stream is moved to that
|
||||
target node.
|
||||
|
||||
This is used by applications such as pavucontrol and is recommended for
|
||||
compatibility with PulseAudio.
|
||||
|
||||
.. note::
|
||||
|
||||
On the metadata, the ``target.node`` key is also supported for
|
||||
compatibility with older versions of PipeWire, but it is deprecated.
|
||||
Please use the ``target.object`` key instead.
|
||||
|
||||
:Default value: ``true``
|
||||
:See also: ``node.stream.restore-target``
|
||||
|
||||
.. describe:: linking.follow-default-target
|
||||
|
||||
When a stream was started with the ``target.object`` property, WirePlumber
|
||||
normally links that stream to that target node and ignores the "default"
|
||||
target for that direction. However, if this option is enabled, WirePlumber
|
||||
will check if the designated target node *is* the "default" target and if so,
|
||||
it will act as if the stream did not have that property.
|
||||
|
||||
In practice, this means that if the "default" target changes at runtime,
|
||||
the stream will be moved to the new "default" target.
|
||||
|
||||
This is what Pulseaudio does and is implemented here for compatibility
|
||||
with some applications that do start with a ``target.object`` property
|
||||
set to the "default" target and expect the stream to be moved when the
|
||||
"default" target changes.
|
||||
|
||||
Note that this logic is only applied on client (i.e. application) streams
|
||||
and *not* on filters.
|
||||
|
||||
:Default value: ``true``
|
||||
|
||||
.. describe:: linking.pause-playback
|
||||
|
||||
When an audio sink is removed, pause media players that have streams
|
||||
playing to it. Pausing is done via MPRIS interface.
|
||||
|
||||
:Default value: ``true``
|
||||
|
||||
.. describe:: node.features.audio.no-dsp
|
||||
|
||||
When this option is set to ``true``, audio nodes will not be configured
|
||||
in dsp mode, meaning that their channels will *not* be split into separate
|
||||
ports and that the audio data will *not* be converted to the float 32 format
|
||||
(F32P). Instead, devices will be configured in passthrough mode and streams
|
||||
will be configured in convert mode, so that their audio data is converted
|
||||
directly to the format that the device is expecting.
|
||||
|
||||
This may be useful if you are trying to minimize audio processing for an
|
||||
embedded system, but it is not recommended for general use.
|
||||
|
||||
.. warning::
|
||||
|
||||
This option **will break** compatibility with JACK applications
|
||||
and may also break certain patchbay applications. Do not enable, unless
|
||||
you understand what you are doing.
|
||||
|
||||
:Default value: ``false``
|
||||
|
||||
.. describe:: node.features.audio.monitor-ports
|
||||
|
||||
This enables the creation of "monitor" ports for audio nodes. Monitor ports
|
||||
are created on nodes that have input ports (i.e. sinks and capture streams)
|
||||
and allow monitoring of the audio data that is being sent to the node.
|
||||
|
||||
This is mostly used by monitoring applications, such as pavucontrol.
|
||||
|
||||
:Default value: ``true``
|
||||
|
||||
.. describe:: node.features.audio.control-port
|
||||
|
||||
This enables the creation of a "control" port for audio nodes. Control ports
|
||||
allow sending MIDI data to the node, allowing for control of certain node's
|
||||
parameters (such as volume) via external controllers.
|
||||
|
||||
:Default value: ``false``
|
||||
|
||||
.. describe:: node.stream.restore-props
|
||||
|
||||
WirePlumber stores stream parameters such as volume and mute status for each
|
||||
client (i.e. application) stream. If this setting is enabled, WirePlumber
|
||||
will restore the previously stored stream parameters when the stream is
|
||||
activated. If it is disabled, stream parameters will be initialized to their
|
||||
default values.
|
||||
|
||||
:Default value: ``true``
|
||||
|
||||
.. describe:: node.stream.restore-target
|
||||
|
||||
When a client (i.e. application) stream is manually moved to a different
|
||||
target node (e.g. via pavucontrol), the target node is stored by WirePlumber.
|
||||
If this setting is enabled, WirePlumber will restore the previously stored
|
||||
target node when the stream is activated.
|
||||
|
||||
.. note::
|
||||
|
||||
This does not restore manual links made by patchbay applications. This
|
||||
is only meant to restore the ``target.object`` property in the "default"
|
||||
metadata, which is manipulated by applications such as pavucontrol when
|
||||
a stream is moved to a different target.
|
||||
|
||||
:Default value: ``true``
|
||||
:See also: ``linking.allow-moving-streams``
|
||||
|
||||
.. describe:: node.stream.default-playback-volume
|
||||
|
||||
The default volume for playback streams to be applied when the stream is
|
||||
activated. This is only applied when ``node.stream.restore-props`` is
|
||||
``true`` and the stream does not have a previously stored volume.
|
||||
|
||||
:Default value: ``1.0``
|
||||
:Range: ``0.0`` to ``1.0``
|
||||
|
||||
.. describe:: node.stream.default-capture-volume
|
||||
|
||||
The default volume for capture streams to be applied when the stream is
|
||||
activated. This is only applied when ``node.stream.restore-props`` is
|
||||
``true`` and the stream does not have a previously stored volume.
|
||||
|
||||
:Default value: ``1.0``
|
||||
:Range: ``0.0`` to ``1.0``
|
||||
|
||||
.. describe:: node.filter.forward-format
|
||||
|
||||
When a "filter" pair of nodes (such as echo-cancel or filter-chain) is
|
||||
linked to a device node that has a different channel map than the filter
|
||||
nodes, this option allows the channel map of the filter nodes to be changed
|
||||
to match the channel map of the device node. The change is applied to both
|
||||
ends of the "filter", so that any streams linked to the filter are also
|
||||
reconfigured to match the target channel map.
|
||||
|
||||
This is useful, for instance, to make sure that an application will be
|
||||
properly configured to output surround audio to a surround device, even
|
||||
when going through a filter that was not explicitly configured to have
|
||||
a surround channel map.
|
||||
|
||||
:Default value: ``false``
|
||||
|
||||
.. describe:: node.restore-default-targets
|
||||
|
||||
This setting enables WirePlumber to store and restore the "default" source
|
||||
and sink targets of the graph. In PulseAudio terminology, this is also known
|
||||
as the "fallback" source and sink.
|
||||
|
||||
When this setting is enabled, WirePlumber will store the "default" source
|
||||
and sink targets when they are changed manually (e.g. via pavucontrol) and
|
||||
restore them when the available nodes change or after a reload/restart.
|
||||
It will also store a history of past selected "default" targets and restore
|
||||
previously selected ones if the currently selected are not available.
|
||||
|
||||
If this is disabled, WirePlumber will pick the best available source
|
||||
and sink targets based on their priorities, but it will also respect
|
||||
manual user selections that are done at runtime - it will just not remember
|
||||
them so that it can restore them at a later time.
|
||||
|
||||
:Default value: ``true``
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ Dependencies
|
|||
|
||||
In order to compile WirePlumber you will need:
|
||||
|
||||
* GLib >= 2.62
|
||||
* PipeWire 0.3 (>= 0.3.43)
|
||||
* GLib >= 2.68
|
||||
* PipeWire >= 1.0
|
||||
* Lua 5.3 or 5.4
|
||||
|
||||
Lua is optional in the sense that if it is not found in the system, a bundled
|
||||
|
|
|
|||
175
docs/rst/daemon/locations.rst
Normal file
175
docs/rst/daemon/locations.rst
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
.. _daemon_file_locations:
|
||||
|
||||
Locations of WirePlumber's files
|
||||
================================
|
||||
|
||||
.. _config_locations:
|
||||
|
||||
Location of configuration files
|
||||
-------------------------------
|
||||
|
||||
WirePlumber's default locations of its configuration files are the following,
|
||||
in order of priority:
|
||||
|
||||
1. ``$XDG_CONFIG_HOME/wireplumber``
|
||||
2. ``$XDG_CONFIG_DIRS/wireplumber``
|
||||
3. ``$sysconfdir/wireplumber``
|
||||
4. ``$XDG_DATA_DIRS/wireplumber``
|
||||
5. ``$datadir/wireplumber``
|
||||
|
||||
Notes:
|
||||
|
||||
* ``$syscondir`` and ``$datadir`` refer to
|
||||
`meson's directory options <https://mesonbuild.com/Builtin-options.html#directories>`_
|
||||
and are hardcoded at build time
|
||||
* ``$XDG_`` variables refer to the
|
||||
`XDG Base Directory Specification <https://specifications.freedesktop.org/basedir-spec/latest/index.html>`_
|
||||
|
||||
It is recommended that user specific overrides are placed in
|
||||
``$XDG_CONFIG_HOME/wireplumber``, while host-specific configuration is placed in
|
||||
``$XDG_CONFIG_DIRS/wireplumber`` or ``$sysconfdir/wireplumber`` and
|
||||
distribution-provided configuration is placed in ``$XDG_DATA_DIRS/wireplumber``
|
||||
or ``$datadir/wireplumber``.
|
||||
|
||||
At runtime, WirePlumber will seek out the directory with the highest priority
|
||||
that contains the required configuration file. This setup allows a user or
|
||||
system administrator to effortlessly override the configuration files provided
|
||||
by the distribution. They can achieve this by placing a file with an identical
|
||||
name in a higher priority directory.
|
||||
|
||||
It is also possible to override the configuration directory by setting the
|
||||
``WIREPLUMBER_CONFIG_DIR`` environment variable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
WIREPLUMBER_CONFIG_DIR=src/config wireplumber
|
||||
|
||||
``WIREPLUMBER_CONFIG_DIR`` supports listing multiple directories, using the
|
||||
standard path list separator ``:``. If multiple directories are specified,
|
||||
the first one has the highest priority and the last one has the lowest.
|
||||
|
||||
.. note::
|
||||
|
||||
When the configuration directory is overridden with
|
||||
``WIREPLUMBER_CONFIG_DIR``, the default locations are ignored and
|
||||
configuration files are *only* looked up in the directories specified by this
|
||||
variable.
|
||||
|
||||
.. _config_locations_fragments:
|
||||
|
||||
Configuration fragments
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
WirePlumber also supports configuration fragments. These are configuration files
|
||||
that are loaded in addition to the main configuration file, allowing to
|
||||
override or extend the configuration without having to copy the whole file.
|
||||
See also the :ref:`config_conf_file_fragments` section for semantics.
|
||||
|
||||
Configuration fragments are always loaded from subdirectories of the main search
|
||||
directories that have the same name as the configuration file, with the ``.d``
|
||||
suffix appended. For example, if WirePlumber loads ``wireplumber.conf``, it will
|
||||
also load ``wireplumber.conf.d/*.conf``. Note also that the fragment files need
|
||||
to have the ``.conf`` suffix.
|
||||
|
||||
When WirePlumber loads a configuration file from the default locations, it will
|
||||
also load all configuration fragments that are present in all of the default
|
||||
locations, but following the reverse order of priority. This allows
|
||||
configuration fragments that are installed in more system-wide locations to be
|
||||
overridden by the system administrator or the users.
|
||||
|
||||
For example, assuming WirePlumber loads ``wireplumber.conf``, from any of the
|
||||
search locations, it will also locate and load the following fragments, in this
|
||||
order:
|
||||
|
||||
1. ``$datadir/wireplumber/wireplumber.conf.d/*.conf``
|
||||
2. ``$XDG_DATA_DIRS/wireplumber/wireplumber.conf.d/*.conf``
|
||||
3. ``$sysconfdir/wireplumber/wireplumber.conf.d/*.conf``
|
||||
4. ``$XDG_CONFIG_DIRS/wireplumber/wireplumber.conf.d/*.conf``
|
||||
5. ``$XDG_CONFIG_HOME/wireplumber/wireplumber.conf.d/*.conf``
|
||||
|
||||
Within each search location that contains fragments, the individual fragment
|
||||
files are opened in alphanumerical order. This can be important to know, because
|
||||
the parsing order matters in merging. See :ref:`config_conf_file_fragments`
|
||||
|
||||
.. note::
|
||||
|
||||
When ``WIREPLUMBER_CONFIG_DIR`` is set, the default locations are ignored and
|
||||
fragment files are *only* looked up in the directories specified by this
|
||||
variable.
|
||||
|
||||
.. _config_locations_scripts:
|
||||
|
||||
Location of scripts
|
||||
-------------------
|
||||
|
||||
WirePlumber's default locations of its data files are the following,
|
||||
in order of priority:
|
||||
|
||||
1. ``$XDG_DATA_HOME/wireplumber``
|
||||
2. ``$XDG_DATA_DIRS/wireplumber``
|
||||
3. ``$datadir/wireplumber``
|
||||
|
||||
At runtime, WirePlumber will search the directories for the highest-priority
|
||||
directory to contain the needed data file.
|
||||
|
||||
Scripts are a specific kind of "data" files and are expected to be located
|
||||
within a ``scripts`` subdirectory in the above data search locations. The "data"
|
||||
directory is a somewhat more generic path that may be used for other kinds of
|
||||
data files in the future.
|
||||
|
||||
It is also possible to override the data directory by setting the
|
||||
``WIREPLUMBER_DATA_DIR`` environment variable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
WIREPLUMBER_DATA_DIR=src wireplumber
|
||||
|
||||
As with the default data directories, script files in particular are expected
|
||||
to be located within a ``scripts`` subdirectory, so in the above example the
|
||||
scripts would actually reside in ``src/scripts``.
|
||||
|
||||
``WIREPLUMBER_DATA_DIR`` supports listing multiple directories, using the
|
||||
standard path list separator ``:``. If multiple directories are specified,
|
||||
the first one has the highest priority and the last one has the lowest.
|
||||
|
||||
.. note::
|
||||
|
||||
When ``WIREPLUMBER_DATA_DIR`` is set, the default locations are ignored and
|
||||
scripts are *only* looked up in the directories specified by this variable.
|
||||
|
||||
Location of modules
|
||||
-------------------
|
||||
|
||||
WirePlumber modules
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
WirePlumber's default location of its modules is
|
||||
``$libdir/wireplumber-$api_version``, where ``$libdir`` is set at compile time
|
||||
by the build system. Typically, it ends up being ``/usr/lib/wireplumber-0.5``
|
||||
(or ``/usr/lib/<arch-triplet>/wireplumber-0.5`` on multiarch systems)
|
||||
|
||||
It is possible to override this directory at runtime by setting the
|
||||
``WIREPLUMBER_MODULE_DIR`` environment variable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
WIREPLUMBER_MODULE_DIR=build/modules wireplumber
|
||||
|
||||
``WIREPLUMBER_MODULE_DIR`` supports listing multiple directories, using the
|
||||
standard path list separator ``:``. If multiple directories are specified, the
|
||||
first one has the highest priority and the last one has the lowest.
|
||||
|
||||
.. note::
|
||||
|
||||
When ``WIREPLUMBER_MODULE_DIR`` is set, the default locations are ignored and
|
||||
scripts are *only* looked up in the directories specified by this variable.
|
||||
|
||||
PipeWire and SPA modules
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
PipeWire and SPA modules are not loaded from the same location as WirePlumber's
|
||||
modules. They are loaded from the location that PipeWire loads them.
|
||||
|
||||
It is also possible to override these locations by using environment variables:
|
||||
``SPA_PLUGIN_DIR`` and ``PIPEWIRE_MODULE_DIR``. For more details, refer to
|
||||
PipeWire's documentation.
|
||||
|
|
@ -127,6 +127,42 @@ Above, ``<ID>`` should be replaced by the WirePlumber daemon client ID.
|
|||
Note that PipeWire daemon log levels must be specified by numbers, not
|
||||
letter codes.
|
||||
|
||||
Changing log level via static configuration
|
||||
-------------------------------------------
|
||||
|
||||
If you need to capture logs from WirePlumber at startup or in other circumstances
|
||||
where changing the level at runtime or setting an environment variable is not
|
||||
feasible, then you may also set the log level in the configuration file.
|
||||
|
||||
The log level changes via the ``log.level`` key in the ``context.properties``
|
||||
section:
|
||||
|
||||
.. code::
|
||||
|
||||
context.properties = {
|
||||
log.level = "D"
|
||||
}
|
||||
|
||||
You may use the same syntax as in ``WIREPLUMBER_DEBUG`` to describe the exact
|
||||
logging you want to achieve. For instance, to log debug messages from all
|
||||
scripts and informational messages from everywhere else:
|
||||
|
||||
.. code::
|
||||
|
||||
context.properties = {
|
||||
log.level = "I,s-*:D"
|
||||
}
|
||||
|
||||
The easiest way to configure this is to drop a
|
||||
:ref:`fragment file <config_conf_file_fragments>` that contains just this.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ mkdir -p ~/.config/wireplumber/wireplumber.conf.d
|
||||
$ echo 'context.properties = { log.level = "D" }' > ~/.config/wireplumber/wireplumber.conf.d/log.conf
|
||||
|
||||
See also :ref:`config_modifying_configuration`
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ sphinx_files += files(
|
|||
'installing.rst',
|
||||
'running.rst',
|
||||
'configuration.rst',
|
||||
'locations.rst',
|
||||
'logging.rst',
|
||||
'multi_instance.rst',
|
||||
)
|
||||
|
||||
subdir('configuration')
|
||||
|
|
|
|||
88
docs/rst/daemon/multi_instance.rst
Normal file
88
docs/rst/daemon/multi_instance.rst
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
.. _daemon_multi_instance:
|
||||
|
||||
Running multiple instances
|
||||
==========================
|
||||
|
||||
WirePlumber has the ability to run either as a single instance daemon or as
|
||||
multiple instances, meaning that there can be multiple processes, each one
|
||||
doing a different task.
|
||||
|
||||
The most common use case for such a setup is to separate the graph orchestration
|
||||
tasks from the device monitoring and object creation ones. This can be useful
|
||||
for robustness and security reasons, as it allows restarting the device monitors
|
||||
or running them in different security contexts without affecting the rest of the
|
||||
session management functionality.
|
||||
|
||||
To achieve a multi-instance setup, WirePlumber can be started multiple times
|
||||
with a different :ref:`profile<config_components_and_profiles>` loaded in each
|
||||
instance. This can be achieved using the ``--profile`` command line option to
|
||||
select the profile to load:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ wireplumber --profile=custom
|
||||
|
||||
When no particular profile is specified, the ``main`` profile is loaded.
|
||||
|
||||
For multi-instance configuration, the default ``wireplumber.conf`` specifies 4
|
||||
profiles:
|
||||
|
||||
.. describe:: policy
|
||||
|
||||
This profile runs all the policy scripts, i.e. ones that monitor changes
|
||||
in the graph and execute actions to link nodes, select default devices,
|
||||
create new nodes or configure existing ones differently.
|
||||
|
||||
.. describe:: audio
|
||||
|
||||
The audio profile runs the ALSA and ALSA MIDI monitors, which make audio &
|
||||
MIDI devices available to PipeWire.
|
||||
|
||||
.. describe:: bluetooth
|
||||
|
||||
The bluetooth profile runs the BlueZ and BlueZ MIDI monitors, which enable
|
||||
Bluetooth audio & MIDI devices and other Bluetooth functionality tied to the
|
||||
A2DP, HSP, HFP and BAP profiles, using BlueZ.
|
||||
|
||||
.. describe:: video-capture
|
||||
|
||||
The video-capture profile runs the V4L2 and libcamera monitors, which make
|
||||
video capture devices, such as cameras and HDMI capture cards, available
|
||||
to PipeWire.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``main`` profile includes all the functionality of the ``policy``,
|
||||
``audio``, ``video-capture`` and ``bluetooth`` profiles combined (i.e. it is
|
||||
the default for a standard single instance configuration). You should never
|
||||
load the ``main`` profile alongside these other 4 profiles, as their
|
||||
functionality will conflict.
|
||||
|
||||
.. warning::
|
||||
|
||||
Always ensure that the instances you load serve a different purpose and they
|
||||
do not conflict with each other. Conflicting components executed in parallel
|
||||
will have undefined behavior.
|
||||
|
||||
Systemd integration
|
||||
-------------------
|
||||
|
||||
To make this easier to work with, a template systemd unit is provided, which is
|
||||
meant to be started with the name of the profile as a template argument:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ systemctl --user disable wireplumber # disable the "main" profile instance
|
||||
$ systemctl --user enable wireplumber@policy
|
||||
$ systemctl --user enable wireplumber@audio
|
||||
$ systemctl --user enable wireplumber@video-capture
|
||||
$ systemctl --user enable wireplumber@bluetooth
|
||||
|
||||
.. note::
|
||||
|
||||
In WirePlumber 0.4, the template argument was the name of the configuration
|
||||
file to load, since profiles did not exist. In WirePlumber 0.5, the template
|
||||
argument is the name of the profile and the configuration file is always
|
||||
``wireplumber.conf``. To change the name of the configuration file you need
|
||||
to craft custom systemd unit files and use the ``--config-file`` command line
|
||||
option as needed.
|
||||
|
|
@ -40,7 +40,7 @@ Synopsis:
|
|||
|
||||
$ meson -Dsession-managers="[ 'wireplumber' ]" build
|
||||
$ ninja -C build
|
||||
$ make run
|
||||
$ make -C build run
|
||||
|
||||
Run independently or without installing
|
||||
---------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Events and Hooks
|
||||
================
|
||||
|
||||
Session management is all about reacting to events and taking neccessary
|
||||
Session management is all about reacting to events and taking necessary
|
||||
actions. This is why WirePlumber's logic is all built on events and hooks.
|
||||
|
||||
Events
|
||||
|
|
@ -56,7 +56,7 @@ There are two main types of hooks: ``SimpleEventHook`` and ``AsyncEventHook``.
|
|||
* ``AsyncEventHook`` contains multiple functions, combined together in a state
|
||||
machine using ``WpTransition`` underneath. The hook is completed only after
|
||||
the state machine reaches its final state and this can take any amount of time
|
||||
neccessary.
|
||||
necessary.
|
||||
|
||||
Every hook also has a name, which can be an arbitrary string of characters.
|
||||
Additionally, it has two arrays of names, which declare dependencies between
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ Table of Contents
|
|||
daemon/installing.rst
|
||||
daemon/running.rst
|
||||
daemon/configuration.rst
|
||||
daemon/locations.rst
|
||||
daemon/logging.rst
|
||||
daemon/multi_instance.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
|
@ -20,6 +22,14 @@ Table of Contents
|
|||
design/understanding_wireplumber.rst
|
||||
design/events_and_hooks.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: WirePlumber's Policies
|
||||
|
||||
policies/linking.rst
|
||||
policies/smart_filters.rst
|
||||
policies/software_dsp.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: The WirePlumber Library
|
||||
|
|
@ -32,6 +42,13 @@ Table of Contents
|
|||
|
||||
scripting/lua_api.rst
|
||||
scripting/existing_scripts.rst
|
||||
scripting/custom_scripts.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Tools
|
||||
|
||||
tools/wpctl.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ C API Documentation
|
|||
c_api/link_api.rst
|
||||
c_api/device_api.rst
|
||||
c_api/client_api.rst
|
||||
c_api/permission_manager_api.rst
|
||||
c_api/metadata_api.rst
|
||||
c_api/spa_device_api.rst
|
||||
c_api/impl_node_api.rst
|
||||
|
|
@ -39,3 +40,4 @@ C API Documentation
|
|||
c_api/si_interfaces_api.rst
|
||||
c_api/si_factory_api.rst
|
||||
c_api/state_api.rst
|
||||
c_api/base_dirs_api.rst
|
||||
|
|
|
|||
6
docs/rst/library/c_api/base_dirs_api.rst
Normal file
6
docs/rst/library/c_api/base_dirs_api.rst
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.. _base_dirs_api:
|
||||
|
||||
Base Directories File Lookup
|
||||
============================
|
||||
.. doxygengroup:: wpbasedirs
|
||||
:content-only:
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
# you need to add here any files you add to the api directory as well
|
||||
sphinx_files += files(
|
||||
'base_dirs_api.rst',
|
||||
'client_api.rst',
|
||||
'component_loader_api.rst',
|
||||
'conf_api.rst',
|
||||
|
|
@ -17,6 +18,7 @@ sphinx_files += files(
|
|||
'obj_manager_api.rst',
|
||||
'object_api.rst',
|
||||
'pipewire_object_api.rst',
|
||||
'permission_manager_api.rst',
|
||||
'plugin_api.rst',
|
||||
'port_api.rst',
|
||||
'properties_api.rst',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ PipeWire Metadata
|
|||
|
||||
digraph inheritance {
|
||||
rankdir=LR;
|
||||
GBoxed -> WpMetadataItem
|
||||
GObject -> WpObject;
|
||||
WpObject -> WpProxy;
|
||||
WpProxy -> WpGlobalProxy;
|
||||
|
|
@ -14,6 +15,8 @@ PipeWire Metadata
|
|||
WpMetadata-> WpImplMetadata;
|
||||
}
|
||||
|
||||
.. doxygenstruct:: WpMetadataItem
|
||||
|
||||
.. doxygenstruct:: WpMetadata
|
||||
|
||||
.. doxygenstruct:: WpImplMetadata
|
||||
|
|
|
|||
17
docs/rst/library/c_api/permission_manager_api.rst
Normal file
17
docs/rst/library/c_api/permission_manager_api.rst
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
.. _permission_manager_api:
|
||||
|
||||
WpPermissionManager
|
||||
===================
|
||||
.. graphviz::
|
||||
:align: center
|
||||
|
||||
digraph inheritance {
|
||||
rankdir=LR;
|
||||
GObject -> WpObject;
|
||||
WpObject -> WpPermissionManager;
|
||||
}
|
||||
|
||||
.. doxygenstruct:: WpPermissionManager
|
||||
|
||||
.. doxygengroup:: wppermissionmanager
|
||||
:content-only:
|
||||
|
|
@ -7,10 +7,16 @@ Settings
|
|||
|
||||
digraph inheritance {
|
||||
rankdir=LR;
|
||||
GBoxed -> WpSettingsSpec;
|
||||
GBoxed -> WpSettingsItem;
|
||||
GObject -> WpObject;
|
||||
WpObject -> WpSettings;
|
||||
}
|
||||
|
||||
.. doxygenstruct:: WpSettingsSpec
|
||||
|
||||
.. doxygenstruct:: WpSettingsItem
|
||||
|
||||
.. doxygenstruct:: WpSettings
|
||||
|
||||
.. doxygengroup:: wpsettings
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ sphinx_files += files(
|
|||
|
||||
subdir('daemon')
|
||||
subdir('design')
|
||||
subdir('policies')
|
||||
subdir('library')
|
||||
subdir('scripting')
|
||||
subdir('tools')
|
||||
subdir('resources')
|
||||
|
|
|
|||
140
docs/rst/policies/linking.rst
Normal file
140
docs/rst/policies/linking.rst
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
.. _policies_linking:
|
||||
|
||||
Linking Policy
|
||||
==============
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
The linking policy in WirePlumber is the logic charged to link a PipeWire stream
|
||||
node with a PipeWire device node (most cases), or with another PipeWire stream
|
||||
node (monitoring applications).
|
||||
|
||||
PipeWire stream nodes always have one of the following media classes:
|
||||
|
||||
- Stream/Output/Audio: For audio playback applications (Eg pw-play).
|
||||
- Stream/Input/Audio: For audio capture applications (Eg pw-record).
|
||||
- Stream/Input/Video: For video capture applications (Eg cheese).
|
||||
|
||||
And Pipewire device nodes always have one of the following media classes:
|
||||
|
||||
- Audio/Sink: For audio playback devices (Eg Speakers).
|
||||
- Audio/Source: For audio capture devices (Eg Microphones).
|
||||
- Video/Source: For video capture devices (Eg Cameras).
|
||||
|
||||
By default, since in most cases we want to link a stream node with a device
|
||||
node, the linking policy logic when linking 2 nodes always follows the following
|
||||
assignments:
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph nodes {
|
||||
rankdir=LR;
|
||||
APS [shape=box label=<audio playback stream<BR/>(Stream/Output/Audio)>];
|
||||
APD [shape=box label=<audio playback device<BR/>(Audio/Sink)>];
|
||||
ACS [shape=box label=<audio capture stream<BR/>(Stream/Input/Audio)>];
|
||||
ACD [shape=box label=<audio capture device<BR/>(Audio/Source)>];
|
||||
VCS [shape=box label=<video capture stream<BR/>(Stream/Input/Video)>];
|
||||
VCD [shape=box label=<video capture device<BR/>(Video/Source)>];
|
||||
APS -> APD;
|
||||
ACD -> ACS;
|
||||
VCD -> VCS;
|
||||
}
|
||||
|
||||
|
||||
After that, once the media class of a device node has been selected for a
|
||||
particular stream node, and there are more than 1 device node matching such
|
||||
media class, WirePlumber will select one based on a set of priorities:
|
||||
|
||||
First, it will check if there is a default configured device node for the
|
||||
selected device media class. If there is one, and the node exists, it will link
|
||||
the stream node with such configured default node. Users can easily configure
|
||||
default device nodes for all the 3 different device media classes using tools
|
||||
such as ``pavucontrol`` or ``wpctl``. The logic is implemented in the
|
||||
``linking/find-default-target.lua`` Lua script.
|
||||
|
||||
If there isn't any default node configured, or there is a default node
|
||||
configured but the node does not exist, WirePlumber will instead select the
|
||||
best device node available. The best device node is the node with highest
|
||||
session priority and available routes to the physical device. The logic is
|
||||
implemented in the ``linking/find-best-target.lua`` Lua script.
|
||||
|
||||
If the best node could not be found because the system does not have any,
|
||||
WirePlumber won't link the stream and will send a "no target node available"
|
||||
error to the client.
|
||||
|
||||
|
||||
Stream node linking properties
|
||||
------------------------------
|
||||
|
||||
The above default linking logic behavior can be changed by setting specific
|
||||
properties on the nodes.
|
||||
|
||||
.. note::
|
||||
|
||||
These properties must be set in the **stream** nodes (not the device nodes),
|
||||
otherwise they won't have any effect.
|
||||
|
||||
- **target.object**:
|
||||
|
||||
The name of the desired node for this stream to be linked with.
|
||||
If this property is present, WirePlumber will try to find such node, see if it
|
||||
can be linked with the stream, and if so, will use it instead of the default
|
||||
node or best node. The logic is implemented in the ``linking/find-defined-target.lua``
|
||||
Lua script. Since this property is not set by default, WirePlumber will always
|
||||
link stream nodes to the default or best device node found. This property can be
|
||||
easily set using tools such as ``pw-play`` with the ``--target`` flag.
|
||||
|
||||
Note that any node name can be specified there, even if the name is not a device
|
||||
node name, but another stream node name. If this is the case, WirePlumber will
|
||||
link 2 stream nodes together. An example of this case is the monitoring nodes
|
||||
created by ``pavucontrol`` to monitor audio of all audio devices and streams.
|
||||
|
||||
- **node.dont-reconnect**:
|
||||
|
||||
Boolean indicating whether the stream node should not be reconnected to a new
|
||||
node if its current linked node (target) was destroyed or not. By default it
|
||||
is set to ``false``, so if the property is not present in the stream node, WirePlumber
|
||||
will always try to reconnect the stream node to a new target instead of sending
|
||||
an error to the client. The logic is implemented in the ``linking/prepare-link.lua``
|
||||
Lua script.
|
||||
|
||||
- **node.dont-move**:
|
||||
|
||||
Boolean indicating whether the stream node should not be movable or not at runtime
|
||||
using the metadata. If a stream node is not movable, it means that users cannot
|
||||
relink the stream node to a new target at runtime (using tools such as ``pavucontrol``
|
||||
or ``pw-metadata``) when the stream node is already linked to a different node. By
|
||||
default it is set to ``false``, so if the property is not present, WirePlumber will
|
||||
always move, and therefore link the stream node to a new target if it is defined and
|
||||
updated in the ``target.object`` metadata key.
|
||||
|
||||
- **node.dont-fallback**:
|
||||
|
||||
Boolean indicating whether the stream node should not fallback to a different
|
||||
target if its defined target does not exist (the one defined with the ``target.object``
|
||||
property) or not. Therefore, if this property is set to ``true``, WirePlumber sends
|
||||
a "defined target not found" error to the client and will also destroy the stream
|
||||
node. By default it is set to ``false``, so if the property is not present in the
|
||||
stream node, WirePlumber will always fallback to the default or best target if
|
||||
the defined target was not found.
|
||||
|
||||
- **node.linger**:
|
||||
|
||||
Boolean indicating whether the stream node should linger or not if its defined
|
||||
target was not found and the ``node.dont-fallback`` is set to true. Therefore, if
|
||||
this property is set to ``true``, the defined target was not found, and the
|
||||
``node.dont-fallback`` is set to true, WirePlumber won't send a "defined target not found"
|
||||
error to the client, and won't destroy the stream node. This is useful if we want
|
||||
the stream to wait (without processing any data) until its defined target becomes
|
||||
available. By default it is set to ``false``, so if the property is not present in the
|
||||
stream node, WirePlumber will always destroy the node and send an error to the client
|
||||
if its target was not found and ``node.dont-fallback`` was set to true.
|
||||
|
||||
|
||||
Linking settings
|
||||
----------------
|
||||
|
||||
Apart from the above properties, there are also global settings for the linking
|
||||
policy. See :ref:`config_settings` for more information, the linking settings
|
||||
are prefixed with ``linking.``.
|
||||
6
docs/rst/policies/meson.build
Normal file
6
docs/rst/policies/meson.build
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# you need to add here any files you add to the toc directory as well
|
||||
sphinx_files += files(
|
||||
'linking.rst',
|
||||
'smart_filters.rst',
|
||||
'software_dsp.rst',
|
||||
)
|
||||
380
docs/rst/policies/smart_filters.rst
Normal file
380
docs/rst/policies/smart_filters.rst
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
.. _policies_smart_filters:
|
||||
|
||||
Smart Filters
|
||||
=============
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
The smart filters policy allows automatically linking filters together, in a
|
||||
chain, and tied to a specific target node. This is useful when we want to apply
|
||||
a specific processing chain to a specific device, for example. When a stream is
|
||||
about to be linked to a target node that is associated with a smart filter
|
||||
chain, the policy will automatically link the stream with the first filter in
|
||||
the chain, and the last filter in the chain with the target node. This is done
|
||||
transparently to the client, allowing users to define a specific processing
|
||||
chain for a specific device without having to create setups with virtual sinks
|
||||
(or sources) that must be explicitly targeted by the clients.
|
||||
|
||||
Filters, in general, are nodes that are placed in the middle of the graph and
|
||||
are used to modify the data that passes through them. For example, the
|
||||
*echo-cancel*, the *filter-chain*, or the *loopback* nodes are filters.
|
||||
Filters can be implemented either as a single node or as a pair of nodes with
|
||||
opposite directions. For example, the *null-audio-sink* node can be configured
|
||||
to be a single-node filter. On the other hand, the *filter-chain* is a pair of
|
||||
nodes with opposite directions, where one node captures the audio from the graph
|
||||
and the other node sends the modified audio back to the graph.
|
||||
|
||||
For the purpose of the **smart filters** policy, WirePlumber will only consider
|
||||
pairs of nodes as filters, not single-node ones. More specifically, a pair of
|
||||
nodes will be considered to be a filter by WirePlumber if they have the
|
||||
``node.link-group`` property set to a common value. This property is always set
|
||||
on pairs of nodes that are internally linked together and is a good indicator
|
||||
that the nodes are implementing a filter.
|
||||
|
||||
That pair of nodes **must** always consist of a *stream* node and a *main* node.
|
||||
The main node acts as a virtual device, where the data is sent or captured
|
||||
to/from, and the stream node acts as a regular stream, where the data is sent
|
||||
or received to/from the next node in the graph. This is designated by their
|
||||
media class, as shown in the table below:
|
||||
|
||||
.. list-table::
|
||||
:widths: 30 35 35
|
||||
:header-rows: 1
|
||||
:stub-columns: 1
|
||||
|
||||
* -
|
||||
- Input filter (virtual sink)
|
||||
- Output filter (virtual source)
|
||||
* - Main node
|
||||
- ``Audio/Sink`` (capture)
|
||||
- ``Audio/Source`` (playback)
|
||||
* - Stream node
|
||||
- ``Stream/Output/Audio`` (playback)
|
||||
- ``Stream/Input/Audio`` (capture)
|
||||
|
||||
For instance, if a smart filter is used between an application playback stream
|
||||
and the default audio sink, the graph would look like this:
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph nodes {
|
||||
rankdir=LR;
|
||||
A [shape=box label=<application stream node<BR/>(Stream/Output/Audio)>];
|
||||
FM [shape=box label=<filter main node<BR/>(Audio/Sink)>];
|
||||
FS [shape=box label=<filter stream node<BR/>(Stream/Output/Audio)>];
|
||||
D [shape=box label=<default device node<BR/>(Audio/Sink)>];
|
||||
A -> FM;
|
||||
FS -> D;
|
||||
subgraph cluster_filter {
|
||||
style="dotted";
|
||||
FM; FS;
|
||||
}
|
||||
}
|
||||
|
||||
The same logic is applied if the smart filter is used between an application
|
||||
capture stream and the default audio source, it is just all in the opposite
|
||||
direction. This is how the graph would look like in this case:
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph nodes {
|
||||
rankdir=LR;
|
||||
A [shape=box label=<application stream node<BR/>(Stream/Input/Audio)>];
|
||||
FM [shape=box label=<filter main node<BR/>(Audio/Source)>];
|
||||
FS [shape=box label=<filter stream node<BR/>(Stream/Input/Audio)>];
|
||||
D [shape=box label=<default device node<BR/>(Audio/Source)>];
|
||||
D -> FS;
|
||||
FM -> A;
|
||||
subgraph cluster_filter {
|
||||
style="dotted";
|
||||
FM; FS;
|
||||
}
|
||||
}
|
||||
|
||||
When multiple filters have the same direction, they can also be chained together
|
||||
so that the output of one filter is sent to the input of the next filter. The
|
||||
next section describes how these chains can be described with properties so that
|
||||
they are automatically linked by WirePlumber in any way we want.
|
||||
|
||||
Filter properties
|
||||
-----------------
|
||||
|
||||
When a filter node is created, WirePlumber will check for the presence of the
|
||||
following optional node properties on the **main** node:
|
||||
|
||||
- **filter.smart**
|
||||
|
||||
Boolean indicating whether smart policy will be used for these filter nodes or
|
||||
not. This is disabled by default, therefore filter nodes will be treated as
|
||||
regular nodes, without applying any kind of extra logic. On the other hand, if
|
||||
this property is set to ``true``, automatic (smart) filter policy will be used
|
||||
when linking them. The properties below will then also apply, providing
|
||||
further instructions.
|
||||
|
||||
- **filter.smart.name**
|
||||
|
||||
The unique name of the filter. WirePlumber will use the value of the
|
||||
``node.link-group`` property as the filter name if this property is not set.
|
||||
|
||||
- **filter.smart.disabled**
|
||||
|
||||
Boolean indicating whether the filter should be disabled or not. A disabled
|
||||
filter will never be used under any circumstances. If the property is not set,
|
||||
WirePlumber will consider the filter as enabled (i.e. disabled = false).
|
||||
|
||||
- **filter.smart.targetable**
|
||||
|
||||
Boolean indicating whether the filter can be directly linked with clients that
|
||||
have it defined as a target (Eg: ``pw-play --target <filter-name>``) or not.
|
||||
This can be useful when a client wants to be linked with a filter that is in
|
||||
the middle of the chain in order to bypass the filters that are placed before
|
||||
the selected one. If the property is not set, WirePlumber will consider the
|
||||
filter not targetable by default, meaning filters will never by bypassed by
|
||||
clients, and clients will always be linked with the first filter in the chain.
|
||||
|
||||
- **filter.smart.target**
|
||||
|
||||
A JSON object that defines the matching properties of the filter's target
|
||||
node. A filter target can never be another filter node (WirePlumber will
|
||||
ignore it), it must be a device or virtual sink (or source, depending on the
|
||||
direction of the filter). If this property is not set, WirePlumber will use
|
||||
the default sink/source as the target.
|
||||
|
||||
- **filter.smart.before**
|
||||
|
||||
A JSON array containing the names of the filters that are supposed to be
|
||||
chained after this filter (i.e. this filter here should be chained *before*
|
||||
those). If not set, WirePlumber will link the filters by order of creation.
|
||||
|
||||
- **filter.smart.after**
|
||||
|
||||
A JSON array containing the names of the filters that are supposed to be
|
||||
chained before this filter (i.e. this filter here should be chained *after*
|
||||
those). If not set, WirePlumber will link the filters by order of creation.
|
||||
|
||||
.. note::
|
||||
|
||||
These properties must be set on the filter's **main** node, not the stream
|
||||
node.
|
||||
|
||||
As an example, we will describe here how to create 2 loopback filters in
|
||||
PipeWire's configuration, with names loopback-1 and loopback-2, that will be
|
||||
linked with the default audio device, and use loopback-2 filter as the last
|
||||
filter in the chain.
|
||||
|
||||
The PipeWire configuration files for the 2 filters should be like this:
|
||||
|
||||
- ~/.config/pipewire/pipewire.conf.d/loopback-1.conf:
|
||||
|
||||
.. code-block::
|
||||
:emphasize-lines: 8-11
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-loopback
|
||||
args = {
|
||||
node.name = loopback-1-sink
|
||||
node.description = "Loopback 1 Sink"
|
||||
capture.props = {
|
||||
audio.position = [ FL FR ]
|
||||
media.class = Audio/Sink
|
||||
filter.smart = true
|
||||
filter.smart.name = loopback-1
|
||||
filter.smart.before = [ loopback-2 ]
|
||||
}
|
||||
playback.props = {
|
||||
audio.position = [ FL FR ]
|
||||
node.passive = true
|
||||
stream.dont-remix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
- ~/.config/pipewire/pipewire.conf.d/loopback-2.conf:
|
||||
|
||||
.. code-block::
|
||||
:emphasize-lines: 8-10
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-loopback
|
||||
args = {
|
||||
node.name = loopback-2-sink
|
||||
node.description = "Loopback 2 Sink"
|
||||
capture.props = {
|
||||
audio.position = [ FL FR ]
|
||||
media.class = Audio/Sink
|
||||
filter.smart = true
|
||||
filter.smart.name = loopback-2
|
||||
}
|
||||
playback.props = {
|
||||
audio.position = [ FL FR ]
|
||||
node.passive = true
|
||||
stream.dont-remix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
After restarting PipeWire to apply the configuration changes, playing a test
|
||||
wave audio file with paplay to the default device should result in the following
|
||||
graph:
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph nodes {
|
||||
rankdir=LR;
|
||||
paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
|
||||
L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
|
||||
L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
|
||||
L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
|
||||
L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
|
||||
device [shape=box label=<default device node<BR/>(Audio/Sink)>];
|
||||
paplay -> L1M;
|
||||
L1S -> L2M;
|
||||
L2S -> device;
|
||||
subgraph cluster_filter1 {
|
||||
style="dotted";
|
||||
L1M; L1S;
|
||||
}
|
||||
subgraph cluster_filter2 {
|
||||
style="dotted";
|
||||
L2M; L2S;
|
||||
}
|
||||
}
|
||||
|
||||
Now, if we remove the ``filter.smart.before = [ loopback-2 ]`` property from the
|
||||
loopback-1 filter, and add a ``filter.smart.before = [ loopback-1 ]`` property
|
||||
in the loopback-2 filter configuration file, WirePlumber should link the
|
||||
loopback-1 filter as the last filter in the chain, like this:
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph nodes {
|
||||
rankdir=LR;
|
||||
paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
|
||||
L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
|
||||
L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
|
||||
L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
|
||||
L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
|
||||
device [shape=box label=<default device node<BR/>(Audio/Sink)>];
|
||||
paplay -> L2M;
|
||||
L2S -> L1M;
|
||||
L1S -> device;
|
||||
subgraph cluster_filter1 {
|
||||
style="dotted";
|
||||
L1M; L1S;
|
||||
}
|
||||
subgraph cluster_filter2 {
|
||||
style="dotted";
|
||||
L2M; L2S;
|
||||
}
|
||||
}
|
||||
|
||||
In addition, the filters can have different targets. For example, we can define
|
||||
the filters like this:
|
||||
|
||||
- ~/.config/pipewire/pipewire.conf.d/loopback-1.conf:
|
||||
|
||||
.. code-block::
|
||||
:emphasize-lines: 12
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-loopback
|
||||
args = {
|
||||
node.name = loopback-1-sink
|
||||
node.description = "Loopback 1 Sink"
|
||||
capture.props = {
|
||||
audio.position = [ FL FR ]
|
||||
media.class = Audio/Sink
|
||||
filter.smart = true
|
||||
filter.smart.name = loopback-1
|
||||
filter.smart.after = [ loopback-2 ]
|
||||
filter.smart.target = { node.name = "not-default-audio-device" }
|
||||
}
|
||||
playback.props = {
|
||||
audio.position = [ FL FR ]
|
||||
node.passive = true
|
||||
stream.dont-remix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
- ~/.config/pipewire/pipewire.conf.d/loopback-2.conf:
|
||||
|
||||
.. code-block::
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-loopback
|
||||
args = {
|
||||
node.name = loopback-2-sink
|
||||
node.description = "Loopback 2 Sink"
|
||||
capture.props = {
|
||||
audio.position = [ FL FR ]
|
||||
media.class = Audio/Sink
|
||||
filter.smart = true
|
||||
filter.smart.name = loopback-2
|
||||
}
|
||||
playback.props = {
|
||||
audio.position = [ FL FR ]
|
||||
node.passive = true
|
||||
stream.dont-remix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
In this case, playing a test wave audio file with paplay to the
|
||||
``not-default-audio-device`` device should result in the following graph:
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph nodes {
|
||||
rankdir=LR;
|
||||
paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
|
||||
L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
|
||||
L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
|
||||
L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
|
||||
L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
|
||||
device [shape=box label=<not-default-audio-device node<BR/>(Audio/Sink)>];
|
||||
paplay -> L2M;
|
||||
L2S -> L1M;
|
||||
L1S -> device;
|
||||
subgraph cluster_filter1 {
|
||||
style="dotted";
|
||||
L1M; L1S;
|
||||
}
|
||||
subgraph cluster_filter2 {
|
||||
style="dotted";
|
||||
L2M; L2S;
|
||||
}
|
||||
}
|
||||
|
||||
In this configuration, the loopback-1 filter will only be linked if the
|
||||
application stream is targeting the device node called
|
||||
"not-default-audio-device".
|
||||
|
||||
Filters metadata
|
||||
----------------
|
||||
|
||||
Similar to the default metadata, it is also possible to override the filter
|
||||
properties using the "filters" metadata object. This allow users to change the
|
||||
filters policy at runtime.
|
||||
|
||||
For example, assuming the id of the *loopback-1* main node is ``40``, we can
|
||||
disable the filter by setting its ``filter.smart.disabled`` metadata key to
|
||||
``true`` using the ``pw-metadata`` tool like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pw-metadata -n filters 40 "filter.smart.disabled" true Spa:String:JSON
|
||||
|
||||
We can also change the target of a filter at runtime:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pw-metadata -n filters 40 "filter.smart.target" "{ node.name = new-target-node-name }" Spa:String:JSON
|
||||
|
||||
Every time a key in the filters metadata changes, all filters are unlinked and
|
||||
re-linked properly, following the new policy.
|
||||
106
docs/rst/policies/software_dsp.rst
Normal file
106
docs/rst/policies/software_dsp.rst
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
.. _policies_software_dsp:
|
||||
|
||||
Automatic Software DSP
|
||||
======================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
WirePlumber provides a mechanism for transparently handling oddball and embedded
|
||||
devices that require software DSP to be done in userspace. Devices such as smartphones,
|
||||
TVs, portable speakers, and even some laptops implement an audio subsystem designed
|
||||
under the assumption that the hardware sink/source will be "backed" by some sort
|
||||
of transparent DSP mechanism. That is, the hardware device itself should not be
|
||||
directly accessed, and expects to be sent preprocessed/pre-routed samples. Often,
|
||||
especially with Android handsets, these samples are preprocessed or pre-routed
|
||||
by the vendor's proprietary userspace.
|
||||
|
||||
WirePlumber's automatic software DSP mechanism aims to replicate this functionality in
|
||||
a standardised and configurable way. The target device sink/source is hidden from
|
||||
other PipeWire clients, and a virtual node is linked to it. This virtual
|
||||
node is then presented to clients as *the* node, allowing implementers to specify
|
||||
any custom processing or routing in a way that is transparent to users, the kernel,
|
||||
and the hardware.
|
||||
|
||||
|
||||
Activating
|
||||
----------
|
||||
|
||||
In addition to the ``node.software-dsp.rules`` section, the ``node.software-dsp``
|
||||
:ref:`feature <config_features>` must be enabled in the desired profile(s).
|
||||
|
||||
|
||||
Matching a node
|
||||
---------------
|
||||
|
||||
Matching rules are specified in ``node.software-dsp.rules``. The ``create-filter``
|
||||
action specifies behaviour at node insertion. All node properties can be matched
|
||||
on, including any type-specific properties such as ``alsa.id``.
|
||||
|
||||
|
||||
Configurable properties
|
||||
-----------------------
|
||||
|
||||
.. describe:: filter-graph
|
||||
|
||||
SPA-JSON object describing the software DSP node. This is passed as-is as
|
||||
an argument to ``libpipewire-module-filter-chain``. See the
|
||||
`filter-chain documentation <https://docs.pipewire.org/page_module_filter_chain.html>`_
|
||||
for details on what options can be set in this object.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``target.object`` property of the virtual node should be configured
|
||||
statically to point to the node matched by the rule.
|
||||
|
||||
.. describe:: filter-path
|
||||
|
||||
Absolute path to a file on disk storing a SPA-JSON object as plain text. This will be
|
||||
parsed by WirePlumber into a WpConf object with a single section called
|
||||
``node.software-dsp.graph``, then passed as-is into ``libpipewire-module-filter-chain``.
|
||||
|
||||
.. note::
|
||||
|
||||
``filter-graph`` and ``filter-path`` are mutually exclusive, with the former taking
|
||||
precedence if both are present in the matched rule.
|
||||
|
||||
.. describe:: hide-parent
|
||||
|
||||
Boolean indicating whether or not the matched node should be hidden from
|
||||
clients. ``node/software-dsp.lua`` will set the permissions for all clients other
|
||||
than WirePlumber itself to ``'-'``. This prevents use of the node by any
|
||||
userspace software except for WirePlumber itself.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block::
|
||||
:caption: wireplumber.conf.d/99-my-dsp.conf
|
||||
|
||||
node.software-dsp.rules = [
|
||||
{
|
||||
matches = [
|
||||
{ "node.name" = "alsa_output.platform-sound.HiFi__Speaker__sink" }
|
||||
{ "alsa.id" = "~WeirdHardware*" } # Wildcard match
|
||||
]
|
||||
|
||||
actions = {
|
||||
create-filter = {
|
||||
filter-graph = {} # Virtual node goes here
|
||||
filter-path = "/path/to/spa.json"
|
||||
hide-parent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
wireplumber.profiles = {
|
||||
main = {
|
||||
node.software-dsp = required
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
This will match any sinks with the UCM HiFi Speaker profile set or cards
|
||||
containing the string "WeirdHardware" at the start of their name.
|
||||
41
docs/rst/scripting/custom_scripts.rst
Normal file
41
docs/rst/scripting/custom_scripts.rst
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
.. _scripting_custom_scripts:
|
||||
|
||||
Custom Scripts
|
||||
==============
|
||||
|
||||
The locations where WirePlumber searches for scripts is explained in
|
||||
:ref:`config_locations_scripts`.
|
||||
|
||||
Scripts are not loaded automatically; a component muse be defined for them, and
|
||||
this component must be included in a profile. See
|
||||
:ref:`config_components_and_profiles`.
|
||||
|
||||
Full example
|
||||
------------
|
||||
|
||||
Let's assume that ``~/.local/share/wireplumber/scripts/90-hello-world.lua``
|
||||
contains the following script:
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
log = Log.open_topic("hello-world")
|
||||
log.info("Hello world")
|
||||
|
||||
In order for it to run, we'll define a component and include it in the default
|
||||
profile by including the following configuration (for example, in
|
||||
``~/.config/wireplumber/wireplumber.conf.d/90-hello-world.conf``):
|
||||
|
||||
.. code-block::
|
||||
|
||||
wireplumber.components = [
|
||||
{
|
||||
name = "90-hello-world.lua", type = script/lua
|
||||
provides = hello-world
|
||||
}
|
||||
]
|
||||
|
||||
wireplumber.profiles = {
|
||||
main = {
|
||||
hello-world = required
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,33 @@
|
|||
Debug Logging
|
||||
=============
|
||||
|
||||
Constructors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. function:: Log.open_topic(topic)
|
||||
|
||||
Opens a LogTopic with the given topic name. Well known script topics are
|
||||
described in :ref:`daemon_logging`, and messages from scripts shall use
|
||||
**s-***.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
local obj
|
||||
log = Log.open_topic ("s-linking")
|
||||
log:info (obj, "an info message on obj")
|
||||
log:debug ("a debug message")
|
||||
|
||||
Above example shows how to output debug logs.
|
||||
|
||||
:param string topic: The log topic to open
|
||||
:returns: the log topic object
|
||||
:rtype: Log (:c:struct:`WpLogTopic`)
|
||||
|
||||
Methods
|
||||
~~~~~~~
|
||||
|
||||
.. function:: Log.warning(object, message)
|
||||
|
||||
Logs a warning message, like :c:macro:`wp_warning_object`
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
sphinx_files += files(
|
||||
'lua_api.rst',
|
||||
'existing_scripts.rst',
|
||||
'custom_scripts.rst',
|
||||
)
|
||||
|
||||
subdir('lua_api')
|
||||
|
|
|
|||
3
docs/rst/tools/meson.build
Normal file
3
docs/rst/tools/meson.build
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
sphinx_files += files(
|
||||
'wpctl.rst',
|
||||
)
|
||||
309
docs/rst/tools/wpctl.rst
Normal file
309
docs/rst/tools/wpctl.rst
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
wpctl(1)
|
||||
========
|
||||
|
||||
SYNOPSIS
|
||||
--------
|
||||
|
||||
**wpctl** [*COMMAND*] [*COMMAND_OPTIONS*]
|
||||
|
||||
DESCRIPTION
|
||||
-----------
|
||||
|
||||
**wpctl** is a command-line control tool for WirePlumber, the PipeWire session
|
||||
manager. It provides an interface to inspect, control, and configure audio and
|
||||
video devices, nodes, and their properties within a PipeWire media server.
|
||||
|
||||
WirePlumber manages audio and video routing, device configuration, and session
|
||||
policies. **wpctl** allows users to interact with these components, change
|
||||
volume levels, set default devices, inspect object properties, and modify
|
||||
settings.
|
||||
|
||||
COMMANDS
|
||||
--------
|
||||
|
||||
status
|
||||
^^^^^^
|
||||
|
||||
**wpctl status** [**-k**\|\ **--nick**] [**-n**\|\ **--name**]
|
||||
|
||||
Displays the current state of objects in PipeWire, including devices, sinks,
|
||||
sources, filters, and streams. Shows a hierarchical view of the audio/video
|
||||
system.
|
||||
|
||||
Options:
|
||||
**-k**, **--nick**
|
||||
Display device and node nicknames instead of descriptions
|
||||
**-n**, **--name**
|
||||
Display device and node names instead of descriptions
|
||||
|
||||
get-volume
|
||||
^^^^^^^^^^
|
||||
|
||||
**wpctl get-volume** *ID*
|
||||
|
||||
Displays volume information about the specified node, including current volume
|
||||
level and mute state.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Node ID or special identifier (see `SPECIAL IDENTIFIERS`_)
|
||||
|
||||
inspect
|
||||
^^^^^^^
|
||||
|
||||
**wpctl inspect** *ID* [**-r**\|\ **--referenced**] [**-a**\|\ **--associated**]
|
||||
|
||||
Displays detailed information about the specified object, including all
|
||||
properties and metadata.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Object ID or special identifier
|
||||
|
||||
Options:
|
||||
**-r**, **--referenced**
|
||||
Show objects that are referenced in properties
|
||||
**-a**, **--associated**
|
||||
Show associated objects
|
||||
|
||||
set-default
|
||||
^^^^^^^^^^^
|
||||
|
||||
**wpctl set-default** *ID*
|
||||
|
||||
Sets the specified device node to be the default target of its kind (capture or
|
||||
playback) for new streams that require auto-connection.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Sink or source node ID
|
||||
|
||||
set-volume
|
||||
^^^^^^^^^^
|
||||
|
||||
**wpctl set-volume** *ID* *VOL*\ [**%**]\ [**-**\|\ **+**] [**-p**\|\ **--pid**] [**-l** *LIMIT*\|\ **--limit** *LIMIT*]
|
||||
|
||||
Sets the volume of the specified node.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Node ID, special identifier, or PID (with --pid)
|
||||
*VOL*\ [**%**]\ [**-**\|\ **+**]
|
||||
Volume specification:
|
||||
|
||||
- *VOL* - Set volume to specific value (1.0 = 100%)
|
||||
- *VOL*\ **%** - Set volume to percentage (50% = 0.5)
|
||||
- *VOL*\ **+** - Increase volume by value
|
||||
- *VOL*\ **-** - Decrease volume by value
|
||||
- *VOL*\ **%+** - Increase volume by percentage
|
||||
- *VOL*\ **%-** - Decrease volume by percentage
|
||||
|
||||
Options:
|
||||
**-p**, **--pid**
|
||||
Treat ID as a process ID and affect all nodes associated with it
|
||||
**-l** *LIMIT*, **--limit** *LIMIT*
|
||||
Limit final volume to below this value (floating point, 1.0 = 100%)
|
||||
|
||||
Examples:
|
||||
Set volume to 50%: ``wpctl set-volume @DEFAULT_SINK@ 0.5``
|
||||
|
||||
Increase volume by 10%: ``wpctl set-volume 42 10%+``
|
||||
|
||||
Set volume for all nodes of PID 1234: ``wpctl set-volume --pid 1234 0.8``
|
||||
|
||||
set-mute
|
||||
^^^^^^^^
|
||||
|
||||
**wpctl set-mute** *ID* **1**\|\ **0**\|\ **toggle** [**-p**\|\ **--pid**]
|
||||
|
||||
Changes the mute state of the specified node.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Node ID, special identifier, or PID (with --pid)
|
||||
**1**\|\ **0**\|\ **toggle**
|
||||
Mute state: 1 (mute), 0 (unmute), or toggle current state
|
||||
|
||||
Options:
|
||||
**-p**, **--pid**
|
||||
Treat ID as a process ID and affect all nodes associated with it
|
||||
|
||||
set-profile
|
||||
^^^^^^^^^^^
|
||||
|
||||
**wpctl set-profile** *ID* *INDEX*
|
||||
|
||||
Sets the profile of the specified device to the given index.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Device ID or special identifier
|
||||
*INDEX*
|
||||
Profile index (integer, 0 typically means 'off')
|
||||
|
||||
set-route
|
||||
^^^^^^^^^
|
||||
|
||||
**wpctl set-route** *ID* *INDEX*
|
||||
|
||||
Sets the route of the specified device to the given index.
|
||||
|
||||
Arguments:
|
||||
*ID*
|
||||
Device node ID or special identifier
|
||||
*INDEX*
|
||||
Route index (integer, 0 typically means 'off')
|
||||
|
||||
clear-default
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
**wpctl clear-default** [*ID*]
|
||||
|
||||
Clears the default configured node. If no ID is specified, clears all default
|
||||
nodes.
|
||||
|
||||
Arguments:
|
||||
*ID* (optional)
|
||||
Settings ID to clear (0-2 for Audio/Sink, Audio/Source, Video/Source).
|
||||
If omitted, clears all defaults.
|
||||
|
||||
settings
|
||||
^^^^^^^^
|
||||
|
||||
**wpctl settings** [*KEY*] [*VAL*] [**-d**\|\ **--delete**] [**-s**\|\ **--save**] [**-r**\|\ **--reset**]
|
||||
|
||||
Shows, changes, or removes WirePlumber settings.
|
||||
|
||||
Arguments:
|
||||
*KEY* (optional)
|
||||
Setting key name
|
||||
*VAL* (optional)
|
||||
Setting value (JSON format)
|
||||
|
||||
Options:
|
||||
**-d**, **--delete**
|
||||
Delete the saved setting value (no KEY means delete all)
|
||||
**-s**, **--save**
|
||||
Save the setting value (no KEY means save all, no VAL means current value)
|
||||
**-r**, **--reset**
|
||||
Reset the setting to its default value
|
||||
|
||||
Behavior:
|
||||
- No arguments: Show all settings
|
||||
- KEY only: Show specific setting value
|
||||
- KEY and VAL: Set specific setting value
|
||||
|
||||
set-log-level
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
**wpctl set-log-level** [*ID*] *LEVEL*
|
||||
|
||||
Sets the log level of a client.
|
||||
|
||||
Arguments:
|
||||
*ID* (optional)
|
||||
Client ID. If omitted, applies to WirePlumber. Use 0 for PipeWire server.
|
||||
*LEVEL*
|
||||
Log level (e.g., ``0``, ``1``, ``2``, ``3``, ``4``, ``5``, ``E``, ``W``, ``N``, ``I``, ``D``, ``T``).
|
||||
Use ``-`` to unset the log level.
|
||||
|
||||
SPECIAL IDENTIFIERS
|
||||
-------------------
|
||||
|
||||
Instead of numeric IDs, **wpctl** accepts these special identifiers for
|
||||
commonly used defaults:
|
||||
|
||||
**@DEFAULT_SINK@**, **@DEFAULT_AUDIO_SINK@**
|
||||
The current default audio sink (playback device)
|
||||
|
||||
**@DEFAULT_SOURCE@**, **@DEFAULT_AUDIO_SOURCE@**
|
||||
The current default audio source (capture device)
|
||||
|
||||
**@DEFAULT_VIDEO_SOURCE@**
|
||||
The current default video source (camera)
|
||||
|
||||
These identifiers are resolved at runtime to the appropriate node IDs.
|
||||
|
||||
EXIT STATUS
|
||||
-----------
|
||||
|
||||
**wpctl** returns the following exit codes:
|
||||
|
||||
0
|
||||
Success
|
||||
1
|
||||
General error (e.g., invalid arguments, connection failure)
|
||||
2
|
||||
Could not connect to PipeWire
|
||||
3
|
||||
Command-specific error (e.g., object not found)
|
||||
|
||||
EXAMPLES
|
||||
--------
|
||||
|
||||
Display system status::
|
||||
|
||||
wpctl status
|
||||
|
||||
Set default audio sink::
|
||||
|
||||
wpctl set-default 42
|
||||
|
||||
Set volume to 75% on default sink::
|
||||
|
||||
wpctl set-volume @DEFAULT_SINK@ 75%
|
||||
|
||||
Increase volume by 5% on a specific node::
|
||||
|
||||
wpctl set-volume 42 5%+
|
||||
|
||||
Mute the default source::
|
||||
|
||||
wpctl set-mute @DEFAULT_SOURCE@ 1
|
||||
|
||||
Toggle mute on default sink::
|
||||
|
||||
wpctl set-mute @DEFAULT_SINK@ toggle
|
||||
|
||||
Inspect a device with associated objects::
|
||||
|
||||
wpctl inspect --associated 30
|
||||
|
||||
Show all WirePlumber settings::
|
||||
|
||||
wpctl settings
|
||||
|
||||
Set a specific setting::
|
||||
|
||||
wpctl settings bluetooth.autoswitch true
|
||||
|
||||
Save all current settings::
|
||||
|
||||
wpctl settings --save
|
||||
|
||||
Set log level for WirePlumber to debug::
|
||||
|
||||
wpctl set-log-level D
|
||||
|
||||
Set log level for a specific client::
|
||||
|
||||
wpctl set-log-level 42 W
|
||||
|
||||
NOTES
|
||||
-----
|
||||
|
||||
Object IDs can be found using the **status** command. The hierarchical display
|
||||
shows IDs for devices, nodes, and other objects.
|
||||
|
||||
Volume values are floating-point numbers where 1.0 represents 100% volume.
|
||||
Values can exceed 1.0 to introduce volume amplification.
|
||||
|
||||
When using the **--pid** option, **wpctl** will find all audio nodes associated
|
||||
with the specified process ID and apply the operation to all of them.
|
||||
|
||||
SEE ALSO
|
||||
--------
|
||||
|
||||
**pipewire**\ (1), **pw-cli**\ (1), **pw-dump**\ (1), **wireplumber**\ (1)
|
||||
|
||||
WirePlumber Documentation: https://pipewire.pages.freedesktop.org/wireplumber/
|
||||
368
lib/wp/base-dirs.c
Normal file
368
lib/wp/base-dirs.c
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2020 Collabora Ltd.
|
||||
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "base-dirs.h"
|
||||
#include "log.h"
|
||||
#include "wpversion.h"
|
||||
#include "wpbuildbasedirs.h"
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-base-dirs")
|
||||
|
||||
/*!
|
||||
* \defgroup wpbasedirs Base Directories File Lookup
|
||||
*/
|
||||
|
||||
/* Returns /basedir/subdir/filename, with filename treated as a module
|
||||
* if WP_BASE_DIRS_FLAG_MODULE is set.
|
||||
* The basedir is assumed to be either an absolute path or NULL.
|
||||
* The subdir is assumed to be a path relative to basedir or NULL.
|
||||
*/
|
||||
static gchar *
|
||||
make_path (guint flags, const gchar *basedir, const gchar *subdir,
|
||||
const gchar *filename)
|
||||
{
|
||||
g_autofree gchar *full_basedir = NULL;
|
||||
g_autofree gchar *full_filename = NULL;
|
||||
|
||||
/* merge subdir into basedir, if necessary */
|
||||
if (subdir) {
|
||||
full_basedir = g_canonicalize_filename (subdir, basedir);
|
||||
basedir = full_basedir;
|
||||
}
|
||||
|
||||
if (flags & WP_BASE_DIRS_FLAG_MODULE) {
|
||||
g_autofree gchar *basename = g_path_get_basename (filename);
|
||||
g_autofree gchar *dirname = g_path_get_dirname (filename);
|
||||
const gchar *prefix = "";
|
||||
const gchar *suffix = "";
|
||||
if (!g_str_has_prefix (basename, "lib"))
|
||||
prefix = "lib";
|
||||
if (!g_str_has_suffix (basename, ".so"))
|
||||
suffix = ".so";
|
||||
full_filename = g_strconcat (dirname, G_DIR_SEPARATOR_S,
|
||||
prefix, basename, suffix, NULL);
|
||||
filename = full_filename;
|
||||
}
|
||||
|
||||
return g_canonicalize_filename (filename, basedir);
|
||||
}
|
||||
|
||||
static GPtrArray *
|
||||
lookup_dirs (guint flags, gboolean is_absolute)
|
||||
{
|
||||
g_autoptr(GPtrArray) dirs = g_ptr_array_new_with_free_func (g_free);
|
||||
const gchar *dir;
|
||||
const gchar *subdir =
|
||||
(flags & WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER) ? "wireplumber" : ".";
|
||||
|
||||
/* Compile the list of lookup directories in priority order */
|
||||
if (is_absolute) {
|
||||
g_ptr_array_add (dirs, NULL);
|
||||
}
|
||||
else if ((flags & WP_BASE_DIRS_ENV_CONFIG) &&
|
||||
(dir = g_getenv ("WIREPLUMBER_CONFIG_DIR"))) {
|
||||
g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
|
||||
for (guint i = 0; env_dirs[i]; i++) {
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
|
||||
}
|
||||
}
|
||||
else if ((flags & WP_BASE_DIRS_ENV_DATA) &&
|
||||
(dir = g_getenv ("WIREPLUMBER_DATA_DIR"))) {
|
||||
g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
|
||||
for (guint i = 0; env_dirs[i]; i++) {
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
|
||||
}
|
||||
}
|
||||
else if ((flags & WP_BASE_DIRS_ENV_MODULE) &&
|
||||
(dir = g_getenv ("WIREPLUMBER_MODULE_DIR"))) {
|
||||
g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
|
||||
for (guint i = 0; env_dirs[i]; i++) {
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (flags & WP_BASE_DIRS_XDG_CONFIG_HOME) {
|
||||
dir = g_get_user_config_dir ();
|
||||
if (G_LIKELY (g_path_is_absolute (dir)))
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, dir));
|
||||
}
|
||||
if (flags & WP_BASE_DIRS_XDG_DATA_HOME) {
|
||||
dir = g_get_user_data_dir ();
|
||||
if (G_LIKELY (g_path_is_absolute (dir)))
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, dir));
|
||||
}
|
||||
if (flags & WP_BASE_DIRS_XDG_CONFIG_DIRS) {
|
||||
const gchar * const *xdg_dirs = g_get_system_config_dirs ();
|
||||
for (guint i = 0; xdg_dirs[i]; i++) {
|
||||
if (G_LIKELY (g_path_is_absolute (xdg_dirs[i])))
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, xdg_dirs[i]));
|
||||
}
|
||||
}
|
||||
if (flags & WP_BASE_DIRS_BUILD_SYSCONFDIR) {
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_SYSCONFDIR));
|
||||
}
|
||||
if (flags & WP_BASE_DIRS_XDG_DATA_DIRS) {
|
||||
const gchar * const *xdg_dirs = g_get_system_data_dirs ();
|
||||
for (guint i = 0; xdg_dirs[i]; i++) {
|
||||
if (G_LIKELY (g_path_is_absolute (xdg_dirs[i])))
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, xdg_dirs[i]));
|
||||
}
|
||||
}
|
||||
if (flags & WP_BASE_DIRS_BUILD_DATADIR) {
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_DATADIR));
|
||||
}
|
||||
if (flags & WP_BASE_DIRS_BUILD_LIBDIR) {
|
||||
subdir = (flags & WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER) ?
|
||||
"wireplumber-" WIREPLUMBER_API_VERSION : ".";
|
||||
g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_LIBDIR));
|
||||
}
|
||||
}
|
||||
|
||||
return g_steal_pointer (&dirs);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Searches for \a filename in the hierarchy of directories specified
|
||||
* by the \a flags parameter
|
||||
*
|
||||
* Returns the highest priority file found in the hierarchy of directories
|
||||
* specified by the \a flags parameter. The \a subdir parameter is the name
|
||||
* of the subdirectory to search in, inside the specified directories. If
|
||||
* \a subdir is NULL, the base path of each directory is used.
|
||||
*
|
||||
* The \a filename parameter is the name of the file to search for. If the
|
||||
* file is found, its full path is returned. If the file is not found, NULL
|
||||
* is returned. The file is considered found if it is a regular file.
|
||||
*
|
||||
* If the \a filename is an absolute path, it is tested for existence and
|
||||
* returned as is, ignoring the lookup directories in \a flags as well as
|
||||
* the \a subdir parameter.
|
||||
*
|
||||
* \ingroup wpbasedirs
|
||||
* \param flags flags to specify the directories to look into and other
|
||||
* options specific to the kind of file being looked up
|
||||
* \param subdir (nullable): the name of the subdirectory to search in,
|
||||
* inside the specified directories
|
||||
* \param filename the name of the file to search for
|
||||
* \returns (transfer full) (nullable): A newly allocated string with the
|
||||
* absolute, canonicalized file path, or NULL if the file was not found.
|
||||
* \since 0.5.0
|
||||
*/
|
||||
gchar *
|
||||
wp_base_dirs_find_file (WpBaseDirsFlags flags, const gchar * subdir,
|
||||
const gchar * filename)
|
||||
{
|
||||
gboolean is_absolute = g_path_is_absolute (filename);
|
||||
g_autoptr (GPtrArray) dir_paths = lookup_dirs (flags, is_absolute);
|
||||
gchar *ret = NULL;
|
||||
|
||||
/* ignore the subdir if filename is absolute */
|
||||
if (is_absolute)
|
||||
subdir = NULL;
|
||||
|
||||
for (guint i = 0; i < dir_paths->len; i++) {
|
||||
g_autofree gchar *path = make_path (flags, g_ptr_array_index (dir_paths, i),
|
||||
subdir, filename);
|
||||
wp_trace ("test file: %s", path);
|
||||
if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) {
|
||||
ret = g_steal_pointer (&path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wp_debug ("lookup '%s', return: %s", filename, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct conffile_iterator_item
|
||||
{
|
||||
gchar *filename;
|
||||
gchar *path;
|
||||
};
|
||||
|
||||
static void
|
||||
conffile_iterator_item_clear (struct conffile_iterator_item *item)
|
||||
{
|
||||
g_free (item->filename);
|
||||
g_free (item->path);
|
||||
}
|
||||
|
||||
struct conffile_iterator_data
|
||||
{
|
||||
GArray *items;
|
||||
guint idx;
|
||||
};
|
||||
|
||||
static void
|
||||
conffile_iterator_reset (WpIterator *it)
|
||||
{
|
||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
it_data->idx = 0;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
conffile_iterator_next (WpIterator *it, GValue *item)
|
||||
{
|
||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
|
||||
if (it_data->idx < it_data->items->len) {
|
||||
const gchar *path = g_array_index (it_data->items,
|
||||
struct conffile_iterator_item, it_data->idx).path;
|
||||
it_data->idx++;
|
||||
g_value_init (item, G_TYPE_STRING);
|
||||
g_value_set_string (item, path);
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
conffile_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
|
||||
gpointer data)
|
||||
{
|
||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
|
||||
for (guint i = 0; i < it_data->items->len; i++) {
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
const gchar *path = g_array_index (it_data->items,
|
||||
struct conffile_iterator_item, i).path;
|
||||
g_value_init (&item, G_TYPE_STRING);
|
||||
g_value_set_string (&item, path);
|
||||
if (!func (&item, ret, data))
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
conffile_iterator_finalize (WpIterator *it)
|
||||
{
|
||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
g_clear_pointer (&it_data->items, g_array_unref);
|
||||
}
|
||||
|
||||
static const WpIteratorMethods conffile_iterator_methods = {
|
||||
.version = WP_ITERATOR_METHODS_VERSION,
|
||||
.reset = conffile_iterator_reset,
|
||||
.next = conffile_iterator_next,
|
||||
.fold = conffile_iterator_fold,
|
||||
.finalize = conffile_iterator_finalize,
|
||||
};
|
||||
|
||||
static gint
|
||||
conffile_iterator_item_compare (const struct conffile_iterator_item *a,
|
||||
const struct conffile_iterator_item *b)
|
||||
{
|
||||
return g_strcmp0 (a->filename, b->filename);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Creates an iterator to iterate over all files that match \a suffix
|
||||
* within the \a subdir of the directories specified in \a flags
|
||||
*
|
||||
* The \a subdir parameter is the name of the subdirectory to search in,
|
||||
* inside the directories specified by \a flags. If \a subdir is NULL,
|
||||
* the base path of each directory is used. If \a subdir is an absolute path,
|
||||
* files are only looked up in that directory and the directories in \a flags
|
||||
* are ignored.
|
||||
*
|
||||
* The \a suffix parameter is the filename suffix to match. If \a suffix is
|
||||
* NULL, all files are matched.
|
||||
*
|
||||
* The iterator will iterate over the absolute paths of all the files
|
||||
* files found, in the order of priority of the directories, starting from
|
||||
* the lowest priority directory (e.g. /usr/share/wireplumber) and ending
|
||||
* with the highest priority directory (e.g. $XDG_CONFIG_HOME/wireplumber).
|
||||
* Files within each directory are also sorted by filename.
|
||||
*
|
||||
* \ingroup wpbasedirs
|
||||
* \param flags flags to specify the directories to look into and other
|
||||
* options specific to the kind of file being looked up
|
||||
* \param subdir (nullable): the name of the subdirectory to search in,
|
||||
* inside the configuration directories
|
||||
* \param suffix (nullable): The filename suffix, NULL matches all entries
|
||||
* \returns (transfer full): a new iterator iterating over strings which are
|
||||
* absolute & canonicalized paths to the files found
|
||||
* \since 0.5.0
|
||||
*/
|
||||
WpIterator *
|
||||
wp_base_dirs_new_files_iterator (WpBaseDirsFlags flags,
|
||||
const gchar * subdir, const gchar * suffix)
|
||||
{
|
||||
g_autoptr (GArray) items =
|
||||
g_array_new (FALSE, FALSE, sizeof (struct conffile_iterator_item));
|
||||
g_autoptr (GPtrArray) dir_paths = NULL;
|
||||
|
||||
g_array_set_clear_func (items, (GDestroyNotify) conffile_iterator_item_clear);
|
||||
|
||||
if (subdir == NULL)
|
||||
subdir = ".";
|
||||
|
||||
/* Note: this list is highest-priority first */
|
||||
dir_paths = lookup_dirs (flags, g_path_is_absolute (subdir));
|
||||
|
||||
/* Run backwards through the list to get files in lowest-priority-first order */
|
||||
for (guint i = dir_paths->len; i > 0; i--) {
|
||||
g_autofree gchar *dirpath =
|
||||
g_canonicalize_filename (subdir, g_ptr_array_index (dir_paths, i - 1));
|
||||
g_autoptr (GDir) dir = g_dir_open (dirpath, 0, NULL);
|
||||
|
||||
if (dir) {
|
||||
g_autoptr (GArray) dir_items = g_array_new (FALSE, FALSE,
|
||||
sizeof (struct conffile_iterator_item));
|
||||
|
||||
wp_trace ("searching dir: %s", dirpath);
|
||||
|
||||
/* Store all filenames with their full path in the local array */
|
||||
const gchar *filename;
|
||||
while ((filename = g_dir_read_name (dir))) {
|
||||
if (filename[0] == '.')
|
||||
continue;
|
||||
|
||||
if (suffix && !g_str_has_suffix (filename, suffix))
|
||||
continue;
|
||||
|
||||
/* verify the file is regular and canonicalize the path */
|
||||
g_autofree gchar *path = make_path (flags, dirpath, NULL, filename);
|
||||
if (!g_file_test (path, G_FILE_TEST_IS_REGULAR))
|
||||
continue;
|
||||
|
||||
/* remove item with the same filename from the global items array,
|
||||
so that lower priority files can be shadowed */
|
||||
for (guint j = 0; j < items->len; j++) {
|
||||
struct conffile_iterator_item *item = &g_array_index (items,
|
||||
struct conffile_iterator_item, j);
|
||||
if (g_strcmp0 (item->filename, filename) == 0) {
|
||||
g_array_remove_index (items, j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* append in the local array */
|
||||
g_array_append_val (dir_items, ((struct conffile_iterator_item) {
|
||||
.filename = g_strdup (filename),
|
||||
.path = g_steal_pointer (&path),
|
||||
}));
|
||||
}
|
||||
|
||||
/* Sort files of the current dir by filename */
|
||||
g_array_sort (dir_items, (GCompareFunc) conffile_iterator_item_compare);
|
||||
|
||||
/* Append the sorted files to the global array */
|
||||
g_array_append_vals (items, dir_items->data, dir_items->len);
|
||||
}
|
||||
}
|
||||
|
||||
/* Construct iterator */
|
||||
WpIterator *it = wp_iterator_new (&conffile_iterator_methods,
|
||||
sizeof (struct conffile_iterator_data));
|
||||
struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
it_data->items = g_steal_pointer (&items);
|
||||
it_data->idx = 0;
|
||||
return g_steal_pointer (&it);
|
||||
}
|
||||
89
lib/wp/base-dirs.h
Normal file
89
lib/wp/base-dirs.h
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2024 Collabora Ltd.
|
||||
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef __WIREPLUMBER_BASE_DIRS_H__
|
||||
#define __WIREPLUMBER_BASE_DIRS_H__
|
||||
|
||||
#include "defs.h"
|
||||
#include "iterator.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/*!
|
||||
* \brief Flags to specify lookup directories
|
||||
* \ingroup wpbasedirs
|
||||
*
|
||||
* These flags can be used to specify which directories to look for a file in.
|
||||
* The flags can be combined to search in multiple directories at once. Some
|
||||
* flags may also used to specify the type of the file being looked up or other
|
||||
* lookup parameters.
|
||||
*
|
||||
* Lookup is performed in the same order as the flags are listed here. Note that
|
||||
* if a WirePlumber-specific environment variable is set ($WIREPLUMBER_*_DIR)
|
||||
* and the equivalent WP_BASE_DIRS_ENV_* flag is specified, the lookup in other
|
||||
* directories is skipped, even if the file is not found in the
|
||||
* environment-specified directory.
|
||||
*/
|
||||
typedef enum { /*< flags >*/
|
||||
WP_BASE_DIRS_ENV_CONFIG = (1 << 0), /*!< $WIREPLUMBER_CONFIG_DIR */
|
||||
WP_BASE_DIRS_ENV_DATA = (1 << 1), /*!< $WIREPLUMBER_DATA_DIR */
|
||||
WP_BASE_DIRS_ENV_MODULE = (1 << 2), /*!< $WIREPLUMBER_MODULE_DIR */
|
||||
|
||||
WP_BASE_DIRS_XDG_CONFIG_HOME = (1 << 8), /*!< XDG_CONFIG_HOME */
|
||||
WP_BASE_DIRS_XDG_DATA_HOME = (1 << 9), /*!< XDG_DATA_HOME */
|
||||
|
||||
WP_BASE_DIRS_XDG_CONFIG_DIRS = (1 << 10), /*!< XDG_CONFIG_DIRS */
|
||||
WP_BASE_DIRS_BUILD_SYSCONFDIR = (1 << 11), /*!< compile-time $sysconfdir (/etc) */
|
||||
|
||||
WP_BASE_DIRS_XDG_DATA_DIRS = (1 << 12), /*!< XDG_DATA_DIRS */
|
||||
WP_BASE_DIRS_BUILD_DATADIR = (1 << 13), /*!< compile-time $datadir ($prefix/share) */
|
||||
|
||||
WP_BASE_DIRS_BUILD_LIBDIR = (1 << 14), /*!< compile-time $libdir ($prefix/lib) */
|
||||
|
||||
/*! the file is a loadable module; prepend "lib" and append ".so" if needed */
|
||||
WP_BASE_DIRS_FLAG_MODULE = (1 << 24),
|
||||
|
||||
/*! append "/wireplumber" to the location, except in the case of locations
|
||||
that are specified via WirePlumber-specific environment variables;
|
||||
in LIBDIR, append "/wireplumber-$API_version" instead */
|
||||
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER = (1 << 25),
|
||||
|
||||
WP_BASE_DIRS_CONFIGURATION =
|
||||
(WP_BASE_DIRS_ENV_CONFIG |
|
||||
WP_BASE_DIRS_XDG_CONFIG_HOME |
|
||||
WP_BASE_DIRS_XDG_CONFIG_DIRS |
|
||||
WP_BASE_DIRS_BUILD_SYSCONFDIR |
|
||||
WP_BASE_DIRS_XDG_DATA_DIRS |
|
||||
WP_BASE_DIRS_BUILD_DATADIR |
|
||||
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
|
||||
|
||||
WP_BASE_DIRS_DATA =
|
||||
(WP_BASE_DIRS_ENV_DATA |
|
||||
WP_BASE_DIRS_XDG_DATA_HOME |
|
||||
WP_BASE_DIRS_XDG_DATA_DIRS |
|
||||
WP_BASE_DIRS_BUILD_DATADIR |
|
||||
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
|
||||
|
||||
WP_BASE_DIRS_MODULE =
|
||||
(WP_BASE_DIRS_ENV_MODULE |
|
||||
WP_BASE_DIRS_BUILD_LIBDIR |
|
||||
WP_BASE_DIRS_FLAG_MODULE |
|
||||
WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
|
||||
} WpBaseDirsFlags;
|
||||
|
||||
WP_API
|
||||
gchar * wp_base_dirs_find_file (WpBaseDirsFlags flags,
|
||||
const gchar * subdir, const gchar * filename);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_base_dirs_new_files_iterator (WpBaseDirsFlags flags,
|
||||
const gchar * subdir, const gchar * suffix);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
#include "client.h"
|
||||
#include "log.h"
|
||||
#include "private/pipewire-object-mixin.h"
|
||||
#include "private/permission-manager.h"
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")
|
|||
struct _WpClient
|
||||
{
|
||||
WpGlobalProxy parent;
|
||||
GWeakRef permission_manager;
|
||||
};
|
||||
|
||||
static void wp_client_pw_object_mixin_priv_interface_init (
|
||||
|
|
@ -39,6 +41,7 @@ G_DEFINE_TYPE_WITH_CODE (WpClient, wp_client, WP_TYPE_GLOBAL_PROXY,
|
|||
static void
|
||||
wp_client_init (WpClient * self)
|
||||
{
|
||||
g_weak_ref_init (&self->permission_manager, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -76,11 +79,27 @@ wp_client_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
|
|||
static void
|
||||
wp_client_pw_proxy_destroyed (WpProxy * proxy)
|
||||
{
|
||||
WpClient *self = WP_CLIENT (proxy);
|
||||
|
||||
wp_client_attach_permission_manager (self, NULL);
|
||||
|
||||
wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
|
||||
|
||||
WP_PROXY_CLASS (wp_client_parent_class)->pw_proxy_destroyed (proxy);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_impl_node_finalize (GObject * object)
|
||||
{
|
||||
WpClient *self = WP_CLIENT (object);
|
||||
|
||||
wp_client_attach_permission_manager (self, NULL);
|
||||
|
||||
g_weak_ref_clear (&self->permission_manager);
|
||||
|
||||
G_OBJECT_CLASS (wp_client_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_client_class_init (WpClientClass * klass)
|
||||
{
|
||||
|
|
@ -88,6 +107,7 @@ wp_client_class_init (WpClientClass * klass)
|
|||
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
|
||||
WpProxyClass *proxy_class = (WpProxyClass *) klass;
|
||||
|
||||
object_class->finalize = wp_impl_node_finalize;
|
||||
object_class->get_property = wp_pw_object_mixin_get_property;
|
||||
|
||||
wpobject_class->get_supported_features =
|
||||
|
|
@ -221,3 +241,30 @@ wp_client_update_properties (WpClient * self, WpProperties * updates)
|
|||
|
||||
g_warn_if_fail (client_update_properties_result >= 0);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Attaches a permission manager in the client to handle permissions
|
||||
* automatically.
|
||||
*
|
||||
* \ingroup wpclient
|
||||
* \param self the client
|
||||
* \param pm (transfer none) (nullable): the permission manager to attach, or
|
||||
* NULL to detach the current permission manager.
|
||||
*/
|
||||
void
|
||||
wp_client_attach_permission_manager (WpClient *self, WpPermissionManager *pm)
|
||||
{
|
||||
g_autoptr (WpPermissionManager) curr_pm = NULL;
|
||||
|
||||
g_return_if_fail (WP_IS_CLIENT (self));
|
||||
|
||||
curr_pm = g_weak_ref_get (&self->permission_manager);
|
||||
if (curr_pm == pm)
|
||||
return;
|
||||
|
||||
if (curr_pm)
|
||||
wp_permission_manager_remove_client (curr_pm, self);
|
||||
if (pm)
|
||||
wp_permission_manager_add_client (pm, self);
|
||||
g_weak_ref_set (&self->permission_manager, pm);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#define __WIREPLUMBER_CLIENT_H__
|
||||
|
||||
#include "global-proxy.h"
|
||||
#include "permission-manager.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
|
|
@ -37,6 +38,10 @@ void wp_client_update_permissions_array (WpClient * self,
|
|||
WP_API
|
||||
void wp_client_update_properties (WpClient * self, WpProperties * updates);
|
||||
|
||||
WP_API
|
||||
void wp_client_attach_permission_manager (WpClient *self,
|
||||
WpPermissionManager *pm);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -153,8 +153,8 @@ on_component_loader_load_done (WpComponentLoader * cl, GAsyncResult * res,
|
|||
* provide if it loads successfully; this can be queried later with
|
||||
* wp_core_test_feature()
|
||||
* \param cancellable (nullable): optional GCancellable
|
||||
* \param callback (scope async): the callback to call when the operation is done
|
||||
* \param data (closure): data to pass to \a callback
|
||||
* \param callback (scope async)(closure data): the callback to call when the operation is done
|
||||
* \param data data to pass to \a callback
|
||||
*/
|
||||
void
|
||||
wp_core_load_component (WpCore * self, const gchar * component,
|
||||
|
|
|
|||
721
lib/wp/conf.c
721
lib/wp/conf.c
|
|
@ -6,13 +6,14 @@
|
|||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "core.h"
|
||||
#include "conf.h"
|
||||
#include "log.h"
|
||||
#include "object-interest.h"
|
||||
#include "json-utils.h"
|
||||
#include "base-dirs.h"
|
||||
#include "error.h"
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <spa/utils/result.h>
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")
|
||||
|
||||
|
|
@ -26,19 +27,40 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")
|
|||
* configuration.
|
||||
*/
|
||||
|
||||
typedef struct _WpConfSection WpConfSection;
|
||||
struct _WpConfSection
|
||||
{
|
||||
gchar *name;
|
||||
WpSpaJson *value;
|
||||
gchar *location;
|
||||
};
|
||||
|
||||
static void
|
||||
wp_conf_section_clear (WpConfSection * section)
|
||||
{
|
||||
g_free (section->name);
|
||||
g_clear_pointer (§ion->value, wp_spa_json_unref);
|
||||
g_free (section->location);
|
||||
}
|
||||
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpConfSection, wp_conf_section_clear)
|
||||
|
||||
struct _WpConf
|
||||
{
|
||||
GObject parent;
|
||||
|
||||
/* Props */
|
||||
GWeakRef core;
|
||||
gchar *name;
|
||||
WpProperties *properties;
|
||||
|
||||
GHashTable *sections;
|
||||
/* Private */
|
||||
GArray *conf_sections; /* element-type: WpConfSection */
|
||||
GPtrArray *files; /* element-type: GMappedFile* */
|
||||
};
|
||||
|
||||
enum {
|
||||
PROP_0,
|
||||
PROP_CORE,
|
||||
PROP_NAME,
|
||||
PROP_PROPERTIES,
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)
|
||||
|
|
@ -46,10 +68,9 @@ G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)
|
|||
static void
|
||||
wp_conf_init (WpConf * self)
|
||||
{
|
||||
g_weak_ref_init (&self->core, NULL);
|
||||
|
||||
self->sections = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
|
||||
(GDestroyNotify) wp_spa_json_unref);
|
||||
self->conf_sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));
|
||||
g_array_set_clear_func (self->conf_sections, (GDestroyNotify) wp_conf_section_clear);
|
||||
self->files = g_ptr_array_new_with_free_func ((GDestroyNotify) g_mapped_file_unref);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -59,8 +80,11 @@ wp_conf_set_property (GObject * object, guint property_id,
|
|||
WpConf *self = WP_CONF (object);
|
||||
|
||||
switch (property_id) {
|
||||
case PROP_CORE:
|
||||
g_weak_ref_set (&self->core, g_value_get_object (value));
|
||||
case PROP_NAME:
|
||||
self->name = g_value_dup_string (value);
|
||||
break;
|
||||
case PROP_PROPERTIES:
|
||||
self->properties = g_value_dup_boxed (value);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
|
|
@ -75,8 +99,11 @@ wp_conf_get_property (GObject * object, guint property_id,
|
|||
WpConf *self = WP_CONF (object);
|
||||
|
||||
switch (property_id) {
|
||||
case PROP_CORE:
|
||||
g_value_take_object (value, g_weak_ref_get (&self->core));
|
||||
case PROP_NAME:
|
||||
g_value_set_string (value, self->name);
|
||||
break;
|
||||
case PROP_PROPERTIES:
|
||||
g_value_take_boxed (value, wp_properties_copy (self->properties));
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
|
|
@ -89,8 +116,11 @@ wp_conf_finalize (GObject * object)
|
|||
{
|
||||
WpConf *self = WP_CONF (object);
|
||||
|
||||
g_clear_pointer (&self->sections, g_hash_table_unref);
|
||||
g_weak_ref_clear (&self->core);
|
||||
wp_conf_close (self);
|
||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||
g_clear_pointer (&self->conf_sections, g_array_unref);
|
||||
g_clear_pointer (&self->files, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->name, g_free);
|
||||
|
||||
G_OBJECT_CLASS (wp_conf_parent_class)->finalize (object);
|
||||
}
|
||||
|
|
@ -104,346 +134,469 @@ wp_conf_class_init (WpConfClass * klass)
|
|||
object_class->set_property = wp_conf_set_property;
|
||||
object_class->get_property = wp_conf_get_property;
|
||||
|
||||
g_object_class_install_property (object_class, PROP_CORE,
|
||||
g_param_spec_object ("core", "core", "The WpCore", WP_TYPE_CORE,
|
||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
||||
g_object_class_install_property(object_class, PROP_NAME,
|
||||
g_param_spec_string ("name", "name", "The name of the configuration file",
|
||||
NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property(object_class, PROP_PROPERTIES,
|
||||
g_param_spec_boxed ("properties", "properties", "WpProperties",
|
||||
WP_TYPE_PROPERTIES, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the WpConf instance that is associated with the
|
||||
* given core.
|
||||
* \brief Creates a new WpConf object
|
||||
*
|
||||
* This method will also create the instance and register it with the core
|
||||
* if it had not been created before.
|
||||
* This does not open the files, it only creates the object. For most use cases,
|
||||
* you should use wp_conf_new_open() instead.
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param core the core
|
||||
* \returns (transfer full): the WpConf instance
|
||||
* \param name the name of the configuration file
|
||||
* \param properties (transfer full) (nullable): a WpProperties with keys
|
||||
* specifying how to load the WpConf object
|
||||
* \returns (transfer full): a new WpConf object
|
||||
*/
|
||||
WpConf *
|
||||
wp_conf_get_instance (WpCore *core)
|
||||
wp_conf_new (const gchar * name, WpProperties * properties)
|
||||
{
|
||||
WpConf *conf = wp_core_find_object (core,
|
||||
(GEqualFunc) WP_IS_CONF, NULL);
|
||||
|
||||
if (G_UNLIKELY (!conf)) {
|
||||
conf = g_object_new (WP_TYPE_CONF,
|
||||
"core", core,
|
||||
NULL);
|
||||
|
||||
wp_core_register_object (core, g_object_ref (conf));
|
||||
|
||||
wp_info_object (conf, "created wpconf object");
|
||||
}
|
||||
|
||||
return conf;
|
||||
g_return_val_if_fail (name, NULL);
|
||||
g_autoptr (WpProperties) props = properties;
|
||||
return g_object_new (WP_TYPE_CONF, "name", name,
|
||||
"properties", props,
|
||||
NULL);
|
||||
}
|
||||
|
||||
static gint
|
||||
merge_section_cb (void *data, const char *location, const char *section,
|
||||
const char *str, size_t len)
|
||||
/*!
|
||||
* \brief Creates a new WpConf object and opens the configuration file and its
|
||||
* fragments, keeping them mapped in memory for further access.
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param name the name of the configuration file
|
||||
* \param properties (transfer full) (nullable): a WpProperties with keys
|
||||
* specifying how to load the WpConf object
|
||||
* \param error (out) (nullable): return location for a GError, or NULL
|
||||
* \returns (transfer full) (nullable): a new WpConf object, or NULL
|
||||
* if an error occurred
|
||||
*/
|
||||
WpConf *
|
||||
wp_conf_new_open (const gchar * name, WpProperties * properties, GError ** error)
|
||||
{
|
||||
WpSpaJson **res_section = (WpSpaJson **)data;
|
||||
g_autoptr (WpSpaJson) json = NULL;
|
||||
gboolean override;
|
||||
g_return_val_if_fail (name, NULL);
|
||||
|
||||
g_return_val_if_fail (res_section, -EINVAL);
|
||||
g_autoptr (WpConf) self = wp_conf_new (name, properties);
|
||||
if (!wp_conf_open (self, error))
|
||||
return NULL;
|
||||
return g_steal_pointer (&self);
|
||||
}
|
||||
|
||||
override = g_str_has_prefix (section, OVERRIDE_SECTION_PREFIX);
|
||||
if (override)
|
||||
section += strlen (OVERRIDE_SECTION_PREFIX);
|
||||
static gboolean
|
||||
detect_old_conf_format (WpConf * self, GMappedFile *file)
|
||||
{
|
||||
const gchar *data = g_mapped_file_get_contents (file);
|
||||
gsize size = g_mapped_file_get_length (file);
|
||||
|
||||
wp_debug ("loading section %s (override=%d) from %s", section, override,
|
||||
location);
|
||||
/* wireplumber 0.4 used to have components of type = config/lua */
|
||||
return g_strrstr_len (data, size, "config/lua") ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
/* Only allow sections to be objects or arrays */
|
||||
json = wp_spa_json_new_wrap_stringn (str, len);
|
||||
if (!wp_spa_json_is_container (json)) {
|
||||
wp_warning (
|
||||
"skipping section %s from %s as it is not JSON object or array",
|
||||
section, location);
|
||||
return 0;
|
||||
static gboolean
|
||||
open_and_load_sections (WpConf * self, const gchar *path, GError ** error)
|
||||
{
|
||||
const gchar *as_section = NULL;
|
||||
|
||||
if (self->properties) {
|
||||
/* as-section="some.name" means that the entire file will be stored as a
|
||||
single JSON value that will be accessible through wp_conf_get_section()
|
||||
using "some.name" - no parsing is done; the value is expected to be a
|
||||
container */
|
||||
as_section = wp_properties_get (self->properties, "as-section");
|
||||
wp_debug_object (self, "Reading config file as single section: %s", as_section);
|
||||
}
|
||||
|
||||
/* Merge section if it was defined previously and the 'override.' prefix is
|
||||
* not used */
|
||||
if (!override && *res_section) {
|
||||
g_autoptr (WpSpaJson) merged =
|
||||
wp_json_utils_merge_containers (*res_section, json);
|
||||
if (!merged) {
|
||||
wp_warning (
|
||||
"skipping merge of %s from %s as JSON values are not compatible",
|
||||
section, location);
|
||||
return 0;
|
||||
g_autoptr (GMappedFile) file = g_mapped_file_new (path, FALSE, error);
|
||||
if (!file)
|
||||
return FALSE;
|
||||
|
||||
if (!g_mapped_file_get_contents (file) || g_mapped_file_get_length (file) == 0) {
|
||||
wp_notice_object (self, "Ignoring empty configuration file at '%s'", path);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* test if the file is a relic from 0.4 */
|
||||
if (detect_old_conf_format (self, file)) {
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"The configuration file at '%s' is likely an old WirePlumber 0.4 config "
|
||||
"and is not supported anymore. Try removing it.", path);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
g_autoptr (GArray) sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));
|
||||
g_array_set_clear_func (sections, (GDestroyNotify) wp_conf_section_clear);
|
||||
|
||||
g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_stringn (
|
||||
g_mapped_file_get_contents (file), g_mapped_file_get_length (file));
|
||||
|
||||
if (as_section) {
|
||||
WpConfSection section = { 0, };
|
||||
|
||||
if (!wp_spa_json_is_container (json)) {
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"invalid single-section config file start (expected an object or array): %.*s",
|
||||
(int) wp_spa_json_get_size (json), wp_spa_json_get_data (json));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
g_clear_pointer (res_section, wp_spa_json_unref);
|
||||
*res_section = g_steal_pointer (&merged);
|
||||
wp_debug ("section %s from %s loaded", location, section);
|
||||
section.name = g_strdup (as_section);
|
||||
section.value = g_steal_pointer (&json);
|
||||
section.location = g_strdup (path);
|
||||
g_array_append_val (sections, section);
|
||||
}
|
||||
|
||||
/* Otherwise always replace */
|
||||
else {
|
||||
g_clear_pointer (res_section, wp_spa_json_unref);
|
||||
*res_section = g_steal_pointer (&json);
|
||||
wp_debug ("section %s from %s loaded", location, section);
|
||||
g_auto (WpConfSection) section = { 0, };
|
||||
g_autoptr (WpSpaJson) tmp = NULL, toplevel = NULL;
|
||||
g_autoptr (WpSpaJsonParser) parser = wp_spa_json_parser_new_undefined (json);
|
||||
|
||||
/* get the very first token */
|
||||
tmp = wp_spa_json_parser_get_json (parser);
|
||||
|
||||
/* if the top-level token is an object, parse that instead */
|
||||
if (tmp && wp_spa_json_is_object (tmp)) {
|
||||
g_clear_pointer (&parser, wp_spa_json_parser_unref);
|
||||
toplevel = g_steal_pointer (&tmp);
|
||||
parser = wp_spa_json_parser_new_object (toplevel);
|
||||
tmp = wp_spa_json_parser_get_json (parser);
|
||||
}
|
||||
|
||||
while (TRUE) {
|
||||
if (!tmp)
|
||||
break;
|
||||
|
||||
/* if !is_string, but we want to support strings without quotes */
|
||||
if (wp_spa_json_is_container (tmp) ||
|
||||
wp_spa_json_is_int (tmp) ||
|
||||
wp_spa_json_is_float (tmp) ||
|
||||
wp_spa_json_is_boolean (tmp) ||
|
||||
wp_spa_json_is_null (tmp))
|
||||
{
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"invalid section name (not a string): %.*s",
|
||||
(int) wp_spa_json_get_size (tmp), wp_spa_json_get_data (tmp));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
section.name = wp_spa_json_parse_string (tmp);
|
||||
g_clear_pointer (&tmp, wp_spa_json_unref);
|
||||
|
||||
/* parse the section contents */
|
||||
tmp = wp_spa_json_parser_get_json (parser);
|
||||
if (!tmp) {
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"section '%s' has no value", section.name);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
section.value = g_steal_pointer (&tmp);
|
||||
section.location = g_strdup (path);
|
||||
g_array_append_val (sections, section);
|
||||
memset (§ion, 0, sizeof (section));
|
||||
|
||||
/* parse the next section name */
|
||||
tmp = wp_spa_json_parser_get_json (parser);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
/* store the mapped file and the sections; note that the stored WpSpaJson
|
||||
still point to the data in the GMappedFile, so this is why we keep the
|
||||
GMappedFile alive */
|
||||
g_ptr_array_add (self->files, g_steal_pointer (&file));
|
||||
g_array_append_vals (self->conf_sections, sections->data, sections->len);
|
||||
g_array_set_clear_func (sections, NULL);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
ensure_section_loaded (WpConf *self, const gchar *section)
|
||||
/*!
|
||||
* \brief Opens the configuration file and its fragments and keeps them
|
||||
* mapped in memory for further access.
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
* \param error (out)(nullable): return location for a GError, or NULL
|
||||
* \returns TRUE on success, FALSE on error
|
||||
*/
|
||||
gboolean
|
||||
wp_conf_open (WpConf * self, GError ** error)
|
||||
{
|
||||
g_autoptr (WpCore) core = NULL;
|
||||
struct pw_context *pw_ctx = NULL;
|
||||
g_autoptr (WpSpaJson) json_section = NULL;
|
||||
g_autofree gchar *override_section = NULL;
|
||||
const gchar *no_frags = NULL;
|
||||
|
||||
if (g_hash_table_contains (self->sections, section))
|
||||
return;
|
||||
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
|
||||
|
||||
core = g_weak_ref_get (&self->core);
|
||||
g_return_if_fail (core);
|
||||
pw_ctx = wp_core_get_pw_context (core);
|
||||
g_return_if_fail (pw_ctx);
|
||||
g_autofree gchar *path = NULL;
|
||||
g_autoptr (WpIterator) iterator = NULL;
|
||||
g_auto (GValue) value = G_VALUE_INIT;
|
||||
|
||||
pw_context_conf_section_for_each (pw_ctx, section, merge_section_cb,
|
||||
&json_section);
|
||||
override_section = g_strdup_printf (OVERRIDE_SECTION_PREFIX "%s", section);
|
||||
pw_context_conf_section_for_each (pw_ctx, override_section, merge_section_cb,
|
||||
&json_section);
|
||||
if (self->properties) {
|
||||
no_frags = wp_properties_get (self->properties, "no-fragments");
|
||||
}
|
||||
|
||||
if (json_section)
|
||||
g_hash_table_insert (self->sections, g_strdup (section),
|
||||
g_steal_pointer (&json_section));
|
||||
/*
|
||||
* open the config file - if the path supplied is absolute,
|
||||
* wp_base_dirs_find_file will ignore WP_BASE_DIRS_CONFIGURATION
|
||||
*/
|
||||
path = wp_base_dirs_find_file (WP_BASE_DIRS_CONFIGURATION, NULL, self->name);
|
||||
if (path) {
|
||||
wp_info_object (self, "opening config file: %s", path);
|
||||
if (!open_and_load_sections (self, path, error))
|
||||
return FALSE;
|
||||
}
|
||||
g_clear_pointer (&path, g_free);
|
||||
|
||||
/* open the .conf.d/ fragments */
|
||||
if (!no_frags) {
|
||||
path = g_strdup_printf ("%s.d", self->name);
|
||||
iterator = wp_base_dirs_new_files_iterator (WP_BASE_DIRS_CONFIGURATION, path,
|
||||
".conf");
|
||||
|
||||
for (; wp_iterator_next (iterator, &value); g_value_unset (&value)) {
|
||||
const gchar *filename = g_value_get_string (&value);
|
||||
|
||||
wp_info_object (self, "opening fragment file: %s", filename);
|
||||
|
||||
g_autoptr (GError) e = NULL;
|
||||
if (!open_and_load_sections (self, filename, &e)) {
|
||||
wp_warning_object (self, "failed to open '%s': %s", filename, e->message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self->files->len == 0) {
|
||||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
|
||||
"Could not locate configuration file '%s'", self->name);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Closes the configuration file and its fragments
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
*/
|
||||
void
|
||||
wp_conf_close (WpConf * self)
|
||||
{
|
||||
g_return_if_fail (WP_IS_CONF (self));
|
||||
|
||||
g_array_set_size (self->conf_sections, 0);
|
||||
g_ptr_array_set_size (self->files, 0);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests if the configuration files are open
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
* \returns TRUE if the configuration files are open, FALSE otherwise
|
||||
*/
|
||||
gboolean
|
||||
wp_conf_is_open (WpConf * self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
|
||||
return self->files->len > 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the name of the configuration file
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
* \returns the name of the configuration file
|
||||
*/
|
||||
const gchar *
|
||||
wp_conf_get_name (WpConf * self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
||||
return self->name;
|
||||
}
|
||||
|
||||
static WpSpaJson *
|
||||
ensure_merged_section (WpConf * self, const gchar *section)
|
||||
{
|
||||
g_autoptr (WpSpaJson) merged = NULL;
|
||||
WpConfSection *merged_section = NULL;
|
||||
|
||||
/* check if the section is already merged */
|
||||
for (guint i = 0; i < self->conf_sections->len; i++) {
|
||||
WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
|
||||
if (g_str_equal (s->name, section)) {
|
||||
if (!s->location) {
|
||||
wp_debug_object (self, "section %s is already merged", section);
|
||||
return wp_spa_json_ref (s->value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Iterate over the sections and merge them */
|
||||
for (guint i = 0; i < self->conf_sections->len; i++) {
|
||||
WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
|
||||
const gchar *s_name = s->name;
|
||||
|
||||
/* skip the "override." prefix and take a note */
|
||||
gboolean override = g_str_has_prefix (s_name, OVERRIDE_SECTION_PREFIX);
|
||||
if (override)
|
||||
s_name += strlen (OVERRIDE_SECTION_PREFIX);
|
||||
|
||||
if (g_str_equal (s_name, section)) {
|
||||
/* Merge sections if a previous value exists and
|
||||
the 'override.' prefix is not present */
|
||||
if (!override && merged) {
|
||||
g_autoptr (WpSpaJson) new_merged =
|
||||
wp_json_utils_merge_containers (merged, s->value);
|
||||
if (!merged) {
|
||||
wp_warning_object (self,
|
||||
"skipping merge of '%s' from '%s' as JSON containers are not compatible",
|
||||
section, s->location);
|
||||
continue;
|
||||
}
|
||||
|
||||
g_clear_pointer (&merged, wp_spa_json_unref);
|
||||
merged = g_steal_pointer (&new_merged);
|
||||
merged_section = NULL;
|
||||
}
|
||||
/* Otherwise always replace */
|
||||
else {
|
||||
g_clear_pointer (&merged, wp_spa_json_unref);
|
||||
merged = wp_spa_json_ref (s->value);
|
||||
merged_section = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* cache the result */
|
||||
if (merged_section) {
|
||||
/* if the merged json came from a single location, just clear
|
||||
the location from that WpConfSection to mark it as the result */
|
||||
wp_info_object (self, "section '%s' is used as-is from '%s'", section,
|
||||
merged_section->location);
|
||||
g_clear_pointer (&merged_section->location, g_free);
|
||||
} else if (merged) {
|
||||
/* if the merged json came from multiple locations, create a new
|
||||
WpConfSection to store it */
|
||||
WpConfSection s = { g_strdup (section), wp_spa_json_ref (merged), NULL };
|
||||
g_array_append_val (self->conf_sections, s);
|
||||
wp_info_object (self, "section '%s' is merged from multiple locations",
|
||||
section);
|
||||
} else {
|
||||
wp_info_object (self, "section '%s' is not defined", section);
|
||||
}
|
||||
|
||||
return g_steal_pointer (&merged);
|
||||
}
|
||||
|
||||
/*!
|
||||
* This method will get the JSON value of a specific section from the
|
||||
* configuration. If the same section is defined in multiple locations, the
|
||||
* sections with the same name will be either merged in case of arrays and
|
||||
* objects, or overridden in case of boolean, int, double and strings. The
|
||||
* passed fallback value will be returned if the section does not exist.
|
||||
* objects, or overridden in case of boolean, int, double and strings.
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
* \param section the section name
|
||||
* \param fallback (transfer full)(nullable): the fallback value
|
||||
* \returns (transfer full): the JSON value of the section
|
||||
* \returns (transfer full) (nullable): the JSON value of the section or NULL
|
||||
* if the section does not exist
|
||||
*/
|
||||
WpSpaJson *
|
||||
wp_conf_get_section (WpConf *self, const gchar *section, WpSpaJson *fallback)
|
||||
wp_conf_get_section (WpConf *self, const gchar *section)
|
||||
{
|
||||
WpSpaJson *s;
|
||||
g_autoptr (WpSpaJson) fb = fallback;
|
||||
|
||||
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
||||
|
||||
ensure_section_loaded (self, section);
|
||||
|
||||
s = g_hash_table_lookup (self->sections, section);
|
||||
if (!s)
|
||||
return fb ? g_steal_pointer (&fb) : NULL;
|
||||
|
||||
return wp_spa_json_ref (s);
|
||||
}
|
||||
|
||||
/*!
|
||||
* This is a convenient function to access a JSON value from an object
|
||||
* section in the configuration. If the section is an array, or the key does
|
||||
* not exist in the object section, it will return the passed fallback value.
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
* \param section the section name
|
||||
* \param key the key name
|
||||
* \param fallback (transfer full)(nullable): the fallback value
|
||||
* \returns (transfer full): the JSON value of the section's key if it exists,
|
||||
* or the passed fallback value otherwise
|
||||
*/
|
||||
WpSpaJson *
|
||||
wp_conf_get_value (WpConf *self, const gchar *section, const gchar *key,
|
||||
WpSpaJson *fallback)
|
||||
{
|
||||
g_autoptr (WpSpaJson) s = NULL;
|
||||
g_autoptr (WpSpaJson) fb = fallback;
|
||||
WpSpaJson *v;
|
||||
|
||||
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
||||
g_return_val_if_fail (section, NULL);
|
||||
g_return_val_if_fail (key, NULL);
|
||||
|
||||
s = wp_conf_get_section (self, section, NULL);
|
||||
if (!s)
|
||||
goto return_fallback;
|
||||
|
||||
if (!wp_spa_json_is_object (s)) {
|
||||
wp_warning_object (self,
|
||||
"Cannot get JSON key %s from %s as section is not an JSON object",
|
||||
key, section);
|
||||
goto return_fallback;
|
||||
}
|
||||
|
||||
if (wp_spa_json_object_get (s, key, "J", &v, NULL))
|
||||
return v;
|
||||
|
||||
return_fallback:
|
||||
return fb ? g_steal_pointer (&fb) : NULL;
|
||||
return ensure_merged_section (self, section);
|
||||
}
|
||||
|
||||
/*!
|
||||
* This is a convenient function to access a boolean value from an object
|
||||
* section in the configuration. If the section is an array, or the key does
|
||||
* not exist in the object section, it will return the passed fallback value.
|
||||
* \brief Updates the given properties with the values of a specific section
|
||||
* from the configuration.
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
* \param section the section name
|
||||
* \param key the key name
|
||||
* \param fallback the fallback value
|
||||
* \returns the boolean value of the section's key if it exists and could be
|
||||
* parsed, or the passed fallback value otherwise
|
||||
*/
|
||||
gboolean
|
||||
wp_conf_get_value_boolean (WpConf *self, const gchar *section,
|
||||
const gchar *key, gboolean fallback)
|
||||
{
|
||||
g_autoptr (WpSpaJson) s = NULL;
|
||||
gboolean v;
|
||||
|
||||
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
|
||||
g_return_val_if_fail (section, FALSE);
|
||||
g_return_val_if_fail (key, FALSE);
|
||||
|
||||
s = wp_conf_get_section (self, section, NULL);
|
||||
if (!s)
|
||||
return fallback;
|
||||
|
||||
if (!wp_spa_json_is_object (s)) {
|
||||
wp_warning_object (self,
|
||||
"Cannot get boolean key %s from %s as section is not an JSON object",
|
||||
key, section);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return wp_spa_json_object_get (s, key, "b", &v, NULL) ? v : fallback;
|
||||
}
|
||||
|
||||
/*!
|
||||
* This is a convenient function to access a int value from an object
|
||||
* section in the configuration. If the section is an array, or the key does
|
||||
* not exist in the object section, it will return the passed fallback value.
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
* \param section the section name
|
||||
* \param key the key name
|
||||
* \param fallback the fallback value
|
||||
* \returns the int value of the section's key if it exists and could be
|
||||
* parsed, or the passed fallback value otherwise
|
||||
* \param props the properties to update
|
||||
* \returns the number of properties updated
|
||||
*/
|
||||
gint
|
||||
wp_conf_get_value_int (WpConf *self, const gchar *section,
|
||||
const gchar *key, gint fallback)
|
||||
wp_conf_section_update_props (WpConf *self, const gchar *section,
|
||||
WpProperties *props)
|
||||
{
|
||||
g_autoptr (WpSpaJson) s = NULL;
|
||||
gint v;
|
||||
g_autoptr (WpSpaJson) json = NULL;
|
||||
|
||||
g_return_val_if_fail (WP_IS_CONF (self), 0);
|
||||
g_return_val_if_fail (section, 0);
|
||||
g_return_val_if_fail (key, 0);
|
||||
g_return_val_if_fail (WP_IS_CONF (self), -1);
|
||||
g_return_val_if_fail (section, -1);
|
||||
g_return_val_if_fail (props, -1);
|
||||
|
||||
s = wp_conf_get_section (self, section, NULL);
|
||||
if (!s)
|
||||
return fallback;
|
||||
|
||||
if (!wp_spa_json_is_object (s)) {
|
||||
wp_warning_object (self,
|
||||
"Cannot get int key %s from %s as section is not an JSON object",
|
||||
key, section);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return wp_spa_json_object_get (s, key, "i", &v, NULL) ? v : fallback;
|
||||
json = wp_conf_get_section (self, section);
|
||||
if (!json)
|
||||
return 0;
|
||||
return wp_properties_update_from_json (props, json);
|
||||
}
|
||||
|
||||
#include "private/parse-conf-section.c"
|
||||
|
||||
/*!
|
||||
* This is a convenient function to access a float value from an object
|
||||
* section in the configuration. If the section is an array, or the key does
|
||||
* not exist in the object section, it will return the passed fallback value.
|
||||
* \brief Parses standard pw_context sections from \a conf
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
* \param section the section name
|
||||
* \param key the key name
|
||||
* \param fallback the fallback value
|
||||
* \returns the float value of the section's key if it exists and could be
|
||||
* parsed, or the passed fallback value otherwise
|
||||
* \param context the associated pw_context
|
||||
*/
|
||||
float
|
||||
wp_conf_get_value_float (WpConf *self, const gchar *section,
|
||||
const gchar *key, float fallback)
|
||||
void
|
||||
wp_conf_parse_pw_context_sections (WpConf * self, struct pw_context * context)
|
||||
{
|
||||
g_autoptr (WpSpaJson) s = NULL;
|
||||
float v;
|
||||
gint res;
|
||||
WpProperties *conf_wp;
|
||||
struct pw_properties *conf_pw;
|
||||
|
||||
g_return_val_if_fail (WP_IS_CONF (self), 0);
|
||||
g_return_val_if_fail (section, 0);
|
||||
g_return_val_if_fail (key, 0);
|
||||
g_return_if_fail (WP_IS_CONF (self));
|
||||
g_return_if_fail (context);
|
||||
|
||||
s = wp_conf_get_section (self, section, NULL);
|
||||
if (!s)
|
||||
return fallback;
|
||||
|
||||
if (!wp_spa_json_is_object (s)) {
|
||||
wp_warning_object (self,
|
||||
"Cannot get float key %s from %s as section is not an JSON object",
|
||||
key, section);
|
||||
return fallback;
|
||||
/* convert needed sections into a pipewire-style conf dictionary */
|
||||
conf_wp = wp_properties_new ("config.path", "wpconf", NULL);
|
||||
{
|
||||
g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.spa-libs");
|
||||
if (j) {
|
||||
g_autofree gchar *js = wp_spa_json_parse_string (j);
|
||||
wp_properties_set (conf_wp, "context.spa-libs", js);
|
||||
}
|
||||
}
|
||||
|
||||
return wp_spa_json_object_get (s, key, "f", &v, NULL) ? v : fallback;
|
||||
}
|
||||
|
||||
/*!
|
||||
* This is a convenient function to access a string value from an object
|
||||
* section in the configuration. If the section is an array, or the key does
|
||||
* not exist in the object section, it will return the passed fallback value.
|
||||
*
|
||||
* \ingroup wpconf
|
||||
* \param self the configuration
|
||||
* \param section the section name
|
||||
* \param key the key name
|
||||
* \param fallback (nullable): the fallback value
|
||||
* \returns (transfer full): the string value of the section's key if it exists
|
||||
* and could be parsed, or the passed fallback value otherwise
|
||||
*/
|
||||
gchar *
|
||||
wp_conf_get_value_string (WpConf *self, const gchar *section,
|
||||
const gchar *key, const gchar *fallback)
|
||||
{
|
||||
g_autoptr (WpSpaJson) s = NULL;
|
||||
gchar *v;
|
||||
|
||||
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
||||
g_return_val_if_fail (section, NULL);
|
||||
g_return_val_if_fail (key, NULL);
|
||||
|
||||
s = wp_conf_get_section (self, section, NULL);
|
||||
if (!s)
|
||||
goto return_fallback;
|
||||
|
||||
if (!wp_spa_json_is_object (s)) {
|
||||
wp_warning_object (self,
|
||||
"Cannot get string key %s from %s as section is not an JSON object",
|
||||
key, section);
|
||||
goto return_fallback;
|
||||
{
|
||||
g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.modules");
|
||||
if (j) {
|
||||
g_autofree gchar *js = wp_spa_json_parse_string (j);
|
||||
wp_properties_set (conf_wp, "context.modules", js);
|
||||
}
|
||||
}
|
||||
conf_pw = wp_properties_unref_and_take_pw_properties (conf_wp);
|
||||
|
||||
if (wp_spa_json_object_get (s, key, "s", &v, NULL))
|
||||
return v;
|
||||
/* parse sections */
|
||||
if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.spa-libs")) < 0)
|
||||
goto error;
|
||||
wp_info_object (self, "parsed %d context.spa-libs items", res);
|
||||
|
||||
return_fallback:
|
||||
return fallback ? g_strdup (fallback) : NULL;
|
||||
if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.modules")) < 0)
|
||||
goto error;
|
||||
if (res > 0)
|
||||
wp_info_object (self, "parsed %d context.modules items", res);
|
||||
else
|
||||
wp_warning_object (self, "no modules loaded from context.modules");
|
||||
|
||||
out:
|
||||
pw_properties_free (conf_pw);
|
||||
return;
|
||||
|
||||
error:
|
||||
wp_critical_object (self, "failed to parse pw_context sections: %s",
|
||||
spa_strerror (res));
|
||||
goto out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
struct pw_context;
|
||||
|
||||
/*!
|
||||
* \brief The WpConf GType
|
||||
* \ingroup wpconf
|
||||
|
|
@ -24,31 +26,34 @@ WP_API
|
|||
G_DECLARE_FINAL_TYPE (WpConf, wp_conf, WP, CONF, GObject)
|
||||
|
||||
WP_API
|
||||
WpConf * wp_conf_get_instance (WpCore * core);
|
||||
WpConf * wp_conf_new (const gchar * name, WpProperties * properties);
|
||||
|
||||
WP_API
|
||||
WpSpaJson * wp_conf_get_section (WpConf *self, const gchar *section,
|
||||
WpSpaJson *fallback);
|
||||
WpConf * wp_conf_new_open (const gchar * name, WpProperties * properties,
|
||||
GError ** error);
|
||||
|
||||
WP_API
|
||||
WpSpaJson *wp_conf_get_value (WpConf *self,
|
||||
const gchar *section, const gchar *key, WpSpaJson *fallback);
|
||||
gboolean wp_conf_open (WpConf * self, GError ** error);
|
||||
|
||||
WP_API
|
||||
gboolean wp_conf_get_value_boolean (WpConf *self,
|
||||
const gchar *section, const gchar *key, gboolean fallback);
|
||||
void wp_conf_close (WpConf * self);
|
||||
|
||||
WP_API
|
||||
gint wp_conf_get_value_int (WpConf *self,
|
||||
const gchar *section, const gchar *key, gint fallback);
|
||||
gboolean wp_conf_is_open (WpConf * self);
|
||||
|
||||
WP_API
|
||||
float wp_conf_get_value_float (WpConf *self,
|
||||
const gchar *section, const gchar *key, float fallback);
|
||||
const gchar * wp_conf_get_name (WpConf * self);
|
||||
|
||||
WP_API
|
||||
gchar *wp_conf_get_value_string (WpConf *self,
|
||||
const gchar *section, const gchar *key, const gchar *fallback);
|
||||
WpSpaJson * wp_conf_get_section (WpConf *self, const gchar *section);
|
||||
|
||||
WP_API
|
||||
gint wp_conf_section_update_props (WpConf * self, const gchar * section,
|
||||
WpProperties * props);
|
||||
|
||||
WP_API
|
||||
void wp_conf_parse_pw_context_sections (WpConf * self,
|
||||
struct pw_context * context);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
|
|
|
|||
252
lib/wp/core.c
252
lib/wp/core.c
|
|
@ -30,20 +30,23 @@ struct _WpLoopSource
|
|||
{
|
||||
GSource parent;
|
||||
struct pw_loop *loop;
|
||||
gboolean entered;
|
||||
};
|
||||
|
||||
static gboolean
|
||||
wp_loop_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
||||
{
|
||||
WpLoopSource *ls = WP_LOOP_SOURCE (s);
|
||||
int result;
|
||||
|
||||
wp_trace_boxed (G_TYPE_SOURCE, s, "entering pw main loop");
|
||||
if (!ls->entered) {
|
||||
wp_trace_boxed (G_TYPE_SOURCE, s, "entering pw main loop");
|
||||
pw_loop_enter (ls->loop);
|
||||
ls->entered = TRUE;
|
||||
g_source_set_ready_time (s, -1);
|
||||
}
|
||||
|
||||
pw_loop_enter (WP_LOOP_SOURCE(s)->loop);
|
||||
result = pw_loop_iterate (WP_LOOP_SOURCE(s)->loop, 0);
|
||||
pw_loop_leave (WP_LOOP_SOURCE(s)->loop);
|
||||
|
||||
wp_trace_boxed (G_TYPE_SOURCE, s, "leaving pw main loop");
|
||||
result = pw_loop_iterate (ls->loop, 0);
|
||||
|
||||
if (G_UNLIKELY (result < 0))
|
||||
wp_warning_boxed (G_TYPE_SOURCE, s,
|
||||
|
|
@ -55,7 +58,21 @@ wp_loop_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
|||
static void
|
||||
wp_loop_source_finalize (GSource * s)
|
||||
{
|
||||
pw_loop_destroy (WP_LOOP_SOURCE(s)->loop);
|
||||
WpLoopSource *ls = WP_LOOP_SOURCE (s);
|
||||
|
||||
wp_trace_boxed (G_TYPE_SOURCE, s, "finalize loop source");
|
||||
|
||||
/* Source should be left from the thread it was entered from.
|
||||
*
|
||||
* This puts additional restrictions to upper layers on how WpLoopSource (and
|
||||
* WpCore) can be used: they must be finalized from the GMainContext thread.
|
||||
*/
|
||||
if (ls->entered) {
|
||||
wp_trace_boxed (G_TYPE_SOURCE, s, "leaving pw main loop");
|
||||
pw_loop_leave (ls->loop);
|
||||
}
|
||||
|
||||
pw_loop_destroy (ls->loop);
|
||||
}
|
||||
|
||||
static GSourceFuncs source_funcs = {
|
||||
|
|
@ -75,6 +92,9 @@ wp_loop_source_new (void)
|
|||
pw_loop_get_fd (WP_LOOP_SOURCE(s)->loop),
|
||||
G_IO_IN | G_IO_ERR | G_IO_HUP);
|
||||
|
||||
/* dispatch immediately to enter the loop */
|
||||
g_source_set_ready_time (s, 0);
|
||||
|
||||
return (GSource *) s;
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +116,25 @@ wp_loop_source_new (void)
|
|||
* objects that appear in the registry, making them accessible through
|
||||
* the WpObjectManager API.
|
||||
*
|
||||
* The core is also responsible for loading components, which are defined in
|
||||
* the main configuration file. Components are loaded when
|
||||
* WP_CORE_FEATURE_COMPONENTS is activated.
|
||||
*
|
||||
* \b Configuration
|
||||
*
|
||||
* The main configuration file needs to be created and opened before the core
|
||||
* is created, using the WpConf API. It is then passed to the core as an
|
||||
* argument in the constructor.
|
||||
*
|
||||
* If a configuration file is not provided, the core will let the underlying
|
||||
* `pw_context` load its own configuration, based on the rules that apply to
|
||||
* all pipewire clients (e.g. it respects the `PIPEWIRE_CONFIG_NAME` environment
|
||||
* variable and loads "client.conf" as a last resort).
|
||||
*
|
||||
* If a configuration file is provided, the core does not let the underlying
|
||||
* `pw_context` load any configuration and instead uses the provided WpConf
|
||||
* object.
|
||||
*
|
||||
* \gproperties
|
||||
*
|
||||
* \gproperty{g-main-context, GMainContext *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
|
||||
|
|
@ -110,6 +149,9 @@ wp_loop_source_new (void)
|
|||
* \gproperty{pw-core, gpointer (struct pw_core *), G_PARAM_READABLE,
|
||||
* The pipewire core}
|
||||
*
|
||||
* \gproperty{conf, WpConf *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
|
||||
* The main configuration file}
|
||||
*
|
||||
* \gsignals
|
||||
*
|
||||
* \par connected
|
||||
|
|
@ -151,16 +193,25 @@ struct _WpCore
|
|||
struct spa_hook core_listener;
|
||||
struct spa_hook proxy_core_listener;
|
||||
|
||||
/* the main configuration file */
|
||||
WpConf *conf;
|
||||
|
||||
WpRegistry registry;
|
||||
GHashTable *async_tasks; // <int seq, GTask*>
|
||||
};
|
||||
|
||||
struct context_data {
|
||||
grefcount rc;
|
||||
GSource *loop_source;
|
||||
};
|
||||
|
||||
enum {
|
||||
PROP_0,
|
||||
PROP_G_MAIN_CONTEXT,
|
||||
PROP_PROPERTIES,
|
||||
PROP_PW_CONTEXT,
|
||||
PROP_PW_CORE,
|
||||
PROP_CONF,
|
||||
};
|
||||
|
||||
enum {
|
||||
|
|
@ -276,16 +327,32 @@ static void
|
|||
wp_core_constructed (GObject *object)
|
||||
{
|
||||
WpCore *self = WP_CORE (object);
|
||||
g_autoptr (GSource) source = NULL;
|
||||
|
||||
/* loop */
|
||||
source = wp_loop_source_new ();
|
||||
g_source_attach (source, self->g_main_context);
|
||||
|
||||
/* context */
|
||||
if (!self->pw_context) {
|
||||
struct pw_properties *p = NULL;
|
||||
const gchar *str = NULL;
|
||||
g_autoptr (GSource) source = wp_loop_source_new ();
|
||||
|
||||
/* use our own configuration file, if specified */
|
||||
if (self->conf) {
|
||||
wp_info_object (self, "using configuration file: %s",
|
||||
wp_conf_get_name (self->conf));
|
||||
|
||||
/* ensure we have our very own properties set,
|
||||
since we are going to modify it */
|
||||
self->properties = self->properties ?
|
||||
wp_properties_ensure_unique_owner (self->properties) :
|
||||
wp_properties_new_empty ();
|
||||
|
||||
/* load context.properties */
|
||||
wp_conf_section_update_props (self->conf, "context.properties",
|
||||
self->properties);
|
||||
|
||||
/* disable loading of a configuration file in pw_context */
|
||||
wp_properties_set (self->properties, PW_KEY_CONFIG_NAME, "null");
|
||||
wp_properties_set (self->properties, "context.modules.allow-empty", "true");
|
||||
}
|
||||
|
||||
/* properties are fully stored in the pw_context, no need to keep a copy */
|
||||
p = self->properties ?
|
||||
|
|
@ -293,26 +360,34 @@ wp_core_constructed (GObject *object)
|
|||
self->properties = NULL;
|
||||
|
||||
self->pw_context = pw_context_new (WP_LOOP_SOURCE(source)->loop, p,
|
||||
sizeof (grefcount));
|
||||
sizeof (struct context_data));
|
||||
g_return_if_fail (self->pw_context);
|
||||
|
||||
/* use the same config option as pipewire to set the log level */
|
||||
p = (struct pw_properties *) pw_context_get_properties (self->pw_context);
|
||||
if (!g_getenv("WIREPLUMBER_DEBUG") &&
|
||||
(str = pw_properties_get(p, "log.level")) != NULL) {
|
||||
if (!wp_log_set_global_level (str))
|
||||
if (!wp_log_set_level (str))
|
||||
wp_warning ("ignoring invalid log.level in config file: %s", str);
|
||||
}
|
||||
|
||||
/* parse pw_context specific configuration sections */
|
||||
if (self->conf)
|
||||
wp_conf_parse_pw_context_sections (self->conf, self->pw_context);
|
||||
|
||||
/* Init refcount */
|
||||
grefcount *rc = pw_context_get_user_data (self->pw_context);
|
||||
g_return_if_fail (rc);
|
||||
g_ref_count_init (rc);
|
||||
struct context_data *cd = pw_context_get_user_data (self->pw_context);
|
||||
g_return_if_fail (cd);
|
||||
g_ref_count_init (&cd->rc);
|
||||
cd->loop_source = g_source_ref (source);
|
||||
|
||||
/* Start source */
|
||||
g_source_attach (source, self->g_main_context);
|
||||
} else {
|
||||
/* Increase refcount */
|
||||
grefcount *rc = pw_context_get_user_data (self->pw_context);
|
||||
g_return_if_fail (rc);
|
||||
g_ref_count_inc (rc);
|
||||
struct context_data *cd = pw_context_get_user_data (self->pw_context);
|
||||
g_return_if_fail (cd);
|
||||
g_ref_count_inc (&cd->rc);
|
||||
}
|
||||
|
||||
G_OBJECT_CLASS (wp_core_parent_class)->constructed (object);
|
||||
|
|
@ -333,18 +408,24 @@ static void
|
|||
wp_core_finalize (GObject * obj)
|
||||
{
|
||||
WpCore *self = WP_CORE (obj);
|
||||
grefcount *rc = pw_context_get_user_data (self->pw_context);
|
||||
g_return_if_fail (rc);
|
||||
struct context_data *cd = pw_context_get_user_data (self->pw_context);
|
||||
g_return_if_fail (cd);
|
||||
|
||||
wp_core_disconnect (self);
|
||||
|
||||
/* Clear pw-context if refcount reaches 0 */
|
||||
if (g_ref_count_dec (rc))
|
||||
if (g_ref_count_dec (&cd->rc)) {
|
||||
GSource *source = cd->loop_source;
|
||||
|
||||
g_clear_pointer (&self->pw_context, pw_context_destroy);
|
||||
g_source_destroy (source);
|
||||
g_source_unref (source);
|
||||
}
|
||||
|
||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||
g_clear_pointer (&self->g_main_context, g_main_context_unref);
|
||||
g_clear_pointer (&self->async_tasks, g_hash_table_unref);
|
||||
g_clear_object (&self->conf);
|
||||
|
||||
wp_debug_object (self, "WpCore destroyed");
|
||||
|
||||
|
|
@ -370,6 +451,9 @@ wp_core_get_property (GObject * object, guint property_id,
|
|||
case PROP_PW_CORE:
|
||||
g_value_set_pointer (value, self->pw_core);
|
||||
break;
|
||||
case PROP_CONF:
|
||||
g_value_set_object (value, self->conf);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
break;
|
||||
|
|
@ -392,6 +476,9 @@ wp_core_set_property (GObject * object, guint property_id,
|
|||
case PROP_PW_CONTEXT:
|
||||
self->pw_context = g_value_get_pointer (value);
|
||||
break;
|
||||
case PROP_CONF:
|
||||
self->conf = g_value_dup_object (value);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
break;
|
||||
|
|
@ -447,6 +534,11 @@ on_components_loaded (WpCore * self, GAsyncResult *res,
|
|||
return;
|
||||
}
|
||||
|
||||
if (self->conf) {
|
||||
wp_info_object (self, "done loading components, closing conf file...");
|
||||
wp_conf_close (self->conf);
|
||||
}
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_COMPONENTS, 0);
|
||||
}
|
||||
|
||||
|
|
@ -541,6 +633,11 @@ wp_core_class_init (WpCoreClass * klass)
|
|||
g_param_spec_pointer ("pw-core", "pw-core", "The pipewire core",
|
||||
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (object_class, PROP_CONF,
|
||||
g_param_spec_object ("conf", "conf", "The main configuration file",
|
||||
WP_TYPE_CONF,
|
||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
signals[SIGNAL_CONNECTED] = g_signal_new ("connected",
|
||||
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
||||
G_TYPE_NONE, 0);
|
||||
|
|
@ -555,16 +652,20 @@ wp_core_class_init (WpCoreClass * klass)
|
|||
*
|
||||
* \ingroup wpcore
|
||||
* \param context (transfer none) (nullable): the GMainContext to use for events
|
||||
* \param properties (transfer full) (nullable): additional properties, which are
|
||||
* passed to pw_context_new() and pw_context_connect()
|
||||
* \param conf (transfer full) (nullable): the main configuration file
|
||||
* \param properties (transfer full) (nullable): additional properties, which
|
||||
* are also passed to pw_context_new() and pw_context_connect()
|
||||
* \returns (transfer full): a new WpCore
|
||||
*/
|
||||
WpCore *
|
||||
wp_core_new (GMainContext *context, WpProperties * properties)
|
||||
wp_core_new (GMainContext * context, WpConf * conf, WpProperties * properties)
|
||||
{
|
||||
g_autoptr (WpConf) c = conf;
|
||||
g_autoptr (WpProperties) props = properties;
|
||||
|
||||
return g_object_new (WP_TYPE_CORE,
|
||||
"g-main-context", context,
|
||||
"conf", conf,
|
||||
"properties", properties,
|
||||
"pw-context", NULL,
|
||||
NULL);
|
||||
|
|
@ -583,6 +684,7 @@ wp_core_clone (WpCore * self)
|
|||
return g_object_new (WP_TYPE_CORE,
|
||||
"core", self,
|
||||
"g-main-context", self->g_main_context,
|
||||
"conf", self->conf,
|
||||
"properties", self->properties,
|
||||
"pw-context", self->pw_context,
|
||||
NULL);
|
||||
|
|
@ -618,6 +720,20 @@ wp_core_get_export_core (WpCore * self)
|
|||
return wp_core_find_object (self, find_export_core, NULL);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the main configuration file of the core
|
||||
*
|
||||
* \ingroup wpcore
|
||||
* \param self the core
|
||||
* \returns (transfer full) (nullable): the main configuration file
|
||||
*/
|
||||
WpConf *
|
||||
wp_core_get_conf (WpCore * self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_CORE (self), NULL);
|
||||
return self->conf ? g_object_ref (self->conf) : NULL;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the GMainContext of the core
|
||||
*
|
||||
|
|
@ -735,6 +851,37 @@ wp_core_get_vm_type (WpCore *self)
|
|||
return res;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
wp_core_connect_internal (WpCore *self, int fd)
|
||||
{
|
||||
struct pw_properties *p = NULL;
|
||||
|
||||
/* Don't do anything if core is already connected */
|
||||
if (self->pw_core)
|
||||
return TRUE;
|
||||
|
||||
/* Connect */
|
||||
p = self->properties ? wp_properties_to_pw_properties (self->properties) : NULL;
|
||||
|
||||
if (fd == -1)
|
||||
self->pw_core = pw_context_connect (self->pw_context, p, 0);
|
||||
else
|
||||
self->pw_core = pw_context_connect_fd (self->pw_context, fd, p, 0);
|
||||
|
||||
if (!self->pw_core)
|
||||
return FALSE;
|
||||
|
||||
/* Add the core listeners */
|
||||
pw_core_add_listener (self->pw_core, &self->core_listener, &core_events, self);
|
||||
pw_proxy_add_listener((struct pw_proxy*)self->pw_core,
|
||||
&self->proxy_core_listener, &proxy_core_events, self);
|
||||
|
||||
/* Add the registry listener */
|
||||
wp_registry_attach (&self->registry, self->pw_core);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Connects this core to the PipeWire server.
|
||||
*
|
||||
|
|
@ -748,29 +895,31 @@ wp_core_get_vm_type (WpCore *self)
|
|||
gboolean
|
||||
wp_core_connect (WpCore *self)
|
||||
{
|
||||
struct pw_properties *p = NULL;
|
||||
|
||||
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
|
||||
|
||||
/* Don't do anything if core is already connected */
|
||||
if (self->pw_core)
|
||||
return TRUE;
|
||||
return wp_core_connect_internal (self, -1);
|
||||
}
|
||||
|
||||
/* Connect */
|
||||
p = self->properties ? wp_properties_to_pw_properties (self->properties) : NULL;
|
||||
self->pw_core = pw_context_connect (self->pw_context, p, 0);
|
||||
if (!self->pw_core)
|
||||
return FALSE;
|
||||
/*!
|
||||
* \brief Connects this core to the PipeWire server on the given socket.
|
||||
*
|
||||
* When connection succeeds, the WpCore \c "connected" signal is emitted.
|
||||
*
|
||||
* \ingroup wpcore
|
||||
* \param self the core
|
||||
* \param fd the connected socket to use, the socket will be closed
|
||||
* automatically on disconnect or error
|
||||
* \returns TRUE if the core is effectively connected or FALSE if
|
||||
* connection failed
|
||||
* \since 0.5.6
|
||||
*/
|
||||
gboolean
|
||||
wp_core_connect_fd (WpCore *self, int fd)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
|
||||
g_return_val_if_fail (fd > -1, FALSE);
|
||||
|
||||
/* Add the core listeners */
|
||||
pw_core_add_listener (self->pw_core, &self->core_listener, &core_events, self);
|
||||
pw_proxy_add_listener((struct pw_proxy*)self->pw_core,
|
||||
&self->proxy_core_listener, &proxy_core_events, self);
|
||||
|
||||
/* Add the registry listener */
|
||||
wp_registry_attach (&self->registry, self->pw_core);
|
||||
|
||||
return TRUE;
|
||||
return wp_core_connect_internal (self, fd);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
@ -989,8 +1138,8 @@ wp_core_update_properties (WpCore * self, WpProperties * updates)
|
|||
* \ingroup wpcore
|
||||
* \param self the core
|
||||
* \param source (out) (optional): the source
|
||||
* \param function (scope notified): the function to call
|
||||
* \param data (closure): data to pass to \a function
|
||||
* \param function (scope notified)(closure data)(destroy destroy): the function to call
|
||||
* \param data data to pass to \a function
|
||||
* \param destroy (nullable): a function to destroy \a data
|
||||
*/
|
||||
void
|
||||
|
|
@ -1054,8 +1203,8 @@ wp_core_idle_add_closure (WpCore * self, GSource **source, GClosure * closure)
|
|||
* \param self the core
|
||||
* \param source (out) (optional): the source
|
||||
* \param timeout_ms the timeout in milliseconds
|
||||
* \param function (scope notified): the function to call
|
||||
* \param data (closure): data to pass to \a function
|
||||
* \param function (scope notified)(closure data)(destroy destroy): the function to call
|
||||
* \param data data to pass to \a function
|
||||
* \param destroy (nullable): a function to destroy \a data
|
||||
*/
|
||||
void
|
||||
|
|
@ -1118,8 +1267,8 @@ wp_core_timeout_add_closure (WpCore * self, GSource **source, guint timeout_ms,
|
|||
* \ingroup wpcore
|
||||
* \param self the core
|
||||
* \param cancellable (nullable): a GCancellable to cancel the operation
|
||||
* \param callback (scope async): a function to call when the operation is done
|
||||
* \param user_data (closure): data to pass to \a callback
|
||||
* \param callback (scope async)(closure user_data): a function to call when the operation is done
|
||||
* \param user_data data to pass to \a callback
|
||||
* \returns TRUE if the sync operation was started, FALSE if an error
|
||||
* occurred before returning from this function
|
||||
*/
|
||||
|
|
@ -1229,6 +1378,7 @@ wp_core_sync_finish (WpCore * self, GAsyncResult * res, GError ** error)
|
|||
/*!
|
||||
* \brief Finds a registered object
|
||||
*
|
||||
* \ingroup wpcore
|
||||
* \param self the core
|
||||
* \param func (scope call): a function that takes the object being searched
|
||||
* as the first argument and \a data as the second. it should return TRUE if
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "object.h"
|
||||
#include "properties.h"
|
||||
#include "spa-json.h"
|
||||
#include "conf.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
|
|
@ -41,7 +42,8 @@ G_DECLARE_FINAL_TYPE (WpCore, wp_core, WP, CORE, WpObject)
|
|||
/* Basic */
|
||||
|
||||
WP_API
|
||||
WpCore * wp_core_new (GMainContext *context, WpProperties * properties);
|
||||
WpCore * wp_core_new (GMainContext * context, WpConf * conf,
|
||||
WpProperties * properties);
|
||||
|
||||
WP_API
|
||||
WpCore * wp_core_clone (WpCore * self);
|
||||
|
|
@ -49,6 +51,9 @@ WpCore * wp_core_clone (WpCore * self);
|
|||
WP_API
|
||||
WpCore * wp_core_get_export_core (WpCore * self);
|
||||
|
||||
WP_API
|
||||
WpConf * wp_core_get_conf (WpCore * self);
|
||||
|
||||
WP_API
|
||||
GMainContext * wp_core_get_g_main_context (WpCore * self);
|
||||
|
||||
|
|
@ -66,6 +71,9 @@ gchar *wp_core_get_vm_type (WpCore *self);
|
|||
WP_API
|
||||
gboolean wp_core_connect (WpCore *self);
|
||||
|
||||
WP_API
|
||||
gboolean wp_core_connect_fd (WpCore *self, int fd);
|
||||
|
||||
WP_API
|
||||
void wp_core_disconnect (WpCore *self);
|
||||
|
||||
|
|
|
|||
141
lib/wp/device.c
141
lib/wp/device.c
|
|
@ -199,6 +199,7 @@ struct _WpSpaDevice
|
|||
struct spa_hook listener;
|
||||
WpProperties *properties;
|
||||
GPtrArray *managed_objs;
|
||||
GPtrArray *pending_obj_config;
|
||||
};
|
||||
|
||||
enum {
|
||||
|
|
@ -225,11 +226,19 @@ object_unref_safe (gpointer object)
|
|||
g_object_unref (object);
|
||||
}
|
||||
|
||||
static void
|
||||
pod_unref_safe (gpointer object)
|
||||
{
|
||||
if (object)
|
||||
wp_spa_pod_unref (object);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_spa_device_init (WpSpaDevice * self)
|
||||
{
|
||||
self->properties = wp_properties_new_empty ();
|
||||
self->managed_objs = g_ptr_array_new_with_free_func (object_unref_safe);
|
||||
self->pending_obj_config = g_ptr_array_new_with_free_func (pod_unref_safe);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -262,6 +271,7 @@ wp_spa_device_finalize (GObject * object)
|
|||
g_clear_pointer (&self->handle, pw_unload_spa_handle);
|
||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||
g_clear_pointer (&self->managed_objs, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->pending_obj_config, g_ptr_array_unref);
|
||||
|
||||
G_OBJECT_CLASS (wp_spa_device_parent_class)->finalize (object);
|
||||
}
|
||||
|
|
@ -313,8 +323,8 @@ spa_device_event_info (void *data, const struct spa_device_info *info)
|
|||
WpSpaDevice *self = WP_SPA_DEVICE (data);
|
||||
|
||||
/*
|
||||
* This is emited syncrhonously at the time we add the listener and
|
||||
* before object_info is emited. It gives us additional properties
|
||||
* This is emitted synchronously at the time we add the listener and
|
||||
* before object_info is emitted. It gives us additional properties
|
||||
* about the device, like the "api.alsa.card.*" ones that are not
|
||||
* set by the monitor
|
||||
*/
|
||||
|
|
@ -322,6 +332,67 @@ spa_device_event_info (void *data, const struct spa_device_info *info)
|
|||
wp_properties_update_from_dict (self->properties, info->props);
|
||||
}
|
||||
|
||||
static WpSpaPod *
|
||||
pending_obj_config_pop (WpSpaDevice *self, guint32 id)
|
||||
{
|
||||
if (id < self->pending_obj_config->len)
|
||||
return g_steal_pointer (&g_ptr_array_index (self->pending_obj_config, id));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
pending_obj_config_set (WpSpaDevice *self, guint32 id, WpSpaPod *props)
|
||||
{
|
||||
if (id >= self->pending_obj_config->len)
|
||||
g_ptr_array_set_size (self->pending_obj_config, id + 1);
|
||||
|
||||
gpointer *ptr = &g_ptr_array_index (self->pending_obj_config, id);
|
||||
pod_unref_safe (*ptr);
|
||||
*ptr = props;
|
||||
}
|
||||
|
||||
static void
|
||||
append_props (WpSpaPodBuilder *b, WpSpaPod *props, GHashTable *used)
|
||||
{
|
||||
g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (props);
|
||||
GValue next = G_VALUE_INIT;
|
||||
|
||||
for (; wp_iterator_next (it, &next); g_value_unset (&next)) {
|
||||
WpSpaPod *p = g_value_get_boxed (&next);
|
||||
const char *key;
|
||||
g_autoptr (WpSpaPod) value = NULL;
|
||||
|
||||
if (!wp_spa_pod_get_property (p, &key, &value))
|
||||
continue;
|
||||
if (g_hash_table_contains(used, key))
|
||||
continue;
|
||||
|
||||
wp_spa_pod_builder_add_property (b, key);
|
||||
wp_spa_pod_builder_add_pod (b, value);
|
||||
|
||||
g_hash_table_add (used, (gpointer) g_strdup (key));
|
||||
}
|
||||
}
|
||||
|
||||
static WpSpaPod *
|
||||
merge_props (WpSpaPod *old_props, WpSpaPod *new_props)
|
||||
{
|
||||
g_autoptr (GHashTable) used = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
||||
g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_object (
|
||||
"Spa:Pod:Object:Param:Props", "Props");
|
||||
|
||||
if (new_props) {
|
||||
append_props (b, new_props, used);
|
||||
wp_spa_pod_unref (new_props);
|
||||
}
|
||||
if (old_props) {
|
||||
append_props (b, old_props, used);
|
||||
wp_spa_pod_unref (old_props);
|
||||
}
|
||||
|
||||
return wp_spa_pod_builder_end (b);
|
||||
}
|
||||
|
||||
static void
|
||||
spa_device_event_event (void *data, const struct spa_event *event)
|
||||
{
|
||||
|
|
@ -341,10 +412,18 @@ spa_device_event_event (void *data, const struct spa_event *event)
|
|||
NULL))
|
||||
child = wp_spa_device_get_managed_object (self, id);
|
||||
|
||||
if (child && !g_strcmp0 (type, "ObjectConfig") &&
|
||||
WP_IS_PIPEWIRE_OBJECT (child) && props) {
|
||||
wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (child), "Props", 0,
|
||||
g_steal_pointer (&props));
|
||||
if (!g_strcmp0 (type, "ObjectConfig") && props) {
|
||||
if (child && WP_IS_PIPEWIRE_OBJECT (child)) {
|
||||
wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (child), "Props", 0,
|
||||
g_steal_pointer (&props));
|
||||
} else if (!child) {
|
||||
/* Save Props set on ids pending for a managed object */
|
||||
WpSpaPod *pending_props = pending_obj_config_pop (self, id);
|
||||
if (pending_props) {
|
||||
pending_props = merge_props (pending_props, g_steal_pointer(&props));
|
||||
pending_obj_config_set (self, id, pending_props);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -359,12 +438,23 @@ spa_device_event_object_info (void *data, uint32_t id,
|
|||
g_autoptr (WpProperties) props = NULL;
|
||||
|
||||
type = spa_debug_type_short_name (info->type);
|
||||
props = wp_properties_new_wrap_dict (info->props);
|
||||
props = wp_properties_new_copy_dict (info->props);
|
||||
|
||||
wp_debug_object (self, "object info: id:%u type:%s factory:%s",
|
||||
id, type, info->factory_name);
|
||||
|
||||
if (id < self->managed_objs->len &&
|
||||
g_ptr_array_index (self->managed_objs, id) != NULL) {
|
||||
wp_debug_object (self, "object already exists, removing");
|
||||
g_signal_emit (self, spa_device_signals[SIGNAL_OBJECT_REMOVED], 0, id);
|
||||
wp_spa_device_store_managed_object (self, id, NULL);
|
||||
}
|
||||
|
||||
g_signal_emit (self, spa_device_signals[SIGNAL_CREATE_OBJECT], 0,
|
||||
id, type, info->factory_name, props);
|
||||
}
|
||||
else {
|
||||
wp_debug_object (self, "object removed: id:%u", id);
|
||||
g_signal_emit (self, spa_device_signals[SIGNAL_OBJECT_REMOVED], 0, id);
|
||||
wp_spa_device_store_managed_object (self, id, NULL);
|
||||
}
|
||||
|
|
@ -447,6 +537,7 @@ wp_spa_device_deactivate (WpObject * object, WpObjectFeatures features)
|
|||
WpSpaDevice *self = WP_SPA_DEVICE (object);
|
||||
spa_hook_remove (&self->listener);
|
||||
g_ptr_array_set_size (self->managed_objs, 0);
|
||||
g_ptr_array_set_size (self->pending_obj_config, 0);
|
||||
wp_object_update_features (object, 0, WP_SPA_DEVICE_FEATURE_ENABLED);
|
||||
}
|
||||
}
|
||||
|
|
@ -697,4 +788,40 @@ wp_spa_device_store_managed_object (WpSpaDevice * self, guint id,
|
|||
if (*ptr)
|
||||
g_object_unref (*ptr);
|
||||
*ptr = object;
|
||||
|
||||
/* Clear pending status, and set pending props if any */
|
||||
g_autoptr(WpSpaPod) props = pending_obj_config_pop (self, id);
|
||||
|
||||
if (props && object && WP_IS_PIPEWIRE_OBJECT (object)) {
|
||||
wp_trace_boxed (WP_TYPE_SPA_POD, props, "pending ObjectConfig, object %d", id);
|
||||
wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (object), "Props", 0,
|
||||
g_steal_pointer (&props));
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Marks a managed object id pending.
|
||||
*
|
||||
* When an object id is pending, Props from received ObjectConfig events
|
||||
* for the id are saved. When \ref wp_spa_device_store_managed_object later sets
|
||||
* an object for the id, the saved Props are immediately set on the object and
|
||||
* pending status is cleared.
|
||||
*
|
||||
* If an object is already set for the id, this has no effect.
|
||||
*
|
||||
* \ingroup wpspadevice
|
||||
* \param self the spa device
|
||||
* \param id the (device-internal) id of the object
|
||||
*/
|
||||
void
|
||||
wp_spa_device_set_managed_pending (WpSpaDevice * self, guint id)
|
||||
{
|
||||
g_return_if_fail (WP_IS_SPA_DEVICE (self));
|
||||
|
||||
g_autoptr (GObject) obj = wp_spa_device_get_managed_object (self, id);
|
||||
if (obj)
|
||||
return;
|
||||
|
||||
pending_obj_config_set (self, id,
|
||||
wp_spa_pod_new_object ("Spa:Pod:Object:Param:Props", "Props", NULL));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ WP_API
|
|||
void wp_spa_device_store_managed_object (WpSpaDevice * self, guint id,
|
||||
GObject * object);
|
||||
|
||||
WP_API
|
||||
void wp_spa_device_set_managed_pending (WpSpaDevice * self, guint id);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -15,6 +15,161 @@
|
|||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")
|
||||
|
||||
typedef struct _HookData HookData;
|
||||
struct _HookData
|
||||
{
|
||||
struct spa_list link;
|
||||
WpEventHook *hook;
|
||||
GPtrArray *dependencies;
|
||||
};
|
||||
|
||||
static inline HookData *
|
||||
hook_data_new (WpEventHook * hook)
|
||||
{
|
||||
HookData *hook_data = g_new0 (HookData, 1);
|
||||
spa_list_init (&hook_data->link);
|
||||
hook_data->hook = g_object_ref (hook);
|
||||
hook_data->dependencies = g_ptr_array_new ();
|
||||
return hook_data;
|
||||
}
|
||||
|
||||
static void
|
||||
hook_data_free (HookData *self)
|
||||
{
|
||||
g_clear_object (&self->hook);
|
||||
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
|
||||
g_free (self);
|
||||
}
|
||||
|
||||
static inline void
|
||||
record_dependency (struct spa_list *list, const gchar *target,
|
||||
const gchar *dependency)
|
||||
{
|
||||
HookData *hook_data;
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
|
||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline gboolean
|
||||
hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
||||
{
|
||||
HookData *hook_data;
|
||||
if (!spa_list_is_empty (list)) {
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
sort_hooks (GPtrArray *hooks)
|
||||
{
|
||||
struct spa_list collected, result, remaining;
|
||||
HookData *sorted_hook_data = NULL;
|
||||
|
||||
spa_list_init (&collected);
|
||||
spa_list_init (&result);
|
||||
spa_list_init (&remaining);
|
||||
|
||||
for (guint i = 0; i < hooks->len; i++) {
|
||||
WpEventHook *hook = g_ptr_array_index (hooks, i);
|
||||
HookData *hook_data = hook_data_new (hook);
|
||||
|
||||
/* record "after" dependencies directly */
|
||||
const gchar * const * strv =
|
||||
wp_event_hook_get_runs_after_hooks (hook_data->hook);
|
||||
while (strv && *strv) {
|
||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
|
||||
strv++;
|
||||
}
|
||||
|
||||
spa_list_append (&collected, &hook_data->link);
|
||||
}
|
||||
|
||||
if (!spa_list_is_empty (&collected)) {
|
||||
HookData *hook_data;
|
||||
|
||||
/* convert "before" dependencies into "after" dependencies */
|
||||
spa_list_for_each (hook_data, &collected, link) {
|
||||
const gchar * const * strv =
|
||||
wp_event_hook_get_runs_before_hooks (hook_data->hook);
|
||||
while (strv && *strv) {
|
||||
/* record hook_data->hook as a dependency of the *strv hook */
|
||||
record_dependency (&collected, *strv,
|
||||
wp_event_hook_get_name (hook_data->hook));
|
||||
strv++;
|
||||
}
|
||||
}
|
||||
|
||||
/* sort */
|
||||
while (!spa_list_is_empty (&collected)) {
|
||||
gboolean made_progress = FALSE;
|
||||
|
||||
/* examine each hook to see if its dependencies are satisfied in the
|
||||
result list; if yes, then append it to the result too */
|
||||
spa_list_consume (hook_data, &collected, link) {
|
||||
guint deps_satisfied = 0;
|
||||
|
||||
spa_list_remove (&hook_data->link);
|
||||
|
||||
for (guint i = 0; i < hook_data->dependencies->len; i++) {
|
||||
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
|
||||
/* if the dependency is already in the sorted result list or if
|
||||
it doesn't exist at all, we consider it satisfied */
|
||||
if (hook_exists_in (dep, &result) ||
|
||||
!(hook_exists_in (dep, &collected) ||
|
||||
hook_exists_in (dep, &remaining))) {
|
||||
deps_satisfied++;
|
||||
}
|
||||
}
|
||||
|
||||
if (deps_satisfied == hook_data->dependencies->len) {
|
||||
spa_list_append (&result, &hook_data->link);
|
||||
made_progress = TRUE;
|
||||
} else {
|
||||
spa_list_append (&remaining, &hook_data->link);
|
||||
}
|
||||
}
|
||||
|
||||
if (made_progress) {
|
||||
/* run again with the remaining hooks */
|
||||
spa_list_insert_list (&collected, &remaining);
|
||||
spa_list_init (&remaining);
|
||||
}
|
||||
else if (!spa_list_is_empty (&remaining)) {
|
||||
/* if we did not make any progress towards growing the result list,
|
||||
it means the dependencies cannot be satisfied because of circles */
|
||||
spa_list_consume (hook_data, &result, link) {
|
||||
spa_list_remove (&hook_data->link);
|
||||
hook_data_free (hook_data);
|
||||
}
|
||||
spa_list_consume (hook_data, &remaining, link) {
|
||||
spa_list_remove (&hook_data->link);
|
||||
hook_data_free (hook_data);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* clear hooks and add the sorted ones */
|
||||
g_ptr_array_set_size (hooks, 0);
|
||||
spa_list_consume (sorted_hook_data, &result, link) {
|
||||
spa_list_remove (&sorted_hook_data->link);
|
||||
g_ptr_array_add (hooks, g_object_ref (sorted_hook_data->hook));
|
||||
hook_data_free (sorted_hook_data);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
typedef struct _EventData EventData;
|
||||
struct _EventData
|
||||
{
|
||||
|
|
@ -49,7 +204,8 @@ struct _WpEventDispatcher
|
|||
GObject parent;
|
||||
|
||||
GWeakRef core;
|
||||
GPtrArray *hooks; /* registered hooks */
|
||||
GHashTable *defined_hooks; /* registered hooks for defined events */
|
||||
GPtrArray *undefined_hooks; /* registered hooks for undefined events */
|
||||
GSource *source; /* the event loop source */
|
||||
GList *events; /* the events stack */
|
||||
struct spa_system *system;
|
||||
|
|
@ -104,7 +260,7 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
|||
|
||||
/* get the highest priority event */
|
||||
GList *levent = g_list_first (d->events);
|
||||
while (levent) {
|
||||
if (levent) {
|
||||
EventData *event_data = (EventData *) (levent->data);
|
||||
WpEvent *event = event_data->event;
|
||||
GCancellable *cancellable = wp_event_get_cancellable (event);
|
||||
|
|
@ -144,6 +300,8 @@ wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
|
|||
|
||||
/* get the next event */
|
||||
levent = g_list_first (d->events);
|
||||
if (levent && !((EventData *) levent->data)->current_hook_in_async)
|
||||
spa_system_eventfd_write (d->system, d->eventfd, 1);
|
||||
}
|
||||
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
|
@ -160,7 +318,9 @@ static void
|
|||
wp_event_dispatcher_init (WpEventDispatcher * self)
|
||||
{
|
||||
g_weak_ref_init (&self->core, NULL);
|
||||
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
self->defined_hooks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
|
||||
(GDestroyNotify)g_ptr_array_unref);
|
||||
self->undefined_hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
|
||||
self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
|
||||
((WpEventSource *) self->source)->dispatcher = self;
|
||||
|
|
@ -184,7 +344,8 @@ wp_event_dispatcher_finalize (GObject * object)
|
|||
|
||||
close (self->eventfd);
|
||||
|
||||
g_clear_pointer (&self->hooks, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->defined_hooks, g_hash_table_unref);
|
||||
g_clear_pointer (&self->undefined_hooks, g_ptr_array_unref);
|
||||
g_weak_ref_clear (&self->core);
|
||||
|
||||
G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
|
||||
|
|
@ -264,7 +425,7 @@ wp_event_dispatcher_push_event (WpEventDispatcher * self, WpEvent * event)
|
|||
|
||||
self->events = g_list_insert_sorted (self->events, event_data,
|
||||
(GCompareFunc) event_cmp_func);
|
||||
wp_trace_object (self, "pushed event (%s)", wp_event_get_name (event));
|
||||
wp_debug_object (self, "pushed event (%s)", wp_event_get_name (event));
|
||||
|
||||
/* wakeup the GSource */
|
||||
spa_system_eventfd_write (self->system, self->eventfd, 1);
|
||||
|
|
@ -284,6 +445,10 @@ void
|
|||
wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
||||
WpEventHook * hook)
|
||||
{
|
||||
g_autoptr (GPtrArray) event_types = NULL;
|
||||
gboolean is_defined = FALSE;
|
||||
const gchar *hook_name;
|
||||
|
||||
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
||||
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
||||
|
||||
|
|
@ -292,12 +457,79 @@ wp_event_dispatcher_register_hook (WpEventDispatcher * self,
|
|||
g_return_if_fail (already_registered_dispatcher == NULL);
|
||||
|
||||
wp_event_hook_set_dispatcher (hook, self);
|
||||
g_ptr_array_add (self->hooks, g_object_ref (hook));
|
||||
|
||||
/* Register the event hook in the defined hooks table if it is defined */
|
||||
hook_name = wp_event_hook_get_name (hook);
|
||||
event_types = wp_event_hook_get_matching_event_types (hook);
|
||||
if (event_types) {
|
||||
for (guint i = 0; i < event_types->len; i++) {
|
||||
const gchar *event_type = g_ptr_array_index (event_types, i);
|
||||
GPtrArray *hooks;
|
||||
|
||||
wp_debug_object (self, "Registering hook %s for defined event type %s",
|
||||
hook_name, event_type);
|
||||
|
||||
/* Check if the event type was registered in the hash table */
|
||||
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
|
||||
if (hooks) {
|
||||
g_ptr_array_add (hooks, g_object_ref (hook));
|
||||
if (!sort_hooks (hooks))
|
||||
goto sort_error;
|
||||
} else {
|
||||
GPtrArray *new_hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
/* Add undefined hooks */
|
||||
for (guint i = 0; i < self->undefined_hooks->len; i++) {
|
||||
WpEventHook *uh = g_ptr_array_index (self->undefined_hooks, i);
|
||||
g_ptr_array_add (new_hooks, g_object_ref (uh));
|
||||
}
|
||||
/* Add current hook */
|
||||
g_ptr_array_add (new_hooks, g_object_ref (hook));
|
||||
g_hash_table_insert (self->defined_hooks, g_strdup (event_type),
|
||||
new_hooks);
|
||||
if (!sort_hooks (new_hooks))
|
||||
goto sort_error;
|
||||
}
|
||||
|
||||
is_defined = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise just register it as undefined hook */
|
||||
if (!is_defined) {
|
||||
GHashTableIter iter;
|
||||
gpointer value;
|
||||
|
||||
wp_debug_object (self, "Registering hook %s for undefined event types",
|
||||
hook_name);
|
||||
|
||||
/* Add it to the defined hooks table */
|
||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
||||
GPtrArray *defined_hooks = value;
|
||||
g_ptr_array_add (defined_hooks, g_object_ref (hook));
|
||||
if (!sort_hooks (defined_hooks))
|
||||
goto sort_error;
|
||||
}
|
||||
|
||||
/* Add it to the undefined hooks */
|
||||
g_ptr_array_add (self->undefined_hooks, g_object_ref (hook));
|
||||
if (!sort_hooks (self->undefined_hooks))
|
||||
goto sort_error;
|
||||
}
|
||||
|
||||
wp_info_object (self, "Registered hook %s successfully", hook_name);
|
||||
return;
|
||||
|
||||
sort_error:
|
||||
/* Unregister hook */
|
||||
wp_event_dispatcher_unregister_hook (self, hook);
|
||||
wp_warning_object (self,
|
||||
"Could not register hook %s because of circular dependencies", hook_name);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Unregisters an event hook
|
||||
* \ingroup wpeventdispacher
|
||||
* \ingroup wpeventdispatcher
|
||||
*
|
||||
* \param self the event dispatcher
|
||||
* \param hook (transfer none): the hook to unregister
|
||||
|
|
@ -306,6 +538,9 @@ void
|
|||
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
||||
WpEventHook * hook)
|
||||
{
|
||||
GHashTableIter iter;
|
||||
gpointer value;
|
||||
|
||||
g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
|
||||
g_return_if_fail (WP_IS_EVENT_HOOK (hook));
|
||||
|
||||
|
|
@ -314,11 +549,29 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
|||
g_return_if_fail (already_registered_dispatcher == self);
|
||||
|
||||
wp_event_hook_set_dispatcher (hook, NULL);
|
||||
g_ptr_array_remove_fast (self->hooks, hook);
|
||||
|
||||
/* Remove hook from defined table and undefined list */
|
||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
||||
GPtrArray *defined_hooks = value;
|
||||
g_ptr_array_remove (defined_hooks, hook);
|
||||
}
|
||||
g_ptr_array_remove (self->undefined_hooks, hook);
|
||||
}
|
||||
|
||||
static void
|
||||
add_unique (GPtrArray *array, WpEventHook * hook)
|
||||
{
|
||||
for (guint i = 0; i < array->len; i++)
|
||||
if (g_ptr_array_index (array, i) == hook)
|
||||
return;
|
||||
g_ptr_array_add (array, g_object_ref (hook));
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to iterate over all the registered hooks
|
||||
* \deprecated Use \ref wp_event_dispatcher_new_hooks_for_event_type_iterator
|
||||
* instead.
|
||||
* \ingroup wpeventdispatcher
|
||||
*
|
||||
* \param self the event dispatcher
|
||||
|
|
@ -327,7 +580,56 @@ wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
|||
WpIterator *
|
||||
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
||||
{
|
||||
GPtrArray *items =
|
||||
g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL);
|
||||
GPtrArray *items = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
GHashTableIter iter;
|
||||
gpointer value;
|
||||
|
||||
/* Add all defined hooks */
|
||||
g_hash_table_iter_init (&iter, self->defined_hooks);
|
||||
while (g_hash_table_iter_next (&iter, NULL, &value)) {
|
||||
GPtrArray *hooks = value;
|
||||
for (guint i = 0; i < hooks->len; i++) {
|
||||
WpEventHook *hook = g_ptr_array_index (hooks, i);
|
||||
add_unique (items, hook);
|
||||
}
|
||||
}
|
||||
|
||||
/* Add all undefined hooks */
|
||||
for (guint i = 0; i < self->undefined_hooks->len; i++) {
|
||||
WpEventHook *hook = g_ptr_array_index (self->undefined_hooks, i);
|
||||
add_unique (items, hook);
|
||||
}
|
||||
|
||||
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to iterate over the registered hooks for a
|
||||
* particular event type.
|
||||
* \ingroup wpeventdispatcher
|
||||
*
|
||||
* \param self the event dispatcher
|
||||
* \param event_type the event type
|
||||
* \return (transfer full): a new iterator
|
||||
* \since 0.5.13
|
||||
*/
|
||||
WpIterator *
|
||||
wp_event_dispatcher_new_hooks_for_event_type_iterator (
|
||||
WpEventDispatcher * self, const gchar *event_type)
|
||||
{
|
||||
GPtrArray *items;
|
||||
GPtrArray *hooks;
|
||||
|
||||
hooks = g_hash_table_lookup (self->defined_hooks, event_type);
|
||||
if (hooks) {
|
||||
wp_debug_object (self, "Using %d defined hooks for event type %s",
|
||||
hooks->len, event_type);
|
||||
} else {
|
||||
hooks = self->undefined_hooks;
|
||||
wp_debug_object (self, "Using %d undefined hooks for event type %s",
|
||||
hooks->len, event_type);
|
||||
}
|
||||
|
||||
items = g_ptr_array_copy (hooks, (GCopyFunc) g_object_ref, NULL);
|
||||
return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,12 @@ void wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
|
|||
WpEventHook * hook);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self);
|
||||
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
|
||||
G_GNUC_DEPRECATED_FOR (wp_event_dispatcher_new_hooks_for_event_type_iterator);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_event_dispatcher_new_hooks_for_event_type_iterator (
|
||||
WpEventDispatcher * self, const gchar *event_type);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ wp_event_hook_get_runs_after_hooks (WpEventHook * self)
|
|||
/*!
|
||||
* \brief Returns the associated event dispatcher
|
||||
*
|
||||
* \private
|
||||
* \ingroup wpeventhook
|
||||
* \param self the event hook
|
||||
* \return (transfer full)(nullable): the event dispatcher on which this hook
|
||||
|
|
@ -197,6 +198,15 @@ wp_event_hook_get_dispatcher (WpEventHook * self)
|
|||
return g_weak_ref_get (&priv->dispatcher);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the associated event dispatcher
|
||||
*
|
||||
* \private
|
||||
* \ingroup wpeventhook
|
||||
* \param self the event hook
|
||||
* \param dispatcher (transfer none): the event dispatcher on which
|
||||
* this hook is registered
|
||||
*/
|
||||
void
|
||||
wp_event_hook_set_dispatcher (WpEventHook * self, WpEventDispatcher * dispatcher)
|
||||
{
|
||||
|
|
@ -229,9 +239,9 @@ wp_event_hook_runs_for_event (WpEventHook * self, WpEvent * event)
|
|||
* \param self the event hook
|
||||
* \param event the event that triggered the hook
|
||||
* \param cancellable (nullable): a GCancellable to cancel the async operation
|
||||
* \param callback (scope async): a callback to fire after execution of the hook
|
||||
* \param callback (scope async)(closure callback_data): a callback to fire after execution of the hook
|
||||
* has completed
|
||||
* \param callback_data (closure): data for the callback
|
||||
* \param callback_data data for the callback
|
||||
*/
|
||||
void
|
||||
wp_event_hook_run (WpEventHook * self,
|
||||
|
|
@ -244,6 +254,24 @@ wp_event_hook_run (WpEventHook * self,
|
|||
callback_data);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets all the matching event types for this hook if any.
|
||||
*
|
||||
* \ingroup wpeventhook
|
||||
* \param self the event hook
|
||||
* \returns (element-type gchar*) (transfer full) (nullable): the matching
|
||||
* event types for this hook if any.
|
||||
* \since 0.5.13
|
||||
*/
|
||||
GPtrArray *
|
||||
wp_event_hook_get_matching_event_types (WpEventHook * self)
|
||||
{
|
||||
g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
|
||||
g_return_val_if_fail (
|
||||
WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types, NULL);
|
||||
return WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types (self);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Finishes the async operation that was started by wp_event_hook_run()
|
||||
*
|
||||
|
|
@ -311,36 +339,63 @@ wp_interest_event_hook_runs_for_event (WpEventHook * hook, WpEvent * event)
|
|||
wp_interest_event_hook_get_instance_private (self);
|
||||
g_autoptr (WpProperties) properties = wp_event_get_properties (event);
|
||||
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
||||
GType gtype = subject ? G_OBJECT_TYPE (subject) : WP_TYPE_EVENT;
|
||||
guint i;
|
||||
WpObjectInterest *interest = NULL;
|
||||
WpInterestMatch match;
|
||||
|
||||
const unsigned int MATCH_ALL_PROPS = (WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
|
||||
WP_INTEREST_MATCH_PW_PROPERTIES |
|
||||
WP_INTEREST_MATCH_G_PROPERTIES);
|
||||
|
||||
for (i = 0; i < priv->interests->len; i++) {
|
||||
interest = g_ptr_array_index (priv->interests, i);
|
||||
match = wp_object_interest_matches_full (interest,
|
||||
WP_INTEREST_MATCH_FLAGS_CHECK_ALL,
|
||||
gtype, subject, properties, properties);
|
||||
|
||||
/* the interest may have a GType that matches the GType of the subject
|
||||
or it may have WP_TYPE_EVENT as its GType, in which case it will
|
||||
match any type of subject */
|
||||
if (match == WP_INTEREST_MATCH_ALL)
|
||||
return TRUE;
|
||||
else if (subject && (match & MATCH_ALL_PROPS) == MATCH_ALL_PROPS) {
|
||||
match = wp_object_interest_matches_full (interest, 0,
|
||||
WP_TYPE_EVENT, NULL, NULL, NULL);
|
||||
if (match & WP_INTEREST_MATCH_GTYPE)
|
||||
if (wp_object_interest_matches_full (interest,
|
||||
WP_INTEREST_MATCH_FLAGS_NONE,
|
||||
WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL)
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
add_unique (GPtrArray *array, const gchar * lookup)
|
||||
{
|
||||
for (guint i = 0; i < array->len; i++)
|
||||
if (g_str_equal (g_ptr_array_index (array, i), lookup))
|
||||
return;
|
||||
g_ptr_array_add (array, g_strdup (lookup));
|
||||
}
|
||||
|
||||
static GPtrArray *
|
||||
wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
|
||||
{
|
||||
WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
|
||||
WpInterestEventHookPrivate *priv =
|
||||
wp_interest_event_hook_get_instance_private (self);
|
||||
g_autoptr (GPtrArray) res = g_ptr_array_new_with_free_func (g_free);
|
||||
guint i;
|
||||
|
||||
for (i = 0; i < priv->interests->len; i++) {
|
||||
WpObjectInterest *interest = g_ptr_array_index (priv->interests, i);
|
||||
if (wp_object_interest_matches_full (interest, WP_INTEREST_MATCH_FLAGS_NONE,
|
||||
WP_TYPE_EVENT, NULL, NULL, NULL) & WP_INTEREST_MATCH_GTYPE) {
|
||||
g_autoptr (GPtrArray) values =
|
||||
wp_object_interest_find_defined_constraint_values (interest,
|
||||
WP_CONSTRAINT_TYPE_NONE, "event.type");
|
||||
if (!values || values->len == 0) {
|
||||
/* We always consider the hook undefined if it has at least one interest
|
||||
* without a defined 'event.type' constraint */
|
||||
return NULL;
|
||||
} else {
|
||||
for (guint j = 0; j < values->len; j++) {
|
||||
GVariant *v = g_ptr_array_index (values, j);
|
||||
if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING)) {
|
||||
const gchar *v_str = g_variant_get_string (v, NULL);
|
||||
add_unique (res, v_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return g_steal_pointer (&res);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
|
||||
{
|
||||
|
|
@ -349,6 +404,8 @@ wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
|
|||
|
||||
object_class->finalize = wp_interest_event_hook_finalize;
|
||||
hook_class->runs_for_event = wp_interest_event_hook_runs_for_event;
|
||||
hook_class->get_matching_event_types =
|
||||
wp_interest_event_hook_get_matching_event_types;
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
|||
|
|
@ -39,8 +39,10 @@ struct _WpEventHookClass
|
|||
|
||||
gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);
|
||||
|
||||
GPtrArray * (*get_matching_event_types) (WpEventHook *self);
|
||||
|
||||
/*< private >*/
|
||||
WP_PADDING(5)
|
||||
WP_PADDING(4)
|
||||
};
|
||||
|
||||
WP_API
|
||||
|
|
@ -67,6 +69,9 @@ void wp_event_hook_run (WpEventHook * self,
|
|||
WpEvent * event, GCancellable * cancellable,
|
||||
GAsyncReadyCallback callback, gpointer callback_data);
|
||||
|
||||
WP_API
|
||||
GPtrArray * wp_event_hook_get_matching_event_types (WpEventHook * self);
|
||||
|
||||
WP_API
|
||||
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
|
||||
GError ** error);
|
||||
|
|
|
|||
253
lib/wp/event.c
253
lib/wp/event.c
|
|
@ -17,37 +17,11 @@
|
|||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")
|
||||
|
||||
typedef struct _HookData HookData;
|
||||
struct _HookData
|
||||
{
|
||||
struct spa_list link;
|
||||
WpEventHook *hook;
|
||||
GPtrArray *dependencies;
|
||||
};
|
||||
|
||||
static inline HookData *
|
||||
hook_data_new (WpEventHook * hook)
|
||||
{
|
||||
HookData *hook_data = g_new0 (HookData, 1);
|
||||
spa_list_init (&hook_data->link);
|
||||
hook_data->hook = g_object_ref (hook);
|
||||
hook_data->dependencies = g_ptr_array_new ();
|
||||
return hook_data;
|
||||
}
|
||||
|
||||
static void
|
||||
hook_data_free (HookData *self)
|
||||
{
|
||||
g_clear_object (&self->hook);
|
||||
g_clear_pointer (&self->dependencies, g_ptr_array_unref);
|
||||
g_free (self);
|
||||
}
|
||||
|
||||
struct _WpEvent
|
||||
{
|
||||
grefcount ref;
|
||||
GData *datalist;
|
||||
struct spa_list hooks;
|
||||
GPtrArray *hooks;
|
||||
|
||||
/* immutable fields */
|
||||
gint priority;
|
||||
|
|
@ -96,7 +70,7 @@ wp_event_new (const gchar * type, gint priority, WpProperties * properties,
|
|||
WpEvent * self = g_new0 (WpEvent, 1);
|
||||
g_ref_count_init (&self->ref);
|
||||
g_datalist_init (&self->datalist);
|
||||
spa_list_init (&self->hooks);
|
||||
self->hooks = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
|
||||
self->priority = priority;
|
||||
self->properties = properties ?
|
||||
|
|
@ -155,11 +129,7 @@ wp_event_get_name(WpEvent *self)
|
|||
static void
|
||||
wp_event_free (WpEvent * self)
|
||||
{
|
||||
HookData *hook_data;
|
||||
spa_list_consume (hook_data, &self->hooks, link) {
|
||||
spa_list_remove (&hook_data->link);
|
||||
hook_data_free (hook_data);
|
||||
}
|
||||
g_clear_pointer (&self->hooks, g_ptr_array_unref);
|
||||
g_datalist_clear (&self->datalist);
|
||||
g_clear_pointer (&self->properties, wp_properties_unref);
|
||||
g_clear_object (&self->source);
|
||||
|
|
@ -316,33 +286,6 @@ wp_event_get_data (WpEvent * self, const gchar * key)
|
|||
return g_datalist_get_data (&self->datalist, key);
|
||||
}
|
||||
|
||||
static inline void
|
||||
record_dependency (struct spa_list *list, const gchar *target,
|
||||
const gchar *dependency)
|
||||
{
|
||||
HookData *hook_data;
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
|
||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline gboolean
|
||||
hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
||||
{
|
||||
HookData *hook_data;
|
||||
if (!spa_list_is_empty (list)) {
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Collects all the hooks registered in the \a dispatcher that run for
|
||||
* this \a event
|
||||
|
|
@ -355,188 +298,37 @@ hook_exists_in (const gchar *hook_name, struct spa_list *list)
|
|||
gboolean
|
||||
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
|
||||
{
|
||||
struct spa_list collected, result, remaining;
|
||||
g_autoptr (WpIterator) all_hooks = NULL;
|
||||
g_auto (GValue) value = G_VALUE_INIT;
|
||||
const gchar *event_type = NULL;
|
||||
|
||||
g_return_val_if_fail (event != NULL, FALSE);
|
||||
g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);
|
||||
|
||||
/* hooks already collected */
|
||||
if (!spa_list_is_empty (&event->hooks))
|
||||
return TRUE;
|
||||
/* Clear all current hooks */
|
||||
g_ptr_array_set_size (event->hooks, 0);
|
||||
|
||||
spa_list_init (&collected);
|
||||
spa_list_init (&result);
|
||||
spa_list_init (&remaining);
|
||||
/* Get the event type */
|
||||
event_type = wp_properties_get (event->properties, "event.type");
|
||||
wp_debug_object (dispatcher, "Collecting hooks for event %s with type %s",
|
||||
event->name, event_type);
|
||||
|
||||
/* collect hooks that run for this event */
|
||||
all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher);
|
||||
/* Collect hooks that run for this event */
|
||||
all_hooks = wp_event_dispatcher_new_hooks_for_event_type_iterator (dispatcher,
|
||||
event_type);
|
||||
while (wp_iterator_next (all_hooks, &value)) {
|
||||
WpEventHook *hook = g_value_get_object (&value);
|
||||
|
||||
if (wp_event_hook_runs_for_event (hook, event)) {
|
||||
HookData *hook_data = hook_data_new (hook);
|
||||
|
||||
/* record "after" dependencies directly */
|
||||
const gchar * const * strv =
|
||||
wp_event_hook_get_runs_after_hooks (hook_data->hook);
|
||||
while (strv && *strv) {
|
||||
g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
|
||||
strv++;
|
||||
}
|
||||
|
||||
spa_list_append (&collected, &hook_data->link);
|
||||
|
||||
wp_trace_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
|
||||
g_ptr_array_add (event->hooks, g_object_ref (hook));
|
||||
wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
|
||||
WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
|
||||
}
|
||||
|
||||
g_value_unset (&value);
|
||||
}
|
||||
|
||||
if (!spa_list_is_empty (&collected)) {
|
||||
HookData *hook_data;
|
||||
|
||||
/* convert "before" dependencies into "after" dependencies */
|
||||
spa_list_for_each (hook_data, &collected, link) {
|
||||
const gchar * const * strv =
|
||||
wp_event_hook_get_runs_before_hooks (hook_data->hook);
|
||||
while (strv && *strv) {
|
||||
/* record hook_data->hook as a dependency of the *strv hook */
|
||||
record_dependency (&collected, *strv,
|
||||
wp_event_hook_get_name (hook_data->hook));
|
||||
strv++;
|
||||
}
|
||||
}
|
||||
|
||||
/* sort */
|
||||
while (!spa_list_is_empty (&collected)) {
|
||||
gboolean made_progress = FALSE;
|
||||
|
||||
/* examine each hook to see if its dependencies are satisfied in the
|
||||
result list; if yes, then append it to the result too */
|
||||
spa_list_consume (hook_data, &collected, link) {
|
||||
guint deps_satisfied = 0;
|
||||
|
||||
spa_list_remove (&hook_data->link);
|
||||
|
||||
for (guint i = 0; i < hook_data->dependencies->len; i++) {
|
||||
const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
|
||||
/* if the dependency is already in the sorted result list or if
|
||||
it doesn't exist at all, we consider it satisfied */
|
||||
if (hook_exists_in (dep, &result) ||
|
||||
!(hook_exists_in (dep, &collected) ||
|
||||
hook_exists_in (dep, &remaining))) {
|
||||
deps_satisfied++;
|
||||
}
|
||||
}
|
||||
|
||||
if (deps_satisfied == hook_data->dependencies->len) {
|
||||
spa_list_append (&result, &hook_data->link);
|
||||
made_progress = TRUE;
|
||||
} else {
|
||||
spa_list_append (&remaining, &hook_data->link);
|
||||
}
|
||||
}
|
||||
|
||||
if (made_progress) {
|
||||
/* run again with the remaining hooks */
|
||||
spa_list_insert_list (&collected, &remaining);
|
||||
spa_list_init (&remaining);
|
||||
}
|
||||
else if (!spa_list_is_empty (&remaining)) {
|
||||
/* if we did not make any progress towards growing the result list,
|
||||
it means the dependencies cannot be satisfied because of circles */
|
||||
wp_critical_boxed (WP_TYPE_EVENT, event, "detected circular "
|
||||
"dependencies in the collected hooks!");
|
||||
|
||||
/* clean up */
|
||||
spa_list_consume (hook_data, &result, link) {
|
||||
spa_list_remove (&hook_data->link);
|
||||
hook_data_free (hook_data);
|
||||
}
|
||||
spa_list_consume (hook_data, &remaining, link) {
|
||||
spa_list_remove (&hook_data->link);
|
||||
hook_data_free (hook_data);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spa_list_insert_list (&event->hooks, &result);
|
||||
return !spa_list_is_empty (&event->hooks);
|
||||
return event->hooks->len > 0;
|
||||
}
|
||||
|
||||
struct event_hooks_iterator_data
|
||||
{
|
||||
WpEvent *event;
|
||||
HookData *cur;
|
||||
};
|
||||
|
||||
static void
|
||||
event_hooks_iterator_reset (WpIterator *it)
|
||||
{
|
||||
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
struct spa_list *list = &it_data->event->hooks;
|
||||
|
||||
if (!spa_list_is_empty (list))
|
||||
it_data->cur = spa_list_first (&it_data->event->hooks, HookData, link);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
event_hooks_iterator_next (WpIterator *it, GValue *item)
|
||||
{
|
||||
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
struct spa_list *list = &it_data->event->hooks;
|
||||
|
||||
if (!spa_list_is_empty (list) &&
|
||||
!spa_list_is_end (it_data->cur, list, link)) {
|
||||
g_value_init (item, WP_TYPE_EVENT_HOOK);
|
||||
g_value_set_object (item, it_data->cur->hook);
|
||||
it_data->cur = spa_list_next (it_data->cur, link);
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
event_hooks_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
|
||||
gpointer data)
|
||||
{
|
||||
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
struct spa_list *list = &it_data->event->hooks;
|
||||
HookData *hook_data;
|
||||
|
||||
if (!spa_list_is_empty (list)) {
|
||||
spa_list_for_each (hook_data, list, link) {
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
g_value_init (&item, WP_TYPE_EVENT_HOOK);
|
||||
g_value_set_object (&item, hook_data->hook);
|
||||
if (!func (&item, ret, data))
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
event_hooks_iterator_finalize (WpIterator *it)
|
||||
{
|
||||
struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
|
||||
wp_event_unref (it_data->event);
|
||||
}
|
||||
|
||||
static const WpIteratorMethods event_hooks_iterator_methods = {
|
||||
.version = WP_ITERATOR_METHODS_VERSION,
|
||||
.reset = event_hooks_iterator_reset,
|
||||
.next = event_hooks_iterator_next,
|
||||
.fold = event_hooks_iterator_fold,
|
||||
.finalize = event_hooks_iterator_finalize,
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator that iterates over all the hooks that were
|
||||
* collected by wp_event_collect_hooks()
|
||||
|
|
@ -547,15 +339,8 @@ static const WpIteratorMethods event_hooks_iterator_methods = {
|
|||
WpIterator *
|
||||
wp_event_new_hooks_iterator (WpEvent * event)
|
||||
{
|
||||
WpIterator *it = NULL;
|
||||
struct event_hooks_iterator_data *it_data;
|
||||
GPtrArray *hooks;
|
||||
hooks = g_ptr_array_copy (event->hooks, (GCopyFunc) g_object_ref, NULL);
|
||||
return wp_iterator_new_ptr_array (hooks, WP_TYPE_EVENT_HOOK);
|
||||
|
||||
g_return_val_if_fail (event != NULL, NULL);
|
||||
|
||||
it = wp_iterator_new (&event_hooks_iterator_methods,
|
||||
sizeof (struct event_hooks_iterator_data));
|
||||
it_data = wp_iterator_get_user_data (it);
|
||||
it_data->event = wp_event_ref (event);
|
||||
event_hooks_iterator_reset (it);
|
||||
return it;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,6 +250,20 @@ wp_global_proxy_destroyed (WpProxy * proxy)
|
|||
WpGlobalProxyPrivate *priv =
|
||||
wp_global_proxy_get_instance_private (self);
|
||||
|
||||
if (priv->global && priv->global->proxy &&
|
||||
(priv->global->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY)) {
|
||||
/* We can end up here as a result of _request_destroy() followed by
|
||||
* _deactivate(FEATURE_BOUND), where the latter triggers this callback
|
||||
* before _remove_global is processed.
|
||||
* If proxy is owned, it is gone now, so not much owned left.
|
||||
* self might be cleaned up soon, so this is a good time to remove
|
||||
* the non-refcounted backreference in global. If not done now, _dispose()
|
||||
* does not have a chance to cleanup (as the reference to global is gone).
|
||||
* If remove_global then comes in later, there is no more real work to
|
||||
* do when WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY is removed
|
||||
*/
|
||||
wp_global_rm_flag (priv->global, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
|
||||
}
|
||||
g_clear_pointer (&priv->global, wp_global_unref);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -178,9 +178,9 @@ wp_iterator_next (WpIterator *self, GValue *item)
|
|||
*
|
||||
* \ingroup wpiterator
|
||||
* \param self the iterator
|
||||
* \param func (scope call): the fold function
|
||||
* \param func (scope call)(closure data): the fold function
|
||||
* \param ret (inout): the accumulator data
|
||||
* \param data (closure): the user data
|
||||
* \param data the user data
|
||||
* \returns TRUE if all the items were processed, FALSE otherwise.
|
||||
*/
|
||||
gboolean
|
||||
|
|
@ -200,8 +200,8 @@ wp_iterator_fold (WpIterator *self, WpIteratorFoldFunc func, GValue *ret,
|
|||
*
|
||||
* \ingroup wpiterator
|
||||
* \param self the iterator
|
||||
* \param func (scope call): the foreach function
|
||||
* \param data (closure): the user data
|
||||
* \param func (scope call)(closure data): the foreach function
|
||||
* \param data the user data
|
||||
* \returns TRUE if all the items were processed, FALSE otherwise.
|
||||
*/
|
||||
gboolean
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ match_rules_cb (void *data, const char *location, const char *action,
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Matches the given properties against a set of rules descriped in JSON
|
||||
* \brief Matches the given properties against a set of rules described in JSON
|
||||
* and calls the given callback to perform actions on a successful match.
|
||||
*
|
||||
* The given JSON should be an array of objects, where each object has a
|
||||
|
|
@ -53,7 +53,7 @@ match_rules_cb (void *data, const char *location, const char *action,
|
|||
* {
|
||||
* matches = [
|
||||
* # any of the items in matches needs to match, if one does,
|
||||
* # actions are emited.
|
||||
* # actions are emitted.
|
||||
* {
|
||||
* # all keys must match the value. ! negates. ~ starts regex.
|
||||
* <key> = <value>
|
||||
|
|
@ -72,8 +72,8 @@ match_rules_cb (void *data, const char *location, const char *action,
|
|||
* \ingroup wpjsonutils
|
||||
* \param json a JSON array containing rules in the described format
|
||||
* \param match_props (transfer none): the properties to match against the rules
|
||||
* \param callback (scope call): a function to call for each action on a successful match
|
||||
* \param data (closure callback): data to be passed to \a callback
|
||||
* \param callback (scope call)(closure data): a function to call for each action on a successful match
|
||||
* \param data data to be passed to \a callback
|
||||
* \param error (out)(optional): the error that occurred, if any
|
||||
* \returns FALSE if an error occurred, TRUE otherwise
|
||||
*/
|
||||
|
|
@ -117,7 +117,7 @@ update_props_cb (gpointer data, const gchar * action, WpSpaJson * value,
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Matches the given properties against a set of rules descriped in JSON
|
||||
* \brief Matches the given properties against a set of rules described in JSON
|
||||
* and updates the properties if the rule actions include the "update-props"
|
||||
* action.
|
||||
*
|
||||
|
|
@ -211,12 +211,12 @@ merge_json_objects (WpSpaJson *a, WpSpaJson *b)
|
|||
g_return_val_if_fail (wp_iterator_next (it, &item), NULL);
|
||||
val = g_value_dup_boxed (&item);
|
||||
|
||||
if (!override &&
|
||||
if (!override && wp_spa_json_is_container (val) &&
|
||||
(wp_spa_json_object_get (a, key_str, "J", &j, NULL) ||
|
||||
wp_spa_json_object_get (a, override_key_str, "J", &j, NULL))) {
|
||||
g_autoptr (WpSpaJson) merged = wp_json_utils_merge_containers (j, val);
|
||||
if (!merged) {
|
||||
wp_warning ("skipping merge of %s as JSON values are not compatible",
|
||||
wp_warning ("skipping merge of %s as JSON values are not compatible containers",
|
||||
key_str);
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-link")
|
|||
* \code
|
||||
* void
|
||||
* state_changed_callback (WpLink * self,
|
||||
* WpLinkState * old_state,
|
||||
* WpLinkState * new_state,
|
||||
* WpLinkState old_state,
|
||||
* WpLinkState new_state,
|
||||
* gpointer user_data)
|
||||
* \endcode
|
||||
*
|
||||
|
|
|
|||
533
lib/wp/log.c
533
lib/wp/log.c
|
|
@ -18,8 +18,39 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-log")
|
|||
* \{
|
||||
*/
|
||||
/*!
|
||||
* \def WP_LOG_LEVEL_TRACE
|
||||
* \brief A custom GLib log level for trace messages (see GLogLevelFlags)
|
||||
* \def WP_DEFINE_LOCAL_LOG_TOPIC(name)
|
||||
* \brief Defines a static \em WpLogTopic* variable called \em WP_LOCAL_LOG_TOPIC
|
||||
*
|
||||
* The log topic is automatically initialized to the given topic \a name when
|
||||
* it is first used. The default logging macros expect this variable to be
|
||||
* defined, so it is a good coding practice in the WirePlumber codebase to
|
||||
* start all files at the top with:
|
||||
* \code
|
||||
* WP_DEFINE_LOCAL_LOG_TOPIC ("some-topic")
|
||||
* \endcode
|
||||
*
|
||||
* \param name The name of the log topic
|
||||
*/
|
||||
/*!
|
||||
* \def WP_LOG_TOPIC_STATIC(var, name)
|
||||
* \brief Defines a static \em WpLogTopic* variable called \a var with the given
|
||||
* topic \a name
|
||||
* \param var The name of the variable to define
|
||||
* \param name The name of the log topic
|
||||
*/
|
||||
/*!
|
||||
* \def WP_LOG_TOPIC(var, name)
|
||||
* \brief Defines a \em WpLogTopic* variable called \a var with the given
|
||||
* topic \a name. Unlike WP_LOG_TOPIC_STATIC(), the variable defined here is
|
||||
* not static, so it can be linked to by other object files.
|
||||
* \param var The name of the variable to define
|
||||
* \param name The name of the log topic
|
||||
*/
|
||||
/*!
|
||||
* \def WP_LOG_TOPIC_EXTERN(var)
|
||||
* \brief Declares an extern \em WpLogTopic* variable called \a var.
|
||||
* This variable is meant to be defined in a .c file with WP_LOG_TOPIC()
|
||||
* \param var The name of the variable to declare
|
||||
*/
|
||||
/*!
|
||||
* \def WP_OBJECT_FORMAT
|
||||
|
|
@ -176,24 +207,33 @@ static GString *spa_dbg_str = NULL;
|
|||
|
||||
#include <spa/debug/pod.h>
|
||||
|
||||
#define DEFAULT_LOG_LEVEL 4 /* MESSAGE */
|
||||
#define DEFAULT_LOG_LEVEL_FLAGS (G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR)
|
||||
|
||||
struct log_topic_pattern
|
||||
{
|
||||
GPatternSpec *spec;
|
||||
gchar *spec_str;
|
||||
gint log_level;
|
||||
};
|
||||
|
||||
static struct {
|
||||
gboolean use_color;
|
||||
gboolean output_is_journal;
|
||||
gboolean set_pw_log;
|
||||
gint global_log_level;
|
||||
GLogLevelFlags global_log_level_flags;
|
||||
struct log_topic_pattern *patterns;
|
||||
GPtrArray *log_topics;
|
||||
GMutex log_topics_lock;
|
||||
} log_state = {
|
||||
.use_color = FALSE,
|
||||
.output_is_journal = FALSE,
|
||||
.global_log_level = 4 /* MESSAGE */,
|
||||
.global_log_level_flags = G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR,
|
||||
.set_pw_log = FALSE,
|
||||
.global_log_level = DEFAULT_LOG_LEVEL,
|
||||
.global_log_level_flags = DEFAULT_LOG_LEVEL_FLAGS,
|
||||
.patterns = NULL,
|
||||
.log_topics = NULL,
|
||||
};
|
||||
|
||||
/* reference: https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit */
|
||||
|
|
@ -260,6 +300,8 @@ level_index_from_flags (GLogLevelFlags log_level)
|
|||
static G_GNUC_CONST inline GLogLevelFlags
|
||||
level_index_to_flag (gint lvl_index)
|
||||
{
|
||||
if (lvl_index < 0 || lvl_index >= (gint) G_N_ELEMENTS (log_level_info))
|
||||
return 0;
|
||||
return log_level_info [lvl_index].log_level_flags;
|
||||
}
|
||||
|
||||
|
|
@ -300,6 +342,8 @@ level_index_from_spa (gint spa_lvl, gboolean warn_to_notice)
|
|||
static G_GNUC_CONST inline gint
|
||||
level_index_to_spa (gint lvl_index)
|
||||
{
|
||||
if (lvl_index < 0 || lvl_index >= (gint) G_N_ELEMENTS (log_level_info))
|
||||
return 0;
|
||||
return log_level_info [lvl_index].spa_level;
|
||||
}
|
||||
|
||||
|
|
@ -325,106 +369,6 @@ level_index_from_string (const char *str, gint *lvl)
|
|||
return FALSE;
|
||||
}
|
||||
|
||||
/* private, called from wp_init() */
|
||||
void
|
||||
wp_log_init (gint flags)
|
||||
{
|
||||
const gchar *level_str;
|
||||
gint global_log_level = log_state.global_log_level;
|
||||
struct log_topic_pattern *patterns = NULL, *pttrn;
|
||||
gint n_tokens = 0;
|
||||
gchar **tokens = NULL;
|
||||
|
||||
level_str = g_getenv ("WIREPLUMBER_DEBUG");
|
||||
|
||||
log_state.use_color = g_log_writer_supports_color (fileno (stderr));
|
||||
log_state.output_is_journal = g_log_writer_is_journald (fileno (stderr));
|
||||
|
||||
if (level_str && level_str[0] != '\0') {
|
||||
/* [<glob>:]<level>,..., */
|
||||
tokens = pw_split_strv (level_str, ",", INT_MAX, &n_tokens);
|
||||
}
|
||||
|
||||
/* allocate enough space to hold all pattern specs */
|
||||
patterns = g_malloc_n ((n_tokens + 2), sizeof (struct log_topic_pattern));
|
||||
pttrn = patterns;
|
||||
if (!patterns)
|
||||
g_error ("unable to allocate space for %d log patterns", n_tokens + 2);
|
||||
|
||||
for (gint i = 0; i < n_tokens; i++) {
|
||||
gint n_tok;
|
||||
gchar **tok;
|
||||
gint lvl;
|
||||
|
||||
tok = pw_split_strv (tokens[i], ":", 2, &n_tok);
|
||||
if (n_tok == 2 && level_index_from_string (tok[1], &lvl)) {
|
||||
pttrn->spec = g_pattern_spec_new (tok[0]);
|
||||
pttrn->log_level = lvl;
|
||||
pttrn++;
|
||||
} else if (n_tok == 1 && level_index_from_string (tok[0], &lvl)) {
|
||||
global_log_level = lvl;
|
||||
} else {
|
||||
/* note that this is going to initialize the wp-log topic here */
|
||||
wp_warning ("Ignoring invalid format in WIREPLUMBER_DEBUG: '%s'",
|
||||
tokens[i]);
|
||||
}
|
||||
|
||||
pw_free_strv (tok);
|
||||
}
|
||||
|
||||
/* disable pipewire connection trace by default */
|
||||
pttrn->spec = g_pattern_spec_new ("conn.*");
|
||||
pttrn->log_level = 0;
|
||||
pttrn++;
|
||||
|
||||
/* terminate with NULL */
|
||||
pttrn->spec = NULL;
|
||||
pttrn->log_level = 0;
|
||||
|
||||
pw_free_strv (tokens);
|
||||
|
||||
log_state.patterns = patterns;
|
||||
log_state.global_log_level = global_log_level;
|
||||
log_state.global_log_level_flags =
|
||||
level_index_to_full_flags (global_log_level);
|
||||
|
||||
/* set the log level also on the spa_log */
|
||||
wp_spa_log_get_instance()->level = level_index_to_spa (global_log_level);
|
||||
|
||||
if (flags & WP_INIT_SET_GLIB_LOG)
|
||||
g_log_set_writer_func (wp_log_writer_default, NULL, NULL);
|
||||
|
||||
/* set PIPEWIRE_DEBUG and the spa_log interface that pipewire will use */
|
||||
if (flags & WP_INIT_SET_PW_LOG && !g_getenv ("WIREPLUMBER_NO_PW_LOG")) {
|
||||
/* always set PIPEWIRE_DEBUG for 2 reasons:
|
||||
* 1. to overwrite it from the environment, in case the user has set it
|
||||
* 2. to prevent pw_context from parsing "log.level" from the config file;
|
||||
* we do this ourselves here and allows us to have more control over
|
||||
* the whole process.
|
||||
*/
|
||||
gchar lvl_str[2];
|
||||
g_snprintf (lvl_str, 2, "%d", wp_spa_log_get_instance ()->level);
|
||||
g_warn_if_fail (g_setenv ("PIPEWIRE_DEBUG", lvl_str, TRUE));
|
||||
pw_log_set_level (wp_spa_log_get_instance ()->level);
|
||||
pw_log_set (wp_spa_log_get_instance ());
|
||||
}
|
||||
}
|
||||
|
||||
gboolean
|
||||
wp_log_set_global_level (const gchar *log_level)
|
||||
{
|
||||
gint level;
|
||||
if (level_index_from_string (log_level, &level)) {
|
||||
log_state.global_log_level = level;
|
||||
log_state.global_log_level_flags = level_index_to_full_flags (level);
|
||||
wp_spa_log_get_instance()->level = level_index_to_spa (level);
|
||||
pw_log_set_level (level_index_to_spa (level));
|
||||
return TRUE;
|
||||
} else {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
static gint
|
||||
find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
|
||||
{
|
||||
|
|
@ -452,6 +396,255 @@ find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
|
|||
return log_level;
|
||||
}
|
||||
|
||||
static void
|
||||
log_topic_update_level (WpLogTopic *topic)
|
||||
{
|
||||
gint log_level = find_topic_log_level (topic->topic_name, NULL);
|
||||
gint flags = topic->flags & ~WP_LOG_TOPIC_LEVEL_MASK;
|
||||
|
||||
flags |= level_index_to_full_flags (log_level);
|
||||
|
||||
topic->flags = flags;
|
||||
}
|
||||
|
||||
static void
|
||||
update_log_topic_levels (void)
|
||||
{
|
||||
guint i;
|
||||
|
||||
g_mutex_lock (&log_state.log_topics_lock);
|
||||
|
||||
if (log_state.log_topics)
|
||||
for (i = 0; i < log_state.log_topics->len; ++i)
|
||||
log_topic_update_level (g_ptr_array_index (log_state.log_topics, i));
|
||||
|
||||
g_mutex_unlock (&log_state.log_topics_lock);
|
||||
}
|
||||
|
||||
static void
|
||||
free_patterns (struct log_topic_pattern *patterns)
|
||||
{
|
||||
struct log_topic_pattern *p = patterns;
|
||||
|
||||
while (p && p->spec) {
|
||||
g_clear_pointer (&p->spec, g_pattern_spec_free);
|
||||
g_clear_pointer (&p->spec_str, g_free);
|
||||
++p;
|
||||
}
|
||||
|
||||
g_free (patterns);
|
||||
}
|
||||
|
||||
/* Parse value to log level and patterns. If no global level in string,
|
||||
global_log_level is not modified. */
|
||||
static gboolean
|
||||
parse_log_level (const gchar *level_str, struct log_topic_pattern **global_patterns, gint *global_log_level)
|
||||
{
|
||||
struct log_topic_pattern *patterns = NULL, *pttrn;
|
||||
gint n_tokens = 0;
|
||||
gchar **tokens = NULL;
|
||||
int level = *global_log_level;
|
||||
|
||||
*global_patterns = NULL;
|
||||
|
||||
if (level_str && level_str[0] != '\0') {
|
||||
/* [<glob>:]<level>,..., */
|
||||
tokens = pw_split_strv (level_str, ",", INT_MAX, &n_tokens);
|
||||
}
|
||||
|
||||
/* allocate enough space to hold all pattern specs */
|
||||
patterns = g_malloc_n ((n_tokens + 2), sizeof (struct log_topic_pattern));
|
||||
pttrn = patterns;
|
||||
if (!patterns)
|
||||
g_error ("unable to allocate space for %d log patterns", n_tokens + 2);
|
||||
|
||||
for (gint i = 0; i < n_tokens; i++) {
|
||||
gint n_tok;
|
||||
gchar **tok;
|
||||
gint lvl;
|
||||
|
||||
tok = pw_split_strv (tokens[i], ":", 2, &n_tok);
|
||||
if (n_tok == 2 && level_index_from_string (tok[1], &lvl)) {
|
||||
pttrn->spec = g_pattern_spec_new (tok[0]);
|
||||
pttrn->spec_str = g_strdup (tok[0]);
|
||||
pttrn->log_level = lvl;
|
||||
pttrn++;
|
||||
} else if (n_tok == 1 && level_index_from_string (tok[0], &lvl)) {
|
||||
level = lvl;
|
||||
} else {
|
||||
pttrn->spec = NULL;
|
||||
pw_free_strv (tok);
|
||||
free_patterns (patterns);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
pw_free_strv (tok);
|
||||
}
|
||||
|
||||
/* disable pipewire connection trace by default */
|
||||
pttrn->spec = g_pattern_spec_new ("conn.*");
|
||||
pttrn->spec_str = g_strdup ("conn.*");
|
||||
pttrn->log_level = 0;
|
||||
pttrn++;
|
||||
|
||||
/* terminate with NULL */
|
||||
pttrn->spec = NULL;
|
||||
pttrn->spec_str = NULL;
|
||||
pttrn->log_level = 0;
|
||||
|
||||
pw_free_strv (tokens);
|
||||
|
||||
*global_patterns = patterns;
|
||||
*global_log_level = level;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gchar *
|
||||
format_pw_log_level_string (gint level, const struct log_topic_pattern *patterns)
|
||||
{
|
||||
GString *str = g_string_new (NULL);
|
||||
const struct log_topic_pattern *p;
|
||||
|
||||
g_string_printf (str, "%d", level_index_to_spa (level));
|
||||
|
||||
for (p = patterns; p && p->spec; ++p)
|
||||
g_string_append_printf (str, ",%s:%d", p->spec_str, level_index_to_spa (p->log_level));
|
||||
|
||||
return g_string_free (str, FALSE);
|
||||
}
|
||||
|
||||
gboolean
|
||||
wp_log_set_level (const gchar *level_str)
|
||||
{
|
||||
gint level;
|
||||
GLogLevelFlags flags;
|
||||
struct log_topic_pattern *patterns;
|
||||
|
||||
level = DEFAULT_LOG_LEVEL;
|
||||
if (!parse_log_level (level_str, &patterns, &level))
|
||||
return FALSE;
|
||||
|
||||
flags = level_index_to_full_flags (level);
|
||||
|
||||
g_mutex_lock (&log_state.log_topics_lock);
|
||||
log_state.global_log_level = level;
|
||||
log_state.global_log_level_flags = flags;
|
||||
SPA_SWAP (log_state.patterns, patterns);
|
||||
g_mutex_unlock (&log_state.log_topics_lock);
|
||||
|
||||
free_patterns (patterns);
|
||||
|
||||
update_log_topic_levels ();
|
||||
|
||||
wp_spa_log_get_instance()->level = level_index_to_spa (level);
|
||||
|
||||
if (log_state.set_pw_log) {
|
||||
#if PW_CHECK_VERSION(1,1,0)
|
||||
g_autofree gchar *pw_pattern = format_pw_log_level_string (log_state.global_log_level, log_state.patterns);
|
||||
pw_log_set_level_string (pw_pattern);
|
||||
#else
|
||||
pw_log_set_level (level_index_to_spa (level));
|
||||
#endif
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief private, called from wp_init()
|
||||
* \ingroup wplog
|
||||
* \private
|
||||
*/
|
||||
void
|
||||
wp_log_init (gint flags)
|
||||
{
|
||||
log_state.use_color = g_log_writer_supports_color (fileno (stderr));
|
||||
log_state.output_is_journal = g_log_writer_is_journald (fileno (stderr));
|
||||
log_state.set_pw_log = flags & WP_INIT_SET_PW_LOG && !g_getenv ("WIREPLUMBER_NO_PW_LOG");
|
||||
|
||||
if (flags & WP_INIT_SET_GLIB_LOG)
|
||||
g_log_set_writer_func (wp_log_writer_default, NULL, NULL);
|
||||
|
||||
/* set the spa_log interface that pipewire will use */
|
||||
if (log_state.set_pw_log)
|
||||
pw_log_set (wp_spa_log_get_instance ());
|
||||
|
||||
if (!wp_log_set_level (g_getenv ("WIREPLUMBER_DEBUG"))) {
|
||||
wp_warning ("Ignoring invalid value in WIREPLUMBER_DEBUG");
|
||||
wp_log_set_level (NULL);
|
||||
}
|
||||
|
||||
if (log_state.set_pw_log) {
|
||||
/* always set PIPEWIRE_DEBUG for 2 reasons:
|
||||
* 1. to overwrite it from the environment, in case the user has set it
|
||||
* 2. to prevent pw_context from parsing "log.level" from the config file;
|
||||
* we do this ourselves here and allows us to have more control over
|
||||
* the whole process.
|
||||
*/
|
||||
g_autofree gchar *lvl_str = format_pw_log_level_string (log_state.global_log_level, log_state.patterns);
|
||||
g_warn_if_fail (g_setenv ("PIPEWIRE_DEBUG", lvl_str, TRUE));
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
log_topic_register (WpLogTopic *topic)
|
||||
{
|
||||
if (!log_state.log_topics)
|
||||
log_state.log_topics = g_ptr_array_new ();
|
||||
|
||||
g_ptr_array_add (log_state.log_topics, topic);
|
||||
|
||||
log_topic_update_level (topic);
|
||||
topic->flags |= WP_LOG_TOPIC_FLAG_INITIALIZED;
|
||||
}
|
||||
|
||||
static void
|
||||
log_topic_unregister (WpLogTopic *topic)
|
||||
{
|
||||
if (!log_state.log_topics)
|
||||
return;
|
||||
|
||||
g_ptr_array_remove_fast (log_state.log_topics, topic);
|
||||
|
||||
if (log_state.log_topics->len == 0) {
|
||||
g_ptr_array_free (log_state.log_topics, TRUE);
|
||||
log_state.log_topics = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Registers a log topic.
|
||||
*
|
||||
* The log topic must be unregistered using \ref wp_log_topic_unregister
|
||||
* before its lifetime ends.
|
||||
*
|
||||
* This function is threadsafe.
|
||||
*
|
||||
* \ingroup wplog
|
||||
*/
|
||||
void
|
||||
wp_log_topic_register (WpLogTopic *topic)
|
||||
{
|
||||
g_mutex_lock (&log_state.log_topics_lock);
|
||||
log_topic_register (topic);
|
||||
g_mutex_unlock (&log_state.log_topics_lock);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Unregisters a log topic.
|
||||
*
|
||||
* This function is threadsafe.
|
||||
*
|
||||
* \ingroup wplog
|
||||
*/
|
||||
void
|
||||
wp_log_topic_unregister (WpLogTopic *topic)
|
||||
{
|
||||
g_mutex_lock (&log_state.log_topics_lock);
|
||||
log_topic_unregister (topic);
|
||||
g_mutex_unlock (&log_state.log_topics_lock);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Initializes a log topic. Internal function, don't use it directly
|
||||
* \ingroup wplog
|
||||
|
|
@ -459,21 +652,17 @@ find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
|
|||
void
|
||||
wp_log_topic_init (WpLogTopic *topic)
|
||||
{
|
||||
g_bit_lock (&topic->flags, 30);
|
||||
if ((topic->flags & (1u << 31)) == 0) {
|
||||
bool has_custom_level;
|
||||
gint log_level = find_topic_log_level (topic->topic_name, &has_custom_level);
|
||||
|
||||
gint flags = topic->flags;
|
||||
flags |= level_index_to_full_flags (log_level);
|
||||
flags |= (1u << 31); /* initialized = true */
|
||||
if (has_custom_level)
|
||||
flags |= (1u << 29); /* has_custom_level = true */
|
||||
|
||||
topic->global_flags = &log_state.global_log_level_flags;
|
||||
topic->flags = flags;
|
||||
g_mutex_lock (&log_state.log_topics_lock);
|
||||
if ((topic->flags & WP_LOG_TOPIC_FLAG_INITIALIZED) == 0) {
|
||||
if (topic->flags & WP_LOG_TOPIC_FLAG_STATIC) {
|
||||
/* Auto-register log topics that have infinite lifetime */
|
||||
log_topic_register (topic);
|
||||
} else {
|
||||
log_topic_update_level (topic);
|
||||
topic->flags |= WP_LOG_TOPIC_FLAG_INITIALIZED;
|
||||
}
|
||||
}
|
||||
g_bit_unlock (&topic->flags, 30);
|
||||
g_mutex_unlock (&log_state.log_topics_lock);
|
||||
}
|
||||
|
||||
typedef struct _WpLogFields WpLogFields;
|
||||
|
|
@ -485,6 +674,7 @@ struct _WpLogFields
|
|||
const gchar *func;
|
||||
const gchar *message;
|
||||
gint log_level;
|
||||
gboolean debug;
|
||||
GType object_type;
|
||||
gconstpointer object;
|
||||
};
|
||||
|
|
@ -493,6 +683,7 @@ static void
|
|||
wp_log_fields_init (WpLogFields *lf,
|
||||
const gchar *log_topic,
|
||||
gint log_level,
|
||||
gboolean debug,
|
||||
const gchar *file,
|
||||
const gchar *line,
|
||||
const gchar *func,
|
||||
|
|
@ -502,6 +693,7 @@ wp_log_fields_init (WpLogFields *lf,
|
|||
{
|
||||
lf->log_topic = log_topic ? log_topic : "default";
|
||||
lf->log_level = log_level;
|
||||
lf->debug = debug;
|
||||
lf->file = file;
|
||||
lf->line = line;
|
||||
lf->func = func;
|
||||
|
|
@ -510,11 +702,18 @@ wp_log_fields_init (WpLogFields *lf,
|
|||
lf->message = message ? message : "(null)";
|
||||
}
|
||||
|
||||
static gboolean
|
||||
wp_want_debug_log (const struct spa_log_topic *topic)
|
||||
{
|
||||
return spa_log_level_topic_enabled (wp_spa_log_get_instance(), topic, SPA_LOG_LEVEL_DEBUG);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_log_fields_init_from_glib (WpLogFields *lf, GLogLevelFlags log_level_flags,
|
||||
const GLogField *fields, gsize n_fields)
|
||||
{
|
||||
wp_log_fields_init (lf, NULL, level_index_from_flags (log_level_flags),
|
||||
wp_want_debug_log (NULL),
|
||||
NULL, NULL, NULL, 0, NULL, NULL);
|
||||
|
||||
for (guint i = 0; i < n_fields; i++) {
|
||||
|
|
@ -573,15 +772,52 @@ wp_log_fields_write_to_stream (WpLogFields *lf, FILE *s)
|
|||
static gboolean
|
||||
wp_log_fields_write_to_journal (WpLogFields *lf)
|
||||
{
|
||||
gsize n_fields = 6;
|
||||
GLogField fields[6] = {
|
||||
{ "PRIORITY", log_level_info[lf->log_level].priority, -1 },
|
||||
{ "CODE_FILE", lf->file ? lf->file : "", -1 },
|
||||
{ "CODE_LINE", lf->line ? lf->line : "", -1 },
|
||||
{ "CODE_FUNC", lf->func ? lf->func : "", -1 },
|
||||
{ "TOPIC", lf->log_topic ? lf->log_topic : "", -1 },
|
||||
{ "MESSAGE", lf->message ? lf->message : "", -1 },
|
||||
};
|
||||
GLogField fields[10];
|
||||
gsize n_fields = 0;
|
||||
g_autofree gchar *full_message = NULL;
|
||||
const gchar *message = lf->message ? lf->message : "";
|
||||
g_autofree gchar *pid = g_strdup_printf("%d", getpid());
|
||||
g_autofree gchar *tid = g_strdup_printf("%d", gettid());
|
||||
#ifdef HAS_SHORT_NAME
|
||||
const gchar *syslog_identifier = program_invocation_short_name;
|
||||
#else
|
||||
const gchar *syslog_identifier = g_get_prgname();
|
||||
#endif
|
||||
|
||||
if (lf->debug) {
|
||||
if (lf->file && lf->line && lf->func) {
|
||||
g_autofree gchar *file = g_path_get_basename(lf->file);
|
||||
|
||||
message = full_message = g_strdup_printf("%c %s%s[%s:%s:%s]: %s",
|
||||
log_level_info[lf->log_level].name,
|
||||
lf->log_topic ? lf->log_topic : "",
|
||||
lf->log_topic ? " " : "",
|
||||
file, lf->line, lf->func, message);
|
||||
} else {
|
||||
message = full_message = g_strdup_printf("%c %s%s%s",
|
||||
log_level_info[lf->log_level].name,
|
||||
lf->log_topic ? lf->log_topic : "",
|
||||
lf->log_topic ? ": " : "",
|
||||
message);
|
||||
}
|
||||
} else if (lf->log_topic) {
|
||||
message = full_message = g_strdup_printf("%s: %s", lf->log_topic, message);
|
||||
}
|
||||
|
||||
fields[n_fields++] = (GLogField) { "SYSLOG_PID", pid, -1 };
|
||||
fields[n_fields++] = (GLogField) { "TID", tid, -1 };
|
||||
fields[n_fields++] = (GLogField) { "SYSLOG_IDENTIFIER", syslog_identifier, -1 };
|
||||
fields[n_fields++] = (GLogField) { "SYSLOG_FACILITY", "3", -1 };
|
||||
fields[n_fields++] = (GLogField) { "PRIORITY", log_level_info[lf->log_level].priority, -1 };
|
||||
if (lf->file)
|
||||
fields[n_fields++] = (GLogField) { "CODE_FILE", lf->file, -1 };
|
||||
if (lf->line)
|
||||
fields[n_fields++] = (GLogField) { "CODE_LINE", lf->line, -1 };
|
||||
if (lf->func)
|
||||
fields[n_fields++] = (GLogField) { "CODE_FUNC", lf->func, -1 };
|
||||
if (lf->log_topic)
|
||||
fields[n_fields++] = (GLogField) { "TOPIC", lf->log_topic, -1 };
|
||||
fields[n_fields++] = (GLogField) { "MESSAGE", message, -1 };
|
||||
|
||||
/* the log level flags are not used in this function, so we can pass 0 */
|
||||
return (g_log_writer_journald (0, fields, n_fields, NULL) == G_LOG_WRITER_HANDLED);
|
||||
|
|
@ -671,6 +907,8 @@ wp_log_writer_default (GLogLevelFlags log_level_flags,
|
|||
/*!
|
||||
* \brief Used internally by the debug logging macros. Avoid using it directly.
|
||||
*
|
||||
* \deprecated Use \ref wp_logt_checked instead.
|
||||
*
|
||||
* This assumes that the arguments are correct and that the log_topic is
|
||||
* enabled for the given log_level. No additional checks are performed.
|
||||
* \ingroup wplog
|
||||
|
|
@ -696,6 +934,46 @@ wp_log_checked (
|
|||
va_end (args);
|
||||
|
||||
wp_log_fields_init (&lf, log_topic, level_index_from_flags (log_level_flags),
|
||||
wp_want_debug_log (NULL),
|
||||
file, line, func, object_type, object, message);
|
||||
wp_log_fields_log (&lf);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Used internally by the debug logging macros. Avoid using it directly.
|
||||
*
|
||||
* This assumes that the arguments are correct and that the log_topic is
|
||||
* enabled for the given log_level. No additional checks are performed.
|
||||
* \ingroup wplog
|
||||
*/
|
||||
void
|
||||
wp_logt_checked (
|
||||
const WpLogTopic *topic,
|
||||
GLogLevelFlags log_level_flags,
|
||||
const gchar *file,
|
||||
const gchar *line,
|
||||
const gchar *func,
|
||||
GType object_type,
|
||||
gconstpointer object,
|
||||
const gchar *message_format,
|
||||
...)
|
||||
{
|
||||
WpLogFields lf = {0};
|
||||
g_autofree gchar *message = NULL;
|
||||
va_list args;
|
||||
const gchar *log_topic = topic ? topic->topic_name : NULL;
|
||||
gboolean debug;
|
||||
|
||||
if (topic)
|
||||
debug = (topic->flags & WP_LOG_TOPIC_LEVEL_MASK & G_LOG_LEVEL_DEBUG);
|
||||
else
|
||||
debug = wp_want_debug_log (NULL);
|
||||
|
||||
va_start (args, message_format);
|
||||
message = g_strdup_vprintf (message_format, args);
|
||||
va_end (args);
|
||||
|
||||
wp_log_fields_init (&lf, log_topic, level_index_from_flags (log_level_flags), debug,
|
||||
file, line, func, object_type, object, message);
|
||||
wp_log_fields_log (&lf);
|
||||
}
|
||||
|
|
@ -719,6 +997,7 @@ wp_spa_log_logtv (void *object,
|
|||
message = g_strdup_vprintf (fmt, args);
|
||||
|
||||
wp_log_fields_init (&lf, topic ? topic->topic : NULL, log_level,
|
||||
wp_want_debug_log (topic),
|
||||
file, line_str, func, 0, NULL, message);
|
||||
wp_log_fields_log (&lf);
|
||||
}
|
||||
|
|
|
|||
98
lib/wp/log.h
98
lib/wp/log.h
|
|
@ -14,7 +14,21 @@
|
|||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define WP_LOG_LEVEL_TRACE (1 << G_LOG_LEVEL_USER_SHIFT)
|
||||
/*!
|
||||
* \brief A custom GLib log level for trace messages (extension of GLogLevelFlags)
|
||||
* \ingroup wplog
|
||||
*/
|
||||
static const guint WP_LOG_LEVEL_TRACE = (1 << 8);
|
||||
|
||||
/*
|
||||
The above WP_LOG_LEVEL_TRACE constant is intended to be defined as
|
||||
(1 << G_LOG_LEVEL_USER_SHIFT), but due to a gobject-introspection bug
|
||||
we define it with the value of G_LOG_LEVEL_USER_SHIFT, which is 8, so
|
||||
that it ends up correctly in the bindings. To avoid value mismatches,
|
||||
we statically verify here that G_LOG_LEVEL_USER_SHIFT is indeed 8.
|
||||
See https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/540
|
||||
*/
|
||||
G_STATIC_ASSERT (G_LOG_LEVEL_USER_SHIFT == 8);
|
||||
|
||||
#define WP_OBJECT_FORMAT "<%s:%p>"
|
||||
#define WP_OBJECT_ARGS(object) \
|
||||
|
|
@ -24,57 +38,68 @@ WP_PRIVATE_API
|
|||
void wp_log_init (gint flags);
|
||||
|
||||
WP_API
|
||||
gboolean wp_log_set_global_level (const gchar *log_level);
|
||||
gboolean wp_log_set_level (const gchar *log_level);
|
||||
|
||||
typedef struct _WpLogTopic WpLogTopic;
|
||||
struct _WpLogTopic {
|
||||
/*!
|
||||
* \brief WpLogTopic flags
|
||||
* \ingroup wplog
|
||||
*/
|
||||
typedef enum { /*< flags >*/
|
||||
/*! the lower 16 bits of the flags are GLogLevelFlags */
|
||||
WP_LOG_TOPIC_LEVEL_MASK = 0xFFFF,
|
||||
/*! the log topic has infinite lifetime (lives on static storage) */
|
||||
WP_LOG_TOPIC_FLAG_STATIC = 1u << 30,
|
||||
/*! the log topic has been initialized */
|
||||
WP_LOG_TOPIC_FLAG_INITIALIZED = 1u << 31,
|
||||
} WpLogTopicFlags;
|
||||
|
||||
/*!
|
||||
* \brief A structure representing a log topic
|
||||
* \ingroup wplog
|
||||
*/
|
||||
typedef struct {
|
||||
const char *topic_name;
|
||||
WpLogTopicFlags flags;
|
||||
|
||||
/*< private >*/
|
||||
/*
|
||||
* lower 16 bits: GLogLevelFlags
|
||||
* bit 29: has_custom_level
|
||||
* bit 30: a g_bit_lock
|
||||
* bit 31: 1 - initialized, 0 - not initialized
|
||||
*/
|
||||
gint flags;
|
||||
gint *global_flags;
|
||||
WP_PADDING(2)
|
||||
};
|
||||
WP_PADDING(3)
|
||||
} WpLogTopic;
|
||||
|
||||
#define WP_LOG_TOPIC_EXTERN(var) \
|
||||
extern WpLogTopic * var;
|
||||
|
||||
#define WP_LOG_TOPIC(var, t) \
|
||||
WpLogTopic var##_struct = { .topic_name = t, .flags = 0 }; \
|
||||
#define WP_LOG_TOPIC(var, name) \
|
||||
WpLogTopic var##_struct = { .topic_name = name, .flags = WP_LOG_TOPIC_FLAG_STATIC }; \
|
||||
WpLogTopic * var = &(var##_struct);
|
||||
|
||||
#define WP_LOG_TOPIC_STATIC(var, t) \
|
||||
static WpLogTopic var##_struct = { .topic_name = t, .flags = 0 }; \
|
||||
#define WP_LOG_TOPIC_STATIC(var, name) \
|
||||
static WpLogTopic var##_struct = { .topic_name = name, .flags = WP_LOG_TOPIC_FLAG_STATIC }; \
|
||||
static G_GNUC_UNUSED WpLogTopic * var = &(var##_struct);
|
||||
|
||||
#define WP_DEFINE_LOCAL_LOG_TOPIC(t) \
|
||||
WP_LOG_TOPIC_STATIC(WP_LOCAL_LOG_TOPIC, t)
|
||||
#define WP_DEFINE_LOCAL_LOG_TOPIC(name) \
|
||||
WP_LOG_TOPIC_STATIC(WP_LOCAL_LOG_TOPIC, name)
|
||||
|
||||
/* make glib log functions also use the local log topic */
|
||||
#ifdef G_LOG_DOMAIN
|
||||
# undef G_LOG_DOMAIN
|
||||
#ifdef WP_USE_LOCAL_LOG_TOPIC_IN_G_LOG
|
||||
# ifdef G_LOG_DOMAIN
|
||||
# undef G_LOG_DOMAIN
|
||||
# endif
|
||||
# define G_LOG_DOMAIN (WP_LOCAL_LOG_TOPIC->topic_name)
|
||||
#endif
|
||||
#define G_LOG_DOMAIN (WP_LOCAL_LOG_TOPIC->topic_name)
|
||||
|
||||
WP_API
|
||||
void wp_log_topic_init (WpLogTopic *topic);
|
||||
|
||||
WP_API
|
||||
void wp_log_topic_register (WpLogTopic *topic);
|
||||
|
||||
WP_API
|
||||
void wp_log_topic_unregister (WpLogTopic *topic);
|
||||
|
||||
static inline gboolean
|
||||
wp_log_topic_is_initialized (WpLogTopic *topic)
|
||||
{
|
||||
return (topic->flags & (1u << 31)) != 0;
|
||||
}
|
||||
|
||||
static inline gboolean
|
||||
wp_log_topic_has_custom_level (WpLogTopic *topic)
|
||||
{
|
||||
return (topic->flags & (1u << 29)) != 0;
|
||||
return (topic->flags & WP_LOG_TOPIC_FLAG_INITIALIZED) != 0;
|
||||
}
|
||||
|
||||
static inline gboolean
|
||||
|
|
@ -84,10 +109,7 @@ wp_log_topic_is_enabled (WpLogTopic *topic, GLogLevelFlags log_level)
|
|||
if (G_UNLIKELY (!wp_log_topic_is_initialized (topic)))
|
||||
wp_log_topic_init (topic);
|
||||
|
||||
if (wp_log_topic_has_custom_level (topic))
|
||||
return (topic->flags & (log_level & 0xFFFF)) != 0;
|
||||
else
|
||||
return (*topic->global_flags & (log_level & 0xFFFF)) != 0;
|
||||
return (topic->flags & log_level & WP_LOG_TOPIC_LEVEL_MASK) != 0;
|
||||
}
|
||||
|
||||
#define wp_local_log_topic_is_enabled(log_level) \
|
||||
|
|
@ -99,6 +121,12 @@ GLogWriterOutput wp_log_writer_default (GLogLevelFlags log_level,
|
|||
|
||||
WP_API
|
||||
void wp_log_checked (const gchar *log_topic, GLogLevelFlags log_level,
|
||||
const gchar *file, const gchar *line, const gchar *func,
|
||||
GType object_type, gconstpointer object,
|
||||
const gchar *message_format, ...) G_GNUC_PRINTF (8, 9) G_GNUC_DEPRECATED_FOR (wp_logt_checked);
|
||||
|
||||
WP_API
|
||||
void wp_logt_checked (const WpLogTopic *topic, GLogLevelFlags log_level,
|
||||
const gchar *file, const gchar *line, const gchar *func,
|
||||
GType object_type, gconstpointer object,
|
||||
const gchar *message_format, ...) G_GNUC_PRINTF (8, 9);
|
||||
|
|
@ -106,7 +134,7 @@ void wp_log_checked (const gchar *log_topic, GLogLevelFlags log_level,
|
|||
#define wp_log(topic, level, type, object, ...) \
|
||||
({ \
|
||||
if (G_UNLIKELY (wp_log_topic_is_enabled (topic, level))) \
|
||||
wp_log_checked (topic->topic_name, level, __FILE__, G_STRINGIFY (__LINE__), \
|
||||
wp_logt_checked (topic, level, __FILE__, G_STRINGIFY (__LINE__), \
|
||||
G_STRFUNC, type, object, __VA_ARGS__); \
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
wp_lib_sources = files(
|
||||
'base-dirs.c',
|
||||
'client.c',
|
||||
'component-loader.c',
|
||||
'conf.c',
|
||||
|
|
@ -21,8 +22,10 @@ wp_lib_sources = files(
|
|||
'object.c',
|
||||
'object-interest.c',
|
||||
'object-manager.c',
|
||||
'permission-manager.c',
|
||||
'plugin.c',
|
||||
'port.c',
|
||||
'proc-utils.c',
|
||||
'properties.c',
|
||||
'proxy.c',
|
||||
'proxy-interfaces.c',
|
||||
|
|
@ -40,9 +43,11 @@ wp_lib_sources = files(
|
|||
wp_lib_priv_sources = files(
|
||||
'private/pipewire-object-mixin.c',
|
||||
'private/internal-comp-loader.c',
|
||||
'private/registry.c',
|
||||
)
|
||||
|
||||
wp_lib_headers = files(
|
||||
'base-dirs.h',
|
||||
'client.h',
|
||||
'component-loader.h',
|
||||
'conf.h',
|
||||
|
|
@ -65,8 +70,10 @@ wp_lib_headers = files(
|
|||
'object.h',
|
||||
'object-interest.h',
|
||||
'object-manager.h',
|
||||
'permission-manager.h',
|
||||
'plugin.h',
|
||||
'port.h',
|
||||
'proc-utils.h',
|
||||
'properties.h',
|
||||
'proxy.h',
|
||||
'proxy-interfaces.h',
|
||||
|
|
@ -109,22 +116,26 @@ wpversion = configure_file(
|
|||
)
|
||||
wp_gen_sources += [wpversion]
|
||||
|
||||
wpbuildbasedirs_data = configuration_data()
|
||||
wpbuildbasedirs_data.set('BUILD_SYSCONFDIR', '"@0@"'.format(get_option('prefix') / get_option('sysconfdir')))
|
||||
wpbuildbasedirs_data.set('BUILD_DATADIR', '"@0@"'.format(get_option('prefix') / get_option('datadir')))
|
||||
wpbuildbasedirs_data.set('BUILD_LIBDIR', '"@0@"'.format(get_option('prefix') / get_option('libdir')))
|
||||
wpbuildbasedirs_data.set('BUILD_LOCALEDIR', '"@0@"'.format(get_option('prefix') / get_option('localedir')))
|
||||
wpbuildbasedirs = configure_file (
|
||||
output : 'wpbuildbasedirs.h',
|
||||
configuration : wpbuildbasedirs_data,
|
||||
)
|
||||
|
||||
wp_lib = library('wireplumber-' + wireplumber_api_version,
|
||||
wp_lib_sources, wp_lib_priv_sources, wpenums_c, wpenums_h, wpversion,
|
||||
wp_lib_sources, wp_lib_priv_sources, wpenums_c, wpenums_h, wpversion, wpbuildbasedirs,
|
||||
c_args : [
|
||||
'-D_GNU_SOURCE',
|
||||
'-DG_LOG_USE_STRUCTURED',
|
||||
'-DWIREPLUMBER_DEFAULT_MODULE_DIR="@0@"'.format(wireplumber_module_dir),
|
||||
'-DWIREPLUMBER_DEFAULT_CONFIG_DIR="@0@"'.format(wireplumber_config_dir),
|
||||
'-DWIREPLUMBER_DEFAULT_DATA_DIR="@0@"'.format(wireplumber_data_dir),
|
||||
'-DLOCALE_DIR="@0@"'.format(wireplumber_locale_dir),
|
||||
'-DBUILDING_WP',
|
||||
],
|
||||
install: true,
|
||||
include_directories: wp_lib_include_dir,
|
||||
dependencies : [gobject_dep, gmodule_dep, gio_dep, pipewire_dep, libintl_dep],
|
||||
soversion: wireplumber_so_version,
|
||||
version: meson.project_version(),
|
||||
version: wireplumber_libversion,
|
||||
)
|
||||
|
||||
wp_dep = declare_dependency(
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-metadata")
|
|||
* gchar * value,
|
||||
* gpointer user_data)
|
||||
* \endcode
|
||||
* Emited when metadata change
|
||||
* Emitted when metadata change
|
||||
*
|
||||
* Parameters:
|
||||
* - `subject` - the metadata subject id
|
||||
|
|
@ -319,6 +319,129 @@ wp_metadata_class_init (WpMetadataClass * klass)
|
|||
G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \struct WpMetadataItem
|
||||
*
|
||||
* WpMetadataItem holds the subject, key, type and value of a metadata entry.
|
||||
*/
|
||||
struct _WpMetadataItem
|
||||
{
|
||||
WpMetadata *metadata;
|
||||
guint32 subject;
|
||||
const gchar *key;
|
||||
const gchar *type;
|
||||
const gchar *value;
|
||||
};
|
||||
|
||||
G_DEFINE_BOXED_TYPE (WpMetadataItem, wp_metadata_item,
|
||||
wp_metadata_item_ref, wp_metadata_item_unref)
|
||||
|
||||
static WpMetadataItem *
|
||||
wp_metadata_item_new (WpMetadata *metadata, guint32 subject, const gchar *key,
|
||||
const gchar *type, const gchar *value)
|
||||
{
|
||||
WpMetadataItem *self = g_rc_box_new0 (WpMetadataItem);
|
||||
self->metadata = g_object_ref (metadata);
|
||||
self->subject = subject;
|
||||
self->key = key;
|
||||
self->type = type;
|
||||
self->value = value;
|
||||
return self;
|
||||
}
|
||||
|
||||
static void
|
||||
wp_metadata_item_free (gpointer p)
|
||||
{
|
||||
WpMetadataItem *self = p;
|
||||
g_clear_object (&self->metadata);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Increases the reference count of a metadata item object
|
||||
* \ingroup wpmetadata
|
||||
* \param self a metadata item object
|
||||
* \returns (transfer full): \a self with an additional reference count on it
|
||||
* \since 0.5.0
|
||||
*/
|
||||
WpMetadataItem *
|
||||
wp_metadata_item_ref (WpMetadataItem *self)
|
||||
{
|
||||
return g_rc_box_acquire (self);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Decreases the reference count on \a self and frees it when the ref
|
||||
* count reaches zero.
|
||||
* \ingroup wpmetadata
|
||||
* \param self (transfer full): a metadata item object
|
||||
* \since 0.5.0
|
||||
*/
|
||||
void
|
||||
wp_metadata_item_unref (WpMetadataItem *self)
|
||||
{
|
||||
g_rc_box_release_full (self, wp_metadata_item_free);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the subject from a metadata item
|
||||
*
|
||||
* \ingroup wpmetadata
|
||||
* \param self the item held by the GValue that was returned from the WpIterator
|
||||
* of wp_metadata_new_iterator()
|
||||
* \returns the metadata subject of the \a item
|
||||
* \since 0.5.0
|
||||
*/
|
||||
guint32
|
||||
wp_metadata_item_get_subject (WpMetadataItem * self)
|
||||
{
|
||||
return self->subject;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the key from a metadata item
|
||||
*
|
||||
* \ingroup wpmetadata
|
||||
* \param self the item held by the GValue that was returned from the WpIterator
|
||||
* of wp_metadata_new_iterator()
|
||||
* \returns (transfer none): the metadata key of the \a item
|
||||
* \since 0.5.0
|
||||
*/
|
||||
const gchar *
|
||||
wp_metadata_item_get_key (WpMetadataItem * self)
|
||||
{
|
||||
return self->key;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the value type from a metadata item
|
||||
*
|
||||
* \ingroup wpmetadata
|
||||
* \param self the item held by the GValue that was returned from the WpIterator
|
||||
* of wp_metadata_new_iterator()
|
||||
* \returns (transfer none): the metadata value type of the \a item
|
||||
* \since 0.5.0
|
||||
*/
|
||||
const gchar *
|
||||
wp_metadata_item_get_value_type (WpMetadataItem * self)
|
||||
{
|
||||
return self->type;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the value from a metadata item
|
||||
*
|
||||
* \ingroup wpmetadata
|
||||
* \param self the item held by the GValue that was returned from the WpIterator
|
||||
* of wp_metadata_new_iterator()
|
||||
* \returns (transfer none): the metadata value of the \a item
|
||||
* \since 0.5.0
|
||||
*/
|
||||
const gchar *
|
||||
wp_metadata_item_get_value (WpMetadataItem * self)
|
||||
{
|
||||
return self->value;
|
||||
}
|
||||
|
||||
struct metadata_iterator_data
|
||||
{
|
||||
WpMetadata *metadata;
|
||||
|
|
@ -346,8 +469,11 @@ metadata_iterator_next (WpIterator *it, GValue *item)
|
|||
while (pw_array_check (&priv->metadata, it_data->item)) {
|
||||
if ((it_data->subject == PW_ID_ANY ||
|
||||
it_data->subject == it_data->item->subject)) {
|
||||
g_value_init (item, G_TYPE_POINTER);
|
||||
g_value_set_pointer (item, (gpointer) it_data->item);
|
||||
g_autoptr (WpMetadataItem) mi = wp_metadata_item_new (it_data->metadata,
|
||||
it_data->item->subject, it_data->item->key, it_data->item->type,
|
||||
it_data->item->value);
|
||||
g_value_init (item, WP_TYPE_METADATA_ITEM);
|
||||
g_value_take_boxed (item, g_steal_pointer (&mi));
|
||||
it_data->item++;
|
||||
return TRUE;
|
||||
}
|
||||
|
|
@ -369,8 +495,11 @@ metadata_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
|
|||
if ((it_data->subject == PW_ID_ANY ||
|
||||
it_data->subject == it_data->item->subject)) {
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
g_value_init (&item, G_TYPE_POINTER);
|
||||
g_value_set_pointer (&item, (gpointer) i);
|
||||
g_autoptr (WpMetadataItem) mi = wp_metadata_item_new (it_data->metadata,
|
||||
it_data->item->subject, it_data->item->key, it_data->item->type,
|
||||
it_data->item->value);
|
||||
g_value_init (&item, WP_TYPE_METADATA_ITEM);
|
||||
g_value_take_boxed (&item, g_steal_pointer (&mi));
|
||||
if (!func (&item, ret, data))
|
||||
return FALSE;
|
||||
}
|
||||
|
|
@ -407,8 +536,7 @@ static const WpIteratorMethods metadata_iterator_methods = {
|
|||
* \param self a metadata object
|
||||
* \param subject the metadata subject id, or -1 (PW_ID_ANY)
|
||||
* \returns (transfer full): an iterator that iterates over the found metadata.
|
||||
* Use wp_metadata_iterator_item_extract() to parse the items returned by
|
||||
* this iterator.
|
||||
* The type of the iterator item is WpMetadataItem.
|
||||
*/
|
||||
WpIterator *
|
||||
wp_metadata_new_iterator (WpMetadata * self, guint32 subject)
|
||||
|
|
@ -429,33 +557,6 @@ wp_metadata_new_iterator (WpMetadata * self, guint32 subject)
|
|||
return g_steal_pointer (&it);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Extracts the metadata subject, key, type and value out of a
|
||||
* GValue that was returned from the WpIterator of wp_metadata_find()
|
||||
*
|
||||
* \ingroup wpmetadata
|
||||
* \param item a GValue that was returned from the WpIterator of wp_metadata_find()
|
||||
* \param subject (out)(optional): the subject id of the current item
|
||||
* \param key (out)(optional)(transfer none): the key of the current item
|
||||
* \param type (out)(optional)(transfer none): the type of the current item
|
||||
* \param value (out)(optional)(transfer none): the value of the current item
|
||||
*/
|
||||
void
|
||||
wp_metadata_iterator_item_extract (const GValue * item, guint32 * subject,
|
||||
const gchar ** key, const gchar ** type, const gchar ** value)
|
||||
{
|
||||
const struct item *i = g_value_get_pointer (item);
|
||||
g_return_if_fail (i != NULL);
|
||||
if (subject)
|
||||
*subject = i->subject;
|
||||
if (key)
|
||||
*key = i->key;
|
||||
if (type)
|
||||
*type = i->type;
|
||||
if (value)
|
||||
*value = i->value;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Finds the metadata value given its \a subject and \a key.
|
||||
*
|
||||
|
|
@ -474,8 +575,10 @@ wp_metadata_find (WpMetadata * self, guint32 subject, const gchar * key,
|
|||
g_auto (GValue) val = G_VALUE_INIT;
|
||||
it = wp_metadata_new_iterator (self, subject);
|
||||
for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
|
||||
const gchar *k = NULL, *t = NULL, *v = NULL;
|
||||
wp_metadata_iterator_item_extract (&val, NULL, &k, &t, &v);
|
||||
WpMetadataItem *mi = g_value_get_boxed (&val);
|
||||
const gchar *k = wp_metadata_item_get_key (mi);
|
||||
const gchar *t = wp_metadata_item_get_value_type (mi);
|
||||
const gchar *v = wp_metadata_item_get_value (mi);
|
||||
if (g_strcmp0 (k, key) == 0) {
|
||||
if (type)
|
||||
*type = t;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,36 @@
|
|||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/*!
|
||||
* \brief The WpMetadataItem GType
|
||||
* \ingroup wpmetadata
|
||||
*/
|
||||
#define WP_TYPE_METADATA_ITEM (wp_metadata_item_get_type ())
|
||||
WP_API
|
||||
GType wp_metadata_item_get_type (void);
|
||||
|
||||
typedef struct _WpMetadataItem WpMetadataItem;
|
||||
|
||||
WP_API
|
||||
WpMetadataItem *wp_metadata_item_ref (WpMetadataItem *self);
|
||||
|
||||
WP_API
|
||||
void wp_metadata_item_unref (WpMetadataItem *self);
|
||||
|
||||
WP_API
|
||||
guint32 wp_metadata_item_get_subject (WpMetadataItem * self);
|
||||
|
||||
WP_API
|
||||
const gchar * wp_metadata_item_get_key (WpMetadataItem * self);
|
||||
|
||||
WP_API
|
||||
const gchar * wp_metadata_item_get_value_type (WpMetadataItem * self);
|
||||
|
||||
WP_API
|
||||
const gchar * wp_metadata_item_get_value (WpMetadataItem * self);
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpMetadataItem, wp_metadata_item_unref)
|
||||
|
||||
/*!
|
||||
* \brief An extension of WpProxyFeatures for WpMetadata objects
|
||||
* \ingroup wpmetadata
|
||||
|
|
@ -42,10 +72,6 @@ struct _WpMetadataClass
|
|||
WP_API
|
||||
WpIterator * wp_metadata_new_iterator (WpMetadata * self, guint32 subject);
|
||||
|
||||
WP_API
|
||||
void wp_metadata_iterator_item_extract (const GValue * item, guint32 * subject,
|
||||
const gchar ** key, const gchar ** type, const gchar ** value);
|
||||
|
||||
WP_API
|
||||
const gchar * wp_metadata_find (WpMetadata * self, guint32 subject,
|
||||
const gchar * key, const gchar ** type);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ struct _WpImplModule
|
|||
WpProperties *props; /* only used during module load */
|
||||
|
||||
struct pw_impl_module *pw_impl_module;
|
||||
struct spa_hook impl_module_listener;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE (WpImplModule, wp_impl_module, G_TYPE_OBJECT);
|
||||
|
|
@ -46,6 +47,17 @@ enum {
|
|||
PROP_PW_IMPL_MODULE,
|
||||
};
|
||||
|
||||
static void impl_module_free (void *data)
|
||||
{
|
||||
WpImplModule *self = WP_IMPL_MODULE (data);
|
||||
self->pw_impl_module = NULL;
|
||||
}
|
||||
|
||||
static const struct pw_impl_module_events impl_module_events = {
|
||||
PW_VERSION_IMPL_MODULE_EVENTS,
|
||||
.free = impl_module_free,
|
||||
};
|
||||
|
||||
static void
|
||||
wp_impl_module_init (WpImplModule * self)
|
||||
{
|
||||
|
|
@ -80,10 +92,15 @@ wp_impl_module_constructed (GObject * object)
|
|||
self->pw_impl_module =
|
||||
pw_context_load_module (context, self->name, self->args, props);
|
||||
|
||||
if (self->pw_impl_module && self->props) {
|
||||
/* With the module loaded, properties are just passthrough now */
|
||||
wp_properties_unref (self->props);
|
||||
self->props = NULL;
|
||||
if (self->pw_impl_module) {
|
||||
if (self->props) {
|
||||
/* With the module loaded, properties are just passthrough now */
|
||||
wp_properties_unref (self->props);
|
||||
self->props = NULL;
|
||||
}
|
||||
|
||||
pw_impl_module_add_listener (self->pw_impl_module,
|
||||
&self->impl_module_listener, &impl_module_events, self);
|
||||
}
|
||||
|
||||
G_OBJECT_CLASS (wp_impl_module_parent_class)->constructed (object);
|
||||
|
|
@ -104,6 +121,8 @@ wp_impl_module_finalize (GObject * object)
|
|||
|
||||
if (self->props)
|
||||
wp_properties_unref (self->props);
|
||||
|
||||
G_OBJECT_CLASS (wp_impl_module_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-node")
|
|||
* \code
|
||||
* void
|
||||
* state_changed_callback (WpNode * self,
|
||||
* WpNodeState * old_state,
|
||||
* WpNodeState * new_state,
|
||||
* WpNodeState old_state,
|
||||
* WpNodeState new_state,
|
||||
* gpointer user_data)
|
||||
* \endcode
|
||||
*
|
||||
|
|
@ -631,7 +631,7 @@ wp_node_send_command (WpNode * self, const gchar * command)
|
|||
|
||||
struct spa_command cmd =
|
||||
SPA_NODE_COMMAND_INIT(wp_spa_id_value_number (command_value));
|
||||
pw_node_send_command (wp_proxy_get_pw_proxy (WP_PROXY (self)), &cmd);
|
||||
pw_node_send_command ((struct pw_node*)wp_proxy_get_pw_proxy (WP_PROXY (self)), &cmd);
|
||||
}
|
||||
|
||||
/*! \defgroup wpimplnode WpImplNode */
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ G_DEFINE_BOXED_TYPE (WpObjectInterest, wp_object_interest,
|
|||
* For further reading on the constraint's arguments, see
|
||||
* wp_object_interest_add_constraint()
|
||||
*
|
||||
* For example, this interest matches objects that are descendands of WpProxy
|
||||
* For example, this interest matches objects that are descendants of WpProxy
|
||||
* with a "bound-id" between 0 and 100 (inclusive), with a pipewire property
|
||||
* called "format.dsp" that contains the string "audio" somewhere in the value
|
||||
* and with a pipewire property "port.name" being present (with any value):
|
||||
|
|
@ -770,6 +770,8 @@ wp_object_interest_matches_full (WpObjectInterest * self,
|
|||
if (!pw_global_props && WP_IS_SESSION_ITEM (object)) {
|
||||
WpSessionItem *si = (WpSessionItem *) object;
|
||||
pw_global_props = props = wp_session_item_get_properties (si);
|
||||
if (!pw_props)
|
||||
pw_props = props;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -879,3 +881,51 @@ wp_object_interest_matches_full (WpObjectInterest * self,
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Finds all the defined constraint values for a subject in \a self.
|
||||
*
|
||||
* A defined constraint value is the value of a constraint with the 'equal' or
|
||||
* 'in-list' verb, because the full value must be defined with those verbs. This
|
||||
* can be useful for cases where we want to enumerate interests that are
|
||||
* interested in specific subjects.
|
||||
*
|
||||
* \ingroup wpobjectinterest
|
||||
* \param self the object interest
|
||||
* \param type the constraint type
|
||||
* \param subject the subject that the constraint applies to
|
||||
* \returns (element-type GVariant) (transfer full) (nullable): the defined
|
||||
* constraint values for this object interest.
|
||||
* \since 0.5.13
|
||||
*/
|
||||
GPtrArray *
|
||||
wp_object_interest_find_defined_constraint_values (WpObjectInterest * self,
|
||||
WpConstraintType type, const gchar * subject)
|
||||
{
|
||||
GPtrArray *res = g_ptr_array_new_with_free_func (
|
||||
(GDestroyNotify)g_variant_unref);
|
||||
struct constraint *c;
|
||||
|
||||
pw_array_for_each (c, &self->constraints) {
|
||||
if ((c->type == type || WP_CONSTRAINT_TYPE_NONE == type) &&
|
||||
g_str_equal (c->subject, subject)) {
|
||||
switch (c->verb) {
|
||||
case WP_CONSTRAINT_VERB_EQUALS:
|
||||
g_ptr_array_add (res, g_variant_ref (c->value));
|
||||
break;
|
||||
case WP_CONSTRAINT_VERB_IN_LIST: {
|
||||
GVariantIter iter;
|
||||
GVariant *child;
|
||||
g_variant_iter_init (&iter, c->value);
|
||||
while ((child = g_variant_iter_next_value (&iter)))
|
||||
g_ptr_array_add (res, child);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,18 +67,17 @@ typedef enum { /*< flags >*/
|
|||
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES = (1 << 1),
|
||||
WP_INTEREST_MATCH_PW_PROPERTIES = (1 << 2),
|
||||
WP_INTEREST_MATCH_G_PROPERTIES = (1 << 3),
|
||||
} WpInterestMatch;
|
||||
|
||||
/*!
|
||||
* \brief Special WpInterestMatch value that indicates that all constraints
|
||||
* have been matched
|
||||
* \ingroup wpobjectinterest
|
||||
*/
|
||||
#define WP_INTEREST_MATCH_ALL \
|
||||
(WP_INTEREST_MATCH_GTYPE | \
|
||||
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES | \
|
||||
WP_INTEREST_MATCH_PW_PROPERTIES | \
|
||||
WP_INTEREST_MATCH_G_PROPERTIES)
|
||||
/*!
|
||||
* Special WpInterestMatch value that indicates that all constraints
|
||||
* have been matched
|
||||
*/
|
||||
WP_INTEREST_MATCH_ALL =
|
||||
(WP_INTEREST_MATCH_GTYPE |
|
||||
WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
|
||||
WP_INTEREST_MATCH_PW_PROPERTIES |
|
||||
WP_INTEREST_MATCH_G_PROPERTIES),
|
||||
} WpInterestMatch;
|
||||
|
||||
/*!
|
||||
* \brief Flags to alter the behaviour of wp_object_interest_matches_full()
|
||||
|
|
@ -86,7 +85,7 @@ typedef enum { /*< flags >*/
|
|||
*/
|
||||
typedef enum { /*< flags >*/
|
||||
WP_INTEREST_MATCH_FLAGS_NONE = 0,
|
||||
/*! check all the constraints instead of returning after the first mis-match */
|
||||
/*! check all the constraints instead of returning after the first mismatch */
|
||||
WP_INTEREST_MATCH_FLAGS_CHECK_ALL = (1 << 0),
|
||||
} WpInterestMatchFlags;
|
||||
|
||||
|
|
@ -131,6 +130,10 @@ WpInterestMatch wp_object_interest_matches_full (WpObjectInterest * self,
|
|||
WpInterestMatchFlags flags, GType object_type, gpointer object,
|
||||
WpProperties * pw_props, WpProperties * pw_global_props);
|
||||
|
||||
WP_API
|
||||
GPtrArray * wp_object_interest_find_defined_constraint_values (
|
||||
WpObjectInterest * self, WpConstraintType type, const gchar * subject);
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)
|
||||
|
||||
G_END_DECLS
|
||||
|
|
|
|||
|
|
@ -587,7 +587,7 @@ wp_object_manager_lookup (WpObjectManager * self, GType gtype, ...)
|
|||
*
|
||||
* \ingroup wpobjectmanager
|
||||
* \param self the object manager
|
||||
* \param interest (transfer full): the interst
|
||||
* \param interest (transfer full): the interest
|
||||
* \returns (type GObject)(transfer full)(nullable): the first managed object
|
||||
* that matches the lookup interest, or NULL if no object matches
|
||||
*/
|
||||
|
|
@ -672,7 +672,13 @@ idle_emit_objects_changed (WpObjectManager * self)
|
|||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void
|
||||
/*!
|
||||
* \brief Checks if the object manager should emit the 'objects-changed' signal
|
||||
* \private
|
||||
* \ingroup wpobjectmanager
|
||||
* \param self the object manager
|
||||
*/
|
||||
void
|
||||
wp_object_manager_maybe_objects_changed (WpObjectManager * self)
|
||||
{
|
||||
wp_trace_object (self, "pending:%u changed:%d idle_source:%p installed:%d",
|
||||
|
|
@ -724,8 +730,15 @@ wp_object_manager_maybe_objects_changed (WpObjectManager * self)
|
|||
}
|
||||
}
|
||||
|
||||
/* caller must also call wp_object_manager_maybe_objects_changed() after */
|
||||
static void
|
||||
/*!
|
||||
* \brief Adds an object to the object manager.
|
||||
* \private
|
||||
* \ingroup wpobjectmanager
|
||||
* \param self the object manager
|
||||
* \param object (transfer none): the object to add
|
||||
* \note caller must also call wp_object_manager_maybe_objects_changed() after
|
||||
*/
|
||||
void
|
||||
wp_object_manager_add_object (WpObjectManager * self, gpointer object)
|
||||
{
|
||||
if (wp_object_manager_is_interested_in_object (self, object)) {
|
||||
|
|
@ -736,8 +749,15 @@ wp_object_manager_add_object (WpObjectManager * self, gpointer object)
|
|||
}
|
||||
}
|
||||
|
||||
/* caller must also call wp_object_manager_maybe_objects_changed() after */
|
||||
static void
|
||||
/*!
|
||||
* \brief Removes an object from the object manager.
|
||||
* \private
|
||||
* \ingroup wpobjectmanager
|
||||
* \param self the object manager
|
||||
* \param object the object to remove
|
||||
* \note caller must also call wp_object_manager_maybe_objects_changed() after
|
||||
*/
|
||||
void
|
||||
wp_object_manager_rm_object (WpObjectManager * self, gpointer object)
|
||||
{
|
||||
guint index;
|
||||
|
|
@ -765,8 +785,15 @@ on_proxy_ready (GObject * proxy, GAsyncResult * res, gpointer data)
|
|||
wp_object_manager_maybe_objects_changed (self);
|
||||
}
|
||||
|
||||
/* caller must also call wp_object_manager_maybe_objects_changed() after */
|
||||
static void
|
||||
/*!
|
||||
* \brief Adds a global object to the object manager.
|
||||
* \private
|
||||
* \ingroup wpobjectmanager
|
||||
* \param self the object manager
|
||||
* \param global the global object to add
|
||||
* \note caller must also call wp_object_manager_maybe_objects_changed() after
|
||||
*/
|
||||
void
|
||||
wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
|
||||
{
|
||||
WpProxyFeatures features = 0;
|
||||
|
|
@ -795,405 +822,6 @@ wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* WpRegistry:
|
||||
*
|
||||
* The registry keeps track of registered objects on the wireplumber core.
|
||||
* There are 3 kinds of registered objects:
|
||||
*
|
||||
* 1) PipeWire global objects, which live in another process.
|
||||
*
|
||||
* These objects are represented by a WpGlobal with the
|
||||
* WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag set. They appear when
|
||||
* the registry_global() event is fired and are removed by
|
||||
* registry_global_remove(). These objects do not have an associated
|
||||
* WpProxy, unless there is at least one WpObjectManager that is interested
|
||||
* in them. In this case, a WpProxy is constructed and it is owned by the
|
||||
* WpGlobal until the global is removed by the registry_global_remove() event.
|
||||
*
|
||||
* 2) PipeWire global objects, which were constructed by this process, either
|
||||
* by calling into a remove factory (see wp_node_new_from_factory()) or
|
||||
* by exporting a local object (WpImplSession etc...).
|
||||
*
|
||||
* These objects are also represented by a WpGlobal, which may however be
|
||||
* constructed before they appear on the registry. The associated WpProxy
|
||||
* calls into wp_registry_prepare_new_global() at the time it receives
|
||||
* the 'bound' event and creates a global that has the
|
||||
* WP_GLOBAL_FLAG_OWNED_BY_PROXY flag enabled. As the flag name suggests,
|
||||
* these globals are "owned" by the WpProxy and the WpGlobal has no ref
|
||||
* on the WpProxy itself. This allows destroying the proxy in client code
|
||||
* by dropping its last reference.
|
||||
*
|
||||
* Normally, these global objects also appear on the pipewire registry. When
|
||||
* this happens, the WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag is also added
|
||||
* and that keeps an additional reference on the global (both flags must
|
||||
* be dropped before the WpGlobal is destroyed).
|
||||
*
|
||||
* In some cases, such an object might appear first on the registry and
|
||||
* then receive the 'bound' event. In order to handle this situation, globals
|
||||
* are not advertised immediately when they appear on the registry, but
|
||||
* they are added on a tmp_globals list instead, which is emptied on the
|
||||
* next core sync. In all cases, the proxy 'bound' and the registry 'global'
|
||||
* events will be fired in the same sync cycle, so we can catch a late
|
||||
* 'bound' event and still associate the proxy with the WpGlobal before
|
||||
* object managers are notified about the existence of this global.
|
||||
*
|
||||
* 3) WirePlumber global objects (WpModule, WpPlugin, WpSiFactory).
|
||||
*
|
||||
* These are local objects that have nothing to do with PipeWire. They do not
|
||||
* have a global id and they are also not subclasses of WpProxy. The registry
|
||||
* always owns a reference on them, so that they are kept alive for as long
|
||||
* as the WpCore is alive.
|
||||
*/
|
||||
|
||||
#undef G_LOG_DOMAIN
|
||||
#define G_LOG_DOMAIN "wp-registry"
|
||||
|
||||
void
|
||||
wp_registry_notify_add_object (WpRegistry *self, gpointer object)
|
||||
{
|
||||
for (guint i = 0; i < self->object_managers->len; i++) {
|
||||
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
|
||||
wp_object_manager_add_object (om, object);
|
||||
wp_object_manager_maybe_objects_changed (om);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_notify_rm_object (WpRegistry *self, gpointer object)
|
||||
{
|
||||
for (guint i = 0; i < self->object_managers->len; i++) {
|
||||
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
|
||||
wp_object_manager_rm_object (om, object);
|
||||
wp_object_manager_maybe_objects_changed (om);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
object_manager_destroyed (gpointer data, GObject * om)
|
||||
{
|
||||
WpRegistry *self = data;
|
||||
g_ptr_array_remove_fast (self->object_managers, om);
|
||||
}
|
||||
|
||||
/* find the subclass of WpPipewireGloabl that can handle
|
||||
the given pipewire interface type of the given version */
|
||||
static inline GType
|
||||
find_proxy_instance_type (const char * type, guint32 version)
|
||||
{
|
||||
g_autofree GType *children;
|
||||
guint n_children;
|
||||
|
||||
children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children);
|
||||
|
||||
for (guint i = 0; i < n_children; i++) {
|
||||
WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]);
|
||||
if (g_strcmp0 (klass->pw_iface_type, type) == 0 &&
|
||||
klass->pw_iface_version == version) {
|
||||
g_type_class_unref (klass);
|
||||
return children[i];
|
||||
}
|
||||
|
||||
g_type_class_unref (klass);
|
||||
}
|
||||
|
||||
return WP_TYPE_GLOBAL_PROXY;
|
||||
}
|
||||
|
||||
/* called by the registry when a global appears */
|
||||
static void
|
||||
registry_global (void *data, uint32_t id, uint32_t permissions,
|
||||
const char *type, uint32_t version, const struct spa_dict *props)
|
||||
{
|
||||
WpRegistry *self = data;
|
||||
GType gtype = find_proxy_instance_type (type, version);
|
||||
|
||||
wp_debug_object (wp_registry_get_core (self),
|
||||
"global:%u perm:0x%x type:%s/%u -> %s",
|
||||
id, permissions, type, version, g_type_name (gtype));
|
||||
|
||||
wp_registry_prepare_new_global (self, id, permissions,
|
||||
WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY, gtype, NULL, props, NULL);
|
||||
}
|
||||
|
||||
/* called by the registry when a global is removed */
|
||||
static void
|
||||
registry_global_remove (void *data, uint32_t id)
|
||||
{
|
||||
WpRegistry *self = data;
|
||||
WpGlobal *global = NULL;
|
||||
|
||||
if (id < self->globals->len)
|
||||
global = g_ptr_array_index (self->globals, id);
|
||||
|
||||
/* if not found, look in the tmp_globals, as it may still not be exposed */
|
||||
if (!global) {
|
||||
for (guint i = 0; i < self->tmp_globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
|
||||
if (g->id == id) {
|
||||
global = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_return_if_fail (global &&
|
||||
global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||
|
||||
wp_debug_object (wp_registry_get_core (self),
|
||||
"global removed:%u type:%s", id, g_type_name (global->type));
|
||||
|
||||
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||
}
|
||||
|
||||
static const struct pw_registry_events registry_events = {
|
||||
PW_VERSION_REGISTRY_EVENTS,
|
||||
.global = registry_global,
|
||||
.global_remove = registry_global_remove,
|
||||
};
|
||||
|
||||
void
|
||||
wp_registry_init (WpRegistry *self)
|
||||
{
|
||||
self->globals =
|
||||
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
||||
self->tmp_globals =
|
||||
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
||||
self->objects = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
self->object_managers = g_ptr_array_new ();
|
||||
self->features = g_ptr_array_new_with_free_func (g_free);
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_clear (WpRegistry *self)
|
||||
{
|
||||
wp_registry_detach (self);
|
||||
g_clear_pointer (&self->globals, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->tmp_globals, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->features, g_ptr_array_unref);
|
||||
|
||||
/* remove all the registered objects
|
||||
this will normally also destroy the object managers, eventually, since
|
||||
they are normally ref'ed by modules, which are registered objects */
|
||||
{
|
||||
g_autoptr (GPtrArray) objlist = g_steal_pointer (&self->objects);
|
||||
|
||||
while (objlist->len > 0) {
|
||||
g_autoptr (GObject) object = g_ptr_array_steal_index_fast (objlist,
|
||||
objlist->len - 1);
|
||||
wp_registry_notify_rm_object (self, object);
|
||||
}
|
||||
}
|
||||
|
||||
/* in case there are any object managers left,
|
||||
remove the weak ref on them and let them be... */
|
||||
{
|
||||
g_autoptr (GPtrArray) object_mgrs;
|
||||
GObject *om;
|
||||
|
||||
object_mgrs = g_steal_pointer (&self->object_managers);
|
||||
|
||||
while (object_mgrs->len > 0) {
|
||||
om = g_ptr_array_steal_index_fast (object_mgrs, object_mgrs->len - 1);
|
||||
g_object_weak_unref (om, object_manager_destroyed, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_attach (WpRegistry *self, struct pw_core *pw_core)
|
||||
{
|
||||
self->pw_registry = pw_core_get_registry (pw_core,
|
||||
PW_VERSION_REGISTRY, 0);
|
||||
pw_registry_add_listener (self->pw_registry, &self->listener,
|
||||
®istry_events, self);
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_detach (WpRegistry *self)
|
||||
{
|
||||
if (self->pw_registry) {
|
||||
spa_hook_remove (&self->listener);
|
||||
pw_proxy_destroy ((struct pw_proxy *) self->pw_registry);
|
||||
self->pw_registry = NULL;
|
||||
}
|
||||
|
||||
/* remove pipewire globals */
|
||||
GPtrArray *objlist = self->globals;
|
||||
while (objlist && objlist->len > 0) {
|
||||
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
|
||||
objlist->len - 1);
|
||||
|
||||
if (!global)
|
||||
continue;
|
||||
|
||||
if (global->proxy)
|
||||
wp_registry_notify_rm_object (self, global->proxy);
|
||||
|
||||
/* remove the APPEARS_ON_REGISTRY flag to unref the proxy if it is owned
|
||||
by the registry; set registry to NULL to avoid further interference */
|
||||
global->registry = NULL;
|
||||
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||
|
||||
/* the registry's ref on global is dropped here; it may still live if
|
||||
there is a proxy that owns a ref on it, but global->registry is set
|
||||
to NULL, so there is no further interference */
|
||||
}
|
||||
|
||||
/* drop tmp globals as well */
|
||||
objlist = self->tmp_globals;
|
||||
while (objlist && objlist->len > 0) {
|
||||
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
|
||||
objlist->len - 1);
|
||||
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
expose_tmp_globals (WpCore *core)
|
||||
{
|
||||
WpRegistry *self = wp_core_get_registry (core);
|
||||
g_autoptr (GPtrArray) tmp_globals = NULL;
|
||||
g_autoptr (GPtrArray) object_managers = NULL;
|
||||
|
||||
/* in case the registry was cleared in the meantime... */
|
||||
if (G_UNLIKELY (!self->tmp_globals))
|
||||
return G_SOURCE_REMOVE;
|
||||
|
||||
/* steal the tmp_globals list and replace it with an empty one */
|
||||
tmp_globals = self->tmp_globals;
|
||||
self->tmp_globals =
|
||||
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
||||
|
||||
wp_debug_object (core, "exposing %u new globals", tmp_globals->len);
|
||||
|
||||
/* traverse in the order that the globals appeared on the registry */
|
||||
for (guint i = 0; i < tmp_globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
|
||||
|
||||
/* if global was already removed, drop it */
|
||||
if (g->flags == 0 || g->id == SPA_ID_INVALID)
|
||||
continue;
|
||||
|
||||
/* if old global is owned by proxy, remove it */
|
||||
if (self->globals->len > g->id) {
|
||||
WpGlobal *old_g = g_ptr_array_index (self->globals, g->id);
|
||||
if (old_g && (old_g->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY))
|
||||
wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
|
||||
}
|
||||
|
||||
g_return_val_if_fail (self->globals->len <= g->id ||
|
||||
g_ptr_array_index (self->globals, g->id) == NULL, G_SOURCE_REMOVE);
|
||||
|
||||
/* set the registry, so that wp_global_rm_flag() can work full-scale */
|
||||
g->registry = self;
|
||||
|
||||
/* store it in the globals list */
|
||||
if (self->globals->len <= g->id)
|
||||
g_ptr_array_set_size (self->globals, g->id + 1);
|
||||
g_ptr_array_index (self->globals, g->id) = wp_global_ref (g);
|
||||
}
|
||||
|
||||
object_managers = g_ptr_array_copy (self->object_managers,
|
||||
(GCopyFunc) g_object_ref, NULL);
|
||||
g_ptr_array_set_free_func (object_managers, g_object_unref);
|
||||
|
||||
/* notify object managers */
|
||||
for (guint i = 0; i < object_managers->len; i++) {
|
||||
WpObjectManager *om = g_ptr_array_index (object_managers, i);
|
||||
|
||||
for (guint i = 0; i < tmp_globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
|
||||
|
||||
/* if global was already removed, drop it */
|
||||
if (g->flags == 0 || g->id == SPA_ID_INVALID)
|
||||
continue;
|
||||
|
||||
wp_object_manager_add_global (om, g);
|
||||
}
|
||||
wp_object_manager_maybe_objects_changed (om);
|
||||
}
|
||||
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/*
|
||||
* \param new_global (out) (transfer full) (optional): the new global
|
||||
*
|
||||
* This is normally called up to 2 times in the same sync cycle:
|
||||
* one from registry_global(), another from the proxy bound event
|
||||
* Unfortunately the order in which those 2 events happen is specific
|
||||
* to the implementation of the object, which is why this is implemented
|
||||
* with a temporary globals list that get exposed later to the object managers
|
||||
*/
|
||||
void
|
||||
wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
|
||||
guint32 permissions, guint32 flag, GType type,
|
||||
WpGlobalProxy *proxy, const struct spa_dict *props,
|
||||
WpGlobal ** new_global)
|
||||
{
|
||||
g_autoptr (WpGlobal) global = NULL;
|
||||
WpCore *core = wp_registry_get_core (self);
|
||||
|
||||
g_return_if_fail (flag != 0);
|
||||
|
||||
for (guint i = 0; i < self->tmp_globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
|
||||
if (g->id == id) {
|
||||
global = wp_global_ref (g);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wp_debug_object (core, "%s WpGlobal:%u type:%s proxy:%p",
|
||||
global ? "reuse" : "new", id, g_type_name (type),
|
||||
(global && global->proxy) ? global->proxy : proxy);
|
||||
|
||||
if (!global) {
|
||||
global = g_rc_box_new0 (WpGlobal);
|
||||
global->flags = flag;
|
||||
global->id = id;
|
||||
global->type = type;
|
||||
global->permissions = permissions;
|
||||
global->properties = props ?
|
||||
wp_properties_new_copy_dict (props) : wp_properties_new_empty ();
|
||||
global->proxy = proxy;
|
||||
g_ptr_array_add (self->tmp_globals, wp_global_ref (global));
|
||||
|
||||
/* ensure we have 'object.id' so that we can filter by id on object managers */
|
||||
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, "%u", global->id);
|
||||
|
||||
/* schedule exposing when adding the first global */
|
||||
if (self->tmp_globals->len == 1) {
|
||||
wp_core_idle_add_closure (core, NULL,
|
||||
g_cclosure_new_object (G_CALLBACK (expose_tmp_globals), G_OBJECT (core)));
|
||||
}
|
||||
} else {
|
||||
/* store the most permissive permissions */
|
||||
if (permissions > global->permissions)
|
||||
global->permissions = permissions;
|
||||
|
||||
global->flags |= flag;
|
||||
|
||||
/* store the most deep type (i.e. WpImplNode instead of WpNode),
|
||||
so that object-manager interests can work more accurately
|
||||
if the interest is on a specific subclass */
|
||||
if (g_type_depth (type) > g_type_depth (global->type))
|
||||
global->type = type;
|
||||
|
||||
if (proxy) {
|
||||
g_return_if_fail (global->proxy == NULL);
|
||||
global->proxy = proxy;
|
||||
}
|
||||
|
||||
if (props)
|
||||
wp_properties_update_from_dict (global->properties, props);
|
||||
}
|
||||
|
||||
if (new_global)
|
||||
*new_global = g_steal_pointer (&global);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Installs the object manager on this core, activating its internal
|
||||
* management engine.
|
||||
|
|
@ -1209,111 +837,12 @@ void
|
|||
wp_core_install_object_manager (WpCore * self, WpObjectManager * om)
|
||||
{
|
||||
WpRegistry *reg;
|
||||
guint i;
|
||||
|
||||
g_return_if_fail (WP_IS_CORE (self));
|
||||
g_return_if_fail (WP_IS_OBJECT_MANAGER (om));
|
||||
|
||||
reg = wp_core_get_registry (self);
|
||||
|
||||
g_object_weak_ref (G_OBJECT (om), object_manager_destroyed, reg);
|
||||
g_ptr_array_add (reg->object_managers, om);
|
||||
g_weak_ref_set (&om->core, self);
|
||||
|
||||
/* add pre-existing objects to the object manager,
|
||||
in case it's interested in them */
|
||||
for (i = 0; i < reg->globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (reg->globals, i);
|
||||
/* check if null because the globals array can have gaps */
|
||||
if (g)
|
||||
wp_object_manager_add_global (om, g);
|
||||
}
|
||||
for (i = 0; i < reg->objects->len; i++) {
|
||||
GObject *o = g_ptr_array_index (reg->objects, i);
|
||||
wp_object_manager_add_object (om, o);
|
||||
}
|
||||
|
||||
wp_object_manager_maybe_objects_changed (om);
|
||||
}
|
||||
|
||||
/* WpGlobal */
|
||||
|
||||
G_DEFINE_BOXED_TYPE (WpGlobal, wp_global, wp_global_ref, wp_global_unref)
|
||||
|
||||
void
|
||||
wp_global_rm_flag (WpGlobal *global, guint rm_flag)
|
||||
{
|
||||
WpRegistry *reg = global->registry;
|
||||
guint32 id = global->id;
|
||||
|
||||
/* no flag to remove */
|
||||
if (!(global->flags & rm_flag))
|
||||
return;
|
||||
|
||||
wp_trace_boxed (WP_TYPE_GLOBAL, global,
|
||||
"remove global %u flag 0x%x [flags:0x%x, reg:%p]",
|
||||
id, rm_flag, global->flags, reg);
|
||||
|
||||
/* global was owned by the proxy; by removing the flag, we clear out
|
||||
also the proxy pointer, which is presumably no longer valid and we
|
||||
notify all listeners that the proxy is gone */
|
||||
if (rm_flag == WP_GLOBAL_FLAG_OWNED_BY_PROXY) {
|
||||
global->flags &= ~WP_GLOBAL_FLAG_OWNED_BY_PROXY;
|
||||
if (reg && global->proxy) {
|
||||
wp_registry_notify_rm_object (reg, global->proxy);
|
||||
}
|
||||
global->proxy = NULL;
|
||||
}
|
||||
/* registry removed the global */
|
||||
else if (rm_flag == WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) {
|
||||
global->flags &= ~WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY;
|
||||
|
||||
/* destroy the proxy if it exists */
|
||||
if (global->proxy) {
|
||||
/* steal the proxy to avoid calling wp_registry_notify_rm_object()
|
||||
again while removing OWNED_BY_PROXY;
|
||||
keep a temporary ref so that _deactivate() doesn't crash in case the
|
||||
pw-proxy-destroyed signal causes external references to be dropped */
|
||||
g_autoptr (WpGlobalProxy) proxy =
|
||||
g_object_ref (g_steal_pointer (&global->proxy));
|
||||
|
||||
/* notify all listeners that the proxy is gone */
|
||||
if (reg)
|
||||
wp_registry_notify_rm_object (reg, proxy);
|
||||
|
||||
/* remove FEATURE_BOUND to destroy the underlying pw_proxy */
|
||||
wp_object_deactivate (WP_OBJECT (proxy), WP_PROXY_FEATURE_BOUND);
|
||||
|
||||
/* stop all in-progress activations */
|
||||
wp_object_abort_activation (WP_OBJECT (proxy), "PipeWire proxy removed");
|
||||
|
||||
/* if the proxy is not owning the global, unref it */
|
||||
if (global->flags == 0)
|
||||
g_object_unref (proxy);
|
||||
}
|
||||
|
||||
/* It's possible to receive consecutive {add, remove, add} events for the
|
||||
* same id. Since the WpGlobal might not be destroyed immediately below,
|
||||
* (e.g. it's in tmp_globals list), we must invalidate the id now, so that
|
||||
* this WpGlobal is not used in reference to objects added later.
|
||||
*/
|
||||
global->id = SPA_ID_INVALID;
|
||||
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, NULL);
|
||||
}
|
||||
|
||||
/* drop the registry's ref on global when it does not appear on the registry anymore */
|
||||
if (!(global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) && reg) {
|
||||
g_clear_pointer (&g_ptr_array_index (reg->globals, id), wp_global_unref);
|
||||
}
|
||||
}
|
||||
|
||||
struct pw_proxy *
|
||||
wp_global_bind (WpGlobal * global)
|
||||
{
|
||||
g_return_val_if_fail (global->proxy, NULL);
|
||||
g_return_val_if_fail (global->registry, NULL);
|
||||
|
||||
WpProxyClass *klass = WP_PROXY_GET_CLASS (global->proxy);
|
||||
return pw_registry_bind (global->registry->pw_registry, global->id,
|
||||
klass->pw_iface_type, klass->pw_iface_version, 0);
|
||||
reg = wp_core_get_registry (self);
|
||||
wp_registry_install_object_manager (reg, om);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,22 @@ WP_API
|
|||
gpointer wp_object_manager_lookup_full (WpObjectManager * self,
|
||||
WpObjectInterest * interest);
|
||||
|
||||
/* private */
|
||||
|
||||
typedef struct _WpGlobal WpGlobal;
|
||||
|
||||
WP_PRIVATE_API
|
||||
void wp_object_manager_maybe_objects_changed (WpObjectManager * self);
|
||||
|
||||
WP_PRIVATE_API
|
||||
void wp_object_manager_add_object (WpObjectManager * self, gpointer object);
|
||||
|
||||
WP_PRIVATE_API
|
||||
void wp_object_manager_rm_object (WpObjectManager * self, gpointer object);
|
||||
|
||||
WP_PRIVATE_API
|
||||
void wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -441,8 +441,8 @@ on_transition_completed (WpTransition * transition, GParamSpec * param,
|
|||
* \param self the object
|
||||
* \param features the features to enable
|
||||
* \param cancellable (nullable): a cancellable for the async operation
|
||||
* \param callback (scope async): a function to call when activation is complete
|
||||
* \param user_data (closure): data for \a callback
|
||||
* \param callback (scope async)(closure user_data): a function to call when activation is complete
|
||||
* \param user_data data for \a callback
|
||||
*/
|
||||
void
|
||||
wp_object_activate (WpObject * self,
|
||||
|
|
|
|||
707
lib/wp/permission-manager.c
Normal file
707
lib/wp/permission-manager.c
Normal file
|
|
@ -0,0 +1,707 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2026 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <pipewire/permission.h>
|
||||
#include <pipewire/pipewire.h>
|
||||
|
||||
#include "private/permission-manager.h"
|
||||
#include "permission-manager.h"
|
||||
#include "proxy-interfaces.h"
|
||||
#include "object-manager.h"
|
||||
#include "json-utils.h"
|
||||
#include "error.h"
|
||||
#include "core.h"
|
||||
#include "log.h"
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-permission-manager")
|
||||
|
||||
/*! \defgroup wppermissionmanager WpPermissionManager */
|
||||
/*!
|
||||
* \struct WpPermissionManager
|
||||
*
|
||||
* The WpPermissionManager class is in charge of updating automatically
|
||||
* permissions on interested objects every time they are added or removed for
|
||||
* a particular client.
|
||||
*
|
||||
* WpPermissionManager API.
|
||||
*/
|
||||
|
||||
typedef struct _PermissionMatch PermissionMatch;
|
||||
struct _PermissionMatch
|
||||
{
|
||||
guint32 id;
|
||||
guint32 permissions;
|
||||
GClosure *closure;
|
||||
WpObjectInterest *interest;
|
||||
WpSpaJson *rules;
|
||||
};
|
||||
|
||||
static guint
|
||||
get_next_id ()
|
||||
{
|
||||
static guint32 next_id = 0;
|
||||
g_atomic_int_inc (&next_id);
|
||||
return next_id;
|
||||
}
|
||||
|
||||
static PermissionMatch *
|
||||
permission_match_new (guint32 perms, GClosure *closure,
|
||||
WpObjectInterest * interest, WpSpaJson * rules)
|
||||
{
|
||||
PermissionMatch *match = g_new0 (PermissionMatch, 1);
|
||||
match->id = get_next_id ();
|
||||
match->permissions = perms;
|
||||
match->closure = closure ? g_closure_ref (closure) : NULL;
|
||||
match->interest = interest ? wp_object_interest_ref (interest) : NULL;
|
||||
match->rules = rules ? wp_spa_json_ref (rules) : NULL;
|
||||
return match;
|
||||
}
|
||||
|
||||
static void
|
||||
permission_interest_free (PermissionMatch *self)
|
||||
{
|
||||
g_clear_pointer (&self->closure, g_closure_unref);
|
||||
g_clear_pointer (&self->interest, wp_object_interest_unref);
|
||||
g_clear_pointer (&self->rules, wp_spa_json_unref);
|
||||
g_free (self);
|
||||
}
|
||||
|
||||
struct _WpPermissionManager
|
||||
{
|
||||
WpObject parent;
|
||||
|
||||
guint32 default_perms;
|
||||
guint32 core_perms;
|
||||
GPtrArray *clients;
|
||||
GHashTable *matches;
|
||||
|
||||
WpObjectManager *om;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE (WpPermissionManager, wp_permission_manager, WP_TYPE_OBJECT)
|
||||
|
||||
static void
|
||||
wp_permission_manager_init (WpPermissionManager * self)
|
||||
{
|
||||
/* Init default permissions to all */
|
||||
self->default_perms = PW_PERM_R | PW_PERM_W | PW_PERM_X;
|
||||
|
||||
/* Core permissions not set by default (inherit from default_perms) */
|
||||
self->core_perms = PW_PERM_INVALID;
|
||||
|
||||
/* Init permission interests table */
|
||||
self->matches = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
|
||||
(GDestroyNotify)permission_interest_free);
|
||||
|
||||
/* Init clients list */
|
||||
self->clients = g_ptr_array_new_with_free_func (
|
||||
(GDestroyNotify) g_object_unref);
|
||||
}
|
||||
|
||||
enum {
|
||||
STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START,
|
||||
};
|
||||
|
||||
static WpObjectFeatures
|
||||
wp_permission_manager_get_supported_features (WpObject * self)
|
||||
{
|
||||
return WP_PERMISSION_MANAGER_LOADED;
|
||||
}
|
||||
|
||||
static guint
|
||||
wp_permission_manager_activate_get_next_step (WpObject * self,
|
||||
WpFeatureActivationTransition * transition, guint step,
|
||||
WpObjectFeatures missing)
|
||||
{
|
||||
g_return_val_if_fail (missing == WP_PERMISSION_MANAGER_LOADED,
|
||||
WP_TRANSITION_STEP_ERROR);
|
||||
|
||||
return STEP_LOAD;
|
||||
}
|
||||
|
||||
static guint32
|
||||
invoke_permissions_closure (WpPermissionManager *self, WpClient *client,
|
||||
WpGlobalProxy *object, GClosure *closure)
|
||||
{
|
||||
GValue args[3] = { G_VALUE_INIT, G_VALUE_INIT, G_VALUE_INIT };
|
||||
GValue ret = G_VALUE_INIT;
|
||||
guint32 perms;
|
||||
|
||||
g_value_init (&args[0], WP_TYPE_PERMISSION_MANAGER);
|
||||
g_value_set_object (&args[0], self);
|
||||
g_value_init (&args[1], WP_TYPE_CLIENT);
|
||||
g_value_set_object (&args[1], client);
|
||||
g_value_init (&args[2], WP_TYPE_GLOBAL_PROXY);
|
||||
g_value_set_object (&args[2], object);
|
||||
g_value_init (&ret, G_TYPE_UINT);
|
||||
|
||||
g_closure_invoke (closure, &ret, 3, args, NULL);
|
||||
perms = g_value_get_uint (&ret);
|
||||
|
||||
g_value_unset (&args[0]);
|
||||
g_value_unset (&args[1]);
|
||||
g_value_unset (&args[2]);
|
||||
g_value_unset (&ret);
|
||||
|
||||
return perms;
|
||||
}
|
||||
|
||||
typedef struct _MatchRulesCallbackData MatchRulesCallbackData;
|
||||
struct _MatchRulesCallbackData {
|
||||
gboolean matched;
|
||||
guint32 perms;
|
||||
};
|
||||
|
||||
static gboolean
|
||||
match_rules_cb (gpointer data, const gchar * action, WpSpaJson * value,
|
||||
GError ** e)
|
||||
{
|
||||
MatchRulesCallbackData *cb_data = (MatchRulesCallbackData *)data;
|
||||
g_autofree gchar *perms_str = NULL;
|
||||
guint32 perms = 0;
|
||||
|
||||
if (!g_str_equal (action, "set-permissions")) {
|
||||
if (e)
|
||||
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"Action name '%s' is not valid", action);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!wp_spa_json_is_string (value)) {
|
||||
if (e)
|
||||
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"Action '%s' must be a string", action);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Parse permissions */
|
||||
perms_str = wp_spa_json_parse_string (value);
|
||||
if (g_strcmp0 (perms_str, "all") == 0) {
|
||||
perms = PW_PERM_ALL;
|
||||
} else if (perms_str) {
|
||||
for (guint i = 0; i < strlen (perms_str); i++) {
|
||||
switch (perms_str[i]) {
|
||||
case 'r': perms |= PW_PERM_R; break;
|
||||
case 'w': perms |= PW_PERM_W; break;
|
||||
case 'x': perms |= PW_PERM_X; break;
|
||||
case 'm': perms |= PW_PERM_M; break;
|
||||
case 'l': perms |= PW_PERM_L; break;
|
||||
case '-': break;
|
||||
default: {
|
||||
if (e)
|
||||
g_set_error (e, WP_DOMAIN_LIBRARY,
|
||||
WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"Permissions '%s' are not valid", perms_str);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cb_data) {
|
||||
cb_data->matched = TRUE;
|
||||
cb_data->perms |= perms;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
get_rules_matched_object_permissions (WpPermissionManager *self,
|
||||
WpSpaJson *rules, WpGlobalProxy *object, guint32 *perms)
|
||||
{
|
||||
g_autoptr (GError) e = NULL;
|
||||
g_autoptr (WpProperties) gp_props = NULL;
|
||||
g_autoptr (WpProperties) po_props = NULL;
|
||||
MatchRulesCallbackData data = { FALSE, 0 };
|
||||
|
||||
/* Check global proxy properties */
|
||||
gp_props = wp_global_proxy_get_global_properties (object);
|
||||
if (gp_props && !wp_json_utils_match_rules (rules, gp_props, match_rules_cb,
|
||||
&data, &e))
|
||||
goto error;
|
||||
|
||||
/* Also check pipewire object properties if it is a pipewire object */
|
||||
if (WP_IS_PIPEWIRE_OBJECT (object)) {
|
||||
po_props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (object));
|
||||
if (po_props && !wp_json_utils_match_rules (rules, po_props, match_rules_cb,
|
||||
&data, &e))
|
||||
goto error;
|
||||
}
|
||||
|
||||
/* Set permissions if there was a match */
|
||||
if (data.matched && perms)
|
||||
*perms = data.perms;
|
||||
|
||||
return data.matched;
|
||||
|
||||
error:
|
||||
wp_warning_object (self, "Malformed JSON match rules: %s", e->message);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
get_matched_object_permissions (WpPermissionManager *self, PermissionMatch *m,
|
||||
WpClient *client, WpGlobalProxy *object, guint32 *perms)
|
||||
{
|
||||
/* Check interest */
|
||||
if (m->interest && wp_object_interest_matches (m->interest, object)) {
|
||||
if (!perms)
|
||||
return TRUE;
|
||||
*perms = m->closure ? invoke_permissions_closure (self, client, object,
|
||||
m->closure) : m->permissions;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Check rules */
|
||||
if (m->rules)
|
||||
return get_rules_matched_object_permissions (self, m->rules, object, perms);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static GArray *
|
||||
build_permissions_array (WpPermissionManager *self, WpClient *client)
|
||||
{
|
||||
g_autoptr (WpIterator) it = NULL;
|
||||
g_auto (GValue) value = G_VALUE_INIT;
|
||||
struct pw_permission def_perm = { PW_ID_ANY, self->default_perms };
|
||||
GArray *arr = g_array_new (FALSE, FALSE, sizeof (struct pw_permission));
|
||||
|
||||
/* Add default permissions */
|
||||
g_array_append_val (arr, def_perm);
|
||||
|
||||
/* Add core permissions if explicitly set (core is not in the OM since it is
|
||||
* implicit in the PipeWire connection and not sent through the registry) */
|
||||
if (self->core_perms != PW_PERM_INVALID) {
|
||||
struct pw_permission core_perm = { PW_ID_CORE, self->core_perms };
|
||||
g_array_append_val (arr, core_perm);
|
||||
}
|
||||
|
||||
/* Add object specific permissions in the array */
|
||||
it = wp_object_manager_new_iterator (self->om);
|
||||
for (; wp_iterator_next (it, &value); g_value_unset (&value)) {
|
||||
WpGlobalProxy *object = g_value_get_object (&value);
|
||||
GHashTableIter iter;
|
||||
PermissionMatch *match = NULL;
|
||||
g_hash_table_iter_init (&iter, self->matches);
|
||||
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&match)) {
|
||||
guint32 perms = PW_PERM_INVALID;
|
||||
if (get_matched_object_permissions (self, match, client, object, &perms)
|
||||
&& perms != PW_PERM_INVALID) {
|
||||
struct pw_permission obj_perm = { 0, };
|
||||
obj_perm.id = wp_proxy_get_bound_id (WP_PROXY (object));
|
||||
obj_perm.permissions = perms;
|
||||
g_array_append_val (arr, obj_perm);;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Merge permissions with same object ID */
|
||||
for (guint i = 0; i < arr->len; i++) {
|
||||
for (guint j = i + 1; j < arr->len; ) {
|
||||
struct pw_permission *a = &g_array_index (arr, struct pw_permission, i);
|
||||
struct pw_permission *b = &g_array_index (arr, struct pw_permission, j);
|
||||
if (a->id == b->id) {
|
||||
a->permissions |= b->permissions;
|
||||
g_array_remove_index (arr, j);
|
||||
} else {
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
static void
|
||||
update_client_permissions (WpPermissionManager *self, WpClient *client)
|
||||
{
|
||||
guint32 bound_id = 0;
|
||||
g_autoptr (GArray) perms = NULL;
|
||||
|
||||
/* Dont do anything if the permission manager is not activated */
|
||||
if (!(wp_object_get_active_features (WP_OBJECT (self)) &
|
||||
WP_PERMISSION_MANAGER_LOADED))
|
||||
return;
|
||||
|
||||
/* Make sure the client proxy is still valid */
|
||||
if (!wp_proxy_get_pw_proxy (WP_PROXY (client)))
|
||||
return;
|
||||
|
||||
bound_id = wp_proxy_get_bound_id (WP_PROXY (client));
|
||||
perms = build_permissions_array (self, client);
|
||||
|
||||
wp_info_object (self,
|
||||
"Updating permissions on client %u: any=%c%c%c%c%c len=%u",
|
||||
bound_id,
|
||||
!!(self->default_perms & PW_PERM_R) ? 'r' : '-',
|
||||
!!(self->default_perms & PW_PERM_W) ? 'w' : '-',
|
||||
!!(self->default_perms & PW_PERM_X) ? 'x' : '-',
|
||||
!!(self->default_perms & PW_PERM_M) ? 'm' : '-',
|
||||
!!(self->default_perms & PW_PERM_L) ? 'l' : '-',
|
||||
perms->len);
|
||||
|
||||
wp_client_update_permissions_array (client, perms->len,
|
||||
(const struct pw_permission *) perms->data);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
has_object_match (WpPermissionManager *self, WpGlobalProxy *object)
|
||||
{
|
||||
GHashTableIter iter;
|
||||
PermissionMatch *m = NULL;
|
||||
|
||||
g_hash_table_iter_init (&iter, self->matches);
|
||||
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&m)) {
|
||||
if (m->interest && wp_object_interest_matches (m->interest, object))
|
||||
return TRUE;
|
||||
if (m->rules && get_rules_matched_object_permissions (self, m->rules,
|
||||
object, NULL))
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
update_permissions (WpPermissionManager *self)
|
||||
{
|
||||
for (guint i = 0; i < self->clients->len; i++) {
|
||||
WpClient *client = g_ptr_array_index (self->clients, i);
|
||||
update_client_permissions (self, client);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_object_added_or_removed (WpObjectManager *om, WpGlobalProxy *object,
|
||||
gpointer d)
|
||||
{
|
||||
WpPermissionManager * self = WP_PERMISSION_MANAGER (d);
|
||||
|
||||
if (has_object_match (self, object))
|
||||
update_permissions (self);
|
||||
}
|
||||
|
||||
static void
|
||||
on_object_manager_installed (WpObjectManager *om, gpointer d)
|
||||
{
|
||||
WpTransition * transition = WP_TRANSITION (d);
|
||||
WpPermissionManager * self = wp_transition_get_source_object (transition);
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), WP_PERMISSION_MANAGER_LOADED, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_permission_manager_activate_execute_step (WpObject * object,
|
||||
WpFeatureActivationTransition * transition, guint step,
|
||||
WpObjectFeatures missing)
|
||||
{
|
||||
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
|
||||
g_autoptr (WpCore) core = wp_object_get_core (object);
|
||||
|
||||
switch (step) {
|
||||
case STEP_LOAD: {
|
||||
/* Install object manager */
|
||||
g_clear_object (&self->om);
|
||||
self->om = wp_object_manager_new ();
|
||||
wp_object_manager_add_interest (self->om, WP_TYPE_GLOBAL_PROXY, NULL);
|
||||
wp_object_manager_request_object_features (self->om,
|
||||
WP_TYPE_GLOBAL_PROXY, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
|
||||
g_signal_connect_object (self->om, "object-added",
|
||||
G_CALLBACK (on_object_added_or_removed), self, 0);
|
||||
g_signal_connect_object (self->om, "object-removed",
|
||||
G_CALLBACK (on_object_added_or_removed), self, 0);
|
||||
g_signal_connect_object (self->om, "installed",
|
||||
G_CALLBACK (on_object_manager_installed), transition, 0);
|
||||
wp_core_install_object_manager (core, self->om);
|
||||
break;
|
||||
}
|
||||
|
||||
case WP_TRANSITION_STEP_ERROR:
|
||||
break;
|
||||
default:
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
wp_permission_manager_deactivate (WpObject * object, WpObjectFeatures features)
|
||||
{
|
||||
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
|
||||
|
||||
g_clear_object (&self->om);
|
||||
|
||||
wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_permission_manager_finalize (GObject * object)
|
||||
{
|
||||
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
|
||||
|
||||
g_clear_pointer (&self->clients, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->matches, g_hash_table_unref);
|
||||
|
||||
g_clear_object (&self->om);
|
||||
|
||||
G_OBJECT_CLASS (wp_permission_manager_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
wp_permission_manager_class_init (WpPermissionManagerClass * klass)
|
||||
{
|
||||
GObjectClass * object_class = (GObjectClass *) klass;
|
||||
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
|
||||
|
||||
object_class->finalize = wp_permission_manager_finalize;
|
||||
|
||||
wpobject_class->get_supported_features =
|
||||
wp_permission_manager_get_supported_features;
|
||||
wpobject_class->activate_get_next_step =
|
||||
wp_permission_manager_activate_get_next_step;
|
||||
wpobject_class->activate_execute_step =
|
||||
wp_permission_manager_activate_execute_step;
|
||||
wpobject_class->deactivate = wp_permission_manager_deactivate;
|
||||
}
|
||||
|
||||
void
|
||||
wp_permission_manager_add_client (WpPermissionManager *self, WpClient *client)
|
||||
{
|
||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
||||
|
||||
g_ptr_array_add (self->clients, g_object_ref (client));
|
||||
update_client_permissions (self, client);
|
||||
}
|
||||
|
||||
void
|
||||
wp_permission_manager_remove_client (WpPermissionManager *self,
|
||||
WpClient *client)
|
||||
{
|
||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
||||
|
||||
g_ptr_array_remove_fast (self->clients, client);
|
||||
update_client_permissions (self, client);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Creates a new WpPermissionManager object
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param core the WpCore
|
||||
* \returns (transfer full): a new WpPermissionManager object
|
||||
*/
|
||||
WpPermissionManager *
|
||||
wp_permission_manager_new (WpCore * core)
|
||||
{
|
||||
g_return_val_if_fail (core, NULL);
|
||||
|
||||
return g_object_new (WP_TYPE_PERMISSION_MANAGER, "core", core, NULL);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the default permissions that will be applied to all objects that
|
||||
* don't match any interest
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param self the permission manager
|
||||
* \param permissions the default permissions to apply
|
||||
*/
|
||||
void
|
||||
wp_permission_manager_set_default_permissions (WpPermissionManager *self,
|
||||
guint32 permissions)
|
||||
{
|
||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
||||
|
||||
if (self->default_perms != permissions) {
|
||||
self->default_perms = permissions;
|
||||
update_permissions (self);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the permissions that will be applied to the core object (ID 0).
|
||||
*
|
||||
* The core object is not visible to the permission manager's object manager
|
||||
* because it is implicit in the PipeWire connection and not sent through the
|
||||
* registry. This method allows setting explicit permissions on it, independent
|
||||
* of the default permissions.
|
||||
*
|
||||
* If not set (or set to PW_PERM_INVALID), the core inherits default_permissions.
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param self the permission manager
|
||||
* \param permissions the permissions to apply to the core object
|
||||
*/
|
||||
void
|
||||
wp_permission_manager_set_core_permissions (WpPermissionManager *self,
|
||||
guint32 permissions)
|
||||
{
|
||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
||||
|
||||
if (self->core_perms != permissions) {
|
||||
self->core_perms = permissions;
|
||||
update_permissions (self);
|
||||
}
|
||||
}
|
||||
|
||||
static guint32
|
||||
wp_permission_manager_add_match (WpPermissionManager *self,
|
||||
PermissionMatch *match)
|
||||
{
|
||||
guint id = match->id;
|
||||
g_hash_table_insert (self->matches, GUINT_TO_POINTER (id), match);
|
||||
update_permissions (self);
|
||||
return id;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Adds an interest match to apply permissions with callback in matched
|
||||
* objects.
|
||||
*
|
||||
* Interest consists of a GType that the object must be an ancestor of
|
||||
* (g_type_is_a() must match) and optionally, a set of additional constraints
|
||||
* on certain properties of the object. Refer to WpObjectInterest for more details.
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param self the permission manager
|
||||
* \param callback (scope async): the permissions match callback
|
||||
* \param user_data data to pass to \a callback
|
||||
* \param interest (transfer full): the interest
|
||||
* \returns the added match ID, or SPA_ID_INVALID if error
|
||||
*/
|
||||
guint32
|
||||
wp_permission_manager_add_interest_match (WpPermissionManager *self,
|
||||
WpPermissionMatchCallback callback, gpointer user_data,
|
||||
WpObjectInterest * interest)
|
||||
{
|
||||
GClosure *closure = g_cclosure_new (G_CALLBACK (callback), user_data, NULL);
|
||||
return wp_permission_manager_add_interest_match_closure (self, closure,
|
||||
interest);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Adds an interest match to apply permissions with closure in matched
|
||||
* objects.
|
||||
*
|
||||
* Interest consists of a GType that the object must be an ancestor of
|
||||
* (g_type_is_a() must match) and optionally, a set of additional constraints
|
||||
* on certain properties of the object. Refer to WpObjectInterest for more details.
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param self the permission manager
|
||||
* \param closure (transfer full): the closure to apply permissions
|
||||
* \param interest (transfer full): the interest
|
||||
* \returns the added match ID, or SPA_ID_INVALID if error
|
||||
*/
|
||||
guint32
|
||||
wp_permission_manager_add_interest_match_closure (WpPermissionManager *self,
|
||||
GClosure *closure, WpObjectInterest * interest)
|
||||
{
|
||||
g_autoptr (WpObjectInterest) i = interest;
|
||||
g_autoptr (GClosure) c = closure;
|
||||
PermissionMatch *match;
|
||||
|
||||
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
|
||||
g_return_val_if_fail (closure, SPA_ID_INVALID);
|
||||
g_return_val_if_fail (i, SPA_ID_INVALID);
|
||||
|
||||
if (G_CLOSURE_NEEDS_MARSHAL (closure))
|
||||
g_closure_set_marshal (closure, g_cclosure_marshal_generic);
|
||||
|
||||
match = permission_match_new (PW_PERM_INVALID, c, i, NULL);
|
||||
return wp_permission_manager_add_match (self, match);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Adds an interest match to apply same permissions in matched objects.
|
||||
*
|
||||
* Interest consists of a GType that the object must be an ancestor of
|
||||
* (g_type_is_a() must match) and optionally, a set of additional constraints
|
||||
* on certain properties of the object. Refer to WpObjectInterest for more details.
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param self the permission manager
|
||||
* \param permissions the permissions to apply
|
||||
* \param interest (transfer full): the interest
|
||||
* \returns the added match ID, or SPA_ID_INVALID if error
|
||||
*/
|
||||
guint32
|
||||
wp_permission_manager_add_interest_match_simple (WpPermissionManager *self,
|
||||
guint32 permissions, WpObjectInterest * interest)
|
||||
{
|
||||
g_autoptr (WpObjectInterest) i = interest;
|
||||
PermissionMatch *match;
|
||||
|
||||
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
|
||||
g_return_val_if_fail (i, SPA_ID_INVALID);
|
||||
|
||||
match = permission_match_new (permissions, NULL, i, NULL);
|
||||
return wp_permission_manager_add_match (self, match);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Adds a rules match to apply permissions in matched objects.
|
||||
*
|
||||
* The rules must be defined in a JSON object using the same format as all
|
||||
* the wireplumber/pipewire rules.
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param self the permission manager
|
||||
* \param rules (transfer full): the JSON rules
|
||||
* \returns the added match ID, or SPA_ID_INVALID if error
|
||||
*/
|
||||
guint32
|
||||
wp_permission_manager_add_rules_match (WpPermissionManager *self,
|
||||
WpSpaJson *rules)
|
||||
{
|
||||
g_autoptr (WpSpaJson) r = rules;
|
||||
PermissionMatch *match;
|
||||
|
||||
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
|
||||
g_return_val_if_fail (r, SPA_ID_INVALID);
|
||||
|
||||
match = permission_match_new (PW_PERM_INVALID, NULL, NULL, rules);
|
||||
return wp_permission_manager_add_match (self, match);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Removes the previously added match so that the associated permissions
|
||||
* are not applied anymore.
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param self the permission manager
|
||||
* \param match_id the match ID to remove
|
||||
*/
|
||||
void
|
||||
wp_permission_manager_remove_match (WpPermissionManager *self, guint32 match_id)
|
||||
{
|
||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
||||
g_return_if_fail (match_id != SPA_ID_INVALID);
|
||||
|
||||
g_hash_table_remove (self->matches, GUINT_TO_POINTER (match_id));
|
||||
update_permissions (self);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Updates permissions on all clients the permission manager has.
|
||||
*
|
||||
* The permission manager already updates permissions on all clients
|
||||
* automatically when a new client or object is added, however, this might be
|
||||
* needed if interests with closures or callbacks were added and something
|
||||
* changed externally.
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param self the permission manager
|
||||
*/
|
||||
void
|
||||
wp_permission_manager_update_permissions (WpPermissionManager *self)
|
||||
{
|
||||
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
|
||||
|
||||
update_permissions (self);
|
||||
}
|
||||
88
lib/wp/permission-manager.h
Normal file
88
lib/wp/permission-manager.h
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2026 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@ollabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef __WIREPLUMBER_PERMISSION_MANAGER_H__
|
||||
#define __WIREPLUMBER_PERMISSION_MANAGER_H__
|
||||
|
||||
#include "object-interest.h"
|
||||
#include "global-proxy.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/*!
|
||||
* \brief Flags to be used as WpObjectFeatures for WpPermissionManager.
|
||||
* \ingroup wppermissionmanager
|
||||
*/
|
||||
typedef enum { /*< flags >*/
|
||||
/*! Loads the permission manager */
|
||||
WP_PERMISSION_MANAGER_LOADED = (1 << 0),
|
||||
} WpPermissionManagerFeatures;
|
||||
|
||||
/*!
|
||||
* \brief The WpPermissionManager GType
|
||||
* \ingroup wppermissionmanager
|
||||
*/
|
||||
#define WP_TYPE_PERMISSION_MANAGER (wp_permission_manager_get_type ())
|
||||
|
||||
WP_API
|
||||
G_DECLARE_FINAL_TYPE (WpPermissionManager, wp_permission_manager, WP,
|
||||
PERMISSION_MANAGER, WpObject)
|
||||
|
||||
typedef struct _WpClient WpClient;
|
||||
|
||||
/*!
|
||||
* \brief callback to set permissions on the matched global object
|
||||
*
|
||||
* \ingroup wppermissionmanager
|
||||
* \param self the permission manager
|
||||
* \param client the client that will have its permissions updated
|
||||
* \param object the matched global
|
||||
* \param user_data the passed data
|
||||
*/
|
||||
typedef guint32 (*WpPermissionMatchCallback) (WpPermissionManager *self,
|
||||
WpClient *client, WpGlobalProxy *object, gpointer user_data);
|
||||
|
||||
WP_API
|
||||
WpPermissionManager * wp_permission_manager_new (WpCore * core);
|
||||
|
||||
WP_API
|
||||
void wp_permission_manager_set_default_permissions (
|
||||
WpPermissionManager *self, guint32 permissions);
|
||||
|
||||
WP_API
|
||||
void wp_permission_manager_set_core_permissions (
|
||||
WpPermissionManager *self, guint32 permissions);
|
||||
|
||||
WP_API
|
||||
guint32 wp_permission_manager_add_interest_match (WpPermissionManager *self,
|
||||
WpPermissionMatchCallback callback, gpointer user_data,
|
||||
WpObjectInterest * interest);
|
||||
|
||||
WP_API
|
||||
guint32 wp_permission_manager_add_interest_match_closure (
|
||||
WpPermissionManager *self, GClosure *closure, WpObjectInterest * interest);
|
||||
|
||||
WP_API
|
||||
guint32 wp_permission_manager_add_interest_match_simple (
|
||||
WpPermissionManager *self, guint32 permissions,
|
||||
WpObjectInterest * interest);
|
||||
|
||||
WP_API
|
||||
guint32 wp_permission_manager_add_rules_match (WpPermissionManager *self,
|
||||
WpSpaJson *rules);
|
||||
|
||||
WP_API
|
||||
void wp_permission_manager_remove_match (WpPermissionManager *self,
|
||||
guint32 match_id);
|
||||
|
||||
WP_API
|
||||
void wp_permission_manager_update_permissions (WpPermissionManager *self);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -198,7 +198,7 @@ wp_plugin_find (WpCore * core, const gchar * plugin_name)
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Retreives the name of a plugin.
|
||||
* \brief Retrieves the name of a plugin.
|
||||
*
|
||||
* \ingroup wpplugin
|
||||
* \param self the plugin
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ struct _ComponentData
|
|||
grefcount ref;
|
||||
/* an identifier for this component that is understandable by the end user */
|
||||
gchar *printable_id;
|
||||
/* the provided feature name (points to same storage as the id) or NULL */
|
||||
/* the provided feature name */
|
||||
gchar *provides;
|
||||
/* the original state of the feature (required / optional / disabled) */
|
||||
FeatureState state;
|
||||
|
|
@ -39,6 +39,8 @@ struct _ComponentData
|
|||
WpSpaJson *arguments;
|
||||
GPtrArray *requires; /* value-type: string (owned) */
|
||||
GPtrArray *wants; /* value-type: string (owned) */
|
||||
GPtrArray *before; /* value-type: string (owned) */
|
||||
GPtrArray *after; /* value-type: string (owned) */
|
||||
|
||||
/* TRUE when the component is in the final sorted list */
|
||||
gboolean visited;
|
||||
|
|
@ -174,6 +176,8 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
|
|||
g_ref_count_init (&comp->ref);
|
||||
comp->requires = g_ptr_array_new_with_free_func (g_free);
|
||||
comp->wants = g_ptr_array_new_with_free_func (g_free);
|
||||
comp->before = g_ptr_array_new_with_free_func (g_free);
|
||||
comp->after = g_ptr_array_new_with_free_func (g_free);
|
||||
|
||||
props = wp_properties_new_json (json);
|
||||
if (rules && !wp_json_utils_match_rules (rules, props, component_rule_match_cb,
|
||||
|
|
@ -201,7 +205,7 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
|
|||
comp->printable_id = g_strdup_printf ("%s [%s]", comp->provides, comp->type);
|
||||
}
|
||||
} else {
|
||||
comp->provides = NULL;
|
||||
comp->provides = g_strdup_printf ("__anonymous_%p", comp);
|
||||
comp->state = FEATURE_STATE_REQUIRED;
|
||||
comp->printable_id = g_strdup_printf ("[%s: %s]", comp->type, comp->name);
|
||||
}
|
||||
|
|
@ -228,6 +232,28 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
|
|||
}
|
||||
}
|
||||
|
||||
if ((str = wp_properties_get (props, "before"))) {
|
||||
g_autoptr (WpSpaJson) comp_before = wp_spa_json_new_wrap_string (str);
|
||||
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_before);
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
|
||||
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
||||
WpSpaJson *dep = g_value_get_boxed (&item);
|
||||
g_ptr_array_add (comp->before, wp_spa_json_to_string (dep));
|
||||
}
|
||||
}
|
||||
|
||||
if ((str = wp_properties_get (props, "after"))) {
|
||||
g_autoptr (WpSpaJson) comp_after = wp_spa_json_new_wrap_string (str);
|
||||
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_after);
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
|
||||
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
||||
WpSpaJson *dep = g_value_get_boxed (&item);
|
||||
g_ptr_array_add (comp->after, wp_spa_json_to_string (dep));
|
||||
}
|
||||
}
|
||||
|
||||
return g_steal_pointer (&comp);
|
||||
}
|
||||
|
||||
|
|
@ -241,6 +267,8 @@ component_data_free (ComponentData * self)
|
|||
g_clear_pointer (&self->arguments, wp_spa_json_unref);
|
||||
g_clear_pointer (&self->requires, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->wants, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->before, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->after, g_ptr_array_unref);
|
||||
g_free (self);
|
||||
}
|
||||
|
||||
|
|
@ -297,6 +325,100 @@ wp_component_array_load_task_get_next_step (WpTransition * transition, guint ste
|
|||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
component_equals (const ComponentData * comp, const gchar * provides)
|
||||
{
|
||||
return g_str_equal (provides, comp->provides);
|
||||
}
|
||||
|
||||
static inline gboolean
|
||||
component_exists_in (const gchar *comp_provides, GPtrArray *list)
|
||||
{
|
||||
return g_ptr_array_find_with_equal_func (list, comp_provides,
|
||||
(GEqualFunc) component_equals, NULL);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
sort_components_before_after (WpComponentArrayLoadTask * self, GError ** error)
|
||||
{
|
||||
g_autoptr (GPtrArray) remaining = g_ptr_array_new_with_free_func (
|
||||
(GDestroyNotify) component_data_unref);
|
||||
g_autoptr (GPtrArray) result = g_ptr_array_new_with_free_func (
|
||||
(GDestroyNotify) component_data_unref);
|
||||
|
||||
for (guint i = 0; i < self->components->len; i++) {
|
||||
ComponentData *comp = g_ptr_array_index (self->components, i);
|
||||
|
||||
/* implicitly add all "requires" and "wants" as "after" dependencies */
|
||||
g_ptr_array_extend (comp->after, comp->requires, (GCopyFunc) g_strdup, NULL);
|
||||
g_ptr_array_extend (comp->after, comp->wants, (GCopyFunc) g_strdup, NULL);
|
||||
|
||||
/* convert "before" dependencies into "after" dependencies */
|
||||
for (guint j = 0; j < comp->before->len; j++) {
|
||||
gchar *target_provides = g_ptr_array_index (comp->before, j);
|
||||
for (guint k = 0; k < self->components->len; k++) {
|
||||
ComponentData *target = g_ptr_array_index (self->components, k);
|
||||
if (g_str_equal (target_provides, target->provides)) {
|
||||
g_ptr_array_insert (target->after, -1, g_strdup (comp->provides));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* sort */
|
||||
while (self->components->len > 0) {
|
||||
gboolean made_progress = FALSE;
|
||||
|
||||
/* examine each component to see if its dependencies are satisfied in the
|
||||
result list; if yes, then append it to the result too */
|
||||
while (self->components->len > 0) {
|
||||
ComponentData *comp = g_ptr_array_steal_index (self->components, 0);
|
||||
guint deps_satisfied = 0;
|
||||
|
||||
wp_trace_object (self, "examining: %s", comp->printable_id);
|
||||
|
||||
for (guint i = 0; i < comp->after->len; i++) {
|
||||
const gchar *dep = g_ptr_array_index (comp->after, i);
|
||||
/* if the dependency is already in the sorted result list or if
|
||||
it doesn't exist at all, we consider it satisfied */
|
||||
if (component_exists_in (dep, result) ||
|
||||
!(component_exists_in (dep, self->components) ||
|
||||
component_exists_in (dep, remaining))) {
|
||||
deps_satisfied++;
|
||||
}
|
||||
|
||||
wp_trace_object (self, "depends: %s, satisfied: %u/%u",
|
||||
dep, deps_satisfied, comp->after->len);
|
||||
}
|
||||
|
||||
if (deps_satisfied == comp->after->len) {
|
||||
wp_trace_object (self, "sorted: %s", comp->printable_id);
|
||||
|
||||
g_ptr_array_add (result, comp);
|
||||
made_progress = TRUE;
|
||||
} else {
|
||||
g_ptr_array_add (remaining, comp);
|
||||
}
|
||||
}
|
||||
|
||||
if (made_progress) {
|
||||
/* run again with the remaining components */
|
||||
g_ptr_array_extend_and_steal (self->components, g_ptr_array_ref (remaining));
|
||||
}
|
||||
else if (remaining->len > 0) {
|
||||
/* if we did not make any progress towards growing the result list,
|
||||
it means the dependencies cannot be satisfied because of circles */
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"detected circular before/after dependencies in the components!");
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/* transfer the result array back to self->components */
|
||||
g_ptr_array_extend_and_steal (self->components, g_steal_pointer (&result));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gchar *
|
||||
print_dep_chain (ComponentData *comp)
|
||||
{
|
||||
|
|
@ -409,9 +531,8 @@ parse_components (WpComponentArrayLoadTask * self, GError ** error)
|
|||
if (comp->state == FEATURE_STATE_REQUIRED)
|
||||
g_ptr_array_add (required_components, component_data_ref (comp));
|
||||
|
||||
if (comp->provides)
|
||||
g_hash_table_insert (self->feat_components, comp->provides,
|
||||
component_data_ref (comp));
|
||||
g_hash_table_insert (self->feat_components, comp->provides,
|
||||
component_data_ref (comp));
|
||||
}
|
||||
|
||||
/* topological sorting based on depth-first search */
|
||||
|
|
@ -424,6 +545,10 @@ parse_components (WpComponentArrayLoadTask * self, GError ** error)
|
|||
}
|
||||
}
|
||||
|
||||
/* sort again, taking into account before/after dependencies */
|
||||
if (!sort_components_before_after (self, error))
|
||||
return FALSE;
|
||||
|
||||
/* terminate the array with NULL */
|
||||
g_ptr_array_add (self->components, NULL);
|
||||
|
||||
|
|
@ -598,7 +723,7 @@ ensure_no_media_session_task_idle (GTask * task)
|
|||
}
|
||||
|
||||
static void
|
||||
ensure_no_media_session (GTask * task, WpCore * core)
|
||||
ensure_no_media_session (GTask * task, WpCore * core, WpSpaJson * args)
|
||||
{
|
||||
WpObjectManager *om = wp_object_manager_new ();
|
||||
|
||||
|
|
@ -619,7 +744,7 @@ ensure_no_media_session (GTask * task, WpCore * core)
|
|||
}
|
||||
|
||||
static void
|
||||
load_export_core (GTask * task, WpCore * core)
|
||||
load_export_core (GTask * task, WpCore * core, WpSpaJson * args)
|
||||
{
|
||||
g_autofree gchar *export_core_name = NULL;
|
||||
g_autoptr (WpCore) export_core = NULL;
|
||||
|
|
@ -641,12 +766,27 @@ load_export_core (GTask * task, WpCore * core)
|
|||
g_task_return_pointer (task, g_steal_pointer (&export_core), g_object_unref);
|
||||
}
|
||||
|
||||
static void
|
||||
load_settings_instance (GTask * task, WpCore * core, WpSpaJson * args)
|
||||
{
|
||||
g_autofree gchar *metadata_name = NULL;
|
||||
if (args)
|
||||
wp_spa_json_object_get (args, "metadata.name", "s", &metadata_name, NULL);
|
||||
|
||||
wp_info_object (core, "loading settings instance '%s'...",
|
||||
metadata_name ? metadata_name : "(default: sm-settings)");
|
||||
|
||||
WpSettings *settings = wp_settings_new (core, metadata_name);
|
||||
g_task_return_pointer (task, settings, g_object_unref);
|
||||
}
|
||||
|
||||
static const struct {
|
||||
const gchar * name;
|
||||
void (*load) (GTask *, WpCore *);
|
||||
void (*load) (GTask *, WpCore *, WpSpaJson *);
|
||||
} builtin_components[] = {
|
||||
{ "ensure-no-media-session", ensure_no_media_session },
|
||||
{ "export-core", load_export_core },
|
||||
{ "settings-instance", load_settings_instance },
|
||||
};
|
||||
|
||||
/*** WpInternalCompLoader ***/
|
||||
|
|
@ -683,10 +823,12 @@ load_module (WpCore * core, const gchar * module_name, WpSpaJson * args,
|
|||
GModule *gmodule;
|
||||
gpointer module_init;
|
||||
|
||||
if (!g_file_test (module_name, G_FILE_TEST_EXISTS))
|
||||
module_path = g_module_build_path (wp_get_module_dir (), module_name);
|
||||
else
|
||||
module_path = g_strdup (module_name);
|
||||
module_path = wp_base_dirs_find_file (WP_BASE_DIRS_MODULE, NULL, module_name);
|
||||
if (!module_path) {
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
|
||||
"Failed to locate module %s", module_name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
wp_trace_object (core, "loading %s from %s", module_name, module_path);
|
||||
|
||||
|
|
@ -708,6 +850,65 @@ load_module (WpCore * core, const gchar * module_name, WpSpaJson * args,
|
|||
return ((WpModuleInitFunc) module_init) (core, args, error);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
parse_profile_description (WpProperties * profile, WpSpaJson * all_profiles_j,
|
||||
const gchar * profile_name, GPtrArray * inherited_set, GError ** error)
|
||||
{
|
||||
g_autoptr (WpSpaJson) profile_j = NULL;
|
||||
g_autoptr (WpSpaJson) inherits_j = NULL;
|
||||
|
||||
if (!all_profiles_j) {
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"wireplumber.profiles section does not exist in the configuration");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!wp_spa_json_is_object (all_profiles_j)) {
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"wireplumber.profiles section is not an object");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!wp_spa_json_object_get (all_profiles_j, profile_name, "J", &profile_j, NULL)) {
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"profile '%s' not found in the configuration", profile_name);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!wp_spa_json_is_object (profile_j)) {
|
||||
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"profile description of '%s' is not an object", profile_name);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* mark as inherited */
|
||||
g_ptr_array_add (inherited_set, g_strdup (profile_name));
|
||||
|
||||
if (wp_spa_json_object_get (profile_j, "inherits", "J", &inherits_j, NULL) &&
|
||||
wp_spa_json_is_array (inherits_j)) {
|
||||
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (inherits_j);
|
||||
g_auto (GValue) item = G_VALUE_INIT;
|
||||
|
||||
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
||||
WpSpaJson *inherited_j = g_value_get_boxed (&item);
|
||||
g_autofree gchar *inherited_profile = wp_spa_json_to_string (inherited_j);
|
||||
|
||||
/* skip if already inherited - avoid loops */
|
||||
if (g_ptr_array_find_with_equal_func (inherited_set, inherited_profile,
|
||||
g_str_equal, NULL))
|
||||
continue;
|
||||
|
||||
if (!parse_profile_description (profile, all_profiles_j, inherited_profile,
|
||||
inherited_set, error))
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
wp_properties_update_from_json (profile, profile_j);
|
||||
wp_properties_set (profile, "inherits", NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
wp_internal_comp_loader_supports_type (WpComponentLoader * cl,
|
||||
const gchar * type)
|
||||
|
|
@ -734,25 +935,26 @@ wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core,
|
|||
if (g_str_equal (type, "profile")) {
|
||||
/* component name is the profile name;
|
||||
component list and profile features are loaded from config */
|
||||
g_autoptr (WpConf) conf = wp_conf_get_instance (core);
|
||||
g_autoptr (WpSpaJson) profile_json = NULL;
|
||||
g_autoptr (WpConf) conf = wp_core_get_conf (core);
|
||||
g_autoptr (GPtrArray) inherited_set = g_ptr_array_new_with_free_func (g_free);
|
||||
g_autoptr (WpSpaJson) all_profiles_j = NULL;
|
||||
g_autoptr (GError) error = NULL;
|
||||
const gchar *profile_name = component;
|
||||
|
||||
profile_json =
|
||||
wp_conf_get_value (conf, "wireplumber.profiles", component, NULL);
|
||||
if (!profile_json) {
|
||||
wp_info ("Loading profile '%s'", profile_name);
|
||||
|
||||
all_profiles_j = wp_conf_get_section (conf, "wireplumber.profiles");
|
||||
|
||||
if (!parse_profile_description (profile, all_profiles_j, profile_name,
|
||||
inherited_set, &error)) {
|
||||
g_autoptr (GTask) task = g_task_new (self, cancellable, callback, data);
|
||||
g_task_set_source_tag (task, wp_internal_comp_loader_load);
|
||||
g_task_return_new_error (G_TASK (task), WP_DOMAIN_LIBRARY,
|
||||
WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
||||
"profile '%s' not found in configuration", component);
|
||||
g_task_return_error (G_TASK (task), g_steal_pointer (&error));
|
||||
return;
|
||||
}
|
||||
|
||||
wp_properties_update_from_json (profile, profile_json);
|
||||
|
||||
components = wp_conf_get_section (conf, "wireplumber.components", NULL);
|
||||
|
||||
rules = wp_conf_get_section (conf, "wireplumber.components.rules", NULL);
|
||||
components = wp_conf_get_section (conf, "wireplumber.components");
|
||||
rules = wp_conf_get_section (conf, "wireplumber.components.rules");
|
||||
}
|
||||
else {
|
||||
/* component list is retrieved from args; profile features are empty */
|
||||
|
|
@ -796,7 +998,7 @@ wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core,
|
|||
else if (g_str_equal (type, "built-in")) {
|
||||
for (guint i = 0; i < G_N_ELEMENTS (builtin_components); i++) {
|
||||
if (g_str_equal (component, builtin_components[i].name)) {
|
||||
builtin_components[i].load (task, core);
|
||||
builtin_components[i].load (task, core, args);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
158
lib/wp/private/parse-conf-section.c
Normal file
158
lib/wp/private/parse-conf-section.c
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
/* PipeWire */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
/*
|
||||
This is a partial copy of functions from libpipewire's conf.c that is meant to
|
||||
live here temporarily until pw_context_parse_conf_section() is fixed upstream.
|
||||
See https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/1925
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <spa/utils/result.h>
|
||||
#include <spa/utils/string.h>
|
||||
#include <spa/utils/json.h>
|
||||
#include <spa/utils/cleanup.h>
|
||||
|
||||
#include <pipewire/impl.h>
|
||||
|
||||
struct data {
|
||||
struct pw_context *context;
|
||||
struct pw_properties *props;
|
||||
int count;
|
||||
};
|
||||
|
||||
/* context.spa-libs = {
|
||||
* <factory-name regex> = <library-name>
|
||||
* }
|
||||
*/
|
||||
static int parse_spa_libs(void *user_data, const char *location,
|
||||
const char *section, const char *str, size_t len)
|
||||
{
|
||||
struct data *d = user_data;
|
||||
struct pw_context *context = d->context;
|
||||
struct spa_json it[2];
|
||||
char key[512], value[512];
|
||||
|
||||
spa_json_init(&it[0], str, len);
|
||||
if (spa_json_enter_object(&it[0], &it[1]) < 0) {
|
||||
pw_log_error("config file error: context.spa-libs is not an object");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
|
||||
if (spa_json_get_string(&it[1], value, sizeof(value)) > 0) {
|
||||
pw_context_add_spa_lib(context, key, value);
|
||||
d->count++;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static int load_module(struct pw_context *context, const char *key, const char *args, const char *flags)
|
||||
{
|
||||
if (pw_context_load_module(context, key, args, NULL) == NULL) {
|
||||
if (errno == ENOENT && flags && strstr(flags, "ifexists") != NULL) {
|
||||
pw_log_info("%p: skipping unavailable module %s",
|
||||
context, key);
|
||||
} else if (flags == NULL || strstr(flags, "nofail") == NULL) {
|
||||
pw_log_error("%p: could not load mandatory module \"%s\": %m",
|
||||
context, key);
|
||||
return -errno;
|
||||
} else {
|
||||
pw_log_info("%p: could not load optional module \"%s\": %m",
|
||||
context, key);
|
||||
}
|
||||
} else {
|
||||
pw_log_info("%p: loaded module %s", context, key);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* context.modules = [
|
||||
* { name = <module-name>
|
||||
* ( args = { <key> = <value> ... } )
|
||||
* ( flags = [ ( ifexists ) ( nofail ) ]
|
||||
* ( condition = [ { key = value, .. } .. ] )
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
static int parse_modules(void *user_data, const char *location,
|
||||
const char *section, const char *str, size_t len)
|
||||
{
|
||||
struct data *d = user_data;
|
||||
struct pw_context *context = d->context;
|
||||
struct spa_json it[4];
|
||||
char key[512];
|
||||
int res = 0;
|
||||
|
||||
spa_autofree char *s = strndup(str, len);
|
||||
spa_json_init(&it[0], s, len);
|
||||
if (spa_json_enter_array(&it[0], &it[1]) < 0) {
|
||||
pw_log_error("config file error: context.modules is not an array");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
while (spa_json_enter_object(&it[1], &it[2]) > 0) {
|
||||
char *name = NULL, *args = NULL, *flags = NULL;
|
||||
bool have_match = true;
|
||||
|
||||
while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
|
||||
const char *val;
|
||||
int len;
|
||||
|
||||
if ((len = spa_json_next(&it[2], &val)) <= 0)
|
||||
break;
|
||||
|
||||
if (spa_streq(key, "name")) {
|
||||
name = (char*)val;
|
||||
spa_json_parse_stringn(val, len, name, len+1);
|
||||
} else if (spa_streq(key, "args")) {
|
||||
if (spa_json_is_container(val, len))
|
||||
len = spa_json_container_len(&it[2], val, len);
|
||||
|
||||
args = (char*)val;
|
||||
spa_json_parse_stringn(val, len, args, len+1);
|
||||
} else if (spa_streq(key, "flags")) {
|
||||
if (spa_json_is_container(val, len))
|
||||
len = spa_json_container_len(&it[2], val, len);
|
||||
flags = (char*)val;
|
||||
spa_json_parse_stringn(val, len, flags, len+1);
|
||||
}
|
||||
}
|
||||
if (!have_match)
|
||||
continue;
|
||||
|
||||
if (name != NULL)
|
||||
res = load_module(context, name, args, flags);
|
||||
|
||||
if (res < 0)
|
||||
break;
|
||||
|
||||
d->count++;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static int _pw_context_parse_conf_section(struct pw_context *context,
|
||||
struct pw_properties *conf, const char *section)
|
||||
{
|
||||
struct data data = { .context = context };
|
||||
int res;
|
||||
|
||||
if (spa_streq(section, "context.spa-libs"))
|
||||
res = pw_conf_section_for_each(&conf->dict, section,
|
||||
parse_spa_libs, &data);
|
||||
else if (spa_streq(section, "context.modules"))
|
||||
res = pw_conf_section_for_each(&conf->dict, section,
|
||||
parse_modules, &data);
|
||||
else
|
||||
res = -EINVAL;
|
||||
|
||||
return res == 0 ? data.count : res;
|
||||
}
|
||||
26
lib/wp/private/permission-manager.h
Normal file
26
lib/wp/private/permission-manager.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2026 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
|
||||
#define __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
|
||||
|
||||
#include "client.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct _WpPermissionManager WpPermissionManager;
|
||||
|
||||
void wp_permission_manager_add_client (WpPermissionManager *self,
|
||||
WpClient *client);
|
||||
|
||||
void wp_permission_manager_remove_client (WpPermissionManager *self,
|
||||
WpClient *client);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -783,7 +783,7 @@ wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update)
|
|||
G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);
|
||||
|
||||
g_clear_pointer (&d->properties, wp_properties_unref);
|
||||
d->properties = wp_properties_new_wrap_dict (props);
|
||||
d->properties = wp_properties_new_copy_dict (props);
|
||||
|
||||
g_object_notify (G_OBJECT (instance), "properties");
|
||||
}
|
||||
|
|
|
|||
515
lib/wp/private/registry.c
Normal file
515
lib/wp/private/registry.c
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2019-2024 Collabora Ltd.
|
||||
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "registry.h"
|
||||
#include "object-manager.h"
|
||||
#include "log.h"
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-registry")
|
||||
|
||||
/*
|
||||
* WpRegistry:
|
||||
*
|
||||
* The registry keeps track of registered objects on the wireplumber core.
|
||||
* There are 3 kinds of registered objects:
|
||||
*
|
||||
* 1) PipeWire global objects, which live in another process.
|
||||
*
|
||||
* These objects are represented by a WpGlobal with the
|
||||
* WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag set. They appear when
|
||||
* the registry_global() event is fired and are removed by
|
||||
* registry_global_remove(). These objects do not have an associated
|
||||
* WpProxy, unless there is at least one WpObjectManager that is interested
|
||||
* in them. In this case, a WpProxy is constructed and it is owned by the
|
||||
* WpGlobal until the global is removed by the registry_global_remove() event.
|
||||
*
|
||||
* 2) PipeWire global objects, which were constructed by this process, either
|
||||
* by calling into a remove factory (see wp_node_new_from_factory()) or
|
||||
* by exporting a local object (WpImplSession etc...).
|
||||
*
|
||||
* These objects are also represented by a WpGlobal, which may however be
|
||||
* constructed before they appear on the registry. The associated WpProxy
|
||||
* calls into wp_registry_prepare_new_global() at the time it receives
|
||||
* the 'bound' event and creates a global that has the
|
||||
* WP_GLOBAL_FLAG_OWNED_BY_PROXY flag enabled. As the flag name suggests,
|
||||
* these globals are "owned" by the WpProxy and the WpGlobal has no ref
|
||||
* on the WpProxy itself. This allows destroying the proxy in client code
|
||||
* by dropping its last reference.
|
||||
*
|
||||
* Normally, these global objects also appear on the pipewire registry. When
|
||||
* this happens, the WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag is also added
|
||||
* and that keeps an additional reference on the global (both flags must
|
||||
* be dropped before the WpGlobal is destroyed).
|
||||
*
|
||||
* In some cases, such an object might appear first on the registry and
|
||||
* then receive the 'bound' event. In order to handle this situation, globals
|
||||
* are not advertised immediately when they appear on the registry, but
|
||||
* they are added on a tmp_globals list instead, which is emptied on the
|
||||
* next core sync. In all cases, the proxy 'bound' and the registry 'global'
|
||||
* events will be fired in the same sync cycle, so we can catch a late
|
||||
* 'bound' event and still associate the proxy with the WpGlobal before
|
||||
* object managers are notified about the existence of this global.
|
||||
*
|
||||
* 3) WirePlumber global objects (WpModule, WpPlugin, WpSiFactory).
|
||||
*
|
||||
* These are local objects that have nothing to do with PipeWire. They do not
|
||||
* have a global id and they are also not subclasses of WpProxy. The registry
|
||||
* always owns a reference on them, so that they are kept alive for as long
|
||||
* as the WpCore is alive.
|
||||
*/
|
||||
|
||||
static void
|
||||
object_manager_destroyed (gpointer data, GObject * om)
|
||||
{
|
||||
WpRegistry *self = data;
|
||||
g_ptr_array_remove_fast (self->object_managers, om);
|
||||
}
|
||||
|
||||
/* find the subclass of WpPipewireGloabl that can handle
|
||||
the given pipewire interface type of the given version */
|
||||
static inline GType
|
||||
find_proxy_instance_type (const char * type, guint32 version)
|
||||
{
|
||||
g_autofree GType *children;
|
||||
guint n_children;
|
||||
|
||||
children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children);
|
||||
|
||||
for (guint i = 0; i < n_children; i++) {
|
||||
WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]);
|
||||
if (g_strcmp0 (klass->pw_iface_type, type) == 0 &&
|
||||
klass->pw_iface_version == version) {
|
||||
g_type_class_unref (klass);
|
||||
return children[i];
|
||||
}
|
||||
|
||||
g_type_class_unref (klass);
|
||||
}
|
||||
|
||||
return WP_TYPE_GLOBAL_PROXY;
|
||||
}
|
||||
|
||||
/* called by the registry when a global appears */
|
||||
static void
|
||||
registry_global (void *data, uint32_t id, uint32_t permissions,
|
||||
const char *type, uint32_t version, const struct spa_dict *props)
|
||||
{
|
||||
WpRegistry *self = data;
|
||||
GType gtype = find_proxy_instance_type (type, version);
|
||||
|
||||
wp_debug_object (wp_registry_get_core (self),
|
||||
"global:%u perm:0x%x type:%s/%u -> %s",
|
||||
id, permissions, type, version, g_type_name (gtype));
|
||||
|
||||
wp_registry_prepare_new_global (self, id, permissions,
|
||||
WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY, gtype, NULL, props, NULL);
|
||||
}
|
||||
|
||||
/* called by the registry when a global is removed */
|
||||
static void
|
||||
registry_global_remove (void *data, uint32_t id)
|
||||
{
|
||||
WpRegistry *self = data;
|
||||
WpGlobal *global = NULL;
|
||||
|
||||
if (id < self->globals->len)
|
||||
global = g_ptr_array_index (self->globals, id);
|
||||
|
||||
/* if not found, look in the tmp_globals, as it may still not be exposed */
|
||||
if (!global) {
|
||||
for (guint i = 0; i < self->tmp_globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
|
||||
if (g->id == id) {
|
||||
global = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_return_if_fail (global &&
|
||||
global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||
|
||||
wp_debug_object (wp_registry_get_core (self),
|
||||
"global removed:%u type:%s", id, g_type_name (global->type));
|
||||
|
||||
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||
}
|
||||
|
||||
static const struct pw_registry_events registry_events = {
|
||||
PW_VERSION_REGISTRY_EVENTS,
|
||||
.global = registry_global,
|
||||
.global_remove = registry_global_remove,
|
||||
};
|
||||
|
||||
void
|
||||
wp_registry_init (WpRegistry *self)
|
||||
{
|
||||
self->globals =
|
||||
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
||||
self->tmp_globals =
|
||||
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
||||
self->objects = g_ptr_array_new_with_free_func (g_object_unref);
|
||||
self->object_managers = g_ptr_array_new ();
|
||||
self->features = g_ptr_array_new_with_free_func (g_free);
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_clear (WpRegistry *self)
|
||||
{
|
||||
wp_registry_detach (self);
|
||||
g_clear_pointer (&self->globals, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->tmp_globals, g_ptr_array_unref);
|
||||
g_clear_pointer (&self->features, g_ptr_array_unref);
|
||||
|
||||
/* remove all the registered objects
|
||||
this will normally also destroy the object managers, eventually, since
|
||||
they are normally ref'ed by modules, which are registered objects */
|
||||
{
|
||||
g_autoptr (GPtrArray) objlist = g_steal_pointer (&self->objects);
|
||||
|
||||
while (objlist->len > 0) {
|
||||
g_autoptr (GObject) object = g_ptr_array_steal_index_fast (objlist,
|
||||
objlist->len - 1);
|
||||
wp_registry_notify_rm_object (self, object);
|
||||
}
|
||||
}
|
||||
|
||||
/* in case there are any object managers left,
|
||||
remove the weak ref on them and let them be... */
|
||||
{
|
||||
g_autoptr (GPtrArray) object_mgrs;
|
||||
GObject *om;
|
||||
|
||||
object_mgrs = g_steal_pointer (&self->object_managers);
|
||||
|
||||
while (object_mgrs->len > 0) {
|
||||
om = g_ptr_array_steal_index_fast (object_mgrs, object_mgrs->len - 1);
|
||||
g_object_weak_unref (om, object_manager_destroyed, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_attach (WpRegistry *self, struct pw_core *pw_core)
|
||||
{
|
||||
self->pw_registry = pw_core_get_registry (pw_core,
|
||||
PW_VERSION_REGISTRY, 0);
|
||||
pw_registry_add_listener (self->pw_registry, &self->listener,
|
||||
®istry_events, self);
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_detach (WpRegistry *self)
|
||||
{
|
||||
if (self->pw_registry) {
|
||||
spa_hook_remove (&self->listener);
|
||||
pw_proxy_destroy ((struct pw_proxy *) self->pw_registry);
|
||||
self->pw_registry = NULL;
|
||||
}
|
||||
|
||||
/* remove pipewire globals */
|
||||
GPtrArray *objlist = self->globals;
|
||||
while (objlist && objlist->len > 0) {
|
||||
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
|
||||
objlist->len - 1);
|
||||
|
||||
if (!global)
|
||||
continue;
|
||||
|
||||
if (global->proxy)
|
||||
wp_registry_notify_rm_object (self, global->proxy);
|
||||
|
||||
/* remove the APPEARS_ON_REGISTRY flag to unref the proxy if it is owned
|
||||
by the registry; set registry to NULL to avoid further interference */
|
||||
global->registry = NULL;
|
||||
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||
|
||||
/* the registry's ref on global is dropped here; it may still live if
|
||||
there is a proxy that owns a ref on it, but global->registry is set
|
||||
to NULL, so there is no further interference */
|
||||
}
|
||||
|
||||
/* drop tmp globals as well */
|
||||
objlist = self->tmp_globals;
|
||||
while (objlist && objlist->len > 0) {
|
||||
g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
|
||||
objlist->len - 1);
|
||||
wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
expose_tmp_globals (WpCore *core)
|
||||
{
|
||||
WpRegistry *self = wp_core_get_registry (core);
|
||||
g_autoptr (GPtrArray) tmp_globals = NULL;
|
||||
g_autoptr (GPtrArray) object_managers = NULL;
|
||||
|
||||
/* in case the registry was cleared in the meantime... */
|
||||
if (G_UNLIKELY (!self->tmp_globals))
|
||||
return G_SOURCE_REMOVE;
|
||||
|
||||
/* steal the tmp_globals list and replace it with an empty one */
|
||||
tmp_globals = self->tmp_globals;
|
||||
self->tmp_globals =
|
||||
g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
|
||||
|
||||
wp_debug_object (core, "exposing %u new globals", tmp_globals->len);
|
||||
|
||||
/* traverse in the order that the globals appeared on the registry */
|
||||
for (guint i = 0; i < tmp_globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
|
||||
|
||||
/* if global was already removed, drop it */
|
||||
if (g->flags == 0 || g->id == SPA_ID_INVALID)
|
||||
continue;
|
||||
|
||||
/* if old global is owned by proxy, remove it */
|
||||
if (self->globals->len > g->id) {
|
||||
WpGlobal *old_g = g_ptr_array_index (self->globals, g->id);
|
||||
if (old_g && (old_g->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY))
|
||||
wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
|
||||
}
|
||||
|
||||
g_return_val_if_fail (self->globals->len <= g->id ||
|
||||
g_ptr_array_index (self->globals, g->id) == NULL, G_SOURCE_REMOVE);
|
||||
|
||||
/* set the registry, so that wp_global_rm_flag() can work full-scale */
|
||||
g->registry = self;
|
||||
|
||||
/* store it in the globals list */
|
||||
if (self->globals->len <= g->id)
|
||||
g_ptr_array_set_size (self->globals, g->id + 1);
|
||||
g_ptr_array_index (self->globals, g->id) = wp_global_ref (g);
|
||||
}
|
||||
|
||||
object_managers = g_ptr_array_copy (self->object_managers,
|
||||
(GCopyFunc) g_object_ref, NULL);
|
||||
g_ptr_array_set_free_func (object_managers, g_object_unref);
|
||||
|
||||
/* notify object managers */
|
||||
for (guint i = 0; i < object_managers->len; i++) {
|
||||
WpObjectManager *om = g_ptr_array_index (object_managers, i);
|
||||
|
||||
for (guint i = 0; i < tmp_globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (tmp_globals, i);
|
||||
|
||||
/* if global was already removed, drop it */
|
||||
if (g->flags == 0 || g->id == SPA_ID_INVALID)
|
||||
continue;
|
||||
|
||||
wp_object_manager_add_global (om, g);
|
||||
}
|
||||
wp_object_manager_maybe_objects_changed (om);
|
||||
}
|
||||
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/*
|
||||
* \param new_global (out) (transfer full) (optional): the new global
|
||||
*
|
||||
* This is normally called up to 2 times in the same sync cycle:
|
||||
* one from registry_global(), another from the proxy bound event
|
||||
* Unfortunately the order in which those 2 events happen is specific
|
||||
* to the implementation of the object, which is why this is implemented
|
||||
* with a temporary globals list that get exposed later to the object managers
|
||||
*/
|
||||
void
|
||||
wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
|
||||
guint32 permissions, guint32 flag, GType type,
|
||||
WpGlobalProxy *proxy, const struct spa_dict *props,
|
||||
WpGlobal ** new_global)
|
||||
{
|
||||
g_autoptr (WpGlobal) global = NULL;
|
||||
WpCore *core = wp_registry_get_core (self);
|
||||
|
||||
g_return_if_fail (flag != 0);
|
||||
|
||||
for (guint i = 0; i < self->tmp_globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
|
||||
if (g->id == id) {
|
||||
global = wp_global_ref (g);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wp_debug_object (core, "%s WpGlobal:%u type:%s proxy:%p",
|
||||
global ? "reuse" : "new", id, g_type_name (type),
|
||||
(global && global->proxy) ? global->proxy : proxy);
|
||||
|
||||
if (!global) {
|
||||
global = g_rc_box_new0 (WpGlobal);
|
||||
global->flags = flag;
|
||||
global->id = id;
|
||||
global->type = type;
|
||||
global->permissions = permissions;
|
||||
global->properties = props ?
|
||||
wp_properties_new_copy_dict (props) : wp_properties_new_empty ();
|
||||
global->proxy = proxy;
|
||||
g_ptr_array_add (self->tmp_globals, wp_global_ref (global));
|
||||
|
||||
/* ensure we have 'object.id' so that we can filter by id on object managers */
|
||||
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, "%u", global->id);
|
||||
|
||||
/* schedule exposing when adding the first global */
|
||||
if (self->tmp_globals->len == 1) {
|
||||
wp_core_idle_add_closure (core, NULL,
|
||||
g_cclosure_new_object (G_CALLBACK (expose_tmp_globals), G_OBJECT (core)));
|
||||
}
|
||||
} else {
|
||||
/* store the most permissive permissions */
|
||||
if (permissions > global->permissions)
|
||||
global->permissions = permissions;
|
||||
|
||||
global->flags |= flag;
|
||||
|
||||
/* store the most deep type (i.e. WpImplNode instead of WpNode),
|
||||
so that object-manager interests can work more accurately
|
||||
if the interest is on a specific subclass */
|
||||
if (g_type_depth (type) > g_type_depth (global->type))
|
||||
global->type = type;
|
||||
|
||||
if (proxy) {
|
||||
g_return_if_fail (global->proxy == NULL);
|
||||
global->proxy = proxy;
|
||||
}
|
||||
|
||||
if (props)
|
||||
wp_properties_update_from_dict (global->properties, props);
|
||||
}
|
||||
|
||||
if (new_global)
|
||||
*new_global = g_steal_pointer (&global);
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_notify_add_object (WpRegistry *self, gpointer object)
|
||||
{
|
||||
for (guint i = 0; i < self->object_managers->len; i++) {
|
||||
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
|
||||
wp_object_manager_add_object (om, object);
|
||||
wp_object_manager_maybe_objects_changed (om);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_notify_rm_object (WpRegistry *self, gpointer object)
|
||||
{
|
||||
for (guint i = 0; i < self->object_managers->len; i++) {
|
||||
WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
|
||||
wp_object_manager_rm_object (om, object);
|
||||
wp_object_manager_maybe_objects_changed (om);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
wp_registry_install_object_manager (WpRegistry * self, WpObjectManager * om)
|
||||
{
|
||||
guint i;
|
||||
|
||||
g_object_weak_ref (G_OBJECT (om), object_manager_destroyed, self);
|
||||
g_ptr_array_add (self->object_managers, om);
|
||||
|
||||
/* add pre-existing objects to the object manager,
|
||||
in case it's interested in them */
|
||||
for (i = 0; i < self->globals->len; i++) {
|
||||
WpGlobal *g = g_ptr_array_index (self->globals, i);
|
||||
/* check if null because the globals array can have gaps */
|
||||
if (g)
|
||||
wp_object_manager_add_global (om, g);
|
||||
}
|
||||
for (i = 0; i < self->objects->len; i++) {
|
||||
GObject *o = g_ptr_array_index (self->objects, i);
|
||||
wp_object_manager_add_object (om, o);
|
||||
}
|
||||
|
||||
wp_object_manager_maybe_objects_changed (om);
|
||||
}
|
||||
|
||||
/* WpGlobal */
|
||||
|
||||
G_DEFINE_BOXED_TYPE (WpGlobal, wp_global, wp_global_ref, wp_global_unref)
|
||||
|
||||
void
|
||||
wp_global_rm_flag (WpGlobal *global, guint rm_flag)
|
||||
{
|
||||
WpRegistry *reg = global->registry;
|
||||
guint32 id = global->id;
|
||||
|
||||
/* no flag to remove */
|
||||
if (!(global->flags & rm_flag))
|
||||
return;
|
||||
|
||||
wp_trace_boxed (WP_TYPE_GLOBAL, global,
|
||||
"remove global %u flag 0x%x [flags:0x%x, reg:%p]",
|
||||
id, rm_flag, global->flags, reg);
|
||||
|
||||
/* global was owned by the proxy; by removing the flag, we clear out
|
||||
also the proxy pointer, which is presumably no longer valid and we
|
||||
notify all listeners that the proxy is gone */
|
||||
if (rm_flag == WP_GLOBAL_FLAG_OWNED_BY_PROXY) {
|
||||
global->flags &= ~WP_GLOBAL_FLAG_OWNED_BY_PROXY;
|
||||
if (reg && global->proxy) {
|
||||
wp_registry_notify_rm_object (reg, global->proxy);
|
||||
}
|
||||
global->proxy = NULL;
|
||||
}
|
||||
/* registry removed the global */
|
||||
else if (rm_flag == WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) {
|
||||
global->flags &= ~WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY;
|
||||
|
||||
/* destroy the proxy if it exists */
|
||||
if (global->proxy) {
|
||||
/* steal the proxy to avoid calling wp_registry_notify_rm_object()
|
||||
again while removing OWNED_BY_PROXY;
|
||||
keep a temporary ref so that _deactivate() doesn't crash in case the
|
||||
pw-proxy-destroyed signal causes external references to be dropped */
|
||||
g_autoptr (WpGlobalProxy) proxy =
|
||||
g_object_ref (g_steal_pointer (&global->proxy));
|
||||
|
||||
/* notify all listeners that the proxy is gone */
|
||||
if (reg)
|
||||
wp_registry_notify_rm_object (reg, proxy);
|
||||
|
||||
/* remove FEATURE_BOUND to destroy the underlying pw_proxy */
|
||||
wp_object_deactivate (WP_OBJECT (proxy), WP_PROXY_FEATURE_BOUND);
|
||||
|
||||
/* stop all in-progress activations */
|
||||
wp_object_abort_activation (WP_OBJECT (proxy), "PipeWire proxy removed");
|
||||
|
||||
/* if the proxy is not owning the global, unref it */
|
||||
if (global->flags == 0)
|
||||
g_object_unref (proxy);
|
||||
}
|
||||
|
||||
/* It's possible to receive consecutive {add, remove, add} events for the
|
||||
* same id. Since the WpGlobal might not be destroyed immediately below,
|
||||
* (e.g. it's in tmp_globals list), we must invalidate the id now, so that
|
||||
* this WpGlobal is not used in reference to objects added later.
|
||||
*/
|
||||
global->id = SPA_ID_INVALID;
|
||||
wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, NULL);
|
||||
}
|
||||
|
||||
/* drop the registry's ref on global when it does not appear on the registry anymore */
|
||||
if (!(global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) && reg) {
|
||||
g_clear_pointer (&g_ptr_array_index (reg->globals, id), wp_global_unref);
|
||||
}
|
||||
}
|
||||
|
||||
struct pw_proxy *
|
||||
wp_global_bind (WpGlobal * global)
|
||||
{
|
||||
g_return_val_if_fail (global->proxy, NULL);
|
||||
g_return_val_if_fail (global->registry, NULL);
|
||||
|
||||
WpProxyClass *klass = WP_PROXY_GET_CLASS (global->proxy);
|
||||
return pw_registry_bind (global->registry->pw_registry, global->id,
|
||||
klass->pw_iface_type, klass->pw_iface_version, 0);
|
||||
}
|
||||
|
|
@ -46,6 +46,9 @@ void wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
|
|||
void wp_registry_notify_add_object (WpRegistry * self, gpointer object);
|
||||
void wp_registry_notify_rm_object (WpRegistry * self, gpointer object);
|
||||
|
||||
void wp_registry_install_object_manager (WpRegistry * self,
|
||||
WpObjectManager * om);
|
||||
|
||||
static inline void
|
||||
wp_registry_mark_feature_provided (WpRegistry * reg, const gchar * feature)
|
||||
{
|
||||
|
|
|
|||
218
lib/wp/proc-utils.c
Normal file
218
lib/wp/proc-utils.c
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2024 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <spa/utils/cleanup.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "proc-utils.h"
|
||||
|
||||
#define MAX_ARGS 1024
|
||||
|
||||
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-proc-utils")
|
||||
|
||||
/*! \defgroup wpprocutils Process Utilities */
|
||||
|
||||
/*!
|
||||
* \struct WpProcInfo
|
||||
*
|
||||
* WpProcInfo holds information of a process.
|
||||
*/
|
||||
struct _WpProcInfo {
|
||||
grefcount ref;
|
||||
pid_t pid;
|
||||
pid_t parent;
|
||||
gchar *cgroup;
|
||||
gchar *args[MAX_ARGS];
|
||||
guint n_args;
|
||||
};
|
||||
|
||||
G_DEFINE_BOXED_TYPE (WpProcInfo, wp_proc_info, wp_proc_info_ref,
|
||||
wp_proc_info_unref)
|
||||
|
||||
/*!
|
||||
* \brief Increases the reference count of a process information object
|
||||
* \ingroup wpprocutils
|
||||
* \param self a process information object
|
||||
* \returns (transfer full): \a self with an additional reference count on it
|
||||
*/
|
||||
WpProcInfo *
|
||||
wp_proc_info_ref (WpProcInfo * self)
|
||||
{
|
||||
g_ref_count_inc (&self->ref);
|
||||
return self;
|
||||
}
|
||||
|
||||
static void
|
||||
wp_proc_info_free (WpProcInfo * self)
|
||||
{
|
||||
g_clear_pointer (&self->cgroup, g_free);
|
||||
for (guint i = 0; i < MAX_ARGS; i++)
|
||||
g_clear_pointer (&self->args[i], free);
|
||||
g_slice_free (WpProcInfo, self);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Decreases the reference count on \a self and frees it when the ref
|
||||
* count reaches zero.
|
||||
* \ingroup wpprocutils
|
||||
* \param self (transfer full): a process information object
|
||||
*/
|
||||
void
|
||||
wp_proc_info_unref (WpProcInfo * self)
|
||||
{
|
||||
if (g_ref_count_dec (&self->ref))
|
||||
wp_proc_info_free (self);
|
||||
}
|
||||
|
||||
static WpProcInfo *
|
||||
wp_proc_info_new (pid_t pid)
|
||||
{
|
||||
WpProcInfo *self = g_slice_new0 (WpProcInfo);
|
||||
g_ref_count_init (&self->ref);
|
||||
self->pid = pid;
|
||||
self->parent = 0;
|
||||
self->cgroup = NULL;
|
||||
for (guint i = 0; i < MAX_ARGS; i++)
|
||||
self->args[i] = NULL;
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the PID of a process information object
|
||||
* \ingroup wpprocutils
|
||||
* \param self the process information object
|
||||
* \returns the PID of the process information object
|
||||
*/
|
||||
pid_t
|
||||
wp_proc_info_get_pid (WpProcInfo * self)
|
||||
{
|
||||
return self->pid;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the parent PID of a process information object
|
||||
* \ingroup wpprocutils
|
||||
* \param self the process information object
|
||||
* \returns the parent PID of the process information object
|
||||
*/
|
||||
pid_t
|
||||
wp_proc_info_get_parent_pid (WpProcInfo * self)
|
||||
{
|
||||
return self->parent;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the number of args of a process information object
|
||||
* \ingroup wpprocutils
|
||||
* \param self the process information object
|
||||
* \returns the number of args of the process information object
|
||||
*/
|
||||
guint
|
||||
wp_proc_info_get_n_args (WpProcInfo * self)
|
||||
{
|
||||
return self->n_args;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the indexed arg of a process information object
|
||||
* \ingroup wpprocutils
|
||||
* \param self the process information object
|
||||
* \param index the index of the arg
|
||||
* \returns the indexed arg of the process information object
|
||||
*/
|
||||
const gchar *
|
||||
wp_proc_info_get_arg (WpProcInfo * self, guint index)
|
||||
{
|
||||
if (index >= self->n_args)
|
||||
return NULL;
|
||||
return self->args[index];
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Gets the systemd cgroup of a process information object
|
||||
* \ingroup wpprocutils
|
||||
* \param self the process information object
|
||||
* \returns the systemd cgroup of the process information object
|
||||
*/
|
||||
const gchar *
|
||||
wp_proc_info_get_cgroup (WpProcInfo * self)
|
||||
{
|
||||
return self->cgroup;
|
||||
}
|
||||
|
||||
static FILE *
|
||||
fdopenat (int dirfd, const char *path, int flags, const char *mode, mode_t perm)
|
||||
{
|
||||
int fd = openat (dirfd, path, flags, perm);
|
||||
if (fd >= 0) {
|
||||
FILE *f = fdopen (fd, mode);
|
||||
if (f)
|
||||
return f;
|
||||
close (fd);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
* \brief Gets the process information of a given PID
|
||||
* \ingroup wpprocutils
|
||||
* \param pid the PID to get the process information from
|
||||
* \returns: (transfer full): the process information of the given PID
|
||||
*/
|
||||
WpProcInfo *
|
||||
wp_proc_utils_get_proc_info (pid_t pid)
|
||||
{
|
||||
WpProcInfo *ret = wp_proc_info_new (pid);
|
||||
char path [64];
|
||||
spa_autoclose int base_fd = -1;
|
||||
FILE *file;
|
||||
g_autofree gchar *line = NULL;
|
||||
size_t size = 0;
|
||||
|
||||
snprintf (path, sizeof(path), "/proc/%d", pid);
|
||||
base_fd = open (path,
|
||||
O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY, 0);
|
||||
if (base_fd < 0) {
|
||||
wp_info ("Could not open process info directory %s, skipping", path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Get parent PID */
|
||||
file = fdopenat (base_fd, "status",
|
||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
|
||||
if (file) {
|
||||
while (getline (&line, &size, file) > 1)
|
||||
if (sscanf (line, "PPid:%d\n", &ret->parent) == 1)
|
||||
break;
|
||||
fclose (file);
|
||||
}
|
||||
|
||||
/* Get cgroup */
|
||||
file = fdopenat (base_fd, "cgroup",
|
||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
|
||||
if (file) {
|
||||
if (getline (&line, &size, file) > 1)
|
||||
ret->cgroup = g_strstrip (g_strdup (line));
|
||||
fclose (file);
|
||||
}
|
||||
|
||||
/* Get args */
|
||||
file = fdopenat (base_fd, "cmdline",
|
||||
O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY, "r", 0);
|
||||
if (file) {
|
||||
while (getdelim (&line, &size, 0, file) > 1 && ret->n_args < MAX_ARGS)
|
||||
ret->args[ret->n_args++] = g_strdup (line);
|
||||
fclose (file);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
54
lib/wp/proc-utils.h
Normal file
54
lib/wp/proc-utils.h
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/* WirePlumber
|
||||
*
|
||||
* Copyright © 2024 Collabora Ltd.
|
||||
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef __WIREPLUMBER_PROC_UTILS_H__
|
||||
#define __WIREPLUMBER_PROC_UTILS_H__
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/*!
|
||||
* \brief The WpProcInfo GType
|
||||
* \ingroup wpprocutils
|
||||
*/
|
||||
#define WP_TYPE_PROC_INFO (wp_proc_info_get_type ())
|
||||
WP_API
|
||||
GType wp_proc_info_get_type (void);
|
||||
|
||||
typedef struct _WpProcInfo WpProcInfo;
|
||||
|
||||
WP_API
|
||||
WpProcInfo *wp_proc_info_ref (WpProcInfo * self);
|
||||
|
||||
WP_API
|
||||
void wp_proc_info_unref (WpProcInfo * self);
|
||||
|
||||
WP_API
|
||||
pid_t wp_proc_info_get_pid (WpProcInfo * self);
|
||||
|
||||
WP_API
|
||||
pid_t wp_proc_info_get_parent_pid (WpProcInfo * self);
|
||||
|
||||
WP_API
|
||||
guint wp_proc_info_get_n_args (WpProcInfo * self);
|
||||
|
||||
WP_API
|
||||
const gchar *wp_proc_info_get_arg (WpProcInfo * self, guint index);
|
||||
|
||||
WP_API
|
||||
const gchar *wp_proc_info_get_cgroup (WpProcInfo * self);
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpProcInfo, wp_proc_info_unref)
|
||||
|
||||
WP_API
|
||||
WpProcInfo *wp_proc_utils_get_proc_info (pid_t pid);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -203,7 +203,7 @@ wp_properties_new_wrap (const struct pw_properties * props)
|
|||
* allowing reading & writing properties on that \a props structure through
|
||||
* the WpProperties API.
|
||||
*
|
||||
* In constrast with wp_properties_new_wrap(), this function assumes ownership
|
||||
* In contrast with wp_properties_new_wrap(), this function assumes ownership
|
||||
* of the \a props structure, so it will try to free \a props when it is destroyed.
|
||||
*
|
||||
* \ingroup wpproperties
|
||||
|
|
@ -1036,7 +1036,7 @@ wp_properties_unref_and_take_pw_properties (WpProperties * self)
|
|||
* \ingroup wpproperties
|
||||
* \param self a properties object
|
||||
* \param other a set of properties to match
|
||||
* \returns TRUE if all matches were successfull, FALSE if at least one
|
||||
* \returns TRUE if all matches were successful, FALSE if at least one
|
||||
* property value did not match
|
||||
*/
|
||||
gboolean
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ wp_pipewire_object_default_init (WpPipewireObjectInterface * iface)
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Retrieves the native infor structure of this object
|
||||
* \brief Retrieves the native info structure of this object
|
||||
* (pw_node_info, pw_port_info, etc...)
|
||||
*
|
||||
* \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
|
||||
|
|
@ -213,8 +213,8 @@ wp_pipewire_object_get_param_info (WpPipewireObject * self)
|
|||
* \param id (nullable): the parameter id to enumerate or NULL for all parameters
|
||||
* \param filter (nullable): a param filter or NULL
|
||||
* \param cancellable (nullable): a cancellable for the async operation
|
||||
* \param callback (scope async): a callback to call with the result
|
||||
* \param user_data (closure): data to pass to \a callback
|
||||
* \param callback (scope async)(closure user_data): a callback to call with the result
|
||||
* \param user_data data to pass to \a callback
|
||||
*/
|
||||
void
|
||||
wp_pipewire_object_enum_params (WpPipewireObject * self, const gchar * id,
|
||||
|
|
|
|||
|
|
@ -31,30 +31,28 @@ typedef enum { /*< flags >*/
|
|||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG = (1 << 8),
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE = (1 << 9),
|
||||
|
||||
/*!
|
||||
* The minimal feature set for proxies implementing WpPipewireObject.
|
||||
* This is a subset of \em WP_PIPEWIRE_OBJECT_FEATURES_ALL
|
||||
*/
|
||||
WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL =
|
||||
(WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO),
|
||||
|
||||
/*!
|
||||
* The complete common feature set for proxies implementing
|
||||
* WpPipewireObject. This is a subset of \em WP_OBJECT_FEATURES_ALL
|
||||
*/
|
||||
WP_PIPEWIRE_OBJECT_FEATURES_ALL =
|
||||
(WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL |
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS |
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT |
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE |
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG |
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE),
|
||||
|
||||
WP_PROXY_FEATURE_CUSTOM_START = (1 << 16), /*< skip >*/
|
||||
} WpProxyFeatures;
|
||||
|
||||
/*!
|
||||
* \brief The minimal feature set for proxies implementing WpPipewireObject.
|
||||
* This is a subset of \em WP_PIPEWIRE_OBJECT_FEATURES_ALL
|
||||
* \ingroup wpproxy
|
||||
*/
|
||||
#define WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL \
|
||||
(WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO)
|
||||
|
||||
/*!
|
||||
* \brief The complete common feature set for proxies implementing
|
||||
* WpPipewireObject. This is a subset of \em WP_OBJECT_FEATURES_ALL
|
||||
* \ingroup wpproxy
|
||||
*/
|
||||
#define WP_PIPEWIRE_OBJECT_FEATURES_ALL \
|
||||
(WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL | \
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS | \
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT | \
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE | \
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG | \
|
||||
WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE)
|
||||
|
||||
/*!
|
||||
* \brief The WpProxy GType
|
||||
* \ingroup wpproxy
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ on_session_item_proxy_destroyed_deferred (WpSessionItem * item)
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Helper callback for sub-classes that deffers and unexports
|
||||
* \brief Helper callback for sub-classes that defers and unexports
|
||||
* the session item.
|
||||
*
|
||||
* Only meant to be used when the pipewire proxy destroyed signal is triggered.
|
||||
|
|
|
|||
1302
lib/wp/settings.c
1302
lib/wp/settings.c
File diff suppressed because it is too large
Load diff
|
|
@ -12,8 +12,88 @@
|
|||
#include "object.h"
|
||||
#include "spa-json.h"
|
||||
|
||||
#define WP_SETTINGS_SCHEMA_METADATA_NAME_PREFIX "schema-"
|
||||
#define WP_SETTINGS_PERSISTENT_METADATA_NAME_PREFIX "persistent-"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/*!
|
||||
* \brief The different spec types of a setting
|
||||
* \ingroup wpsettings
|
||||
*/
|
||||
typedef enum {
|
||||
WP_SETTINGS_SPEC_TYPE_UNKNOWN,
|
||||
WP_SETTINGS_SPEC_TYPE_BOOL,
|
||||
WP_SETTINGS_SPEC_TYPE_INT,
|
||||
WP_SETTINGS_SPEC_TYPE_FLOAT,
|
||||
WP_SETTINGS_SPEC_TYPE_STRING,
|
||||
WP_SETTINGS_SPEC_TYPE_ARRAY,
|
||||
WP_SETTINGS_SPEC_TYPE_OBJECT,
|
||||
} WpSettingsSpecType;
|
||||
|
||||
typedef struct _WpSettingsSpec WpSettingsSpec;
|
||||
|
||||
/*!
|
||||
* \brief The WpSettingsSpec GType
|
||||
* \ingroup wpsettings
|
||||
*/
|
||||
#define WP_TYPE_SETTINGS_SPEC (wp_settings_spec_get_type ())
|
||||
WP_API
|
||||
GType wp_settings_spec_get_type (void);
|
||||
|
||||
WP_API
|
||||
WpSettingsSpec *wp_settings_spec_ref (WpSettingsSpec * self);
|
||||
|
||||
WP_API
|
||||
void wp_settings_spec_unref (WpSettingsSpec * self);
|
||||
|
||||
WP_API
|
||||
const gchar * wp_settings_spec_get_name (WpSettingsSpec * self);
|
||||
|
||||
WP_API
|
||||
const gchar * wp_settings_spec_get_description (WpSettingsSpec * self);
|
||||
|
||||
WP_API
|
||||
WpSettingsSpecType wp_settings_spec_get_value_type (WpSettingsSpec * self);
|
||||
|
||||
WP_API
|
||||
WpSpaJson * wp_settings_spec_get_default_value (WpSettingsSpec * self);
|
||||
|
||||
WP_API
|
||||
WpSpaJson * wp_settings_spec_get_min_value (WpSettingsSpec * self);
|
||||
|
||||
WP_API
|
||||
WpSpaJson * wp_settings_spec_get_max_value (WpSettingsSpec * self);
|
||||
|
||||
WP_API
|
||||
gboolean wp_settings_spec_check_value (WpSettingsSpec * self, WpSpaJson *value);
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSettingsSpec, wp_settings_spec_unref)
|
||||
|
||||
/*!
|
||||
* \brief The WpSettingsItem GType
|
||||
* \ingroup wpsettings
|
||||
*/
|
||||
#define WP_TYPE_SETTINGS_ITEM (wp_settings_item_get_type ())
|
||||
WP_API
|
||||
GType wp_settings_item_get_type (void);
|
||||
|
||||
typedef struct _WpSettingsItem WpSettingsItem;
|
||||
|
||||
WP_API
|
||||
WpSettingsItem *wp_settings_item_ref (WpSettingsItem *self);
|
||||
|
||||
WP_API
|
||||
void wp_settings_item_unref (WpSettingsItem *self);
|
||||
|
||||
WP_API
|
||||
const gchar * wp_settings_item_get_key (WpSettingsItem * self);
|
||||
|
||||
WP_API
|
||||
WpSpaJson * wp_settings_item_get_value (WpSettingsItem * self);
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSettingsItem, wp_settings_item_unref)
|
||||
|
||||
/*!
|
||||
* \brief Flags to be used as WpObjectFeatures on WpSettings subclasses.
|
||||
* \ingroup wpsettings
|
||||
|
|
@ -33,8 +113,10 @@ WP_API
|
|||
G_DECLARE_FINAL_TYPE (WpSettings, wp_settings, WP, SETTINGS, WpObject)
|
||||
|
||||
WP_API
|
||||
WpSettings * wp_settings_get_instance (WpCore * core,
|
||||
const gchar *metadata_name);
|
||||
WpSettings * wp_settings_new (WpCore * core, const gchar * metadata_name);
|
||||
|
||||
WP_API
|
||||
WpSettings * wp_settings_find (WpCore * core, const gchar * metadata_name);
|
||||
|
||||
/*!
|
||||
* \brief callback conveying the changed setting and its json value
|
||||
|
|
@ -62,7 +144,38 @@ gboolean wp_settings_unsubscribe (WpSettings *self,
|
|||
guintptr subscription_id);
|
||||
|
||||
WP_API
|
||||
WpSpaJson * wp_settings_get (WpSettings *self, const gchar *setting);
|
||||
WpSpaJson * wp_settings_get (WpSettings *self, const gchar *name);
|
||||
|
||||
WP_API
|
||||
WpSpaJson * wp_settings_get_saved (WpSettings *self, const gchar *name);
|
||||
|
||||
WP_API
|
||||
WpSettingsSpec * wp_settings_get_spec (WpSettings *self, const gchar *name);
|
||||
|
||||
WP_API
|
||||
gboolean wp_settings_set (WpSettings *self, const gchar *name,
|
||||
WpSpaJson *value);
|
||||
|
||||
WP_API
|
||||
gboolean wp_settings_reset (WpSettings *self, const char *name);
|
||||
|
||||
WP_API
|
||||
gboolean wp_settings_save (WpSettings *self, const char *name);
|
||||
|
||||
WP_API
|
||||
gboolean wp_settings_delete (WpSettings *self, const char *name);
|
||||
|
||||
WP_API
|
||||
void wp_settings_reset_all (WpSettings *self);
|
||||
|
||||
WP_API
|
||||
void wp_settings_save_all (WpSettings *self);
|
||||
|
||||
WP_API
|
||||
void wp_settings_delete_all (WpSettings *self);
|
||||
|
||||
WP_API
|
||||
WpIterator * wp_settings_new_iterator (WpSettings *self);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ wp_si_adapter_get_ports_format (WpSiAdapter * self, const gchar **mode)
|
|||
* \param self the session item
|
||||
* \param format (transfer full) (nullable): the format to be set
|
||||
* \param mode (nullable): the mode
|
||||
* \param callback (scope async): the callback to call when the operation is done
|
||||
* \param data (closure): user data for \a callback
|
||||
* \param callback (scope async)(closure data): the callback to call when the operation is done
|
||||
* \param data user data for \a callback
|
||||
*/
|
||||
void
|
||||
wp_si_adapter_set_ports_format (WpSiAdapter * self, WpSpaPod *format,
|
||||
|
|
@ -358,8 +358,8 @@ wp_si_acquisition_default_init (WpSiAcquisitionInterface * iface)
|
|||
* \param self the session item
|
||||
* \param acquisitor the link that is trying to acquire a port info item
|
||||
* \param item the item that is being acquired
|
||||
* \param callback (scope async): the callback to call when the operation is done
|
||||
* \param data (closure): user data for \a callback
|
||||
* \param callback (scope async)(closure data): the callback to call when the operation is done
|
||||
* \param data user data for \a callback
|
||||
*/
|
||||
void
|
||||
wp_si_acquisition_acquire (WpSiAcquisition * self, WpSiLink * acquisitor,
|
||||
|
|
|
|||
|
|
@ -580,7 +580,7 @@ wp_spa_json_new_object_valist (const gchar *key, const gchar *format,
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Checks wether the spa json is of type null or not
|
||||
* \brief Checks whether the spa json is of type null or not
|
||||
*
|
||||
* \ingroup wpspajson
|
||||
* \param self the spa json object
|
||||
|
|
@ -593,7 +593,7 @@ wp_spa_json_is_null (WpSpaJson *self)
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Checks wether the spa json is of type boolean or not
|
||||
* \brief Checks whether the spa json is of type boolean or not
|
||||
*
|
||||
* \ingroup wpspajson
|
||||
* \param self the spa json object
|
||||
|
|
@ -606,7 +606,7 @@ wp_spa_json_is_boolean (WpSpaJson *self)
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Checks wether the spa json is of type int or not
|
||||
* \brief Checks whether the spa json is of type int or not
|
||||
*
|
||||
* \ingroup wpspajson
|
||||
* \param self the spa json object
|
||||
|
|
@ -619,7 +619,7 @@ wp_spa_json_is_int (WpSpaJson *self)
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Checks wether the spa json is of type float or not
|
||||
* \brief Checks whether the spa json is of type float or not
|
||||
*
|
||||
* \ingroup wpspajson
|
||||
* \param self the spa json object
|
||||
|
|
@ -632,7 +632,7 @@ wp_spa_json_is_float (WpSpaJson *self)
|
|||
}
|
||||
|
||||
/*!
|
||||
* \brief Checks wether the spa json is of type string or not
|
||||
* \brief Checks whether the spa json is of type string or not
|
||||
*
|
||||
* \ingroup wpspajson
|
||||
* \param self the spa json object
|
||||
|
|
@ -1355,6 +1355,39 @@ wp_spa_json_parser_new_object (WpSpaJson *json)
|
|||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Creates a new spa json parser for undefined type of data. The \a json
|
||||
* object must be valid for the entire life-cycle of the returned parser.
|
||||
*
|
||||
* This function allows creating a parser object for any type of spa json and is
|
||||
* mostly useful to parse non-standard JSON data that should be treated as if it
|
||||
* were an object or array, but does not start with a '{' or '[' character. Such
|
||||
* data can be for instance a comma-separated list of single values (array) or
|
||||
* key-value pairs (object). Such data is also the main configuration file,
|
||||
* which is an object but doesn't start with a '{' character.
|
||||
*
|
||||
* \note If the data is an array or object, the parser will not enter it and the
|
||||
* only token it will be able to parse is the same \a json object that is passed
|
||||
* in as an argument. Use wp_spa_json_parser_new_array() or
|
||||
* wp_spa_json_parser_new_object() to parse arrays or objects.
|
||||
*
|
||||
* \ingroup wpspajson
|
||||
* \param json the spa json to parse
|
||||
* \returns (transfer full): The new spa json parser
|
||||
* \since 0.5.0
|
||||
*/
|
||||
WpSpaJsonParser *
|
||||
wp_spa_json_parser_new_undefined (WpSpaJson *json)
|
||||
{
|
||||
WpSpaJsonParser *self;
|
||||
|
||||
self = g_rc_box_new0 (WpSpaJsonParser);
|
||||
self->json = json;
|
||||
self->data[0] = *json->json;
|
||||
self->pos = &self->data[0];
|
||||
return self;
|
||||
}
|
||||
|
||||
static int
|
||||
check_nested_size (struct spa_json *parent, const gchar *data, int size)
|
||||
{
|
||||
|
|
@ -1491,6 +1524,10 @@ wp_spa_json_parser_get_string (WpSpaJsonParser *self)
|
|||
/*!
|
||||
* \brief Gets the spa json value from a spa json parser object
|
||||
*
|
||||
* \note the returned spa json object references the original data instead
|
||||
* of copying it, therefore the original data must be valid for the entire
|
||||
* life-cycle of the returned object
|
||||
*
|
||||
* \ingroup wpspajson
|
||||
* \param self the spa json parser object
|
||||
* \returns (transfer full): The spa json value or NULL if it could not be
|
||||
|
|
@ -1500,7 +1537,8 @@ WpSpaJson *
|
|||
wp_spa_json_parser_get_json (WpSpaJsonParser *self)
|
||||
{
|
||||
return wp_spa_json_parser_advance (self) ?
|
||||
wp_spa_json_new_wrap (&self->curr) : NULL;
|
||||
wp_spa_json_new_wrap_stringn (self->curr.cur,
|
||||
self->curr.end - self->curr.cur) : NULL;
|
||||
}
|
||||
|
||||
gboolean
|
||||
|
|
|
|||
|
|
@ -244,6 +244,9 @@ WpSpaJsonParser *wp_spa_json_parser_new_array (WpSpaJson *json);
|
|||
WP_API
|
||||
WpSpaJsonParser *wp_spa_json_parser_new_object (WpSpaJson *json);
|
||||
|
||||
WP_API
|
||||
WpSpaJsonParser *wp_spa_json_parser_new_undefined (WpSpaJson *json);
|
||||
|
||||
WP_API
|
||||
gboolean wp_spa_json_parser_get_null (WpSpaJsonParser *self);
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue