pax_global_header00006660000000000000000000000064147535774350014536gustar00rootroot0000000000000052 comment=ae0b72e52ed54ed4547cad68c66298ec3ccab2bb obs-pipewire-audio-capture-1.2.0/000077500000000000000000000000001475357743500167035ustar00rootroot00000000000000obs-pipewire-audio-capture-1.2.0/.clang-format000066400000000000000000000063051475357743500212620ustar00rootroot00000000000000# please use clang-format version 8 or later Standard: Cpp11 AccessModifierOffset: -8 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignEscapedNewlines: Left AlignOperands: true AlignTrailingComments: true #AllowAllArgumentsOnNextLine: false # requires clang-format 9 #AllowAllConstructorInitializersOnNextLine: false # requires clang-format 9 AllowAllParametersOfDeclarationOnNextLine: false AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: Inline AllowShortIfStatementsOnASingleLine: false #AllowShortLambdasOnASingleLine: Inline # requires clang-format 9 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: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DerivePointerAlignment: false DisableFormat: false FixNamespaceComments: false ForEachMacros: - 'json_object_foreach' - 'json_object_foreach_safe' - 'json_array_foreach' IncludeBlocks: Preserve IndentCaseLabels: false IndentPPDirectives: None IndentWidth: 4 IndentWrappedFunctionNames: false KeepEmptyLinesAtTheStartOfBlocks: true MaxEmptyLinesToKeep: 1 NamespaceIndentation: None #ObjCBinPackProtocolList: Auto # requires clang-format 7 ObjCBlockIndentWidth: 8 ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true PenaltyBreakAssignment: 10 PenaltyBreakBeforeFirstCallParameter: 30 PenaltyBreakComment: 10 PenaltyBreakFirstLessLess: 0 PenaltyBreakString: 10 PenaltyExcessCharacter: 100 PenaltyReturnTypeOnItsOwnLine: 60 PointerAlignment: Right ReflowComments: false SortIncludes: false SortUsingDeclarations: false SpaceAfterCStyleCast: false #SpaceAfterLogicalNot: false # requires clang-format 9 SpaceAfterTemplateKeyword: false SpaceBeforeAssignmentOperators: true #SpaceBeforeCtorInitializerColon: true # requires clang-format 7 #SpaceBeforeInheritanceColon: true # requires clang-format 7 SpaceBeforeParens: ControlStatements #SpaceBeforeRangeBasedForLoopColon: true # requires clang-format 7 SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false #StatementMacros: # requires clang-format 8 # - 'Q_OBJECT' TabWidth: 4 #TypenameMacros: # requires clang-format 9 # - 'DARRAY' UseTab: ForContinuationAndIndentationobs-pipewire-audio-capture-1.2.0/.github/000077500000000000000000000000001475357743500202435ustar00rootroot00000000000000obs-pipewire-audio-capture-1.2.0/.github/workflows/000077500000000000000000000000001475357743500223005ustar00rootroot00000000000000obs-pipewire-audio-capture-1.2.0/.github/workflows/main.yml000066400000000000000000000057031475357743500237540ustar00rootroot00000000000000name: 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\ libgles2-mesa-dev\ libpipewire-0.3-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 }} obs-pipewire-audio-capture-1.2.0/.gitignore000066400000000000000000000002641475357743500206750ustar00rootroot00000000000000CMakeLists.txt.user CMakeCache.txt CMakeFiles CMakeScripts Testing Makefile cmake_install.cmake install_manifest.txt compile_commands.json CTestTestfile.cmake _deps /build .vscodeobs-pipewire-audio-capture-1.2.0/CMakeLists.txt000066400000000000000000000022031475357743500214400ustar00rootroot00000000000000cmake_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) obs-pipewire-audio-capture-1.2.0/LICENSE000066400000000000000000000432541475357743500177200ustar00rootroot00000000000000 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. Copyright (C) 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. , 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. obs-pipewire-audio-capture-1.2.0/README.md000066400000000000000000000055271475357743500201730ustar00rootroot00000000000000# 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) ![Device capture properties](assets/device-capture.png) ![App capture properties](assets/app-capture.png) ## Usage ### Requirements - OBS Studio 28.0 or later - [WirePlumber](https://pipewire.pages.freedesktop.org/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`](https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#should-i-uninstall-everything-pulseaudio) 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. See the [PipeWire wiki](https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/home) for more info. ### Installation Get the `linux-pipewire-audio-(version).tar.gz` archive from the [latest release](https://github.com/dimtpap/obs-pipewire-audio-capture/releases/latest) If OBS Studio is installed as a - Regular package: Extract the archive in `~/.config/obs-studio/plugins/` Your files should look like this ``` /home/user/.config/obs-studio/plugins ├── linux-pipewire-audio │ ├── bin │ │ └── 64bit │ │ └── linux-pipewire-audio.so │ └── data │ └── locale │ ... ``` - Flatpak: > [!IMPORTANT] > ***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. Extract the archive in `~/.var/app/com.obsproject.Studio/config/obs-studio/plugins/` Note: If the plugin isn't working run `flatpak override --filesystem=xdg-run/pipewire-0 com.obsproject.Studio` ### Building 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 obs-pipewire-audio-capture-1.2.0/assets/000077500000000000000000000000001475357743500202055ustar00rootroot00000000000000obs-pipewire-audio-capture-1.2.0/assets/app-capture.png000066400000000000000000000251171475357743500231420ustar00rootroot00000000000000PNG  IHDRs pHYs+ IDATxy@Lk9343)Zp%KY/%KْPHY)VŚ%BQδO9J}gssw3 BIB BI BI BI BI BI BI BI[7Gd2y<a0ߺ-!zA)B!Dl񢩘L6IxB_ CNNNFF&==7˦x-B! I5A5GpuBt#bh*\oB}d6B!$ B!$ B!$8 B[cl66/D/X鐵JGy߶m Yuwד& nՓ[OC=V_:SӏD{\3BD#[!o./h|nVRY;>ytPQShѪWwe®ЏCIIKII 7:z!|Lܼ1b/iQԒgLkhĊ yOnqJ}hI5Bq+iԼc 6Y4Rz^鸽|}ݟك\ˠ,~ߓ^ e¨%)߿R8ZLjO:xvG^pf aMli|9ȡHN63G̣oݹ(:rMdY-=mRG3ĠL5&{T aVך ͛`+&Yr b5[m>,[yd̺C69Ϋtř9xi¢˰Vd}EYmڍQYY4!kFP J-jܸ@%(Sxsױ]Y$̎s-<zէCl-`t>Q}U?Ѫ#FZ,yE} NawOZAț:7LNra2 \;% v#/ Ӯs<.!6B=e4;o;`vjaOϥ[_w;]6o/opнٵc[ƥP9o [稢Zָ2E slP)aP:( ۷NYB`S}&PuzNr!ٿ+^鲻"VF΂;FHPaz H~ҿ<",ί8-:q g)EjZ$ũ+w\e3=z'QFʫȔ2_WT!AФ9"~])G>{E>]B1Kye ϩUN`Ii#wZi!}sХ)gn jS_Hͦr ԷO՚ʿˁy|~-;]awW胑52~T[WB?'JuUJ͜U"qnv*TT-ɫ ]D\[l/Y͗ I͌U#":YaËl,Mj)1V%խMrR:- ?d'"O]TPT@P,(OV> <ד)YJ8_Tw*)jg F΂ˬ~KNO73VzQ޵5uƩIB)"1(qJgJoMM ˪MB,m׈r)46n?+4SbӗJ%P) ssDRxt_X/$!J >\N~n͹f%%J)ʓ ׼$@DQ9-+2v.=3+@T)gT3q>>‚>+q敨+ vΨRP,ƈ_,o;JߗB6c捕 \yPڍ(,Z$oݞc5kD0:LٴPʍi>YK*\2k݂>%@ R@em3(} /O*)*)PFUV,(,fEq7mf j#=OS"/si1~?m|W@ LZRdRO>.fm߄5RJhBO㽠*p9 ttC z xb# PVU,6m6U5`)Rl@=—!!8@IEIɼ?Gw'tu=z}Fyqy0ElZ͚xn@}j8 zhl?R9\*^=Ts! W_NUFˊĭZW#.LUn@*5T lczcoStvd_1D=`ؘ$T=1|sLGg>b+(02@ptvkP=c4 Y Ui,|.RVQ^,xSJ)g0Ā @ݎLen\(2./,( ۳?ʅ~]\T*B 9*kaďJ/f}Ga2&ʩ5hq[ΝsnnnCãf_S<̻x2jmߨgW9?2kȑwƞt;ᇊտzhn~Hvd? 3wԩQڨ邏H XҬsEDBrFE^='#OU濍 ,pm'Mo^smrtaa;[M8"]zc9tfgߗQ[ʧr)N˕eJ^rSHgo E=)!&?vYдtA焟t%ς.&6qJ6[$/EiᇜP=XT4sG̚~.PwEGyڪ a@起謫;+-=&y{{Tr.徫^#^nD8dy=K=9`۱y\߷ŷ]nE#k+pw7<~ O%}9lBy#GCs@ Hid۶mmd4lfSm9c7כ1C̎s-)ۺ:١rxA18p zyeC{K>ZQgDv[7gj :xd BCEףlSC! >oeeEdiiTxQyl˷n#M~Fݏטs,Wxf<]KB'PV峚89T?B!ԠF&GBIƋ!WhI/J(lI!Wf`h*>OQ!PЧU`h*H^\\_BD"QqqqzzzwCB!$ G/B!$a/B!$a/B!$a/B!$a/B!$a/B!$a/B!$a/B!$a/B!$a/B!$a/B!$ao݀ x`| B_qN1X, |~_i񢩘L6IxB_ CNNNFF/MśeSx<!$۠Śp8ߺ !BZZW1^4@!>`2[_!B!B!B] ]qB!C/몐$Y7g0OchB{\3B42,{OTYiF?nҩf0\ Iz5Q?7"%q?Jڅ͜6e˜9gUV}E֌3{=xʖL +1x@ /jJK}R'%g8j@j۾wx8'dطcf3a毩Yl;j㬮_P4\e_Ug:7m:4MBt!~}]?dݑIȴi'=ZBh8i23hS'Oz:5nhqb  ש/k+B!T?333GGrtt433kN$/Ǐ{?sş@eF\!@qtɐ1yzߕ4! TivN wR)ۛ7_q5T<Be=/=7qti+oD%R[6]pb^_rZpϳS"YKN2z5wx[9}4礮˗S>;;1׃0~T5A:IG__m$>}iQ׶Y7%E en1PKtѓƖ:l`2TG"B쥽{h˭ʴ=B"0 l.DܤR۶mcu OQ@Nq;4QSdn9z6''/Eiᇜu4X8},(O=uNX \/WbE!%'d1˗ b©l阽ċʿKM!㿋iBIX^p!OG/B!xB! xTb!j"QcOxTB!ByES|FB!'EF6xT"(== B'&SB!$a8zB! xB! xB! xB! xB! xB! xB! xB! xB! xB! xB! xB! c~HUƎ1jEmFQtZZys%%-4B;ߘ s.(/+Mt#19bmC!8z uZ{)bBs3D%)]ȪQz ?zE;T9ۅY-I箿W}V+^E_B-EK#~O۔yȁw3,>K.Zd(ZNvu<GgK=05x8Wѿ=*jc5{r[޻׽4[VXF^pzåU쭮:7\R8ZLjO:M̎ m=SL9[|͊nufw Q$`u[t`Cϼt'*g |epho㺵W|RI]^Xг޽gԜsJՆ޻mtS#e+TΎlj%-}<8Ӽ[Ǐ CoF=DžK״:a3lb)eUN~vj>f$Xu2~\`A2DV̯B}{89EbEnva=w;w24@yJlB:ꈟ_L*(O}wf3xo;@ @5z|6:?7P0pa|I>&Pyb*6a݃b @W\ t b>Ava6TNH0ioZR]q;avmƼ|2 L"IÝ]o5sph;lV!D2?_st#ǒhg];d ]*L{75%UM@ءgB߉Jkh3mp~Ity^v~|Y-nÌ.{]ͨ%AoG/Z] (,Gv ;e]): ..*T(+STV"A1j* ޼y?Brz _#[^Ǒ-SL۩ʂbuzfm;H[Z,[cުniQq0>-W%*M͟>gP;\~L 5U-CjIT<%(AKʜn@(r? *j&/UݑiQ .gֳ]R v-&ghe>d)]UqJJ _WעKH! -zpY5!bSUMկj 55e>|s6%R74W}`%l=}^sQ>{6_y8t86f?9UR/S8>:P1A}=ok) `L,9+E2):Tސ\vpfMV1]ιj[1y%LΪٗ_J5Z-"2VP`7PnH'~)SgK)d3+:C?\R/S-)]Di Ej+Tf[]@07mW_ls$tY4@ mFCy8 ]-tkB$x Ys40UL_]`<< ѷ5&*'m;jTX&{0Zȓ@hV n<򰽓qe[,Z3@<88k]HRFC]zx1sY=x *|K?bisPM9[›ӗ+CC^F {\ѽaQ y 5YV8 ת|%;zPM&!y ЏE34dzLAAAKU/RWVMZ*g帤GIb[w//}?Q}ֶ )~c[tQ7ljigASλEݙ̾U0낟{f^QZ\T^}qL;VONェ:(&<䢻EןBis=ㇾACc #bÏYXrRÁ7b##"qk덚 :6x+.>1.mot.V7y-^w>Sb$]AB=g_]bCĄM4&vE'RvJth˙7]d{bʑ؋O+k޼G'$޾< ;Ƞ}:tJw,=^泜}yB~Y34+ƙO:d mp?];̉l=+`y!!~,|ƌ7r羐JMK?RB!//!//ȷnB!BI.D!a@!a@!a@!a@!a@!a@!vC2خ^IENDB`obs-pipewire-audio-capture-1.2.0/assets/device-capture.png000066400000000000000000000075741475357743500236300ustar00rootroot00000000000000PNG  IHDRK9L\ pHYs+.IDATxy@LJR"glVr Z~keɺrmrZ+WQ6HRct<%믚|ybb1n>   nƊkJCCԽAD"[W$;`oN^!k:[J$޹Nf"Rv @DT*Ize[e@e@e@e@ex4ؘ[7c9-Ukx4^Sj2j4(\hcnna4Gv5ԤT1&xOtT'otZ |RDŎ-U׏OjDCϵimU~lHyk111Dg«H0*Cyz-QoIӈ[K#jAfSS|.>uSzv䓊-mlz%:=dҫ|Voiv_wr|(H`.Y`1:'%ʹ-ZUmD@*#R(j|TZ7zǿ}怢 O9;C{XJTr2aY3 91Z}-Oq/_7s[Uđ,7@^mNdoY&t V{o! FNi7nrc>mz}6 aFCDY&f;Фx:C?bssFvanX ZDuP' q).GvBE>;"*-8sCkɿnlTwd0/W; HE/j] Swzp;[Ҳ՞Uƃ"c &.LI 3iOp}CNN _,s/]yOB}=2_>W}SPX'D鵏ˉH|>ezCznL^&"ޕ9=}P5l?~w{kFDvP(N(X5}azBo6-/\ZSk' smeǽ=]mrOEjٽ=3<^EAKT:7-g#jSȈoU3ea|e/,bXF^{)pODTvkĕ3?+Ҵ 3ʉ8<{e#UJ=^;w^*T{o)ul/2fn~-^0QvHfŏ7s85 z3|M {x̡?]o{<7^^.ĀEUZ-ȇ`y&MA2 B0> 7=N*XUU#MYrrZ4K 9"Y:JD$UުZD_ )Cq%%5ZRֳG\C b=lO Q>\;T?oeDW0eȃ /*Q~Oܫ㎝Wh@D~页g\~R4*ŧnOzMAk1#k)..V7]*2FTTLC7ͻ@9@9@9@9@9@9J%I@Bn^)y5[e@e@e@e@e@e@e@Ա۩ǩaZc|g+ի.T1n^|.p\[}vC >rG֪DD$li7{3aa׺[DĨ8ésϝ ;ѫq&yvq/  .s4;?7xU^!~[b5EXopqi9 Xv s3] wsf,FS G0hg d<k8Y?!KF< 𹨓>;F}3cs5+-4N,\4r"ѳl"i4$ZADeIRG5!1 w2=򨔈JS _f/>/^̽qf *^?M:+횆mE յ "7,v=#af@EȸqOxW_{暑M5T^ % "aυP ڶ үl|~9X}{VWO\M?ZcnEUɰ_tUW1Kt(yHҾO_!#Էp6j&Q~ΛyUs7ۼ!I7Omd{فyi?f{o:"V w7'G}AVW-ts'S^7Z!Fd9E1cs J|zUvv8x\!-WXHA~N^5e GOqqQş-wpDsW GM~Uƈݺǟ tS~9XFD$+.E܄%r".DR1U;k'(*\;RئZ3SF(w7T\ϦWP/ԧq=cXqUgDT^$)ikDUbGӶkt~o՝+e8L%r<}JF]?3,ja2Dn\,&"TR${Lq2,_Ǩq,IƱzxz$Os/~0:"H|(C\׼Aҍ$|jQf|<t.Rٽ;"w[cU~oڿy>D~aM=* n-Jo-x$'"Fd>ys=QUW:G'#"bDv>]F#"Ym6yL__1ڮ[z<';_PO r""V"/G)Jn߳}URNin%XOO[at ".4"S<1tߑ%"E޹Z5ʨ!k E q7+cuXJJW-Jurbu,=7y5;1kNuH/Խ ɕM\3g44幏wGgCR,njj Xʩ()g=٣=&MYZ9ؖ~ZP.-,$u[_8"_qٝ_O8pwǔ=nv?7oZ~px$;1bOvfY$\cxFɈ`yay$57_d u!OOxXiP9gK;ނ<1`{Y¦d M|}zhb5Z|IԁY.JDT˥!e,V>X] =X,jttKm8V j3v6å|>222444yM[l [nR|?GDBPS^mTQ_V>_EvpxW!''˺w |j {+@e@e@e[)))f0e@e@esH$njew >A[e@e@e@e@e@e@e@e󴈶[!IENDB`obs-pipewire-audio-capture-1.2.0/cmake/000077500000000000000000000000001475357743500177635ustar00rootroot00000000000000obs-pipewire-audio-capture-1.2.0/cmake/FindPipeWire.cmake000066400000000000000000000101601475357743500233100ustar00rootroot00000000000000# .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 Copyright 2014 Martin Gräßlin # Copyright 2018-2020 Jan Grulich # # 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") obs-pipewire-audio-capture-1.2.0/data/000077500000000000000000000000001475357743500176145ustar00rootroot00000000000000obs-pipewire-audio-capture-1.2.0/data/locale/000077500000000000000000000000001475357743500210535ustar00rootroot00000000000000obs-pipewire-audio-capture-1.2.0/data/locale/en-US.ini000066400000000000000000000011621475357743500225030ustar00rootroot00000000000000PipeWireAudioCaptureInput="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" Application="Application" Applications="Applications" ExceptApp="Capture all apps except selected" SelectedApps="Selected Apps" AddToSelected="Add selection"obs-pipewire-audio-capture-1.2.0/data/locale/fr-FR.ini000066400000000000000000000013511475357743500224700ustar00rootroot00000000000000PipeWireAudioCaptureInput="Capture d'entrée audio (PipeWire)" PipeWireAudioCaptureOutput="Capture de sortie audio (PipeWire)" PipeWireAudioCaptureApplication="Capture d'application audio (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" Application="Application" Applications="Applications" ExceptApp="Capturer toutes les applications sauf celles sélectionnées" SelectedApps="Applications sélectionnées" AddToSelected="Ajouter à la sélection" obs-pipewire-audio-capture-1.2.0/data/locale/pt-BR.ini000066400000000000000000000010141475357743500224740ustar00rootroot00000000000000PipeWireAudioCaptureInput="Captura de Entrada de Áudio (PipeWire)" PipeWireAudioCaptureOutput="Saída de Captura de Áudio (PipeWire)" PipeWireAudioCaptureApplication="Captura de Áudio de Aplicativo (PipeWire)" MatchPriority="Prioridade da Correspondência" MatchBinaryFirst="Corresponder ao nome do binário, se falhar, ao nome do app" MatchAppNameFirst="Corresponder pelo nome do app, se falhar, ao nome do binário" Device="Dispositivo" Application="Aplicativo" ExceptApp="Capturar todos os apps, exceto o selecionado" obs-pipewire-audio-capture-1.2.0/src/000077500000000000000000000000001475357743500174725ustar00rootroot00000000000000obs-pipewire-audio-capture-1.2.0/src/linux-pipewire-audio.c000066400000000000000000000024431475357743500237210ustar00rootroot00000000000000/* linux-pipewire-audio.c * * Copyright 2022-2024 Dimitris Papaioannou * * 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 . * * SPDX-License-Identifier: GPL-2.0-or-later */ #include #include #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 }obs-pipewire-audio-capture-1.2.0/src/pipewire-audio-capture-app.c000066400000000000000000001046331475357743500250070ustar00rootroot00000000000000/* pipewire-audio-capture-app.c * * Copyright 2022-2024 Dimitris Papaioannou * * 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 . * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "pipewire-audio.h" #include /* 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] 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] 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] 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] 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 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] 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] 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] 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) { struct capture_sink_port *port = da_push_back_new(pwac->sink.ports); port->channel = bstrdup(channel); port->id = global_id; } 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 bool make_capture_sink(struct obs_pw_audio_capture_app *pwac, uint32_t channels, const char *position) { /* HACK: In order to hide the app capture sink from PulseAudio applications, for example to prevent them from intentionally outputting * to it, or to not fill up desktop audio control menus with sinks, the media class is set to Audio/Sink/Internal. * This works because pipewire-pulse only reports nodes with the media class set to Audio/Sink as proper outputs * https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/0.3.72/src/modules/module-protocol-pulse/manager.c?ref_type=tags#L944 * and because of https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/1564#note_1861698, which works the same for * deciding what nodes need an audio adapter. */ struct pw_properties *sink_props = pw_properties_new(PW_KEY_FACTORY_NAME, "support.null-audio-sink", PW_KEY_MEDIA_CLASS, "Audio/Sink/Internal", 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] Failed to create app capture sink"); return false; } 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); while (pwac->sink.id == SPA_ID_INVALID || pwac->sink.serial == SPA_ID_INVALID || pwac->sink.ports.num != channels) { /* Iterate until the sink is bound and all the ports are registered */ pw_loop_iterate(pw_thread_loop_get_loop(pwac->pw.thread_loop), -1); } if (pwac->sink.serial == 0) { pw_proxy_destroy(pwac->sink.proxy); return false; } blog(LOG_INFO, "[pipewire] Created app capture sink %u with %u channels and position %s", pwac->sink.id, channels, position); connect_targets(pwac); pwac->sink.autoconnect_targets = true; if (obs_pw_audio_stream_connect(&pwac->pw.audio, pwac->sink.id, pwac->sink.serial, channels) < 0) { blog(LOG_WARNING, "[pipewire] Error connecting stream %p to app capture sink %u", pwac->pw.audio.stream, pwac->sink.id); } return true; } 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_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; } struct obs_pw_audio_capture_app *pwac = data; /** Use stereo if * - The default sink uses the Pro Audio profile, since all streams will be configured to use stereo * https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#what-is-the-pro-audio-profile * - The default sink doesn't have the needed props and there isn't already an app capture sink */ const char *channels = spa_dict_lookup(info->props, PW_KEY_AUDIO_CHANNELS); const char *position = spa_dict_lookup(info->props, SPA_KEY_AUDIO_POSITION); if (!channels || !position) { if (pwac->sink.proxy) { return; } channels = "2"; position = "FL,FR"; } else if (astrstri(position, "AUX")) { /* Pro Audio sinks use AUX0,AUX1... and so on as their position (see link above) */ channels = "2"; position = "FL,FR"; } uint32_t c = strtoul(channels, NULL, 10); if (!c) { return; } /* No need to create a new capture sink if the channels are the same */ if (pwac->sink.channels == c && !dstr_is_empty(&pwac->sink.position) && dstr_cmp(&pwac->sink.position, position) == 0) { return; } destroy_capture_sink(pwac); make_capture_sink(pwac, c, position); } static const struct pw_node_events default_sink_events = { PW_VERSION_NODE_EVENTS, .info = on_default_sink_info_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] 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] 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); } /* ------------------------------------------------- */ /* 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] No object serial found on app capture sink %u", id); pwac->sink.serial = 0; } else { pwac->sink.serial = strtoul(ser, NULL, 10); } } 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] 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); } obs-pipewire-audio-capture-1.2.0/src/pipewire-audio-capture-device.c000066400000000000000000000345631475357743500254720ustar00rootroot00000000000000/* pipewire-audio-capture-device.c * * Copyright 2022-2024 Dimitris Papaioannou * * 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 . * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "pipewire-audio.h" #include /* 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 (!node->channels) { return; } 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] %p streaming from %u", pwac->pw.audio.stream, node->serial); } else { pwac->connected_serial = SPA_ID_INVALID; blog(LOG_WARNING, "[pipewire] 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_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 *channels = spa_dict_lookup(info->props, PW_KEY_AUDIO_CHANNELS); if (!channels) { return; } uint32_t c = strtoul(channels, NULL, 10); struct target_node *n = data; if (n->channels == c) { return; } n->channels = c; 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, .info = on_node_info_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); } /* ------------------------------------------------- */ /* 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] 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; if (pwac->default_info.autoconnect) { 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] 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] 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); } obs-pipewire-audio-capture-1.2.0/src/pipewire-audio.c000066400000000000000000000423411475357743500225650ustar00rootroot00000000000000/* pipewire-audio.c * * Copyright 2022-2024 Dimitris Papaioannou * * 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 . * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "pipewire-audio.h" #include #include /* 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] 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] Stream %p failed to parse audio format info", s->stream); } else { blog(LOG_INFO, "[pipewire] %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) { enum spa_audio_channel pos[8]; obs_channels_to_spa_audio_position(pos, audio_channels); uint8_t buffer[2048]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); const struct spa_pod *params[1]; 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)); 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] 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] Error starting threaded mainloop"); return false; } pw->core = pw_context_connect(pw->context, NULL, 0); if (!pw->core) { blog(LOG_WARNING, "[pipewire] 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] Failed to create stream"); return false; } blog(LOG_INFO, "[pipewire] 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; } /* ------------------------------------------------- */ obs-pipewire-audio-capture-1.2.0/src/pipewire-audio.h000066400000000000000000000117421475357743500225730ustar00rootroot00000000000000/* pipewire-audio.h * * Copyright 2022-2024 Dimitris Papaioannou * * 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 . * * SPDX-License-Identifier: GPL-2.0-or-later */ /* Stuff used by the PipeWire audio capture sources */ #pragma once #include #include #include #include /* 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); /* ------------------------------------------------- */