[
  {
    "path": ".clang-format",
    "content": "# please use clang-format version 16 or later\n\nStandard: c++17\nAccessModifierOffset: -8\nAlignAfterOpenBracket: Align\nAlignConsecutiveAssignments: false\nAlignConsecutiveDeclarations: false\nAlignEscapedNewlines: Left\nAlignOperands: true\nAlignTrailingComments: true\nAllowAllArgumentsOnNextLine: false\nAllowAllConstructorInitializersOnNextLine: false\nAllowAllParametersOfDeclarationOnNextLine: false\nAllowShortBlocksOnASingleLine: false\nAllowShortCaseLabelsOnASingleLine: false\nAllowShortFunctionsOnASingleLine: Inline\nAllowShortIfStatementsOnASingleLine: false\nAllowShortLambdasOnASingleLine: Inline\nAllowShortLoopsOnASingleLine: false\nAlwaysBreakAfterDefinitionReturnType: None\nAlwaysBreakAfterReturnType: None\nAlwaysBreakBeforeMultilineStrings: false\nAlwaysBreakTemplateDeclarations: false\nBinPackArguments: true\nBinPackParameters: true\nBraceWrapping:\n  AfterClass: false\n  AfterControlStatement: false\n  AfterEnum: false\n  AfterFunction: true\n  AfterNamespace: false\n  AfterObjCDeclaration: false\n  AfterStruct: false\n  AfterUnion: false\n  AfterExternBlock: false\n  BeforeCatch: false\n  BeforeElse: false\n  IndentBraces: false\n  SplitEmptyFunction: true\n  SplitEmptyRecord: true\n  SplitEmptyNamespace: true\nBreakBeforeBinaryOperators: None\nBreakBeforeBraces: Custom\nBreakBeforeTernaryOperators: true\nBreakConstructorInitializers: BeforeColon\nBreakStringLiterals: false  # apparently unpredictable\nColumnLimit: 120\nCompactNamespaces: false\nConstructorInitializerAllOnOneLineOrOnePerLine: true\nConstructorInitializerIndentWidth: 8\nContinuationIndentWidth: 8\nCpp11BracedListStyle: true\nDerivePointerAlignment: false\nDisableFormat: false\nFixNamespaceComments: true\nForEachMacros:\n  - 'json_object_foreach'\n  - 'json_object_foreach_safe'\n  - 'json_array_foreach'\n  - 'HASH_ITER'\nIncludeBlocks: Preserve\nIndentCaseLabels: false\nIndentPPDirectives: None\nIndentWidth: 8\nIndentWrappedFunctionNames: false\nKeepEmptyLinesAtTheStartOfBlocks: true\nMaxEmptyLinesToKeep: 1\nNamespaceIndentation: None\nObjCBinPackProtocolList: Auto\nObjCBlockIndentWidth: 8\nObjCSpaceAfterProperty: true\nObjCSpaceBeforeProtocolList: true\n\nPenaltyBreakAssignment: 10\nPenaltyBreakBeforeFirstCallParameter: 30\nPenaltyBreakComment: 10\nPenaltyBreakFirstLessLess: 0\nPenaltyBreakString: 10\nPenaltyExcessCharacter: 100\nPenaltyReturnTypeOnItsOwnLine: 60\n\nPointerAlignment: Right\nReflowComments: false\nSkipMacroDefinitionBody: true\nSortIncludes: false\nSortUsingDeclarations: false\nSpaceAfterCStyleCast: false\nSpaceAfterLogicalNot: false\nSpaceAfterTemplateKeyword: false\nSpaceBeforeAssignmentOperators: true\nSpaceBeforeCtorInitializerColon: true\nSpaceBeforeInheritanceColon: true\nSpaceBeforeParens: ControlStatements\nSpaceBeforeRangeBasedForLoopColon: true\nSpaceInEmptyParentheses: false\nSpacesBeforeTrailingComments: 1\nSpacesInAngles: false\nSpacesInCStyleCastParentheses: false\nSpacesInContainerLiterals: false\nSpacesInParentheses: false\nSpacesInSquareBrackets: false\nStatementMacros:\n  - 'Q_OBJECT'\nTabWidth: 8\nTypenameMacros:\n  - 'DARRAY'\nUseTab: ForContinuationAndIndentation\n---\nLanguage: ObjC\nAccessModifierOffset: 2\nAlignArrayOfStructures: Right\nAlignConsecutiveAssignments: None\nAlignConsecutiveBitFields: None\nAlignConsecutiveDeclarations: None\nAlignConsecutiveMacros:\n  Enabled: true\n  AcrossEmptyLines: false\n  AcrossComments: true\nAllowShortBlocksOnASingleLine: Never\nAllowShortEnumsOnASingleLine: false\nAllowShortFunctionsOnASingleLine: Empty\nAllowShortIfStatementsOnASingleLine: Never\nAllowShortLambdasOnASingleLine: None\nAttributeMacros: ['__unused', '__autoreleasing', '_Nonnull', '__bridge']\nBitFieldColonSpacing: Both\n#BreakBeforeBraces: Webkit\nBreakBeforeBraces: Custom\nBraceWrapping:\n  AfterCaseLabel: false\n  AfterClass: true\n  AfterControlStatement: Never\n  AfterEnum: false\n  AfterFunction: true\n  AfterNamespace: false\n  AfterObjCDeclaration: false\n  AfterStruct: false\n  AfterUnion: false\n  AfterExternBlock: false\n  BeforeCatch: false\n  BeforeElse: false\n  BeforeLambdaBody: false\n  BeforeWhile: false\n  IndentBraces: false\n  SplitEmptyFunction: false\n  SplitEmptyRecord: false\n  SplitEmptyNamespace: true\nBreakAfterAttributes: Never\nBreakArrays: false\nBreakBeforeConceptDeclarations: Allowed\nBreakBeforeInlineASMColon: OnlyMultiline\nBreakConstructorInitializers: AfterColon\nBreakInheritanceList: AfterComma\nColumnLimit: 120\nConstructorInitializerIndentWidth: 4\nContinuationIndentWidth: 4\nEmptyLineAfterAccessModifier: Never\nEmptyLineBeforeAccessModifier: LogicalBlock\nExperimentalAutoDetectBinPacking: false\nFixNamespaceComments: true\nIndentAccessModifiers: false\nIndentCaseBlocks: false\nIndentCaseLabels: true\nIndentExternBlock: Indent\nIndentGotoLabels: false\nIndentRequiresClause: true\nIndentWidth: 4\nIndentWrappedFunctionNames: true\nInsertBraces: false\nInsertNewlineAtEOF: true\nKeepEmptyLinesAtTheStartOfBlocks: false\nLambdaBodyIndentation: Signature\nNamespaceIndentation: All\nObjCBinPackProtocolList: Auto\nObjCBlockIndentWidth: 4\nObjCBreakBeforeNestedBlockParam: false\nObjCSpaceAfterProperty: true\nObjCSpaceBeforeProtocolList: true\nPPIndentWidth: -1\nPackConstructorInitializers: NextLine\nQualifierAlignment: Leave\nReferenceAlignment: Right\nRemoveSemicolon: false\nRequiresClausePosition: WithPreceding\nRequiresExpressionIndentation: OuterScope\nSeparateDefinitionBlocks: Leave\nShortNamespaceLines: 1\nSortIncludes: false\n#SortUsingDeclarations: LexicographicNumeric\nSortUsingDeclarations: true\nSpaceAfterCStyleCast: true\nSpaceAfterLogicalNot: false\nSpaceAroundPointerQualifiers: Default\nSpaceBeforeCaseColon: false\nSpaceBeforeCpp11BracedList: true\nSpaceBeforeCtorInitializerColon: true\nSpaceBeforeInheritanceColon: true\nSpaceBeforeParens: ControlStatements\nSpaceBeforeRangeBasedForLoopColon: true\nSpaceBeforeSquareBrackets: false\nSpaceInEmptyBlock: false\nSpaceInEmptyParentheses: false\nSpacesBeforeTrailingComments: 2\nSpacesInConditionalStatement: false\nSpacesInLineCommentPrefix:\n  Minimum: 1\n  Maximum: -1\nStandard: c++17\nTabWidth: 4\nUseTab: Never\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ninsert_final_newline = true\ntrim_trailing_whitespace = true\ncharset = utf-8\nindent_style = tab\nindent_size = 8\n\n[CMakeLists.txt]\nindent_style = space\nindent_size = 2\n\n[**/CMakeLists.txt]\nindent_style = space\nindent_size = 2\n\n[cmake/**/*.cmake]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Build\n\non:\n  push:\n    paths:\n      - .github/workflows/main.yml\n      - 'src/**'\n      - 'cmake/**'\n      - 'CMakeLists.txt'\n    tags:\n      - '*'\n    branches:\n      - '**'\n\njobs:\n  build-plugin:\n    strategy:\n      matrix:\n        obs-version: ['28.0.0', '30.2.0']\n    name: 'Build Plugin'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Restore OBS from cache\n        uses: actions/cache@v4\n        id: cache-obs\n        with:\n          path: ${{ github.workspace }}/obs/\n          key: ${{ matrix.obs-version }}\n      - name: Checkout OBS\n        if: steps.cache-obs.outputs.cache-hit != 'true'\n        uses: actions/checkout@v4\n        with:\n          repository: 'obsproject/obs-studio'\n          path: 'obs-src'\n          ref: ${{ matrix.obs-version }}\n          submodules: 'recursive'\n      - name: 'Install system dependencies'\n        run: |\n          sudo apt update\n          sudo apt install cmake ninja-build pkg-config clang clang-format build-essential curl ccache git zsh\\\n                           libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev\\\n                           libcurl4-openssl-dev\\\n                           libxcb1-dev libx11-xcb-dev\\\n                           libgl1-mesa-dev\\\n                           libglvnd-dev\\\n                           libgles2-mesa-dev\\\n                           libpipewire-0.3-dev\\\n                           uuid-dev\\\n                           uthash-dev libjansson-dev\n      - name: 'Configure OBS'\n        if: steps.cache-obs.outputs.cache-hit != 'true'\n        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\n      - name: 'Build OBS'\n        if: steps.cache-obs.outputs.cache-hit != 'true'\n        run: cmake --build obs-src/build -j4\n      - name: 'Install OBS'\n        if: steps.cache-obs.outputs.cache-hit != 'true'\n        run: cmake --install obs-src/build --prefix obs\n      - name: 'Checkout'\n        uses: actions/checkout@v4\n        with:\n          path: 'plugin'\n      - name: 'Configure'\n        run: cmake -B ./plugin/build -S ./plugin -DCMAKE_BUILD_TYPE=RelWithDebInfo -Dlibobs_DIR=\"$GITHUB_WORKSPACE/obs/lib/cmake/libobs/\"\n      - name: 'Build'\n        run: cmake --build ./plugin/build -j4\n      - name: 'Package'\n        run: |\n          mkdir -p ./linux-pipewire-audio/bin/64bit\n          cp ./plugin/build/linux-pipewire-audio.so ./linux-pipewire-audio/bin/64bit/linux-pipewire-audio.so\n          cp -r ./plugin/data/ ./linux-pipewire-audio/data/\n          tar -zcvf linux-pipewire-audio-$OBS_VERSION.tar.gz linux-pipewire-audio\n        env:\n          OBS_VERSION: ${{ matrix.obs-version }}\n      - name: 'Upload'\n        uses: actions/upload-artifact@v4\n        with:\n          path: linux-pipewire-audio-${{ matrix.obs-version }}.tar.gz\n          name: linux-pipewire-audio-${{ matrix.obs-version }}\n"
  },
  {
    "path": ".gitignore",
    "content": "CMakeLists.txt.user\nCMakeCache.txt\nCMakeFiles\nCMakeScripts\nTesting\nMakefile\ncmake_install.cmake\ninstall_manifest.txt\ncompile_commands.json\nCTestTestfile.cmake\n_deps\n/build\n\n.vscode"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10)\nproject(linux-pipewire-audio)\n\ninclude(GNUInstallDirs)\n\nset(linux-pipewire-audio_SOURCES\n\t\t\tsrc/linux-pipewire-audio.c\n\t\t\tsrc/pipewire-audio.h\n\t\t\tsrc/pipewire-audio.c\n\t\t\tsrc/pipewire-audio-capture-device.c\n\t\t\tsrc/pipewire-audio-capture-app.c\n)\n\nadd_library(linux-pipewire-audio MODULE ${linux-pipewire-audio_SOURCES})\n\nlist(APPEND CMAKE_MODULE_PATH \"${CMAKE_CURRENT_LIST_DIR}/cmake\")\n\nfind_package(libobs REQUIRED)\nfind_package(PipeWire REQUIRED)\n\nset(linux-pipewire-audio_INCLUDES\n\t${PIPEWIRE_INCLUDE_DIRS}\n\t${SPA_INCLUDE_DIRS}\n)\n\nadd_definitions(\n\t${PIPEWIRE_DEFINITIONS}\n)\n\nset(linux-pipewire-audio_LIBRARIES\n\tOBS::libobs\n\t${PIPEWIRE_LIBRARIES}\n)\n\ntarget_link_libraries(linux-pipewire-audio ${linux-pipewire-audio_LIBRARIES})\ntarget_compile_options(linux-pipewire-audio PRIVATE -Wall)\n\ninclude_directories(SYSTEM\n\t${linux-pipewire-audio_INCLUDES}\n)\n\nset_target_properties(linux-pipewire-audio PROPERTIES PREFIX \"\")\n\ninstall(TARGETS linux-pipewire-audio LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/obs-plugins)\ninstall(DIRECTORY data/locale DESTINATION ${CMAKE_INSTALL_DATADIR}/obs/obs-plugins/linux-pipewire-audio)\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "README.md",
    "content": "# Audio device and application capture for OBS Studio using PipeWire\n\nThis plugin adds 3 sources for capturing audio outputs, inputs and applications using [PipeWire](https://pipewire.org)\n![Device capture properties](assets/device-capture.png)\n![App capture properties](assets/app-capture.png)\n\n## Usage\n### Requirements\n- OBS Studio 28.0 or later\n- WirePlumber\n\nPipeWire 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))\n\nFor the plugin to be able to capture applications, PipeWire should be set up to handle audio on your system.\nFor most applications, the `pipewire-pulse` compatibility layer should be enough, but there are also\n`pipewire-jack` and `pipewire-alsa`. If applications aren't showing up in the plugin, your system may be\nmissing one of those components.\n\n### Installation\n1. Get the `linux-pipewire-audio-(version).tar.gz` archive from the [latest release](https://github.com/dimtpap/obs-pipewire-audio-capture/releases/latest)\n2. In OBS Studio, go to **File**, then click **Show Settings Folder**\n3. In the folder that opens, create a folder called `plugins` if it doesn't already exist\n4. Extract the archive you downloaded in the `plugins` folder\n5. Restart OBS Studio\n6. If you're using the Flatpak and the sources aren't working, run `flatpak override --filesystem=xdg-run/pipewire-0 com.obsproject.Studio` and restart OBS Studio\n\nYour files should look like this\n```\n.../obs-studio/plugins\n├── linux-pipewire-audio\n│   ├── bin\n│   │   └── 64bit\n│   │       └── linux-pipewire-audio.so\n│   └── data\n│       └── locale\n│           ...\n```\n> [!IMPORTANT]\n> ## Flatpak users note\n> ***THIS INSTALLATION METHOD IS UNSUPPORTED BY THE OBS STUDIO TEAM AND CAN BREAK AT ANY TIME***\n> This plugin relies on a Flatpak permission that OBS Studio could remove at any time, so it can't be on Flathub.\n> If after updating OBS Studio the plugin stops working, check the latest release for a new version, or build the plugin yourself\n> against the latest OBS Studio.\n>\n> Note that native OBS Studio packages do not have this problem.\n\n### Building (for development)\nEnsure you have CMake, PipeWire and OBS Studio/libobs development packages, then in the repo's root:\n```sh\ncmake -B build -DCMAKE_INSTALL_PREFIX=\"/usr\" -DCMAKE_BUILD_TYPE=RelWithDebInfo\ncmake --build build\n# To install it system-wide:\ncmake --install build\n```\n## Inclusion in upstream OBS Studio\nThis 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\n"
  },
  {
    "path": "cmake/FindPipeWire.cmake",
    "content": "# .rst: FindPipeWire\n# -------\n#\n# Try to find PipeWire on a Unix system.\n#\n# This will define the following variables:\n#\n# ``PIPEWIRE_FOUND`` True if (the requested version of) PipeWire is available\n# ``PIPEWIRE_VERSION`` The version of PipeWire ``PIPEWIRE_LIBRARIES`` This can\n# be passed to target_link_libraries() instead of the ``PipeWire::PipeWire``\n# target ``PIPEWIRE_INCLUDE_DIRS`` This should be passed to\n# target_include_directories() if the target is not used for linking\n# ``PIPEWIRE_COMPILE_FLAGS`` This should be passed to target_compile_options()\n# if the target is not used for linking\n#\n# If ``PIPEWIRE_FOUND`` is TRUE, it will also define the following imported\n# target:\n#\n# ``PipeWire::PipeWire`` The PipeWire library\n#\n# In general we recommend using the imported target, as it is easier to use.\n# Bear in mind, however, that if the target is in the link interface of an\n# exported library, it must be made available by the package config file.\n\n# =============================================================================\n# Copyright 2014 Alex Merry <alex.merry@kde.org> Copyright 2014 Martin Gräßlin\n# <mgraesslin@kde.org> Copyright 2018-2020 Jan Grulich <jgrulich@redhat.com>\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are met:\n#\n# 1. Redistributions of source code must retain the copyright notice, this list\n#    of conditions and the following disclaimer.\n# 2. Redistributions in binary form must reproduce the copyright notice, this\n#    list of conditions and the following disclaimer in the documentation and/or\n#    other materials provided with the distribution.\n# 3. The name of the author may not be used to endorse or promote products\n#    derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED\n# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO\n# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR\n# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER\n# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n# =============================================================================\n\n# Use pkg-config to get the directories and then use these values in the\n# FIND_PATH() and FIND_LIBRARY() calls\nfind_package(PkgConfig QUIET)\n\npkg_search_module(PKG_PIPEWIRE QUIET libpipewire-0.3)\npkg_search_module(PKG_SPA QUIET libspa-0.2)\n\nset(PIPEWIRE_COMPILE_FLAGS \"${PKG_PIPEWIRE_CFLAGS}\" \"${PKG_SPA_CFLAGS}\")\nset(PIPEWIRE_VERSION \"${PKG_PIPEWIRE_VERSION}\")\n\nfind_path(\n  PIPEWIRE_INCLUDE_DIRS\n  NAMES pipewire/pipewire.h\n  HINTS ${PKG_PIPEWIRE_INCLUDE_DIRS} ${PKG_PIPEWIRE_INCLUDE_DIRS}/pipewire-0.3)\n\nfind_path(\n  SPA_INCLUDE_DIRS\n  NAMES spa/param/props.h\n  HINTS ${PKG_SPA_INCLUDE_DIRS} ${PKG_SPA_INCLUDE_DIRS}/spa-0.2)\n\nfind_library(\n  PIPEWIRE_LIBRARIES\n  NAMES pipewire-0.3\n  HINTS ${PKG_PIPEWIRE_LIBRARY_DIRS})\n\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(\n  PipeWire\n  FOUND_VAR PIPEWIRE_FOUND\n  REQUIRED_VARS PIPEWIRE_LIBRARIES PIPEWIRE_INCLUDE_DIRS SPA_INCLUDE_DIRS\n  VERSION_VAR PIPEWIRE_VERSION)\n\nif(PIPEWIRE_FOUND AND NOT TARGET PipeWire::PipeWire)\n  add_library(PipeWire::PipeWire UNKNOWN IMPORTED)\n  set_target_properties(\n    PipeWire::PipeWire\n    PROPERTIES IMPORTED_LOCATION \"${PIPEWIRE_LIBRARIES}\"\n               INTERFACE_COMPILE_OPTIONS \"${PIPEWIRE_COMPILE_FLAGS}\"\n               INTERFACE_INCLUDE_DIRECTORIES\n               \"${PIPEWIRE_INCLUDE_DIRS};${SPA_INCLUDE_DIRS}\")\nendif()\n\nmark_as_advanced(PIPEWIRE_LIBRARIES PIPEWIRE_INCLUDE_DIRS)\n\ninclude(FeatureSummary)\nset_package_properties(\n  PipeWire PROPERTIES\n  URL \"https://www.pipewire.org\"\n  DESCRIPTION \"PipeWire - multimedia processing\")\n"
  },
  {
    "path": "data/locale/ar-SA.ini",
    "content": "PipeWireAudioCaptureInput=\"التقاط مدخل الصوت (PipeWire)\"\nPipeWireAudioCaptureOutput=\"التقاط مخرج الصوت (PipeWire)\"\nPipeWireAudioCaptureApplication=\"التقاط صوت تطبيق (PipeWire)\"\nAppCaptureMode=\"وضع الالتقاط\"\nSingleApp=\"تطبيق واحد\"\nMultipleApps=\"عدة تطبيقات\"\nMatchPriority=\"اولوية المقارنة\"\nMatchBinaryFirst=\"قارن باسم الملف التنفيذي، ثم باسم التطبيق\"\nMatchAppNameFirst=\"قارن باسم التطبيق، ثم باسم الملف التنفيذي\"\nDevice=\"الجهاز\"\nDefault=\"افتراضي\"\nApplication=\"تطبيق\"\nApplications=\"تطبيقات\"\nExceptApp=\"التقط جميع التطبيقات ماعدا المحدده\"\nSelectedApps=\"التطبيقات المحدده\"\nAddToSelected=\"اضافة تحديد\"\n"
  },
  {
    "path": "data/locale/de-DE.ini",
    "content": "PipeWireAudioCaptureInput=\"Audioeingabeaufnahme (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Audioausgabeaufnahme (PipeWire)\"\nPipeWireAudioCaptureApplication=\"Anwendungsaudioaufnahme (PipeWire)\"\nAppCaptureMode=\"Aufnahmemodus\"\nSingleApp=\"Einzelne Anwendung\"\nMultipleApps=\"Mehrere Anwendungen\"\nMatchPriority=\"Übereinstimmungspriorität\"\nMatchBinaryFirst=\"Nach Ausführungsdatei abgleichen, dann Anwendungsname\"\nMatchAppNameFirst=\"Nach Anwendungsname abgleichen, dann Ausführungsdatei\"\nDevice=\"Gerät\"\nDefault=\"Standard\"\nApplication=\"Anwendung\"\nApplications=\"Anwendungen\"\nExceptApp=\"Alle Anwendungen außer den ausgewählten aufnehmen\"\nSelectedApps=\"Ausgewählte Anwendungen\"\nAddToSelected=\"Zur Auswahl hinzufügen\"\n"
  },
  {
    "path": "data/locale/en-US.ini",
    "content": "PipeWireAudioCaptureInput=\"Audio Input Capture (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Audio Output Capture (PipeWire)\"\nPipeWireAudioCaptureApplication=\"Application Audio Capture (PipeWire)\"\nAppCaptureMode=\"Capture Mode\"\nSingleApp=\"Single application\"\nMultipleApps=\"Multiple applications\"\nMatchPriority=\"Match Priority\"\nMatchBinaryFirst=\"Match by executable name, fallback to app name\"\nMatchAppNameFirst=\"Match by app name, fallback to executable name\"\nDevice=\"Device\"\nDefault=\"Default\"\nApplication=\"Application\"\nApplications=\"Applications\"\nExceptApp=\"Capture all apps except selected\"\nSelectedApps=\"Selected Apps\"\nAddToSelected=\"Add selection\"\n"
  },
  {
    "path": "data/locale/es-ES.ini",
    "content": "PipeWireAudioCaptureInput=\"Captura de entrada de audio (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Captura de salida de audio (PipeWire)\"\nPipeWireAudioCaptureApplication=\"Captura de audio de la aplicación (PipeWire)\"\nAppCaptureMode=\"Modo de captura\"\nSingleApp=\"Aplicación única\"\nMultipleApps=\"Múltiples aplicaciones\"\nMatchPriority=\"Prioridad de coincidencia\"\nMatchBinaryFirst=\"Coincidir por nombre del ejecutable, luego por nombre de la aplicación\"\nMatchAppNameFirst=\"Coincidir por nombre de la aplicación, luego por nombre del ejecutable\"\nDevice=\"Dispositivo\"\nDefault=\"Por defecto\"\nApplication=\"Aplicación\"\nApplications=\"Aplicaciones\"\nExceptApp=\"Capturar todas las aplicaciones excepto las seleccionadas\"\nSelectedApps=\"Aplicaciones seleccionadas\"\nAddToSelected=\"Agregar a la selección\"\n"
  },
  {
    "path": "data/locale/fr-FR.ini",
    "content": "PipeWireAudioCaptureInput=\"Capture de l'entrée audio (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Capture de la sortie audio (PipeWire)\"\nPipeWireAudioCaptureApplication=\"Capture audio de l'application (PipeWire)\"\nAppCaptureMode=\"Mode de capture\"\nSingleApp=\"Application unique\"\nMultipleApps=\"Applications multiples\"\nMatchPriority=\"Priorité de correspondance\"\nMatchBinaryFirst=\"Correspondance par nom d'executable, repli par nom d'application\"\nMatchAppNameFirst=\"Correspondance par nom d'application, repli par nom d'executable\"\nDevice=\"Appareil\"\nDefault=\"Par défaut\"\nApplication=\"Application\"\nApplications=\"Applications\"\nExceptApp=\"Capturer toutes les applications sauf celles sélectionnées\"\nSelectedApps=\"Applications sélectionnées\"\nAddToSelected=\"Ajouter à la sélection\"\n"
  },
  {
    "path": "data/locale/id-ID.ini",
    "content": "PipeWireAudioCaptureInput=\"Penangkap Input Audio (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Penangkap Output Audio (PipeWire)\"\nPipeWireAudioCaptureApplication=\"Aplikasi Penangkap Audio (PipeWire)\"\nAppCaptureMode=\"Mode Tangkapan\"\nSingleApp=\"Satu aplikasi\"\nMultipleApps=\"Banyak aplikasi\"\nMatchPriority=\"Prioritas Kecocokan\"\nMatchBinaryFirst=\"Sesuai nama program. Jika tidak, cari nama aplikasinya\"\nMatchAppNameFirst=\"Sesuai nama aplikasi. Jika tidak, cari nama programnya\"\nDevice=\"Perangkat\"\nDefault=\"Bawaan\"\nApplication=\"Aplikasi\"\nApplications=\"Aplikasi\"\nExceptApp=\"Tangkap semua aplikasi kecuali yang dipilih\"\nSelectedApps=\"Aplikasi pilihan\"\nAddToSelected=\"Tambahkan Pilihan\"\n"
  },
  {
    "path": "data/locale/pl-PL.ini",
    "content": "PipeWireAudioCaptureInput=\"Przechwytywanie wejścia dźwięku (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Przechwytywanie wyjścia dźwięku (PipeWire)\"\nPipeWireAudioCaptureApplication=\"Przechwytywanie dźwięku aplikacji (PipeWire)\"\nAppCaptureMode=\"Tryb Przechwytywania\"\nSingleApp=\"Jedna aplikacja\"\nMultipleApps=\"Wiele aplikacji\"\nMatchPriority=\"Dopasuj priorytet\"\nMatchBinaryFirst=\"Dopasuj przez nazwę pliku uruchamiąjącego, inaczej użyj nazwę aplikacji\"\nMatchAppNameFirst=\"Dopasuj przez nazwę aplikacji, inaczej użyj nazwę pliku uruchamiąjącego\"\nDevice=\"Urządzenie\"\nDefault=\"Domyślne\"\nApplication=\"Aplikacja\"\nApplications=\"Aplikacje\"\nExceptApp=\"Przechwytuj wszystkie aplikacje poza wybranymi\"\nSelectedApps=\"Wybranne Aplikacje\"\nAddToSelected=\"Dodaj do wybranych\"\n"
  },
  {
    "path": "data/locale/pt-BR.ini",
    "content": "PipeWireAudioCaptureInput=\"Captura de entrada de áudio (PipeWire)\"\nPipeWireAudioCaptureOutput=\"Captura de saída de áudio (PipeWire)\"\nPipeWireAudioCaptureApplication=\"Captura de áudio de aplicativo (PipeWire)\"\nAppCaptureMode=\"Modo de captura\"\nSingleApp=\"Aplicativo único\"\nMultipleApps=\"Múltiplos aplicativos\"\nMatchPriority=\"Prioridade de correspondência\"\nMatchBinaryFirst=\"Corresponder ao nome do executável, se falhar, ao nome do app\"\nMatchAppNameFirst=\"Corresponder ao nome do app, se falhar, ao nome do executável\"\nDevice=\"Dispositivo\"\nDefault=\"Padrão\"\nApplication=\"Aplicativo\"\nApplications=\"Aplicativos\"\nExceptApp=\"Capturar todos os apps, exceto o selecionado\"\nSelectedApps=\"Apps selecionados\"\nAddToSelected=\"Adicionar à seleção\"\n"
  },
  {
    "path": "src/linux-pipewire-audio.c",
    "content": "/* linux-pipewire-audio.c\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 2 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n *\n * SPDX-License-Identifier: GPL-2.0-or-later\n */\n\n#include <obs-module.h>\n\n#include <pipewire/pipewire.h>\n\n#include \"pipewire-audio.h\"\n\nOBS_DECLARE_MODULE()\nOBS_MODULE_USE_DEFAULT_LOCALE(\"linux-pipewire-audio\", \"en-US\")\nMODULE_EXPORT const char *obs_module_description(void)\n{\n\treturn \"PipeWire input, output and application audio capture\";\n}\n\nbool obs_module_load(void)\n{\n\tpw_init(NULL, NULL);\n\n\tpipewire_audio_capture_load();\n\tpipewire_audio_capture_app_load();\n\treturn true;\n}\n\nvoid obs_module_unload(void)\n{\n#if PW_CHECK_VERSION(0, 3, 49)\n\tpw_deinit();\n#endif\n}\n"
  },
  {
    "path": "src/pipewire-audio-capture-app.c",
    "content": "/* pipewire-audio-capture-app.c\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 2 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n *\n * SPDX-License-Identifier: GPL-2.0-or-later\n */\n\n#include \"pipewire-audio.h\"\n\n#include <spa/debug/types.h>\n\n#include <util/dstr.h>\n\n/* Source for capturing applciation audio using PipeWire */\n\nstruct target_node_port {\n\tconst char *channel;\n\tuint32_t id;\n};\n\nstruct target_node {\n\tconst char *name;\n\tconst char *app_name;\n\tconst char *binary;\n\tuint32_t client_id;\n\tuint32_t id;\n\tstruct obs_pw_audio_proxy_list ports;\n\tuint32_t *p_n_nodes;\n\n\tstruct spa_hook node_listener;\n};\n\nstruct target_client {\n\tconst char *app_name;\n\tconst char *binary;\n\tuint32_t id;\n\n\tstruct spa_hook client_listener;\n};\n\nstruct system_sink {\n\tconst char *name;\n\tuint32_t id;\n};\n\nstruct capture_sink_link {\n\tuint32_t id;\n};\n\nstruct capture_sink_port {\n\tconst char *channel;\n\tuint32_t id;\n};\n\nenum capture_mode { CAPTURE_MODE_SINGLE, CAPTURE_MODE_MULTIPLE };\nenum match_priority { MATCH_PRIORITY_BINARY_NAME, MATCH_PRIORITY_APP_NAME };\n\n#define SETTING_CAPTURE_MODE \"CaptureMode\"\n#define SETTING_MATCH_PRIORITY \"MatchPriorty\"\n#define SETTING_EXCLUDE_SELECTIONS \"ExceptApp\"\n#define SETTING_SELECTION_SINGLE \"TargetName\"\n#define SETTING_SELECTION_MULTIPLE \"apps\"\n#define SETTING_AVAILABLE_APPS \"AppToAdd\"\n#define SETTING_ADD_TO_SELECTIONS \"AddToSelected\"\n\n/** This source basically works like this:\n    - Keep track of output streams and their ports, system sinks and the default sink\n\n    - Keep track of the channels of the default system sink and create a new virtual sink,\n      destroying the previously made one, with the same channels, then connect the stream to it\n\n    - Connect any registered or new stream ports to the sink\n*/\nstruct obs_pw_audio_capture_app {\n\tobs_source_t *source;\n\n\tstruct obs_pw_audio_instance pw;\n\n\t/** The app capture sink automatically mixes\n\t  * the audio of all the app streams */\n\tstruct {\n\t\tstruct pw_proxy *proxy;\n\t\tstruct spa_hook proxy_listener;\n\t\tbool autoconnect_targets;\n\t\tuint32_t id;\n\t\tuint32_t serial;\n\t\tuint32_t channels;\n\t\tstruct dstr position;\n\t\tDARRAY(struct capture_sink_port) ports;\n\n\t\t/* Links between app streams and the capture sink */\n\t\tstruct obs_pw_audio_proxy_list links;\n\t} sink;\n\n\t/** Need the default system sink to create\n\t  * the app capture sink with the same audio channels */\n\tstruct obs_pw_audio_proxy_list system_sinks;\n\tstruct {\n\t\tstruct obs_pw_audio_default_node_metadata metadata;\n\t\tstruct pw_proxy *proxy;\n\t\tstruct spa_hook node_listener;\n\t\tstruct spa_hook proxy_listener;\n\t} default_sink;\n\n\tstruct obs_pw_audio_proxy_list clients;\n\n\tstruct obs_pw_audio_proxy_list nodes;\n\tuint32_t n_nodes;\n\n\tenum capture_mode capture_mode;\n\tenum match_priority match_priority;\n\tbool except;\n\tDARRAY(const char *) selections;\n};\n\n/* System sinks */\nstatic void system_sink_destroy_cb(void *data)\n{\n\tstruct system_sink *s = data;\n\tbfree((void *)s->name);\n}\n\nstatic void register_system_sink(struct obs_pw_audio_capture_app *pwac, uint32_t global_id, const char *name)\n{\n\tstruct pw_proxy *sink_proxy = pw_registry_bind(pwac->pw.registry, global_id, PW_TYPE_INTERFACE_Node,\n\t\t\t\t\t\t       PW_VERSION_NODE, sizeof(struct system_sink));\n\tif (!sink_proxy) {\n\t\treturn;\n\t}\n\n\tstruct system_sink *sink = pw_proxy_get_user_data(sink_proxy);\n\tsink->name = bstrdup(name);\n\tsink->id = global_id;\n\n\tobs_pw_audio_proxy_list_append(&pwac->system_sinks, sink_proxy);\n}\n/* ------------------------------------------------- */\n\n/* Target clients */\nstatic void client_destroy_cb(void *data)\n{\n\tstruct target_client *client = data;\n\tbfree((void *)client->app_name);\n\tbfree((void *)client->binary);\n\n\tspa_hook_remove(&client->client_listener);\n}\n\nstatic void on_client_info_cb(void *data, const struct pw_client_info *info)\n{\n\tif ((info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS) == 0 || !info->props || !info->props->n_items) {\n\t\treturn;\n\t}\n\n\tconst char *binary = spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_BINARY);\n\tif (!binary) {\n\t\treturn;\n\t}\n\n\tstruct target_client *client = data;\n\tbfree((void *)client->binary);\n\tclient->binary = bstrdup(binary);\n}\n\nstatic const struct pw_client_events client_events = {\n\tPW_VERSION_CLIENT_EVENTS,\n\t.info = on_client_info_cb,\n};\n\nstatic void register_target_client(struct obs_pw_audio_capture_app *pwac, uint32_t global_id, const char *app_name)\n{\n\tstruct pw_proxy *client_proxy = pw_registry_bind(pwac->pw.registry, global_id, PW_TYPE_INTERFACE_Client,\n\t\t\t\t\t\t\t PW_VERSION_CLIENT, sizeof(struct target_client));\n\tif (!client_proxy) {\n\t\treturn;\n\t}\n\n\tstruct target_client *client = pw_proxy_get_user_data(client_proxy);\n\tclient->binary = NULL;\n\tclient->app_name = bstrdup(app_name);\n\tclient->id = global_id;\n\n\tobs_pw_audio_proxy_list_append(&pwac->clients, client_proxy);\n\tpw_proxy_add_object_listener(client_proxy, &client->client_listener, &client_events, client);\n}\n\n/* Target nodes and ports */\nstatic void port_destroy_cb(void *data)\n{\n\tstruct target_node_port *p = data;\n\tbfree((void *)p->channel);\n}\n\nstatic void node_destroy_cb(void *data)\n{\n\tstruct target_node *node = data;\n\n\tspa_hook_remove(&node->node_listener);\n\n\tobs_pw_audio_proxy_list_clear(&node->ports);\n\n\t(*node->p_n_nodes)--;\n\n\tbfree((void *)node->binary);\n\tbfree((void *)node->app_name);\n\tbfree((void *)node->name);\n}\n\nstatic struct target_node_port *node_register_port(struct target_node *node, uint32_t global_id,\n\t\t\t\t\t\t   struct pw_registry *registry, const char *channel)\n{\n\tstruct pw_proxy *port_proxy = pw_registry_bind(registry, global_id, PW_TYPE_INTERFACE_Port, PW_VERSION_PORT,\n\t\t\t\t\t\t       sizeof(struct target_node_port));\n\tif (!port_proxy) {\n\t\treturn NULL;\n\t}\n\n\tstruct target_node_port *port = pw_proxy_get_user_data(port_proxy);\n\tport->channel = bstrdup(channel);\n\tport->id = global_id;\n\n\tobs_pw_audio_proxy_list_append(&node->ports, port_proxy);\n\n\treturn port;\n}\n\nstatic void on_node_info_cb(void *data, const struct pw_node_info *info)\n{\n\tif ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) == 0 || !info->props || !info->props->n_items) {\n\t\treturn;\n\t}\n\n\tconst char *binary = spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_BINARY);\n\tif (!binary) {\n\t\treturn;\n\t}\n\n\tstruct target_node *node = data;\n\tbfree((void *)node->binary);\n\tnode->binary = bstrdup(binary);\n}\n\nstatic const struct pw_node_events node_events = {\n\tPW_VERSION_NODE_EVENTS,\n\t.info = on_node_info_cb,\n};\n\nstatic void register_target_node(struct obs_pw_audio_capture_app *pwac, uint32_t global_id, uint32_t client_id,\n\t\t\t\t const char *app_name, const char *name)\n{\n\tstruct pw_proxy *node_proxy = pw_registry_bind(pwac->pw.registry, global_id, PW_TYPE_INTERFACE_Node,\n\t\t\t\t\t\t       PW_VERSION_NODE, sizeof(struct target_node));\n\tif (!node_proxy) {\n\t\treturn;\n\t}\n\n\tstruct target_node *node = pw_proxy_get_user_data(node_proxy);\n\tnode->name = bstrdup(name);\n\tnode->app_name = bstrdup(app_name);\n\tnode->binary = NULL;\n\tnode->id = global_id;\n\tnode->client_id = client_id;\n\tnode->p_n_nodes = &pwac->n_nodes;\n\tobs_pw_audio_proxy_list_init(&node->ports, NULL, port_destroy_cb);\n\n\tpwac->n_nodes++;\n\n\tobs_pw_audio_proxy_list_append(&pwac->nodes, node_proxy);\n\tpw_proxy_add_object_listener(node_proxy, &node->node_listener, &node_events, node);\n}\n\nstatic bool node_is_targeted(struct obs_pw_audio_capture_app *pwac, struct target_node *node)\n{\n\tbool targeted = false;\n\tfor (size_t i = 0; i < pwac->selections.num && !targeted; i++) {\n\t\tconst char *selection = pwac->selections.array[i];\n\n\t\ttargeted = (astrcmpi(selection, node->binary) == 0 || astrcmpi(selection, node->app_name) == 0 ||\n\t\t\t    astrcmpi(selection, node->name) == 0);\n\n\t\tif (!targeted && node->client_id) {\n\t\t\tstruct obs_pw_audio_proxy_list_iter iter;\n\t\t\tobs_pw_audio_proxy_list_iter_init(&iter, &pwac->clients);\n\n\t\t\tstruct target_client *client;\n\t\t\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&client)) {\n\t\t\t\tif (client->id == node->client_id) {\n\t\t\t\t\ttargeted = (astrcmpi(selection, client->binary) == 0 ||\n\t\t\t\t\t\t    astrcmpi(selection, client->app_name) == 0);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn targeted ^ pwac->except;\n}\n/* ------------------------------------------------- */\n\n/* App streams <-> Capture sink links */\nstatic void link_bound_cb(void *data, uint32_t global_id)\n{\n\tstruct capture_sink_link *link = data;\n\tlink->id = global_id;\n}\n\nstatic void link_destroy_cb(void *data)\n{\n\tstruct capture_sink_link *link = data;\n\tblog(LOG_DEBUG, \"[pipewire-audio] Link %u destroyed\", link->id);\n}\n\nstatic void link_port_to_sink(struct obs_pw_audio_capture_app *pwac, struct target_node_port *port, uint32_t node_id)\n{\n\tblog(LOG_DEBUG, \"[pipewire-audio] Connecting port %u of node %u to app capture sink\", port->id, node_id);\n\n\tuint32_t p = 0;\n\tif (pwac->sink.channels == 1 && /* Mono capture sink */\n\t    pwac->sink.ports.num >= 1) {\n\t\tp = pwac->sink.ports.array[0].id;\n\t} else {\n\t\tfor (size_t i = 0; i < pwac->sink.ports.num; i++) {\n\t\t\tif (astrcmpi(pwac->sink.ports.array[i].channel, port->channel) == 0) {\n\t\t\t\tp = pwac->sink.ports.array[i].id;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!p) {\n\t\tblog(LOG_WARNING,\n\t\t     \"[pipewire-audio] Could not connect port %u of node %u to app capture sink. No port of app capture sink has channel %s\",\n\t\t     port->id, node_id, port->channel);\n\t\treturn;\n\t}\n\n\tstruct pw_properties *link_props = pw_properties_new(PW_KEY_OBJECT_LINGER, \"false\", NULL);\n\n\tpw_properties_setf(link_props, PW_KEY_LINK_OUTPUT_NODE, \"%u\", node_id);\n\tpw_properties_setf(link_props, PW_KEY_LINK_OUTPUT_PORT, \"%u\", port->id);\n\n\tpw_properties_setf(link_props, PW_KEY_LINK_INPUT_NODE, \"%u\", pwac->sink.id);\n\tpw_properties_setf(link_props, PW_KEY_LINK_INPUT_PORT, \"%u\", p);\n\n\tstruct pw_proxy *link_proxy = pw_core_create_object(pwac->pw.core, \"link-factory\", PW_TYPE_INTERFACE_Link,\n\t\t\t\t\t\t\t    PW_VERSION_LINK, &link_props->dict,\n\t\t\t\t\t\t\t    sizeof(struct capture_sink_link));\n\n\tpw_properties_free(link_props);\n\n\tif (!link_proxy) {\n\t\tblog(LOG_WARNING, \"[pipewire-audio] Could not connect port %u of node %u to app capture sink\", port->id,\n\t\t     node_id);\n\t\treturn;\n\t}\n\n\tstruct capture_sink_link *link = pw_proxy_get_user_data(link_proxy);\n\tlink->id = SPA_ID_INVALID;\n\n\tobs_pw_audio_proxy_list_append(&pwac->sink.links, link_proxy);\n}\n\nstatic void link_node_to_sink(struct obs_pw_audio_capture_app *pwac, struct target_node *node)\n{\n\tstruct obs_pw_audio_proxy_list_iter iter;\n\tobs_pw_audio_proxy_list_iter_init(&iter, &node->ports);\n\n\tstruct target_node_port *port;\n\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&port)) {\n\t\tlink_port_to_sink(pwac, port, node->id);\n\t}\n}\n/* ------------------------------------------------- */\n\n/* App capture sink */\n\n/** The app capture sink is created when there\n  * is info about the system's default sink.\n  * See the on_metadata and on_default_sink callbacks */\nstatic void destroy_sink_links(struct obs_pw_audio_capture_app *pwac)\n{\n\tobs_pw_audio_proxy_list_clear(&pwac->sink.links);\n}\n\nstatic void connect_targets(struct obs_pw_audio_capture_app *pwac)\n{\n\tif (!pwac->sink.proxy) {\n\t\treturn;\n\t}\n\n\tdestroy_sink_links(pwac);\n\n\tif (pwac->selections.num == 0) {\n\t\treturn;\n\t}\n\n\tstruct obs_pw_audio_proxy_list_iter iter;\n\tobs_pw_audio_proxy_list_iter_init(&iter, &pwac->nodes);\n\n\tstruct target_node *node;\n\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {\n\t\tif (node_is_targeted(pwac, node)) {\n\t\t\tlink_node_to_sink(pwac, node);\n\t\t}\n\t}\n}\n\nstatic void finalize_capture_sink(struct obs_pw_audio_capture_app *pwac)\n{\n\tif (!pwac->sink.proxy || pwac->sink.id == SPA_ID_INVALID || pwac->sink.serial == SPA_ID_INVALID ||\n\t    pwac->sink.ports.num != pwac->sink.channels) {\n\t\treturn;\n\t}\n\n\tblog(LOG_DEBUG, \"[pipewire-audio] App capture sink ready\");\n\n\tconnect_targets(pwac);\n\n\tpwac->sink.autoconnect_targets = true;\n\n\tif (obs_pw_audio_stream_connect(&pwac->pw.audio, pwac->sink.id, pwac->sink.serial, pwac->sink.channels) < 0) {\n\t\tblog(LOG_WARNING, \"[pipewire-audio] Error connecting stream %p to app capture sink %u\",\n\t\t     pwac->pw.audio.stream, pwac->sink.id);\n\t}\n}\n\nstatic void on_sink_proxy_bound_cb(void *data, uint32_t global_id)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\tpwac->sink.id = global_id;\n\tda_init(pwac->sink.ports);\n}\n\nstatic void on_sink_proxy_removed_cb(void *data)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\tblog(LOG_WARNING, \"[pipewire-audio] App capture sink %u has been destroyed by the PipeWire remote\",\n\t     pwac->sink.id);\n\tpw_proxy_destroy(pwac->sink.proxy);\n}\n\nstatic void on_sink_proxy_destroy_cb(void *data)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tspa_hook_remove(&pwac->sink.proxy_listener);\n\tspa_zero(pwac->sink.proxy_listener);\n\n\tfor (size_t i = 0; i < pwac->sink.ports.num; i++) {\n\t\tstruct capture_sink_port *p = &pwac->sink.ports.array[i];\n\t\tbfree((void *)p->channel);\n\t}\n\tda_free(pwac->sink.ports);\n\n\tpwac->sink.channels = 0;\n\tdstr_free(&pwac->sink.position);\n\n\tpwac->sink.autoconnect_targets = false;\n\tpwac->sink.proxy = NULL;\n\n\tblog(LOG_DEBUG, \"[pipewire-audio] App capture sink %u destroyed\", pwac->sink.id);\n\n\tpwac->sink.id = SPA_ID_INVALID;\n}\n\nstatic void on_sink_proxy_error_cb(void *data, int seq, int res, const char *message)\n{\n\tUNUSED_PARAMETER(data);\n\tblog(LOG_ERROR, \"[pipewire-audio] App capture sink error: seq:%d res:%d :%s\", seq, res, message);\n}\n\nstatic const struct pw_proxy_events sink_proxy_events = {\n\tPW_VERSION_PROXY_EVENTS,\n\t.bound = on_sink_proxy_bound_cb,\n\t.removed = on_sink_proxy_removed_cb,\n\t.destroy = on_sink_proxy_destroy_cb,\n\t.error = on_sink_proxy_error_cb,\n};\n\nstatic void register_capture_sink_port(struct obs_pw_audio_capture_app *pwac, uint32_t global_id, const char *channel)\n{\n\tblog(LOG_DEBUG, \"[pipewire-audio] Registering app capture sink port %u\", global_id);\n\n\tstruct capture_sink_port *port = da_push_back_new(pwac->sink.ports);\n\tport->channel = bstrdup(channel);\n\tport->id = global_id;\n\n\tfinalize_capture_sink(pwac);\n}\n\nstatic void make_capture_sink(struct obs_pw_audio_capture_app *pwac, uint32_t channels, const char *position)\n{\n\tstruct pw_properties *sink_props = pw_properties_new(PW_KEY_FACTORY_NAME, \"support.null-audio-sink\",\n\t\t\t\t\t\t\t     PW_KEY_MEDIA_CLASS, \"Stream/Input/Audio\",\n\t\t\t\t\t\t\t     PW_KEY_NODE_VIRTUAL, \"true\", SPA_KEY_AUDIO_POSITION,\n\t\t\t\t\t\t\t     position, NULL);\n\n\tpw_properties_setf(sink_props, PW_KEY_NODE_NAME, \"OBS: %s\", obs_source_get_name(pwac->source));\n\n\tpw_properties_setf(sink_props, PW_KEY_AUDIO_CHANNELS, \"%u\", channels);\n\n\tpwac->sink.proxy = pw_core_create_object(pwac->pw.core, \"adapter\", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE,\n\t\t\t\t\t\t &sink_props->dict, 0);\n\n\tpw_properties_free(sink_props);\n\n\tif (!pwac->sink.proxy) {\n\t\tblog(LOG_WARNING, \"[pipewire-audio] Failed to create app capture sink\");\n\t\treturn;\n\t}\n\n\tpwac->sink.channels = channels;\n\tdstr_copy(&pwac->sink.position, position);\n\n\tpwac->sink.id = SPA_ID_INVALID;\n\tpwac->sink.serial = SPA_ID_INVALID;\n\n\tpw_proxy_add_listener(pwac->sink.proxy, &pwac->sink.proxy_listener, &sink_proxy_events, pwac);\n\n\tblog(LOG_DEBUG, \"[pipewire-audio] Created app capture sink\");\n}\n\nstatic void destroy_capture_sink(struct obs_pw_audio_capture_app *pwac)\n{\n\t/* Links are automatically destroyed by PipeWire */\n\n\tif (!pwac->sink.proxy) {\n\t\treturn;\n\t}\n\n\tif (pw_stream_get_state(pwac->pw.audio.stream, NULL) != PW_STREAM_STATE_UNCONNECTED) {\n\t\tpw_stream_disconnect(pwac->pw.audio.stream);\n\t}\n\n\tpwac->sink.autoconnect_targets = false;\n\tpw_proxy_destroy(pwac->sink.proxy);\n}\n/* ------------------------------------------------- */\n\n/* Default system sink */\nstatic void on_default_sink_param_cb(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,\n\t\t\t\t     const struct spa_pod *param)\n{\n\tUNUSED_PARAMETER(seq);\n\tUNUSED_PARAMETER(index);\n\tUNUSED_PARAMETER(next);\n\n\tif (id != SPA_PARAM_EnumFormat) {\n\t\treturn;\n\t}\n\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tuint32_t media_type = 0, media_subtype = 0, parsed_id = 0, channels = 0;\n\tstruct spa_pod *position_pod = NULL;\n\n\tstruct spa_pod_parser p;\n\tspa_pod_parser_pod(&p, param);\n\n\tspa_pod_parser_get_object(&p, SPA_TYPE_OBJECT_Format, &parsed_id, SPA_FORMAT_mediaType, SPA_POD_Id(&media_type),\n\t\t\t\t  SPA_FORMAT_mediaSubtype, SPA_POD_Id(&media_subtype), SPA_FORMAT_AUDIO_channels,\n\t\t\t\t  SPA_POD_OPT_Int(&channels), SPA_FORMAT_AUDIO_position,\n\t\t\t\t  SPA_POD_OPT_Pod(&position_pod));\n\n\tif (pwac->sink.channels && !channels) {\n\t\t// It's likely we got the channels from a proper format already\n\t\treturn;\n\t}\n\n\tif (parsed_id != SPA_PARAM_EnumFormat || media_type != SPA_MEDIA_TYPE_audio || !channels || !position_pod) {\n\t\tgoto stereo_fallback;\n\t}\n\n\tuint32_t position_n = 0;\n\tuint32_t *position_arr = spa_pod_get_array(position_pod, &position_n);\n\n\tstruct dstr position_str;\n\tdstr_init(&position_str);\n\n\tfor (size_t i = 0; i < position_n; i++) {\n\t\tconst char *chn = spa_debug_type_find_short_name(spa_type_audio_channel, position_arr[i]);\n\n\t\tif (strstr(chn, \"AUX\") != NULL) {\n\t\t\t// Sink is configured for pro audio, use stereo\n\t\t\t// https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#what-is-the-pro-audio-profile\n\t\t\tchannels = 2;\n\t\t\tdstr_copy(&position_str, \"FL,FR\");\n\t\t\tbreak;\n\t\t}\n\n\t\tdstr_cat(&position_str, chn);\n\n\t\tif (position_n - 1 != i) {\n\t\t\tdstr_cat_ch(&position_str, ',');\n\t\t}\n\t}\n\n\tif (channels != pwac->sink.channels || dstr_cmpi(&position_str, pwac->sink.position.array) != 0) {\n\t\tdestroy_capture_sink(pwac);\n\t\tmake_capture_sink(pwac, channels, position_str.array);\n\t}\n\n\tdstr_free(&position_str);\n\treturn;\n\nstereo_fallback:\n\tif (pwac->sink.proxy) {\n\t\treturn;\n\t}\n\n\tblog(LOG_WARNING, \"[pipewire-audio] Could not parse format of default sink. Falling back to stereo.\");\n\n\tdestroy_capture_sink(pwac);\n\tmake_capture_sink(pwac, 2, \"[FL,FR]\");\n}\n\nstatic const struct pw_node_events default_sink_events = {\n\tPW_VERSION_NODE_EVENTS,\n\t.param = on_default_sink_param_cb,\n};\n\nstatic void on_default_sink_proxy_removed_cb(void *data)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\tpw_proxy_destroy(pwac->default_sink.proxy);\n}\n\nstatic void on_default_sink_proxy_destroy_cb(void *data)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\tspa_hook_remove(&pwac->default_sink.node_listener);\n\tspa_zero(pwac->default_sink.node_listener);\n\n\tspa_hook_remove(&pwac->default_sink.proxy_listener);\n\tspa_zero(pwac->default_sink.proxy_listener);\n\n\tpwac->default_sink.proxy = NULL;\n}\n\nstatic const struct pw_proxy_events default_sink_proxy_events = {\n\tPW_VERSION_PROXY_EVENTS,\n\t.removed = on_default_sink_proxy_removed_cb,\n\t.destroy = on_default_sink_proxy_destroy_cb,\n};\n\nstatic void default_node_cb(void *data, const char *name)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tblog(LOG_DEBUG, \"[pipewire-audio] New default sink %s\", name);\n\n\t/* Find the new default sink and bind to it to get its channel info */\n\tstruct obs_pw_audio_proxy_list_iter iter;\n\tobs_pw_audio_proxy_list_iter_init(&iter, &pwac->system_sinks);\n\n\tstruct system_sink *temp, *default_sink = NULL;\n\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&temp)) {\n\t\tif (strcmp(name, temp->name) == 0) {\n\t\t\tdefault_sink = temp;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (!default_sink) {\n\t\treturn;\n\t}\n\n\tif (pwac->default_sink.proxy) {\n\t\tpw_proxy_destroy(pwac->default_sink.proxy);\n\t}\n\n\tpwac->default_sink.proxy =\n\t\tpw_registry_bind(pwac->pw.registry, default_sink->id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);\n\tif (!pwac->default_sink.proxy) {\n\t\tif (!pwac->sink.proxy) {\n\t\t\tblog(LOG_WARNING,\n\t\t\t     \"[pipewire-audio] Failed to get default sink info, app capture sink defaulting to stereo\");\n\t\t\tmake_capture_sink(pwac, 2, \"FL,FR\");\n\t\t}\n\t\treturn;\n\t}\n\n\tpw_proxy_add_object_listener(pwac->default_sink.proxy, &pwac->default_sink.node_listener, &default_sink_events,\n\t\t\t\t     pwac);\n\tpw_proxy_add_listener(pwac->default_sink.proxy, &pwac->default_sink.proxy_listener, &default_sink_proxy_events,\n\t\t\t      pwac);\n\n\tpw_node_subscribe_params((struct pw_node *)pwac->default_sink.proxy, (uint32_t[]){SPA_PARAM_EnumFormat}, 1);\n}\n/* ------------------------------------------------- */\n\n/* Registry */\nstatic void on_global_cb(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version,\n\t\t\t const struct spa_dict *props)\n{\n\tUNUSED_PARAMETER(permissions);\n\tUNUSED_PARAMETER(version);\n\n\tif (!props || !type) {\n\t\treturn;\n\t}\n\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tif (id == pwac->sink.id) {\n\t\tconst char *ser = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL);\n\t\tif (!ser) {\n\t\t\tblog(LOG_ERROR, \"[pipewire-audio] No object serial found on app capture sink %u\", id);\n\t\t\tpwac->sink.serial = 0;\n\t\t} else {\n\t\t\tpwac->sink.serial = strtoul(ser, NULL, 10);\n\t\t\tfinalize_capture_sink(pwac);\n\t\t}\n\t}\n\n\tif (strcmp(type, PW_TYPE_INTERFACE_Port) == 0) {\n\t\tconst char *nid, *dir, *chn;\n\t\tif (!(nid = spa_dict_lookup(props, PW_KEY_NODE_ID)) ||\n\t\t    !(dir = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION)) ||\n\t\t    !(chn = spa_dict_lookup(props, PW_KEY_AUDIO_CHANNEL))) {\n\t\t\treturn;\n\t\t}\n\n\t\tuint32_t node_id = strtoul(nid, NULL, 10);\n\n\t\tif (astrcmpi(dir, \"in\") == 0 && node_id == pwac->sink.id) {\n\t\t\tregister_capture_sink_port(pwac, id, chn);\n\t\t} else if (astrcmpi(dir, \"out\") == 0) {\n\t\t\t/* Possibly a target port */\n\t\t\tstruct obs_pw_audio_proxy_list_iter iter;\n\t\t\tobs_pw_audio_proxy_list_iter_init(&iter, &pwac->nodes);\n\n\t\t\tstruct target_node *temp, *node = NULL;\n\t\t\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&temp)) {\n\t\t\t\tif (temp->id == node_id) {\n\t\t\t\t\tnode = temp;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!node) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tstruct target_node_port *port = node_register_port(node, id, pwac->pw.registry, chn);\n\n\t\t\tif (port && pwac->sink.autoconnect_targets && node_is_targeted(pwac, node)) {\n\t\t\t\tlink_port_to_sink(pwac, port, node->id);\n\t\t\t}\n\t\t}\n\t} else if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {\n\t\tconst char *node_name, *media_class;\n\t\tif (!(node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) ||\n\t\t    !(media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS))) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (strcmp(media_class, \"Stream/Output/Audio\") == 0) {\n\t\t\t/* Target node */\n\t\t\tconst char *node_app_name = spa_dict_lookup(props, PW_KEY_APP_NAME);\n\t\t\tif (!node_app_name) {\n\t\t\t\tnode_app_name = node_name;\n\t\t\t}\n\n\t\t\tuint32_t client_id = 0;\n\t\t\tconst char *client_id_str = spa_dict_lookup(props, PW_KEY_CLIENT_ID);\n\t\t\tif (client_id_str) {\n\t\t\t\tclient_id = strtoul(client_id_str, NULL, 10);\n\t\t\t}\n\n\t\t\tregister_target_node(pwac, id, client_id, node_app_name, node_name);\n\t\t} else if (strcmp(media_class, \"Audio/Sink\") == 0) {\n\t\t\tregister_system_sink(pwac, id, node_name);\n\t\t}\n\n\t} else if (strcmp(type, PW_TYPE_INTERFACE_Client) == 0) {\n\t\tconst char *client_app_name = spa_dict_lookup(props, PW_KEY_APP_NAME);\n\t\tregister_target_client(pwac, id, client_app_name);\n\t} else if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) {\n\t\tconst char *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME);\n\t\tif (!name || strcmp(name, \"default\") != 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!obs_pw_audio_default_node_metadata_listen(&pwac->default_sink.metadata, &pwac->pw, id, true,\n\t\t\t\t\t\t\t       default_node_cb, pwac) &&\n\t\t    !pwac->sink.proxy) {\n\t\t\tblog(LOG_WARNING,\n\t\t\t     \"[pipewire-audio] Failed to get default metadata, app capture sink defaulting to stereo\");\n\t\t\tmake_capture_sink(pwac, 2, \"FL,FR\");\n\t\t}\n\t}\n}\n\nstatic const struct pw_registry_events registry_events = {\n\tPW_VERSION_REGISTRY_EVENTS,\n\t.global = on_global_cb,\n};\n/* ------------------------------------------------- */\n\n/* Source */\nstatic bool add_app_clicked(obs_properties_t *properties, obs_property_t *property, void *data)\n{\n\tUNUSED_PARAMETER(properties);\n\tUNUSED_PARAMETER(property);\n\n\tobs_source_t *source = data;\n\n\tobs_data_t *settings = obs_source_get_settings(source);\n\tconst char *app_to_add = obs_data_get_string(settings, SETTING_AVAILABLE_APPS);\n\n\tobs_data_array_t *selections = obs_data_get_array(settings, SETTING_SELECTION_MULTIPLE);\n\tif (obs_data_array_count(selections) == 0) {\n\t\tobs_data_array_release(selections);\n\n\t\tselections = obs_data_array_create();\n\t\tobs_data_set_array(settings, SETTING_SELECTION_MULTIPLE, selections);\n\t}\n\n\t/* Don't add if selection is already in the list */\n\n\tbool should_add = true;\n\tfor (size_t i = 0; i < obs_data_array_count(selections) && should_add; i++) {\n\t\tobs_data_t *item = obs_data_array_item(selections, i);\n\n\t\tshould_add = astrcmpi(obs_data_get_string(item, \"value\"), app_to_add) != 0;\n\n\t\tobs_data_release(item);\n\t}\n\n\tif (should_add) {\n\t\tobs_data_t *new_entry = obs_data_create();\n\t\tobs_data_set_bool(new_entry, \"hidden\", false);\n\t\tobs_data_set_bool(new_entry, \"selected\", false);\n\t\tobs_data_set_string(new_entry, \"value\", app_to_add);\n\n\t\tobs_data_array_push_back(selections, new_entry);\n\n\t\tobs_data_release(new_entry);\n\n\t\tobs_source_update(source, settings);\n\t}\n\n\tobs_data_array_release(selections);\n\tobs_data_release(settings);\n\n\treturn should_add;\n}\n\nstatic int cmp_targets(const void *a, const void *b)\n{\n\tconst char *a_str = *(char **)a;\n\tconst char *b_str = *(char **)b;\n\treturn strcmp(a_str, b_str);\n}\n\nstatic const char *choose_display_string(struct obs_pw_audio_capture_app *pwac, const char *binary,\n\t\t\t\t\t const char *app_name)\n{\n\tswitch (pwac->match_priority) {\n\tcase MATCH_PRIORITY_BINARY_NAME:\n\t\treturn binary ? binary : app_name;\n\tcase MATCH_PRIORITY_APP_NAME:\n\t\treturn app_name ? app_name : binary;\n\tdefault:\n\t\treturn NULL;\n\t}\n}\n\nstatic void populate_avaiable_apps_list(obs_property_t *list, struct obs_pw_audio_capture_app *pwac)\n{\n\tDARRAY(const char *) targets;\n\tda_init(targets);\n\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\n\tda_reserve(targets, pwac->n_nodes);\n\n\tstruct obs_pw_audio_proxy_list_iter iter;\n\tobs_pw_audio_proxy_list_iter_init(&iter, &pwac->nodes);\n\n\tstruct target_node *node;\n\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {\n\t\tconst char *display = choose_display_string(pwac, node->binary, node->app_name);\n\n\t\tif (!display) {\n\t\t\tdisplay = node->name;\n\t\t}\n\n\t\tda_push_back(targets, &display);\n\t}\n\n\tobs_pw_audio_proxy_list_iter_init(&iter, &pwac->clients);\n\n\tstruct target_client *client;\n\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&client)) {\n\t\tconst char *display = choose_display_string(pwac, client->binary, client->app_name);\n\n\t\tif (display) {\n\t\t\tda_push_back(targets, &display);\n\t\t}\n\t}\n\n\t/* Show just one entry per target */\n\n\tqsort(targets.array, targets.num, sizeof(const char *), cmp_targets);\n\n\tfor (size_t i = 0; i < targets.num; i++) {\n\t\tif (i == 0 || strcmp(targets.array[i - 1], targets.array[i]) != 0) {\n\t\t\tobs_property_list_add_string(list, targets.array[i], targets.array[i]);\n\t\t}\n\t}\n\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n\n\tda_free(targets);\n}\n\nstatic bool capture_mode_modified(void *data, obs_properties_t *properties, obs_property_t *property,\n\t\t\t\t  obs_data_t *settings)\n{\n\tUNUSED_PARAMETER(property);\n\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tenum capture_mode mode = obs_data_get_int(settings, SETTING_CAPTURE_MODE);\n\n\tswitch (mode) {\n\tcase CAPTURE_MODE_SINGLE: {\n\t\tobs_properties_remove_by_name(properties, SETTING_SELECTION_MULTIPLE);\n\t\tobs_properties_remove_by_name(properties, SETTING_AVAILABLE_APPS);\n\t\tobs_properties_remove_by_name(properties, SETTING_ADD_TO_SELECTIONS);\n\n\t\tobs_property_t *available_apps =\n\t\t\tobs_properties_add_list(properties, SETTING_SELECTION_SINGLE, obs_module_text(\"Application\"),\n\t\t\t\t\t\tOBS_COMBO_TYPE_EDITABLE, OBS_COMBO_FORMAT_STRING);\n\n\t\tpopulate_avaiable_apps_list(available_apps, pwac);\n\n\t\tbreak;\n\t}\n\tcase CAPTURE_MODE_MULTIPLE: {\n\t\tobs_properties_remove_by_name(properties, SETTING_SELECTION_SINGLE);\n\n\t\tobs_properties_add_editable_list(properties, SETTING_SELECTION_MULTIPLE,\n\t\t\t\t\t\t obs_module_text(\"SelectedApps\"), OBS_EDITABLE_LIST_TYPE_STRINGS, NULL,\n\t\t\t\t\t\t NULL);\n\n\t\tobs_property_t *available_apps = obs_properties_add_list(properties, SETTING_AVAILABLE_APPS,\n\t\t\t\t\t\t\t\t\t obs_module_text(\"Applications\"),\n\t\t\t\t\t\t\t\t\t OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);\n\n\t\tpopulate_avaiable_apps_list(available_apps, pwac);\n\n\t\tobs_properties_add_button2(properties, SETTING_ADD_TO_SELECTIONS, obs_module_text(\"AddToSelected\"),\n\t\t\t\t\t   add_app_clicked, pwac->source);\n\n\t\tbreak;\n\t}\n\t}\n\n\treturn true;\n}\n\nstatic bool match_priority_modified(void *data, obs_properties_t *properties, obs_property_t *property,\n\t\t\t\t    obs_data_t *settings)\n{\n\tUNUSED_PARAMETER(property);\n\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tenum capture_mode mode = obs_data_get_int(settings, SETTING_CAPTURE_MODE);\n\n\tobs_property_t *targets = NULL;\n\tswitch (mode) {\n\tdefault:\n\tcase CAPTURE_MODE_SINGLE:\n\t\ttargets = obs_properties_get(properties, SETTING_SELECTION_SINGLE);\n\t\tbreak;\n\tcase CAPTURE_MODE_MULTIPLE:\n\t\ttargets = obs_properties_get(properties, SETTING_AVAILABLE_APPS);\n\t\tbreak;\n\t}\n\n\tif (targets == NULL) {\n\t\treturn false;\n\t}\n\n\tobs_property_list_clear(targets);\n\n\tpopulate_avaiable_apps_list(targets, pwac);\n\n\treturn true;\n}\n\nstatic void build_selections(struct obs_pw_audio_capture_app *pwac, obs_data_t *settings)\n{\n\tswitch (pwac->capture_mode) {\n\tcase CAPTURE_MODE_SINGLE: {\n\t\tconst char *selection = bstrdup(obs_data_get_string(settings, SETTING_SELECTION_SINGLE));\n\t\tda_push_back(pwac->selections, &selection);\n\t\tbreak;\n\t}\n\tcase CAPTURE_MODE_MULTIPLE: {\n\t\tobs_data_array_t *selections = obs_data_get_array(settings, SETTING_SELECTION_MULTIPLE);\n\t\tfor (size_t i = 0; i < obs_data_array_count(selections); i++) {\n\t\t\tobs_data_t *item = obs_data_array_item(selections, i);\n\n\t\t\tconst char *selection = bstrdup(obs_data_get_string(item, \"value\"));\n\t\t\tda_push_back(pwac->selections, &selection);\n\n\t\t\tobs_data_release(item);\n\t\t}\n\t\tobs_data_array_release(selections);\n\t\tbreak;\n\t}\n\t}\n}\n\nstatic void clear_selections(struct obs_pw_audio_capture_app *pwac)\n{\n\tfor (size_t i = 0; i < pwac->selections.num; i++) {\n\t\tconst char *selection = pwac->selections.array[i];\n\t\tbfree((void *)selection);\n\t}\n\n\tpwac->selections.num = 0;\n}\n\nstatic void *pipewire_audio_capture_app_create(obs_data_t *settings, obs_source_t *source)\n{\n\tstruct obs_pw_audio_capture_app *pwac = bzalloc(sizeof(struct obs_pw_audio_capture_app));\n\n\tif (!obs_pw_audio_instance_init(&pwac->pw, &registry_events, pwac, true, false, source)) {\n\t\tobs_pw_audio_instance_destroy(&pwac->pw);\n\n\t\tbfree(pwac);\n\t\treturn NULL;\n\t}\n\n\tpwac->source = source;\n\n\tobs_pw_audio_proxy_list_init(&pwac->nodes, NULL, node_destroy_cb);\n\tobs_pw_audio_proxy_list_init(&pwac->clients, NULL, client_destroy_cb);\n\tobs_pw_audio_proxy_list_init(&pwac->sink.links, link_bound_cb, link_destroy_cb);\n\tobs_pw_audio_proxy_list_init(&pwac->system_sinks, NULL, system_sink_destroy_cb);\n\n\tpwac->sink.id = SPA_ID_INVALID;\n\tdstr_init(&pwac->sink.position);\n\n\tpwac->capture_mode = obs_data_get_int(settings, SETTING_CAPTURE_MODE);\n\tpwac->match_priority = obs_data_get_int(settings, SETTING_MATCH_PRIORITY);\n\tpwac->except = obs_data_get_bool(settings, SETTING_EXCLUDE_SELECTIONS);\n\n\tda_init(pwac->selections);\n\tbuild_selections(pwac, settings);\n\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n\n\treturn pwac;\n}\n\nstatic void pipewire_audio_capture_app_defaults(obs_data_t *settings)\n{\n\tobs_data_set_default_int(settings, SETTING_CAPTURE_MODE, CAPTURE_MODE_SINGLE);\n\tobs_data_set_default_int(settings, SETTING_MATCH_PRIORITY, MATCH_PRIORITY_BINARY_NAME);\n\tobs_data_set_default_bool(settings, SETTING_EXCLUDE_SELECTIONS, false);\n\n\tobs_data_array_t *arr = obs_data_array_create();\n\tobs_data_set_default_array(settings, SETTING_SELECTION_MULTIPLE, arr);\n\tobs_data_array_release(arr);\n}\n\nstatic obs_properties_t *pipewire_audio_capture_app_properties(void *data)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tobs_properties_t *p = obs_properties_create();\n\n\tobs_property_t *capture_mode = obs_properties_add_list(\n\t\tp, SETTING_CAPTURE_MODE, obs_module_text(\"AppCaptureMode\"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);\n\tobs_property_list_add_int(capture_mode, obs_module_text(\"SingleApp\"), CAPTURE_MODE_SINGLE);\n\tobs_property_list_add_int(capture_mode, obs_module_text(\"MultipleApps\"), CAPTURE_MODE_MULTIPLE);\n\tobs_property_set_modified_callback2(capture_mode, capture_mode_modified, pwac);\n\n\tobs_property_t *match_priority = obs_properties_add_list(\n\t\tp, SETTING_MATCH_PRIORITY, obs_module_text(\"MatchPriority\"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);\n\tobs_property_list_add_int(match_priority, obs_module_text(\"MatchBinaryFirst\"), MATCH_PRIORITY_BINARY_NAME);\n\tobs_property_list_add_int(match_priority, obs_module_text(\"MatchAppNameFirst\"), MATCH_PRIORITY_APP_NAME);\n\tobs_property_set_modified_callback2(match_priority, match_priority_modified, pwac);\n\n\tobs_properties_add_bool(p, SETTING_EXCLUDE_SELECTIONS, obs_module_text(\"ExceptApp\"));\n\n\treturn p;\n}\n\nstatic void pipewire_audio_capture_app_update(void *data, obs_data_t *settings)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\n\tpwac->capture_mode = obs_data_get_int(settings, SETTING_CAPTURE_MODE);\n\tpwac->match_priority = obs_data_get_int(settings, SETTING_MATCH_PRIORITY);\n\tpwac->except = obs_data_get_bool(settings, SETTING_EXCLUDE_SELECTIONS);\n\n\tclear_selections(pwac);\n\tbuild_selections(pwac, settings);\n\n\tconnect_targets(pwac);\n\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n}\n\nstatic void pipewire_audio_capture_app_show(void *data)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\tpw_stream_set_active(pwac->pw.audio.stream, true);\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n}\n\nstatic void pipewire_audio_capture_app_hide(void *data)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\tpw_stream_set_active(pwac->pw.audio.stream, false);\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n}\n\nstatic void pipewire_audio_capture_app_destroy(void *data)\n{\n\tstruct obs_pw_audio_capture_app *pwac = data;\n\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\n\tobs_pw_audio_proxy_list_clear(&pwac->nodes);\n\tobs_pw_audio_proxy_list_clear(&pwac->system_sinks);\n\n\tobs_pw_audio_proxy_list_clear(&pwac->clients);\n\n\tdestroy_capture_sink(pwac);\n\n\tif (pwac->default_sink.proxy) {\n\t\tpw_proxy_destroy(pwac->default_sink.proxy);\n\t}\n\tif (pwac->default_sink.metadata.proxy) {\n\t\tpw_proxy_destroy(pwac->default_sink.metadata.proxy);\n\t}\n\n\tobs_pw_audio_instance_destroy(&pwac->pw);\n\n\tdstr_free(&pwac->sink.position);\n\n\tclear_selections(pwac);\n\tda_free(pwac->selections);\n\n\tbfree(pwac);\n}\n\nstatic const char *pipewire_audio_capture_app_name(void *data)\n{\n\tUNUSED_PARAMETER(data);\n\treturn obs_module_text(\"PipeWireAudioCaptureApplication\");\n}\n\nvoid pipewire_audio_capture_app_load(void)\n{\n\tconst struct obs_source_info pipewire_audio_capture_application = {\n\t\t.id = \"pipewire_audio_application_capture\",\n\t\t.type = OBS_SOURCE_TYPE_INPUT,\n\t\t.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE,\n\t\t.get_name = pipewire_audio_capture_app_name,\n\t\t.create = pipewire_audio_capture_app_create,\n\t\t.get_defaults = pipewire_audio_capture_app_defaults,\n\t\t.get_properties = pipewire_audio_capture_app_properties,\n\t\t.update = pipewire_audio_capture_app_update,\n\t\t.show = pipewire_audio_capture_app_show,\n\t\t.hide = pipewire_audio_capture_app_hide,\n\t\t.destroy = pipewire_audio_capture_app_destroy,\n\t\t.icon_type = OBS_ICON_TYPE_PROCESS_AUDIO_OUTPUT,\n\t};\n\n\tobs_register_source(&pipewire_audio_capture_application);\n}\n"
  },
  {
    "path": "src/pipewire-audio-capture-device.c",
    "content": "/* pipewire-audio-capture-device.c\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 2 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n *\n * SPDX-License-Identifier: GPL-2.0-or-later\n */\n\n#include \"pipewire-audio.h\"\n\n#include <util/dstr.h>\n\n/* Source for capturing device audio using PipeWire */\n\nstruct target_node {\n\tconst char *friendly_name;\n\tconst char *name;\n\tuint32_t serial;\n\tuint32_t id;\n\tuint32_t channels;\n\n\tstruct spa_hook node_listener;\n\n\tstruct obs_pw_audio_capture_device *pwac;\n};\n\nenum capture_type {\n\tCAPTURE_TYPE_INPUT,\n\tCAPTURE_TYPE_OUTPUT,\n};\n\n#define SETTING_TARGET_SERIAL \"TargetId\"\n#define SETTING_TARGET_NAME \"TargetName\"\n\nstruct obs_pw_audio_capture_device {\n\tobs_source_t *source;\n\n\tenum capture_type capture_type;\n\n\tstruct obs_pw_audio_instance pw;\n\n\tstruct {\n\t\tstruct obs_pw_audio_default_node_metadata metadata;\n\t\tbool autoconnect;\n\t\tuint32_t node_serial;\n\t\tstruct dstr name;\n\t} default_info;\n\n\tstruct obs_pw_audio_proxy_list targets;\n\n\tstruct dstr target_name;\n\tuint32_t connected_serial;\n};\n\nstatic void start_streaming(struct obs_pw_audio_capture_device *pwac, struct target_node *node)\n{\n\tdstr_copy(&pwac->target_name, node->name);\n\n\tif (pw_stream_get_state(pwac->pw.audio.stream, NULL) != PW_STREAM_STATE_UNCONNECTED) {\n\t\tif (node->serial == pwac->connected_serial) {\n\t\t\t/* Already connected to this node */\n\t\t\treturn;\n\t\t}\n\n\t\tpw_stream_disconnect(pwac->pw.audio.stream);\n\t\tpwac->connected_serial = SPA_ID_INVALID;\n\t}\n\n\tif (obs_pw_audio_stream_connect(&pwac->pw.audio, node->id, node->serial, node->channels) == 0) {\n\t\tpwac->connected_serial = node->serial;\n\t\tblog(LOG_INFO, \"[pipewire-audio] %p streaming from %u\", pwac->pw.audio.stream, node->serial);\n\t} else {\n\t\tpwac->connected_serial = SPA_ID_INVALID;\n\t\tblog(LOG_WARNING, \"[pipewire-audio] Error connecting stream %p\", pwac->pw.audio.stream);\n\t}\n\n\tpw_stream_set_active(pwac->pw.audio.stream, obs_source_active(pwac->source));\n}\n\nstruct target_node *get_node_by_name(struct obs_pw_audio_capture_device *pwac, const char *name)\n{\n\tstruct obs_pw_audio_proxy_list_iter iter;\n\tobs_pw_audio_proxy_list_iter_init(&iter, &pwac->targets);\n\n\tstruct target_node *node;\n\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {\n\t\tif (strcmp(node->name, name) == 0) {\n\t\t\treturn node;\n\t\t}\n\t}\n\n\treturn NULL;\n}\n\nstruct target_node *get_node_by_serial(struct obs_pw_audio_capture_device *pwac, uint32_t serial)\n{\n\tstruct obs_pw_audio_proxy_list_iter iter;\n\tobs_pw_audio_proxy_list_iter_init(&iter, &pwac->targets);\n\n\tstruct target_node *node;\n\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {\n\t\tif (node->serial == serial) {\n\t\t\treturn node;\n\t\t}\n\t}\n\n\treturn NULL;\n}\n\n/* Target node */\nstatic void on_node_param_cb(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,\n\t\t\t     const struct spa_pod *param)\n{\n\tUNUSED_PARAMETER(seq);\n\tUNUSED_PARAMETER(index);\n\tUNUSED_PARAMETER(next);\n\n\tif (id != SPA_PARAM_EnumFormat) {\n\t\treturn;\n\t}\n\n\tstruct target_node *n = data;\n\n\tstruct spa_pod_parser p;\n\tspa_pod_parser_pod(&p, param);\n\n\tuint32_t media_type = 0, media_subtype = 0, parsed_id = 0, channels = 0;\n\n\tspa_pod_parser_get_object(&p, SPA_TYPE_OBJECT_Format, &parsed_id, SPA_FORMAT_mediaType, SPA_POD_Id(&media_type),\n\t\t\t\t  SPA_FORMAT_mediaSubtype, SPA_POD_Id(&media_subtype), SPA_FORMAT_AUDIO_channels,\n\t\t\t\t  SPA_POD_OPT_Int(&channels));\n\n\tif (n->channels && !channels) {\n\t\t// It's likely we got the channels from a proper format already\n\t\treturn;\n\t}\n\n\tif (media_type != SPA_MEDIA_TYPE_audio) {\n\t\tblog(LOG_WARNING,\n\t\t     \"[pipewire-audio] Could not parse target node format. Channels may be mapped incorrectly.\");\n\t}\n\n\tn->channels = channels;\n\n\tstruct obs_pw_audio_capture_device *pwac = n->pwac;\n\n\tbool not_streamed = pwac->connected_serial != n->serial;\n\tbool has_default_node_name = !dstr_is_empty(&pwac->default_info.name) &&\n\t\t\t\t     dstr_cmp(&pwac->default_info.name, n->name) == 0;\n\tbool is_new_default_node = not_streamed && has_default_node_name;\n\n\tbool stream_is_unconnected = pw_stream_get_state(pwac->pw.audio.stream, NULL) == PW_STREAM_STATE_UNCONNECTED;\n\tbool node_has_target_name = !dstr_is_empty(&pwac->target_name) && dstr_cmp(&pwac->target_name, n->name) == 0;\n\n\tif ((pwac->default_info.autoconnect && is_new_default_node) ||\n\t    (stream_is_unconnected && node_has_target_name)) {\n\t\tstart_streaming(pwac, n);\n\t}\n}\n\nstatic const struct pw_node_events node_events = {\n\tPW_VERSION_NODE_EVENTS,\n\t.param = on_node_param_cb,\n};\n\nstatic void node_destroy_cb(void *data)\n{\n\tstruct target_node *n = data;\n\n\tstruct obs_pw_audio_capture_device *pwac = n->pwac;\n\tif (n->serial == pwac->connected_serial) {\n\t\tif (pw_stream_get_state(pwac->pw.audio.stream, NULL) != PW_STREAM_STATE_UNCONNECTED) {\n\t\t\tpw_stream_disconnect(pwac->pw.audio.stream);\n\t\t}\n\t\tpwac->connected_serial = SPA_ID_INVALID;\n\t}\n\n\tspa_hook_remove(&n->node_listener);\n\n\tbfree((void *)n->friendly_name);\n\tbfree((void *)n->name);\n}\n\nstatic void register_target_node(struct obs_pw_audio_capture_device *pwac, const char *friendly_name, const char *name,\n\t\t\t\t uint32_t object_serial, uint32_t global_id)\n{\n\tstruct pw_proxy *node_proxy = pw_registry_bind(pwac->pw.registry, global_id, PW_TYPE_INTERFACE_Node,\n\t\t\t\t\t\t       PW_VERSION_NODE, sizeof(struct target_node));\n\tif (!node_proxy) {\n\t\treturn;\n\t}\n\n\tstruct target_node *n = pw_proxy_get_user_data(node_proxy);\n\tn->friendly_name = bstrdup(friendly_name);\n\tn->name = bstrdup(name);\n\tn->id = global_id;\n\tn->serial = object_serial;\n\tn->channels = 0;\n\tn->pwac = pwac;\n\n\tobs_pw_audio_proxy_list_append(&pwac->targets, node_proxy);\n\n\tspa_zero(n->node_listener);\n\tpw_proxy_add_object_listener(node_proxy, &n->node_listener, &node_events, n);\n\n\tpw_node_subscribe_params((struct pw_node *)node_proxy, (uint32_t[]){SPA_PARAM_EnumFormat}, 1);\n}\n/* ------------------------------------------------- */\n\n/* Default device metadata */\nstatic void default_node_cb(void *data, const char *name)\n{\n\tstruct obs_pw_audio_capture_device *pwac = data;\n\n\tblog(LOG_DEBUG, \"[pipewire-audio] New default device %s\", name);\n\n\tdstr_copy(&pwac->default_info.name, name);\n\n\tstruct target_node *n = get_node_by_name(pwac, name);\n\tif (n) {\n\t\tpwac->default_info.node_serial = n->serial;\n\t\t// Connect now or wait for the param ballback to connect this\n\t\tif (pwac->default_info.autoconnect && n->channels) {\n\t\t\tstart_streaming(pwac, n);\n\t\t}\n\t}\n}\n/* ------------------------------------------------- */\n\n/* Registry */\nstatic void on_global_cb(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version,\n\t\t\t const struct spa_dict *props)\n{\n\tUNUSED_PARAMETER(permissions);\n\tUNUSED_PARAMETER(version);\n\n\tstruct obs_pw_audio_capture_device *pwac = data;\n\n\tif (!props || !type) {\n\t\treturn;\n\t}\n\n\tif (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {\n\t\tconst char *node_name, *media_class;\n\t\tif (!(node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) ||\n\t\t    !(media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS))) {\n\t\t\treturn;\n\t\t}\n\n\t\t/* Target device */\n\t\tif ((pwac->capture_type == CAPTURE_TYPE_INPUT &&\n\t\t     (strcmp(media_class, \"Audio/Source\") == 0 || strcmp(media_class, \"Audio/Source/Virtual\") == 0)) ||\n\t\t    (pwac->capture_type == CAPTURE_TYPE_OUTPUT &&\n\t\t     (strcmp(media_class, \"Audio/Sink\") == 0 || strcmp(media_class, \"Audio/Duplex\") == 0))) {\n\n\t\t\tconst char *ser = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL);\n\t\t\tif (!ser) {\n\t\t\t\tblog(LOG_WARNING, \"[pipewire-audio] No object serial found on node %u\", id);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tuint32_t object_serial = strtoul(ser, NULL, 10);\n\n\t\t\tconst char *node_friendly_name = spa_dict_lookup(props, PW_KEY_NODE_NICK);\n\t\t\tif (!node_friendly_name) {\n\t\t\t\tnode_friendly_name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);\n\t\t\t\tif (!node_friendly_name) {\n\t\t\t\t\tnode_friendly_name = node_name;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tregister_target_node(pwac, node_friendly_name, node_name, object_serial, id);\n\t\t}\n\t} else if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) {\n\t\tconst char *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME);\n\t\tif (!name || strcmp(name, \"default\") != 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!obs_pw_audio_default_node_metadata_listen(&pwac->default_info.metadata, &pwac->pw, id,\n\t\t\t\t\t\t\t       pwac->capture_type == CAPTURE_TYPE_OUTPUT,\n\t\t\t\t\t\t\t       default_node_cb, pwac)) {\n\t\t\tblog(LOG_WARNING,\n\t\t\t     \"[pipewire-audio] Failed to get default metadata, cannot detect default audio devices\");\n\t\t}\n\t}\n}\n\nstatic const struct pw_registry_events registry_events = {\n\tPW_VERSION_REGISTRY_EVENTS,\n\t.global = on_global_cb,\n};\n/* ------------------------------------------------- */\n\n/* Source */\nstatic void *pipewire_audio_capture_create(obs_data_t *settings, obs_source_t *source, enum capture_type capture_type)\n{\n\tstruct obs_pw_audio_capture_device *pwac = bzalloc(sizeof(struct obs_pw_audio_capture_device));\n\n\tif (!obs_pw_audio_instance_init(&pwac->pw, &registry_events, pwac, capture_type == CAPTURE_TYPE_OUTPUT, true,\n\t\t\t\t\tsource)) {\n\t\tobs_pw_audio_instance_destroy(&pwac->pw);\n\n\t\tbfree(pwac);\n\t\treturn NULL;\n\t}\n\n\tpwac->source = source;\n\tpwac->capture_type = capture_type;\n\tpwac->default_info.node_serial = SPA_ID_INVALID;\n\tpwac->connected_serial = SPA_ID_INVALID;\n\n\tobs_pw_audio_proxy_list_init(&pwac->targets, NULL, node_destroy_cb);\n\n\tif (obs_data_get_int(settings, SETTING_TARGET_SERIAL) != PW_ID_ANY) {\n\t\t/** Reset id setting, PipeWire node ids may not persist between sessions.\n\t\t  * Connecting to saved target will happen based on the TargetName setting\n\t\t  * once target has connected */\n\t\tobs_data_set_int(settings, SETTING_TARGET_SERIAL, 0);\n\t} else {\n\t\tpwac->default_info.autoconnect = true;\n\t}\n\n\tdstr_init_copy(&pwac->target_name, obs_data_get_string(settings, SETTING_TARGET_NAME));\n\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n\n\treturn pwac;\n}\n\nstatic void *pipewire_audio_capture_input_create(obs_data_t *settings, obs_source_t *source)\n{\n\treturn pipewire_audio_capture_create(settings, source, CAPTURE_TYPE_INPUT);\n}\n\nstatic void *pipewire_audio_capture_output_create(obs_data_t *settings, obs_source_t *source)\n{\n\treturn pipewire_audio_capture_create(settings, source, CAPTURE_TYPE_OUTPUT);\n}\n\nstatic void pipewire_audio_capture_defaults(obs_data_t *settings)\n{\n\tobs_data_set_default_int(settings, SETTING_TARGET_SERIAL, PW_ID_ANY);\n}\n\nstatic obs_properties_t *pipewire_audio_capture_properties(void *data)\n{\n\tstruct obs_pw_audio_capture_device *pwac = data;\n\n\tobs_properties_t *p = obs_properties_create();\n\n\tobs_property_t *targets_list = obs_properties_add_list(p, SETTING_TARGET_SERIAL, obs_module_text(\"Device\"),\n\t\t\t\t\t\t\t       OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);\n\n\tobs_property_list_add_int(targets_list, obs_module_text(\"Default\"), PW_ID_ANY);\n\n\tif (!pwac->default_info.autoconnect) {\n\t\tobs_data_t *settings = obs_source_get_settings(pwac->source);\n\t\t/* Saved target serial may be different from connected because a previously connected\n\t\t   node may have been replaced by one with the same name */\n\t\tobs_data_set_int(settings, SETTING_TARGET_SERIAL, pwac->connected_serial);\n\t\tobs_data_release(settings);\n\t}\n\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\n\tstruct obs_pw_audio_proxy_list_iter iter;\n\tobs_pw_audio_proxy_list_iter_init(&iter, &pwac->targets);\n\n\tstruct target_node *node;\n\twhile (obs_pw_audio_proxy_list_iter_next(&iter, (void **)&node)) {\n\t\tobs_property_list_add_int(targets_list, node->friendly_name, node->serial);\n\t}\n\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n\n\treturn p;\n}\n\nstatic void pipewire_audio_capture_update(void *data, obs_data_t *settings)\n{\n\tstruct obs_pw_audio_capture_device *pwac = data;\n\n\tuint32_t new_node_serial = obs_data_get_int(settings, SETTING_TARGET_SERIAL);\n\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\n\tif ((pwac->default_info.autoconnect = new_node_serial == PW_ID_ANY)) {\n\t\tif (pwac->default_info.node_serial != SPA_ID_INVALID) {\n\t\t\tstart_streaming(pwac, get_node_by_serial(pwac, pwac->default_info.node_serial));\n\t\t}\n\t} else {\n\t\tstruct target_node *new_node = get_node_by_serial(pwac, new_node_serial);\n\t\tif (new_node) {\n\t\t\tstart_streaming(pwac, new_node);\n\n\t\t\tobs_data_set_string(settings, SETTING_TARGET_NAME, pwac->target_name.array);\n\t\t}\n\t}\n\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n}\n\nstatic void pipewire_audio_capture_show(void *data)\n{\n\tstruct obs_pw_audio_capture_device *pwac = data;\n\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\tpw_stream_set_active(pwac->pw.audio.stream, true);\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n}\n\nstatic void pipewire_audio_capture_hide(void *data)\n{\n\tstruct obs_pw_audio_capture_device *pwac = data;\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\tpw_stream_set_active(pwac->pw.audio.stream, false);\n\tpw_thread_loop_unlock(pwac->pw.thread_loop);\n}\n\nstatic void pipewire_audio_capture_destroy(void *data)\n{\n\tstruct obs_pw_audio_capture_device *pwac = data;\n\n\tpw_thread_loop_lock(pwac->pw.thread_loop);\n\n\tobs_pw_audio_proxy_list_clear(&pwac->targets);\n\n\tif (pwac->default_info.metadata.proxy) {\n\t\tpw_proxy_destroy(pwac->default_info.metadata.proxy);\n\t}\n\n\tobs_pw_audio_instance_destroy(&pwac->pw);\n\n\tdstr_free(&pwac->default_info.name);\n\tdstr_free(&pwac->target_name);\n\n\tbfree(pwac);\n}\n\nstatic const char *pipewire_audio_capture_input_name(void *data)\n{\n\tUNUSED_PARAMETER(data);\n\treturn obs_module_text(\"PipeWireAudioCaptureInput\");\n}\n\nstatic const char *pipewire_audio_capture_output_name(void *data)\n{\n\tUNUSED_PARAMETER(data);\n\treturn obs_module_text(\"PipeWireAudioCaptureOutput\");\n}\n\nvoid pipewire_audio_capture_load(void)\n{\n\tconst struct obs_source_info pipewire_audio_capture_input = {\n\t\t.id = \"pipewire_audio_input_capture\",\n\t\t.type = OBS_SOURCE_TYPE_INPUT,\n\t\t.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE,\n\t\t.get_name = pipewire_audio_capture_input_name,\n\t\t.create = pipewire_audio_capture_input_create,\n\t\t.get_defaults = pipewire_audio_capture_defaults,\n\t\t.get_properties = pipewire_audio_capture_properties,\n\t\t.update = pipewire_audio_capture_update,\n\t\t.show = pipewire_audio_capture_show,\n\t\t.hide = pipewire_audio_capture_hide,\n\t\t.destroy = pipewire_audio_capture_destroy,\n\t\t.icon_type = OBS_ICON_TYPE_AUDIO_INPUT,\n\t};\n\tconst struct obs_source_info pipewire_audio_capture_output = {\n\t\t.id = \"pipewire_audio_output_capture\",\n\t\t.type = OBS_SOURCE_TYPE_INPUT,\n\t\t.output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_DO_NOT_SELF_MONITOR,\n\t\t.get_name = pipewire_audio_capture_output_name,\n\t\t.create = pipewire_audio_capture_output_create,\n\t\t.get_defaults = pipewire_audio_capture_defaults,\n\t\t.get_properties = pipewire_audio_capture_properties,\n\t\t.update = pipewire_audio_capture_update,\n\t\t.show = pipewire_audio_capture_show,\n\t\t.hide = pipewire_audio_capture_hide,\n\t\t.destroy = pipewire_audio_capture_destroy,\n\t\t.icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT,\n\t};\n\n\tobs_register_source(&pipewire_audio_capture_input);\n\tobs_register_source(&pipewire_audio_capture_output);\n}\n"
  },
  {
    "path": "src/pipewire-audio.c",
    "content": "/* pipewire-audio.c\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 2 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n *\n * SPDX-License-Identifier: GPL-2.0-or-later\n */\n\n#include \"pipewire-audio.h\"\n\n#include <util/platform.h>\n\n#include <spa/utils/json.h>\n\n/* Utilities */\nbool json_object_find(const char *obj, const char *key, char *value, size_t len)\n{\n\t/* From PipeWire's source */\n\n\tstruct spa_json it[2];\n\tconst char *v;\n\tchar k[128];\n\n\tspa_json_init(&it[0], obj, strlen(obj));\n\tif (spa_json_enter_object(&it[0], &it[1]) <= 0) {\n\t\treturn false;\n\t}\n\n\twhile (spa_json_get_string(&it[1], k, sizeof(k)) > 0) {\n\t\tif (spa_streq(k, key)) {\n\t\t\tif (spa_json_get_string(&it[1], value, len) > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (spa_json_next(&it[1], &v) <= 0) {\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn false;\n}\n/* ------------------------------------------------- */\n\n/* PipeWire stream wrapper */\nvoid obs_channels_to_spa_audio_position(enum spa_audio_channel *position, uint32_t channels)\n{\n\tswitch (channels) {\n\tcase 1:\n\t\tposition[0] = SPA_AUDIO_CHANNEL_MONO;\n\t\tbreak;\n\tcase 2:\n\t\tposition[0] = SPA_AUDIO_CHANNEL_FL;\n\t\tposition[1] = SPA_AUDIO_CHANNEL_FR;\n\t\tbreak;\n\tcase 3:\n\t\tposition[0] = SPA_AUDIO_CHANNEL_FL;\n\t\tposition[1] = SPA_AUDIO_CHANNEL_FR;\n\t\tposition[2] = SPA_AUDIO_CHANNEL_LFE;\n\t\tbreak;\n\tcase 4:\n\t\tposition[0] = SPA_AUDIO_CHANNEL_FL;\n\t\tposition[1] = SPA_AUDIO_CHANNEL_FR;\n\t\tposition[2] = SPA_AUDIO_CHANNEL_FC;\n\t\tposition[3] = SPA_AUDIO_CHANNEL_RC;\n\t\tbreak;\n\tcase 5:\n\t\tposition[0] = SPA_AUDIO_CHANNEL_FL;\n\t\tposition[1] = SPA_AUDIO_CHANNEL_FR;\n\t\tposition[2] = SPA_AUDIO_CHANNEL_FC;\n\t\tposition[3] = SPA_AUDIO_CHANNEL_LFE;\n\t\tposition[4] = SPA_AUDIO_CHANNEL_RC;\n\t\tbreak;\n\tcase 6:\n\t\tposition[0] = SPA_AUDIO_CHANNEL_FL;\n\t\tposition[1] = SPA_AUDIO_CHANNEL_FR;\n\t\tposition[2] = SPA_AUDIO_CHANNEL_FC;\n\t\tposition[3] = SPA_AUDIO_CHANNEL_LFE;\n\t\tposition[4] = SPA_AUDIO_CHANNEL_RL;\n\t\tposition[5] = SPA_AUDIO_CHANNEL_RR;\n\t\tbreak;\n\tcase 8:\n\t\tposition[0] = SPA_AUDIO_CHANNEL_FL;\n\t\tposition[1] = SPA_AUDIO_CHANNEL_FR;\n\t\tposition[2] = SPA_AUDIO_CHANNEL_FC;\n\t\tposition[3] = SPA_AUDIO_CHANNEL_LFE;\n\t\tposition[4] = SPA_AUDIO_CHANNEL_RL;\n\t\tposition[5] = SPA_AUDIO_CHANNEL_RR;\n\t\tposition[6] = SPA_AUDIO_CHANNEL_SL;\n\t\tposition[7] = SPA_AUDIO_CHANNEL_SR;\n\t\tbreak;\n\tdefault:\n\t\tfor (size_t i = 0; i < channels; i++) {\n\t\t\tposition[i] = SPA_AUDIO_CHANNEL_UNKNOWN;\n\t\t}\n\t\tbreak;\n\t}\n}\n\nenum audio_format spa_to_obs_audio_format(enum spa_audio_format format)\n{\n\tswitch (format) {\n\tcase SPA_AUDIO_FORMAT_U8:\n\t\treturn AUDIO_FORMAT_U8BIT;\n\tcase SPA_AUDIO_FORMAT_S16_LE:\n\t\treturn AUDIO_FORMAT_16BIT;\n\tcase SPA_AUDIO_FORMAT_S32_LE:\n\t\treturn AUDIO_FORMAT_32BIT;\n\tcase SPA_AUDIO_FORMAT_F32_LE:\n\t\treturn AUDIO_FORMAT_FLOAT;\n\tcase SPA_AUDIO_FORMAT_U8P:\n\t\treturn AUDIO_FORMAT_U8BIT_PLANAR;\n\tcase SPA_AUDIO_FORMAT_S16P:\n\t\treturn AUDIO_FORMAT_16BIT_PLANAR;\n\tcase SPA_AUDIO_FORMAT_S32P:\n\t\treturn AUDIO_FORMAT_32BIT_PLANAR;\n\tcase SPA_AUDIO_FORMAT_F32P:\n\t\treturn AUDIO_FORMAT_FLOAT_PLANAR;\n\tdefault:\n\t\treturn AUDIO_FORMAT_UNKNOWN;\n\t}\n}\n\nenum speaker_layout spa_to_obs_speakers(uint32_t channels)\n{\n\tswitch (channels) {\n\tcase 1:\n\t\treturn SPEAKERS_MONO;\n\tcase 2:\n\t\treturn SPEAKERS_STEREO;\n\tcase 3:\n\t\treturn SPEAKERS_2POINT1;\n\tcase 4:\n\t\treturn SPEAKERS_4POINT0;\n\tcase 5:\n\t\treturn SPEAKERS_4POINT1;\n\tcase 6:\n\t\treturn SPEAKERS_5POINT1;\n\tcase 8:\n\t\treturn SPEAKERS_7POINT1;\n\tdefault:\n\t\treturn SPEAKERS_UNKNOWN;\n\t}\n}\n\nbool spa_to_obs_pw_audio_info(struct obs_pw_audio_info *info, const struct spa_pod *param)\n{\n\tstruct spa_audio_info_raw audio_info;\n\n\tif (spa_format_audio_raw_parse(param, &audio_info) < 0) {\n\t\tinfo->sample_rate = 0;\n\t\tinfo->format = AUDIO_FORMAT_UNKNOWN;\n\t\tinfo->speakers = SPEAKERS_UNKNOWN;\n\n\t\treturn false;\n\t}\n\n\tinfo->sample_rate = audio_info.rate;\n\tinfo->speakers = spa_to_obs_speakers(audio_info.channels);\n\tinfo->format = spa_to_obs_audio_format(audio_info.format);\n\n\treturn true;\n}\n\nstatic void on_process_cb(void *data)\n{\n\tuint64_t now = os_gettime_ns();\n\n\tstruct obs_pw_audio_stream *s = data;\n\n\tstruct pw_buffer *b = pw_stream_dequeue_buffer(s->stream);\n\n\tif (!b) {\n\t\treturn;\n\t}\n\n\tstruct spa_buffer *buf = b->buffer;\n\n\tif (!s->info.sample_rate || buf->n_datas == 0 || buf->datas[0].chunk->stride == 0 ||\n\t    buf->datas[0].type != SPA_DATA_MemPtr) {\n\t\tgoto queue;\n\t}\n\n\tstruct obs_source_audio out = {\n\t\t.frames = buf->datas[0].chunk->size / buf->datas[0].chunk->stride,\n\t\t.speakers = s->info.speakers,\n\t\t.format = s->info.format,\n\t\t.samples_per_sec = s->info.sample_rate,\n\t};\n\n\tfor (size_t i = 0; i < buf->n_datas && i < MAX_AV_PLANES; i++) {\n\t\tout.data[i] = buf->datas[i].data;\n\t}\n\n\tif (s->info.sample_rate && s->pos->clock.rate_diff) {\n\t\t/** Taken from PipeWire's implementation of JACK's jack_get_cycle_times\n\t\t  * (https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/0.3.52/pipewire-jack/src/pipewire-jack.c#L5639)\n\t\t  * which is used in the linux-jack plugin to correctly set the timestamp\n\t\t  * (https://github.com/obsproject/obs-studio/blob/27.2.4/plugins/linux-jack/jack-wrapper.c#L87) */\n\t\tdouble period_nsecs = s->pos->clock.duration * (double)SPA_NSEC_PER_SEC /\n\t\t\t\t      (s->info.sample_rate * s->pos->clock.rate_diff);\n\n\t\tout.timestamp = now - (uint64_t)period_nsecs;\n\t} else {\n\t\tout.timestamp = now - audio_frames_to_ns(s->info.sample_rate, out.frames);\n\t}\n\n\tobs_source_output_audio(s->output, &out);\n\nqueue:\n\tpw_stream_queue_buffer(s->stream, b);\n}\n\nstatic void on_state_changed_cb(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error)\n{\n\tUNUSED_PARAMETER(old);\n\n\tstruct obs_pw_audio_stream *s = data;\n\n\tblog(LOG_DEBUG, \"[pipewire-audio] Stream %p state: \\\"%s\\\" (error: %s)\", s->stream,\n\t     pw_stream_state_as_string(state), error ? error : \"none\");\n}\n\nstatic void on_param_changed_cb(void *data, uint32_t id, const struct spa_pod *param)\n{\n\tif (!param || id != SPA_PARAM_Format) {\n\t\treturn;\n\t}\n\n\tstruct obs_pw_audio_stream *s = data;\n\n\tif (!spa_to_obs_pw_audio_info(&s->info, param)) {\n\t\tblog(LOG_WARNING, \"[pipewire-audio] Stream %p failed to parse audio format info\", s->stream);\n\t} else {\n\t\tblog(LOG_INFO, \"[pipewire-audio] %p Got format: rate %u - channels %u - format %u\", s->stream,\n\t\t     s->info.sample_rate, s->info.speakers, s->info.format);\n\t}\n}\n\nstatic void on_io_changed_cb(void *data, uint32_t id, void *area, uint32_t size)\n{\n\tUNUSED_PARAMETER(size);\n\n\tstruct obs_pw_audio_stream *s = data;\n\n\tif (id == SPA_IO_Position) {\n\t\ts->pos = area;\n\t}\n}\n\nstatic const struct pw_stream_events stream_events = {\n\tPW_VERSION_STREAM_EVENTS,\n\t.process = on_process_cb,\n\t.state_changed = on_state_changed_cb,\n\t.param_changed = on_param_changed_cb,\n\t.io_changed = on_io_changed_cb,\n};\n\nint obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s, uint32_t target_id, uint32_t target_serial,\n\t\t\t\tuint32_t audio_channels)\n{\n\tif (audio_channels == 0) {\n\t\tblog(LOG_WARNING,\n\t\t     \"[pipewire-audio] Stream %p connecting without channel info. Channels may be mapped incorrectly.\",\n\t\t     s);\n\t}\n\n\tif (audio_channels > 8) {\n\t\tblog(LOG_WARNING,\n\t\t     \"[pipewire-audio] Stream %p cannot use %u > 8 channels. This is likely a Pro Audio node, will use stereo instead.\",\n\t\t     s, audio_channels);\n\t\taudio_channels = 2;\n\t}\n\n\tenum spa_audio_channel pos[8];\n\n\tuint8_t buffer[2048];\n\tstruct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));\n\tconst struct spa_pod *params[1];\n\n\tif (audio_channels) {\n\t\tobs_channels_to_spa_audio_position(pos, audio_channels);\n\n\t\tparams[0] = spa_pod_builder_add_object(\n\t\t\t&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType,\n\t\t\tSPA_POD_Id(SPA_MEDIA_TYPE_audio), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),\n\t\t\tSPA_FORMAT_AUDIO_channels, SPA_POD_Int(audio_channels), SPA_FORMAT_AUDIO_position,\n\t\t\tSPA_POD_Array(sizeof(enum spa_audio_channel), SPA_TYPE_Id, audio_channels, pos),\n\t\t\tSPA_FORMAT_AUDIO_format,\n\t\t\tSPA_POD_CHOICE_ENUM_Id(9, SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_S16_LE,\n\t\t\t\t\t       SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_F32_LE, SPA_AUDIO_FORMAT_U8P,\n\t\t\t\t\t       SPA_AUDIO_FORMAT_S16P, SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_F32P));\n\t} else {\n\t\tparams[0] = spa_pod_builder_add_object(\n\t\t\t&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType,\n\t\t\tSPA_POD_Id(SPA_MEDIA_TYPE_audio), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),\n\t\t\tSPA_FORMAT_AUDIO_format,\n\t\t\tSPA_POD_CHOICE_ENUM_Id(9, SPA_AUDIO_FORMAT_F32P, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_S16_LE,\n\t\t\t\t\t       SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_F32_LE, SPA_AUDIO_FORMAT_U8P,\n\t\t\t\t\t       SPA_AUDIO_FORMAT_S16P, SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_F32P));\n\t}\n\n\tstruct pw_properties *stream_props = pw_properties_new(NULL, NULL);\n\tpw_properties_setf(stream_props, PW_KEY_TARGET_OBJECT, \"%u\", target_serial);\n\tpw_stream_update_properties(s->stream, &stream_props->dict);\n\tpw_properties_free(stream_props);\n\n\treturn pw_stream_connect(\n\t\ts->stream, PW_DIRECTION_INPUT, target_id,\n\t\tPW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_DONT_RECONNECT, params, 1);\n}\n/* ------------------------------------------------- */\n\n/* Common PipeWire components */\nstatic void on_core_done_cb(void *data, uint32_t id, int seq)\n{\n\tstruct obs_pw_audio_instance *pw = data;\n\n\tif (id == PW_ID_CORE && pw->seq == seq) {\n\t\tpw_thread_loop_signal(pw->thread_loop, false);\n\t}\n}\n\nstatic void on_core_error_cb(void *data, uint32_t id, int seq, int res, const char *message)\n{\n\tstruct obs_pw_audio_instance *pw = data;\n\n\tblog(LOG_ERROR, \"[pipewire-audio] Error id:%u seq:%d res:%d :%s\", id, seq, res, message);\n\n\tpw_thread_loop_signal(pw->thread_loop, false);\n}\n\nstatic const struct pw_core_events core_events = {\n\tPW_VERSION_CORE_EVENTS,\n\t.done = on_core_done_cb,\n\t.error = on_core_error_cb,\n};\n\nbool obs_pw_audio_instance_init(struct obs_pw_audio_instance *pw, const struct pw_registry_events *registry_events,\n\t\t\t\tvoid *registry_cb_data, bool stream_capture_sink, bool stream_want_driver,\n\t\t\t\tobs_source_t *stream_output)\n{\n\tpw->thread_loop = pw_thread_loop_new(\"PipeWire thread loop\", NULL);\n\tpw->context = pw_context_new(pw_thread_loop_get_loop(pw->thread_loop), NULL, 0);\n\n\tpw_thread_loop_lock(pw->thread_loop);\n\n\tif (pw_thread_loop_start(pw->thread_loop) < 0) {\n\t\tblog(LOG_WARNING, \"[pipewire-audio] Error starting threaded mainloop\");\n\t\treturn false;\n\t}\n\n\tpw->core = pw_context_connect(pw->context, NULL, 0);\n\tif (!pw->core) {\n\t\tblog(LOG_WARNING, \"[pipewire-audio] Error creating PipeWire core\");\n\t\treturn false;\n\t}\n\n\tpw_core_add_listener(pw->core, &pw->core_listener, &core_events, pw);\n\n\tpw->registry = pw_core_get_registry(pw->core, PW_VERSION_REGISTRY, 0);\n\tif (!pw->registry) {\n\t\treturn false;\n\t}\n\tpw_registry_add_listener(pw->registry, &pw->registry_listener, registry_events, registry_cb_data);\n\n\tstruct pw_properties *stream_props =\n\t\tpw_properties_new(PW_KEY_MEDIA_NAME, obs_source_get_name(stream_output), PW_KEY_MEDIA_TYPE, \"Audio\",\n\t\t\t\t  PW_KEY_MEDIA_CATEGORY, \"Capture\", PW_KEY_MEDIA_ROLE, \"Production\",\n\t\t\t\t  PW_KEY_NODE_WANT_DRIVER, stream_want_driver ? \"true\" : \"false\",\n\t\t\t\t  PW_KEY_STREAM_CAPTURE_SINK, stream_capture_sink ? \"true\" : \"false\", NULL);\n\n\tpw_properties_setf(stream_props, PW_KEY_NODE_NAME, \"OBS: %s\", obs_source_get_name(stream_output));\n\n\tpw->audio.output = stream_output;\n\tpw->audio.stream = pw_stream_new(pw->core, obs_source_get_name(stream_output), stream_props);\n\n\tif (!pw->audio.stream) {\n\t\tblog(LOG_WARNING, \"[pipewire-audio] Failed to create stream\");\n\t\treturn false;\n\t}\n\tblog(LOG_INFO, \"[pipewire-audio] Created stream %p\", pw->audio.stream);\n\n\tpw_stream_add_listener(pw->audio.stream, &pw->audio.stream_listener, &stream_events, &pw->audio);\n\n\treturn true;\n}\n\nvoid obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw)\n{\n\tif (pw->audio.stream) {\n\t\tspa_hook_remove(&pw->audio.stream_listener);\n\t\tif (pw_stream_get_state(pw->audio.stream, NULL) != PW_STREAM_STATE_UNCONNECTED) {\n\t\t\tpw_stream_disconnect(pw->audio.stream);\n\t\t}\n\t\tpw_stream_destroy(pw->audio.stream);\n\t}\n\n\tif (pw->registry) {\n\t\tspa_hook_remove(&pw->registry_listener);\n\t\tspa_zero(pw->registry_listener);\n\t\tpw_proxy_destroy((struct pw_proxy *)pw->registry);\n\t}\n\n\tpw_thread_loop_unlock(pw->thread_loop);\n\tpw_thread_loop_stop(pw->thread_loop);\n\n\tif (pw->core) {\n\t\tspa_hook_remove(&pw->core_listener);\n\t\tspa_zero(pw->core_listener);\n\t\tpw_core_disconnect(pw->core);\n\t}\n\n\tif (pw->context) {\n\t\tpw_context_destroy(pw->context);\n\t}\n\n\tpw_thread_loop_destroy(pw->thread_loop);\n}\n\nvoid obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw)\n{\n\tpw->seq = pw_core_sync(pw->core, PW_ID_CORE, pw->seq);\n}\n/* ------------------------------------------------- */\n\n/* PipeWire metadata */\nstatic int on_metadata_property_cb(void *data, uint32_t id, const char *key, const char *type, const char *value)\n{\n\tUNUSED_PARAMETER(type);\n\n\tstruct obs_pw_audio_default_node_metadata *metadata = data;\n\n\tif (id == PW_ID_CORE && key && value &&\n\t    strcmp(key, metadata->wants_sink ? \"default.audio.sink\" : \"default.audio.source\") == 0) {\n\t\tchar val[128];\n\t\tif (json_object_find(value, \"name\", val, sizeof(val)) && *val) {\n\t\t\tmetadata->default_node_callback(metadata->data, val);\n\t\t}\n\t}\n\n\treturn 0;\n}\n\nstatic const struct pw_metadata_events metadata_events = {\n\tPW_VERSION_METADATA_EVENTS,\n\t.property = on_metadata_property_cb,\n};\n\nstatic void on_metadata_proxy_removed_cb(void *data)\n{\n\tstruct obs_pw_audio_default_node_metadata *metadata = data;\n\tpw_proxy_destroy(metadata->proxy);\n}\n\nstatic void on_metadata_proxy_destroy_cb(void *data)\n{\n\tstruct obs_pw_audio_default_node_metadata *metadata = data;\n\n\tspa_hook_remove(&metadata->metadata_listener);\n\tspa_hook_remove(&metadata->proxy_listener);\n\tspa_zero(metadata->metadata_listener);\n\tspa_zero(metadata->proxy_listener);\n\n\tmetadata->proxy = NULL;\n}\n\nstatic const struct pw_proxy_events metadata_proxy_events = {\n\tPW_VERSION_PROXY_EVENTS,\n\t.removed = on_metadata_proxy_removed_cb,\n\t.destroy = on_metadata_proxy_destroy_cb,\n};\n\nbool obs_pw_audio_default_node_metadata_listen(struct obs_pw_audio_default_node_metadata *metadata,\n\t\t\t\t\t       struct obs_pw_audio_instance *pw, uint32_t global_id, bool wants_sink,\n\t\t\t\t\t       void (*default_node_callback)(void *data, const char *name), void *data)\n{\n\tif (metadata->proxy) {\n\t\tpw_proxy_destroy(metadata->proxy);\n\t}\n\n\tstruct pw_proxy *metadata_proxy =\n\t\tpw_registry_bind(pw->registry, global_id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0);\n\tif (!metadata_proxy) {\n\t\treturn false;\n\t}\n\n\tmetadata->proxy = metadata_proxy;\n\n\tmetadata->wants_sink = wants_sink;\n\n\tmetadata->default_node_callback = default_node_callback;\n\tmetadata->data = data;\n\n\tpw_proxy_add_object_listener(metadata->proxy, &metadata->metadata_listener, &metadata_events, metadata);\n\tpw_proxy_add_listener(metadata->proxy, &metadata->proxy_listener, &metadata_proxy_events, metadata);\n\n\treturn true;\n}\n/* ------------------------------------------------- */\n\n/* Proxied objects */\nstruct obs_pw_audio_proxied_object {\n\tvoid (*bound_callback)(void *data, uint32_t global_id);\n\tvoid (*destroy_callback)(void *data);\n\n\tstruct pw_proxy *proxy;\n\tstruct spa_hook proxy_listener;\n\n\tstruct spa_list link;\n};\n\nstatic void on_proxy_bound_cb(void *data, uint32_t global_id)\n{\n\tstruct obs_pw_audio_proxied_object *obj = data;\n\tif (obj->bound_callback) {\n\t\tobj->bound_callback(pw_proxy_get_user_data(obj->proxy), global_id);\n\t}\n}\n\nstatic void on_proxy_removed_cb(void *data)\n{\n\tstruct obs_pw_audio_proxied_object *obj = data;\n\tpw_proxy_destroy(obj->proxy);\n}\n\nstatic void on_proxy_destroy_cb(void *data)\n{\n\tstruct obs_pw_audio_proxied_object *obj = data;\n\tspa_hook_remove(&obj->proxy_listener);\n\n\tspa_list_remove(&obj->link);\n\n\tif (obj->destroy_callback) {\n\t\tobj->destroy_callback(pw_proxy_get_user_data(obj->proxy));\n\t}\n\n\tbfree(data);\n}\n\nstatic const struct pw_proxy_events proxy_events = {\n\tPW_VERSION_PROXY_EVENTS,\n\t.bound = on_proxy_bound_cb,\n\t.removed = on_proxy_removed_cb,\n\t.destroy = on_proxy_destroy_cb,\n};\n\nvoid obs_pw_audio_proxied_object_new(struct pw_proxy *proxy, struct spa_list *list,\n\t\t\t\t     void (*bound_callback)(void *data, uint32_t global_id),\n\t\t\t\t     void (*destroy_callback)(void *data))\n{\n\tstruct obs_pw_audio_proxied_object *obj = bmalloc(sizeof(struct obs_pw_audio_proxied_object));\n\n\tobj->proxy = proxy;\n\tobj->bound_callback = bound_callback;\n\tobj->destroy_callback = destroy_callback;\n\n\tspa_list_append(list, &obj->link);\n\n\tspa_zero(obj->proxy_listener);\n\tpw_proxy_add_listener(obj->proxy, &obj->proxy_listener, &proxy_events, obj);\n}\n\nvoid *obs_pw_audio_proxied_object_get_user_data(struct obs_pw_audio_proxied_object *obj)\n{\n\treturn pw_proxy_get_user_data(obj->proxy);\n}\n\nvoid obs_pw_audio_proxy_list_init(struct obs_pw_audio_proxy_list *list,\n\t\t\t\t  void (*bound_callback)(void *data, uint32_t global_id),\n\t\t\t\t  void (*destroy_callback)(void *data))\n{\n\tspa_list_init(&list->list);\n\n\tlist->bound_callback = bound_callback;\n\tlist->destroy_callback = destroy_callback;\n}\n\nvoid obs_pw_audio_proxy_list_append(struct obs_pw_audio_proxy_list *list, struct pw_proxy *proxy)\n{\n\tobs_pw_audio_proxied_object_new(proxy, &list->list, list->bound_callback, list->destroy_callback);\n}\n\nvoid obs_pw_audio_proxy_list_clear(struct obs_pw_audio_proxy_list *list)\n{\n\tstruct obs_pw_audio_proxied_object *obj, *temp;\n\tspa_list_for_each_safe(obj, temp, &list->list, link)\n\t{\n\t\tpw_proxy_destroy(obj->proxy);\n\t}\n}\n\nvoid obs_pw_audio_proxy_list_iter_init(struct obs_pw_audio_proxy_list_iter *iter, struct obs_pw_audio_proxy_list *list)\n{\n\titer->proxy_list = list;\n\titer->current = spa_list_first(&list->list, struct obs_pw_audio_proxied_object, link);\n}\n\nbool obs_pw_audio_proxy_list_iter_next(struct obs_pw_audio_proxy_list_iter *iter, void **proxy_user_data)\n{\n\tif (spa_list_is_empty(&iter->proxy_list->list)) {\n\t\treturn false;\n\t}\n\n\tif (spa_list_is_end(iter->current, &iter->proxy_list->list, link)) {\n\t\treturn false;\n\t}\n\n\t*proxy_user_data = obs_pw_audio_proxied_object_get_user_data(iter->current);\n\titer->current = spa_list_next(iter->current, link);\n\n\treturn true;\n}\n/* ------------------------------------------------- */\n"
  },
  {
    "path": "src/pipewire-audio.h",
    "content": "/* pipewire-audio.h\n *\n * Copyright 2022-2026 Dimitris Papaioannou <dimtpap@protonmail.com>\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 2 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n *\n * SPDX-License-Identifier: GPL-2.0-or-later\n */\n\n/* Stuff used by the PipeWire audio capture sources */\n\n#pragma once\n\n#include <obs-module.h>\n\n#include <pipewire/pipewire.h>\n#include <pipewire/extensions/metadata.h>\n#include <spa/param/audio/format-utils.h>\n\n/* PipeWire Stream wrapper */\n\n/**\n * Audio metadata\n */\nstruct obs_pw_audio_info {\n\tuint32_t sample_rate;\n\tenum audio_format format;\n\tenum speaker_layout speakers;\n};\n\n/**\n * PipeWire stream wrapper that outputs to an OBS source\n */\nstruct obs_pw_audio_stream {\n\tstruct pw_stream *stream;\n\tstruct spa_hook stream_listener;\n\tstruct obs_pw_audio_info info;\n\tstruct spa_io_position *pos;\n\n\tobs_source_t *output;\n};\n\n/**\n * Connect a stream with the default params\n * @return 0 on success, < 0 on error\n */\nint obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s, uint32_t target_id, uint32_t target_serial,\n\t\t\t\tuint32_t channels);\n/* ------------------------------------------------- */\n\n/**\n * Common PipeWire components\n */\nstruct obs_pw_audio_instance {\n\tstruct pw_thread_loop *thread_loop;\n\tstruct pw_context *context;\n\n\tstruct pw_core *core;\n\tstruct spa_hook core_listener;\n\tint seq;\n\n\tstruct pw_registry *registry;\n\tstruct spa_hook registry_listener;\n\n\tstruct obs_pw_audio_stream audio;\n};\n\n/**\n * Initialize a PipeWire instance\n * @warning The thread loop is left locked\n * @return true on success, false on error\n */\nbool obs_pw_audio_instance_init(struct obs_pw_audio_instance *pw, const struct pw_registry_events *registry_events,\n\t\t\t\tvoid *registry_cb_data, bool stream_capture_sink, bool stream_want_driver,\n\t\t\t\tobs_source_t *stream_output);\n\n/**\n * Destroy a PipeWire instance\n * @warning Call with the thread loop locked\n */\nvoid obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw);\n\n/**\n * Trigger a PipeWire core sync\n */\nvoid obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw);\n/* ------------------------------------------------- */\n\n/**\n * PipeWire metadata\n */\nstruct obs_pw_audio_default_node_metadata {\n\tstruct pw_proxy *proxy;\n\tstruct spa_hook proxy_listener;\n\tstruct spa_hook metadata_listener;\n\n\tbool wants_sink;\n\n\tvoid (*default_node_callback)(void *data, const char *name);\n\tvoid *data;\n};\n\n/**\n * Add listeners to the metadata\n * @return true on success, false on error\n */\nbool obs_pw_audio_default_node_metadata_listen(struct obs_pw_audio_default_node_metadata *metadata,\n\t\t\t\t\t       struct obs_pw_audio_instance *pw, uint32_t global_id, bool wants_sink,\n\t\t\t\t\t       void (*default_node_callback)(void *data, const char *name), void *data);\n/* ------------------------------------------------- */\n\n/* Helpers for storing remote PipeWire objects */\n\n/**\n * Wrapper over a PipeWire proxy that's a member of a spa_list.\n * Automatically handles adding and removing itself from the list.\n */\nstruct obs_pw_audio_proxied_object;\n\n/**\n * Get the user data of a proxied object\n */\nvoid *obs_pw_audio_proxied_object_get_user_data(struct obs_pw_audio_proxied_object *obj);\n\n/**\n * Convenience wrapper over spa_lists that holds proxied objects\n */\nstruct obs_pw_audio_proxy_list {\n\tstruct spa_list list;\n\tvoid (*bound_callback)(void *data, uint32_t global_id);\n\tvoid (*destroy_callback)(void *data);\n};\n\nvoid obs_pw_audio_proxy_list_init(struct obs_pw_audio_proxy_list *list,\n\t\t\t\t  void (*bound_callback)(void *data, uint32_t global_id),\n\t\t\t\t  void (*destroy_callback)(void *data));\n\nvoid obs_pw_audio_proxy_list_append(struct obs_pw_audio_proxy_list *list, struct pw_proxy *proxy);\n\n/**\n * Destroy all stored proxies.\n */\nvoid obs_pw_audio_proxy_list_clear(struct obs_pw_audio_proxy_list *list);\n\n/**\n * Iterator over all user data of the proxies in the list\n */\nstruct obs_pw_audio_proxy_list_iter {\n\tstruct obs_pw_audio_proxy_list *proxy_list;\n\tstruct obs_pw_audio_proxied_object *current;\n};\n\nvoid obs_pw_audio_proxy_list_iter_init(struct obs_pw_audio_proxy_list_iter *iter, struct obs_pw_audio_proxy_list *list);\n\n/**\n * @return true when there are more items to process, false otherwise\n */\nbool obs_pw_audio_proxy_list_iter_next(struct obs_pw_audio_proxy_list_iter *iter, void **proxy_user_data);\n/* ------------------------------------------------- */\n\n/* Sources */\nvoid pipewire_audio_capture_load(void);\nvoid pipewire_audio_capture_app_load(void);\n/* ------------------------------------------------- */\n"
  }
]