Repository: dimtpap/obs-pipewire-audio-capture
Branch: main
Commit: abb4bd02a1c3
Files: 21
Total size: 114.4 KB
Directory structure:
gitextract_tn9d_h5o/
├── .clang-format
├── .editorconfig
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── CMakeLists.txt
├── LICENSE
├── README.md
├── cmake/
│ └── FindPipeWire.cmake
├── data/
│ └── locale/
│ ├── ar-SA.ini
│ ├── de-DE.ini
│ ├── en-US.ini
│ ├── es-ES.ini
│ ├── fr-FR.ini
│ ├── id-ID.ini
│ ├── pl-PL.ini
│ └── pt-BR.ini
└── src/
├── linux-pipewire-audio.c
├── pipewire-audio-capture-app.c
├── pipewire-audio-capture-device.c
├── pipewire-audio.c
└── pipewire-audio.h
================================================
FILE CONTENTS
================================================
================================================
FILE: .clang-format
================================================
# please use clang-format version 16 or later
Standard: c++17
AccessModifierOffset: -8
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: false
AllowShortLambdasOnASingleLine: Inline
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Custom
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
BreakStringLiterals: false # apparently unpredictable
ColumnLimit: 120
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 8
ContinuationIndentWidth: 8
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
FixNamespaceComments: true
ForEachMacros:
- 'json_object_foreach'
- 'json_object_foreach_safe'
- 'json_array_foreach'
- 'HASH_ITER'
IncludeBlocks: Preserve
IndentCaseLabels: false
IndentPPDirectives: None
IndentWidth: 8
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: true
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 8
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 10
PenaltyBreakBeforeFirstCallParameter: 30
PenaltyBreakComment: 10
PenaltyBreakFirstLessLess: 0
PenaltyBreakString: 10
PenaltyExcessCharacter: 100
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
ReflowComments: false
SkipMacroDefinitionBody: true
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
StatementMacros:
- 'Q_OBJECT'
TabWidth: 8
TypenameMacros:
- 'DARRAY'
UseTab: ForContinuationAndIndentation
---
Language: ObjC
AccessModifierOffset: 2
AlignArrayOfStructures: Right
AlignConsecutiveAssignments: None
AlignConsecutiveBitFields: None
AlignConsecutiveDeclarations: None
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true
AllowShortBlocksOnASingleLine: Never
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: None
AttributeMacros: ['__unused', '__autoreleasing', '_Nonnull', '__bridge']
BitFieldColonSpacing: Both
#BreakBeforeBraces: Webkit
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
AfterClass: true
AfterControlStatement: Never
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: true
BreakAfterAttributes: Never
BreakArrays: false
BreakBeforeConceptDeclarations: Allowed
BreakBeforeInlineASMColon: OnlyMultiline
BreakConstructorInitializers: AfterColon
BreakInheritanceList: AfterComma
ColumnLimit: 120
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
IndentAccessModifiers: false
IndentCaseBlocks: false
IndentCaseLabels: true
IndentExternBlock: Indent
IndentGotoLabels: false
IndentRequiresClause: true
IndentWidth: 4
IndentWrappedFunctionNames: true
InsertBraces: false
InsertNewlineAtEOF: true
KeepEmptyLinesAtTheStartOfBlocks: false
LambdaBodyIndentation: Signature
NamespaceIndentation: All
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: false
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PPIndentWidth: -1
PackConstructorInitializers: NextLine
QualifierAlignment: Leave
ReferenceAlignment: Right
RemoveSemicolon: false
RequiresClausePosition: WithPreceding
RequiresExpressionIndentation: OuterScope
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SortIncludes: false
#SortUsingDeclarations: LexicographicNumeric
SortUsingDeclarations: true
SpaceAfterCStyleCast: true
SpaceAfterLogicalNot: false
SpaceAroundPointerQualifiers: Default
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInConditionalStatement: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
Standard: c++17
TabWidth: 4
UseTab: Never
================================================
FILE: .editorconfig
================================================
root = true
[*]
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = tab
indent_size = 8
[CMakeLists.txt]
indent_style = space
indent_size = 2
[**/CMakeLists.txt]
indent_style = space
indent_size = 2
[cmake/**/*.cmake]
indent_style = space
indent_size = 2
================================================
FILE: .github/workflows/main.yml
================================================
name: Build
on:
push:
paths:
- .github/workflows/main.yml
- 'src/**'
- 'cmake/**'
- 'CMakeLists.txt'
tags:
- '*'
branches:
- '**'
jobs:
build-plugin:
strategy:
matrix:
obs-version: ['28.0.0', '30.2.0']
name: 'Build Plugin'
runs-on: ubuntu-latest
steps:
- name: Restore OBS from cache
uses: actions/cache@v4
id: cache-obs
with:
path: ${{ github.workspace }}/obs/
key: ${{ matrix.obs-version }}
- name: Checkout OBS
if: steps.cache-obs.outputs.cache-hit != 'true'
uses: actions/checkout@v4
with:
repository: 'obsproject/obs-studio'
path: 'obs-src'
ref: ${{ matrix.obs-version }}
submodules: 'recursive'
- name: 'Install system dependencies'
run: |
sudo apt update
sudo apt install cmake ninja-build pkg-config clang clang-format build-essential curl ccache git zsh\
libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev\
libcurl4-openssl-dev\
libxcb1-dev libx11-xcb-dev\
libgl1-mesa-dev\
libglvnd-dev\
libgles2-mesa-dev\
libpipewire-0.3-dev\
uuid-dev\
uthash-dev libjansson-dev
- name: 'Configure OBS'
if: steps.cache-obs.outputs.cache-hit != 'true'
run: cmake -B obs-src/build -S obs-src -DOBS_CMAKE_VERSION=3 -DENABLE_BROWSER=OFF -DENABLE_UI=OFF -DENABLE_SCRIPTING=OFF -DENABLE_PULSEAUDIO=OFF -DENABLE_WAYLAND=OFF -DENABLE_PLUGINS=OFF
- name: 'Build OBS'
if: steps.cache-obs.outputs.cache-hit != 'true'
run: cmake --build obs-src/build -j4
- name: 'Install OBS'
if: steps.cache-obs.outputs.cache-hit != 'true'
run: cmake --install obs-src/build --prefix obs
- name: 'Checkout'
uses: actions/checkout@v4
with:
path: 'plugin'
- name: 'Configure'
run: cmake -B ./plugin/build -S ./plugin -DCMAKE_BUILD_TYPE=RelWithDebInfo -Dlibobs_DIR="$GITHUB_WORKSPACE/obs/lib/cmake/libobs/"
- name: 'Build'
run: cmake --build ./plugin/build -j4
- name: 'Package'
run: |
mkdir -p ./linux-pipewire-audio/bin/64bit
cp ./plugin/build/linux-pipewire-audio.so ./linux-pipewire-audio/bin/64bit/linux-pipewire-audio.so
cp -r ./plugin/data/ ./linux-pipewire-audio/data/
tar -zcvf linux-pipewire-audio-$OBS_VERSION.tar.gz linux-pipewire-audio
env:
OBS_VERSION: ${{ matrix.obs-version }}
- name: 'Upload'
uses: actions/upload-artifact@v4
with:
path: linux-pipewire-audio-${{ matrix.obs-version }}.tar.gz
name: linux-pipewire-audio-${{ matrix.obs-version }}
================================================
FILE: .gitignore
================================================
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
/build
.vscode
================================================
FILE: CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.10)
project(linux-pipewire-audio)
include(GNUInstallDirs)
set(linux-pipewire-audio_SOURCES
src/linux-pipewire-audio.c
src/pipewire-audio.h
src/pipewire-audio.c
src/pipewire-audio-capture-device.c
src/pipewire-audio-capture-app.c
)
add_library(linux-pipewire-audio MODULE ${linux-pipewire-audio_SOURCES})
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake")
find_package(libobs REQUIRED)
find_package(PipeWire REQUIRED)
set(linux-pipewire-audio_INCLUDES
${PIPEWIRE_INCLUDE_DIRS}
${SPA_INCLUDE_DIRS}
)
add_definitions(
${PIPEWIRE_DEFINITIONS}
)
set(linux-pipewire-audio_LIBRARIES
OBS::libobs
${PIPEWIRE_LIBRARIES}
)
target_link_libraries(linux-pipewire-audio ${linux-pipewire-audio_LIBRARIES})
target_compile_options(linux-pipewire-audio PRIVATE -Wall)
include_directories(SYSTEM
${linux-pipewire-audio_INCLUDES}
)
set_target_properties(linux-pipewire-audio PROPERTIES PREFIX "")
install(TARGETS linux-pipewire-audio LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/obs-plugins)
install(DIRECTORY data/locale DESTINATION ${CMAKE_INSTALL_DATADIR}/obs/obs-plugins/linux-pipewire-audio)
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
================================================
FILE: README.md
================================================
# Audio device and application capture for OBS Studio using PipeWire
This plugin adds 3 sources for capturing audio outputs, inputs and applications using [PipeWire](https://pipewire.org)


## Usage
### Requirements
- OBS Studio 28.0 or later
- WirePlumber
PipeWire 0.3.62 or later is highly recommended ([#17](https://github.com/dimtpap/obs-pipewire-audio-capture/issues/17), [PipeWire#2874](https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/2874))
For the plugin to be able to capture applications, PipeWire should be set up to handle audio on your system.
For most applications, the `pipewire-pulse` compatibility layer should be enough, but there are also
`pipewire-jack` and `pipewire-alsa`. If applications aren't showing up in the plugin, your system may be
missing one of those components.
### Installation
1. Get the `linux-pipewire-audio-(version).tar.gz` archive from the [latest release](https://github.com/dimtpap/obs-pipewire-audio-capture/releases/latest)
2. In OBS Studio, go to **File**, then click **Show Settings Folder**
3. In the folder that opens, create a folder called `plugins` if it doesn't already exist
4. Extract the archive you downloaded in the `plugins` folder
5. Restart OBS Studio
6. If you're using the Flatpak and the sources aren't working, run `flatpak override --filesystem=xdg-run/pipewire-0 com.obsproject.Studio` and restart OBS Studio
Your files should look like this
```
.../obs-studio/plugins
├── linux-pipewire-audio
│ ├── bin
│ │ └── 64bit
│ │ └── linux-pipewire-audio.so
│ └── data
│ └── locale
│ ...
```
> [!IMPORTANT]
> ## Flatpak users note
> ***THIS INSTALLATION METHOD IS UNSUPPORTED BY THE OBS STUDIO TEAM AND CAN BREAK AT ANY TIME***
> This plugin relies on a Flatpak permission that OBS Studio could remove at any time, so it can't be on Flathub.
> If after updating OBS Studio the plugin stops working, check the latest release for a new version, or build the plugin yourself
> against the latest OBS Studio.
>
> Note that native OBS Studio packages do not have this problem.
### Building (for development)
Ensure you have CMake, PipeWire and OBS Studio/libobs development packages, then in the repo's root:
```sh
cmake -B build -DCMAKE_INSTALL_PREFIX="/usr" -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build
# To install it system-wide:
cmake --install build
```
## Inclusion in upstream OBS Studio
This plugin is currently in the process of being worked on to merge into upstream OBS Studio. See https://github.com/obsproject/obs-studio/pull/6207
================================================
FILE: cmake/FindPipeWire.cmake
================================================
# .rst: FindPipeWire
# -------
#
# Try to find PipeWire on a Unix system.
#
# This will define the following variables:
#
# ``PIPEWIRE_FOUND`` True if (the requested version of) PipeWire is available
# ``PIPEWIRE_VERSION`` The version of PipeWire ``PIPEWIRE_LIBRARIES`` This can
# be passed to target_link_libraries() instead of the ``PipeWire::PipeWire``
# target ``PIPEWIRE_INCLUDE_DIRS`` This should be passed to
# target_include_directories() if the target is not used for linking
# ``PIPEWIRE_COMPILE_FLAGS`` This should be passed to target_compile_options()
# if the target is not used for linking
#
# If ``PIPEWIRE_FOUND`` is TRUE, it will also define the following imported
# target:
#
# ``PipeWire::PipeWire`` The PipeWire library
#
# In general we recommend using the imported target, as it is easier to use.
# Bear in mind, however, that if the target is in the link interface of an
# exported library, it must be made available by the package config file.
# =============================================================================
# Copyright 2014 Alex Merry <alex.merry@kde.org> Copyright 2014 Martin Gräßlin
# <mgraesslin@kde.org> Copyright 2018-2020 Jan Grulich <jgrulich@redhat.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the copyright notice, this list
# of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# =============================================================================
# Use pkg-config to get the directories and then use these values in the
# FIND_PATH() and FIND_LIBRARY() calls
find_package(PkgConfig QUIET)
pkg_search_module(PKG_PIPEWIRE QUIET libpipewire-0.3)
pkg_search_module(PKG_SPA QUIET libspa-0.2)
set(PIPEWIRE_COMPILE_FLAGS "${PKG_PIPEWIRE_CFLAGS}" "${PKG_SPA_CFLAGS}")
set(PIPEWIRE_VERSION "${PKG_PIPEWIRE_VERSION}")
find_path(
PIPEWIRE_INCLUDE_DIRS
NAMES pipewire/pipewire.h
HINTS ${PKG_PIPEWIRE_INCLUDE_DIRS} ${PKG_PIPEWIRE_INCLUDE_DIRS}/pipewire-0.3)
find_path(
SPA_INCLUDE_DIRS
NAMES spa/param/props.h
HINTS ${PKG_SPA_INCLUDE_DIRS} ${PKG_SPA_INCLUDE_DIRS}/spa-0.2)
find_library(
PIPEWIRE_LIBRARIES
NAMES pipewire-0.3
HINTS ${PKG_PIPEWIRE_LIBRARY_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
PipeWire
FOUND_VAR PIPEWIRE_FOUND
REQUIRED_VARS PIPEWIRE_LIBRARIES PIPEWIRE_INCLUDE_DIRS SPA_INCLUDE_DIRS
VERSION_VAR PIPEWIRE_VERSION)
if(PIPEWIRE_FOUND AND NOT TARGET PipeWire::PipeWire)
add_library(PipeWire::PipeWire UNKNOWN IMPORTED)
set_target_properties(
PipeWire::PipeWire
PROPERTIES IMPORTED_LOCATION "${PIPEWIRE_LIBRARIES}"
INTERFACE_COMPILE_OPTIONS "${PIPEWIRE_COMPILE_FLAGS}"
INTERFACE_INCLUDE_DIRECTORIES
"${PIPEWIRE_INCLUDE_DIRS};${SPA_INCLUDE_DIRS}")
endif()
mark_as_advanced(PIPEWIRE_LIBRARIES PIPEWIRE_INCLUDE_DIRS)
include(FeatureSummary)
set_package_properties(
PipeWire PROPERTIES
URL "https://www.pipewire.org"
DESCRIPTION "PipeWire - multimedia processing")
================================================
FILE: data/locale/ar-SA.ini
================================================
PipeWireAudioCaptureInput="التقاط مدخل الصوت (PipeWire)"
PipeWireAudioCaptureOutput="التقاط مخرج الصوت (PipeWire)"
PipeWireAudioCaptureApplication="التقاط صوت تطبيق (PipeWire)"
AppCaptureMode="وضع الالتقاط"
SingleApp="تطبيق واحد"
MultipleApps="عدة تطبيقات"
MatchPriority="اولوية المقارنة"
MatchBinaryFirst="قارن باسم الملف التنفيذي، ثم باسم التطبيق"
MatchAppNameFirst="قارن باسم التطبيق، ثم باسم الملف التنفيذي"
Device="الجهاز"
Default="افتراضي"
Application="تطبيق"
Applications="تطبيقات"
ExceptApp="التقط جميع التطبيقات ماعدا المحدده"
SelectedApps="التطبيقات المحدده"
AddToSelected="اضافة تحديد"
================================================
FILE: data/locale/de-DE.ini
================================================
PipeWireAudioCaptureInput="Audioeingabeaufnahme (PipeWire)"
PipeWireAudioCaptureOutput="Audioausgabeaufnahme (PipeWire)"
PipeWireAudioCaptureApplication="Anwendungsaudioaufnahme (PipeWire)"
AppCaptureMode="Aufnahmemodus"
SingleApp="Einzelne Anwendung"
MultipleApps="Mehrere Anwendungen"
MatchPriority="Übereinstimmungspriorität"
MatchBinaryFirst="Nach Ausführungsdatei abgleichen, dann Anwendungsname"
MatchAppNameFirst="Nach Anwendungsname abgleichen, dann Ausführungsdatei"
Device="Gerät"
Default="Standard"
Application="Anwendung"
Applications="Anwendungen"
ExceptApp="Alle Anwendungen außer den ausgewählten aufnehmen"
SelectedApps="Ausgewählte Anwendungen"
AddToSelected="Zur Auswahl hinzufügen"
================================================
FILE: data/locale/en-US.ini
================================================
PipeWireAudioCaptureInput="Audio Input Capture (PipeWire)"
PipeWireAudioCaptureOutput="Audio Output Capture (PipeWire)"
PipeWireAudioCaptureApplication="Application Audio Capture (PipeWire)"
AppCaptureMode="Capture Mode"
SingleApp="Single application"
MultipleApps="Multiple applications"
MatchPriority="Match Priority"
MatchBinaryFirst="Match by executable name, fallback to app name"
MatchAppNameFirst="Match by app name, fallback to executable name"
Device="Device"
Default="Default"
Application="Application"
Applications="Applications"
ExceptApp="Capture all apps except selected"
SelectedApps="Selected Apps"
AddToSelected="Add selection"
================================================
FILE: data/locale/es-ES.ini
================================================
PipeWireAudioCaptureInput="Captura de entrada de audio (PipeWire)"
PipeWireAudioCaptureOutput="Captura de salida de audio (PipeWire)"
PipeWireAudioCaptureApplication="Captura de audio de la aplicación (PipeWire)"
AppCaptureMode="Modo de captura"
SingleApp="Aplicación única"
MultipleApps="Múltiples aplicaciones"
MatchPriority="Prioridad de coincidencia"
MatchBinaryFirst="Coincidir por nombre del ejecutable, luego por nombre de la aplicación"
MatchAppNameFirst="Coincidir por nombre de la aplicación, luego por nombre del ejecutable"
Device="Dispositivo"
Default="Por defecto"
Application="Aplicación"
Applications="Aplicaciones"
ExceptApp="Capturar todas las aplicaciones excepto las seleccionadas"
SelectedApps="Aplicaciones seleccionadas"
AddToSelected="Agregar a la selección"
================================================
FILE: data/locale/fr-FR.ini
================================================
PipeWireAudioCaptureInput="Capture de l'entrée audio (PipeWire)"
PipeWireAudioCaptureOutput="Capture de la sortie audio (PipeWire)"
PipeWireAudioCaptureApplication="Capture audio de l'application (PipeWire)"
AppCaptureMode="Mode de capture"
SingleApp="Application unique"
MultipleApps="Applications multiples"
MatchPriority="Priorité de correspondance"
MatchBinaryFirst="Correspondance par nom d'executable, repli par nom d'application"
MatchAppNameFirst="Correspondance par nom d'application, repli par nom d'executable"
Device="Appareil"
Default="Par défaut"
Application="Application"
Applications="Applications"
ExceptApp="Capturer toutes les applications sauf celles sélectionnées"
SelectedApps="Applications sélectionnées"
AddToSelected="Ajouter à la sélection"
================================================
FILE: data/locale/id-ID.ini
================================================
PipeWireAudioCaptureInput="Penangkap Input Audio (PipeWire)"
PipeWireAudioCaptureOutput="Penangkap Output Audio (PipeWire)"
PipeWireAudioCaptureApplication="Aplikasi Penangkap Audio (PipeWire)"
AppCaptureMode="Mode Tangkapan"
SingleApp="Satu aplikasi"
MultipleApps="Banyak aplikasi"
MatchPriority="Prioritas Kecocokan"
MatchBinaryFirst="Sesuai nama program. Jika tidak, cari nama aplikasinya"
MatchAppNameFirst="Sesuai nama aplikasi. Jika tidak, cari nama programnya"
Device="Perangkat"
Default="Bawaan"
Application="Aplikasi"
Applications="Aplikasi"
ExceptApp="Tangkap semua aplikasi kecuali yang dipilih"
SelectedApps="Aplikasi pilihan"
AddToSelected="Tambahkan Pilihan"
================================================
FILE: data/locale/pl-PL.ini
================================================
PipeWireAudioCaptureInput="Przechwytywanie wejścia dźwięku (PipeWire)"
PipeWireAudioCaptureOutput="Przechwytywanie wyjścia dźwięku (PipeWire)"
PipeWireAudioCaptureApplication="Przechwytywanie dźwięku aplikacji (PipeWire)"
AppCaptureMode="Tryb Przechwytywania"
SingleApp="Jedna aplikacja"
MultipleApps="Wiele aplikacji"
MatchPriority="Dopasuj priorytet"
MatchBinaryFirst="Dopasuj przez nazwę pliku uruchamiąjącego, inaczej użyj nazwę aplikacji"
MatchAppNameFirst="Dopasuj przez nazwę aplikacji, inaczej użyj nazwę pliku uruchamiąjącego"
Device="Urządzenie"
Default="Domyślne"
Application="Aplikacja"
Applications="Aplikacje"
ExceptApp="Przechwytuj wszystkie aplikacje poza wybranymi"
SelectedApps="Wybranne Aplikacje"
AddToSelected="Dodaj do wybranych"
================================================
FILE: data/locale/pt-BR.ini
================================================
PipeWireAudioCaptureInput="Captura de entrada de áudio (PipeWire)"
PipeWireAudioCaptureOutput="Captura de saída de áudio (PipeWire)"
PipeWireAudioCaptureApplication="Captura de áudio de aplicativo (PipeWire)"
AppCaptureMode="Modo de captura"
SingleApp="Aplicativo único"
MultipleApps="Múltiplos aplicativos"
MatchPriority="Prioridade de correspondência"
MatchBinaryFirst="Corresponder ao nome do executável, se falhar, ao nome do app"
MatchAppNameFirst="Corresponder ao nome do app, se falhar, ao nome do executável"
Device="Dispositivo"
Default="Padrão"
Application="Aplicativo"
Applications="Aplicativos"
ExceptApp="Capturar todos os apps, exceto o selecionado"
SelectedApps="Apps selecionados"
AddToSelected="Adicionar à seleção"
================================================
FILE: src/linux-pipewire-audio.c
================================================
/* linux-pipewire-audio.c
*
* Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <obs-module.h>
#include <pipewire/pipewire.h>
#include "pipewire-audio.h"
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("linux-pipewire-audio", "en-US")
MODULE_EXPORT const char *obs_module_description(void)
{
return "PipeWire input, output and application audio capture";
}
bool obs_module_load(void)
{
pw_init(NULL, NULL);
pipewire_audio_capture_load();
pipewire_audio_capture_app_load();
return true;
}
void obs_module_unload(void)
{
#if PW_CHECK_VERSION(0, 3, 49)
pw_deinit();
#endif
}
================================================
FILE: src/pipewire-audio-capture-app.c
================================================
/* pipewire-audio-capture-app.c
*
* Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "pipewire-audio.h"
#include <spa/debug/types.h>
#include <util/dstr.h>
/* Source for capturing applciation audio using PipeWire */
struct target_node_port {
const char *channel;
uint32_t id;
};
struct target_node {
const char *name;
const char *app_name;
const char *binary;
uint32_t client_id;
uint32_t id;
struct obs_pw_audio_proxy_list ports;
uint32_t *p_n_nodes;
struct spa_hook node_listener;
};
struct target_client {
const char *app_name;
const char *binary;
uint32_t id;
struct spa_hook client_listener;
};
struct system_sink {
const char *name;
uint32_t id;
};
struct capture_sink_link {
uint32_t id;
};
struct capture_sink_port {
const char *channel;
uint32_t id;
};
enum capture_mode { CAPTURE_MODE_SINGLE, CAPTURE_MODE_MULTIPLE };
enum match_priority { MATCH_PRIORITY_BINARY_NAME, MATCH_PRIORITY_APP_NAME };
#define SETTING_CAPTURE_MODE "CaptureMode"
#define SETTING_MATCH_PRIORITY "MatchPriorty"
#define SETTING_EXCLUDE_SELECTIONS "ExceptApp"
#define SETTING_SELECTION_SINGLE "TargetName"
#define SETTING_SELECTION_MULTIPLE "apps"
#define SETTING_AVAILABLE_APPS "AppToAdd"
#define SETTING_ADD_TO_SELECTIONS "AddToSelected"
/** This source basically works like this:
- Keep track of output streams and their ports, system sinks and the default sink
- Keep track of the channels of the default system sink and create a new virtual sink,
destroying the previously made one, with the same channels, then connect the stream to it
- Connect any registered or new stream ports to the sink
*/
struct obs_pw_audio_capture_app {
obs_source_t *source;
struct obs_pw_audio_instance pw;
/** The app capture sink automatically mixes
* the audio of all the app streams */
struct {
struct pw_proxy *proxy;
struct spa_hook proxy_listener;
bool autoconnect_targets;
uint32_t id;
uint32_t serial;
uint32_t channels;
struct dstr position;
DARRAY(struct capture_sink_port) ports;
/* Links between app streams and the capture sink */
struct obs_pw_audio_proxy_list links;
} sink;
/** Need the default system sink to create
* the app capture sink with the same audio channels */
struct obs_pw_audio_proxy_list system_sinks;
struct {
struct obs_pw_audio_default_node_metadata metadata;
struct pw_proxy *proxy;
struct spa_hook node_listener;
struct spa_hook proxy_listener;
} default_sink;
struct obs_pw_audio_proxy_list clients;
struct obs_pw_audio_proxy_list nodes;
uint32_t n_nodes;
enum capture_mode capture_mode;
enum match_priority match_priority;
bool except;
DARRAY(const char *) selections;
};
/* System sinks */
static void system_sink_destroy_cb(void *data)
{
struct system_sink *s = data;
bfree((void *)s->name);
}
static void register_system_sink(struct obs_pw_audio_capture_app *pwac, uint32_t global_id, const char *name)
{
struct pw_proxy *sink_proxy = pw_registry_bind(pwac->pw.registry, global_id, PW_TYPE_INTERFACE_Node,
PW_VERSION_NODE, sizeof(struct system_sink));
if (!sink_proxy) {
return;
}
struct system_sink *sink = pw_proxy_get_user_data(sink_proxy);
sink->name = bstrdup(name);
sink->id = global_id;
obs_pw_audio_proxy_list_append(&pwac->system_sinks, sink_proxy);
}
/* ------------------------------------------------- */
/* Target clients */
static void client_destroy_cb(void *data)
{
struct target_client *client = data;
bfree((void *)client->app_name);
bfree((void *)client->binary);
spa_hook_remove(&client->client_listener);
}
static void on_client_info_cb(void *data, const struct pw_client_info *info)
{
if ((info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS) == 0 || !info->props || !info->props->n_items) {
return;
}
const char *binary = spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_BINARY);
if (!binary) {
return;
}
struct target_client *client = data;
bfree((void *)client->binary);
client->binary = bstrdup(binary);
}
static const struct pw_client_events client_events = {
PW_VERSION_CLIENT_EVENTS,
.info = on_client_info_cb,
};
static void register_target_client(struct obs_pw_audio_capture_app *pwac, uint32_t global_id, const char *app_name)
{
struct pw_proxy *client_proxy = pw_registry_bind(pwac->pw.registry, global_id, PW_TYPE_INTERFACE_Client,
PW_VERSION_CLIENT, sizeof(struct target_client));
if (!client_proxy) {
return;
}
struct target_client *client = pw_proxy_get_user_data(client_proxy);
client->binary = NULL;
client->app_name = bstrdup(app_name);
client->id = global_id;
obs_pw_audio_proxy_list_append(&pwac->clients, client_proxy);
pw_proxy_add_object_listener(client_proxy, &client->client_listener, &client_events, client);
}
/* Target nodes and ports */
static void port_destroy_cb(void *data)
{
struct target_node_port *p = data;
bfree((void *)p->channel);
}
static void node_destroy_cb(void *data)
{
struct target_node *node = data;
spa_hook_remove(&node->node_listener);
obs_pw_audio_proxy_list_clear(&node->ports);
(*node->p_n_nodes)--;
bfree((void *)node->binary);
bfree((void *)node->app_name);
bfree((void *)node->name);
}
static struct target_node_port *node_register_port(struct target_node *node, uint32_t global_id,
struct pw_registry *registry, const char *channel)
{
struct pw_proxy *port_proxy = pw_registry_bind(registry, global_id, PW_TYPE_INTERFACE_Port, PW_VERSION_PORT,
sizeof(struct target_node_port));
if (!port_proxy) {
return NULL;
}
struct target_node_port *port = pw_proxy_get_user_data(port_proxy);
port->channel = bstrdup(channel);
port->id = global_id;
obs_pw_audio_proxy_list_append(&node->ports, port_proxy);
return port;
}
static void on_node_info_cb(void *data, const struct pw_node_info *info)
{
if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) == 0 || !info->props || !info->props->n_items) {
return;
}
const char *binary = spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_BINARY);
if (!binary) {
return;
}
struct target_node *node = data;
bfree((void *)node->binary);
node->binary = bstrdup(binary);
}
static const struct pw_node_events node_events = {
PW_VERSION_NODE_EVENTS,
.info = on_node_info_cb,
};
static void register_target_node(struct obs_pw_audio_capture_app *pwac, uint32_t global_id, uint32_t client_id,
const char *app_name, const char *name)
{
struct pw_proxy *node_proxy = pw_registry_bind(pwac->pw.registry, global_id, PW_TYPE_INTERFACE_Node,
PW_VERSION_NODE, sizeof(struct target_node));
if (!node_proxy) {
return;
}
struct target_node *node = pw_proxy_get_user_data(node_proxy);
node->name = bstrdup(name);
node->app_name = bstrdup(app_name);
node->binary = NULL;
node->id = global_id;
node->client_id = client_id;
node->p_n_nodes = &pwac->n_nodes;
obs_pw_audio_proxy_list_init(&node->ports, NULL, port_destroy_cb);
pwac->n_nodes++;
obs_pw_audio_proxy_list_append(&pwac->nodes, node_proxy);
pw_proxy_add_object_listener(node_proxy, &node->node_listener, &node_events, node);
}
static bool node_is_targeted(struct obs_pw_audio_capture_app *pwac, struct target_node *node)
{
bool targeted = false;
for (size_t i = 0; i < pwac->selections.num && !targeted; i++) {
const char *selection = pwac->selections.array[i];
targeted = (astrcmpi(selection, node->binary) == 0 || astrcmpi(selection, node->app_name) == 0 ||
astrcmpi(selection, node->name) == 0);
if (!targeted && node->client_id) {
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->clients);
struct target_client *client;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&client)) {
if (client->id == node->client_id) {
targeted = (astrcmpi(selection, client->binary) == 0 ||
astrcmpi(selection, client->app_name) == 0);
break;
}
}
}
}
return targeted ^ pwac->except;
}
/* ------------------------------------------------- */
/* App streams <-> Capture sink links */
static void link_bound_cb(void *data, uint32_t global_id)
{
struct capture_sink_link *link = data;
link->id = global_id;
}
static void link_destroy_cb(void *data)
{
struct capture_sink_link *link = data;
blog(LOG_DEBUG, "[pipewire-audio] Link %u destroyed", link->id);
}
static void link_port_to_sink(struct obs_pw_audio_capture_app *pwac, struct target_node_port *port, uint32_t node_id)
{
blog(LOG_DEBUG, "[pipewire-audio] Connecting port %u of node %u to app capture sink", port->id, node_id);
uint32_t p = 0;
if (pwac->sink.channels == 1 && /* Mono capture sink */
pwac->sink.ports.num >= 1) {
p = pwac->sink.ports.array[0].id;
} else {
for (size_t i = 0; i < pwac->sink.ports.num; i++) {
if (astrcmpi(pwac->sink.ports.array[i].channel, port->channel) == 0) {
p = pwac->sink.ports.array[i].id;
break;
}
}
}
if (!p) {
blog(LOG_WARNING,
"[pipewire-audio] Could not connect port %u of node %u to app capture sink. No port of app capture sink has channel %s",
port->id, node_id, port->channel);
return;
}
struct pw_properties *link_props = pw_properties_new(PW_KEY_OBJECT_LINGER, "false", NULL);
pw_properties_setf(link_props, PW_KEY_LINK_OUTPUT_NODE, "%u", node_id);
pw_properties_setf(link_props, PW_KEY_LINK_OUTPUT_PORT, "%u", port->id);
pw_properties_setf(link_props, PW_KEY_LINK_INPUT_NODE, "%u", pwac->sink.id);
pw_properties_setf(link_props, PW_KEY_LINK_INPUT_PORT, "%u", p);
struct pw_proxy *link_proxy = pw_core_create_object(pwac->pw.core, "link-factory", PW_TYPE_INTERFACE_Link,
PW_VERSION_LINK, &link_props->dict,
sizeof(struct capture_sink_link));
pw_properties_free(link_props);
if (!link_proxy) {
blog(LOG_WARNING, "[pipewire-audio] Could not connect port %u of node %u to app capture sink", port->id,
node_id);
return;
}
struct capture_sink_link *link = pw_proxy_get_user_data(link_proxy);
link->id = SPA_ID_INVALID;
obs_pw_audio_proxy_list_append(&pwac->sink.links, link_proxy);
}
static void link_node_to_sink(struct obs_pw_audio_capture_app *pwac, struct target_node *node)
{
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &node->ports);
struct target_node_port *port;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&port)) {
link_port_to_sink(pwac, port, node->id);
}
}
/* ------------------------------------------------- */
/* App capture sink */
/** The app capture sink is created when there
* is info about the system's default sink.
* See the on_metadata and on_default_sink callbacks */
static void destroy_sink_links(struct obs_pw_audio_capture_app *pwac)
{
obs_pw_audio_proxy_list_clear(&pwac->sink.links);
}
static void connect_targets(struct obs_pw_audio_capture_app *pwac)
{
if (!pwac->sink.proxy) {
return;
}
destroy_sink_links(pwac);
if (pwac->selections.num == 0) {
return;
}
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->nodes);
struct target_node *node;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {
if (node_is_targeted(pwac, node)) {
link_node_to_sink(pwac, node);
}
}
}
static void finalize_capture_sink(struct obs_pw_audio_capture_app *pwac)
{
if (!pwac->sink.proxy || pwac->sink.id == SPA_ID_INVALID || pwac->sink.serial == SPA_ID_INVALID ||
pwac->sink.ports.num != pwac->sink.channels) {
return;
}
blog(LOG_DEBUG, "[pipewire-audio] App capture sink ready");
connect_targets(pwac);
pwac->sink.autoconnect_targets = true;
if (obs_pw_audio_stream_connect(&pwac->pw.audio, pwac->sink.id, pwac->sink.serial, pwac->sink.channels) < 0) {
blog(LOG_WARNING, "[pipewire-audio] Error connecting stream %p to app capture sink %u",
pwac->pw.audio.stream, pwac->sink.id);
}
}
static void on_sink_proxy_bound_cb(void *data, uint32_t global_id)
{
struct obs_pw_audio_capture_app *pwac = data;
pwac->sink.id = global_id;
da_init(pwac->sink.ports);
}
static void on_sink_proxy_removed_cb(void *data)
{
struct obs_pw_audio_capture_app *pwac = data;
blog(LOG_WARNING, "[pipewire-audio] App capture sink %u has been destroyed by the PipeWire remote",
pwac->sink.id);
pw_proxy_destroy(pwac->sink.proxy);
}
static void on_sink_proxy_destroy_cb(void *data)
{
struct obs_pw_audio_capture_app *pwac = data;
spa_hook_remove(&pwac->sink.proxy_listener);
spa_zero(pwac->sink.proxy_listener);
for (size_t i = 0; i < pwac->sink.ports.num; i++) {
struct capture_sink_port *p = &pwac->sink.ports.array[i];
bfree((void *)p->channel);
}
da_free(pwac->sink.ports);
pwac->sink.channels = 0;
dstr_free(&pwac->sink.position);
pwac->sink.autoconnect_targets = false;
pwac->sink.proxy = NULL;
blog(LOG_DEBUG, "[pipewire-audio] App capture sink %u destroyed", pwac->sink.id);
pwac->sink.id = SPA_ID_INVALID;
}
static void on_sink_proxy_error_cb(void *data, int seq, int res, const char *message)
{
UNUSED_PARAMETER(data);
blog(LOG_ERROR, "[pipewire-audio] App capture sink error: seq:%d res:%d :%s", seq, res, message);
}
static const struct pw_proxy_events sink_proxy_events = {
PW_VERSION_PROXY_EVENTS,
.bound = on_sink_proxy_bound_cb,
.removed = on_sink_proxy_removed_cb,
.destroy = on_sink_proxy_destroy_cb,
.error = on_sink_proxy_error_cb,
};
static void register_capture_sink_port(struct obs_pw_audio_capture_app *pwac, uint32_t global_id, const char *channel)
{
blog(LOG_DEBUG, "[pipewire-audio] Registering app capture sink port %u", global_id);
struct capture_sink_port *port = da_push_back_new(pwac->sink.ports);
port->channel = bstrdup(channel);
port->id = global_id;
finalize_capture_sink(pwac);
}
static void make_capture_sink(struct obs_pw_audio_capture_app *pwac, uint32_t channels, const char *position)
{
struct pw_properties *sink_props = pw_properties_new(PW_KEY_FACTORY_NAME, "support.null-audio-sink",
PW_KEY_MEDIA_CLASS, "Stream/Input/Audio",
PW_KEY_NODE_VIRTUAL, "true", SPA_KEY_AUDIO_POSITION,
position, NULL);
pw_properties_setf(sink_props, PW_KEY_NODE_NAME, "OBS: %s", obs_source_get_name(pwac->source));
pw_properties_setf(sink_props, PW_KEY_AUDIO_CHANNELS, "%u", channels);
pwac->sink.proxy = pw_core_create_object(pwac->pw.core, "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE,
&sink_props->dict, 0);
pw_properties_free(sink_props);
if (!pwac->sink.proxy) {
blog(LOG_WARNING, "[pipewire-audio] Failed to create app capture sink");
return;
}
pwac->sink.channels = channels;
dstr_copy(&pwac->sink.position, position);
pwac->sink.id = SPA_ID_INVALID;
pwac->sink.serial = SPA_ID_INVALID;
pw_proxy_add_listener(pwac->sink.proxy, &pwac->sink.proxy_listener, &sink_proxy_events, pwac);
blog(LOG_DEBUG, "[pipewire-audio] Created app capture sink");
}
static void destroy_capture_sink(struct obs_pw_audio_capture_app *pwac)
{
/* Links are automatically destroyed by PipeWire */
if (!pwac->sink.proxy) {
return;
}
if (pw_stream_get_state(pwac->pw.audio.stream, NULL) != PW_STREAM_STATE_UNCONNECTED) {
pw_stream_disconnect(pwac->pw.audio.stream);
}
pwac->sink.autoconnect_targets = false;
pw_proxy_destroy(pwac->sink.proxy);
}
/* ------------------------------------------------- */
/* Default system sink */
static void on_default_sink_param_cb(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,
const struct spa_pod *param)
{
UNUSED_PARAMETER(seq);
UNUSED_PARAMETER(index);
UNUSED_PARAMETER(next);
if (id != SPA_PARAM_EnumFormat) {
return;
}
struct obs_pw_audio_capture_app *pwac = data;
uint32_t media_type = 0, media_subtype = 0, parsed_id = 0, channels = 0;
struct spa_pod *position_pod = NULL;
struct spa_pod_parser p;
spa_pod_parser_pod(&p, param);
spa_pod_parser_get_object(&p, SPA_TYPE_OBJECT_Format, &parsed_id, SPA_FORMAT_mediaType, SPA_POD_Id(&media_type),
SPA_FORMAT_mediaSubtype, SPA_POD_Id(&media_subtype), SPA_FORMAT_AUDIO_channels,
SPA_POD_OPT_Int(&channels), SPA_FORMAT_AUDIO_position,
SPA_POD_OPT_Pod(&position_pod));
if (pwac->sink.channels && !channels) {
// It's likely we got the channels from a proper format already
return;
}
if (parsed_id != SPA_PARAM_EnumFormat || media_type != SPA_MEDIA_TYPE_audio || !channels || !position_pod) {
goto stereo_fallback;
}
uint32_t position_n = 0;
uint32_t *position_arr = spa_pod_get_array(position_pod, &position_n);
struct dstr position_str;
dstr_init(&position_str);
for (size_t i = 0; i < position_n; i++) {
const char *chn = spa_debug_type_find_short_name(spa_type_audio_channel, position_arr[i]);
if (strstr(chn, "AUX") != NULL) {
// Sink is configured for pro audio, use stereo
// https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#what-is-the-pro-audio-profile
channels = 2;
dstr_copy(&position_str, "FL,FR");
break;
}
dstr_cat(&position_str, chn);
if (position_n - 1 != i) {
dstr_cat_ch(&position_str, ',');
}
}
if (channels != pwac->sink.channels || dstr_cmpi(&position_str, pwac->sink.position.array) != 0) {
destroy_capture_sink(pwac);
make_capture_sink(pwac, channels, position_str.array);
}
dstr_free(&position_str);
return;
stereo_fallback:
if (pwac->sink.proxy) {
return;
}
blog(LOG_WARNING, "[pipewire-audio] Could not parse format of default sink. Falling back to stereo.");
destroy_capture_sink(pwac);
make_capture_sink(pwac, 2, "[FL,FR]");
}
static const struct pw_node_events default_sink_events = {
PW_VERSION_NODE_EVENTS,
.param = on_default_sink_param_cb,
};
static void on_default_sink_proxy_removed_cb(void *data)
{
struct obs_pw_audio_capture_app *pwac = data;
pw_proxy_destroy(pwac->default_sink.proxy);
}
static void on_default_sink_proxy_destroy_cb(void *data)
{
struct obs_pw_audio_capture_app *pwac = data;
spa_hook_remove(&pwac->default_sink.node_listener);
spa_zero(pwac->default_sink.node_listener);
spa_hook_remove(&pwac->default_sink.proxy_listener);
spa_zero(pwac->default_sink.proxy_listener);
pwac->default_sink.proxy = NULL;
}
static const struct pw_proxy_events default_sink_proxy_events = {
PW_VERSION_PROXY_EVENTS,
.removed = on_default_sink_proxy_removed_cb,
.destroy = on_default_sink_proxy_destroy_cb,
};
static void default_node_cb(void *data, const char *name)
{
struct obs_pw_audio_capture_app *pwac = data;
blog(LOG_DEBUG, "[pipewire-audio] New default sink %s", name);
/* Find the new default sink and bind to it to get its channel info */
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->system_sinks);
struct system_sink *temp, *default_sink = NULL;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&temp)) {
if (strcmp(name, temp->name) == 0) {
default_sink = temp;
break;
}
}
if (!default_sink) {
return;
}
if (pwac->default_sink.proxy) {
pw_proxy_destroy(pwac->default_sink.proxy);
}
pwac->default_sink.proxy =
pw_registry_bind(pwac->pw.registry, default_sink->id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
if (!pwac->default_sink.proxy) {
if (!pwac->sink.proxy) {
blog(LOG_WARNING,
"[pipewire-audio] Failed to get default sink info, app capture sink defaulting to stereo");
make_capture_sink(pwac, 2, "FL,FR");
}
return;
}
pw_proxy_add_object_listener(pwac->default_sink.proxy, &pwac->default_sink.node_listener, &default_sink_events,
pwac);
pw_proxy_add_listener(pwac->default_sink.proxy, &pwac->default_sink.proxy_listener, &default_sink_proxy_events,
pwac);
pw_node_subscribe_params((struct pw_node *)pwac->default_sink.proxy, (uint32_t[]){SPA_PARAM_EnumFormat}, 1);
}
/* ------------------------------------------------- */
/* Registry */
static void on_global_cb(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version,
const struct spa_dict *props)
{
UNUSED_PARAMETER(permissions);
UNUSED_PARAMETER(version);
if (!props || !type) {
return;
}
struct obs_pw_audio_capture_app *pwac = data;
if (id == pwac->sink.id) {
const char *ser = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL);
if (!ser) {
blog(LOG_ERROR, "[pipewire-audio] No object serial found on app capture sink %u", id);
pwac->sink.serial = 0;
} else {
pwac->sink.serial = strtoul(ser, NULL, 10);
finalize_capture_sink(pwac);
}
}
if (strcmp(type, PW_TYPE_INTERFACE_Port) == 0) {
const char *nid, *dir, *chn;
if (!(nid = spa_dict_lookup(props, PW_KEY_NODE_ID)) ||
!(dir = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION)) ||
!(chn = spa_dict_lookup(props, PW_KEY_AUDIO_CHANNEL))) {
return;
}
uint32_t node_id = strtoul(nid, NULL, 10);
if (astrcmpi(dir, "in") == 0 && node_id == pwac->sink.id) {
register_capture_sink_port(pwac, id, chn);
} else if (astrcmpi(dir, "out") == 0) {
/* Possibly a target port */
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->nodes);
struct target_node *temp, *node = NULL;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&temp)) {
if (temp->id == node_id) {
node = temp;
break;
}
}
if (!node) {
return;
}
struct target_node_port *port = node_register_port(node, id, pwac->pw.registry, chn);
if (port && pwac->sink.autoconnect_targets && node_is_targeted(pwac, node)) {
link_port_to_sink(pwac, port, node->id);
}
}
} else if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
const char *node_name, *media_class;
if (!(node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) ||
!(media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS))) {
return;
}
if (strcmp(media_class, "Stream/Output/Audio") == 0) {
/* Target node */
const char *node_app_name = spa_dict_lookup(props, PW_KEY_APP_NAME);
if (!node_app_name) {
node_app_name = node_name;
}
uint32_t client_id = 0;
const char *client_id_str = spa_dict_lookup(props, PW_KEY_CLIENT_ID);
if (client_id_str) {
client_id = strtoul(client_id_str, NULL, 10);
}
register_target_node(pwac, id, client_id, node_app_name, node_name);
} else if (strcmp(media_class, "Audio/Sink") == 0) {
register_system_sink(pwac, id, node_name);
}
} else if (strcmp(type, PW_TYPE_INTERFACE_Client) == 0) {
const char *client_app_name = spa_dict_lookup(props, PW_KEY_APP_NAME);
register_target_client(pwac, id, client_app_name);
} else if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) {
const char *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME);
if (!name || strcmp(name, "default") != 0) {
return;
}
if (!obs_pw_audio_default_node_metadata_listen(&pwac->default_sink.metadata, &pwac->pw, id, true,
default_node_cb, pwac) &&
!pwac->sink.proxy) {
blog(LOG_WARNING,
"[pipewire-audio] Failed to get default metadata, app capture sink defaulting to stereo");
make_capture_sink(pwac, 2, "FL,FR");
}
}
}
static const struct pw_registry_events registry_events = {
PW_VERSION_REGISTRY_EVENTS,
.global = on_global_cb,
};
/* ------------------------------------------------- */
/* Source */
static bool add_app_clicked(obs_properties_t *properties, obs_property_t *property, void *data)
{
UNUSED_PARAMETER(properties);
UNUSED_PARAMETER(property);
obs_source_t *source = data;
obs_data_t *settings = obs_source_get_settings(source);
const char *app_to_add = obs_data_get_string(settings, SETTING_AVAILABLE_APPS);
obs_data_array_t *selections = obs_data_get_array(settings, SETTING_SELECTION_MULTIPLE);
if (obs_data_array_count(selections) == 0) {
obs_data_array_release(selections);
selections = obs_data_array_create();
obs_data_set_array(settings, SETTING_SELECTION_MULTIPLE, selections);
}
/* Don't add if selection is already in the list */
bool should_add = true;
for (size_t i = 0; i < obs_data_array_count(selections) && should_add; i++) {
obs_data_t *item = obs_data_array_item(selections, i);
should_add = astrcmpi(obs_data_get_string(item, "value"), app_to_add) != 0;
obs_data_release(item);
}
if (should_add) {
obs_data_t *new_entry = obs_data_create();
obs_data_set_bool(new_entry, "hidden", false);
obs_data_set_bool(new_entry, "selected", false);
obs_data_set_string(new_entry, "value", app_to_add);
obs_data_array_push_back(selections, new_entry);
obs_data_release(new_entry);
obs_source_update(source, settings);
}
obs_data_array_release(selections);
obs_data_release(settings);
return should_add;
}
static int cmp_targets(const void *a, const void *b)
{
const char *a_str = *(char **)a;
const char *b_str = *(char **)b;
return strcmp(a_str, b_str);
}
static const char *choose_display_string(struct obs_pw_audio_capture_app *pwac, const char *binary,
const char *app_name)
{
switch (pwac->match_priority) {
case MATCH_PRIORITY_BINARY_NAME:
return binary ? binary : app_name;
case MATCH_PRIORITY_APP_NAME:
return app_name ? app_name : binary;
default:
return NULL;
}
}
static void populate_avaiable_apps_list(obs_property_t *list, struct obs_pw_audio_capture_app *pwac)
{
DARRAY(const char *) targets;
da_init(targets);
pw_thread_loop_lock(pwac->pw.thread_loop);
da_reserve(targets, pwac->n_nodes);
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->nodes);
struct target_node *node;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {
const char *display = choose_display_string(pwac, node->binary, node->app_name);
if (!display) {
display = node->name;
}
da_push_back(targets, &display);
}
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->clients);
struct target_client *client;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&client)) {
const char *display = choose_display_string(pwac, client->binary, client->app_name);
if (display) {
da_push_back(targets, &display);
}
}
/* Show just one entry per target */
qsort(targets.array, targets.num, sizeof(const char *), cmp_targets);
for (size_t i = 0; i < targets.num; i++) {
if (i == 0 || strcmp(targets.array[i - 1], targets.array[i]) != 0) {
obs_property_list_add_string(list, targets.array[i], targets.array[i]);
}
}
pw_thread_loop_unlock(pwac->pw.thread_loop);
da_free(targets);
}
static bool capture_mode_modified(void *data, obs_properties_t *properties, obs_property_t *property,
obs_data_t *settings)
{
UNUSED_PARAMETER(property);
struct obs_pw_audio_capture_app *pwac = data;
enum capture_mode mode = obs_data_get_int(settings, SETTING_CAPTURE_MODE);
switch (mode) {
case CAPTURE_MODE_SINGLE: {
obs_properties_remove_by_name(properties, SETTING_SELECTION_MULTIPLE);
obs_properties_remove_by_name(properties, SETTING_AVAILABLE_APPS);
obs_properties_remove_by_name(properties, SETTING_ADD_TO_SELECTIONS);
obs_property_t *available_apps =
obs_properties_add_list(properties, SETTING_SELECTION_SINGLE, obs_module_text("Application"),
OBS_COMBO_TYPE_EDITABLE, OBS_COMBO_FORMAT_STRING);
populate_avaiable_apps_list(available_apps, pwac);
break;
}
case CAPTURE_MODE_MULTIPLE: {
obs_properties_remove_by_name(properties, SETTING_SELECTION_SINGLE);
obs_properties_add_editable_list(properties, SETTING_SELECTION_MULTIPLE,
obs_module_text("SelectedApps"), OBS_EDITABLE_LIST_TYPE_STRINGS, NULL,
NULL);
obs_property_t *available_apps = obs_properties_add_list(properties, SETTING_AVAILABLE_APPS,
obs_module_text("Applications"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
populate_avaiable_apps_list(available_apps, pwac);
obs_properties_add_button2(properties, SETTING_ADD_TO_SELECTIONS, obs_module_text("AddToSelected"),
add_app_clicked, pwac->source);
break;
}
}
return true;
}
static bool match_priority_modified(void *data, obs_properties_t *properties, obs_property_t *property,
obs_data_t *settings)
{
UNUSED_PARAMETER(property);
struct obs_pw_audio_capture_app *pwac = data;
enum capture_mode mode = obs_data_get_int(settings, SETTING_CAPTURE_MODE);
obs_property_t *targets = NULL;
switch (mode) {
default:
case CAPTURE_MODE_SINGLE:
targets = obs_properties_get(properties, SETTING_SELECTION_SINGLE);
break;
case CAPTURE_MODE_MULTIPLE:
targets = obs_properties_get(properties, SETTING_AVAILABLE_APPS);
break;
}
if (targets == NULL) {
return false;
}
obs_property_list_clear(targets);
populate_avaiable_apps_list(targets, pwac);
return true;
}
static void build_selections(struct obs_pw_audio_capture_app *pwac, obs_data_t *settings)
{
switch (pwac->capture_mode) {
case CAPTURE_MODE_SINGLE: {
const char *selection = bstrdup(obs_data_get_string(settings, SETTING_SELECTION_SINGLE));
da_push_back(pwac->selections, &selection);
break;
}
case CAPTURE_MODE_MULTIPLE: {
obs_data_array_t *selections = obs_data_get_array(settings, SETTING_SELECTION_MULTIPLE);
for (size_t i = 0; i < obs_data_array_count(selections); i++) {
obs_data_t *item = obs_data_array_item(selections, i);
const char *selection = bstrdup(obs_data_get_string(item, "value"));
da_push_back(pwac->selections, &selection);
obs_data_release(item);
}
obs_data_array_release(selections);
break;
}
}
}
static void clear_selections(struct obs_pw_audio_capture_app *pwac)
{
for (size_t i = 0; i < pwac->selections.num; i++) {
const char *selection = pwac->selections.array[i];
bfree((void *)selection);
}
pwac->selections.num = 0;
}
static void *pipewire_audio_capture_app_create(obs_data_t *settings, obs_source_t *source)
{
struct obs_pw_audio_capture_app *pwac = bzalloc(sizeof(struct obs_pw_audio_capture_app));
if (!obs_pw_audio_instance_init(&pwac->pw, ®istry_events, pwac, true, false, source)) {
obs_pw_audio_instance_destroy(&pwac->pw);
bfree(pwac);
return NULL;
}
pwac->source = source;
obs_pw_audio_proxy_list_init(&pwac->nodes, NULL, node_destroy_cb);
obs_pw_audio_proxy_list_init(&pwac->clients, NULL, client_destroy_cb);
obs_pw_audio_proxy_list_init(&pwac->sink.links, link_bound_cb, link_destroy_cb);
obs_pw_audio_proxy_list_init(&pwac->system_sinks, NULL, system_sink_destroy_cb);
pwac->sink.id = SPA_ID_INVALID;
dstr_init(&pwac->sink.position);
pwac->capture_mode = obs_data_get_int(settings, SETTING_CAPTURE_MODE);
pwac->match_priority = obs_data_get_int(settings, SETTING_MATCH_PRIORITY);
pwac->except = obs_data_get_bool(settings, SETTING_EXCLUDE_SELECTIONS);
da_init(pwac->selections);
build_selections(pwac, settings);
pw_thread_loop_unlock(pwac->pw.thread_loop);
return pwac;
}
static void pipewire_audio_capture_app_defaults(obs_data_t *settings)
{
obs_data_set_default_int(settings, SETTING_CAPTURE_MODE, CAPTURE_MODE_SINGLE);
obs_data_set_default_int(settings, SETTING_MATCH_PRIORITY, MATCH_PRIORITY_BINARY_NAME);
obs_data_set_default_bool(settings, SETTING_EXCLUDE_SELECTIONS, false);
obs_data_array_t *arr = obs_data_array_create();
obs_data_set_default_array(settings, SETTING_SELECTION_MULTIPLE, arr);
obs_data_array_release(arr);
}
static obs_properties_t *pipewire_audio_capture_app_properties(void *data)
{
struct obs_pw_audio_capture_app *pwac = data;
obs_properties_t *p = obs_properties_create();
obs_property_t *capture_mode = obs_properties_add_list(
p, SETTING_CAPTURE_MODE, obs_module_text("AppCaptureMode"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_list_add_int(capture_mode, obs_module_text("SingleApp"), CAPTURE_MODE_SINGLE);
obs_property_list_add_int(capture_mode, obs_module_text("MultipleApps"), CAPTURE_MODE_MULTIPLE);
obs_property_set_modified_callback2(capture_mode, capture_mode_modified, pwac);
obs_property_t *match_priority = obs_properties_add_list(
p, SETTING_MATCH_PRIORITY, obs_module_text("MatchPriority"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_list_add_int(match_priority, obs_module_text("MatchBinaryFirst"), MATCH_PRIORITY_BINARY_NAME);
obs_property_list_add_int(match_priority, obs_module_text("MatchAppNameFirst"), MATCH_PRIORITY_APP_NAME);
obs_property_set_modified_callback2(match_priority, match_priority_modified, pwac);
obs_properties_add_bool(p, SETTING_EXCLUDE_SELECTIONS, obs_module_text("ExceptApp"));
return p;
}
static void pipewire_audio_capture_app_update(void *data, obs_data_t *settings)
{
struct obs_pw_audio_capture_app *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
pwac->capture_mode = obs_data_get_int(settings, SETTING_CAPTURE_MODE);
pwac->match_priority = obs_data_get_int(settings, SETTING_MATCH_PRIORITY);
pwac->except = obs_data_get_bool(settings, SETTING_EXCLUDE_SELECTIONS);
clear_selections(pwac);
build_selections(pwac, settings);
connect_targets(pwac);
pw_thread_loop_unlock(pwac->pw.thread_loop);
}
static void pipewire_audio_capture_app_show(void *data)
{
struct obs_pw_audio_capture_app *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
pw_stream_set_active(pwac->pw.audio.stream, true);
pw_thread_loop_unlock(pwac->pw.thread_loop);
}
static void pipewire_audio_capture_app_hide(void *data)
{
struct obs_pw_audio_capture_app *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
pw_stream_set_active(pwac->pw.audio.stream, false);
pw_thread_loop_unlock(pwac->pw.thread_loop);
}
static void pipewire_audio_capture_app_destroy(void *data)
{
struct obs_pw_audio_capture_app *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
obs_pw_audio_proxy_list_clear(&pwac->nodes);
obs_pw_audio_proxy_list_clear(&pwac->system_sinks);
obs_pw_audio_proxy_list_clear(&pwac->clients);
destroy_capture_sink(pwac);
if (pwac->default_sink.proxy) {
pw_proxy_destroy(pwac->default_sink.proxy);
}
if (pwac->default_sink.metadata.proxy) {
pw_proxy_destroy(pwac->default_sink.metadata.proxy);
}
obs_pw_audio_instance_destroy(&pwac->pw);
dstr_free(&pwac->sink.position);
clear_selections(pwac);
da_free(pwac->selections);
bfree(pwac);
}
static const char *pipewire_audio_capture_app_name(void *data)
{
UNUSED_PARAMETER(data);
return obs_module_text("PipeWireAudioCaptureApplication");
}
void pipewire_audio_capture_app_load(void)
{
const struct obs_source_info pipewire_audio_capture_application = {
.id = "pipewire_audio_application_capture",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE,
.get_name = pipewire_audio_capture_app_name,
.create = pipewire_audio_capture_app_create,
.get_defaults = pipewire_audio_capture_app_defaults,
.get_properties = pipewire_audio_capture_app_properties,
.update = pipewire_audio_capture_app_update,
.show = pipewire_audio_capture_app_show,
.hide = pipewire_audio_capture_app_hide,
.destroy = pipewire_audio_capture_app_destroy,
.icon_type = OBS_ICON_TYPE_PROCESS_AUDIO_OUTPUT,
};
obs_register_source(&pipewire_audio_capture_application);
}
================================================
FILE: src/pipewire-audio-capture-device.c
================================================
/* pipewire-audio-capture-device.c
*
* Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "pipewire-audio.h"
#include <util/dstr.h>
/* Source for capturing device audio using PipeWire */
struct target_node {
const char *friendly_name;
const char *name;
uint32_t serial;
uint32_t id;
uint32_t channels;
struct spa_hook node_listener;
struct obs_pw_audio_capture_device *pwac;
};
enum capture_type {
CAPTURE_TYPE_INPUT,
CAPTURE_TYPE_OUTPUT,
};
#define SETTING_TARGET_SERIAL "TargetId"
#define SETTING_TARGET_NAME "TargetName"
struct obs_pw_audio_capture_device {
obs_source_t *source;
enum capture_type capture_type;
struct obs_pw_audio_instance pw;
struct {
struct obs_pw_audio_default_node_metadata metadata;
bool autoconnect;
uint32_t node_serial;
struct dstr name;
} default_info;
struct obs_pw_audio_proxy_list targets;
struct dstr target_name;
uint32_t connected_serial;
};
static void start_streaming(struct obs_pw_audio_capture_device *pwac, struct target_node *node)
{
dstr_copy(&pwac->target_name, node->name);
if (pw_stream_get_state(pwac->pw.audio.stream, NULL) != PW_STREAM_STATE_UNCONNECTED) {
if (node->serial == pwac->connected_serial) {
/* Already connected to this node */
return;
}
pw_stream_disconnect(pwac->pw.audio.stream);
pwac->connected_serial = SPA_ID_INVALID;
}
if (obs_pw_audio_stream_connect(&pwac->pw.audio, node->id, node->serial, node->channels) == 0) {
pwac->connected_serial = node->serial;
blog(LOG_INFO, "[pipewire-audio] %p streaming from %u", pwac->pw.audio.stream, node->serial);
} else {
pwac->connected_serial = SPA_ID_INVALID;
blog(LOG_WARNING, "[pipewire-audio] Error connecting stream %p", pwac->pw.audio.stream);
}
pw_stream_set_active(pwac->pw.audio.stream, obs_source_active(pwac->source));
}
struct target_node *get_node_by_name(struct obs_pw_audio_capture_device *pwac, const char *name)
{
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->targets);
struct target_node *node;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {
if (strcmp(node->name, name) == 0) {
return node;
}
}
return NULL;
}
struct target_node *get_node_by_serial(struct obs_pw_audio_capture_device *pwac, uint32_t serial)
{
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->targets);
struct target_node *node;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {
if (node->serial == serial) {
return node;
}
}
return NULL;
}
/* Target node */
static void on_node_param_cb(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,
const struct spa_pod *param)
{
UNUSED_PARAMETER(seq);
UNUSED_PARAMETER(index);
UNUSED_PARAMETER(next);
if (id != SPA_PARAM_EnumFormat) {
return;
}
struct target_node *n = data;
struct spa_pod_parser p;
spa_pod_parser_pod(&p, param);
uint32_t media_type = 0, media_subtype = 0, parsed_id = 0, channels = 0;
spa_pod_parser_get_object(&p, SPA_TYPE_OBJECT_Format, &parsed_id, SPA_FORMAT_mediaType, SPA_POD_Id(&media_type),
SPA_FORMAT_mediaSubtype, SPA_POD_Id(&media_subtype), SPA_FORMAT_AUDIO_channels,
SPA_POD_OPT_Int(&channels));
if (n->channels && !channels) {
// It's likely we got the channels from a proper format already
return;
}
if (media_type != SPA_MEDIA_TYPE_audio) {
blog(LOG_WARNING,
"[pipewire-audio] Could not parse target node format. Channels may be mapped incorrectly.");
}
n->channels = channels;
struct obs_pw_audio_capture_device *pwac = n->pwac;
bool not_streamed = pwac->connected_serial != n->serial;
bool has_default_node_name = !dstr_is_empty(&pwac->default_info.name) &&
dstr_cmp(&pwac->default_info.name, n->name) == 0;
bool is_new_default_node = not_streamed && has_default_node_name;
bool stream_is_unconnected = pw_stream_get_state(pwac->pw.audio.stream, NULL) == PW_STREAM_STATE_UNCONNECTED;
bool node_has_target_name = !dstr_is_empty(&pwac->target_name) && dstr_cmp(&pwac->target_name, n->name) == 0;
if ((pwac->default_info.autoconnect && is_new_default_node) ||
(stream_is_unconnected && node_has_target_name)) {
start_streaming(pwac, n);
}
}
static const struct pw_node_events node_events = {
PW_VERSION_NODE_EVENTS,
.param = on_node_param_cb,
};
static void node_destroy_cb(void *data)
{
struct target_node *n = data;
struct obs_pw_audio_capture_device *pwac = n->pwac;
if (n->serial == pwac->connected_serial) {
if (pw_stream_get_state(pwac->pw.audio.stream, NULL) != PW_STREAM_STATE_UNCONNECTED) {
pw_stream_disconnect(pwac->pw.audio.stream);
}
pwac->connected_serial = SPA_ID_INVALID;
}
spa_hook_remove(&n->node_listener);
bfree((void *)n->friendly_name);
bfree((void *)n->name);
}
static void register_target_node(struct obs_pw_audio_capture_device *pwac, const char *friendly_name, const char *name,
uint32_t object_serial, uint32_t global_id)
{
struct pw_proxy *node_proxy = pw_registry_bind(pwac->pw.registry, global_id, PW_TYPE_INTERFACE_Node,
PW_VERSION_NODE, sizeof(struct target_node));
if (!node_proxy) {
return;
}
struct target_node *n = pw_proxy_get_user_data(node_proxy);
n->friendly_name = bstrdup(friendly_name);
n->name = bstrdup(name);
n->id = global_id;
n->serial = object_serial;
n->channels = 0;
n->pwac = pwac;
obs_pw_audio_proxy_list_append(&pwac->targets, node_proxy);
spa_zero(n->node_listener);
pw_proxy_add_object_listener(node_proxy, &n->node_listener, &node_events, n);
pw_node_subscribe_params((struct pw_node *)node_proxy, (uint32_t[]){SPA_PARAM_EnumFormat}, 1);
}
/* ------------------------------------------------- */
/* Default device metadata */
static void default_node_cb(void *data, const char *name)
{
struct obs_pw_audio_capture_device *pwac = data;
blog(LOG_DEBUG, "[pipewire-audio] New default device %s", name);
dstr_copy(&pwac->default_info.name, name);
struct target_node *n = get_node_by_name(pwac, name);
if (n) {
pwac->default_info.node_serial = n->serial;
// Connect now or wait for the param ballback to connect this
if (pwac->default_info.autoconnect && n->channels) {
start_streaming(pwac, n);
}
}
}
/* ------------------------------------------------- */
/* Registry */
static void on_global_cb(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version,
const struct spa_dict *props)
{
UNUSED_PARAMETER(permissions);
UNUSED_PARAMETER(version);
struct obs_pw_audio_capture_device *pwac = data;
if (!props || !type) {
return;
}
if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
const char *node_name, *media_class;
if (!(node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) ||
!(media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS))) {
return;
}
/* Target device */
if ((pwac->capture_type == CAPTURE_TYPE_INPUT &&
(strcmp(media_class, "Audio/Source") == 0 || strcmp(media_class, "Audio/Source/Virtual") == 0)) ||
(pwac->capture_type == CAPTURE_TYPE_OUTPUT &&
(strcmp(media_class, "Audio/Sink") == 0 || strcmp(media_class, "Audio/Duplex") == 0))) {
const char *ser = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL);
if (!ser) {
blog(LOG_WARNING, "[pipewire-audio] No object serial found on node %u", id);
return;
}
uint32_t object_serial = strtoul(ser, NULL, 10);
const char *node_friendly_name = spa_dict_lookup(props, PW_KEY_NODE_NICK);
if (!node_friendly_name) {
node_friendly_name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
if (!node_friendly_name) {
node_friendly_name = node_name;
}
}
register_target_node(pwac, node_friendly_name, node_name, object_serial, id);
}
} else if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) {
const char *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME);
if (!name || strcmp(name, "default") != 0) {
return;
}
if (!obs_pw_audio_default_node_metadata_listen(&pwac->default_info.metadata, &pwac->pw, id,
pwac->capture_type == CAPTURE_TYPE_OUTPUT,
default_node_cb, pwac)) {
blog(LOG_WARNING,
"[pipewire-audio] Failed to get default metadata, cannot detect default audio devices");
}
}
}
static const struct pw_registry_events registry_events = {
PW_VERSION_REGISTRY_EVENTS,
.global = on_global_cb,
};
/* ------------------------------------------------- */
/* Source */
static void *pipewire_audio_capture_create(obs_data_t *settings, obs_source_t *source, enum capture_type capture_type)
{
struct obs_pw_audio_capture_device *pwac = bzalloc(sizeof(struct obs_pw_audio_capture_device));
if (!obs_pw_audio_instance_init(&pwac->pw, ®istry_events, pwac, capture_type == CAPTURE_TYPE_OUTPUT, true,
source)) {
obs_pw_audio_instance_destroy(&pwac->pw);
bfree(pwac);
return NULL;
}
pwac->source = source;
pwac->capture_type = capture_type;
pwac->default_info.node_serial = SPA_ID_INVALID;
pwac->connected_serial = SPA_ID_INVALID;
obs_pw_audio_proxy_list_init(&pwac->targets, NULL, node_destroy_cb);
if (obs_data_get_int(settings, SETTING_TARGET_SERIAL) != PW_ID_ANY) {
/** Reset id setting, PipeWire node ids may not persist between sessions.
* Connecting to saved target will happen based on the TargetName setting
* once target has connected */
obs_data_set_int(settings, SETTING_TARGET_SERIAL, 0);
} else {
pwac->default_info.autoconnect = true;
}
dstr_init_copy(&pwac->target_name, obs_data_get_string(settings, SETTING_TARGET_NAME));
pw_thread_loop_unlock(pwac->pw.thread_loop);
return pwac;
}
static void *pipewire_audio_capture_input_create(obs_data_t *settings, obs_source_t *source)
{
return pipewire_audio_capture_create(settings, source, CAPTURE_TYPE_INPUT);
}
static void *pipewire_audio_capture_output_create(obs_data_t *settings, obs_source_t *source)
{
return pipewire_audio_capture_create(settings, source, CAPTURE_TYPE_OUTPUT);
}
static void pipewire_audio_capture_defaults(obs_data_t *settings)
{
obs_data_set_default_int(settings, SETTING_TARGET_SERIAL, PW_ID_ANY);
}
static obs_properties_t *pipewire_audio_capture_properties(void *data)
{
struct obs_pw_audio_capture_device *pwac = data;
obs_properties_t *p = obs_properties_create();
obs_property_t *targets_list = obs_properties_add_list(p, SETTING_TARGET_SERIAL, obs_module_text("Device"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_list_add_int(targets_list, obs_module_text("Default"), PW_ID_ANY);
if (!pwac->default_info.autoconnect) {
obs_data_t *settings = obs_source_get_settings(pwac->source);
/* Saved target serial may be different from connected because a previously connected
node may have been replaced by one with the same name */
obs_data_set_int(settings, SETTING_TARGET_SERIAL, pwac->connected_serial);
obs_data_release(settings);
}
pw_thread_loop_lock(pwac->pw.thread_loop);
struct obs_pw_audio_proxy_list_iter iter;
obs_pw_audio_proxy_list_iter_init(&iter, &pwac->targets);
struct target_node *node;
while (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {
obs_property_list_add_int(targets_list, node->friendly_name, node->serial);
}
pw_thread_loop_unlock(pwac->pw.thread_loop);
return p;
}
static void pipewire_audio_capture_update(void *data, obs_data_t *settings)
{
struct obs_pw_audio_capture_device *pwac = data;
uint32_t new_node_serial = obs_data_get_int(settings, SETTING_TARGET_SERIAL);
pw_thread_loop_lock(pwac->pw.thread_loop);
if ((pwac->default_info.autoconnect = new_node_serial == PW_ID_ANY)) {
if (pwac->default_info.node_serial != SPA_ID_INVALID) {
start_streaming(pwac, get_node_by_serial(pwac, pwac->default_info.node_serial));
}
} else {
struct target_node *new_node = get_node_by_serial(pwac, new_node_serial);
if (new_node) {
start_streaming(pwac, new_node);
obs_data_set_string(settings, SETTING_TARGET_NAME, pwac->target_name.array);
}
}
pw_thread_loop_unlock(pwac->pw.thread_loop);
}
static void pipewire_audio_capture_show(void *data)
{
struct obs_pw_audio_capture_device *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
pw_stream_set_active(pwac->pw.audio.stream, true);
pw_thread_loop_unlock(pwac->pw.thread_loop);
}
static void pipewire_audio_capture_hide(void *data)
{
struct obs_pw_audio_capture_device *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
pw_stream_set_active(pwac->pw.audio.stream, false);
pw_thread_loop_unlock(pwac->pw.thread_loop);
}
static void pipewire_audio_capture_destroy(void *data)
{
struct obs_pw_audio_capture_device *pwac = data;
pw_thread_loop_lock(pwac->pw.thread_loop);
obs_pw_audio_proxy_list_clear(&pwac->targets);
if (pwac->default_info.metadata.proxy) {
pw_proxy_destroy(pwac->default_info.metadata.proxy);
}
obs_pw_audio_instance_destroy(&pwac->pw);
dstr_free(&pwac->default_info.name);
dstr_free(&pwac->target_name);
bfree(pwac);
}
static const char *pipewire_audio_capture_input_name(void *data)
{
UNUSED_PARAMETER(data);
return obs_module_text("PipeWireAudioCaptureInput");
}
static const char *pipewire_audio_capture_output_name(void *data)
{
UNUSED_PARAMETER(data);
return obs_module_text("PipeWireAudioCaptureOutput");
}
void pipewire_audio_capture_load(void)
{
const struct obs_source_info pipewire_audio_capture_input = {
.id = "pipewire_audio_input_capture",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE,
.get_name = pipewire_audio_capture_input_name,
.create = pipewire_audio_capture_input_create,
.get_defaults = pipewire_audio_capture_defaults,
.get_properties = pipewire_audio_capture_properties,
.update = pipewire_audio_capture_update,
.show = pipewire_audio_capture_show,
.hide = pipewire_audio_capture_hide,
.destroy = pipewire_audio_capture_destroy,
.icon_type = OBS_ICON_TYPE_AUDIO_INPUT,
};
const struct obs_source_info pipewire_audio_capture_output = {
.id = "pipewire_audio_output_capture",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_DO_NOT_SELF_MONITOR,
.get_name = pipewire_audio_capture_output_name,
.create = pipewire_audio_capture_output_create,
.get_defaults = pipewire_audio_capture_defaults,
.get_properties = pipewire_audio_capture_properties,
.update = pipewire_audio_capture_update,
.show = pipewire_audio_capture_show,
.hide = pipewire_audio_capture_hide,
.destroy = pipewire_audio_capture_destroy,
.icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT,
};
obs_register_source(&pipewire_audio_capture_input);
obs_register_source(&pipewire_audio_capture_output);
}
================================================
FILE: src/pipewire-audio.c
================================================
/* pipewire-audio.c
*
* Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "pipewire-audio.h"
#include <util/platform.h>
#include <spa/utils/json.h>
/* Utilities */
bool json_object_find(const char *obj, const char *key, char *value, size_t len)
{
/* From PipeWire's source */
struct spa_json it[2];
const char *v;
char k[128];
spa_json_init(&it[0], obj, strlen(obj));
if (spa_json_enter_object(&it[0], &it[1]) <= 0) {
return false;
}
while (spa_json_get_string(&it[1], k, sizeof(k)) > 0) {
if (spa_streq(k, key)) {
if (spa_json_get_string(&it[1], value, len) > 0) {
return true;
}
} else if (spa_json_next(&it[1], &v) <= 0) {
break;
}
}
return false;
}
/* ------------------------------------------------- */
/* PipeWire stream wrapper */
void obs_channels_to_spa_audio_position(enum spa_audio_channel *position, uint32_t channels)
{
switch (channels) {
case 1:
position[0] = SPA_AUDIO_CHANNEL_MONO;
break;
case 2:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
break;
case 3:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_LFE;
break;
case 4:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_FC;
position[3] = SPA_AUDIO_CHANNEL_RC;
break;
case 5:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_FC;
position[3] = SPA_AUDIO_CHANNEL_LFE;
position[4] = SPA_AUDIO_CHANNEL_RC;
break;
case 6:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_FC;
position[3] = SPA_AUDIO_CHANNEL_LFE;
position[4] = SPA_AUDIO_CHANNEL_RL;
position[5] = SPA_AUDIO_CHANNEL_RR;
break;
case 8:
position[0] = SPA_AUDIO_CHANNEL_FL;
position[1] = SPA_AUDIO_CHANNEL_FR;
position[2] = SPA_AUDIO_CHANNEL_FC;
position[3] = SPA_AUDIO_CHANNEL_LFE;
position[4] = SPA_AUDIO_CHANNEL_RL;
position[5] = SPA_AUDIO_CHANNEL_RR;
position[6] = SPA_AUDIO_CHANNEL_SL;
position[7] = SPA_AUDIO_CHANNEL_SR;
break;
default:
for (size_t i = 0; i < channels; i++) {
position[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
}
break;
}
}
enum audio_format spa_to_obs_audio_format(enum spa_audio_format format)
{
switch (format) {
case SPA_AUDIO_FORMAT_U8:
return AUDIO_FORMAT_U8BIT;
case SPA_AUDIO_FORMAT_S16_LE:
return AUDIO_FORMAT_16BIT;
case SPA_AUDIO_FORMAT_S32_LE:
return AUDIO_FORMAT_32BIT;
case SPA_AUDIO_FORMAT_F32_LE:
return AUDIO_FORMAT_FLOAT;
case SPA_AUDIO_FORMAT_U8P:
return AUDIO_FORMAT_U8BIT_PLANAR;
case SPA_AUDIO_FORMAT_S16P:
return AUDIO_FORMAT_16BIT_PLANAR;
case SPA_AUDIO_FORMAT_S32P:
return AUDIO_FORMAT_32BIT_PLANAR;
case SPA_AUDIO_FORMAT_F32P:
return AUDIO_FORMAT_FLOAT_PLANAR;
default:
return AUDIO_FORMAT_UNKNOWN;
}
}
enum speaker_layout spa_to_obs_speakers(uint32_t channels)
{
switch (channels) {
case 1:
return SPEAKERS_MONO;
case 2:
return SPEAKERS_STEREO;
case 3:
return SPEAKERS_2POINT1;
case 4:
return SPEAKERS_4POINT0;
case 5:
return SPEAKERS_4POINT1;
case 6:
return SPEAKERS_5POINT1;
case 8:
return SPEAKERS_7POINT1;
default:
return SPEAKERS_UNKNOWN;
}
}
bool spa_to_obs_pw_audio_info(struct obs_pw_audio_info *info, const struct spa_pod *param)
{
struct spa_audio_info_raw audio_info;
if (spa_format_audio_raw_parse(param, &audio_info) < 0) {
info->sample_rate = 0;
info->format = AUDIO_FORMAT_UNKNOWN;
info->speakers = SPEAKERS_UNKNOWN;
return false;
}
info->sample_rate = audio_info.rate;
info->speakers = spa_to_obs_speakers(audio_info.channels);
info->format = spa_to_obs_audio_format(audio_info.format);
return true;
}
static void on_process_cb(void *data)
{
uint64_t now = os_gettime_ns();
struct obs_pw_audio_stream *s = data;
struct pw_buffer *b = pw_stream_dequeue_buffer(s->stream);
if (!b) {
return;
}
struct spa_buffer *buf = b->buffer;
if (!s->info.sample_rate || buf->n_datas == 0 || buf->datas[0].chunk->stride == 0 ||
buf->datas[0].type != SPA_DATA_MemPtr) {
goto queue;
}
struct obs_source_audio out = {
.frames = buf->datas[0].chunk->size / buf->datas[0].chunk->stride,
.speakers = s->info.speakers,
.format = s->info.format,
.samples_per_sec = s->info.sample_rate,
};
for (size_t i = 0; i < buf->n_datas && i < MAX_AV_PLANES; i++) {
out.data[i] = buf->datas[i].data;
}
if (s->info.sample_rate && s->pos->clock.rate_diff) {
/** Taken from PipeWire's implementation of JACK's jack_get_cycle_times
* (https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/0.3.52/pipewire-jack/src/pipewire-jack.c#L5639)
* which is used in the linux-jack plugin to correctly set the timestamp
* (https://github.com/obsproject/obs-studio/blob/27.2.4/plugins/linux-jack/jack-wrapper.c#L87) */
double period_nsecs = s->pos->clock.duration * (double)SPA_NSEC_PER_SEC /
(s->info.sample_rate * s->pos->clock.rate_diff);
out.timestamp = now - (uint64_t)period_nsecs;
} else {
out.timestamp = now - audio_frames_to_ns(s->info.sample_rate, out.frames);
}
obs_source_output_audio(s->output, &out);
queue:
pw_stream_queue_buffer(s->stream, b);
}
static void on_state_changed_cb(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error)
{
UNUSED_PARAMETER(old);
struct obs_pw_audio_stream *s = data;
blog(LOG_DEBUG, "[pipewire-audio] Stream %p state: \"%s\" (error: %s)", s->stream,
pw_stream_state_as_string(state), error ? error : "none");
}
static void on_param_changed_cb(void *data, uint32_t id, const struct spa_pod *param)
{
if (!param || id != SPA_PARAM_Format) {
return;
}
struct obs_pw_audio_stream *s = data;
if (!spa_to_obs_pw_audio_info(&s->info, param)) {
blog(LOG_WARNING, "[pipewire-audio] Stream %p failed to parse audio format info", s->stream);
} else {
blog(LOG_INFO, "[pipewire-audio] %p Got format: rate %u - channels %u - format %u", s->stream,
s->info.sample_rate, s->info.speakers, s->info.format);
}
}
static void on_io_changed_cb(void *data, uint32_t id, void *area, uint32_t size)
{
UNUSED_PARAMETER(size);
struct obs_pw_audio_stream *s = data;
if (id == SPA_IO_Position) {
s->pos = area;
}
}
static const struct pw_stream_events stream_events = {
PW_VERSION_STREAM_EVENTS,
.process = on_process_cb,
.state_changed = on_state_changed_cb,
.param_changed = on_param_changed_cb,
.io_changed = on_io_changed_cb,
};
int obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s, uint32_t target_id, uint32_t target_serial,
uint32_t audio_channels)
{
if (audio_channels == 0) {
blog(LOG_WARNING,
"[pipewire-audio] Stream %p connecting without channel info. Channels may be mapped incorrectly.",
s);
}
if (audio_channels > 8) {
blog(LOG_WARNING,
"[pipewire-audio] Stream %p cannot use %u > 8 channels. This is likely a Pro Audio node, will use stereo instead.",
s, audio_channels);
audio_channels = 2;
}
enum spa_audio_channel pos[8];
uint8_t buffer[2048];
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const struct spa_pod *params[1];
if (audio_channels) {
obs_channels_to_spa_audio_position(pos, audio_channels);
params[0] = spa_pod_builder_add_object(
&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType,
SPA_POD_Id(SPA_MEDIA_TYPE_audio), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
SPA_FORMAT_AUDIO_channels, SPA_POD_Int(audio_channels), SPA_FORMAT_AUDIO_position,
SPA_POD_Array(sizeof(enum spa_audio_channel), SPA_TYPE_Id, audio_channels, pos),
SPA_FORMAT_AUDIO_format,
SPA_POD_CHOICE_ENUM_Id(9, SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_S16_LE,
SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_F32_LE, SPA_AUDIO_FORMAT_U8P,
SPA_AUDIO_FORMAT_S16P, SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_F32P));
} else {
params[0] = spa_pod_builder_add_object(
&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType,
SPA_POD_Id(SPA_MEDIA_TYPE_audio), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
SPA_FORMAT_AUDIO_format,
SPA_POD_CHOICE_ENUM_Id(9, SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_S16_LE,
SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_F32_LE, SPA_AUDIO_FORMAT_U8P,
SPA_AUDIO_FORMAT_S16P, SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_F32P));
}
struct pw_properties *stream_props = pw_properties_new(NULL, NULL);
pw_properties_setf(stream_props, PW_KEY_TARGET_OBJECT, "%u", target_serial);
pw_stream_update_properties(s->stream, &stream_props->dict);
pw_properties_free(stream_props);
return pw_stream_connect(
s->stream, PW_DIRECTION_INPUT, target_id,
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_DONT_RECONNECT, params, 1);
}
/* ------------------------------------------------- */
/* Common PipeWire components */
static void on_core_done_cb(void *data, uint32_t id, int seq)
{
struct obs_pw_audio_instance *pw = data;
if (id == PW_ID_CORE && pw->seq == seq) {
pw_thread_loop_signal(pw->thread_loop, false);
}
}
static void on_core_error_cb(void *data, uint32_t id, int seq, int res, const char *message)
{
struct obs_pw_audio_instance *pw = data;
blog(LOG_ERROR, "[pipewire-audio] Error id:%u seq:%d res:%d :%s", id, seq, res, message);
pw_thread_loop_signal(pw->thread_loop, false);
}
static const struct pw_core_events core_events = {
PW_VERSION_CORE_EVENTS,
.done = on_core_done_cb,
.error = on_core_error_cb,
};
bool obs_pw_audio_instance_init(struct obs_pw_audio_instance *pw, const struct pw_registry_events *registry_events,
void *registry_cb_data, bool stream_capture_sink, bool stream_want_driver,
obs_source_t *stream_output)
{
pw->thread_loop = pw_thread_loop_new("PipeWire thread loop", NULL);
pw->context = pw_context_new(pw_thread_loop_get_loop(pw->thread_loop), NULL, 0);
pw_thread_loop_lock(pw->thread_loop);
if (pw_thread_loop_start(pw->thread_loop) < 0) {
blog(LOG_WARNING, "[pipewire-audio] Error starting threaded mainloop");
return false;
}
pw->core = pw_context_connect(pw->context, NULL, 0);
if (!pw->core) {
blog(LOG_WARNING, "[pipewire-audio] Error creating PipeWire core");
return false;
}
pw_core_add_listener(pw->core, &pw->core_listener, &core_events, pw);
pw->registry = pw_core_get_registry(pw->core, PW_VERSION_REGISTRY, 0);
if (!pw->registry) {
return false;
}
pw_registry_add_listener(pw->registry, &pw->registry_listener, registry_events, registry_cb_data);
struct pw_properties *stream_props =
pw_properties_new(PW_KEY_MEDIA_NAME, obs_source_get_name(stream_output), PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Production",
PW_KEY_NODE_WANT_DRIVER, stream_want_driver ? "true" : "false",
PW_KEY_STREAM_CAPTURE_SINK, stream_capture_sink ? "true" : "false", NULL);
pw_properties_setf(stream_props, PW_KEY_NODE_NAME, "OBS: %s", obs_source_get_name(stream_output));
pw->audio.output = stream_output;
pw->audio.stream = pw_stream_new(pw->core, obs_source_get_name(stream_output), stream_props);
if (!pw->audio.stream) {
blog(LOG_WARNING, "[pipewire-audio] Failed to create stream");
return false;
}
blog(LOG_INFO, "[pipewire-audio] Created stream %p", pw->audio.stream);
pw_stream_add_listener(pw->audio.stream, &pw->audio.stream_listener, &stream_events, &pw->audio);
return true;
}
void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw)
{
if (pw->audio.stream) {
spa_hook_remove(&pw->audio.stream_listener);
if (pw_stream_get_state(pw->audio.stream, NULL) != PW_STREAM_STATE_UNCONNECTED) {
pw_stream_disconnect(pw->audio.stream);
}
pw_stream_destroy(pw->audio.stream);
}
if (pw->registry) {
spa_hook_remove(&pw->registry_listener);
spa_zero(pw->registry_listener);
pw_proxy_destroy((struct pw_proxy *)pw->registry);
}
pw_thread_loop_unlock(pw->thread_loop);
pw_thread_loop_stop(pw->thread_loop);
if (pw->core) {
spa_hook_remove(&pw->core_listener);
spa_zero(pw->core_listener);
pw_core_disconnect(pw->core);
}
if (pw->context) {
pw_context_destroy(pw->context);
}
pw_thread_loop_destroy(pw->thread_loop);
}
void obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw)
{
pw->seq = pw_core_sync(pw->core, PW_ID_CORE, pw->seq);
}
/* ------------------------------------------------- */
/* PipeWire metadata */
static int on_metadata_property_cb(void *data, uint32_t id, const char *key, const char *type, const char *value)
{
UNUSED_PARAMETER(type);
struct obs_pw_audio_default_node_metadata *metadata = data;
if (id == PW_ID_CORE && key && value &&
strcmp(key, metadata->wants_sink ? "default.audio.sink" : "default.audio.source") == 0) {
char val[128];
if (json_object_find(value, "name", val, sizeof(val)) && *val) {
metadata->default_node_callback(metadata->data, val);
}
}
return 0;
}
static const struct pw_metadata_events metadata_events = {
PW_VERSION_METADATA_EVENTS,
.property = on_metadata_property_cb,
};
static void on_metadata_proxy_removed_cb(void *data)
{
struct obs_pw_audio_default_node_metadata *metadata = data;
pw_proxy_destroy(metadata->proxy);
}
static void on_metadata_proxy_destroy_cb(void *data)
{
struct obs_pw_audio_default_node_metadata *metadata = data;
spa_hook_remove(&metadata->metadata_listener);
spa_hook_remove(&metadata->proxy_listener);
spa_zero(metadata->metadata_listener);
spa_zero(metadata->proxy_listener);
metadata->proxy = NULL;
}
static const struct pw_proxy_events metadata_proxy_events = {
PW_VERSION_PROXY_EVENTS,
.removed = on_metadata_proxy_removed_cb,
.destroy = on_metadata_proxy_destroy_cb,
};
bool obs_pw_audio_default_node_metadata_listen(struct obs_pw_audio_default_node_metadata *metadata,
struct obs_pw_audio_instance *pw, uint32_t global_id, bool wants_sink,
void (*default_node_callback)(void *data, const char *name), void *data)
{
if (metadata->proxy) {
pw_proxy_destroy(metadata->proxy);
}
struct pw_proxy *metadata_proxy =
pw_registry_bind(pw->registry, global_id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0);
if (!metadata_proxy) {
return false;
}
metadata->proxy = metadata_proxy;
metadata->wants_sink = wants_sink;
metadata->default_node_callback = default_node_callback;
metadata->data = data;
pw_proxy_add_object_listener(metadata->proxy, &metadata->metadata_listener, &metadata_events, metadata);
pw_proxy_add_listener(metadata->proxy, &metadata->proxy_listener, &metadata_proxy_events, metadata);
return true;
}
/* ------------------------------------------------- */
/* Proxied objects */
struct obs_pw_audio_proxied_object {
void (*bound_callback)(void *data, uint32_t global_id);
void (*destroy_callback)(void *data);
struct pw_proxy *proxy;
struct spa_hook proxy_listener;
struct spa_list link;
};
static void on_proxy_bound_cb(void *data, uint32_t global_id)
{
struct obs_pw_audio_proxied_object *obj = data;
if (obj->bound_callback) {
obj->bound_callback(pw_proxy_get_user_data(obj->proxy), global_id);
}
}
static void on_proxy_removed_cb(void *data)
{
struct obs_pw_audio_proxied_object *obj = data;
pw_proxy_destroy(obj->proxy);
}
static void on_proxy_destroy_cb(void *data)
{
struct obs_pw_audio_proxied_object *obj = data;
spa_hook_remove(&obj->proxy_listener);
spa_list_remove(&obj->link);
if (obj->destroy_callback) {
obj->destroy_callback(pw_proxy_get_user_data(obj->proxy));
}
bfree(data);
}
static const struct pw_proxy_events proxy_events = {
PW_VERSION_PROXY_EVENTS,
.bound = on_proxy_bound_cb,
.removed = on_proxy_removed_cb,
.destroy = on_proxy_destroy_cb,
};
void obs_pw_audio_proxied_object_new(struct pw_proxy *proxy, struct spa_list *list,
void (*bound_callback)(void *data, uint32_t global_id),
void (*destroy_callback)(void *data))
{
struct obs_pw_audio_proxied_object *obj = bmalloc(sizeof(struct obs_pw_audio_proxied_object));
obj->proxy = proxy;
obj->bound_callback = bound_callback;
obj->destroy_callback = destroy_callback;
spa_list_append(list, &obj->link);
spa_zero(obj->proxy_listener);
pw_proxy_add_listener(obj->proxy, &obj->proxy_listener, &proxy_events, obj);
}
void *obs_pw_audio_proxied_object_get_user_data(struct obs_pw_audio_proxied_object *obj)
{
return pw_proxy_get_user_data(obj->proxy);
}
void obs_pw_audio_proxy_list_init(struct obs_pw_audio_proxy_list *list,
void (*bound_callback)(void *data, uint32_t global_id),
void (*destroy_callback)(void *data))
{
spa_list_init(&list->list);
list->bound_callback = bound_callback;
list->destroy_callback = destroy_callback;
}
void obs_pw_audio_proxy_list_append(struct obs_pw_audio_proxy_list *list, struct pw_proxy *proxy)
{
obs_pw_audio_proxied_object_new(proxy, &list->list, list->bound_callback, list->destroy_callback);
}
void obs_pw_audio_proxy_list_clear(struct obs_pw_audio_proxy_list *list)
{
struct obs_pw_audio_proxied_object *obj, *temp;
spa_list_for_each_safe(obj, temp, &list->list, link)
{
pw_proxy_destroy(obj->proxy);
}
}
void obs_pw_audio_proxy_list_iter_init(struct obs_pw_audio_proxy_list_iter *iter, struct obs_pw_audio_proxy_list *list)
{
iter->proxy_list = list;
iter->current = spa_list_first(&list->list, struct obs_pw_audio_proxied_object, link);
}
bool obs_pw_audio_proxy_list_iter_next(struct obs_pw_audio_proxy_list_iter *iter, void **proxy_user_data)
{
if (spa_list_is_empty(&iter->proxy_list->list)) {
return false;
}
if (spa_list_is_end(iter->current, &iter->proxy_list->list, link)) {
return false;
}
*proxy_user_data = obs_pw_audio_proxied_object_get_user_data(iter->current);
iter->current = spa_list_next(iter->current, link);
return true;
}
/* ------------------------------------------------- */
================================================
FILE: src/pipewire-audio.h
================================================
/* pipewire-audio.h
*
* Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
/* Stuff used by the PipeWire audio capture sources */
#pragma once
#include <obs-module.h>
#include <pipewire/pipewire.h>
#include <pipewire/extensions/metadata.h>
#include <spa/param/audio/format-utils.h>
/* PipeWire Stream wrapper */
/**
* Audio metadata
*/
struct obs_pw_audio_info {
uint32_t sample_rate;
enum audio_format format;
enum speaker_layout speakers;
};
/**
* PipeWire stream wrapper that outputs to an OBS source
*/
struct obs_pw_audio_stream {
struct pw_stream *stream;
struct spa_hook stream_listener;
struct obs_pw_audio_info info;
struct spa_io_position *pos;
obs_source_t *output;
};
/**
* Connect a stream with the default params
* @return 0 on success, < 0 on error
*/
int obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s, uint32_t target_id, uint32_t target_serial,
uint32_t channels);
/* ------------------------------------------------- */
/**
* Common PipeWire components
*/
struct obs_pw_audio_instance {
struct pw_thread_loop *thread_loop;
struct pw_context *context;
struct pw_core *core;
struct spa_hook core_listener;
int seq;
struct pw_registry *registry;
struct spa_hook registry_listener;
struct obs_pw_audio_stream audio;
};
/**
* Initialize a PipeWire instance
* @warning The thread loop is left locked
* @return true on success, false on error
*/
bool obs_pw_audio_instance_init(struct obs_pw_audio_instance *pw, const struct pw_registry_events *registry_events,
void *registry_cb_data, bool stream_capture_sink, bool stream_want_driver,
obs_source_t *stream_output);
/**
* Destroy a PipeWire instance
* @warning Call with the thread loop locked
*/
void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw);
/**
* Trigger a PipeWire core sync
*/
void obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw);
/* ------------------------------------------------- */
/**
* PipeWire metadata
*/
struct obs_pw_audio_default_node_metadata {
struct pw_proxy *proxy;
struct spa_hook proxy_listener;
struct spa_hook metadata_listener;
bool wants_sink;
void (*default_node_callback)(void *data, const char *name);
void *data;
};
/**
* Add listeners to the metadata
* @return true on success, false on error
*/
bool obs_pw_audio_default_node_metadata_listen(struct obs_pw_audio_default_node_metadata *metadata,
struct obs_pw_audio_instance *pw, uint32_t global_id, bool wants_sink,
void (*default_node_callback)(void *data, const char *name), void *data);
/* ------------------------------------------------- */
/* Helpers for storing remote PipeWire objects */
/**
* Wrapper over a PipeWire proxy that's a member of a spa_list.
* Automatically handles adding and removing itself from the list.
*/
struct obs_pw_audio_proxied_object;
/**
* Get the user data of a proxied object
*/
void *obs_pw_audio_proxied_object_get_user_data(struct obs_pw_audio_proxied_object *obj);
/**
* Convenience wrapper over spa_lists that holds proxied objects
*/
struct obs_pw_audio_proxy_list {
struct spa_list list;
void (*bound_callback)(void *data, uint32_t global_id);
void (*destroy_callback)(void *data);
};
void obs_pw_audio_proxy_list_init(struct obs_pw_audio_proxy_list *list,
void (*bound_callback)(void *data, uint32_t global_id),
void (*destroy_callback)(void *data));
void obs_pw_audio_proxy_list_append(struct obs_pw_audio_proxy_list *list, struct pw_proxy *proxy);
/**
* Destroy all stored proxies.
*/
void obs_pw_audio_proxy_list_clear(struct obs_pw_audio_proxy_list *list);
/**
* Iterator over all user data of the proxies in the list
*/
struct obs_pw_audio_proxy_list_iter {
struct obs_pw_audio_proxy_list *proxy_list;
struct obs_pw_audio_proxied_object *current;
};
void obs_pw_audio_proxy_list_iter_init(struct obs_pw_audio_proxy_list_iter *iter, struct obs_pw_audio_proxy_list *list);
/**
* @return true when there are more items to process, false otherwise
*/
bool obs_pw_audio_proxy_list_iter_next(struct obs_pw_audio_proxy_list_iter *iter, void **proxy_user_data);
/* ------------------------------------------------- */
/* Sources */
void pipewire_audio_capture_load(void);
void pipewire_audio_capture_app_load(void);
/* ------------------------------------------------- */
gitextract_tn9d_h5o/
├── .clang-format
├── .editorconfig
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── CMakeLists.txt
├── LICENSE
├── README.md
├── cmake/
│ └── FindPipeWire.cmake
├── data/
│ └── locale/
│ ├── ar-SA.ini
│ ├── de-DE.ini
│ ├── en-US.ini
│ ├── es-ES.ini
│ ├── fr-FR.ini
│ ├── id-ID.ini
│ ├── pl-PL.ini
│ └── pt-BR.ini
└── src/
├── linux-pipewire-audio.c
├── pipewire-audio-capture-app.c
├── pipewire-audio-capture-device.c
├── pipewire-audio.c
└── pipewire-audio.h
SYMBOL INDEX (155 symbols across 5 files)
FILE: src/linux-pipewire-audio.c
function obs_module_load (line 34) | bool obs_module_load(void)
function obs_module_unload (line 43) | void obs_module_unload(void)
FILE: src/pipewire-audio-capture-app.c
type target_node_port (line 29) | struct target_node_port {
type target_node (line 34) | struct target_node {
type target_client (line 46) | struct target_client {
type system_sink (line 54) | struct system_sink {
type capture_sink_link (line 59) | struct capture_sink_link {
type capture_sink_port (line 63) | struct capture_sink_port {
type capture_mode (line 68) | enum capture_mode { CAPTURE_MODE_SINGLE, CAPTURE_MODE_MULTIPLE }
type match_priority (line 69) | enum match_priority { MATCH_PRIORITY_BINARY_NAME, MATCH_PRIORITY_APP_NAME }
type obs_pw_audio_capture_app (line 87) | struct obs_pw_audio_capture_app {
function system_sink_destroy_cb (line 130) | static void system_sink_destroy_cb(void *data)
function register_system_sink (line 136) | static void register_system_sink(struct obs_pw_audio_capture_app *pwac, ...
function client_destroy_cb (line 153) | static void client_destroy_cb(void *data)
function on_client_info_cb (line 162) | static void on_client_info_cb(void *data, const struct pw_client_info *i...
type pw_client_events (line 178) | struct pw_client_events
function register_target_client (line 183) | static void register_target_client(struct obs_pw_audio_capture_app *pwac...
function port_destroy_cb (line 201) | static void port_destroy_cb(void *data)
function node_destroy_cb (line 207) | static void node_destroy_cb(void *data)
type target_node_port (line 222) | struct target_node_port
type target_node (line 222) | struct target_node
type pw_registry (line 223) | struct pw_registry
type pw_proxy (line 225) | struct pw_proxy
type target_node_port (line 226) | struct target_node_port
type target_node_port (line 231) | struct target_node_port
function on_node_info_cb (line 240) | static void on_node_info_cb(void *data, const struct pw_node_info *info)
type pw_node_events (line 256) | struct pw_node_events
function register_target_node (line 261) | static void register_target_node(struct obs_pw_audio_capture_app *pwac, ...
function node_is_targeted (line 285) | static bool node_is_targeted(struct obs_pw_audio_capture_app *pwac, stru...
function link_bound_cb (line 314) | static void link_bound_cb(void *data, uint32_t global_id)
function link_destroy_cb (line 320) | static void link_destroy_cb(void *data)
function link_port_to_sink (line 326) | static void link_port_to_sink(struct obs_pw_audio_capture_app *pwac, str...
function link_node_to_sink (line 376) | static void link_node_to_sink(struct obs_pw_audio_capture_app *pwac, str...
function destroy_sink_links (line 393) | static void destroy_sink_links(struct obs_pw_audio_capture_app *pwac)
function connect_targets (line 398) | static void connect_targets(struct obs_pw_audio_capture_app *pwac)
function finalize_capture_sink (line 421) | static void finalize_capture_sink(struct obs_pw_audio_capture_app *pwac)
function on_sink_proxy_bound_cb (line 440) | static void on_sink_proxy_bound_cb(void *data, uint32_t global_id)
function on_sink_proxy_removed_cb (line 447) | static void on_sink_proxy_removed_cb(void *data)
function on_sink_proxy_destroy_cb (line 455) | static void on_sink_proxy_destroy_cb(void *data)
function on_sink_proxy_error_cb (line 479) | static void on_sink_proxy_error_cb(void *data, int seq, int res, const c...
type pw_proxy_events (line 485) | struct pw_proxy_events
function register_capture_sink_port (line 493) | static void register_capture_sink_port(struct obs_pw_audio_capture_app *...
function make_capture_sink (line 504) | static void make_capture_sink(struct obs_pw_audio_capture_app *pwac, uin...
function destroy_capture_sink (line 536) | static void destroy_capture_sink(struct obs_pw_audio_capture_app *pwac)
function on_default_sink_param_cb (line 554) | static void on_default_sink_param_cb(void *data, int seq, uint32_t id, u...
type pw_node_events (line 630) | struct pw_node_events
function on_default_sink_proxy_removed_cb (line 635) | static void on_default_sink_proxy_removed_cb(void *data)
function on_default_sink_proxy_destroy_cb (line 641) | static void on_default_sink_proxy_destroy_cb(void *data)
type pw_proxy_events (line 653) | struct pw_proxy_events
function default_node_cb (line 659) | static void default_node_cb(void *data, const char *name)
function on_global_cb (line 705) | static void on_global_cb(void *data, uint32_t id, uint32_t permissions, ...
type pw_registry_events (line 806) | struct pw_registry_events
function add_app_clicked (line 813) | static bool add_app_clicked(obs_properties_t *properties, obs_property_t...
function cmp_targets (line 861) | static int cmp_targets(const void *a, const void *b)
type obs_pw_audio_capture_app (line 868) | struct obs_pw_audio_capture_app
function populate_avaiable_apps_list (line 881) | static void populate_avaiable_apps_list(obs_property_t *list, struct obs...
function capture_mode_modified (line 930) | static bool capture_mode_modified(void *data, obs_properties_t *properti...
function match_priority_modified (line 976) | static bool match_priority_modified(void *data, obs_properties_t *proper...
function build_selections (line 1007) | static void build_selections(struct obs_pw_audio_capture_app *pwac, obs_...
function clear_selections (line 1031) | static void clear_selections(struct obs_pw_audio_capture_app *pwac)
type obs_pw_audio_capture_app (line 1043) | struct obs_pw_audio_capture_app
type obs_pw_audio_capture_app (line 1043) | struct obs_pw_audio_capture_app
function pipewire_audio_capture_app_defaults (line 1074) | static void pipewire_audio_capture_app_defaults(obs_data_t *settings)
function obs_properties_t (line 1085) | static obs_properties_t *pipewire_audio_capture_app_properties(void *data)
function pipewire_audio_capture_app_update (line 1108) | static void pipewire_audio_capture_app_update(void *data, obs_data_t *se...
function pipewire_audio_capture_app_show (line 1126) | static void pipewire_audio_capture_app_show(void *data)
function pipewire_audio_capture_app_hide (line 1135) | static void pipewire_audio_capture_app_hide(void *data)
function pipewire_audio_capture_app_destroy (line 1144) | static void pipewire_audio_capture_app_destroy(void *data)
function pipewire_audio_capture_app_load (line 1180) | void pipewire_audio_capture_app_load(void)
FILE: src/pipewire-audio-capture-device.c
type target_node (line 27) | struct target_node {
type capture_type (line 39) | enum capture_type {
type obs_pw_audio_capture_device (line 47) | struct obs_pw_audio_capture_device {
function start_streaming (line 67) | static void start_streaming(struct obs_pw_audio_capture_device *pwac, st...
type target_node (line 92) | struct target_node
type obs_pw_audio_capture_device (line 92) | struct obs_pw_audio_capture_device
type obs_pw_audio_proxy_list_iter (line 94) | struct obs_pw_audio_proxy_list_iter
type target_node (line 97) | struct target_node
type target_node (line 107) | struct target_node
type obs_pw_audio_capture_device (line 107) | struct obs_pw_audio_capture_device
type obs_pw_audio_proxy_list_iter (line 109) | struct obs_pw_audio_proxy_list_iter
type target_node (line 112) | struct target_node
function on_node_param_cb (line 123) | static void on_node_param_cb(void *data, int seq, uint32_t id, uint32_t ...
type pw_node_events (line 173) | struct pw_node_events
function node_destroy_cb (line 178) | static void node_destroy_cb(void *data)
function register_target_node (line 196) | static void register_target_node(struct obs_pw_audio_capture_device *pwa...
function default_node_cb (line 223) | static void default_node_cb(void *data, const char *name)
function on_global_cb (line 243) | static void on_global_cb(void *data, uint32_t id, uint32_t permissions, ...
type pw_registry_events (line 300) | struct pw_registry_events
type capture_type (line 307) | enum capture_type
type obs_pw_audio_capture_device (line 309) | struct obs_pw_audio_capture_device
type obs_pw_audio_capture_device (line 309) | struct obs_pw_audio_capture_device
function pipewire_audio_capture_defaults (line 352) | static void pipewire_audio_capture_defaults(obs_data_t *settings)
function obs_properties_t (line 357) | static obs_properties_t *pipewire_audio_capture_properties(void *data)
function pipewire_audio_capture_update (line 391) | static void pipewire_audio_capture_update(void *data, obs_data_t *settings)
function pipewire_audio_capture_show (line 415) | static void pipewire_audio_capture_show(void *data)
function pipewire_audio_capture_hide (line 424) | static void pipewire_audio_capture_hide(void *data)
function pipewire_audio_capture_destroy (line 432) | static void pipewire_audio_capture_destroy(void *data)
function pipewire_audio_capture_load (line 464) | void pipewire_audio_capture_load(void)
FILE: src/pipewire-audio.c
function json_object_find (line 28) | bool json_object_find(const char *obj, const char *key, char *value, siz...
function obs_channels_to_spa_audio_position (line 55) | void obs_channels_to_spa_audio_position(enum spa_audio_channel *position...
function spa_to_obs_audio_format (line 109) | enum audio_format spa_to_obs_audio_format(enum spa_audio_format format)
function spa_to_obs_speakers (line 133) | enum speaker_layout spa_to_obs_speakers(uint32_t channels)
function spa_to_obs_pw_audio_info (line 155) | bool spa_to_obs_pw_audio_info(struct obs_pw_audio_info *info, const stru...
function on_process_cb (line 174) | static void on_process_cb(void *data)
function on_state_changed_cb (line 223) | static void on_state_changed_cb(void *data, enum pw_stream_state old, en...
function on_param_changed_cb (line 233) | static void on_param_changed_cb(void *data, uint32_t id, const struct sp...
function on_io_changed_cb (line 249) | static void on_io_changed_cb(void *data, uint32_t id, void *area, uint32...
type pw_stream_events (line 260) | struct pw_stream_events
function obs_pw_audio_stream_connect (line 268) | int obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s, uint32_t ...
function on_core_done_cb (line 324) | static void on_core_done_cb(void *data, uint32_t id, int seq)
function on_core_error_cb (line 333) | static void on_core_error_cb(void *data, uint32_t id, int seq, int res, ...
type pw_core_events (line 342) | struct pw_core_events
function obs_pw_audio_instance_init (line 348) | bool obs_pw_audio_instance_init(struct obs_pw_audio_instance *pw, const ...
function obs_pw_audio_instance_destroy (line 398) | void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw)
function obs_pw_audio_instance_sync (line 430) | void obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw)
function on_metadata_property_cb (line 437) | static int on_metadata_property_cb(void *data, uint32_t id, const char *...
type pw_metadata_events (line 454) | struct pw_metadata_events
function on_metadata_proxy_removed_cb (line 459) | static void on_metadata_proxy_removed_cb(void *data)
function on_metadata_proxy_destroy_cb (line 465) | static void on_metadata_proxy_destroy_cb(void *data)
type pw_proxy_events (line 477) | struct pw_proxy_events
function obs_pw_audio_default_node_metadata_listen (line 483) | bool obs_pw_audio_default_node_metadata_listen(struct obs_pw_audio_defau...
type obs_pw_audio_proxied_object (line 512) | struct obs_pw_audio_proxied_object {
function on_proxy_bound_cb (line 522) | static void on_proxy_bound_cb(void *data, uint32_t global_id)
function on_proxy_removed_cb (line 530) | static void on_proxy_removed_cb(void *data)
function on_proxy_destroy_cb (line 536) | static void on_proxy_destroy_cb(void *data)
type pw_proxy_events (line 550) | struct pw_proxy_events
function obs_pw_audio_proxied_object_new (line 557) | void obs_pw_audio_proxied_object_new(struct pw_proxy *proxy, struct spa_...
type obs_pw_audio_proxied_object (line 573) | struct obs_pw_audio_proxied_object
function obs_pw_audio_proxy_list_init (line 578) | void obs_pw_audio_proxy_list_init(struct obs_pw_audio_proxy_list *list,
function obs_pw_audio_proxy_list_append (line 588) | void obs_pw_audio_proxy_list_append(struct obs_pw_audio_proxy_list *list...
function obs_pw_audio_proxy_list_clear (line 593) | void obs_pw_audio_proxy_list_clear(struct obs_pw_audio_proxy_list *list)
function obs_pw_audio_proxy_list_iter_init (line 602) | void obs_pw_audio_proxy_list_iter_init(struct obs_pw_audio_proxy_list_it...
function obs_pw_audio_proxy_list_iter_next (line 608) | bool obs_pw_audio_proxy_list_iter_next(struct obs_pw_audio_proxy_list_it...
FILE: src/pipewire-audio.h
type obs_pw_audio_info (line 36) | struct obs_pw_audio_info {
type obs_pw_audio_stream (line 45) | struct obs_pw_audio_stream {
type obs_pw_audio_stream (line 58) | struct obs_pw_audio_stream
type obs_pw_audio_instance (line 65) | struct obs_pw_audio_instance {
type obs_pw_audio_instance (line 84) | struct obs_pw_audio_instance
type pw_registry_events (line 84) | struct pw_registry_events
type obs_pw_audio_instance (line 92) | struct obs_pw_audio_instance
type obs_pw_audio_instance (line 97) | struct obs_pw_audio_instance
type obs_pw_audio_default_node_metadata (line 103) | struct obs_pw_audio_default_node_metadata {
type obs_pw_audio_default_node_metadata (line 118) | struct obs_pw_audio_default_node_metadata
type obs_pw_audio_instance (line 119) | struct obs_pw_audio_instance
type obs_pw_audio_proxied_object (line 129) | struct obs_pw_audio_proxied_object
type obs_pw_audio_proxied_object (line 134) | struct obs_pw_audio_proxied_object
type obs_pw_audio_proxy_list (line 139) | struct obs_pw_audio_proxy_list {
type obs_pw_audio_proxy_list (line 145) | struct obs_pw_audio_proxy_list
type obs_pw_audio_proxy_list (line 149) | struct obs_pw_audio_proxy_list
type pw_proxy (line 149) | struct pw_proxy
type obs_pw_audio_proxy_list (line 154) | struct obs_pw_audio_proxy_list
type obs_pw_audio_proxy_list_iter (line 159) | struct obs_pw_audio_proxy_list_iter {
type obs_pw_audio_proxy_list_iter (line 164) | struct obs_pw_audio_proxy_list_iter
type obs_pw_audio_proxy_list (line 164) | struct obs_pw_audio_proxy_list
type obs_pw_audio_proxy_list_iter (line 169) | struct obs_pw_audio_proxy_list_iter
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (125K chars).
[
{
"path": ".clang-format",
"chars": 5931,
"preview": "# please use clang-format version 16 or later\n\nStandard: c++17\nAccessModifierOffset: -8\nAlignAfterOpenBracket: Align\nAli"
},
{
"path": ".editorconfig",
"chars": 298,
"preview": "root = true\n\n[*]\ninsert_final_newline = true\ntrim_trailing_whitespace = true\ncharset = utf-8\nindent_style = tab\nindent_s"
},
{
"path": ".github/workflows/main.yml",
"chars": 3006,
"preview": "name: Build\n\non:\n push:\n paths:\n - .github/workflows/main.yml\n - 'src/**'\n - 'cmake/**'\n - 'CMak"
},
{
"path": ".gitignore",
"chars": 180,
"preview": "CMakeLists.txt.user\nCMakeCache.txt\nCMakeFiles\nCMakeScripts\nTesting\nMakefile\ncmake_install.cmake\ninstall_manifest.txt\ncom"
},
{
"path": "CMakeLists.txt",
"chars": 1155,
"preview": "cmake_minimum_required(VERSION 3.10)\nproject(linux-pipewire-audio)\n\ninclude(GNUInstallDirs)\n\nset(linux-pipewire-audio_SO"
},
{
"path": "LICENSE",
"chars": 18092,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 2, June 1991\n\n Copyright (C) 1989, 1991 Fr"
},
{
"path": "README.md",
"chars": 2661,
"preview": "# Audio device and application capture for OBS Studio using PipeWire\n\nThis plugin adds 3 sources for capturing audio out"
},
{
"path": "cmake/FindPipeWire.cmake",
"chars": 4206,
"preview": "# .rst: FindPipeWire\n# -------\n#\n# Try to find PipeWire on a Unix system.\n#\n# This will define the following variables:\n"
},
{
"path": "data/locale/ar-SA.ini",
"chars": 597,
"preview": "PipeWireAudioCaptureInput=\"التقاط مدخل الصوت (PipeWire)\"\nPipeWireAudioCaptureOutput=\"التقاط مخرج الصوت (PipeWire)\"\nPipeW"
},
{
"path": "data/locale/de-DE.ini",
"chars": 701,
"preview": "PipeWireAudioCaptureInput=\"Audioeingabeaufnahme (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Audioausgabeaufnahme (PipeWire)\""
},
{
"path": "data/locale/en-US.ini",
"chars": 645,
"preview": "PipeWireAudioCaptureInput=\"Audio Input Capture (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Audio Output Capture (PipeWire)\"\n"
},
{
"path": "data/locale/es-ES.ini",
"chars": 783,
"preview": "PipeWireAudioCaptureInput=\"Captura de entrada de audio (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Captura de salida de audi"
},
{
"path": "data/locale/fr-FR.ini",
"chars": 767,
"preview": "PipeWireAudioCaptureInput=\"Capture de l'entrée audio (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Capture de la sortie audio "
},
{
"path": "data/locale/id-ID.ini",
"chars": 673,
"preview": "PipeWireAudioCaptureInput=\"Penangkap Input Audio (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Penangkap Output Audio (PipeWir"
},
{
"path": "data/locale/pl-PL.ini",
"chars": 752,
"preview": "PipeWireAudioCaptureInput=\"Przechwytywanie wejścia dźwięku (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Przechwytywanie wyjśc"
},
{
"path": "data/locale/pt-BR.ini",
"chars": 733,
"preview": "PipeWireAudioCaptureInput=\"Captura de entrada de áudio (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Captura de saída de áudio"
},
{
"path": "src/linux-pipewire-audio.c",
"chars": 1316,
"preview": "/* linux-pipewire-audio.c\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This program is "
},
{
"path": "src/pipewire-audio-capture-app.c",
"chars": 35591,
"preview": "/* pipewire-audio-capture-app.c\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This progr"
},
{
"path": "src/pipewire-audio-capture-device.c",
"chars": 15416,
"preview": "/* pipewire-audio-capture-device.c\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This pr"
},
{
"path": "src/pipewire-audio.c",
"chars": 18626,
"preview": "/* pipewire-audio.c\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This program is free s"
},
{
"path": "src/pipewire-audio.h",
"chars": 5066,
"preview": "/* pipewire-audio.h\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This program is free s"
}
]
About this extraction
This page contains the full source code of the dimtpap/obs-pipewire-audio-capture GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (114.4 KB), approximately 29.8k tokens, and a symbol index with 155 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.